d338855a8e40f707d8a3b4a4c50d48f3375cecc9
[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     "ToolDef",
27     "ToolSelectPanelHelper",
28     "activate_by_name",
29     "activate_by_name_or_cycle",
30     "description_from_name",
31     "keymap_from_name",
32     "keymap_from_context",
33 )
34
35 # Support reloading icons.
36 if "_icon_cache" in locals():
37     release = bpy.app.icons.release
38     for icon_value in _icon_cache.values():
39         if icon_value != 0:
40             release(icon_value)
41     del release
42
43
44 # (filename -> icon_value) map
45 _icon_cache = {}
46
47
48 def _keymap_fn_from_seq(keymap_data):
49
50     # standalone
51     def _props_assign_recursive(rna_props, py_props):
52         for prop_id, value in py_props.items():
53             if isinstance(value, dict):
54                 _props_assign_recursive(getattr(rna_props, prop_id), value)
55             else:
56                 setattr(rna_props, prop_id, value)
57
58     def keymap_fn(km):
59         for op_idname, op_props_dict, kmi_kwargs in keymap_fn.keymap_data:
60             kmi = km.keymap_items.new(op_idname, **kmi_kwargs)
61             kmi_props = kmi.properties
62             if op_props_dict:
63                 _props_assign_recursive(kmi.properties, op_props_dict)
64     keymap_fn.keymap_data = keymap_data
65     return keymap_fn
66
67
68 def _item_is_fn(item):
69     return (not (type(item) is ToolDef) and callable(item))
70
71
72 from collections import namedtuple
73 ToolDef = namedtuple(
74     "ToolDef",
75     (
76         # The name to display in the interface.
77         "text",
78         # Description (for tooltip), when not set, use the description of 'operator'.
79         "description",
80         # The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon.
81         "icon",
82         # An optional cursor to use when this tool is active.
83         "cursor",
84         # An optional gizmo group to activate when the tool is set or None for no gizmo.
85         "widget",
86         # Optional keymap for tool, either:
87         # - A function that populates a keymaps passed in as an argument.
88         # - A tuple filled with triple's of:
89         #   ``(operator_id, operator_properties, keymap_item_args)``.
90         #
91         # Warning: currently 'from_dict' this is a list of one item,
92         # so internally we can swap the keymap function for the keymap it's self.
93         # This isn't very nice and may change, tool definitions shouldn't care about this.
94         "keymap",
95         # Optional data-block assosiated with this tool.
96         # (Typically brush name, usage depends on mode, we could use for non-brush ID's in other modes).
97         "data_block",
98         # Optional primary operator (for introspection only).
99         "operator",
100         # Optional draw settings (operator options, toolsettings).
101         "draw_settings",
102     )
103 )
104 del namedtuple
105
106
107 def from_dict(kw_args):
108     """
109     Use so each tool can avoid defining all members of the named tuple.
110     Also convert the keymap from a tuple into a function
111     (since keymap is a callback).
112     """
113     kw = {
114         "description": None,
115         "icon": None,
116         "cursor": None,
117         "widget": None,
118         "keymap": None,
119         "data_block": None,
120         "operator": None,
121         "draw_settings": None,
122     }
123     kw.update(kw_args)
124
125     keymap = kw["keymap"]
126     if kw["keymap"] is None:
127         pass
128     elif type(keymap) is tuple:
129         keymap = [_keymap_fn_from_seq(keymap)]
130     else:
131         keymap = [keymap]
132     kw["keymap"] = keymap
133     return ToolDef(**kw)
134
135
136 def from_fn(fn):
137     """
138     Use as decorator so we can define functions.
139     """
140     return ToolDef.from_dict(fn())
141
142
143 ToolDef.from_dict = from_dict
144 ToolDef.from_fn = from_fn
145 del from_dict
146 del from_fn
147
148
149 class ToolSelectPanelHelper:
150     """
151     Generic Class, can be used for any toolbar.
152
153     - keymap_prefix:
154       The text prefix for each key-map for this spaces tools.
155     - tools_all():
156       Returns (context_mode, tools) tuple pair for all tools defined.
157     - tools_from_context(context, mode=None):
158       Returns tools available in this context.
159
160     Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``None``.
161     """
162
163     @staticmethod
164     def _tool_class_from_space_type(space_type):
165         return next(
166             (cls for cls in ToolSelectPanelHelper.__subclasses__()
167              if cls.bl_space_type == space_type),
168             None
169         )
170
171     @staticmethod
172     def _icon_value_from_icon_handle(icon_name):
173         import os
174         if icon_name is not None:
175             assert(type(icon_name) is str)
176             icon_value = _icon_cache.get(icon_name)
177             if icon_value is None:
178                 dirname = bpy.utils.resource_path('LOCAL')
179                 if not os.path.exists(dirname):
180                     # TODO(campbell): use a better way of finding datafiles.
181                     dirname = bpy.utils.resource_path('SYSTEM')
182                 filename = os.path.join(dirname, "datafiles", "icons", icon_name + ".dat")
183                 try:
184                     icon_value = bpy.app.icons.new_triangles_from_file(filename)
185                 except Exception as ex:
186                     if not os.path.exists(filename):
187                         print("Missing icons:", filename, ex)
188                     else:
189                         print("Corrupt icon:", filename, ex)
190                     # Use none as a fallback (avoids layout issues).
191                     if icon_name != "none":
192                         icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle("none")
193                     else:
194                         icon_value = 0
195                 _icon_cache[icon_name] = icon_value
196             return icon_value
197         else:
198             return 0
199
200     @staticmethod
201     def _tools_flatten(tools):
202         """
203         Flattens, skips None and calls generators.
204         """
205         for item in tools:
206             if item is None:
207                 yield None
208             elif type(item) is tuple:
209                 for sub_item in item:
210                     if sub_item is None:
211                         yield None
212                     elif _item_is_fn(sub_item):
213                         yield from sub_item(context)
214                     else:
215                         yield sub_item
216             else:
217                 if _item_is_fn(item):
218                     yield from item(context)
219                 else:
220                     yield item
221
222     @staticmethod
223     def _tools_flatten_with_tool_index(tools):
224         for item in tools:
225             if item is None:
226                 yield None, -1
227             elif type(item) is tuple:
228                 i = 0
229                 for sub_item in item:
230                     if sub_item is None:
231                         yield None
232                     elif _item_is_fn(sub_item):
233                         for item_dyn in sub_item(context):
234                             yield item_dyn, i
235                             i += 1
236                     else:
237                         yield sub_item, i
238                         i += 1
239             else:
240                 if _item_is_fn(item):
241                     for item_dyn in item(context):
242                         yield item_dyn, -1
243                 else:
244                     yield item, -1
245
246     @staticmethod
247     def _tool_get_active(context, space_type, mode, with_icon=False):
248         """
249         Return the active Python tool definition and icon name.
250         """
251         workspace = context.workspace
252         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
253         if cls is not None:
254             tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type, mode)
255             tool_active_text = getattr(tool_active, "name", None)
256             for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context, mode)):
257                 if item is not None:
258                     if item.text == tool_active_text:
259                         if with_icon:
260                             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
261                         else:
262                             icon_value = 0
263                         return (item, tool_active, icon_value)
264         return None, None, 0
265
266     @staticmethod
267     def _tool_get_by_name(context, space_type, text):
268         """
269         Return the active Python tool definition and index (if in sub-group, else -1).
270         """
271         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
272         if cls is not None:
273             for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)):
274                 if item is not None:
275                     if item.text == text:
276                         return (cls, item, index)
277         return None, None, -1
278
279     @staticmethod
280     def _tool_active_from_context(context, space_type, mode=None, create=False):
281         if space_type == 'VIEW_3D':
282             if mode is None:
283                 mode = context.mode
284             tool = context.workspace.tools.from_space_view3d_mode(mode, create=create)
285             if tool is not None:
286                 tool.refresh_from_context()
287                 return tool
288         elif space_type == 'IMAGE_EDITOR':
289             space_data = context.space_data
290             if mode is None:
291                 if space_data is None:
292                     mode = 'VIEW'
293                 else:
294                     mode = space_data.mode
295             tool = context.workspace.tools.from_space_image_mode(mode, create=create)
296             if tool is not None:
297                 tool.refresh_from_context()
298                 return tool
299         return None
300
301     @staticmethod
302     def _tool_text_from_button(context):
303         return context.button_operator.name
304
305     @classmethod
306     def _km_action_simple(cls, kc, context_mode, text, keymap_fn):
307         if context_mode is None:
308             context_mode = "All"
309         km_idname = f"{cls.keymap_prefix:s} {context_mode:s}, {text:s}"
310         km = kc.keymaps.get(km_idname)
311         if km is None:
312             km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW')
313             keymap_fn[0](km)
314         keymap_fn[0] = km
315
316     @classmethod
317     def register(cls):
318         wm = bpy.context.window_manager
319
320         # XXX, should we be manipulating the user-keyconfig on load?
321         # Perhaps this should only add when keymap items don't already exist.
322         #
323         # This needs some careful consideration.
324         kc = wm.keyconfigs.user
325
326         # Track which tool-group was last used for non-active groups.
327         # Blender stores the active tool-group index.
328         #
329         # {tool_name_first: index_in_group, ...}
330         cls._tool_group_active = {}
331
332         # ignore in background mode
333         if kc is None:
334             return
335
336         for context_mode, tools in cls.tools_all():
337             for item_parent in tools:
338                 if item_parent is None:
339                     continue
340                 for item in item_parent if (type(item_parent) is tuple) else (item_parent,):
341                     # skip None or generator function
342                     if item is None or _item_is_fn(item):
343                         continue
344                     keymap_data = item.keymap
345                     if keymap_data is not None and callable(keymap_data[0]):
346                         text = item.text
347                         icon_name = item.icon
348                         cls._km_action_simple(kc, context_mode, text, keymap_data)
349
350     # -------------------------------------------------------------------------
351     # Layout Generators
352     #
353     # Meaning of recieved values:
354     # - Bool: True for a separator, otherwise False for regular tools.
355     # - None: Signal to finish (complete any final operations, e.g. add padding).
356
357     @staticmethod
358     def _layout_generator_single_column(layout, scale_y):
359         col = layout.column(align=True)
360         col.scale_y = scale_y
361         is_sep = False
362         while True:
363             if is_sep is True:
364                 col = layout.column(align=True)
365                 col.scale_y = scale_y
366             elif is_sep is None:
367                 yield None
368                 return
369             is_sep = yield col
370
371     @staticmethod
372     def _layout_generator_multi_columns(layout, column_count, scale_y):
373         scale_x = scale_y * 1.1
374         column_last = column_count - 1
375
376         col = layout.column(align=True)
377
378         row = col.row(align=True)
379
380         row.scale_x = scale_x
381         row.scale_y = scale_y
382
383         is_sep = False
384         column_index = 0
385         while True:
386             if is_sep is True:
387                 if column_index != column_last:
388                     row.label(text="")
389                 col = layout.column(align=True)
390                 row = col.row(align=True)
391                 row.scale_x = scale_x
392                 row.scale_y = scale_y
393                 column_index = 0
394
395             is_sep = yield row
396             if is_sep is None:
397                 if column_index == column_last:
398                     row.label(text="")
399                     yield None
400                     return
401
402             if column_index == column_count:
403                 column_index = 0
404                 row = col.row(align=True)
405                 row.scale_x = scale_x
406                 row.scale_y = scale_y
407             column_index += 1
408
409     @staticmethod
410     def _layout_generator_detect_from_region(layout, region, scale_y):
411         """
412         Choose an appropriate layout for the toolbar.
413         """
414         # Currently this just checks the width,
415         # we could have different layouts as preferences too.
416         system = bpy.context.user_preferences.system
417         view2d = region.view2d
418         view2d_scale = (
419             view2d.region_to_view(1.0, 0.0)[0] -
420             view2d.region_to_view(0.0, 0.0)[0]
421         )
422         width_scale = region.width * view2d_scale / system.ui_scale
423
424         if width_scale > 120.0:
425             show_text = True
426             column_count = 1
427         else:
428             show_text = False
429             # 2 column layout, disabled
430             if width_scale > 80.0:
431                 column_count = 2
432                 use_columns = True
433             else:
434                 column_count = 1
435
436         if column_count == 1:
437             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y=scale_y)
438         else:
439             ui_gen = ToolSelectPanelHelper._layout_generator_multi_columns(layout, column_count=column_count, scale_y=scale_y)
440
441         return ui_gen, show_text
442
443     @classmethod
444     def draw_cls(cls, layout, context, detect_layout=True, scale_y=1.75):
445         # Use a classmethod so it can be called outside of a panel context.
446
447         # XXX, this UI isn't very nice.
448         # We might need to create new button types for this.
449         # Since we probably want:
450         # - tool-tips that include multiple key shortcuts.
451         # - ability to click and hold to expose sub-tools.
452
453         space_type = context.space_data.type
454         tool_active_text = getattr(
455             ToolSelectPanelHelper._tool_active_from_context(context, space_type),
456             "name", None,
457         )
458
459         if detect_layout:
460             ui_gen, show_text = cls._layout_generator_detect_from_region(layout, context.region, scale_y)
461         else:
462             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y)
463             show_text = True
464
465         # Start iteration
466         ui_gen.send(None)
467
468         for item in cls.tools_from_context(context):
469             if item is None:
470                 ui_gen.send(True)
471                 continue
472
473             if type(item) is tuple:
474                 is_active = False
475                 i = 0
476                 for i, sub_item in enumerate(item):
477                     if sub_item is None:
478                         continue
479                     is_active = (sub_item.text == tool_active_text)
480                     if is_active:
481                         index = i
482                         break
483                 del i, sub_item
484
485                 if is_active:
486                     # not ideal, write this every time :S
487                     cls._tool_group_active[item[0].text] = index
488                 else:
489                     index = cls._tool_group_active.get(item[0].text, 0)
490
491                 item = item[index]
492                 use_menu = True
493             else:
494                 index = -1
495                 use_menu = False
496
497             is_active = (item.text == tool_active_text)
498             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
499
500             sub = ui_gen.send(False)
501
502             if use_menu:
503                 sub.operator_menu_hold(
504                     "wm.tool_set_by_name",
505                     text=item.text if show_text else "",
506                     depress=is_active,
507                     menu="WM_MT_toolsystem_submenu",
508                     icon_value=icon_value,
509                 ).name = item.text
510             else:
511                 sub.operator(
512                     "wm.tool_set_by_name",
513                     text=item.text if show_text else "",
514                     depress=is_active,
515                     icon_value=icon_value,
516                 ).name = item.text
517         # Signal to finish any remaining layout edits.
518         ui_gen.send(None)
519
520     def draw(self, context):
521         self.draw_cls(self.layout, context)
522
523     @staticmethod
524     def draw_active_tool_header(
525             context, layout,
526             *,
527             show_tool_name=False,
528     ):
529         # BAD DESIGN WARNING: last used tool
530         workspace = context.workspace
531         space_type = workspace.tools_space_type
532         mode = workspace.tools_mode
533         item, tool, icon_value = ToolSelectPanelHelper._tool_get_active(context, space_type, mode, with_icon=True)
534         if item is None:
535             return None
536         # Note: we could show 'item.text' here but it makes the layout jitter when switching tools.
537         # Add some spacing since the icon is currently assuming regular small icon size.
538         layout.label(text="    " + item.text if show_tool_name else " ", icon_value=icon_value)
539         draw_settings = item.draw_settings
540         if draw_settings is not None:
541             draw_settings(context, layout, tool)
542         return tool
543
544
545 # The purpose of this menu is to be a generic popup to select between tools
546 # in cases when a single tool allows to select alternative tools.
547 class WM_MT_toolsystem_submenu(Menu):
548     bl_label = ""
549
550     @staticmethod
551     def _tool_group_from_button(context):
552         # Lookup the tool definitions based on the space-type.
553         cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
554         if cls is not None:
555             button_text = ToolSelectPanelHelper._tool_text_from_button(context)
556             for item_group in cls.tools_from_context(context):
557                 if type(item_group) is tuple:
558                     for sub_item in item_group:
559                         if sub_item.text == button_text:
560                             return cls, item_group
561         return None, None
562
563     def draw(self, context):
564         layout = self.layout
565         layout.scale_y = 2.0
566
567         cls, item_group = self._tool_group_from_button(context)
568         if item_group is None:
569             # Should never happen, just in case
570             layout.label(text="Unable to find toolbar group")
571             return
572
573         for item in item_group:
574             if item is None:
575                 layout.separator()
576                 continue
577             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
578             layout.operator(
579                 "wm.tool_set_by_name",
580                 text=item.text,
581                 icon_value=icon_value,
582             ).name = item.text
583
584
585 def _activate_by_item(context, space_type, item, index):
586     tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True)
587     tool.setup(
588         name=item.text,
589         keymap=item.keymap[0].name if item.keymap is not None else "",
590         cursor=item.cursor or 'DEFAULT',
591         gizmo_group=item.widget or "",
592         data_block=item.data_block or "",
593         operator=item.operator or "",
594         index=index,
595     )
596
597
598 def activate_by_name(context, space_type, text):
599     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
600     if item is None:
601         return False
602     _activate_by_item(context, space_type, item, index)
603     return True
604
605
606 def activate_by_name_or_cycle(context, space_type, text, offset=1):
607
608     # Only cycle when the active tool is activated again.
609     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
610     if item is None:
611         return False
612
613     tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type)
614     text_active = getattr(tool_active, "name", None)
615
616     text_current = ""
617     for item_group in cls.tools_from_context(context):
618         if type(item_group) is tuple:
619             index_current = cls._tool_group_active.get(item_group[0].text, 0)
620             ok = False
621             for i, sub_item in enumerate(item_group):
622                 if sub_item.text == text:
623                     text_current = item_group[index_current].text
624                     break
625             if text_current:
626                 break
627
628     if text_current == "":
629         return activate_by_name(context, space_type, text)
630     if text_active != text_current:
631         return activate_by_name(context, space_type, text_current)
632
633     index_found = (tool_active.index + offset) % len(item_group)
634
635     cls._tool_group_active[item_group[0].text] = index_found
636
637     item_found = item_group[index_found]
638     _activate_by_item(context, space_type, item_found, index_found)
639     return True
640
641
642 def description_from_name(context, space_type, text, *, use_operator=True):
643     # Used directly for tooltips.
644     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
645     if item is None:
646         return False
647
648     # Custom description.
649     description = item.description
650     if description is not None:
651         return description
652
653     # Extract from the operator.
654     if use_operator:
655         operator = item.operator
656
657         if operator is None:
658             if item.keymap is not None:
659                 operator = item.keymap[0].keymap_items[0].idname
660
661         if operator is not None:
662             import _bpy
663             return _bpy.ops.get_rna(operator).bl_rna.description
664     return ""
665
666
667 def keymap_from_name(context, space_type, text):
668     # Used directly for tooltips.
669     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
670     if item is None:
671         return False
672
673     keymap = item.keymap
674     # List container of one.
675     if keymap:
676         return keymap[0]
677     return ""
678
679
680 def keymap_from_context(context, space_type):
681     """
682     Keymap for popup toolbar, currently generated each time.
683     """
684
685     def modifier_keywords_from_item(kmi):
686         kw = {}
687         for (attr, default) in (
688                 ("any", False),
689                 ("shift", False),
690                 ("ctrl", False),
691                 ("alt", False),
692                 ("oskey", False),
693                 ("key_modifier", 'NONE'),
694         ):
695             val = getattr(kmi, attr)
696             if val != default:
697                 kw[attr] = val
698         return kw
699
700     def dict_as_tuple(d):
701         return tuple((k, v) for (k, v) in sorted(d.items()))
702
703     use_simple_keymap = False
704
705     # Generate items when no keys are mapped.
706     use_auto_keymap = True
707
708     km_name = "Toolbar Popup"
709     wm = context.window_manager
710     keyconf = wm.keyconfigs.active
711     keymap = keyconf.keymaps.get(km_name)
712     if keymap is None:
713         keymap = keyconf.keymaps.new(km_name, space_type='EMPTY', region_type='TEMPORARY')
714     for kmi in keymap.keymap_items:
715         keymap.keymap_items.remove(kmi)
716
717     kmi_unique_args = set()
718
719     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
720
721     items_all = [
722         # 0: tool
723         # 1: keymap item (direct access)
724         # 2: keymap item (newly calculated for toolbar)
725         [item, None, None]
726         for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context))
727         if item is not None
728     ]
729
730     if use_simple_keymap:
731         # Simply assign a key from A-Z.
732         for i, (item, _, _) in enumerate(items_all):
733             key = chr(ord('A') + i)
734             kmi = keymap.keymap_items.new("wm.tool_set_by_name", key, 'PRESS')
735             kmi.properties.name = item.text
736     else:
737         for item_container in items_all:
738             item = item_container[0]
739             # Only check the first item in the tools key-map (a little arbitrary).
740             if item.operator is not None:
741                 kmi_found = wm.keyconfigs.find_item_from_operator(
742                     idname=item.operator,
743                     context='INVOKE_REGION_WIN',
744                 )[1]
745             elif item.keymap is not None:
746                 kmi_first = item.keymap[0].keymap_items[0]
747                 kmi_found = wm.keyconfigs.find_item_from_operator(
748                     idname=kmi_first.idname,
749                     # properties=kmi_first.properties,  # prevents matches, don't use.
750                     context='INVOKE_REGION_WIN',
751                 )[1]
752                 del kmi_first
753             else:
754                 kmi_found = None
755             item_container[1] = kmi_found
756
757         # More complex multi-pass test.
758         for item_container in items_all:
759             item, kmi_found = item_container[:2]
760             if kmi_found is None:
761                 continue
762             kmi_found_type = kmi_found.type
763
764             # Only for single keys.
765             if len(kmi_found_type) == 1:
766                 kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
767                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
768                 kmi.properties.name = item.text
769                 item_container[2] = kmi
770                 if use_auto_keymap:
771                     kmi_unique_args.add(dict_as_tuple(kmi_args))
772
773         # Test for key_modifier, where alpha key is used as a 'key_modifier'
774         # (grease pencil holding 'D' for example).
775         for item_container in items_all:
776             item, kmi_found, kmi_exist = item_container
777             if kmi_found is None or kmi_exist:
778                 continue
779
780             kmi_found_type = kmi_found.type
781             if kmi_found_type in {
782                     'LEFTMOUSE',
783                     'RIGHTMOUSE',
784                     'MIDDLEMOUSE',
785                     'BUTTON4MOUSE',
786                     'BUTTON5MOUSE',
787                     'BUTTON6MOUSE',
788                     'BUTTON7MOUSE',
789                     'ACTIONMOUSE',
790                     'SELECTMOUSE',
791             }:
792                 kmi_found_type = kmi_found.key_modifier
793                 # excludes 'NONE'
794                 if len(kmi_found_type) == 1:
795                     kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
796                     del kmi_args["key_modifier"]
797                     kmi_tuple = dict_as_tuple(kmi_args)
798                     if kmi_tuple in kmi_unique_args:
799                         continue
800                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
801                     kmi.properties.name = item.text
802                     item_container[2] = kmi
803                     if use_auto_keymap:
804                         kmi_unique_args.add(kmi_tuple)
805
806         if use_auto_keymap:
807             # Map all unmapped keys to numbers,
808             # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
809
810             # Free events (last used first).
811             kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
812             # Map both numbers and num-pad.
813             kmi_type_dupe = {
814                 'ONE': 'NUMPAD_1',
815                 'TWO': 'NUMPAD_2',
816                 'THREE': 'NUMPAD_3',
817                 'FOUR': 'NUMPAD_4',
818                 'FIVE': 'NUMPAD_5',
819                 'SIX': 'NUMPAD_6',
820                 'SEVEN': 'NUMPAD_7',
821                 'EIGHT': 'NUMPAD_8',
822                 'NINE': 'NUMPAD_9',
823                 'ZERO': 'NUMPAD_0',
824             }
825
826             def iter_free_events():
827                 for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
828                     for e in kmi_type_auto:
829                         yield (e, mod)
830
831             iter_events = iter(iter_free_events())
832
833             for item_container in items_all:
834                 item, kmi_found, kmi_exist = item_container
835                 if kmi_exist:
836                     continue
837                 kmi_args = None
838                 while True:
839                     key, mod = next(iter_events, (None, None))
840                     if key is None:
841                         break
842                     kmi_args = {"type": key, **mod}
843                     kmi_tuple = dict_as_tuple(kmi_args)
844                     if kmi_tuple in kmi_unique_args:
845                         kmi_args = None
846                     else:
847                         break
848
849                 if kmi_args is not None:
850                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
851                     kmi.properties.name = item.text
852                     item_container[2] = kmi
853                     if use_auto_keymap:
854                         kmi_unique_args.add(kmi_tuple)
855
856                     key = kmi_type_dupe.get(kmi_args["type"])
857                     if key is not None:
858                         kmi_args["type"] = key
859                         kmi_tuple = dict_as_tuple(kmi_args)
860                         if not kmi_tuple in kmi_unique_args:
861                             kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
862                             kmi.properties.name = item.text
863                             if use_auto_keymap:
864                                 kmi_unique_args.add(kmi_tuple)
865
866     if True:
867         # The shortcut will show, so we better support running it.
868         kmi_search = wm.keyconfigs.find_item_from_operator(
869             idname="wm.search_menu",
870             context='INVOKE_REGION_WIN',
871         )[1]
872         if kmi_search:
873             keymap.keymap_items.new(
874                 "wm.search_menu",
875                 type=kmi_search.type,
876                 value='PRESS',
877                 **modifier_keywords_from_item(kmi_search),
878             )
879
880     wm.keyconfigs.update()
881     return keymap
882
883
884 classes = (
885     WM_MT_toolsystem_submenu,
886 )
887
888 if __name__ == "__main__":  # only for live edit.
889     from bpy.utils import register_class
890     for cls in classes:
891         register_class(cls)