Keymap: event type filter w/ finding keymap items
[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                         'GPENCIL_PAINT': "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                 else:
240                     kmi_found = None
241                 del kmi_first
242             del km
243         else:
244             kmi_found = None
245         item_container[1] = kmi_found
246
247     # -------------------------------------------------------------------------
248     # Single Key Access
249
250     # More complex multi-pass test.
251     for item_container in items_all:
252         item, kmi_found = item_container[:2]
253         if kmi_found is None:
254             continue
255         kmi_found_type = kmi_found.type
256
257         # Only for single keys.
258         if (
259                 (len(kmi_found_type) == 1) or
260                 # When a tool is being activated instead of running an operator, just copy the shortcut.
261                 (kmi_found.idname in {"wm.tool_set_by_name", "WM_OT_tool_set_by_name"})
262         ):
263             kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
264             if kmi_unique_or_pass(kmi_args):
265                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
266                 kmi.properties.name = item.text
267                 item_container[2] = kmi
268
269     # -------------------------------------------------------------------------
270     # Single Key Modifier
271     #
272     #
273     # Test for key_modifier, where alpha key is used as a 'key_modifier'
274     # (grease pencil holding 'D' for example).
275
276     for item_container in items_all:
277         item, kmi_found, kmi_exist = item_container
278         if kmi_found is None or kmi_exist:
279             continue
280
281         kmi_found_type = kmi_found.type
282         if kmi_found_type in {
283                 'LEFTMOUSE',
284                 'RIGHTMOUSE',
285                 'MIDDLEMOUSE',
286                 'BUTTON4MOUSE',
287                 'BUTTON5MOUSE',
288                 'BUTTON6MOUSE',
289                 'BUTTON7MOUSE',
290         }:
291             kmi_found_type = kmi_found.key_modifier
292             # excludes 'NONE'
293             if len(kmi_found_type) == 1:
294                 kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
295                 del kmi_args["key_modifier"]
296                 if kmi_unique_or_pass(kmi_args):
297                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
298                     kmi.properties.name = item.text
299                     item_container[2] = kmi
300
301     # -------------------------------------------------------------------------
302     # Assign A-Z to Keys
303     #
304     # When th keys are free.
305
306     if use_auto_keymap_alpha:
307         # Map all unmapped keys to numbers,
308         # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
309
310         # First map A-Z.
311         kmi_type_alpha_char = [chr(i) for i in range(65, 91)]
312         kmi_type_alpha_args = {c: {"type": c} for c in kmi_type_alpha_char}
313         kmi_type_alpha_args_tuple = {c: dict_as_tuple(kmi_type_alpha_args[c]) for c in kmi_type_alpha_char}
314         for item_container in items_all:
315             item, kmi_found, kmi_exist = item_container
316             if kmi_exist:
317                 continue
318             kmi_type = item.text[0].upper()
319             kmi_tuple = kmi_type_alpha_args_tuple.get(kmi_type)
320             if kmi_tuple and kmi_tuple not in kmi_unique_args:
321                 kmi_unique_args.add(kmi_tuple)
322                 kmi = keymap.keymap_items.new(
323                     idname="wm.tool_set_by_name",
324                     value='PRESS',
325                     **kmi_type_alpha_args[kmi_type],
326                 )
327                 kmi.properties.name = item.text
328                 item_container[2] = kmi
329         del kmi_type_alpha_char, kmi_type_alpha_args, kmi_type_alpha_args_tuple
330
331     # -------------------------------------------------------------------------
332     # Assign Numbers to Keys
333
334     if use_auto_keymap_num:
335         # Free events (last used first).
336         kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
337         # Map both numbers and num-pad.
338         kmi_type_dupe = {
339             'ONE': 'NUMPAD_1',
340             'TWO': 'NUMPAD_2',
341             'THREE': 'NUMPAD_3',
342             'FOUR': 'NUMPAD_4',
343             'FIVE': 'NUMPAD_5',
344             'SIX': 'NUMPAD_6',
345             'SEVEN': 'NUMPAD_7',
346             'EIGHT': 'NUMPAD_8',
347             'NINE': 'NUMPAD_9',
348             'ZERO': 'NUMPAD_0',
349         }
350
351         def iter_free_events():
352             for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
353                 for e in kmi_type_auto:
354                     yield (e, mod)
355
356         iter_events = iter(iter_free_events())
357
358         for item_container in items_all:
359             item, kmi_found, kmi_exist = item_container
360             if kmi_exist:
361                 continue
362             kmi_args = None
363             while True:
364                 key, mod = next(iter_events, (None, None))
365                 if key is None:
366                     break
367                 kmi_args = {"type": key, **mod}
368                 kmi_tuple = dict_as_tuple(kmi_args)
369                 if kmi_tuple in kmi_unique_args:
370                     kmi_args = None
371                 else:
372                     break
373
374             if kmi_args is not None:
375                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
376                 kmi.properties.name = item.text
377                 item_container[2] = kmi
378                 kmi_unique_args.add(kmi_tuple)
379
380                 key = kmi_type_dupe.get(kmi_args["type"])
381                 if key is not None:
382                     kmi_args["type"] = key
383                     kmi_tuple = dict_as_tuple(kmi_args)
384                     if not kmi_tuple in kmi_unique_args:
385                         kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
386                         kmi.properties.name = item.text
387                         kmi_unique_args.add(kmi_tuple)
388
389
390     # ---------------------
391     # End Keymap Generation
392
393     if use_hack_properties:
394         keymap.keymap_items.remove(kmi_hack)
395         keymap.keymap_items.remove(kmi_hack_brush_select)
396
397     # Keep last so we can try add a key without any modifiers
398     # in the case this toolbar was activated with modifiers.
399     if use_tap_reset:
400         if len(kmi_toolbar_args_type_only) == len(kmi_toolbar_args):
401             kmi_toolbar_args_available = kmi_toolbar_args
402         else:
403             # We have modifiers, see if we have a free key w/o modifiers.
404             kmi_toolbar_tuple = dict_as_tuple(kmi_toolbar_args_type_only)
405             if kmi_toolbar_tuple not in kmi_unique_args:
406                 kmi_toolbar_args_available = kmi_toolbar_args_type_only
407                 kmi_unique_args.add(kmi_toolbar_tuple)
408             else:
409                 kmi_toolbar_args_available = kmi_toolbar_args
410             del kmi_toolbar_tuple
411
412         kmi = keymap.keymap_items.new(
413             "wm.tool_set_by_name",
414             value='PRESS' if use_toolbar_release_hack else 'DOUBLE_CLICK',
415             **kmi_toolbar_args_available,
416         )
417         kmi.properties.name = tap_reset_tool
418
419     if use_release_confirm:
420         kmi = keymap.keymap_items.new(
421             "ui.button_execute",
422             type=kmi_toolbar_type,
423             value='RELEASE',
424             any=True,
425         )
426         kmi.properties.skip_depressed = True
427
428         if use_toolbar_release_hack:
429             # ... or pass through to let the toolbar know we're released.
430             # Let the operator know we're released.
431             kmi = keymap.keymap_items.new(
432                 "wm.tool_set_by_name",
433                 type=kmi_toolbar_type,
434                 value='RELEASE',
435                 any=True,
436             )
437
438     wm.keyconfigs.update()
439     return keymap