From 17addac9e66451cac5b01869aac548548c7d9c1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 08:55:07 +0000 Subject: [PATCH 1/7] Initial plan From e33f6b385e05dca6d19c433b38a20a26b71adc09 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:00:45 +0000 Subject: [PATCH 2/7] Add NodeTypeBrowser panel to GraphEditor with toggle button Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/app.py | 9 +- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 33 +++ .../ui/qml/GraphEditor/NodeTypeBrowser.qml | 270 ++++++++++++++++++ meshroom/ui/qml/GraphEditor/qmldir | 1 + 4 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml diff --git a/meshroom/ui/app.py b/meshroom/ui/app.py index 665f3f2dac..89241343c1 100644 --- a/meshroom/ui/app.py +++ b/meshroom/ui/app.py @@ -279,7 +279,14 @@ def __init__(self, inputArgs): self.engine.addImportPath(qmlDir) # expose available node types that can be instantiated - self.engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": pluginManager.getRegisteredNodePlugins()[n].nodeDescriptor.category} for n in sorted(pluginManager.getRegisteredNodePlugins().keys())}) + def _nodeTypeInfo(nodePlugin): + desc = nodePlugin.nodeDescriptor + documentation = getattr(desc, 'documentation', '') or getattr(desc, '__doc__', '') or '' + return { + "category": desc.category, + "documentation": documentation.strip() if documentation else '', + } + self.engine.rootContext().setContextProperty("_nodeTypes", {n: _nodeTypeInfo(pluginManager.getRegisteredNodePlugins()[n]) for n in sorted(pluginManager.getRegisteredNodePlugins().keys())}) # instantiate the 3D Scene object self._undoStack = commands.UndoStack(self) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index ecfe91ecb6..60e0508a13 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1540,4 +1540,37 @@ Item { draggable.x = bbox.x * draggable.scale * -1 + (root.width - bbox.width * draggable.scale) * 0.5 draggable.y = bbox.y * draggable.scale * -1 + (root.height - bbox.height * draggable.scale) * 0.5 } + + // Node type browser panel (left side) + NodeTypeBrowser { + id: nodeTypeBrowser + anchors.top: parent.top + anchors.left: parent.left + anchors.bottom: parent.bottom + width: 500 + visible: false + nodeTypesModel: root.nodeTypesModel + + onNodeTypeDoubleClicked: function(nodeType) { + var position = getCenterPosition() + var node = uigraph.addNewNode(nodeType, position) + uigraph.selectedNode = node + uigraph.selectNodes([node]) + } + } + + // Button to toggle node type browser panel (top-left) + FloatingPane { + padding: 2 + anchors.top: parent.top + anchors.left: parent.left + + MaterialToolButton { + text: MaterialIcons.category + ToolTip.text: "Node Types" + checked: nodeTypeBrowser.visible + checkable: true + onClicked: nodeTypeBrowser.visible = !nodeTypeBrowser.visible + } + } } diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml new file mode 100644 index 0000000000..1c79dbecee --- /dev/null +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -0,0 +1,270 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Controls 1.0 +import MaterialIcons 2.2 + +/** + * NodeTypeBrowser displays a panel listing node categories and nodes. + * Selecting a category shows the nodes in that category. + * Selecting a node shows its documentation. + * Double-clicking a node creates it in the graph. + */ + +Panel { + id: root + + /// The node types model ({nodeType: {category, documentation}, ...}) + property variant nodeTypesModel: null + + /// Currently selected node type name + property string selectedNodeName: "" + + /// Signal emitted when a node type should be created + signal nodeTypeDoubleClicked(string nodeType) + + title: "Node Types" + clip: true + + SystemPalette { id: activePalette } + + /// Compute a sorted list of categories from the node types model + function getCategories() { + if (!nodeTypesModel) + return [] + var cats = {} + for (var name in nodeTypesModel) { + var cat = nodeTypesModel[name]["category"] + if (!cats[cat]) + cats[cat] = true + } + return Object.keys(cats).sort() + } + + /// Get sorted node names for a given category + function getNodesForCategory(category) { + if (!nodeTypesModel) + return [] + var nodes = [] + for (var name in nodeTypesModel) { + if (nodeTypesModel[name]["category"] === category) + nodes.push(name) + } + return nodes.sort() + } + + RowLayout { + anchors.fill: parent + spacing: 0 + + // Left column: categories + Rectangle { + Layout.preferredWidth: 130 + Layout.fillHeight: true + color: Qt.darker(activePalette.window, 1.05) + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Categories" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } + + ListView { + id: categoryList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: root.getCategories() + currentIndex: -1 + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + delegate: ItemDelegate { + width: categoryList.width + height: 28 + highlighted: categoryList.currentIndex === index + padding: 6 + + contentItem: Label { + text: modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } + + onClicked: { + categoryList.currentIndex = index + nodeList.currentIndex = -1 + root.selectedNodeName = "" + } + } + } + } + } + + // Divider + Rectangle { + width: 1 + Layout.fillHeight: true + color: Qt.darker(activePalette.window, 1.3) + } + + // Middle column: nodes in selected category + Rectangle { + Layout.preferredWidth: 160 + Layout.fillHeight: true + color: activePalette.window + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Nodes" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } + + ListView { + id: nodeList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + currentIndex: -1 + + model: categoryList.currentIndex >= 0 + ? root.getNodesForCategory(root.getCategories()[categoryList.currentIndex]) + : [] + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + delegate: ItemDelegate { + width: nodeList.width + height: 28 + highlighted: nodeList.currentIndex === index + padding: 6 + + contentItem: Label { + text: modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } + + onClicked: { + nodeList.currentIndex = index + root.selectedNodeName = modelData + } + + onDoubleClicked: { + root.nodeTypeDoubleClicked(modelData) + } + } + } + } + } + + // Divider + Rectangle { + width: 1 + Layout.fillHeight: true + color: Qt.darker(activePalette.window, 1.3) + } + + // Right column: node documentation + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: activePalette.window + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Documentation" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ColumnLayout { + width: parent.width + spacing: 4 + + // Node name heading + Label { + visible: root.selectedNodeName !== "" + text: root.selectedNodeName + font.bold: true + font.pointSize: 11 + padding: 8 + bottomPadding: 4 + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + // Documentation text + TextEdit { + visible: root.selectedNodeName !== "" + padding: 8 + topPadding: 4 + Layout.fillWidth: true + width: parent.width + textFormat: TextEdit.MarkdownText + selectByMouse: true + selectionColor: activePalette.highlight + color: activePalette.text + readOnly: true + wrapMode: TextEdit.Wrap + + text: { + if (!root.selectedNodeName || !root.nodeTypesModel) + return "" + var info = root.nodeTypesModel[root.selectedNodeName] + return info ? (info["documentation"] || "") : "" + } + } + + // Placeholder when nothing is selected + Label { + visible: root.selectedNodeName === "" + text: "Select a node to view its documentation." + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + padding: 12 + opacity: 0.6 + Layout.fillWidth: true + } + } + } + } + } + } +} diff --git a/meshroom/ui/qml/GraphEditor/qmldir b/meshroom/ui/qml/GraphEditor/qmldir index 97b068201b..3f8e694b8d 100644 --- a/meshroom/ui/qml/GraphEditor/qmldir +++ b/meshroom/ui/qml/GraphEditor/qmldir @@ -2,6 +2,7 @@ module GraphEditor GraphEditor 1.0 GraphEditor.qml NodeEditor 1.0 NodeEditor.qml +NodeTypeBrowser 1.0 NodeTypeBrowser.qml Node 1.0 Node.qml NodeChunks 1.0 NodeChunks.qml Edge 1.0 Edge.qml From 20c26e8a4118432139140a71d9e44838410db596 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Feb 2026 09:02:23 +0000 Subject: [PATCH 3/7] Address code review: fix button binding, add keyboard support, accessibility Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 3 ++- meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 60e0508a13..3eefd3ae46 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1569,7 +1569,8 @@ Item { text: MaterialIcons.category ToolTip.text: "Node Types" checked: nodeTypeBrowser.visible - checkable: true + Accessible.name: "Toggle Node Types Browser" + Accessible.role: Accessible.Button onClicked: nodeTypeBrowser.visible = !nodeTypeBrowser.visible } } diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml index 1c79dbecee..b28312771d 100644 --- a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -178,6 +178,9 @@ Panel { onDoubleClicked: { root.nodeTypeDoubleClicked(modelData) } + + Keys.onReturnPressed: root.nodeTypeDoubleClicked(modelData) + Keys.onEnterPressed: root.nodeTypeDoubleClicked(modelData) } } } From c05812fbd691226b0d13de0819fda8187a99d241 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:18:54 +0000 Subject: [PATCH 4/7] Make NodeTypeBrowser resizable, widen doc column, fix text wrapping Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 24 ++++++++++++- .../ui/qml/GraphEditor/NodeTypeBrowser.qml | 35 ++++++------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 3eefd3ae46..1f4103ea19 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1547,7 +1547,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.bottom: parent.bottom - width: 500 + width: browserWidthProxy.x visible: false nodeTypesModel: root.nodeTypesModel @@ -1559,6 +1559,28 @@ Item { } } + // Invisible proxy item whose x-coordinate defines the NodeTypeBrowser width (default: 500) + Item { + id: browserWidthProxy + x: 500 + visible: false + } + + // Drag handle on the right edge of the NodeTypeBrowser to resize it + MouseArea { + id: browserResizeHandle + anchors.top: nodeTypeBrowser.top + anchors.bottom: nodeTypeBrowser.bottom + anchors.left: nodeTypeBrowser.right + width: 5 + visible: nodeTypeBrowser.visible + cursorShape: Qt.SizeHorCursor + drag.target: browserWidthProxy + drag.axis: Drag.XAxis + drag.minimumX: 300 + drag.maximumX: root.width - 100 + } + // Button to toggle node type browser panel (top-left) FloatingPane { padding: 2 diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml index b28312771d..57e2548ab9 100644 --- a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -54,14 +54,13 @@ Panel { return nodes.sort() } - RowLayout { + MSplitView { anchors.fill: parent - spacing: 0 // Left column: categories Rectangle { - Layout.preferredWidth: 130 - Layout.fillHeight: true + SplitView.preferredWidth: 120 + SplitView.minimumWidth: 60 color: Qt.darker(activePalette.window, 1.05) ColumnLayout { @@ -114,17 +113,10 @@ Panel { } } - // Divider - Rectangle { - width: 1 - Layout.fillHeight: true - color: Qt.darker(activePalette.window, 1.3) - } - // Middle column: nodes in selected category Rectangle { - Layout.preferredWidth: 160 - Layout.fillHeight: true + SplitView.preferredWidth: 140 + SplitView.minimumWidth: 60 color: activePalette.window ColumnLayout { @@ -186,17 +178,10 @@ Panel { } } - // Divider - Rectangle { - width: 1 - Layout.fillHeight: true - color: Qt.darker(activePalette.window, 1.3) - } - // Right column: node documentation Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true + SplitView.fillWidth: true + SplitView.minimumWidth: 100 color: activePalette.window ColumnLayout { @@ -212,14 +197,16 @@ Panel { } ScrollView { + id: docScrollView Layout.fillWidth: true Layout.fillHeight: true clip: true ScrollBar.vertical.policy: ScrollBar.AlwaysOn ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: availableWidth ColumnLayout { - width: parent.width + width: docScrollView.availableWidth spacing: 4 // Node name heading @@ -240,7 +227,7 @@ Panel { padding: 8 topPadding: 4 Layout.fillWidth: true - width: parent.width + width: docScrollView.availableWidth textFormat: TextEdit.MarkdownText selectByMouse: true selectionColor: activePalette.highlight From faa951a3b77ca2348f9193fd7b009a3ecdbb7dcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:34:53 +0000 Subject: [PATCH 5/7] NodeTypeBrowser: search bar, auto-select, hide on node creation Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/GraphEditor/GraphEditor.qml | 1 + .../ui/qml/GraphEditor/NodeTypeBrowser.qml | 411 +++++++++++------- 2 files changed, 243 insertions(+), 169 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/GraphEditor.qml b/meshroom/ui/qml/GraphEditor/GraphEditor.qml index 1f4103ea19..d0d0ef22e1 100755 --- a/meshroom/ui/qml/GraphEditor/GraphEditor.qml +++ b/meshroom/ui/qml/GraphEditor/GraphEditor.qml @@ -1556,6 +1556,7 @@ Item { var node = uigraph.addNewNode(nodeType, position) uigraph.selectedNode = node uigraph.selectNodes([node]) + nodeTypeBrowser.visible = false } } diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml index 57e2548ab9..59aff20b7e 100644 --- a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -29,12 +29,30 @@ Panel { SystemPalette { id: activePalette } - /// Compute a sorted list of categories from the node types model + /// Lower-cased search filter text + readonly property string filterText: searchField.text.toLowerCase() + + /// Returns true if the given node name matches the current filter + function nodeMatchesFilter(name) { + if (filterText === "") + return true + if (name.toLowerCase().indexOf(filterText) >= 0) + return true + var info = nodeTypesModel ? nodeTypesModel[name] : null + if (info && info["documentation"] && + info["documentation"].toLowerCase().indexOf(filterText) >= 0) + return true + return false + } + + /// Compute a sorted list of categories that have at least one matching node function getCategories() { if (!nodeTypesModel) return [] var cats = {} for (var name in nodeTypesModel) { + if (!nodeMatchesFilter(name)) + continue var cat = nodeTypesModel[name]["category"] if (!cats[cat]) cats[cat] = true @@ -42,215 +60,270 @@ Panel { return Object.keys(cats).sort() } - /// Get sorted node names for a given category + /// Get sorted, filtered node names for a given category function getNodesForCategory(category) { if (!nodeTypesModel) return [] var nodes = [] for (var name in nodeTypesModel) { - if (nodeTypesModel[name]["category"] === category) + if (nodeTypesModel[name]["category"] === category && nodeMatchesFilter(name)) nodes.push(name) } return nodes.sort() } - MSplitView { + /// Select the first available category and its first node + function selectFirstCategory() { + var cats = getCategories() + if (cats.length === 0) { + categoryList.currentIndex = -1 + nodeList.currentIndex = -1 + root.selectedNodeName = "" + return + } + categoryList.currentIndex = 0 + Qt.callLater(function() { + if (nodeList.count > 0) { + nodeList.currentIndex = 0 + root.selectedNodeName = nodeList.model[0] + } else { + nodeList.currentIndex = -1 + root.selectedNodeName = "" + } + }) + } + + onVisibleChanged: { + if (visible) + selectFirstCategory() + } + + ColumnLayout { anchors.fill: parent + spacing: 0 + + // Search bar + TextField { + id: searchField + Layout.fillWidth: true + placeholderText: "Search nodes..." + leftPadding: 8 + onTextChanged: searchDebounce.restart() + } - // Left column: categories - Rectangle { - SplitView.preferredWidth: 120 - SplitView.minimumWidth: 60 - color: Qt.darker(activePalette.window, 1.05) - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Label { - text: "Categories" - font.bold: true - padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } + // Debounce timer: avoids re-filtering on every keystroke + Timer { + id: searchDebounce + interval: 150 + onTriggered: root.selectFirstCategory() + } - ListView { - id: categoryList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: root.getCategories() - currentIndex: -1 + MSplitView { + Layout.fillWidth: true + Layout.fillHeight: true - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + // Left column: categories + Rectangle { + SplitView.preferredWidth: 120 + SplitView.minimumWidth: 60 + color: Qt.darker(activePalette.window, 1.05) - delegate: ItemDelegate { - width: categoryList.width - height: 28 - highlighted: categoryList.currentIndex === index + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Categories" + font.bold: true padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - contentItem: Label { - text: modelData - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + ListView { + id: categoryList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: root.getCategories() + currentIndex: -1 + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + delegate: ItemDelegate { + width: categoryList.width + height: 28 + highlighted: categoryList.currentIndex === index + padding: 6 + + contentItem: Label { + text: modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - background: Rectangle { - color: parent.highlighted - ? activePalette.highlight - : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") - } + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } - onClicked: { - categoryList.currentIndex = index - nodeList.currentIndex = -1 - root.selectedNodeName = "" + onClicked: { + categoryList.currentIndex = index + Qt.callLater(function() { + if (nodeList.count > 0) { + nodeList.currentIndex = 0 + root.selectedNodeName = nodeList.model[0] + } else { + nodeList.currentIndex = -1 + root.selectedNodeName = "" + } + }) + } } } } } - } - - // Middle column: nodes in selected category - Rectangle { - SplitView.preferredWidth: 140 - SplitView.minimumWidth: 60 - color: activePalette.window - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Label { - text: "Nodes" - font.bold: true - padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } - - ListView { - id: nodeList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - currentIndex: -1 - model: categoryList.currentIndex >= 0 - ? root.getNodesForCategory(root.getCategories()[categoryList.currentIndex]) - : [] + // Middle column: nodes in selected category + Rectangle { + SplitView.preferredWidth: 140 + SplitView.minimumWidth: 60 + color: activePalette.window - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { + anchors.fill: parent + spacing: 0 - delegate: ItemDelegate { - width: nodeList.width - height: 28 - highlighted: nodeList.currentIndex === index + Label { + text: "Nodes" + font.bold: true padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - contentItem: Label { - text: modelData - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + ListView { + id: nodeList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + currentIndex: -1 + + model: categoryList.currentIndex >= 0 + ? root.getNodesForCategory(root.getCategories()[categoryList.currentIndex]) + : [] + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + delegate: ItemDelegate { + width: nodeList.width + height: 28 + highlighted: nodeList.currentIndex === index + padding: 6 + + contentItem: Label { + text: modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - background: Rectangle { - color: parent.highlighted - ? activePalette.highlight - : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") - } + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } - onClicked: { - nodeList.currentIndex = index - root.selectedNodeName = modelData - } + onClicked: { + nodeList.currentIndex = index + root.selectedNodeName = modelData + } - onDoubleClicked: { - root.nodeTypeDoubleClicked(modelData) - } + onDoubleClicked: { + root.nodeTypeDoubleClicked(modelData) + } - Keys.onReturnPressed: root.nodeTypeDoubleClicked(modelData) - Keys.onEnterPressed: root.nodeTypeDoubleClicked(modelData) + Keys.onReturnPressed: root.nodeTypeDoubleClicked(modelData) + Keys.onEnterPressed: root.nodeTypeDoubleClicked(modelData) + } } } } - } - // Right column: node documentation - Rectangle { - SplitView.fillWidth: true - SplitView.minimumWidth: 100 - color: activePalette.window - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Label { - text: "Documentation" - font.bold: true - padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } + // Right column: node documentation + Rectangle { + SplitView.fillWidth: true + SplitView.minimumWidth: 100 + color: activePalette.window - ScrollView { - id: docScrollView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - ScrollBar.vertical.policy: ScrollBar.AlwaysOn - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentWidth: availableWidth - - ColumnLayout { - width: docScrollView.availableWidth - spacing: 4 - - // Node name heading - Label { - visible: root.selectedNodeName !== "" - text: root.selectedNodeName - font.bold: true - font.pointSize: 11 - padding: 8 - bottomPadding: 4 - Layout.fillWidth: true - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - } + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Documentation" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - // Documentation text - TextEdit { - visible: root.selectedNodeName !== "" - padding: 8 - topPadding: 4 - Layout.fillWidth: true + ScrollView { + id: docScrollView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: availableWidth + + ColumnLayout { width: docScrollView.availableWidth - textFormat: TextEdit.MarkdownText - selectByMouse: true - selectionColor: activePalette.highlight - color: activePalette.text - readOnly: true - wrapMode: TextEdit.Wrap - - text: { - if (!root.selectedNodeName || !root.nodeTypesModel) - return "" - var info = root.nodeTypesModel[root.selectedNodeName] - return info ? (info["documentation"] || "") : "" + spacing: 4 + + // Node name heading + Label { + visible: root.selectedNodeName !== "" + text: root.selectedNodeName + font.bold: true + font.pointSize: 11 + padding: 8 + bottomPadding: 4 + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere } - } - // Placeholder when nothing is selected - Label { - visible: root.selectedNodeName === "" - text: "Select a node to view its documentation." - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - padding: 12 - opacity: 0.6 - Layout.fillWidth: true + // Documentation text + TextEdit { + visible: root.selectedNodeName !== "" + padding: 8 + topPadding: 4 + Layout.fillWidth: true + width: docScrollView.availableWidth + textFormat: TextEdit.MarkdownText + selectByMouse: true + selectionColor: activePalette.highlight + color: activePalette.text + readOnly: true + wrapMode: TextEdit.Wrap + + text: { + if (!root.selectedNodeName || !root.nodeTypesModel) + return "" + var info = root.nodeTypesModel[root.selectedNodeName] + return info ? (info["documentation"] || "") : "" + } + } + + // Placeholder when nothing is selected + Label { + visible: root.selectedNodeName === "" + text: "Select a node to view its documentation." + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + padding: 12 + opacity: 0.6 + Layout.fillWidth: true + } } } } From e6b730b510ce6a1ebfb999d5e637fc923b70c7fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:08:48 +0000 Subject: [PATCH 6/7] NodeTypeBrowser: SearchBar in headerBar, highlight matched text in blue Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- .../ui/qml/GraphEditor/NodeTypeBrowser.qml | 428 ++++++++++-------- 1 file changed, 231 insertions(+), 197 deletions(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml index 59aff20b7e..e6556ddc71 100644 --- a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -29,8 +29,53 @@ Panel { SystemPalette { id: activePalette } - /// Lower-cased search filter text - readonly property string filterText: searchField.text.toLowerCase() + /// Lower-cased search filter text (sourced from the header SearchBar) + readonly property string filterText: searchBar.text.toLowerCase() + + /// True when the search bar contains a non-empty query + readonly property bool hasActiveFilter: filterText !== "" + + // SearchBar placed in the panel header, matching the ImageGallery pattern + headerBar: RowLayout { + SearchBar { + id: searchBar + toggle: true + maxWidth: 150 + onTextChanged: searchDebounce.restart() + } + } + + // Debounce timer: avoids re-filtering on every keystroke + Timer { + id: searchDebounce + interval: 150 + onTriggered: root.selectFirstCategory() + } + + /// Returns plain text with occurrences of searchTerm wrapped in a blue tag. + /// The input plainText is HTML-escaped before substitution. + function highlightText(plainText, searchTerm) { + if (!searchTerm || searchTerm === "") + return plainText + var escaped = plainText + .replace(/&/g, "&") + .replace(//g, ">") + var lowerEscaped = escaped.toLowerCase() + var result = "" + var pos = 0 + while (pos < escaped.length) { + var idx = lowerEscaped.indexOf(searchTerm, pos) + if (idx < 0) { + result += escaped.substring(pos) + break + } + result += escaped.substring(pos, idx) + result += "" + escaped.substring(idx, idx + searchTerm.length) + "" + pos = idx + searchTerm.length + } + return result + } /// Returns true if the given node name matches the current filter function nodeMatchesFilter(name) { @@ -98,232 +143,221 @@ Panel { selectFirstCategory() } - ColumnLayout { + MSplitView { anchors.fill: parent - spacing: 0 - - // Search bar - TextField { - id: searchField - Layout.fillWidth: true - placeholderText: "Search nodes..." - leftPadding: 8 - onTextChanged: searchDebounce.restart() - } - - // Debounce timer: avoids re-filtering on every keystroke - Timer { - id: searchDebounce - interval: 150 - onTriggered: root.selectFirstCategory() - } - MSplitView { - Layout.fillWidth: true - Layout.fillHeight: true + // Left column: categories + Rectangle { + SplitView.preferredWidth: 120 + SplitView.minimumWidth: 60 + color: Qt.darker(activePalette.window, 1.05) + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Categories" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - // Left column: categories - Rectangle { - SplitView.preferredWidth: 120 - SplitView.minimumWidth: 60 - color: Qt.darker(activePalette.window, 1.05) + ListView { + id: categoryList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: root.getCategories() + currentIndex: -1 - ColumnLayout { - anchors.fill: parent - spacing: 0 + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - Label { - text: "Categories" - font.bold: true + delegate: ItemDelegate { + width: categoryList.width + height: 28 + highlighted: categoryList.currentIndex === index padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } - ListView { - id: categoryList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: root.getCategories() - currentIndex: -1 - - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - - delegate: ItemDelegate { - width: categoryList.width - height: 28 - highlighted: categoryList.currentIndex === index - padding: 6 - - contentItem: Label { - text: modelData - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + contentItem: Label { + text: modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - background: Rectangle { - color: parent.highlighted - ? activePalette.highlight - : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") - } + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } - onClicked: { - categoryList.currentIndex = index - Qt.callLater(function() { - if (nodeList.count > 0) { - nodeList.currentIndex = 0 - root.selectedNodeName = nodeList.model[0] - } else { - nodeList.currentIndex = -1 - root.selectedNodeName = "" - } - }) - } + onClicked: { + categoryList.currentIndex = index + Qt.callLater(function() { + if (nodeList.count > 0) { + nodeList.currentIndex = 0 + root.selectedNodeName = nodeList.model[0] + } else { + nodeList.currentIndex = -1 + root.selectedNodeName = "" + } + }) } } } } + } - // Middle column: nodes in selected category - Rectangle { - SplitView.preferredWidth: 140 - SplitView.minimumWidth: 60 - color: activePalette.window + // Middle column: nodes in selected category + Rectangle { + SplitView.preferredWidth: 140 + SplitView.minimumWidth: 60 + color: activePalette.window + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Nodes" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - ColumnLayout { - anchors.fill: parent - spacing: 0 + ListView { + id: nodeList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + currentIndex: -1 - Label { - text: "Nodes" - font.bold: true - padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } + model: categoryList.currentIndex >= 0 + ? root.getNodesForCategory(root.getCategories()[categoryList.currentIndex]) + : [] - ListView { - id: nodeList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - currentIndex: -1 - - model: categoryList.currentIndex >= 0 - ? root.getNodesForCategory(root.getCategories()[categoryList.currentIndex]) - : [] - - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - - delegate: ItemDelegate { - width: nodeList.width - height: 28 - highlighted: nodeList.currentIndex === index - padding: 6 - - contentItem: Label { - text: modelData - elide: Text.ElideRight - verticalAlignment: Text.AlignVCenter - } + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - background: Rectangle { - color: parent.highlighted - ? activePalette.highlight - : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") - } + delegate: ItemDelegate { + width: nodeList.width + height: 28 + highlighted: nodeList.currentIndex === index + padding: 6 - onClicked: { - nodeList.currentIndex = index - root.selectedNodeName = modelData - } + contentItem: Label { + textFormat: root.hasActiveFilter ? Text.RichText : Text.AutoText + text: root.hasActiveFilter + ? root.highlightText(modelData, root.filterText) + : modelData + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + } - onDoubleClicked: { - root.nodeTypeDoubleClicked(modelData) - } + background: Rectangle { + color: parent.highlighted + ? activePalette.highlight + : (parent.hovered ? Qt.darker(activePalette.window, 1.2) : "transparent") + } + + onClicked: { + nodeList.currentIndex = index + root.selectedNodeName = modelData + } - Keys.onReturnPressed: root.nodeTypeDoubleClicked(modelData) - Keys.onEnterPressed: root.nodeTypeDoubleClicked(modelData) + onDoubleClicked: { + root.nodeTypeDoubleClicked(modelData) } + + Keys.onReturnPressed: root.nodeTypeDoubleClicked(modelData) + Keys.onEnterPressed: root.nodeTypeDoubleClicked(modelData) } } } + } - // Right column: node documentation - Rectangle { - SplitView.fillWidth: true - SplitView.minimumWidth: 100 - color: activePalette.window - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - Label { - text: "Documentation" - font.bold: true - padding: 6 - Layout.fillWidth: true - background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } - } + // Right column: node documentation + Rectangle { + SplitView.fillWidth: true + SplitView.minimumWidth: 100 + color: activePalette.window + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Label { + text: "Documentation" + font.bold: true + padding: 6 + Layout.fillWidth: true + background: Rectangle { color: Qt.darker(activePalette.window, 1.15) } + } - ScrollView { - id: docScrollView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - ScrollBar.vertical.policy: ScrollBar.AlwaysOn - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - contentWidth: availableWidth + ScrollView { + id: docScrollView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: availableWidth + + ColumnLayout { + width: docScrollView.availableWidth + spacing: 4 + + // Node name heading (with search highlight when filter is active) + Label { + visible: root.selectedNodeName !== "" + textFormat: root.hasActiveFilter ? Text.RichText : Text.AutoText + text: root.hasActiveFilter + ? root.highlightText(root.selectedNodeName, root.filterText) + : root.selectedNodeName + font.bold: true + font.pointSize: 11 + padding: 8 + bottomPadding: 4 + Layout.fillWidth: true + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } - ColumnLayout { + // Documentation text + // When a filter is active, render as RichText with highlighted matches. + // When no filter, render as MarkdownText to preserve formatting. + TextEdit { + visible: root.selectedNodeName !== "" + padding: 8 + topPadding: 4 + Layout.fillWidth: true width: docScrollView.availableWidth - spacing: 4 - - // Node name heading - Label { - visible: root.selectedNodeName !== "" - text: root.selectedNodeName - font.bold: true - font.pointSize: 11 - padding: 8 - bottomPadding: 4 - Layout.fillWidth: true - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - } - - // Documentation text - TextEdit { - visible: root.selectedNodeName !== "" - padding: 8 - topPadding: 4 - Layout.fillWidth: true - width: docScrollView.availableWidth - textFormat: TextEdit.MarkdownText - selectByMouse: true - selectionColor: activePalette.highlight - color: activePalette.text - readOnly: true - wrapMode: TextEdit.Wrap - - text: { - if (!root.selectedNodeName || !root.nodeTypesModel) - return "" - var info = root.nodeTypesModel[root.selectedNodeName] - return info ? (info["documentation"] || "") : "" - } + textFormat: root.hasActiveFilter ? TextEdit.RichText : TextEdit.MarkdownText + selectByMouse: true + selectionColor: activePalette.highlight + color: activePalette.text + readOnly: true + wrapMode: TextEdit.Wrap + + text: { + if (!root.selectedNodeName || !root.nodeTypesModel) + return "" + var info = root.nodeTypesModel[root.selectedNodeName] + var rawText = info ? (info["documentation"] || "") : "" + if (!root.hasActiveFilter) + return rawText + return root.highlightText(rawText, root.filterText) } + } - // Placeholder when nothing is selected - Label { - visible: root.selectedNodeName === "" - text: "Select a node to view its documentation." - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - padding: 12 - opacity: 0.6 - Layout.fillWidth: true - } + // Placeholder when nothing is selected + Label { + visible: root.selectedNodeName === "" + text: "Select a node to view its documentation." + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + padding: 12 + opacity: 0.6 + Layout.fillWidth: true } } } From 32d3ef8aa4f6effa5534c2a75cda35888fd3a28f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:35:18 +0000 Subject: [PATCH 7/7] NodeTypeBrowser: preserve category/node selection when filter changes Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- .../ui/qml/GraphEditor/NodeTypeBrowser.qml | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml index e6556ddc71..fd45745db5 100644 --- a/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml +++ b/meshroom/ui/qml/GraphEditor/NodeTypeBrowser.qml @@ -49,7 +49,7 @@ Panel { Timer { id: searchDebounce interval: 150 - onTriggered: root.selectFirstCategory() + onTriggered: root.applyFilter() } /// Returns plain text with occurrences of searchTerm wrapped in a blue tag. @@ -138,6 +138,34 @@ Panel { }) } + /// Called when the search filter changes. + /// Tries to keep the current selection visible; only falls back to the + /// first category/node when the selected node is filtered out. + function applyFilter() { + var cats = getCategories() + if (cats.length === 0) { + categoryList.currentIndex = -1 + nodeList.currentIndex = -1 + root.selectedNodeName = "" + return + } + // Keep the current selection if it still passes the filter + if (root.selectedNodeName !== "" && nodeMatchesFilter(root.selectedNodeName)) { + var currentCategory = nodeTypesModel[root.selectedNodeName]["category"] + var catIdx = cats.indexOf(currentCategory) + if (catIdx >= 0) { + categoryList.currentIndex = catIdx + Qt.callLater(function() { + var nodes = getNodesForCategory(currentCategory) + nodeList.currentIndex = nodes.indexOf(root.selectedNodeName) + }) + return + } + } + // Current selection was filtered out – fall back to first category/node + selectFirstCategory() + } + onVisibleChanged: { if (visible) selectFirstCategory()