Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions meshroom/common/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ def add(self, obj):
assert key is not None
assert key not in self._objects
self._objects[key] = obj

def rename(self, oldKey: str, newKey: str):
""" Rename an element in the dict model

Args:
oldKey (str): Previous key name of the element to replace.
newKey (str): New key name to insert in the model.

Raises:
KeyError: if the new name is already used.
"""
if newKey in self._objects.keys():
raise KeyError(f"Key {newKey} is already in use in {self}")
obj = self._objects[oldKey]
self._objects[newKey] = obj
del self._objects[oldKey]

def pop(self, key):
assert key in self._objects
Expand Down
18 changes: 18 additions & 0 deletions meshroom/common/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ def replace(self, i, obj):
self._objects[i] = obj
self.dataChanged.emit(self.index(i), self.index(i), [])

def rename(self, oldKey: str, newKey: str):
""" Rename an element in the model
Args:
oldKey (str): Previous key name of the element to replace.
newKey (str): New key name to insert in the model.
Raises:
KeyError: if the new name is already used.
"""
if newKey in self._objectByKey.keys():
raise KeyError(f"Key {newKey} is already in use in {self}")
obj = self._objectByKey[oldKey]
index = self.indexOf(obj)
self._objectByKey[newKey] = obj
del self._objectByKey[oldKey]
self.dataChanged.emit(self.index(index), self.index(index), [])

def move(self, fromIndex, toIndex):
""" Moves the item at index position from to index position to
and notifies any views.
Expand Down
24 changes: 24 additions & 0 deletions meshroom/core/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,30 @@ def addNode(self, node, uniqueName=None):
node._applyExpr()
return node

def renameNode(self, node: Node, newName: str):
""" Rename a node in the Node Graph.
If the proposed name is already assigned to a node then it will create a unique name

Args:
node (Node): Node to rename.
newName (str): New name of the node.
"""
# Handle empty string
if not newName:
return
if node.getLocked():
logging.warning(f"Cannot rename node {node} because of the locked status")
return
usedNames = {n._name for n in self._nodes if n != node}
# Make sure we rename to an available name
if newName in usedNames:
newName = self._createUniqueNodeName(newName, usedNames)
# Rename in the dict model
self._nodes.rename(node._name, newName)
# Finally rename the node name property and notify Qt
node._name = newName
node.nodeNameChanged.emit()

def copyNode(self, srcNode: Node, withEdges: bool=False):
"""
Get a copy instance of a node outside the graph.
Expand Down
5 changes: 3 additions & 2 deletions meshroom/core/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ def nameToLabel(self, name):
Returns:
str: the high-level label from the technical node name
"""
t, idx = name.split("_")
t, idx = name.rsplit("_", 1) if "_" in name else (name, "1")
return f"{t}{idx if int(idx) > 1 else ''}"

def getDocumentation(self):
Expand Down Expand Up @@ -2050,7 +2050,8 @@ def _hasDisplayableShape(self):
attr.desc.semantic == "shapeFile"), None) is not None


name = Property(str, getName, constant=True)
nodeNameChanged = Signal()
name = Property(str, getName, notify=nodeNameChanged)
defaultLabel = Property(str, getDefaultLabel, constant=True)
nodeType = Property(str, nodeType.fget, constant=True)
documentation = Property(str, getDocumentation, constant=True)
Expand Down
18 changes: 18 additions & 0 deletions meshroom/ui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,24 @@ def undoImpl(self):
self.graph.removeNode(self.nodeName)


class RenameNodeCommand(GraphCommand):
def __init__(self, graph, node, name, parent=None):
""" Command to rename a node. The new name should not be used yet.
"""
super().__init__(graph, parent)
self.node = node
self.oldName = node._name
self.name = name

def redoImpl(self):
self.setText(f"Rename Node {self.oldName} to {self.name}")
self.graph.renameNode(self.node, self.name)
return self.node._name

def undoImpl(self):
self.graph.renameNode(self.node, self.oldName)


class RemoveNodeCommand(GraphCommand):
def __init__(self, graph, node, parent=None):
super().__init__(graph, parent)
Expand Down
27 changes: 27 additions & 0 deletions meshroom/ui/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import Iterable
import logging
import os
import re
import json
from enum import Enum
from threading import Thread, Event, Lock
Expand Down Expand Up @@ -939,6 +940,32 @@ def addNewNode(self, nodeType, position=None, **kwargs):
position = Position(position.x(), position.y())
return self.push(commands.AddNodeCommand(self._graph, nodeType, position=position, **kwargs))

@Slot(Node, str, result=str)
def renameNode(self, node: Node, newName: str):
""" Triggers the node renaming.

In this function the last `_N` index is removed, then all special characters
(everything except letters and numbers) are removed.
The name uniqueness will be ensured later by adding a suffix (e.g. `_1`, `_2`, ...)

Labels can be used to have special characters in the displayed name.

Args:
node (Node): Node to rename.
newName (str): New name to set.

Returns:
str: The final name of the node.
"""
newName = "_".join(newName.split("_")[:-1]) if "_" in newName else newName
# Eliminate all characters except digits and letters
newName = re.sub(r"[^0-9a-zA-Z]", "", newName)
# Create unique name
uniqueName = self._graph._createUniqueNodeName(newName, {n._name for n in self._graph._nodes if n != node})
if not newName or uniqueName == node._name:
return ""
return self.push(commands.RenameNodeCommand(self._graph, node, uniqueName))

def moveNode(self, node: Node, position: Position):
"""
Move `node` to the given `position`.
Expand Down
21 changes: 16 additions & 5 deletions meshroom/ui/qml/Controls/Panel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Page {
id: root

property alias headerBar: headerLayout.data
property Component titleComponent: null // Allow custom component for title
property alias footerContent: footerLayout.data
property alias icon: iconPlaceHolder.data
property alias loading: loadingIndicator.running
Expand Down Expand Up @@ -62,12 +63,22 @@ Page {
}

// Title
Label {
text: root.title
elide: Text.ElideRight
topPadding: m.vPadding
bottomPadding: m.vPadding
// Either we load the custom root.titleComponent or we just put the root.title
Loader {
id: titleLoader
sourceComponent: root.titleComponent !== null ? root.titleComponent : defaultTitleComponent
Layout.fillWidth: false
}
Component {
id: defaultTitleComponent
Label {
text: root.title
elide: Text.ElideRight
topPadding: m.vPadding
bottomPadding: m.vPadding
}
}

Item {
width: 10
}
Expand Down
10 changes: 9 additions & 1 deletion meshroom/ui/qml/GraphEditor/Node.qml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Item {
property bool selected: false
property bool hovered: false
property bool dragging: mouseArea.drag.active
/// Node label
property string nodeLabel: node ? node.label : ""
/// Combined x and y
property point position: Qt.point(x, y)
/// Styling
Expand Down Expand Up @@ -87,6 +89,12 @@ Item {
root.x = root.node.x
root.y = root.node.y
}
function onNameChanged() {
// HACK: Make sure when the node name changes the node label is updated
root.nodeLabel = ""
// Restore binding to root.node.label
root.nodeLabel = Qt.binding(function() { return root.node.label; })
Comment on lines +94 to +96
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the pattern in NodeEditor.qml, the empty string assignment followed by Qt.binding() restoration is used to force a property update. This is the same fragile workaround pattern. While this works, it would be better to have a consistent approach across the codebase. Consider refactoring both occurrences to use a more explicit state management approach.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah but I don't have another way yet

Copy link
Contributor

@nicolas-lambert-tc nicolas-lambert-tc Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propably use the declarative way by creating a property on the core.BaseNode:
nodeLabel = Property(str, getLabel, notify=nodeNameChanged)
So you can directly use this property in the qml:
text: node.nodeLabel

One advantage is that anywhere you are using this property, any change on it will send the signal, so you don't have to explicitly connect in the qml.

Copy link
Contributor Author

@Alxiice Alxiice Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have issues making this working because nodeLabel is already a property that binds to another signal (internal properties) and it doesn't prevent the issues I had when cancelling the textEdit editing which was the initial reason why I had to force reset the binding.
I will text you directly to see if I missed something in the approach here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, not so obvious problem....

}
}

Timer {
Expand Down Expand Up @@ -399,7 +407,7 @@ Item {
Label {
id: nodeLabel
Layout.fillWidth: true
text: node ? node.label : ""
text: root.nodeLabel
padding: 4
color: root.mainSelected ? activePalette.highlightedText : activePalette.text
elide: Text.ElideMiddle
Expand Down
Loading
Loading