a1a65a44750d056da6aeafa3341a52d95f25dbe1
[blender.git] / release / scripts / modules / bl_keymap_utils / keymap_from_toolbar.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
21 # Dynamically create a keymap which is used by the popup toolbar
22 # for accelerator key access.
23
24 __all__ = (
25     "generate",
26 )
27
28 def generate(context, space_type):
29     """
30     Keymap for popup toolbar, currently generated each time.
31     """
32     from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
33
34     def modifier_keywords_from_item(kmi):
35         kw = {}
36         for (attr, default) in (
37                 ("any", False),
38                 ("shift", False),
39                 ("ctrl", False),
40                 ("alt", False),
41                 ("oskey", False),
42                 ("key_modifier", 'NONE'),
43         ):
44             val = getattr(kmi, attr)
45             if val != default:
46                 kw[attr] = val
47         return kw
48
49     def dict_as_tuple(d):
50         return tuple((k, v) for (k, v) in sorted(d.items()))
51
52     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
53
54     items_all = [
55         # 0: tool
56         # 1: keymap item (direct access)
57         # 2: keymap item (newly calculated for toolbar)
58         [item, None, None]
59         for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context))
60         if item is not None
61     ]
62     items_all_text = {item_container[0].text for item_container in items_all}
63
64     # Press the toolbar popup key again to set the default tool,
65     # this is useful because the select box tool is useful as a way
66     # to 'drop' currently active tools (it's basically a 'none' tool).
67     # so this allows us to quickly go back to a state that allows
68     # a shortcut based workflow (before the tool system was added).
69     use_tap_reset = True
70     # TODO: support other tools for modes which don't use this tool.
71     tap_reset_tool = "Cursor"
72     # Check the tool is available in the current context.
73     if tap_reset_tool not in items_all_text:
74         use_tap_reset = False
75
76     from bl_operators.wm import use_toolbar_release_hack
77
78     # Pie-menu style release to activate.
79     use_release_confirm = True
80
81     # Generate items when no keys are mapped.
82     use_auto_keymap_alpha = False  # Map manially in the default keymap
83     use_auto_keymap_num = True
84
85     # Temporary, only create so we can pass 'properties' to find_item_from_operator.
86     use_hack_properties = True
87
88     km_name_default = "Toolbar Popup"
89     km_name = km_name_default + " <temp>"
90     wm = context.window_manager
91     keyconf = wm.keyconfigs.active
92     keymap = keyconf.keymaps.get(km_name)
93     if keymap is None:
94         keymap = keyconf.keymaps.new(km_name, space_type='EMPTY', region_type='TEMPORARY')
95     for kmi in keymap.keymap_items:
96         keymap.keymap_items.remove(kmi)
97
98     keymap_src = keyconf.keymaps.get(km_name_default)
99     if keymap_src is not None:
100         for kmi_src in keymap_src.keymap_items:
101             # Skip tools that aren't currently shown.
102             if (
103                     (kmi_src.idname == "wm.tool_set_by_name") and
104                     (kmi_src.properties.name not in items_all_text)
105             ):
106                 continue
107             keymap.keymap_items.new_from_item(kmi_src)
108     del keymap_src
109     del items_all_text
110
111
112     kmi_unique_args = set()
113
114     def kmi_unique_or_pass(kmi_args):
115         kmi_unique_len = len(kmi_unique_args)
116         kmi_unique_args.add(dict_as_tuple(kmi_args))
117         return kmi_unique_len != len(kmi_unique_args)
118
119
120     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
121
122     if use_hack_properties:
123         kmi_hack = keymap.keymap_items.new("wm.tool_set_by_name", 'NONE', 'PRESS')
124         kmi_hack_properties = kmi_hack.properties
125         kmi_hack.active = False
126
127         kmi_hack_brush_select = keymap.keymap_items.new("paint.brush_select", 'NONE', 'PRESS')
128         kmi_hack_brush_select_properties = kmi_hack_brush_select.properties
129         kmi_hack_brush_select.active = False
130
131     if use_release_confirm or use_tap_reset:
132         kmi_toolbar = wm.keyconfigs.find_item_from_operator(
133             idname="wm.toolbar",
134         )[1]
135         kmi_toolbar_type = None if not kmi_toolbar else kmi_toolbar.type
136         if use_tap_reset and kmi_toolbar_type is not None:
137             kmi_toolbar_args_type_only = {"type": kmi_toolbar_type}
138             kmi_toolbar_args = {**kmi_toolbar_args_type_only, **modifier_keywords_from_item(kmi_toolbar)}
139         else:
140             use_tap_reset = False
141         del kmi_toolbar
142
143     if use_tap_reset:
144         kmi_found = None
145         if use_hack_properties:
146             # First check for direct assignment, if this tool already has a key, no need to add a new one.
147             kmi_hack_properties.name = tap_reset_tool
148             kmi_found = wm.keyconfigs.find_item_from_operator(
149                 idname="wm.tool_set_by_name",
150                 context='INVOKE_REGION_WIN',
151                 # properties={"name": item.text},
152                 properties=kmi_hack_properties,
153                 include={'KEYBOARD'},
154             )[1]
155             if kmi_found:
156                 use_tap_reset = False
157         del kmi_found
158
159     if use_tap_reset:
160         use_tap_reset = kmi_unique_or_pass(kmi_toolbar_args)
161
162     if use_tap_reset:
163         items_all[:] = [
164             item_container
165             for item_container in items_all
166             if item_container[0].text != tap_reset_tool
167         ]
168
169     # -----------------------
170     # Begin Keymap Generation
171
172     # -------------------------------------------------------------------------
173     # Direct Tool Assignment & Brushes
174
175     for item_container in items_all:
176         item = item_container[0]
177         # Only check the first item in the tools key-map (a little arbitrary).
178         if use_hack_properties:
179             # First check for direct assignment.
180             kmi_hack_properties.name = item.text
181             kmi_found = wm.keyconfigs.find_item_from_operator(
182                 idname="wm.tool_set_by_name",
183                 context='INVOKE_REGION_WIN',
184                 # properties={"name": item.text},
185                 properties=kmi_hack_properties,
186                 include={'KEYBOARD'},
187             )[1]
188
189             if kmi_found is None:
190                 if item.data_block:
191                     # PAINT_OT_brush_select
192                     mode = context.active_object.mode
193                     # See: BKE_paint_get_tool_prop_id_from_paintmode
194                     attr = {
195                         'SCULPT': "sculpt_tool",
196                         'VERTEX_PAINT': "vertex_tool",
197                         'WEIGHT_PAINT': "weight_tool",
198                         'TEXTURE_PAINT': "image_tool",
199                         'PAINT_GPENCIL': "gpencil_tool",
200                     }.get(mode, None)
201                     if attr is not None:
202                         setattr(kmi_hack_brush_select_properties, attr, item.data_block)
203                         kmi_found = wm.keyconfigs.find_item_from_operator(
204                             idname="paint.brush_select",
205                             context='INVOKE_REGION_WIN',
206                             properties=kmi_hack_brush_select_properties,
207                             include={'KEYBOARD'},
208                         )[1]
209                     else:
210                         print("Unsupported mode:", mode)
211                     del mode, attr
212
213         else:
214             kmi_found = None
215
216         if kmi_found is not None:
217             pass
218         elif item.operator is not None:
219             kmi_found = wm.keyconfigs.find_item_from_operator(
220                 idname=item.operator,
221                 context='INVOKE_REGION_WIN',
222                 include={'KEYBOARD'},
223             )[1]
224         elif item.keymap is not None:
225             km = keyconf.keymaps.get(item.keymap[0])
226             if km is None:
227                 print("Keymap", repr(item.keymap[0]), "not found for tool", item.text)
228                 kmi_found = None
229             else:
230                 kmi_first = km.keymap_items
231                 kmi_first = kmi_first[0] if kmi_first else None
232                 if kmi_first is not None:
233                     kmi_found = wm.keyconfigs.find_item_from_operator(
234                         idname=kmi_first.idname,
235                         # properties=kmi_first.properties,  # prevents matches, don't use.
236                         context='INVOKE_REGION_WIN',
237                         include={'KEYBOARD'},
238                     )[1]
239                     if kmi_found is None:
240                         # We need non-keyboard events so keys with 'key_modifier' key is found.
241                         kmi_found = wm.keyconfigs.find_item_from_operator(
242                             idname=kmi_first.idname,
243                             # properties=kmi_first.properties,  # prevents matches, don't use.
244                             context='INVOKE_REGION_WIN',
245                             exclude={'KEYBOARD'},
246                         )[1]
247                         if kmi_found is not None:
248                             if kmi_found.key_modifier == 'NONE':
249                                 kmi_found = None
250                 else:
251                     kmi_found = None
252                 del kmi_first
253             del km
254         else:
255             kmi_found = None
256         item_container[1] = kmi_found
257
258     # -------------------------------------------------------------------------
259     # Single Key Access
260
261     # More complex multi-pass test.
262     for item_container in items_all:
263         item, kmi_found = item_container[:2]
264         if kmi_found is None:
265             continue
266         kmi_found_type = kmi_found.type
267
268         # Only for single keys.
269         if (
270                 (len(kmi_found_type) == 1) or
271                 # When a tool is being activated instead of running an operator, just copy the shortcut.
272                 (kmi_found.idname in {"wm.tool_set_by_name", "WM_OT_tool_set_by_name"})
273         ):
274             kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
275             if kmi_unique_or_pass(kmi_args):
276                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
277                 kmi.properties.name = item.text
278                 item_container[2] = kmi
279
280     # -------------------------------------------------------------------------
281     # Single Key Modifier
282     #
283     #
284     # Test for key_modifier, where alpha key is used as a 'key_modifier'
285     # (grease pencil holding 'D' for example).
286
287     for item_container in items_all:
288         item, kmi_found, kmi_exist = item_container
289         if kmi_found is None or kmi_exist:
290             continue
291
292         kmi_found_type = kmi_found.type
293         if kmi_found_type in {
294                 'LEFTMOUSE',
295                 'RIGHTMOUSE',
296                 'MIDDLEMOUSE',
297                 'BUTTON4MOUSE',
298                 'BUTTON5MOUSE',
299                 'BUTTON6MOUSE',
300                 'BUTTON7MOUSE',
301         }:
302             kmi_found_type = kmi_found.key_modifier
303             # excludes 'NONE'
304             if len(kmi_found_type) == 1:
305                 kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
306                 del kmi_args["key_modifier"]
307                 if kmi_unique_or_pass(kmi_args):
308                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
309                     kmi.properties.name = item.text
310                     item_container[2] = kmi
311
312     # -------------------------------------------------------------------------
313     # Assign A-Z to Keys
314     #
315     # When th keys are free.
316
317     if use_auto_keymap_alpha:
318         # Map all unmapped keys to numbers,
319         # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
320
321         # First map A-Z.
322         kmi_type_alpha_char = [chr(i) for i in range(65, 91)]
323         kmi_type_alpha_args = {c: {"type": c} for c in kmi_type_alpha_char}
324         kmi_type_alpha_args_tuple = {c: dict_as_tuple(kmi_type_alpha_args[c]) for c in kmi_type_alpha_char}
325         for item_container in items_all:
326             item, kmi_found, kmi_exist = item_container
327             if kmi_exist:
328                 continue
329             kmi_type = item.text[0].upper()
330             kmi_tuple = kmi_type_alpha_args_tuple.get(kmi_type)
331             if kmi_tuple and kmi_tuple not in kmi_unique_args:
332                 kmi_unique_args.add(kmi_tuple)
333                 kmi = keymap.keymap_items.new(
334                     idname="wm.tool_set_by_name",
335                     value='PRESS',
336                     **kmi_type_alpha_args[kmi_type],
337                 )
338                 kmi.properties.name = item.text
339                 item_container[2] = kmi
340         del kmi_type_alpha_char, kmi_type_alpha_args, kmi_type_alpha_args_tuple
341
342     # -------------------------------------------------------------------------
343     # Assign Numbers to Keys
344
345     if use_auto_keymap_num:
346         # Free events (last used first).
347         kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
348         # Map both numbers and num-pad.
349         kmi_type_dupe = {
350             'ONE': 'NUMPAD_1',
351             'TWO': 'NUMPAD_2',
352             'THREE': 'NUMPAD_3',
353             'FOUR': 'NUMPAD_4',
354             'FIVE': 'NUMPAD_5',
355             'SIX': 'NUMPAD_6',
356             'SEVEN': 'NUMPAD_7',
357             'EIGHT': 'NUMPAD_8',
358             'NINE': 'NUMPAD_9',
359             'ZERO': 'NUMPAD_0',
360         }
361
362         def iter_free_events():
363             for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
364                 for e in kmi_type_auto:
365                     yield (e, mod)
366
367         iter_events = iter(iter_free_events())
368
369         for item_container in items_all:
370             item, kmi_found, kmi_exist = item_container
371             if kmi_exist:
372                 continue
373             kmi_args = None
374             while True:
375                 key, mod = next(iter_events, (None, None))
376                 if key is None:
377                     break
378                 kmi_args = {"type": key, **mod}
379                 kmi_tuple = dict_as_tuple(kmi_args)
380                 if kmi_tuple in kmi_unique_args:
381                     kmi_args = None
382                 else:
383                     break
384
385             if kmi_args is not None:
386                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
387                 kmi.properties.name = item.text
388                 item_container[2] = kmi
389                 kmi_unique_args.add(kmi_tuple)
390
391                 key = kmi_type_dupe.get(kmi_args["type"])
392                 if key is not None:
393                     kmi_args["type"] = key
394                     kmi_tuple = dict_as_tuple(kmi_args)
395                     if not kmi_tuple in kmi_unique_args:
396                         kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
397                         kmi.properties.name = item.text
398                         kmi_unique_args.add(kmi_tuple)
399
400
401     # ---------------------
402     # End Keymap Generation
403
404     if use_hack_properties:
405         keymap.keymap_items.remove(kmi_hack)
406         keymap.keymap_items.remove(kmi_hack_brush_select)
407
408     # Keep last so we can try add a key without any modifiers
409     # in the case this toolbar was activated with modifiers.
410     if use_tap_reset:
411         if len(kmi_toolbar_args_type_only) == len(kmi_toolbar_args):
412             kmi_toolbar_args_available = kmi_toolbar_args
413         else:
414             # We have modifiers, see if we have a free key w/o modifiers.
415             kmi_toolbar_tuple = dict_as_tuple(kmi_toolbar_args_type_only)
416             if kmi_toolbar_tuple not in kmi_unique_args:
417                 kmi_toolbar_args_available = kmi_toolbar_args_type_only
418                 kmi_unique_args.add(kmi_toolbar_tuple)
419             else:
420                 kmi_toolbar_args_available = kmi_toolbar_args
421             del kmi_toolbar_tuple
422
423         kmi = keymap.keymap_items.new(
424             "wm.tool_set_by_name",
425             value='PRESS' if use_toolbar_release_hack else 'DOUBLE_CLICK',
426             **kmi_toolbar_args_available,
427         )
428         kmi.properties.name = tap_reset_tool
429
430     if use_release_confirm:
431         kmi = keymap.keymap_items.new(
432             "ui.button_execute",
433             type=kmi_toolbar_type,
434             value='RELEASE',
435             any=True,
436         )
437         kmi.properties.skip_depressed = True
438
439         if use_toolbar_release_hack:
440             # ... or pass through to let the toolbar know we're released.
441             # Let the operator know we're released.
442             kmi = keymap.keymap_items.new(
443                 "wm.tool_set_by_name",
444                 type=kmi_toolbar_type,
445                 value='RELEASE',
446                 any=True,
447             )
448
449     wm.keyconfigs.update()
450     return keymap