Skip to content

Commit

Permalink
[ui] Plugins: Added UI for Node Plugin Manager
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
waaake committed Nov 11, 2024
1 parent f1105c3 commit 253b32c
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 20 deletions.
10 changes: 5 additions & 5 deletions meshroom/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
from PySide2.QtWidgets import QApplication

import meshroom
from meshroom.core import pluginManager
from meshroom.core.taskManager import TaskManager
from meshroom.common import Property, Variant, Signal, Slot

from meshroom.ui import components
from meshroom.ui.plugins import NodesPluginManager
from meshroom.ui.components.clipboard import ClipboardHelper
from meshroom.ui.components.filepath import FilepathHelper
from meshroom.ui.components.scene3D import Scene3DHelper, Transformations3DHelper
Expand Down Expand Up @@ -240,16 +240,16 @@ def __init__(self, args):
self.engine.addImportPath(qmlDir)
components.registerTypes()

# expose available node types that can be instantiated
nodesDesc = pluginManager.descriptors
self.engine.rootContext().setContextProperty("_nodeTypes", {n: {"category": nodesDesc[n].category} for n in sorted(nodesDesc.keys())})

# instantiate Reconstruction object
self._undoStack = commands.UndoStack(self)
self._taskManager = TaskManager(self)
self._activeProject = Reconstruction(undoStack=self._undoStack, taskManager=self._taskManager, defaultPipeline=args.pipeline, parent=self)
self._activeProject.setSubmitLabel(args.submitLabel)

# The Plugin manager for UI to communicate with
self._pluginManager = NodesPluginManager(parent=self)
self.engine.rootContext().setContextProperty("_reconstruction", self._activeProject)
self.engine.rootContext().setContextProperty("_pluginator", self._pluginManager)

# those helpers should be available from QML Utils module as singletons, but:
# - qmlRegisterUncreatableType is not yet available in PySide2
Expand Down
120 changes: 120 additions & 0 deletions meshroom/ui/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
""" UI Component for the Plugin System.
"""
# STD
import urllib.parse as _parser

# Qt
from PySide2.QtCore import Slot, QObject, Property, Signal

# Internal
from meshroom.core import pluginManager
from meshroom.common import BaseObject, DictModel


class Plugin(BaseObject):
""" Representation of a Plugin in UI.
"""

def __init__(self, descriptor):
""" Constructor.
Args:
descriptor (NodeDescriptor): A Plugin descriptor.
"""
super().__init__()

self._descriptor = descriptor

# Any Node errors
self._nodeErrors = self._errors()

def _errors(self) -> str:
"""
"""
if not self._descriptor.errors:
return ""

errors = ["Following parameters have invalid default values/ranges:"]

# Add the parameters from the node Errors
errors.extend([f"* Param {param}" for param in self._descriptor.errors])

return "\n".join(errors)

@Slot()
def reload(self):
""" Reloads the plugin descriptor.
"""
self._descriptor.reload()

# Update the Node errors
self._nodeErrors = self._errors()

name = Property(str, lambda self: self._descriptor.name, constant=True)
documentation = Property(str, lambda self: self._descriptor.documentation, constant=True)
loaded = Property(bool, lambda self: bool(self._descriptor.status), constant=True)
version = Property(str, lambda self: self._descriptor.version, constant=True)
path = Property(str, lambda self: self._descriptor.path, constant=True)
errors = Property(str, lambda self: self._nodeErrors, constant=True)
category = Property(str, lambda self: self._descriptor.category, constant=True)



class NodesPluginManager(QObject):
""" UI Plugin Manager Component. Serves as a Bridge between the core Nodes' Plugin Manager and how the
users interact with it.
"""

def __init__(self, parent=None):
""" Constructor.
Keyword Args:
parent (QObject): The Parent for the Plugin Manager.
"""
super().__init__(parent=parent)

# The core Plugin Manager
self._manager = pluginManager

# The plugins as a Model which can be communicated to the frontend
self._plugins = DictModel(keyAttrName='name', parent=self)

# Reset the plugins model
self._reset()

# Signals
pluginsChanged = Signal()

# Properties
plugins = Property(BaseObject, lambda self: self._plugins, notify=pluginsChanged)

# Protected
def _reset(self):
""" Requeries and Resets the Plugins Model from the core Plugin Manager for UI refreshes.
"""
plugins = [Plugin(desc) for desc in self._manager.descriptors.values()]

# Reset the plugins model
self._plugins.reset(plugins)

# Public
@Slot(str)
def load(self, directory):
""" Load plugins from a given directory, which serves as a package of Meshroom Node Modules.
Args:
directory (str): Path to the plugin package to import.
"""
# The incoming directory to this method from the QML FolderDialog component is of the format
# file:///path/to/a/python/package
# Cleanup the provided directory url and convert to a usable Posix path
uri = _parser.urlparse(directory)

# Load the plugin(s) from the provided directory package
self._manager.load(_parser.unquote(uri.path))

# Reset the plugins model
self._reset()

# Emit that the plugins have now been updated
self.pluginsChanged.emit()
32 changes: 31 additions & 1 deletion meshroom/ui/qml/Application.qml
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,17 @@ Page {
uigraph: _reconstruction
}

PluginManager {
id: pluginManager
manager: _pluginator

// When a plugin package has been browsed
onBrowsed: {
// Load Plugins
_pluginator.load(directory)
}
}


// Actions
Action {
Expand Down Expand Up @@ -923,6 +934,25 @@ Page {
border.color: Qt.darker(activePalette.window, 1.15)
}
}

// Button to Launch Plugin Manager
ToolButton {
id: pluginManagerButton
visible: true
text: MaterialIcons.build
font.family: MaterialIcons.fontFamily
font.pointSize: 12
onClicked: {
pluginManager.open()
}
ToolTip.text: "Plugin Manager"
ToolTip.visible: hovered

background: Rectangle {
color: pluginManagerButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
border.color: Qt.darker(activePalette.window, 1.15)
}
}
}

footer: ToolBar {
Expand Down Expand Up @@ -1156,7 +1186,7 @@ Page {
visible: graphEditorPanel.currentTab === 0

uigraph: _reconstruction
nodeTypesModel: _nodeTypes
nodeTypesModel: _pluginator.plugins

onNodeDoubleClicked: {
_reconstruction.setActiveNode(node);
Expand Down
51 changes: 37 additions & 14 deletions meshroom/ui/qml/GraphEditor/GraphEditor.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,40 @@ Item {
property variant uigraph: null /// Meshroom ui graph (UIGraph)
readonly property variant graph: uigraph ? uigraph.graph : null /// core graph contained in ui graph
property variant nodeTypesModel: null /// the list of node types that can be instantiated

readonly property var nodeCategories: {
// Map to hold the node category: node type
let categories = {}

for (var i = 0; i < nodeTypesModel.count; i++) {
// The node at the index
let node = nodeTypesModel.at(i)

// Node Category
let category = node.category;

// Setup an array to allow node type(s) to be added to the category
if (categories[category] === undefined) {
categories[category] = []
}
// Add the nodeType to the category which will show up in the Menu
categories[category].push(node.name)
}

return categories
}

readonly property var nodeTypes: {
// An array to hold the node Types
let types = []

for (var i = 0; i < nodeTypesModel.count; i++) {
types.push(nodeTypesModel.at(i).name)
}

return types
}

property real maxZoom: 2.0
property real minZoom: 0.1

Expand Down Expand Up @@ -223,7 +257,7 @@ Item {
Menu {
id: newNodeMenu
property point spawnPosition
property variant menuKeys: Object.keys(root.nodeTypesModel).concat(Object.values(MeshroomApp.pipelineTemplateNames))
property variant menuKeys: nodeTypes.concat(Object.values(MeshroomApp.pipelineTemplateNames))
height: searchBar.height + nodeMenuRepeater.height + instantiator.height

function createNode(nodeType) {
Expand Down Expand Up @@ -251,21 +285,10 @@ Item {
}

function parseCategories() {
// Organize nodes based on their category
// {"category1": ["node1", "node2"], "category2": ["node3", "node4"]}
let categories = {};
for (const [name, data] of Object.entries(root.nodeTypesModel)) {
let category = data["category"];
if (categories[category] === undefined) {
categories[category] = []
}
categories[category].push(name)
}

// Add a "Pipelines" category, filled with the list of templates to create pipelines from the menu
categories["Pipelines"] = MeshroomApp.pipelineTemplateNames
nodeCategories["Pipelines"] = MeshroomApp.pipelineTemplateNames

return categories
return nodeCategories
}

onVisibleChanged: {
Expand Down
Loading

0 comments on commit 253b32c

Please sign in to comment.