7f85faa4ba6bc547bfeefd747cfa1b17f13adc53
[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             is_hotkey=True,
135         )[1]
136         kmi_toolbar_type = None if not kmi_toolbar else kmi_toolbar.type
137         if use_tap_reset and kmi_toolbar_type is not None:
138             kmi_toolbar_args_type_only = {"type": kmi_toolbar_type}
139             kmi_toolbar_args = {**kmi_toolbar_args_type_only, **modifier_keywords_from_item(kmi_toolbar)}
140         else:
141             use_tap_reset = False
142         del kmi_toolbar
143
144     if use_tap_reset:
145         kmi_found = None
146         if use_hack_properties:
147             # First check for direct assignment, if this tool already has a key, no need to add a new one.
148             kmi_hack_properties.name = tap_reset_tool
149             kmi_found = wm.keyconfigs.find_item_from_operator(
150                 idname="wm.tool_set_by_name",
151                 context='INVOKE_REGION_WIN',
152                 # properties={"name": item.text},
153                 properties=kmi_hack_properties,
154                 is_hotkey=True,
155             )[1]
156             if kmi_found:
157                 use_tap_reset = False
158         del kmi_found
159
160     if use_tap_reset:
161         use_tap_reset = kmi_unique_or_pass(kmi_toolbar_args)
162
163     if use_tap_reset:
164         items_all[:] = [
165             item_container
166             for item_container in items_all
167             if item_container[0].text != tap_reset_tool
168         ]
169
170     # -----------------------
171     # Begin Keymap Generation
172
173     # -------------------------------------------------------------------------
174     # Direct Tool Assignment & Brushes
175
176     for item_container in items_all:
177         item = item_container[0]
178         # Only check the first item in the tools key-map (a little arbitrary).
179         if use_hack_properties:
180             # First check for direct assignment.
181             kmi_hack_properties.name = item.text
182             kmi_found = wm.keyconfigs.find_item_from_operator(
183                 idname="wm.tool_set_by_name",
184                 context='INVOKE_REGION_WIN',
185                 # properties={"name": item.text},
186                 properties=kmi_hack_properties,
187                 is_hotkey=True,
188             )[1]
189
190             if kmi_found is None:
191                 if item.data_block:
192                     # PAINT_OT_brush_select
193                     mode = context.active_object.mode
194                     # See: BKE_paint_get_tool_prop_id_from_paintmode
195                     attr = {
196                         'SCULPT': "sculpt_tool",
197                         'VERTEX_PAINT': "vertex_tool",
198                         'WEIGHT_PAINT': "weight_tool",
199                         'TEXTURE_PAINT': "image_tool",
200                         'GPENCIL_PAINT': "gpencil_tool",
201                     }.get(mode, None)
202                     if attr is not None:
203                         setattr(kmi_hack_brush_select_properties, attr, item.data_block)
204                         kmi_found = wm.keyconfigs.find_item_from_operator(
205                             idname="paint.brush_select",
206                             context='INVOKE_REGION_WIN',
207                             properties=kmi_hack_brush_select_properties,
208                             is_hotkey=True,
209                         )[1]
210                     else:
211                         print("Unsupported mode:", mode)
212                     del mode, attr
213
214         else:
215             kmi_found = None
216
217         if kmi_found is not None:
218             pass
219         elif item.operator is not None:
220             kmi_found = wm.keyconfigs.find_item_from_operator(
221                 idname=item.operator,
222                 context='INVOKE_REGION_WIN',
223                 is_hotkey=True,
224             )[1]
225         elif item.keymap is not None:
226             km = keyconf.keymaps.get(item.keymap[0])
227             if km is None:
228                 print("Keymap", repr(item.keymap[0]), "not found for tool", item.text)
229                 kmi_found = None
230             else:
231                 kmi_first = km.keymap_items
232                 kmi_first = kmi_first[0] if kmi_first else None
233                 if kmi_first is not None:
234                     kmi_found = wm.keyconfigs.find_item_from_operator(
235                         idname=kmi_first.idname,
236                         # properties=kmi_first.properties,  # prevents matches, don't use.
237                         context='INVOKE_REGION_WIN',
238                         is_hotkey=True,
239                     )[1]
240                 else:
241                     kmi_found = None
242                 del kmi_first
243             del km
244         else:
245             kmi_found = None
246         item_container[1] = kmi_found
247
248     # -------------------------------------------------------------------------
249     # Single Key Access
250
251     # More complex multi-pass test.
252     for item_container in items_all:
253         item, kmi_found = item_container[:2]
254         if kmi_found is None:
255             continue
256         kmi_found_type = kmi_found.type
257
258         # Only for single keys.
259         if (
260                 (len(kmi_found_type) == 1) or
261                 # When a tool is being activated instead of running an operator, just copy the shortcut.
262                 (kmi_found.idname in {"wm.tool_set_by_name", "WM_OT_tool_set_by_name"})
263         ):
264             kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
265             if kmi_unique_or_pass(kmi_args):
266                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
267                 kmi.properties.name = item.text
268                 item_container[2] = kmi
269
270     # -------------------------------------------------------------------------
271     # Single Key Modifier
272     #
273     #
274     # Test for key_modifier, where alpha key is used as a 'key_modifier'
275     # (grease pencil holding 'D' for example).
276
277     for item_container in items_all:
278         item, kmi_found, kmi_exist = item_container
279         if kmi_found is None or kmi_exist:
280             continue
281
282         kmi_found_type = kmi_found.type
283         if kmi_found_type in {
284                 'LEFTMOUSE',
285                 'RIGHTMOUSE',
286                 'MIDDLEMOUSE',
287                 'BUTTON4MOUSE',
288                 'BUTTON5MOUSE',
289                 'BUTTON6MOUSE',
290                 'BUTTON7MOUSE',
291         }:
292             kmi_found_type = kmi_found.key_modifier
293             # excludes 'NONE'
294             if len(kmi_found_type) == 1:
295                 kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
296                 del kmi_args["key_modifier"]
297                 if kmi_unique_or_pass(kmi_args):
298                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
299                     kmi.properties.name = item.text
300                     item_container[2] = kmi
301
302     # -------------------------------------------------------------------------
303     # Assign A-Z to Keys
304     #
305     # When th keys are free.
306
307     if use_auto_keymap_alpha:
308         # Map all unmapped keys to numbers,
309         # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
310
311         # First map A-Z.
312         kmi_type_alpha_char = [chr(i) for i in range(65, 91)]
313         kmi_type_alpha_args = {c: {"type": c} for c in kmi_type_alpha_char}
314         kmi_type_alpha_args_tuple = {c: dict_as_tuple(kmi_type_alpha_args[c]) for c in kmi_type_alpha_char}
315         for item_container in items_all:
316             item, kmi_found, kmi_exist = item_container
317             if kmi_exist:
318                 continue
319             kmi_type = item.text[0].upper()
320             kmi_tuple = kmi_type_alpha_args_tuple.get(kmi_type)
321             if kmi_tuple and kmi_tuple not in kmi_unique_args:
322                 kmi_unique_args.add(kmi_tuple)
323                 kmi = keymap.keymap_items.new(
324                     idname="wm.tool_set_by_name",
325                     value='PRESS',
326                     **kmi_type_alpha_args[kmi_type],
327                 )
328                 kmi.properties.name = item.text
329                 item_container[2] = kmi
330         del kmi_type_alpha_char, kmi_type_alpha_args, kmi_type_alpha_args_tuple
331
332     # -------------------------------------------------------------------------
333     # Assign Numbers to Keys
334
335     if use_auto_keymap_num:
336         # Free events (last used first).
337         kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
338         # Map both numbers and num-pad.
339         kmi_type_dupe = {
340             'ONE': 'NUMPAD_1',
341             'TWO': 'NUMPAD_2',
342             'THREE': 'NUMPAD_3',
343             'FOUR': 'NUMPAD_4',
344             'FIVE': 'NUMPAD_5',
345             'SIX': 'NUMPAD_6',
346             'SEVEN': 'NUMPAD_7',
347             'EIGHT': 'NUMPAD_8',
348             'NINE': 'NUMPAD_9',
349             'ZERO': 'NUMPAD_0',
350         }
351
352         def iter_free_events():
353             for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
354                 for e in kmi_type_auto:
355                     yield (e, mod)
356
357         iter_events = iter(iter_free_events())
358
359         for item_container in items_all:
360             item, kmi_found, kmi_exist = item_container
361             if kmi_exist:
362                 continue
363             kmi_args = None
364             while True:
365                 key, mod = next(iter_events, (None, None))
366                 if key is None:
367                     break
368                 kmi_args = {"type": key, **mod}
369                 kmi_tuple = dict_as_tuple(kmi_args)
370                 if kmi_tuple in kmi_unique_args:
371                     kmi_args = None
372                 else:
373                     break
374
375             if kmi_args is not None:
376                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
377                 kmi.properties.name = item.text
378                 item_container[2] = kmi
379                 kmi_unique_args.add(kmi_tuple)
380
381                 key = kmi_type_dupe.get(kmi_args["type"])
382                 if key is not None:
383                     kmi_args["type"] = key
384                     kmi_tuple = dict_as_tuple(kmi_args)
385                     if not kmi_tuple in kmi_unique_args:
386                         kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
387                         kmi.properties.name = item.text
388                         kmi_unique_args.add(kmi_tuple)
389
390
391     # ---------------------
392     # End Keymap Generation
393
394     if use_hack_properties:
395         keymap.keymap_items.remove(kmi_hack)
396         keymap.keymap_items.remove(kmi_hack_brush_select)
397
398     # Keep last so we can try add a key without any modifiers
399     # in the case this toolbar was activated with modifiers.
400     if use_tap_reset:
401         if len(kmi_toolbar_args_type_only) == len(kmi_toolbar_args):
402             kmi_toolbar_args_available = kmi_toolbar_args
403         else:
404             # We have modifiers, see if we have a free key w/o modifiers.
405             kmi_toolbar_tuple = dict_as_tuple(kmi_toolbar_args_type_only)
406             if kmi_toolbar_tuple not in kmi_unique_args:
407                 kmi_toolbar_args_available = kmi_toolbar_args_type_only
408                 kmi_unique_args.add(kmi_toolbar_tuple)
409             else:
410                 kmi_toolbar_args_available = kmi_toolbar_args
411             del kmi_toolbar_tuple
412
413         kmi = keymap.keymap_items.new(
414             "wm.tool_set_by_name",
415             value='PRESS' if use_toolbar_release_hack else 'DOUBLE_CLICK',
416             **kmi_toolbar_args_available,
417         )
418         kmi.properties.name = tap_reset_tool
419
420     if use_release_confirm:
421         kmi = keymap.keymap_items.new(
422             "ui.button_execute",
423             type=kmi_toolbar_type,
424             value='RELEASE',
425             any=True,
426         )
427         kmi.properties.skip_depressed = True
428
429         if use_toolbar_release_hack:
430             # ... or pass through to let the toolbar know we're released.
431             # Let the operator know we're released.
432             kmi = keymap.keymap_items.new(
433                 "wm.tool_set_by_name",
434                 type=kmi_toolbar_type,
435                 value='RELEASE',
436                 any=True,
437             )
438
439     wm.keyconfigs.update()
440     return keymap