Mitigate T64346: Quick Favorites items cant be removed
[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_id",
29     "activate_by_id_or_cycle",
30     "description_from_id",
31     "keymap_from_id",
32 )
33
34 # Support reloading icons.
35 if "_icon_cache" in locals():
36     release = bpy.app.icons.release
37     for icon_value in _icon_cache.values():
38         if icon_value != 0:
39             release(icon_value)
40     del release
41
42
43 # (filename -> icon_value) map
44 _icon_cache = {}
45
46
47 def _keymap_fn_from_seq(keymap_data):
48
49     def keymap_fn(km):
50         if keymap_fn.keymap_data:
51             from bl_keymap_utils.io import keymap_init_from_data
52             keymap_init_from_data(km, keymap_fn.keymap_data)
53     keymap_fn.keymap_data = keymap_data
54     return keymap_fn
55
56
57 def _item_is_fn(item):
58     return (not (type(item) is ToolDef) and callable(item))
59
60
61 from collections import namedtuple
62 ToolDef = namedtuple(
63     "ToolDef",
64     (
65         # Unique tool name (withing space & mode context).
66         "idname",
67         # The name to display in the interface.
68         "label",
69         # Description (for tooltip), when not set, use the description of 'operator',
70         # may be a string or a 'function(context, item, keymap) -> string'.
71         "description",
72         # The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon.
73         "icon",
74         # An optional cursor to use when this tool is active.
75         "cursor",
76         # An optional gizmo group to activate when the tool is set or None for no gizmo.
77         "widget",
78         # Optional keymap for tool, either:
79         # - A function that populates a keymaps passed in as an argument.
80         # - A tuple filled with triple's of:
81         #   ``(operator_id, operator_properties, keymap_item_args)``.
82         #
83         # Warning: currently 'from_dict' this is a list of one item,
84         # so internally we can swap the keymap function for the keymap it's self.
85         # This isn't very nice and may change, tool definitions shouldn't care about this.
86         "keymap",
87         # Optional data-block assosiated with this tool.
88         # (Typically brush name, usage depends on mode, we could use for non-brush ID's in other modes).
89         "data_block",
90         # Optional primary operator (for introspection only).
91         "operator",
92         # Optional draw settings (operator options, tool_settings).
93         "draw_settings",
94         # Optional draw cursor.
95         "draw_cursor",
96     )
97 )
98 del namedtuple
99
100
101 def from_dict(kw_args):
102     """
103     Use so each tool can avoid defining all members of the named tuple.
104     Also convert the keymap from a tuple into a function
105     (since keymap is a callback).
106     """
107     kw = {
108         "description": None,
109         "icon": None,
110         "cursor": None,
111         "widget": None,
112         "keymap": None,
113         "data_block": None,
114         "operator": None,
115         "draw_settings": None,
116         "draw_cursor": None,
117     }
118     kw.update(kw_args)
119
120     keymap = kw["keymap"]
121     if keymap is None:
122         pass
123     elif type(keymap) is tuple:
124         keymap = [_keymap_fn_from_seq(keymap)]
125     else:
126         keymap = [keymap]
127     kw["keymap"] = keymap
128     return ToolDef(**kw)
129
130
131 def from_fn(fn):
132     """
133     Use as decorator so we can define functions.
134     """
135     return ToolDef.from_dict(fn())
136
137
138 def with_args(**kw):
139     def from_fn(fn):
140         return ToolDef.from_dict(fn(**kw))
141     return from_fn
142
143
144 from_fn.with_args = with_args
145 ToolDef.from_dict = from_dict
146 ToolDef.from_fn = from_fn
147 del from_dict, from_fn, with_args
148
149
150 class ToolActivePanelHelper:
151     # Sub-class must define.
152     # bl_space_type = 'VIEW_3D'
153     # bl_region_type = 'UI'
154     bl_label = "Active Tool"
155     # bl_category = "Tool"
156
157     def draw(self, context):
158         layout = self.layout
159         layout.use_property_split = True
160         layout.use_property_decorate = False
161         ToolSelectPanelHelper.draw_active_tool_header(
162             context,
163             layout,
164             show_tool_name=True,
165             tool_key=ToolSelectPanelHelper._tool_key_from_context(context, space_type=self.bl_space_type),
166         )
167
168
169 class ToolSelectPanelHelper:
170     """
171     Generic Class, can be used for any toolbar.
172
173     - keymap_prefix:
174       The text prefix for each key-map for this spaces tools.
175     - tools_all():
176       Returns (context_mode, tools) tuple pair for all tools defined.
177     - tools_from_context(context, mode=None):
178       Returns tools available in this context.
179
180     Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``None``.
181     """
182
183     @staticmethod
184     def _tool_class_from_space_type(space_type):
185         return next(
186             (cls for cls in ToolSelectPanelHelper.__subclasses__()
187              if cls.bl_space_type == space_type),
188             None
189         )
190
191     @staticmethod
192     def _icon_value_from_icon_handle(icon_name):
193         import os
194         if icon_name is not None:
195             assert(type(icon_name) is str)
196             icon_value = _icon_cache.get(icon_name)
197             if icon_value is None:
198                 dirname = bpy.utils.resource_path('LOCAL')
199                 if not os.path.exists(dirname):
200                     # TODO(campbell): use a better way of finding datafiles.
201                     dirname = bpy.utils.resource_path('SYSTEM')
202                 filename = os.path.join(dirname, "datafiles", "icons", icon_name + ".dat")
203                 try:
204                     icon_value = bpy.app.icons.new_triangles_from_file(filename)
205                 except Exception as ex:
206                     if not os.path.exists(filename):
207                         print("Missing icons:", filename, ex)
208                     else:
209                         print("Corrupt icon:", filename, ex)
210                     # Use none as a fallback (avoids layout issues).
211                     if icon_name != "none":
212                         icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle("none")
213                     else:
214                         icon_value = 0
215                 _icon_cache[icon_name] = icon_value
216             return icon_value
217         else:
218             return 0
219
220     @staticmethod
221     def _tools_flatten(tools):
222         """
223         Flattens, skips None and calls generators.
224         """
225         for item in tools:
226             if item is None:
227                 yield None
228             elif type(item) is tuple:
229                 for sub_item in item:
230                     if sub_item is None:
231                         yield None
232                     elif _item_is_fn(sub_item):
233                         yield from sub_item(context)
234                     else:
235                         yield sub_item
236             else:
237                 if _item_is_fn(item):
238                     yield from item(context)
239                 else:
240                     yield item
241
242     @staticmethod
243     def _tools_flatten_with_tool_index(tools):
244         for item in tools:
245             if item is None:
246                 yield None, -1
247             elif type(item) is tuple:
248                 i = 0
249                 for sub_item in item:
250                     if sub_item is None:
251                         yield None, -1
252                     elif _item_is_fn(sub_item):
253                         for item_dyn in sub_item(context):
254                             yield item_dyn, i
255                             i += 1
256                     else:
257                         yield sub_item, i
258                         i += 1
259             else:
260                 if _item_is_fn(item):
261                     for item_dyn in item(context):
262                         yield item_dyn, -1
263                 else:
264                     yield item, -1
265
266     @staticmethod
267     def _tool_get_active(context, space_type, mode, with_icon=False):
268         """
269         Return the active Python tool definition and icon name.
270         """
271         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
272         if cls is not None:
273             tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type, mode)
274             tool_active_id = getattr(tool_active, "idname", None)
275             for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context, mode)):
276                 if item is not None:
277                     if item.idname == tool_active_id:
278                         if with_icon:
279                             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
280                         else:
281                             icon_value = 0
282                         return (item, tool_active, icon_value)
283         return None, None, 0
284
285     @staticmethod
286     def _tool_get_by_id(context, space_type, idname):
287         """
288         Return the active Python tool definition and index (if in sub-group, else -1).
289         """
290         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
291         if cls is not None:
292             for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)):
293                 if item is not None:
294                     if item.idname == idname:
295                         return (cls, item, index)
296         return None, None, -1
297
298     @staticmethod
299     def _tool_active_from_context(context, space_type, mode=None, create=False):
300         if space_type == 'VIEW_3D':
301             if mode is None:
302                 mode = context.mode
303             tool = context.workspace.tools.from_space_view3d_mode(mode, create=create)
304             if tool is not None:
305                 tool.refresh_from_context()
306                 return tool
307         elif space_type == 'IMAGE_EDITOR':
308             space_data = context.space_data
309             if mode is None:
310                 if space_data is None:
311                     mode = 'VIEW'
312                 else:
313                     mode = space_data.mode
314             tool = context.workspace.tools.from_space_image_mode(mode, create=create)
315             if tool is not None:
316                 tool.refresh_from_context()
317                 return tool
318         elif space_type == 'NODE_EDITOR':
319             space_data = context.space_data
320             tool = context.workspace.tools.from_space_node(create=create)
321             if tool is not None:
322                 tool.refresh_from_context()
323                 return tool
324         return None
325
326     @staticmethod
327     def _tool_identifier_from_button(context):
328         return context.button_operator.name
329
330     @classmethod
331     def _km_action_simple(cls, kc, context_descr, label, keymap_fn):
332         km_idname = f"{cls.keymap_prefix:s} {context_descr:s}, {label:s}"
333         km = kc.keymaps.get(km_idname)
334         if km is None:
335             km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW', tool=True)
336             keymap_fn[0](km)
337         keymap_fn[0] = km.name
338
339     # Special internal function, gives use items that contain keymaps.
340     @staticmethod
341     def _tools_flatten_with_keymap(tools):
342         for item_parent in tools:
343             if item_parent is None:
344                 continue
345             for item in item_parent if (type(item_parent) is tuple) else (item_parent,):
346                 # skip None or generator function
347                 if item is None or _item_is_fn(item):
348                     continue
349                 if item.keymap is not None:
350                     yield item
351
352     @classmethod
353     def register(cls):
354         wm = bpy.context.window_manager
355         # Write into defaults, users may modify in preferences.
356         kc = wm.keyconfigs.default
357
358         # Track which tool-group was last used for non-active groups.
359         # Blender stores the active tool-group index.
360         #
361         # {tool_name_first: index_in_group, ...}
362         cls._tool_group_active = {}
363
364         # ignore in background mode
365         if kc is None:
366             return
367
368         for context_mode, tools in cls.tools_all():
369             if context_mode is None:
370                 context_descr = "All"
371             else:
372                 context_descr = context_mode.replace("_", " ").title()
373
374             for item in cls._tools_flatten_with_keymap(tools):
375                 keymap_data = item.keymap
376                 if callable(keymap_data[0]):
377                     cls._km_action_simple(kc, context_descr, item.label, keymap_data)
378
379     @classmethod
380     def keymap_ui_hierarchy(cls, context_mode):
381         # See: bpy_extras.keyconfig_utils
382         for context_mode_test, tools in cls.tools_all():
383             if context_mode_test == context_mode:
384                 for item in cls._tools_flatten_with_keymap(tools):
385                     km_name = item.keymap[0]
386                     # print((km.name, cls.bl_space_type, 'WINDOW', []))
387                     yield (km_name, cls.bl_space_type, 'WINDOW', [])
388
389     # -------------------------------------------------------------------------
390     # Layout Generators
391     #
392     # Meaning of recieved values:
393     # - Bool: True for a separator, otherwise False for regular tools.
394     # - None: Signal to finish (complete any final operations, e.g. add padding).
395
396     @staticmethod
397     def _layout_generator_single_column(layout, scale_y):
398         col = layout.column(align=True)
399         col.scale_y = scale_y
400         is_sep = False
401         while True:
402             if is_sep is True:
403                 col = layout.column(align=True)
404                 col.scale_y = scale_y
405             elif is_sep is None:
406                 yield None
407                 return
408             is_sep = yield col
409
410     @staticmethod
411     def _layout_generator_multi_columns(layout, column_count, scale_y):
412         scale_x = scale_y * 1.1
413         column_last = column_count - 1
414
415         col = layout.column(align=True)
416
417         row = col.row(align=True)
418
419         row.scale_x = scale_x
420         row.scale_y = scale_y
421
422         is_sep = False
423         column_index = 0
424         while True:
425             if is_sep is True:
426                 if column_index != column_last:
427                     row.label(text="")
428                 col = layout.column(align=True)
429                 row = col.row(align=True)
430                 row.scale_x = scale_x
431                 row.scale_y = scale_y
432                 column_index = 0
433
434             is_sep = yield row
435             if is_sep is None:
436                 if column_index == column_last:
437                     row.label(text="")
438                     yield None
439                     return
440
441             if column_index == column_count:
442                 column_index = 0
443                 row = col.row(align=True)
444                 row.scale_x = scale_x
445                 row.scale_y = scale_y
446             column_index += 1
447
448     @staticmethod
449     def _layout_generator_detect_from_region(layout, region, scale_y):
450         """
451         Choose an appropriate layout for the toolbar.
452         """
453         # Currently this just checks the width,
454         # we could have different layouts as preferences too.
455         system = bpy.context.preferences.system
456         view2d = region.view2d
457         view2d_scale = (
458             view2d.region_to_view(1.0, 0.0)[0] -
459             view2d.region_to_view(0.0, 0.0)[0]
460         )
461         width_scale = region.width * view2d_scale / system.ui_scale
462
463         if width_scale > 120.0:
464             show_text = True
465             column_count = 1
466         else:
467             show_text = False
468             # 2 column layout, disabled
469             if width_scale > 80.0:
470                 column_count = 2
471             else:
472                 column_count = 1
473
474         if column_count == 1:
475             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(
476                 layout, scale_y=scale_y,
477             )
478         else:
479             ui_gen = ToolSelectPanelHelper._layout_generator_multi_columns(
480                 layout, column_count=column_count, scale_y=scale_y,
481             )
482
483         return ui_gen, show_text
484
485     @classmethod
486     def draw_cls(cls, layout, context, detect_layout=True, scale_y=1.75):
487         # Use a classmethod so it can be called outside of a panel context.
488
489         # XXX, this UI isn't very nice.
490         # We might need to create new button types for this.
491         # Since we probably want:
492         # - tool-tips that include multiple key shortcuts.
493         # - ability to click and hold to expose sub-tools.
494
495         space_type = context.space_data.type
496         tool_active_id = getattr(
497             ToolSelectPanelHelper._tool_active_from_context(context, space_type),
498             "idname", None,
499         )
500
501         if detect_layout:
502             ui_gen, show_text = cls._layout_generator_detect_from_region(layout, context.region, scale_y)
503         else:
504             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y)
505             show_text = True
506
507         # Start iteration
508         ui_gen.send(None)
509
510         for item in cls.tools_from_context(context):
511             if item is None:
512                 ui_gen.send(True)
513                 continue
514
515             if type(item) is tuple:
516                 is_active = False
517                 i = 0
518                 for i, sub_item in enumerate(item):
519                     if sub_item is None:
520                         continue
521                     is_active = (sub_item.idname == tool_active_id)
522                     if is_active:
523                         index = i
524                         break
525                 del i, sub_item
526
527                 if is_active:
528                     # not ideal, write this every time :S
529                     cls._tool_group_active[item[0].idname] = index
530                 else:
531                     index = cls._tool_group_active.get(item[0].idname, 0)
532
533                 item = item[index]
534                 use_menu = True
535             else:
536                 index = -1
537                 use_menu = False
538
539             is_active = (item.idname == tool_active_id)
540             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
541
542             sub = ui_gen.send(False)
543
544             if use_menu:
545                 sub.operator_menu_hold(
546                     "wm.tool_set_by_id",
547                     text=item.label if show_text else "",
548                     depress=is_active,
549                     menu="WM_MT_toolsystem_submenu",
550                     icon_value=icon_value,
551                 ).name = item.idname
552             else:
553                 sub.operator(
554                     "wm.tool_set_by_id",
555                     text=item.label if show_text else "",
556                     depress=is_active,
557                     icon_value=icon_value,
558                 ).name = item.idname
559         # Signal to finish any remaining layout edits.
560         ui_gen.send(None)
561
562     def draw(self, context):
563         self.draw_cls(self.layout, context)
564
565     @staticmethod
566     def _tool_key_from_context(context, *, space_type=None):
567         if space_type is None:
568             space_data = context.space_data
569             space_type = space_data.type
570         else:
571             space_data = None
572
573         if space_type == 'VIEW_3D':
574             return space_type, context.mode
575         elif space_type == 'IMAGE_EDITOR':
576             if space_data is None:
577                 space_data = context.space_data
578             return space_type, space_data.mode
579         elif space_type == 'NODE_EDITOR':
580             return space_type, None
581         else:
582             return None, None
583
584     @staticmethod
585     def tool_active_from_context(context):
586         space_type = context.space_data.type
587         return ToolSelectPanelHelper._tool_active_from_context(context, space_type)
588
589     @staticmethod
590     def draw_active_tool_header(
591             context, layout,
592             *,
593             show_tool_name=False,
594             tool_key=None,
595     ):
596         if tool_key is None:
597             space_type, mode = ToolSelectPanelHelper._tool_key_from_context(context)
598         else:
599             space_type, mode = tool_key
600
601         if space_type is None:
602             return None
603         item, tool, icon_value = ToolSelectPanelHelper._tool_get_active(context, space_type, mode, with_icon=True)
604         if item is None:
605             return None
606         # Note: we could show 'item.text' here but it makes the layout jitter when switching tools.
607         # Add some spacing since the icon is currently assuming regular small icon size.
608         layout.label(text="    " + item.label if show_tool_name else " ", icon_value=icon_value)
609         if show_tool_name:
610             layout.separator()
611         draw_settings = item.draw_settings
612         if draw_settings is not None:
613             draw_settings(context, layout, tool)
614         return tool
615
616
617 # The purpose of this menu is to be a generic popup to select between tools
618 # in cases when a single tool allows to select alternative tools.
619 class WM_MT_toolsystem_submenu(Menu):
620     bl_label = ""
621
622     @staticmethod
623     def _tool_group_from_button(context):
624         # Lookup the tool definitions based on the space-type.
625         cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
626         if cls is not None:
627             button_identifier = ToolSelectPanelHelper._tool_identifier_from_button(context)
628             for item_group in cls.tools_from_context(context):
629                 if type(item_group) is tuple:
630                     for sub_item in item_group:
631                         if (sub_item is not None) and (sub_item.idname == button_identifier):
632                             return cls, item_group
633         return None, None
634
635     def draw(self, context):
636         layout = self.layout
637         layout.scale_y = 2.0
638
639         _cls, item_group = self._tool_group_from_button(context)
640         if item_group is None:
641             # Should never happen, just in case
642             layout.label(text="Unable to find toolbar group")
643             return
644
645         for item in item_group:
646             if item is None:
647                 layout.separator()
648                 continue
649             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
650             layout.operator(
651                 "wm.tool_set_by_id",
652                 text=item.label,
653                 icon_value=icon_value,
654             ).name = item.idname
655
656
657 def _activate_by_item(context, space_type, item, index):
658     tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True)
659     tool.setup(
660         idname=item.idname,
661         keymap=item.keymap[0] if item.keymap is not None else "",
662         cursor=item.cursor or 'DEFAULT',
663         gizmo_group=item.widget or "",
664         data_block=item.data_block or "",
665         operator=item.operator or "",
666         index=index,
667     )
668
669     WindowManager = bpy.types.WindowManager
670
671     handle_map = _activate_by_item._cursor_draw_handle
672     handle = handle_map.pop(space_type, None)
673     if (handle is not None):
674         WindowManager.draw_cursor_remove(handle)
675     if item.draw_cursor is not None:
676         def handle_fn(context, item, tool, xy):
677             item.draw_cursor(context, tool, xy)
678         handle = WindowManager.draw_cursor_add(handle_fn, (context, item, tool), space_type)
679         handle_map[space_type] = handle
680
681
682 _activate_by_item._cursor_draw_handle = {}
683
684
685 def activate_by_id(context, space_type, text):
686     _cls, item, index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, text)
687     if item is None:
688         return False
689     _activate_by_item(context, space_type, item, index)
690     return True
691
692
693 def activate_by_id_or_cycle(context, space_type, idname, offset=1):
694
695     # Only cycle when the active tool is activated again.
696     cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
697     if item is None:
698         return False
699
700     tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type)
701     id_active = getattr(tool_active, "idname", None)
702
703     id_current = ""
704     for item_group in cls.tools_from_context(context):
705         if type(item_group) is tuple:
706             index_current = cls._tool_group_active.get(item_group[0].idname, 0)
707             for sub_item in item_group:
708                 if sub_item.idname == idname:
709                     id_current = item_group[index_current].idname
710                     break
711             if id_current:
712                 break
713
714     if id_current == "":
715         return activate_by_id(context, space_type, idname)
716     if id_active != id_current:
717         return activate_by_id(context, space_type, id_current)
718
719     index_found = (tool_active.index + offset) % len(item_group)
720
721     cls._tool_group_active[item_group[0].idname] = index_found
722
723     item_found = item_group[index_found]
724     _activate_by_item(context, space_type, item_found, index_found)
725     return True
726
727
728 def description_from_id(context, space_type, idname, *, use_operator=True):
729     # Used directly for tooltips.
730     _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
731     if item is None:
732         return False
733
734     # Custom description.
735     description = item.description
736     if description is not None:
737         if callable(description):
738             km = _keymap_from_item(context, item)
739             return description(context, item, km)
740         return description
741
742     # Extract from the operator.
743     if use_operator:
744         operator = item.operator
745         if operator is None:
746             if item.keymap is not None:
747                 km = _keymap_from_item(context, item)
748                 if km is not None:
749                     for kmi in km.keymap_items:
750                         if kmi.active:
751                             operator = kmi.idname
752                             break
753
754         if operator is not None:
755             import _bpy
756             return _bpy.ops.get_rna_type(operator).description
757     return ""
758
759
760 def item_from_id(context, space_type, idname):
761     # Used directly for tooltips.
762     _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
763     return item
764
765
766 def keymap_from_id(context, space_type, idname):
767     # Used directly for tooltips.
768     _cls, item, _index = ToolSelectPanelHelper._tool_get_by_id(context, space_type, idname)
769     if item is None:
770         return False
771
772     keymap = item.keymap
773     # List container of one.
774     if keymap:
775         return keymap[0]
776     return ""
777
778
779 def _keymap_from_item(context, item):
780     if item.keymap is not None:
781         wm = context.window_manager
782         keyconf = wm.keyconfigs.active
783         return keyconf.keymaps.get(item.keymap[0])
784     return None
785
786
787 classes = (
788     WM_MT_toolsystem_submenu,
789 )
790
791 if __name__ == "__main__":  # only for live edit.
792     from bpy.utils import register_class
793     for cls in classes:
794         register_class(cls)