Tool System: Don't add duplicate keymap items
[blender.git] / release / scripts / startup / bl_ui / space_toolsystem_common.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20 import bpy
21 from bpy.types import (
22     Menu,
23 )
24
25 __all__ = (
26     "ToolSelectPanelHelper",
27 )
28
29
30 class ToolSelectPanelHelper:
31     """
32     Generic Class, can be used for any toolbar.
33
34     - keymap_prefix:
35       The text prefix for each key-map for this spaces tools.
36     - tools_all():
37       Returns all tools defined.
38     - tools_from_context(context):
39       Returns tools available in this context.
40
41     Each tool is a triplet:
42       ``(tool_name, manipulator_group_idname, keymap_actions)``
43     For a separator in the toolbar, use ``None``.
44
45       Where:
46       ``tool_name``
47         is the name to display in the interface.
48       ``manipulator_group_idname``
49         is an optional manipulator group to activate when the tool is set.
50       ``keymap_actions``
51         an optional triple of: ``(operator_id, operator_properties, keymap_item_args)``
52     """
53
54     @staticmethod
55     def _tool_is_group(tool):
56         return type(tool[0]) is not str
57
58     @staticmethod
59     def _tools_flatten(tools):
60         for item in tools:
61             if item is not None:
62                 if ToolSelectPanelHelper._tool_is_group(item):
63                     for sub_item in item:
64                         if sub_item is not None:
65                             yield sub_item
66                 else:
67                     yield item
68
69     @classmethod
70     def _tool_vars_from_def(cls, item):
71         text, mp_idname, actions = item
72         km, km_idname = (None, None) if actions is None else cls._tool_keymap[text]
73         return (km_idname, mp_idname)
74
75     @staticmethod
76     def _tool_vars_from_active_with_index(context):
77         workspace = context.workspace
78         return (
79             (workspace.tool_keymap or None, workspace.tool_manipulator_group or None),
80             workspace.tool_index,
81         )
82
83     @staticmethod
84     def _tool_vars_from_button_with_index(context):
85         props = context.button_operator
86         return (
87             (props.keymap or None or None, props.manipulator_group or None),
88             props.index,
89         )
90
91     @classmethod
92     def _km_actionmouse_simple(cls, kc, text, actions):
93
94         # standalone
95         def props_assign_recursive(rna_props, py_props):
96             for prop_id, value in py_props.items():
97                 if isinstance(value, dict):
98                     props_assign_recursive(getattr(rna_props, prop_id), value)
99                 else:
100                     setattr(rna_props, prop_id, value)
101
102         km_idname = cls.keymap_prefix + text
103         km = kc.keymaps.get(km_idname)
104         if km is not None:
105             return km, km_idname
106         km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW')
107         for op_idname, op_props_dict, kmi_kwargs in actions:
108             kmi = km.keymap_items.new(op_idname, **kmi_kwargs)
109             kmi_props = kmi.properties
110             if op_props_dict:
111                 props_assign_recursive(kmi.properties, op_props_dict)
112         return km, km_idname
113
114     @classmethod
115     def register(cls):
116         wm = bpy.context.window_manager
117
118         # XXX, should we be manipulating the user-keyconfig on load?
119         # Perhaps this should only add when keymap items don't already exist.
120         #
121         # This needs some careful consideration.
122         kc = wm.keyconfigs.user
123
124         # {tool_name: (keymap, keymap_idname, manipulator_group_idname), ...}
125         cls._tool_keymap = {}
126
127         # Track which tool-group was last used for non-active groups.
128         # Blender stores the active tool-group index.
129         #
130         # {tool_name_first: index_in_group, ...}
131         cls._tool_group_active = {}
132
133         # ignore in background mode
134         if kc is None:
135             return
136
137         for item in ToolSelectPanelHelper._tools_flatten(cls.tools_all()):
138             text, mp_idname, actions = item
139             if actions is not None:
140                 km, km_idname = cls._km_actionmouse_simple(kc, text, actions)
141                 cls._tool_keymap[text] = km, km_idname
142
143     def draw(self, context):
144         # XXX, this UI isn't very nice.
145         # We might need to create new button types for this.
146         # Since we probably want:
147         # - tool-tips that include multiple key shortcuts.
148         # - ability to click and hold to expose sub-tools.
149
150         workspace = context.workspace
151         tool_def_active, index_active = self._tool_vars_from_active_with_index(context)
152         layout = self.layout
153
154         for tool_items in self.tools_from_context(context):
155             if tool_items:
156                 col = layout.column(align=True)
157                 for item in tool_items:
158                     if item is None:
159                         col = layout.column(align=True)
160                         continue
161
162                     if self._tool_is_group(item):
163                         is_active = False
164                         i = 0
165                         for i, sub_item in enumerate(item):
166                             if sub_item is None:
167                                 continue
168                             tool_def = self._tool_vars_from_def(sub_item)
169                             is_active = (tool_def == tool_def_active)
170                             if is_active:
171                                 index = i
172                                 break
173                         del i, sub_item
174
175                         if is_active:
176                             # not ideal, write this every time :S
177                             self._tool_group_active[item[0][0]] = index
178                         else:
179                             index = self._tool_group_active.get(item[0][0], 0)
180
181                         item = item[index]
182                         use_menu = True
183                     else:
184                         index = -1
185                         use_menu = False
186
187                     tool_def = self._tool_vars_from_def(item)
188                     is_active = (tool_def == tool_def_active)
189
190                     if use_menu:
191                         props = col.operator_menu_hold(
192                             "wm.tool_set",
193                             text=item[0],
194                             depress=is_active,
195                             menu="WM_MT_toolsystem_submenu",
196                         )
197                     else:
198                         props = col.operator(
199                             "wm.tool_set",
200                             text=item[0],
201                             depress=is_active,
202                         )
203
204                     props.keymap = tool_def[0] or ""
205                     props.manipulator_group = tool_def[1] or ""
206                     props.index = index
207
208     def tools_from_context(cls, context):
209         return (cls._tools[None], cls._tools.get(context.mode, ()))
210
211
212 # The purpose of this menu is to be a generic popup to select between tools
213 # in cases when a single tool allows to select alternative tools.
214 class WM_MT_toolsystem_submenu(Menu):
215     bl_label = ""
216
217     @staticmethod
218     def _tool_group_from_button(context):
219         # Lookup the tool definitions based on the space-type.
220         space_type = context.space_data.type
221         cls = next(
222             (cls for cls in ToolSelectPanelHelper.__subclasses__()
223              if cls.bl_space_type == space_type),
224             None
225         )
226         if cls is not None:
227             tool_def_button, index_button = cls._tool_vars_from_button_with_index(context)
228
229             for item_items in cls.tools_from_context(context):
230                 for item_group in item_items:
231                     if (item_group is not None) and ToolSelectPanelHelper._tool_is_group(item_group):
232                         if index_button < len(item_group):
233                             item = item_group[index_button]
234                             tool_def = cls._tool_vars_from_def(item)
235                             is_active = (tool_def == tool_def_button)
236                             if is_active:
237                                 return cls, item_group, index_button
238         return None, None, -1
239
240     def draw(self, context):
241         layout = self.layout
242         cls, item_group, index_active = self._tool_group_from_button(context)
243         if item_group is None:
244             # Should never happen, just in case
245             layout.label("Unable to find toolbar group")
246             return
247
248         index = 0
249         for item in item_group:
250             if item is None:
251                 layout.separator()
252                 continue
253             tool_def = cls._tool_vars_from_def(item)
254             props = layout.operator(
255                 "wm.tool_set",
256                 text=item[0],
257             )
258             props.keymap = tool_def[0] or ""
259             props.manipulator_group = tool_def[1] or ""
260             props.index = index
261             index += 1
262
263
264 classes = (
265     WM_MT_toolsystem_submenu,
266 )
267
268 if __name__ == "__main__":  # only for live edit.
269     from bpy.utils import register_class
270     for cls in classes:
271         register_class(cls)