Skip to content

Commit d31d134

Browse files
committed
[ui] Plugins: Added UI for Node Plugin Manager
The Plugin Manager UI lets users see the available loaded Node Plugins. Each of the plugins have a detailed descriptive view which shows up when the label of the plugin name is clicked in the UI Node Plugin Manager allows browsing the Python Packages consisting the Node Plugins to load them in the current instance of Meshroom.
1 parent f1105c3 commit d31d134

File tree

7 files changed

+447
-22
lines changed

7 files changed

+447
-22
lines changed

meshroom/core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,13 @@ def nodeVersion(nodeDesc, default=None):
188188
return moduleVersion(nodeDesc.__module__, default)
189189

190190

191-
def registerNodeType(nodeType, module=None):
191+
def registerNodeType(nodeType):
192192
""" Register a Node Type based on a Node Description class.
193193
194194
After registration, nodes of this type can be instantiated in a Graph.
195195
"""
196196
# Register the node in plugin manager
197-
registered = pluginManager.registerNode(nodeType, module=module)
197+
registered = pluginManager.registerNode(nodeType)
198198

199199
# The plugin was already registered
200200
if not registered:

meshroom/ui/app.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
from PySide2.QtWidgets import QApplication
1111

1212
import meshroom
13-
from meshroom.core import pluginManager
1413
from meshroom.core.taskManager import TaskManager
1514
from meshroom.common import Property, Variant, Signal, Slot
1615

1716
from meshroom.ui import components
17+
from meshroom.ui.plugins import NodesPluginManager
1818
from meshroom.ui.components.clipboard import ClipboardHelper
1919
from meshroom.ui.components.filepath import FilepathHelper
2020
from meshroom.ui.components.scene3D import Scene3DHelper, Transformations3DHelper
@@ -240,16 +240,16 @@ def __init__(self, args):
240240
self.engine.addImportPath(qmlDir)
241241
components.registerTypes()
242242

243-
# expose available node types that can be instantiated
244-
nodesDesc = pluginManager.descriptors
245-
self.engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": nodesDesc[n].category} for n in sorted(nodesDesc.keys())})
246-
247243
# instantiate Reconstruction object
248244
self._undoStack = commands.UndoStack(self)
249245
self._taskManager = TaskManager(self)
250246
self._activeProject = Reconstruction(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self)
251247
self._activeProject.setSubmitLabel(args.submitLabel)
248+
249+
# The Plugin manager for UI to communicate with
250+
self._pluginManager = NodesPluginManager(parent=self)
252251
self.engine.rootContext().setContextProperty("_reconstruction", self._activeProject)
252+
self.engine.rootContext().setContextProperty("_pluginator", self._pluginManager)
253253

254254
# those helpers should be available from QML Utils module as singletons, but:
255255
# - qmlRegisterUncreatableType is not yet available in PySide2

meshroom/ui/plugins.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
""" UI Component for the Plugin System.
2+
"""
3+
# STD
4+
import urllib.parse as _parser
5+
6+
# Qt
7+
from PySide2.QtCore import Slot, QObject, Property, Signal
8+
9+
# Internal
10+
from meshroom.core import pluginManager
11+
from meshroom.common import BaseObject, DictModel
12+
13+
14+
class Plugin(BaseObject):
15+
""" Representation of a Plugin in UI.
16+
"""
17+
18+
def __init__(self, descriptor):
19+
""" Constructor.
20+
21+
Args:
22+
descriptor (NodeDescriptor): A Plugin descriptor.
23+
"""
24+
super().__init__()
25+
26+
self._descriptor = descriptor
27+
28+
# Any Node errors
29+
self._nodeErrors = self._errors()
30+
31+
def _errors(self) -> str:
32+
"""
33+
"""
34+
if not self._descriptor.errors:
35+
return ""
36+
37+
errors = ["Following parameters have invalid default values/ranges:"]
38+
39+
# Add the parameters from the node Errors
40+
errors.extend([f"* Param {param}" for param in self._descriptor.errors])
41+
42+
return "\n".join(errors)
43+
44+
@Slot()
45+
def reload(self):
46+
""" Reloads the plugin descriptor.
47+
"""
48+
self._descriptor.reload()
49+
50+
# Update the Node errors
51+
self._nodeErrors = self._errors()
52+
53+
name = Property(str, lambda self: self._descriptor.name, constant=True)
54+
documentation = Property(str, lambda self: self._descriptor.documentation, constant=True)
55+
loaded = Property(bool, lambda self: bool(self._descriptor.status), constant=True)
56+
version = Property(str, lambda self: self._descriptor.version, constant=True)
57+
path = Property(str, lambda self: self._descriptor.path, constant=True)
58+
errors = Property(str, lambda self: self._nodeErrors, constant=True)
59+
category = Property(str, lambda self: self._descriptor.category, constant=True)
60+
61+
62+
63+
class NodesPluginManager(QObject):
64+
""" UI Plugin Manager Component. Serves as a Bridge between the core Nodes' Plugin Manager and how the
65+
users interact with it.
66+
"""
67+
68+
def __init__(self, parent=None):
69+
""" Constructor.
70+
71+
Keyword Args:
72+
parent (QObject): The Parent for the Plugin Manager.
73+
"""
74+
super().__init__(parent=parent)
75+
76+
# The core Plugin Manager
77+
self._manager = pluginManager
78+
79+
# The plugins as a Model which can be communicated to the frontend
80+
self._plugins = DictModel(keyAttrName='name', parent=self)
81+
82+
# Reset the plugins model
83+
self._reset()
84+
85+
# Signals
86+
pluginsChanged = Signal()
87+
88+
# Properties
89+
plugins = Property(BaseObject, lambda self: self._plugins, notify=pluginsChanged)
90+
91+
# Protected
92+
def _reset(self):
93+
""" Requeries and Resets the Plugins Model from the core Plugin Manager for UI refreshes.
94+
"""
95+
plugins = [Plugin(desc) for desc in self._manager.descriptors.values()]
96+
97+
# Reset the plugins model
98+
self._plugins.reset(plugins)
99+
100+
# Public
101+
@Slot(str)
102+
def load(self, directory):
103+
""" Load plugins from a given directory, which serves as a package of Meshroom Node Modules.
104+
105+
Args:
106+
directory (str): Path to the plugin package to import.
107+
"""
108+
# The incoming directory to this method from the QML FolderDialog component is of the format
109+
# file:///path/to/a/python/package
110+
# Cleanup the provided directory url and convert to a usable Posix path
111+
uri = _parser.urlparse(directory)
112+
113+
# Load the plugin(s) from the provided directory package
114+
self._manager.load(_parser.unquote(uri.path))
115+
116+
# Reset the plugins model
117+
self._reset()
118+
119+
# Emit that the plugins have now been updated
120+
self.pluginsChanged.emit()

meshroom/ui/qml/Application.qml

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,17 @@ Page {
433433
uigraph: _reconstruction
434434
}
435435

436+
PluginManager {
437+
id: pluginManager
438+
manager: _pluginator
439+
440+
// When a plugin package has been browsed
441+
onBrowsed: {
442+
// Load Plugins
443+
_pluginator.load(directory)
444+
}
445+
}
446+
436447

437448
// Actions
438449
Action {
@@ -923,6 +934,25 @@ Page {
923934
border.color: Qt.darker(activePalette.window, 1.15)
924935
}
925936
}
937+
938+
// Button to Launch Plugin Manager
939+
ToolButton {
940+
id: pluginManagerButton
941+
visible: true
942+
text: MaterialIcons.build
943+
font.family: MaterialIcons.fontFamily
944+
font.pointSize: 12
945+
onClicked: {
946+
pluginManager.open()
947+
}
948+
ToolTip.text: "Plugin Manager"
949+
ToolTip.visible: hovered
950+
951+
background: Rectangle {
952+
color: pluginManagerButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
953+
border.color: Qt.darker(activePalette.window, 1.15)
954+
}
955+
}
926956
}
927957

928958
footer: ToolBar {
@@ -1156,7 +1186,7 @@ Page {
11561186
visible: graphEditorPanel.currentTab === 0
11571187

11581188
uigraph: _reconstruction
1159-
nodeTypesModel: _nodeTypes
1189+
nodeTypesModel: _pluginator.plugins
11601190

11611191
onNodeDoubleClicked: {
11621192
_reconstruction.setActiveNode(node);

meshroom/ui/qml/GraphEditor/GraphEditor.qml

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,40 @@ Item {
1414
property variant uigraph: null /// Meshroom ui graph (UIGraph)
1515
readonly property variant graph: uigraph ? uigraph.graph : null /// core graph contained in ui graph
1616
property variant nodeTypesModel: null /// the list of node types that can be instantiated
17+
18+
readonly property var nodeCategories: {
19+
// Map to hold the node category: node type
20+
let categories = {}
21+
22+
for (var i = 0; i < nodeTypesModel.count; i++) {
23+
// The node at the index
24+
let node = nodeTypesModel.at(i)
25+
26+
// Node Category
27+
let category = node.category;
28+
29+
// Setup an array to allow node type(s) to be added to the category
30+
if (categories[category] === undefined) {
31+
categories[category] = []
32+
}
33+
// Add the nodeType to the category which will show up in the Menu
34+
categories[category].push(node.name)
35+
}
36+
37+
return categories
38+
}
39+
40+
readonly property var nodeTypes: {
41+
// An array to hold the node Types
42+
let types = []
43+
44+
for (var i = 0; i < nodeTypesModel.count; i++) {
45+
types.push(nodeTypesModel.at(i).name)
46+
}
47+
48+
return types
49+
}
50+
1751
property real maxZoom: 2.0
1852
property real minZoom: 0.1
1953

@@ -223,7 +257,7 @@ Item {
223257
Menu {
224258
id: newNodeMenu
225259
property point spawnPosition
226-
property variant menuKeys: Object.keys(root.nodeTypesModel).concat(Object.values(MeshroomApp.pipelineTemplateNames))
260+
property variant menuKeys: nodeTypes.concat(Object.values(MeshroomApp.pipelineTemplateNames))
227261
height: searchBar.height + nodeMenuRepeater.height + instantiator.height
228262

229263
function createNode(nodeType) {
@@ -251,21 +285,10 @@ Item {
251285
}
252286

253287
function parseCategories() {
254-
// Organize nodes based on their category
255-
// {"category1": ["node1", "node2"], "category2": ["node3", "node4"]}
256-
let categories = {};
257-
for (const [name, data] of Object.entries(root.nodeTypesModel)) {
258-
let category = data["category"];
259-
if (categories[category] === undefined) {
260-
categories[category] = []
261-
}
262-
categories[category].push(name)
263-
}
264-
265288
// Add a "Pipelines" category, filled with the list of templates to create pipelines from the menu
266-
categories["Pipelines"] = MeshroomApp.pipelineTemplateNames
289+
nodeCategories["Pipelines"] = MeshroomApp.pipelineTemplateNames
267290

268-
return categories
291+
return nodeCategories
269292
}
270293

271294
onVisibleChanged: {

0 commit comments

Comments
 (0)