diff --git a/.gitignore b/.gitignore index aa77ced955..a0053ef13a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ build build.* build* .DS_Store -.vscode \ No newline at end of file +.vscode +dev.sh diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a654be4716..7ff7bc7a71 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -44,6 +44,7 @@ target_sources(vnote PRIVATE notebook/inotebookfactory.h notebook/node.cpp notebook/node.h notebook/nodeparameters.cpp notebook/nodeparameters.h + notebook/nodevisual.cpp notebook/nodevisual.h notebook/notebook.cpp notebook/notebook.h notebook/notebookdatabaseaccess.cpp notebook/notebookdatabaseaccess.h notebook/notebookparameters.cpp notebook/notebookparameters.h diff --git a/src/core/notebook/node.cpp b/src/core/notebook/node.cpp index 06de7f1ab4..a9eb8c273c 100644 --- a/src/core/notebook/node.cpp +++ b/src/core/notebook/node.cpp @@ -27,6 +27,7 @@ Node::Node(Flags p_flags, m_modifiedTimeUtc(p_paras.m_modifiedTimeUtc), m_tags(p_paras.m_tags), m_attachmentFolder(p_paras.m_attachmentFolder), + m_visual(p_paras.m_visual), m_parent(p_parent) { Q_ASSERT(m_notebook); @@ -66,6 +67,8 @@ void Node::loadCompleteInfo(const NodeParameters &p_paras, m_modifiedTimeUtc = p_paras.m_modifiedTimeUtc; Q_ASSERT(p_paras.m_tags.isEmpty()); Q_ASSERT(p_paras.m_attachmentFolder.isEmpty()); + + m_visual = p_paras.m_visual; m_children = p_children; m_loaded = true; @@ -493,3 +496,72 @@ void Node::checkSignature() m_signature = generateSignature(); } } + +// 视觉效果相关方法 +const NodeVisual &Node::getVisual() const +{ + return m_visual; +} + +void Node::setVisual(const NodeVisual &p_visual) +{ + m_visual = p_visual; +} + +// 视觉效果便捷访问方法 +const QString &Node::getBackgroundColor() const +{ + return m_visual.getBackgroundColor(); +} + +void Node::setBackgroundColor(const QString &p_backgroundColor) +{ + m_visual.setBackgroundColor(p_backgroundColor); +} + +const QString &Node::getBorderColor() const +{ + return m_visual.getBorderColor(); +} + +void Node::setBorderColor(const QString &p_borderColor) +{ + m_visual.setBorderColor(p_borderColor); +} + +const QString &Node::getNameColor() const +{ + return m_visual.getNameColor(); +} + +void Node::setNameColor(const QString &p_nameColor) +{ + m_visual.setNameColor(p_nameColor); +} + +QString Node::getEffectiveBackgroundColor() const +{ + return getBackgroundColor(); +} + +QString Node::getEffectiveBorderColor() const +{ + return getBorderColor(); +} + +void Node::updateNodeVisual(const NodeVisual &p_visual) +{ + if (m_visual.getBackgroundColor() == p_visual.getBackgroundColor() && + m_visual.getBorderColor() == p_visual.getBorderColor() && + m_visual.getNameColor() == p_visual.getNameColor()) { + return; + } + + m_visual = p_visual; + + // 持久化更新 + getConfigMgr()->updateNodeVisual(this, p_visual); + + // 界面更新 + emit m_notebook->nodeUpdated(this); +} diff --git a/src/core/notebook/node.h b/src/core/notebook/node.h index eb4908168b..4b562eddd7 100644 --- a/src/core/notebook/node.h +++ b/src/core/notebook/node.h @@ -8,6 +8,7 @@ #include #include +#include "nodevisual.h" namespace vnotex { @@ -140,6 +141,26 @@ namespace vnotex QString fetchAttachmentFolderPath(); + // 视觉效果相关方法 + const NodeVisual &getVisual() const; + void setVisual(const NodeVisual &p_visual); + + // 视觉效果便捷访问方法 + const QString &getBackgroundColor() const; + void setBackgroundColor(const QString &p_backgroundColor); + + const QString &getBorderColor() const; + void setBorderColor(const QString &p_borderColor); + + const QString &getNameColor() const; + void setNameColor(const QString &p_nameColor); + + // 获取有效颜色(直接返回设置的颜色) + QString getEffectiveBackgroundColor() const; + QString getEffectiveBorderColor() const; + + void updateNodeVisual(const NodeVisual &p_visual); + virtual QStringList addAttachment(const QString &p_destFolderPath, const QStringList &p_files) = 0; virtual QString newAttachmentFile(const QString &p_destFolderPath, const QString &p_name) = 0; @@ -204,6 +225,8 @@ namespace vnotex QString m_attachmentFolder; + NodeVisual m_visual; + Node *m_parent = nullptr; QVector> m_children; diff --git a/src/core/notebook/nodeparameters.h b/src/core/notebook/nodeparameters.h index b36d597097..c7010aed67 100644 --- a/src/core/notebook/nodeparameters.h +++ b/src/core/notebook/nodeparameters.h @@ -1,34 +1,37 @@ -#ifndef NODEPARAMETERS_H -#define NODEPARAMETERS_H - -#include -#include - -#include - -#include "node.h" - -namespace vnotex -{ - class NodeParameters - { - public: - NodeParameters() = default; - - NodeParameters(ID p_id); - - ID m_id = Node::InvalidId; - - ID m_signature = Node::InvalidId; - - QDateTime m_createdTimeUtc = QDateTime::currentDateTimeUtc(); - - QDateTime m_modifiedTimeUtc = QDateTime::currentDateTimeUtc(); - - QStringList m_tags; - - QString m_attachmentFolder; - }; -} - -#endif // NODEPARAMETERS_H +#ifndef NODEPARAMETERS_H +#define NODEPARAMETERS_H + +#include +#include + +#include + +#include "node.h" +#include "nodevisual.h" + +namespace vnotex +{ + class NodeParameters + { + public: + NodeParameters() = default; + + NodeParameters(ID p_id); + + ID m_id = Node::InvalidId; + + ID m_signature = Node::InvalidId; + + QDateTime m_createdTimeUtc = QDateTime::currentDateTimeUtc(); + + QDateTime m_modifiedTimeUtc = QDateTime::currentDateTimeUtc(); + + QStringList m_tags; + + QString m_attachmentFolder; + + NodeVisual m_visual; + }; +} + +#endif // NODEPARAMETERS_H diff --git a/src/core/notebook/nodevisual.cpp b/src/core/notebook/nodevisual.cpp new file mode 100644 index 0000000000..4841ce031c --- /dev/null +++ b/src/core/notebook/nodevisual.cpp @@ -0,0 +1,29 @@ +#include "nodevisual.h" + +#include +#include + +using namespace vnotex; + +NodeVisual::NodeVisual(const QString &p_backgroundColor, + const QString &p_borderColor, + const QString &p_nameColor) + : m_backgroundColor(p_backgroundColor) + , m_borderColor(p_borderColor) + , m_nameColor(p_nameColor) +{ +} + +bool NodeVisual::hasAnyVisualEffect() const +{ + return !m_backgroundColor.isEmpty() || + !m_borderColor.isEmpty() || + !m_nameColor.isEmpty(); +} + +void NodeVisual::clearAllColors() +{ + m_backgroundColor.clear(); + m_borderColor.clear(); + m_nameColor.clear(); +} diff --git a/src/core/notebook/nodevisual.h b/src/core/notebook/nodevisual.h new file mode 100644 index 0000000000..055d8dbc3e --- /dev/null +++ b/src/core/notebook/nodevisual.h @@ -0,0 +1,48 @@ +#ifndef NODEVISUAL_H +#define NODEVISUAL_H + +#include + +/* + * 节点视觉效果类 + * 自定义节点名称,背景颜色,边框颜色,节点名称颜色 + * 支持清除所有颜色,包括背景颜色,边框颜色,节点名称颜色 + * 支持级联修改,包括背景颜色,边框颜色,节点名称颜色 +*/ +namespace vnotex +{ + class NodeVisual + { + public: + NodeVisual() = default; + + NodeVisual(const QString &p_backgroundColor, + const QString &p_borderColor, + const QString &p_nameColor); + + // 背景颜色 + const QString &getBackgroundColor() const { return m_backgroundColor; } + void setBackgroundColor(const QString &p_color) { m_backgroundColor = p_color; } + + // 边框颜色 + const QString &getBorderColor() const { return m_borderColor; } + void setBorderColor(const QString &p_color) { m_borderColor = p_color; } + + // 节点名称颜色 + const QString &getNameColor() const { return m_nameColor; } + void setNameColor(const QString &p_color) { m_nameColor = p_color; } + + // 判断是否有任何视觉效果 + bool hasAnyVisualEffect() const; + + // 清除所有颜色 + void clearAllColors(); + + private: + QString m_backgroundColor; // 背景颜色 + QString m_borderColor; // 边框颜色 + QString m_nameColor; // 节点名称颜色 + }; +} + +#endif // NODEVISUAL_H diff --git a/src/core/notebookconfigmgr/inotebookconfigmgr.h b/src/core/notebookconfigmgr/inotebookconfigmgr.h index c230432862..9588bd40b3 100644 --- a/src/core/notebookconfigmgr/inotebookconfigmgr.h +++ b/src/core/notebookconfigmgr/inotebookconfigmgr.h @@ -5,6 +5,7 @@ #include #include "notebook/node.h" +#include "notebook/nodevisual.h" namespace vnotex { @@ -84,6 +85,8 @@ namespace vnotex virtual QStringList scanAndImportExternalFiles(Node *p_node) = 0; + virtual void updateNodeVisual(Node *p_node, const NodeVisual &p_visual) = 0; + // Version of the config processing code. virtual int getCodeVersion() const = 0; diff --git a/src/core/notebookconfigmgr/vxnodeconfig.cpp b/src/core/notebookconfigmgr/vxnodeconfig.cpp index c5f148dd26..1782d62b55 100644 --- a/src/core/notebookconfigmgr/vxnodeconfig.cpp +++ b/src/core/notebookconfigmgr/vxnodeconfig.cpp @@ -27,6 +27,12 @@ const QString NodeConfig::c_attachmentFolder = "attachment_folder"; const QString NodeConfig::c_tags = "tags"; +const QString NodeConfig::c_backgroundColor = "background_color"; + +const QString NodeConfig::c_borderColor = "border_color"; + +const QString NodeConfig::c_nameColor = "name_color"; + static ID stringToNodeId(const QString &p_idStr) { auto ret = stringToID(p_idStr); @@ -48,6 +54,17 @@ QJsonObject NodeFileConfig::toJson() const jobj[NodeConfig::c_attachmentFolder] = m_attachmentFolder; jobj[NodeConfig::c_tags] = QJsonArray::fromStringList(m_tags); + // Visual settings + if (!m_backgroundColor.isEmpty()) { + jobj[NodeConfig::c_backgroundColor] = m_backgroundColor; + } + if (!m_borderColor.isEmpty()) { + jobj[NodeConfig::c_borderColor] = m_borderColor; + } + if (!m_nameColor.isEmpty()) { + jobj[NodeConfig::c_nameColor] = m_nameColor; + } + return jobj; } @@ -69,6 +86,17 @@ void NodeFileConfig::fromJson(const QJsonObject &p_jobj) m_tags << arr[i].toString(); } } + + // Visual settings (check if fields exist for backward compatibility) + if (p_jobj.contains(NodeConfig::c_backgroundColor)) { + m_backgroundColor = p_jobj[NodeConfig::c_backgroundColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_borderColor)) { + m_borderColor = p_jobj[NodeConfig::c_borderColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_nameColor)) { + m_nameColor = p_jobj[NodeConfig::c_nameColor].toString(); + } } NodeParameters NodeFileConfig::toNodeParameters() const @@ -80,6 +108,12 @@ NodeParameters NodeFileConfig::toNodeParameters() const paras.m_modifiedTimeUtc = m_modifiedTimeUtc; paras.m_tags = m_tags; paras.m_attachmentFolder = m_attachmentFolder; + + // Visual settings + paras.m_visual.setBackgroundColor(m_backgroundColor); + paras.m_visual.setBorderColor(m_borderColor); + paras.m_visual.setNameColor(m_nameColor); + return paras; } @@ -89,12 +123,46 @@ QJsonObject NodeFolderConfig::toJson() const jobj[NodeConfig::c_name] = m_name; + // Visual settings + if (!m_backgroundColor.isEmpty()) { + jobj[NodeConfig::c_backgroundColor] = m_backgroundColor; + } + if (!m_borderColor.isEmpty()) { + jobj[NodeConfig::c_borderColor] = m_borderColor; + } + if (!m_nameColor.isEmpty()) { + jobj[NodeConfig::c_nameColor] = m_nameColor; + } + return jobj; } void NodeFolderConfig::fromJson(const QJsonObject &p_jobj) { m_name = p_jobj[NodeConfig::c_name].toString(); + + // Visual settings (check if fields exist for backward compatibility) + if (p_jobj.contains(NodeConfig::c_backgroundColor)) { + m_backgroundColor = p_jobj[NodeConfig::c_backgroundColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_borderColor)) { + m_borderColor = p_jobj[NodeConfig::c_borderColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_nameColor)) { + m_nameColor = p_jobj[NodeConfig::c_nameColor].toString(); + } +} + +NodeParameters NodeFolderConfig::toNodeParameters() const +{ + NodeParameters paras; + + // Visual settings + paras.m_visual.setBackgroundColor(m_backgroundColor); + paras.m_visual.setBorderColor(m_borderColor); + paras.m_visual.setNameColor(m_nameColor); + + return paras; } NodeConfig::NodeConfig() @@ -136,6 +204,17 @@ QJsonObject NodeConfig::toJson() const } jobj[NodeConfig::c_folders] = folders; + // Visual settings for the container node itself + if (!m_backgroundColor.isEmpty()) { + jobj[NodeConfig::c_backgroundColor] = m_backgroundColor; + } + if (!m_borderColor.isEmpty()) { + jobj[NodeConfig::c_borderColor] = m_borderColor; + } + if (!m_nameColor.isEmpty()) { + jobj[NodeConfig::c_nameColor] = m_nameColor; + } + return jobj; } @@ -160,6 +239,17 @@ void NodeConfig::fromJson(const QJsonObject &p_jobj) for (int i = 0; i < foldersJson.size(); ++i) { m_folders[i].fromJson(foldersJson[i].toObject()); } + + // Visual settings for the container node itself (check if fields exist for backward compatibility) + if (p_jobj.contains(NodeConfig::c_backgroundColor)) { + m_backgroundColor = p_jobj[NodeConfig::c_backgroundColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_borderColor)) { + m_borderColor = p_jobj[NodeConfig::c_borderColor].toString(); + } + if (p_jobj.contains(NodeConfig::c_nameColor)) { + m_nameColor = p_jobj[NodeConfig::c_nameColor].toString(); + } } NodeParameters NodeConfig::toNodeParameters() const @@ -169,5 +259,11 @@ NodeParameters NodeConfig::toNodeParameters() const paras.m_signature = m_signature; paras.m_createdTimeUtc = m_createdTimeUtc; paras.m_modifiedTimeUtc = m_modifiedTimeUtc; + + // Visual settings for the container node itself + paras.m_visual.setBackgroundColor(m_backgroundColor); + paras.m_visual.setBorderColor(m_borderColor); + paras.m_visual.setNameColor(m_nameColor); + return paras; } diff --git a/src/core/notebookconfigmgr/vxnodeconfig.h b/src/core/notebookconfigmgr/vxnodeconfig.h index 24066b4069..8df5504b6d 100644 --- a/src/core/notebookconfigmgr/vxnodeconfig.h +++ b/src/core/notebookconfigmgr/vxnodeconfig.h @@ -36,6 +36,11 @@ namespace vnotex QString m_attachmentFolder; QStringList m_tags; + + // Visual settings + QString m_backgroundColor; + QString m_borderColor; + QString m_nameColor; }; @@ -46,7 +51,14 @@ namespace vnotex void fromJson(const QJsonObject &p_jobj); + NodeParameters toNodeParameters() const; + QString m_name; + + // Visual settings + QString m_backgroundColor; + QString m_borderColor; + QString m_nameColor; }; @@ -81,6 +93,11 @@ namespace vnotex QVector m_folders; + // Visual settings for the container node itself + QString m_backgroundColor; + QString m_borderColor; + QString m_nameColor; + static const QString c_version; static const QString c_id; @@ -100,6 +117,12 @@ namespace vnotex static const QString c_attachmentFolder; static const QString c_tags; + + static const QString c_backgroundColor; + + static const QString c_borderColor; + + static const QString c_nameColor; }; } } diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp index f0e8841655..3b053ca59d 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -197,6 +198,10 @@ void VXNotebookConfigMgr::loadFolderNode(Node *p_node, const NodeConfig &p_confi p_node); inheritNodeFlags(p_node, folderNode.data()); folderNode->setExists(getBackend()->existsDir(PathUtils::concatenateFilePath(basePath, folder.m_name))); + + // 设置视觉效果信息 + NodeParameters visualParams = folder.toNodeParameters(); + folderNode->setVisual(visualParams.m_visual); children.push_back(folderNode); } @@ -376,6 +381,11 @@ QSharedPointer VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_n p_node->getCreatedTimeUtc(), p_node->getModifiedTimeUtc()); + // Set visual settings for the container node itself + config->m_backgroundColor = p_node->getBackgroundColor(); + config->m_borderColor = p_node->getBorderColor(); + config->m_nameColor = p_node->getNameColor(); + for (const auto &child : p_node->getChildrenRef()) { if (child->hasContent()) { NodeFileConfig fileConfig; @@ -386,12 +396,22 @@ QSharedPointer VXNotebookConfigMgr::nodeToNodeConfig(const Node *p_n fileConfig.m_modifiedTimeUtc = child->getModifiedTimeUtc(); fileConfig.m_attachmentFolder = child->getAttachmentFolder(); fileConfig.m_tags = child->getTags(); + + // Visual settings + fileConfig.m_backgroundColor = child->getBackgroundColor(); + fileConfig.m_borderColor = child->getBorderColor(); + fileConfig.m_nameColor = child->getNameColor(); config->m_files.push_back(fileConfig); } else { Q_ASSERT(child->isContainer()); NodeFolderConfig folderConfig; folderConfig.m_name = child->getName(); + + // Visual settings + folderConfig.m_backgroundColor = child->getBackgroundColor(); + folderConfig.m_borderColor = child->getBorderColor(); + folderConfig.m_nameColor = child->getNameColor(); config->m_folders.push_back(folderConfig); } @@ -1112,3 +1132,16 @@ bool VXNotebookConfigMgr::sameNotebook(const Node *p_node) const { return p_node ? p_node->getNotebook() == getNotebook() : true; } + +void VXNotebookConfigMgr::updateNodeVisual(Node *p_node, const NodeVisual &p_visual) +{ + Q_ASSERT(sameNotebook(p_node)); + + p_node->setVisual(p_visual); + + if (p_node->isRoot()) { + return; + } + + saveNode(p_node); +} diff --git a/src/core/notebookconfigmgr/vxnotebookconfigmgr.h b/src/core/notebookconfigmgr/vxnotebookconfigmgr.h index 787ecaf0dc..ee4578db1a 100644 --- a/src/core/notebookconfigmgr/vxnotebookconfigmgr.h +++ b/src/core/notebookconfigmgr/vxnotebookconfigmgr.h @@ -83,6 +83,8 @@ namespace vnotex QStringList scanAndImportExternalFiles(Node *p_node) Q_DECL_OVERRIDE; + void updateNodeVisual(Node *p_node, const NodeVisual &p_visual) Q_DECL_OVERRIDE; + private: void createEmptyRootNode(); diff --git a/src/widgets/notebooknodeexplorer.cpp b/src/widgets/notebooknodeexplorer.cpp index 64da0b26d3..0855dcc8b6 100644 --- a/src/widgets/notebooknodeexplorer.cpp +++ b/src/widgets/notebooknodeexplorer.cpp @@ -6,11 +6,17 @@ #include #include #include +#include +#include +#include +#include +#include #include #include #include #include +#include #include #include "messageboxhelper.h" #include "vnotex.h" @@ -44,6 +50,39 @@ using namespace vnotex; QIcon NotebookNodeExplorer::s_nodeIcons[NodeIcon::MaxIcons]; +// 节点视觉委托类 避免修改颜色后整个树刷新 +NodeColorDelegate::NodeColorDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +void NodeColorDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + // 先绘制默认内容 + QStyledItemDelegate::paint(painter, option, index); + + // 获取节点数据 + auto item = static_cast(index.internalPointer()); + if (!item) { + return; + } + + // 从 item 的 UserRole 中获取颜色信息 + QVariant borderColorVar = item->data(0, Qt::UserRole + 1000); // 使用特殊的角色存储边框颜色 + if (borderColorVar.isValid() && !borderColorVar.toString().isEmpty()) { + QString borderColor = borderColorVar.toString(); + QColor color(borderColor); + + if (color.isValid()) { + painter->save(); + QPen pen(color, 2, Qt::SolidLine); + painter->setPen(pen); + painter->drawRect(option.rect.adjusted(1, 1, -1, -1)); + painter->restore(); + } + } +} + NotebookNodeExplorer::NodeData::NodeData() { } @@ -284,6 +323,9 @@ void NotebookNodeExplorer::setupMasterExplorer(QWidget *p_parent) auto data = getItemNodeData(p_item); activateItemNode(data); }); + + // 设置自定义委托以支持边框绘制 + m_masterExplorer->setItemDelegate(new NodeColorDelegate(this)); } void NotebookNodeExplorer::activateItemNode(const NodeData &p_data) @@ -542,6 +584,7 @@ void NotebookNodeExplorer::fillMasterItem(QTreeWidgetItem *p_item, Node *p_node, p_item->setText(Column::Name, p_node->getName()); p_item->setIcon(Column::Name, getIcon(p_node)); p_item->setToolTip(Column::Name, generateToolTip(p_node)); + applyNodeColors(p_item, p_node); } void NotebookNodeExplorer::fillMasterItem(QTreeWidgetItem *p_item, const QSharedPointer &p_node) const @@ -558,6 +601,7 @@ void NotebookNodeExplorer::fillSlaveItem(QListWidgetItem *p_item, Node *p_node) p_item->setText(p_node->getName()); p_item->setIcon(getIcon(p_node)); p_item->setToolTip(generateToolTip(p_node)); + applyNodeColors(p_item, p_node); } void NotebookNodeExplorer::fillSlaveItem(QListWidgetItem *p_item, const QSharedPointer &p_node) const @@ -568,6 +612,175 @@ void NotebookNodeExplorer::fillSlaveItem(QListWidgetItem *p_item, const QSharedP p_item->setToolTip(tr("[External] %1").arg(p_node->getName())); } +void NotebookNodeExplorer::applyNodeColors(QTreeWidgetItem *p_item, Node *p_node) const +{ + if (!p_item || !p_node) { + return; + } + + QString backgroundColor = p_node->getEffectiveBackgroundColor(); + QString borderColor = p_node->getEffectiveBorderColor(); + QString nameColor = p_node->getNameColor(); + + // 设置背景色 + if (!backgroundColor.isEmpty()) { + p_item->setBackground(Column::Name, QBrush(QColor(backgroundColor))); + } else { + p_item->setBackground(Column::Name, QBrush()); + } + + // 设置节点名称颜色 + if (!nameColor.isEmpty()) { + p_item->setForeground(Column::Name, QBrush(QColor(nameColor))); + } else { + p_item->setForeground(Column::Name, QBrush()); + } + + // 设置边框色(通过 UserRole 存储,由自定义委托绘制) + if (!borderColor.isEmpty()) { + p_item->setData(Column::Name, Qt::UserRole + 1000, borderColor); + } else { + p_item->setData(Column::Name, Qt::UserRole + 1000, QVariant()); + } +} + +void NotebookNodeExplorer::applyNodeColors(QListWidgetItem *p_item, Node *p_node) const +{ + if (!p_item || !p_node) { + return; + } + + QString backgroundColor = p_node->getEffectiveBackgroundColor(); + QString nameColor = p_node->getNameColor(); + + // 设置背景色 + if (!backgroundColor.isEmpty()) { + p_item->setBackground(QBrush(QColor(backgroundColor))); + } else { + p_item->setBackground(QBrush()); + } + + // 设置节点名称颜色 + if (!nameColor.isEmpty()) { + p_item->setForeground(QBrush(QColor(nameColor))); + } else { + p_item->setForeground(QBrush()); + } + + // 对于 ListWidget,边框效果暂时不实现 + // TODO: 如果需要,也可以为 ListWidget 创建自定义委托 +} + +void NotebookNodeExplorer::updateCurrentNodeVisualDirectly(Node *p_node, const NodeVisual &p_visual) +{ + if (!p_node) { + return; + } + + // 直接更新 Node 对象的视觉信息,但不发送刷新信号 + p_node->setVisual(p_visual); + + // 更新数据库 + p_node->getConfigMgr()->updateNodeVisual(p_node, p_visual); + + // 直接更新当前显示的 item,避免整个树刷新 + auto treeItem = findCurrentTreeWidgetItem(p_node); + if (treeItem) { + applyNodeColors(treeItem, p_node); + } + + auto listItem = findCurrentListWidgetItem(p_node); + if (listItem) { + applyNodeColors(listItem, p_node); + } +} + +void NotebookNodeExplorer::setCascadeColorRecursively(Node *p_node, const QString &p_backgroundColor, const QString &p_borderColor, const QString &p_nameColor) +{ + if (!p_node) { + return; + } + + // 设置当前节点的颜色 + NodeVisual visual = p_node->getVisual(); + + visual.setBackgroundColor(p_backgroundColor); + visual.setBorderColor(p_borderColor); + visual.setNameColor(p_nameColor); + + updateCurrentNodeVisualDirectly(p_node, visual); + + // 如果是容器节点,递归设置所有子节点 + if (p_node->isContainer() && p_node->isLoaded()) { + for (const auto &child : p_node->getChildrenRef()) { + setCascadeColorRecursively(child.data(), p_backgroundColor, p_borderColor, p_nameColor); + } + } +} + +QTreeWidgetItem *NotebookNodeExplorer::findCurrentTreeWidgetItem(Node *p_node) const +{ + if (!p_node || !m_masterExplorer) { + return nullptr; + } + + // 遍历所有可见的 TreeWidgetItem 查找匹配的节点 + std::function findInTree = [&](QTreeWidgetItem *parent) -> QTreeWidgetItem* { + if (!parent) { + // 搜索顶级项目 + for (int i = 0; i < m_masterExplorer->topLevelItemCount(); ++i) { + auto item = m_masterExplorer->topLevelItem(i); + auto data = getItemNodeData(item); + if (data.isNode() && data.getNode() == p_node) { + return item; + } + + // 递归搜索子项目 + auto result = findInTree(item); + if (result) { + return result; + } + } + } else { + // 搜索子项目 + for (int i = 0; i < parent->childCount(); ++i) { + auto item = parent->child(i); + auto data = getItemNodeData(item); + if (data.isNode() && data.getNode() == p_node) { + return item; + } + + // 递归搜索子项目 + auto result = findInTree(item); + if (result) { + return result; + } + } + } + return nullptr; + }; + + return findInTree(nullptr); +} + +QListWidgetItem *NotebookNodeExplorer::findCurrentListWidgetItem(Node *p_node) const +{ + if (!p_node || !m_slaveExplorer) { + return nullptr; + } + + // 遍历所有 ListWidgetItem 查找匹配的节点 + for (int i = 0; i < m_slaveExplorer->count(); ++i) { + auto item = m_slaveExplorer->item(i); + auto data = getItemNodeData(item); + if (data.isNode() && data.getNode() == p_node) { + return item; + } + } + + return nullptr; +} + const QIcon &NotebookNodeExplorer::getIcon(const Node *p_node) const { if (p_node->hasContent()) { @@ -1011,6 +1224,26 @@ void NotebookNodeExplorer::createContextMenuOnNode(QMenu *p_menu, const Node *p_ createAndAddAction(Action::OpenLocation, p_menu, p_master); + // 添加视觉设置子菜单 + auto visualMenu = WidgetsFactory::createMenu(tr("Visual Settings"), p_menu); + createAndAddAction(Action::SetBackgroundColor, visualMenu, p_master); + createAndAddAction(Action::SetBorderColor, visualMenu, p_master); + createAndAddAction(Action::SetNameColor, visualMenu, p_master); + + // 只有文件夹节点才能设置级联色 + if (p_node->isContainer()) { + visualMenu->addSeparator(); + auto cascadeMenu = WidgetsFactory::createMenu(tr("Cascade Color Settings"), visualMenu); + createAndAddAction(Action::SetCascadeBackgroundColor, cascadeMenu, p_master); + createAndAddAction(Action::SetCascadeBorderColor, cascadeMenu, p_master); + createAndAddAction(Action::ClearCascadeColors, cascadeMenu, p_master); + visualMenu->addMenu(cascadeMenu); + } + + visualMenu->addSeparator(); + createAndAddAction(Action::ClearColors, visualMenu, p_master); + p_menu->addMenu(visualMenu); + createAndAddAction(Action::Properties, p_menu, p_master); } } @@ -1375,6 +1608,162 @@ QAction *NotebookNodeExplorer::createAction(Action p_act, QObject *p_parent, boo }); break; + case Action::SetBackgroundColor: + act = new QAction(tr("Set Background Color"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + QColor defaultColor = node->getBackgroundColor().isEmpty() ? QColor(Qt::white) : QColor(node->getBackgroundColor()); + auto color = QColorDialog::getColor(defaultColor, this, tr("Select Background Color")); + if (color.isValid()) { + NodeVisual visual = node->getVisual(); + visual.setBackgroundColor(color.name()); + updateCurrentNodeVisualDirectly(node, visual); + } + }); + break; + + case Action::SetBorderColor: + act = new QAction(tr("Set Border Color"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + QColor defaultColor = node->getBorderColor().isEmpty() ? QColor(Qt::black) : QColor(node->getBorderColor()); + auto color = QColorDialog::getColor(defaultColor, this, tr("Select Border Color")); + if (color.isValid()) { + NodeVisual visual = node->getVisual(); + visual.setBorderColor(color.name()); + updateCurrentNodeVisualDirectly(node, visual); + } + }); + break; + + case Action::SetNameColor: + act = new QAction(tr("Set Name Color"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + QColor defaultColor = node->getNameColor().isEmpty() ? QColor(Qt::black) : QColor(node->getNameColor()); + auto color = QColorDialog::getColor(defaultColor, this, tr("Select Name Color")); + if (color.isValid()) { + NodeVisual visual = node->getVisual(); + visual.setNameColor(color.name()); + updateCurrentNodeVisualDirectly(node, visual); + } + }); + break; + + case Action::SetCascadeBackgroundColor: + act = new QAction(tr("Set Cascade Background Color"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + if (!node->isContainer()) { + return; // 只有文件夹才能设置级联色 + } + + QColor defaultColor = node->getBackgroundColor().isEmpty() ? QColor("#e6f3ff") : QColor(node->getBackgroundColor()); + auto color = QColorDialog::getColor(defaultColor, this, tr("Select Cascade Background Color")); + if (color.isValid()) { + setCascadeColorRecursively(node, color.name(), QString(), QString()); + } + }); + break; + + case Action::SetCascadeBorderColor: + act = new QAction(tr("Set Cascade Border Color"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + if (!node->isContainer()) { + return; // 只有文件夹才能设置级联色 + } + + QColor defaultColor = node->getBorderColor().isEmpty() ? QColor("#3A75F2") : QColor(node->getBorderColor()); + auto color = QColorDialog::getColor(defaultColor, this, tr("Select Cascade Border Color")); + if (color.isValid()) { + setCascadeColorRecursively(node, QString(), color.name(), QString()); + } + }); + break; + + case Action::ClearCascadeColors: + act = new QAction(tr("Clear Cascade Colors"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + // 递归清除子节点 + setCascadeColorRecursively(node, QString(), QString(), QString()); + }); + break; + + case Action::ClearColors: + act = new QAction(tr("Clear Colors"), p_parent); + connect(act, &QAction::triggered, + this, [this, p_master]() { + auto node = p_master ? getCurrentMasterNode() : getCurrentSlaveNode(); + if (!node) { + return; + } + + if (checkInvalidNode(node)) { + return; + } + + NodeVisual visual; + visual.clearAllColors(); + updateCurrentNodeVisualDirectly(node, visual); + }); + break; + default: Q_ASSERT(false); break; diff --git a/src/widgets/notebooknodeexplorer.h b/src/widgets/notebooknodeexplorer.h index 88fdcdbe8a..dceb0f2d08 100644 --- a/src/widgets/notebooknodeexplorer.h +++ b/src/widgets/notebooknodeexplorer.h @@ -6,14 +6,19 @@ #include #include #include +#include #include "qtreewidgetstatecache.h" #include "clipboarddata.h" #include "navigationmodewrapper.h" #include "global.h" +#include class QSplitter; class QMenu; +class QPainter; +class QStyleOptionViewItem; +class QModelIndex; namespace vnotex { @@ -25,6 +30,21 @@ namespace vnotex class Event; class ExternalNode; + /* + * 自定义委托类,用于绘制节点边框 + * 支持节点名称,背景颜色,边框颜色,节点名称颜色 + * 支持级联修改,包括背景颜色,边框颜色,节点名称颜色 + */ + class NodeColorDelegate : public QStyledItemDelegate + { + Q_OBJECT + public: + explicit NodeColorDelegate(QObject *parent = nullptr); + + protected: + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + }; + class NotebookNodeExplorer : public QWidget { Q_OBJECT @@ -153,7 +173,15 @@ namespace vnotex Read, ExpandAll, PinToQuickAccess, - Tag + Tag, + VisualSettings, + SetBackgroundColor, + SetBorderColor, + SetNameColor, + SetCascadeBackgroundColor, + SetCascadeBorderColor, + ClearCascadeColors, + ClearColors }; struct CacheData @@ -195,6 +223,18 @@ namespace vnotex void fillSlaveItem(QListWidgetItem *p_item, const QSharedPointer &p_node) const; + void applyNodeColors(QTreeWidgetItem *p_item, Node *p_node) const; + + void applyNodeColors(QListWidgetItem *p_item, Node *p_node) const; + + void updateCurrentNodeVisualDirectly(Node *p_node, const NodeVisual &p_visual); + + void setCascadeColorRecursively(Node *p_node, const QString &p_backgroundColor, const QString &p_borderColor, const QString &p_nameColor); + + QTreeWidgetItem *findCurrentTreeWidgetItem(Node *p_node) const; + + QListWidgetItem *findCurrentListWidgetItem(Node *p_node) const; + const QIcon &getIcon(const Node *p_node) const; const QIcon &getIcon(const ExternalNode *p_node) const;