diff --git a/NodeGraphQt/__init__.py b/NodeGraphQt/__init__.py index 601a8897..aa004d71 100644 --- a/NodeGraphQt/__init__.py +++ b/NodeGraphQt/__init__.py @@ -80,14 +80,14 @@ def __init__(self): '"NodeGraphQt.vendor.Qt ({})"'.format(qtpy_ver)) from .base.graph import NodeGraph -from .base.menu import Menu, MenuCommand +from .base.menu import NodesMenu, NodeGraphMenu, NodeGraphCommand from .base.node import NodeObject, BaseNode, BackdropNode from .base.port import Port from .pkg_info import __version__ as VERSION from .pkg_info import __license__ as LICENSE # functions -from .base.actions import setup_context_menu +from .base.utils import setup_context_menu # widgets from .widgets.node_tree import NodeTreeWidget @@ -95,7 +95,19 @@ def __init__(self): __version__ = VERSION __all__ = [ - 'BackdropNode', 'BaseNode', 'LICENSE', 'Menu', 'MenuCommand', 'NodeGraph', - 'NodeObject', 'NodeTreeWidget', 'Port', 'PropertiesBinWidget', 'VERSION', - 'constants', 'setup_context_menu' + 'BackdropNode', + 'BaseNode', + 'LICENSE', + 'NodeGraph', + 'NodeGraphCommand', + 'NodeGraphMenu', + 'NodeObject', + 'NodeTreeWidget', + 'NodesMenu', + 'Port', + 'PropertiesBinWidget', + 'VERSION', + 'constants', + 'setup_context_menu' ] + diff --git a/NodeGraphQt/base/graph.py b/NodeGraphQt/base/graph.py index e47cc40f..5d8335fb 100644 --- a/NodeGraphQt/base/graph.py +++ b/NodeGraphQt/base/graph.py @@ -10,7 +10,7 @@ NodeMovedCmd, PortConnectedCmd) from NodeGraphQt.base.factory import NodeFactory -from NodeGraphQt.base.menu import Menu +from NodeGraphQt.base.menu import NodeGraphMenu, NodesMenu from NodeGraphQt.base.model import NodeGraphModel from NodeGraphQt.base.node import NodeObject from NodeGraphQt.base.port import Port @@ -391,23 +391,75 @@ def end_undo(self): def context_menu(self): """ - Returns the node graph root context menu object. + Returns the main context menu from the node graph. + + Note: + This is a convenience function to + :meth:`NodeGraphQt.NodeGraph.get_context_menu` + with the arg ``menu="graph"`` + + Returns: + NodeGraphQt.NodeGraphMenu: context menu object. + """ + return self.get_context_menu('graph') + + def context_nodes_menu(self): + """ + Returns the main context menu for the nodes. + + Note: + This is a convenience function to + :meth:`NodeGraphQt.NodeGraph.get_context_menu` + with the arg ``menu="nodes"`` Returns: - Menu: context menu object. + NodeGraphQt.NodesMenu: context menu object. """ - return Menu(self._viewer, self._viewer.context_menu()) + return self.get_context_menu('nodes') - def disable_context_menu(self, disabled=True): + def get_context_menu(self, menu): """ - Disable/Enable node graph context menu. + Returns the context menu specified by the name. + + Menu types: + "graph" - context menu from the node graph. + "nodes" - context menu for the nodes. + + Args: + menu (str): menu name. + + Returns: + NodeGraphMenu or NodesMenu: context menu object. + """ + menus = self._viewer.context_menus() + if menus.get(menu): + if menu == 'graph': + return NodeGraphMenu(self, menus[menu]) + elif menu == 'nodes': + return NodesMenu(self, menus[menu]) + + def disable_context_menu(self, disabled=True, name='all'): + """ + Disable/Enable context menus from the node graph. + + Menu Types: + - ``"all"`` all context menus from the node graph. + - ``"graph"`` context menu from the node graph. + - ``"nodes"`` context menu for the nodes. Args: disabled (bool): true to enable context menu. + name (str): menu name. (default: ``"all"``) """ - menu = self._viewer.context_menu() - menu.setDisabled(disabled) - menu.setVisible(not disabled) + if name == 'all': + for k, menu in self._viewer.context_menus().items(): + menu.setDisabled(disabled) + menu.setVisible(not disabled) + return + menus = self._viewer.context_menus() + if menus.get(name): + menus[name].setDisabled(disabled) + menus[name].setVisible(not disabled) def acyclic(self): """ diff --git a/NodeGraphQt/base/menu.py b/NodeGraphQt/base/menu.py index 700a58fc..440d31b4 100644 --- a/NodeGraphQt/base/menu.py +++ b/NodeGraphQt/base/menu.py @@ -1,26 +1,45 @@ #!/usr/bin/python from distutils.version import LooseVersion -from NodeGraphQt import QtGui, QtCore, QtWidgets -from NodeGraphQt.widgets.stylesheet import STYLE_QMENU +from NodeGraphQt import QtGui, QtCore +from NodeGraphQt.errors import NodeMenuError +from NodeGraphQt.widgets.actions import BaseMenu, GraphAction, NodeAction -class Menu(object): +class NodeGraphMenu(object): """ - base class for a menu item. + The ``NodeGraphMenu`` is the context menu triggered from the node graph. + + example to accessing the node graph context menu. + + .. code-block:: python + :linenos: + + from NodeGraphQt import NodeGraph + + node_graph = NodeGraph() + + # get the context menu for the node graph. + context_menu = node_graph.get_context_menu('graph') """ - def __init__(self, viewer, qmenu): - self.__viewer = viewer - self.__qmenu = qmenu + def __init__(self, graph, qmenu): + self._graph = graph + self._qmenu = qmenu def __repr__(self): - cls_name = self.__class__.__name__ - return '<{}("{}") object at {}>'.format(cls_name, self.name(), hex(id(self))) + return '<{}("{}") object at {}>'.format( + self.__class__.__name__, self.name(), hex(id(self))) @property def qmenu(self): - return self.__qmenu + """ + The underlying qmenu. + + Returns: + BaseMenu: qmenu object. + """ + return self._qmenu def name(self): """ @@ -39,11 +58,11 @@ def get_menu(self, name): name (str): name of the menu. Returns: - NodeGraphQt.Menu: menu item. + NodeGraphQt.NodeGraphMenu: menu item. """ - for action in self.qmenu.actions(): - if action.menu() and action.menu().title() == name: - return Menu(self.__viewer, action.menu()) + menu = self.qmenu.get_menu(name) + if menu: + return NodeGraphMenu(self._graph, menu) def get_command(self, name): """ @@ -57,7 +76,7 @@ def get_command(self, name): """ for action in self.qmenu.actions(): if not action.menu() and action.text() == name: - return MenuCommand(self.__viewer, action) + return NodeGraphCommand(self._graph, action) def all_commands(self): """ @@ -76,7 +95,7 @@ def get_actions(menu): actions += get_actions(action.menu()) return actions child_actions = get_actions(self.qmenu) - return [MenuCommand(self.__viewer, a) for a in child_actions] + return [NodeGraphCommand(self._graph, a) for a in child_actions] def add_menu(self, name): """ @@ -86,12 +105,11 @@ def add_menu(self, name): name (str): menu name. Returns: - NodeGraphQt.Menu: the appended menu item. + NodeGraphQt.NodeGraphMenu: the appended menu item. """ - menu = QtWidgets.QMenu(name, self.qmenu) - menu.setStyleSheet(STYLE_QMENU) + menu = BaseMenu(name, self.qmenu) self.qmenu.addMenu(menu) - return Menu(self.__viewer, menu) + return NodeGraphMenu(self._graph, menu) def add_command(self, name, func=None, shortcut=None): """ @@ -99,21 +117,22 @@ def add_command(self, name, func=None, shortcut=None): Args: name (str): command name. - func (function): command function. - shortcut (str): function shotcut key. + func (function): command function eg. "func(``graph``)". + shortcut (str): shotcut key. Returns: NodeGraphQt.MenuCommand: the appended command. """ - action = QtWidgets.QAction(name, self.__viewer) + action = GraphAction(name, self._graph.viewer()) + action.graph = self._graph if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): action.setShortcutVisibleInContextMenu(True) if shortcut: action.setShortcut(shortcut) if func: - action.triggered.connect(func) + action.executed.connect(func) qaction = self.qmenu.addAction(action) - return MenuCommand(self.__viewer, qaction) + return NodeGraphCommand(self._graph, qaction) def add_separator(self): """ @@ -122,22 +141,104 @@ def add_separator(self): self.qmenu.addSeparator() -class MenuCommand(object): +class NodesMenu(NodeGraphMenu): + """ + The ``NodesMenu`` is the context menu triggered from the nodes. + + **Inherited from:** :class:`NodeGraphQt.NodeGraphMenu` + + example for adding a command to the nodes context menu. + + .. code-block:: python + :linenos: + + from NodeGraphQt import BaseNode, NodeGraph + + # example node. + class MyNode(BaseNode): + + __identifier__ = 'com.chantasticvfx' + NODE_NAME = 'my node' + + def __init__(self): + super(MyNode, self).__init__() + self.add_input('in') + self.add_output('out') + + # create node graph. + node_graph = NodeGraph() + + # register example node. + node_graph.register_node(MyNode) + + # get the context menu for the nodes. + nodes_menu = node_graph.get_context_menu('nodes') + + # create a command + def test_func(graph, node): + print('Clicked on node: {}'.format(node.name())) + + nodes_menu.add_command('test', + func=test_func, + node_type='com.chantasticvfx.MyNode') + """ - base class for a menu command. + + def add_command(self, name, func=None, node_type=None): + """ + Re-implemented to add a command to the specified node type menu. + + Args: + name (str): command name. + func (function): command function eg. "func(``graph``, ``node``)". + node_type (str): specified node type for the command. + + Returns: + NodeGraphQt.MenuCommand: the appended command. + """ + if not node_type: + raise NodeMenuError('Node type not specified!') + + node_menu = self.qmenu.get_menu(node_type) + if not node_menu: + node_menu = BaseMenu(node_type, self.qmenu) + self.qmenu.addMenu(node_menu) + + if not self.qmenu.isEnabled(): + self.qmenu.setDisabled(False) + + action = NodeAction(name, self._graph.viewer()) + action.graph = self._graph + if LooseVersion(QtCore.qVersion()) >= LooseVersion('5.10'): + action.setShortcutVisibleInContextMenu(True) + if func: + action.executed.connect(func) + qaction = node_menu.addAction(action) + return NodeGraphCommand(self._graph, qaction) + + +class NodeGraphCommand(object): + """ + Node graph menu command. """ - def __init__(self, viewer, qaction): - self.__viewer = viewer - self.__qaction = qaction + def __init__(self, graph, qaction): + self._graph = graph + self._qaction = qaction def __repr__(self): - cls_name = self.__class__.__name__ - return 'NodeGraphQt.{}(\'{}\')'.format(cls_name, self.name()) + return '<{}("{}") object at {}>'.format( + self.__class__.__name__, self.name(), hex(id(self))) @property def qaction(self): - return self.__qaction + """ + The underlying qaction. + + Returns: + BaseAction: qaction object. + """ + return self._qaction def name(self): """ diff --git a/NodeGraphQt/base/actions.py b/NodeGraphQt/base/utils.py similarity index 64% rename from NodeGraphQt/base/actions.py rename to NodeGraphQt/base/utils.py index 1284a39d..2d384f64 100644 --- a/NodeGraphQt/base/actions.py +++ b/NodeGraphQt/base/utils.py @@ -8,31 +8,33 @@ def setup_context_menu(graph): """ Sets up the node graphs context menu with some basic menus and commands. + .. code-block:: python + :linenos: + + from NodeGraphQt import NodeGraph, setup_context_menu + + graph = NodeGraph() + setup_context_menu(graph) + Args: graph (NodeGraphQt.NodeGraph): node graph. """ - root_menu = graph.context_menu() + root_menu = graph.get_context_menu('graph') file_menu = root_menu.add_menu('&File') edit_menu = root_menu.add_menu('&Edit') # create "File" menu. - file_menu.add_command('Open...', - lambda: _open_session(graph), - QtGui.QKeySequence.Open) - file_menu.add_command('Save...', - lambda: _save_session(graph), - QtGui.QKeySequence.Save) - file_menu.add_command('Save As...', - lambda: _save_session_as(graph), - 'Ctrl+Shift+s') - file_menu.add_command('Clear', lambda: _clear_session(graph)) + file_menu.add_command('Open...', _open_session, QtGui.QKeySequence.Open) + file_menu.add_command('Save...', _save_session, QtGui.QKeySequence.Save) + file_menu.add_command('Save As...', _save_session_as, 'Ctrl+Shift+s') + file_menu.add_command('Clear', _clear_session) file_menu.add_separator() - file_menu.add_command('Zoom In', lambda: _zoom_in(graph), '=') - file_menu.add_command('Zoom Out', lambda: _zoom_out(graph), '-') - file_menu.add_command('Reset Zoom', graph.reset_zoom, 'h') + file_menu.add_command('Zoom In', _zoom_in, '=') + file_menu.add_command('Zoom Out', _zoom_out, '-') + file_menu.add_command('Reset Zoom', _reset_zoom, 'h') # create "Edit" menu. undo_actn = graph.undo_stack().createUndoAction(graph.viewer(), '&Undo') @@ -48,29 +50,21 @@ def setup_context_menu(graph): edit_menu.qmenu.addAction(redo_actn) edit_menu.add_separator() - edit_menu.add_command('Clear Undo History', lambda: _clear_undo(graph)) + edit_menu.add_command('Clear Undo History', _clear_undo) edit_menu.add_separator() - edit_menu.add_command('Copy', graph.copy_nodes, QtGui.QKeySequence.Copy) - edit_menu.add_command('Paste', graph.paste_nodes, QtGui.QKeySequence.Paste) - edit_menu.add_command('Delete', - lambda: graph.delete_nodes(graph.selected_nodes()), - QtGui.QKeySequence.Delete) + edit_menu.add_command('Copy', _copy_nodes, QtGui.QKeySequence.Copy) + edit_menu.add_command('Paste', _paste_nodes, QtGui.QKeySequence.Paste) + edit_menu.add_command('Delete', _delete_nodes, QtGui.QKeySequence.Delete) edit_menu.add_separator() - edit_menu.add_command('Select all', graph.select_all, 'Ctrl+A') - edit_menu.add_command('Deselect all', graph.clear_selection, 'Ctrl+Shift+A') - edit_menu.add_command('Enable/Disable', - lambda: graph.disable_nodes(graph.selected_nodes()), - 'd') + edit_menu.add_command('Select all', _select_all_nodes, 'Ctrl+A') + edit_menu.add_command('Deselect all', _clear_node_selection, 'Ctrl+Shift+A') + edit_menu.add_command('Enable/Disable', _disable_nodes, 'd') - edit_menu.add_command('Duplicate', - lambda: graph.duplicate_nodes(graph.selected_nodes()), - 'Alt+c') - edit_menu.add_command('Center Selection', - graph.fit_to_selection, - 'f') + edit_menu.add_command('Duplicate', _duplicate_nodes, 'Alt+c') + edit_menu.add_command('Center Selection', _fit_to_selection, 'f') edit_menu.add_separator() @@ -100,6 +94,10 @@ def _zoom_out(graph): graph.set_zoom(zoom) +def _reset_zoom(graph): + graph.reset_zoom() + + def _open_session(graph): """ Prompts a file open dialog to load a session. @@ -168,3 +166,35 @@ def _clear_undo(graph): msg = 'Clear all undo history, Are you sure?' if viewer.question_dialog('Clear Undo History', msg): graph.undo_stack().clear() + + +def _copy_nodes(graph): + graph.copy_nodes() + + +def _paste_nodes(graph): + graph.paste_nodes() + + +def _delete_nodes(graph): + graph.delete_nodes(graph.selected_nodes()) + + +def _select_all_nodes(graph): + graph.select_all() + + +def _clear_node_selection(graph): + graph.clear_selection() + + +def _disable_nodes(graph): + graph.disable_nodes(graph.selected_nodes()) + + +def _duplicate_nodes(graph): + graph.duplicate_nodes(graph.selected_nodes()) + + +def _fit_to_selection(graph): + graph.fit_to_selection() diff --git a/NodeGraphQt/errors.py b/NodeGraphQt/errors.py index 0a557e0b..e88e5956 100644 --- a/NodeGraphQt/errors.py +++ b/NodeGraphQt/errors.py @@ -1,18 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - - -class NodePropertyError(Exception): - pass - - -class NodeWidgetError(Exception): - pass - - -class NodeRegistrationError(Exception): - pass - - -class PortRegistrationError(Exception): - pass +class NodeMenuError(Exception): pass +class NodePropertyError(Exception): pass +class NodeWidgetError(Exception): pass +class NodeRegistrationError(Exception): pass +class PortRegistrationError(Exception): pass diff --git a/NodeGraphQt/widgets/actions.py b/NodeGraphQt/widgets/actions.py new file mode 100644 index 00000000..d1c1e6fd --- /dev/null +++ b/NodeGraphQt/widgets/actions.py @@ -0,0 +1,57 @@ +#!/usr/bin/python +from NodeGraphQt import QtCore, QtWidgets +from NodeGraphQt.widgets.stylesheet import STYLE_QMENU + + +class BaseMenu(QtWidgets.QMenu): + + def __init__(self, *args, **kwargs): + super(BaseMenu, self).__init__(*args, **kwargs) + self.setStyleSheet(STYLE_QMENU) + + def hideEvent(self, event): + super(BaseMenu, self).hideEvent(event) + for a in self.actions(): + if hasattr(a, 'node_id'): + a.node_id = None + + def get_menu(self, name): + for action in self.actions(): + if action.menu() and action.menu().title() == name: + return action.menu() + + +class GraphAction(QtWidgets.QAction): + + executed = QtCore.Signal(object) + + def __init__(self, *args, **kwargs): + super(GraphAction, self).__init__(*args, **kwargs) + self.graph = None + self.triggered.connect(self._on_triggered) + + def _on_triggered(self): + self.executed.emit(self.graph) + + def get_action(self, name): + for action in self.qmenu.actions(): + if not action.menu() and action.text() == name: + return action + + +class NodeAction(GraphAction): + + executed = QtCore.Signal(object, object) + + def __init__(self, *args, **kwargs): + super(NodeAction, self).__init__(*args, **kwargs) + self.node_id = None + + def _on_triggered(self): + node = self.graph.get_node_by_id(self.node_id) + self.executed.emit(self.graph, node) + + def get_action(self, name): + for action in self.qmenu.actions(): + if not action.menu() and action.text() == name: + return action diff --git a/NodeGraphQt/widgets/viewer.py b/NodeGraphQt/widgets/viewer.py index 5c7bbc49..e2381c01 100644 --- a/NodeGraphQt/widgets/viewer.py +++ b/NodeGraphQt/widgets/viewer.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -import os import math +import os from NodeGraphQt import QtGui, QtCore, QtWidgets from NodeGraphQt.constants import (IN_PORT, OUT_PORT, @@ -12,8 +12,8 @@ from NodeGraphQt.qgraphics.pipe import Pipe, LivePipe from NodeGraphQt.qgraphics.port import PortItem from NodeGraphQt.qgraphics.slicer import SlicerPipe +from NodeGraphQt.widgets.actions import BaseMenu from NodeGraphQt.widgets.scene import NodeScene -from NodeGraphQt.widgets.stylesheet import STYLE_QMENU from NodeGraphQt.widgets.tab_search import TabSearchWidget ZOOM_MIN = -0.95 @@ -71,8 +71,6 @@ def __init__(self, parent=None): self.scene().addItem(self._SLICER_PIPE) self._undo_stack = QtWidgets.QUndoStack(self) - self._context_menu = QtWidgets.QMenu('main', self) - self._context_menu.setStyleSheet(STYLE_QMENU) self._search_widget = TabSearchWidget(self) self._search_widget.search_submitted.connect(self._on_search_submitted) @@ -82,7 +80,13 @@ def __init__(self, parent=None): menu_bar.setNativeMenuBar(False) # shortcuts don't work with "setVisibility(False)". menu_bar.resize(0, 0) - menu_bar.addMenu(self._context_menu) + + self._ctx_menu = BaseMenu('NodeGraph', self) + self._ctx_node_menu = BaseMenu('Nodes', self) + menu_bar.addMenu(self._ctx_menu) + menu_bar.addMenu(self._ctx_node_menu) + + self._ctx_node_menu.setDisabled(True) self.acyclic = True self.LMB_state = False @@ -153,8 +157,23 @@ def resizeEvent(self, event): def contextMenuEvent(self, event): self.RMB_state = False - if self._context_menu.isEnabled(): - self._context_menu.exec_(event.globalPos()) + ctx_menu = None + + if self._ctx_node_menu.isEnabled(): + pos = self.mapToScene(self._previous_pos) + items = self._items_near(pos) + nodes = [i for i in items if isinstance(i, AbstractNodeItem)] + if nodes: + node = nodes[0] + ctx_menu = self._ctx_node_menu.get_menu(node.type_) + if ctx_menu: + for action in ctx_menu.actions(): + if not action.menu(): + action.node_id = node.id + + ctx_menu = ctx_menu or self._ctx_menu + if ctx_menu.isEnabled(): + ctx_menu.exec_(event.globalPos()) else: return super(NodeViewer, self).contextMenuEvent(event) @@ -589,8 +608,9 @@ def tab_search_toggle(self): self._search_widget.setVisible(state) self.clearFocus() - def context_menu(self): - return self._context_menu + def context_menus(self): + return {'graph': self._ctx_menu, + 'nodes': self._ctx_node_menu} def question_dialog(self, text, title='Node Graph'): dlg = QtWidgets.QMessageBox.question( diff --git a/docs/menu.rst b/docs/menu.rst index 577f280f..e0ca7463 100644 --- a/docs/menu.rst +++ b/docs/menu.rst @@ -4,72 +4,63 @@ Menus .. image:: _images/menu_hotkeys.png :width: 50% -The ``NodeGraphQt.setup_context_menu`` has a built in function that'll populate the node graphs context menu a few -default menus and commands. -.. code-block:: python - :linenos: - - from NodeGraphQt import NodeGraph, setup_context_menu - - graph = NodeGraph() - setup_context_menu(graph) - -example adding "Foo" menu to the node graphs context menu. +Here's an example where we add a ``"Foo"`` menu and then a ``"Bar"`` command with +the function ``my_test()`` registered. .. code-block:: python :linenos: from NodeGraphQt import NodeGraph + # test function. + def my_test(graph): + selected_nodes = graph.selected_nodes() + print('Number of nodes selected: {}'.format(len(selected_nodes))) + # create node graph. graph = NodeGraph() # get the main context menu. - root_menu = graph.context_menu() + context_menu = node_graph.get_context_menu('graph') # add a menu called "Foo". - foo_menu = root_menu.add_menu('Foo') - -add "Bar" command to the "Foo" menu. - -.. code-block:: python - :linenos: - :lineno-start: 11 - - # test function. - def my_test(): - print('Hello World') + foo_menu = context_menu.add_menu('Foo') # add "Bar" command to the "Foo" menu. + # we also assign a short cut key "Shift+t" for this example. foo_menu.add_command('Bar', my_test, 'Shift+t') ---- +The ``NodeGraphQt.setup_context_menu`` is a built in function that'll populate +the node graphs context menu a few default menus and commands. + + .. autofunction:: NodeGraphQt.setup_context_menu :noindex: +Graph Menu +********** +The context menu triggered from the node graph. -Menu -**** +.. autoclass:: NodeGraphQt.NodeGraphMenu + :members: -Node graph menu. +Nodes Menu +********** ----- +The context menu triggered from a node. -.. autoclass:: NodeGraphQt.Menu +.. autoclass:: NodeGraphQt.NodesMenu :members: Command ******* -Node graph menu command. - ----- - -.. autoclass:: NodeGraphQt.MenuCommand +.. autoclass:: NodeGraphQt.NodeGraphCommand :members: