Skip to content

Commit 8ab7dcd

Browse files
committed
refactored context menu setup and support for deserializing JSON menu files. #254
1 parent eb86fa2 commit 8ab7dcd

File tree

4 files changed

+186
-90
lines changed

4 files changed

+186
-90
lines changed

NodeGraphQt/base/graph.py

Lines changed: 92 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,28 @@ def __init__(self, parent=None, **kwargs):
158158
kwargs.get('viewer') or NodeViewer(undo_stack=self._undo_stack))
159159
self._viewer.set_layout_direction(layout_direction)
160160

161+
self._context_menu = {}
162+
163+
self._register_context_menu()
161164
self._register_builtin_nodes()
162165
self._wire_signals()
163166

164167
def __repr__(self):
165168
return '<{}("root") object at {}>'.format(
166169
self.__class__.__name__, hex(id(self)))
167170

171+
def _register_context_menu(self):
172+
"""
173+
Register the default context menus.
174+
"""
175+
if not self._viewer:
176+
return
177+
menus = self._viewer.context_menus()
178+
if menus.get('graph'):
179+
self._context_menu['graph'] = NodeGraphMenu(self, menus['graph'])
180+
if menus.get('nodes'):
181+
self._context_menu['nodes'] = NodesMenu(self, menus['nodes'])
182+
168183
def _register_builtin_nodes(self):
169184
"""
170185
Register the default builtin nodes to the :meth:`NodeGraph.node_factory`
@@ -688,68 +703,62 @@ def get_context_menu(self, menu):
688703
Returns:
689704
NodeGraphQt.NodeGraphMenu or NodeGraphQt.NodesMenu: context menu object.
690705
"""
691-
menus = self._viewer.context_menus()
692-
if menus.get(menu):
693-
if menu == 'graph':
694-
return NodeGraphMenu(self, menus[menu])
695-
elif menu == 'nodes':
696-
return NodesMenu(self, menus[menu])
706+
return self._context_menu.get(menu)
697707

698-
@staticmethod
699-
def _build_context_menu_command(menu, data):
708+
def _deserialize_context_menu(self, menu, menu_data):
700709
"""
701-
Create context menu command from serialized data.
710+
Populate context menu from a dictionary.
702711
703712
Args:
704713
menu (NodeGraphQt.NodeGraphMenu or NodeGraphQt.NodesMenu):
705-
menu object.
706-
data (dict): serialized menu command data.
714+
parent context menu.
715+
menu_data (list[dict] or dict): serialized menu data.
707716
"""
717+
if not menu:
718+
raise ValueError('No context menu named: "{}"'.format(menu))
719+
708720
import sys
709721
import importlib.util
710722

711-
full_path = os.path.abspath(data['file'])
712-
base_dir, file_name = os.path.split(full_path)
713-
base_name = os.path.basename(base_dir)
714-
file_name, _ = file_name.split('.')
723+
def build_menu_command(menu, data):
724+
"""
725+
Create menu command from serialized data.
715726
716-
mod_name = '{}.{}'.format(base_name, file_name)
727+
Args:
728+
menu (NodeGraphQt.NodeGraphMenu or NodeGraphQt.NodesMenu):
729+
menu object.
730+
data (dict): serialized menu command data.
731+
"""
732+
full_path = os.path.abspath(data['file'])
733+
base_dir, file_name = os.path.split(full_path)
734+
base_name = os.path.basename(base_dir)
735+
file_name, _ = file_name.split('.')
717736

718-
spec = importlib.util.spec_from_file_location(mod_name, full_path)
719-
mod = importlib.util.module_from_spec(spec)
720-
sys.modules[mod_name] = mod
721-
spec.loader.exec_module(mod)
737+
mod_name = '{}.{}'.format(base_name, file_name)
722738

723-
cmd_func = getattr(mod, data['function_name'])
724-
cmd_name = data.get('label') or '<command>'
725-
cmd_shortcut = data.get('shortcut')
726-
menu.add_command(cmd_name, cmd_func, cmd_shortcut)
739+
spec = importlib.util.spec_from_file_location(mod_name, full_path)
740+
mod = importlib.util.module_from_spec(spec)
741+
sys.modules[mod_name] = mod
742+
spec.loader.exec_module(mod)
727743

728-
def _build_context_menu(self, menu, menu_data):
729-
"""
730-
Populate context menu from a dictionary.
731-
732-
Args:
733-
menu (NodeGraphQt.NodeGraphMenu or NodeGraphQt.NodesMenu):
734-
parent context menu.
735-
menu_data (list[dict] or dict): serialized menu data.
736-
"""
737-
if not menu:
738-
raise ValueError('No context menu named: "{}"'.format(menu))
744+
cmd_func = getattr(mod, data['function_name'])
745+
cmd_name = data.get('label') or '<command>'
746+
cmd_shortcut = data.get('shortcut')
747+
menu.add_command(cmd_name, cmd_func, cmd_shortcut)
739748

740749
if isinstance(menu_data, dict):
741750
item_type = menu_data.get('type')
742751
if item_type == 'separator':
743752
menu.add_separator()
744753
elif item_type == 'command':
745-
self._build_context_menu_command(menu, menu_data)
754+
build_menu_command(menu, menu_data)
746755
elif item_type == 'menu':
747756
sub_menu = menu.add_menu(menu_data['label'])
748757
items = menu_data.get('items', [])
749-
self._build_context_menu(sub_menu, items)
758+
self._deserialize_context_menu(sub_menu, items)
750759
elif isinstance(menu_data, list):
751760
for item_data in menu_data:
752-
self._build_context_menu(menu, item_data)
761+
self._deserialize_context_menu(menu, item_data)
753762

754763
def set_context_menu(self, menu_name, data):
755764
"""
@@ -782,7 +791,7 @@ def set_context_menu(self, menu_name, data):
782791
data (dict): serialized menu data.
783792
"""
784793
context_menu = self.get_context_menu(menu_name)
785-
self._build_context_menu(context_menu, data)
794+
self._deserialize_context_menu(context_menu, data)
786795

787796
def set_context_menu_from_file(self, file_path, menu=None):
788797
"""
@@ -803,8 +812,7 @@ def set_context_menu_from_file(self, file_path, menu=None):
803812
with open(file_path) as f:
804813
data = json.load(f)
805814
context_menu = self.get_context_menu(menu)
806-
self._build_context_menu(context_menu, data)
807-
815+
self._deserialize_context_menu(context_menu, data)
808816

809817
def disable_context_menu(self, disabled=True, name='all'):
810818
"""
@@ -2126,6 +2134,9 @@ def __init__(self, parent=None, node=None, node_factory=None, **kwargs):
21262134
del self._widget
21272135
del self._sub_graphs
21282136

2137+
# clone context menu from the parent node graph.
2138+
self._clone_context_menu_from_parent()
2139+
21292140
def __repr__(self):
21302141
return '<{}("{}") object at {}>'.format(
21312142
self.__class__.__name__, self._node.name(), hex(id(self)))
@@ -2136,6 +2147,46 @@ def _register_builtin_nodes(self):
21362147
"""
21372148
return
21382149

2150+
def _clone_context_menu_from_parent(self):
2151+
"""
2152+
Clone the context menus from the parent node graph.
2153+
"""
2154+
undo_regex = re.compile(r'^\&Undo(?:| \w+)')
2155+
redo_regex = re.compile(r'^\&Redo(?:| \w+)')
2156+
2157+
def clone_menu(menu, menu_to_clone):
2158+
"""
2159+
Args:
2160+
menu (NodeGraphQt.NodeGraphMenu):
2161+
menu_to_clone (NodeGraphQt.NodeGraphMenu):
2162+
"""
2163+
sub_items = []
2164+
for idx, item in enumerate(menu_to_clone.get_items()):
2165+
if item is None:
2166+
menu.add_separator()
2167+
continue
2168+
name = item.name()
2169+
if isinstance(item, NodeGraphMenu):
2170+
sub_menu = menu.add_menu(name)
2171+
sub_items.append([sub_menu, item])
2172+
continue
2173+
2174+
if idx <= 1:
2175+
if undo_regex.match(name) or redo_regex.match(name):
2176+
continue
2177+
menu.add_command(
2178+
name,
2179+
func=item.slot_function,
2180+
shortcut=item.qaction.shortcut()
2181+
)
2182+
2183+
for sub_menu, to_clone in sub_items:
2184+
clone_menu(sub_menu, to_clone)
2185+
2186+
graph_menu = self.get_context_menu('graph')
2187+
parent_menu = self.parent_graph.get_context_menu('graph')
2188+
clone_menu(graph_menu, parent_menu)
2189+
21392190
def _build_port_nodes(self):
21402191
"""
21412192
Build the corresponding input & output nodes from the parent node ports

NodeGraphQt/base/menu.py

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class NodeGraphMenu(object):
2929
def __init__(self, graph, qmenu):
3030
self._graph = graph
3131
self._qmenu = qmenu
32+
self._name = qmenu.title()
33+
self._menus = {}
34+
self._commands = {}
35+
self._items = []
3236

3337
def __repr__(self):
3438
return '<{}("{}") object at {}>'.format(
@@ -37,10 +41,10 @@ def __repr__(self):
3741
@property
3842
def qmenu(self):
3943
"""
40-
The underlying qmenu.
44+
The underlying QMenu.
4145
4246
Returns:
43-
BaseMenu: qmenu object.
47+
BaseMenu: menu object.
4448
"""
4549
return self._qmenu
4650

@@ -51,7 +55,16 @@ def name(self):
5155
Returns:
5256
str: label name.
5357
"""
54-
return self.qmenu.title()
58+
return self._name
59+
60+
def get_items(self):
61+
"""
62+
Return the menu items in the order they were added.
63+
64+
Returns:
65+
list: current menu items.
66+
"""
67+
return self._items
5568

5669
def get_menu(self, name):
5770
"""
@@ -63,9 +76,7 @@ def get_menu(self, name):
6376
Returns:
6477
NodeGraphQt.NodeGraphMenu: menu item.
6578
"""
66-
menu = self.qmenu.get_menu(name)
67-
if menu:
68-
return NodeGraphMenu(self._graph, menu)
79+
self._menus.get(name)
6980

7081
def get_command(self, name):
7182
"""
@@ -77,28 +88,7 @@ def get_command(self, name):
7788
Returns:
7889
NodeGraphQt.MenuCommand: context menu command.
7990
"""
80-
for action in self.qmenu.actions():
81-
if not action.menu() and action.text() == name:
82-
return NodeGraphCommand(self._graph, action)
83-
84-
def all_commands(self):
85-
"""
86-
Returns all child and sub child commands from the current context menu.
87-
88-
Returns:
89-
list[NodeGraphQt.MenuCommand]: list of commands.
90-
"""
91-
def get_actions(menu):
92-
actions = []
93-
for action in menu.actions():
94-
if not action.menu():
95-
if not action.isSeparator():
96-
actions.append(action)
97-
else:
98-
actions += get_actions(action.menu())
99-
return actions
100-
child_actions = get_actions(self.qmenu)
101-
return [NodeGraphCommand(self._graph, a) for a in child_actions]
91+
return self._commands.get(name)
10292

10393
def add_menu(self, name):
10494
"""
@@ -110,9 +100,14 @@ def add_menu(self, name):
110100
Returns:
111101
NodeGraphQt.NodeGraphMenu: the appended menu item.
112102
"""
113-
menu = BaseMenu(name, self.qmenu)
114-
self.qmenu.addMenu(menu)
115-
return NodeGraphMenu(self._graph, menu)
103+
if name in self._menus:
104+
raise NodeMenuError('menu object "{}" already exists!'.format(name))
105+
base_menu = BaseMenu(name, self.qmenu)
106+
self.qmenu.addMenu(base_menu)
107+
menu = NodeGraphMenu(self._graph, base_menu)
108+
self._menus[name] = menu
109+
self._items.append(menu)
110+
return menu
116111

117112
def add_command(self, name, func=None, shortcut=None):
118113
"""
@@ -121,7 +116,7 @@ def add_command(self, name, func=None, shortcut=None):
121116
Args:
122117
name (str): command name.
123118
func (function): command function eg. "func(``graph``)".
124-
shortcut (str): shotcut key.
119+
shortcut (str): shortcut key.
125120
126121
Returns:
127122
NodeGraphQt.NodeGraphCommand: the appended command.
@@ -146,14 +141,18 @@ def add_command(self, name, func=None, shortcut=None):
146141
action.setShortcut(shortcut)
147142
if func:
148143
action.executed.connect(func)
149-
qaction = self.qmenu.addAction(action)
150-
return NodeGraphCommand(self._graph, qaction)
144+
self.qmenu.addAction(action)
145+
command = NodeGraphCommand(self._graph, action, func)
146+
self._commands[name] = command
147+
self._items.append(command)
148+
return command
151149

152150
def add_separator(self):
153151
"""
154152
Adds a separator to the menu.
155153
"""
156154
self.qmenu.addSeparator()
155+
self._items.append(None)
157156

158157

159158
class NodesMenu(NodeGraphMenu):
@@ -222,17 +221,22 @@ def add_command(self, name, func=None, node_type=None, node_class=None):
222221
menu.addAction(action)
223222

224223
qaction = node_menu.addAction(action)
225-
return NodeGraphCommand(self._graph, qaction)
224+
command = NodeGraphCommand(self._graph, qaction, func)
225+
self._commands[name] = command
226+
self._items.append(command)
227+
return command
226228

227229

228230
class NodeGraphCommand(object):
229231
"""
230232
Node graph menu command.
231233
"""
232234

233-
def __init__(self, graph, qaction):
235+
def __init__(self, graph, qaction, func=None):
234236
self._graph = graph
235237
self._qaction = qaction
238+
self._name = qaction.text()
239+
self._func = func
236240

237241
def __repr__(self):
238242
return '<{}("{}") object at {}>'.format(
@@ -248,14 +252,24 @@ def qaction(self):
248252
"""
249253
return self._qaction
250254

255+
@property
256+
def slot_function(self):
257+
"""
258+
The function executed by this command.
259+
260+
Returns:
261+
function: command function.
262+
"""
263+
return self._func
264+
251265
def name(self):
252266
"""
253267
Returns the name for the menu command.
254268
255269
Returns:
256270
str: label name.
257271
"""
258-
return self.qaction.text()
272+
return self._name
259273

260274
def set_shortcut(self, shortcut=None):
261275
"""

0 commit comments

Comments
 (0)