953 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			953 lines
		
	
	
		
			34 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # ##### BEGIN GPL LICENSE BLOCK #####
 | |
| #
 | |
| #  This program is free software; you can redistribute it and/or
 | |
| #  modify it under the terms of the GNU General Public License
 | |
| #  as published by the Free Software Foundation; either version 2
 | |
| #  of the License, or (at your option) any later version.
 | |
| #
 | |
| #  This program is distributed in the hope that it will be useful,
 | |
| #  but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| #  GNU General Public License for more details.
 | |
| #
 | |
| #  You should have received a copy of the GNU General Public License
 | |
| #  along with this program; if not, write to the Free Software Foundation,
 | |
| #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 | |
| #
 | |
| # ##### END GPL LICENSE BLOCK #####
 | |
| 
 | |
| # <pep8 compliant>
 | |
| import bpy
 | |
| from bpy.types import (
 | |
|     Menu,
 | |
| )
 | |
| 
 | |
| __all__ = (
 | |
|     "ToolDef",
 | |
|     "ToolSelectPanelHelper",
 | |
|     "activate_by_name",
 | |
|     "activate_by_name_or_cycle",
 | |
|     "description_from_name",
 | |
|     "keymap_from_name",
 | |
|     "keymap_from_context",
 | |
| )
 | |
| 
 | |
| # Support reloading icons.
 | |
| if "_icon_cache" in locals():
 | |
|     release = bpy.app.icons.release
 | |
|     for icon_value in _icon_cache.values():
 | |
|         if icon_value != 0:
 | |
|             release(icon_value)
 | |
|     del release
 | |
| 
 | |
| 
 | |
| # (filename -> icon_value) map
 | |
| _icon_cache = {}
 | |
| 
 | |
| 
 | |
| def _keymap_fn_from_seq(keymap_data):
 | |
| 
 | |
|     # standalone
 | |
|     def _props_assign_recursive(rna_props, py_props):
 | |
|         for prop_id, value in py_props.items():
 | |
|             if isinstance(value, dict):
 | |
|                 _props_assign_recursive(getattr(rna_props, prop_id), value)
 | |
|             else:
 | |
|                 setattr(rna_props, prop_id, value)
 | |
| 
 | |
|     def keymap_fn(km):
 | |
|         for op_idname, op_props_dict, kmi_kwargs in keymap_fn.keymap_data:
 | |
|             kmi = km.keymap_items.new(op_idname, **kmi_kwargs)
 | |
|             kmi_props = kmi.properties
 | |
|             if op_props_dict:
 | |
|                 _props_assign_recursive(kmi.properties, op_props_dict)
 | |
|     keymap_fn.keymap_data = keymap_data
 | |
|     return keymap_fn
 | |
| 
 | |
| 
 | |
| def _item_is_fn(item):
 | |
|     return (not (type(item) is ToolDef) and callable(item))
 | |
| 
 | |
| 
 | |
| from collections import namedtuple
 | |
| ToolDef = namedtuple(
 | |
|     "ToolDef",
 | |
|     (
 | |
|         # The name to display in the interface.
 | |
|         "text",
 | |
|         # Description (for tooltip), when not set, use the description of 'operator'.
 | |
|         "description",
 | |
|         # The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon.
 | |
|         "icon",
 | |
|         # An optional cursor to use when this tool is active.
 | |
|         "cursor",
 | |
|         # An optional gizmo group to activate when the tool is set or None for no gizmo.
 | |
|         "widget",
 | |
|         # Optional keymap for tool, either:
 | |
|         # - A function that populates a keymaps passed in as an argument.
 | |
|         # - A tuple filled with triple's of:
 | |
|         #   ``(operator_id, operator_properties, keymap_item_args)``.
 | |
|         #
 | |
|         # Warning: currently 'from_dict' this is a list of one item,
 | |
|         # so internally we can swap the keymap function for the keymap it's self.
 | |
|         # This isn't very nice and may change, tool definitions shouldn't care about this.
 | |
|         "keymap",
 | |
|         # Optional data-block assosiated with this tool.
 | |
|         # (Typically brush name, usage depends on mode, we could use for non-brush ID's in other modes).
 | |
|         "data_block",
 | |
|         # Optional primary operator (for introspection only).
 | |
|         "operator",
 | |
|         # Optional draw settings (operator options, toolsettings).
 | |
|         "draw_settings",
 | |
|     )
 | |
| )
 | |
| del namedtuple
 | |
| 
 | |
| 
 | |
| def from_dict(kw_args):
 | |
|     """
 | |
|     Use so each tool can avoid defining all members of the named tuple.
 | |
|     Also convert the keymap from a tuple into a function
 | |
|     (since keymap is a callback).
 | |
|     """
 | |
|     kw = {
 | |
|         "description": None,
 | |
|         "icon": None,
 | |
|         "cursor": None,
 | |
|         "widget": None,
 | |
|         "keymap": None,
 | |
|         "data_block": None,
 | |
|         "operator": None,
 | |
|         "draw_settings": None,
 | |
|     }
 | |
|     kw.update(kw_args)
 | |
| 
 | |
|     keymap = kw["keymap"]
 | |
|     if kw["keymap"] is None:
 | |
|         pass
 | |
|     elif type(keymap) is tuple:
 | |
|         keymap = [_keymap_fn_from_seq(keymap)]
 | |
|     else:
 | |
|         keymap = [keymap]
 | |
|     kw["keymap"] = keymap
 | |
|     return ToolDef(**kw)
 | |
| 
 | |
| 
 | |
| def from_fn(fn):
 | |
|     """
 | |
|     Use as decorator so we can define functions.
 | |
|     """
 | |
|     return ToolDef.from_dict(fn())
 | |
| 
 | |
| 
 | |
| ToolDef.from_dict = from_dict
 | |
| ToolDef.from_fn = from_fn
 | |
| del from_dict
 | |
| del from_fn
 | |
| 
 | |
| 
 | |
| class ToolSelectPanelHelper:
 | |
|     """
 | |
|     Generic Class, can be used for any toolbar.
 | |
| 
 | |
|     - keymap_prefix:
 | |
|       The text prefix for each key-map for this spaces tools.
 | |
|     - tools_all():
 | |
|       Returns (context_mode, tools) tuple pair for all tools defined.
 | |
|     - tools_from_context(context, mode=None):
 | |
|       Returns tools available in this context.
 | |
| 
 | |
|     Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``None``.
 | |
|     """
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_class_from_space_type(space_type):
 | |
|         return next(
 | |
|             (cls for cls in ToolSelectPanelHelper.__subclasses__()
 | |
|              if cls.bl_space_type == space_type),
 | |
|             None
 | |
|         )
 | |
| 
 | |
|     @staticmethod
 | |
|     def _icon_value_from_icon_handle(icon_name):
 | |
|         import os
 | |
|         if icon_name is not None:
 | |
|             assert(type(icon_name) is str)
 | |
|             icon_value = _icon_cache.get(icon_name)
 | |
|             if icon_value is None:
 | |
|                 dirname = bpy.utils.resource_path('LOCAL')
 | |
|                 if not os.path.exists(dirname):
 | |
|                     # TODO(campbell): use a better way of finding datafiles.
 | |
|                     dirname = bpy.utils.resource_path('SYSTEM')
 | |
|                 filename = os.path.join(dirname, "datafiles", "icons", icon_name + ".dat")
 | |
|                 try:
 | |
|                     icon_value = bpy.app.icons.new_triangles_from_file(filename)
 | |
|                 except Exception as ex:
 | |
|                     if not os.path.exists(filename):
 | |
|                         print("Missing icons:", filename, ex)
 | |
|                     else:
 | |
|                         print("Corrupt icon:", filename, ex)
 | |
|                     # Use none as a fallback (avoids layout issues).
 | |
|                     if icon_name != "none":
 | |
|                         icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle("none")
 | |
|                     else:
 | |
|                         icon_value = 0
 | |
|                 _icon_cache[icon_name] = icon_value
 | |
|             return icon_value
 | |
|         else:
 | |
|             return 0
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tools_flatten(tools):
 | |
|         """
 | |
|         Flattens, skips None and calls generators.
 | |
|         """
 | |
|         for item in tools:
 | |
|             if item is None:
 | |
|                 yield None
 | |
|             elif type(item) is tuple:
 | |
|                 for sub_item in item:
 | |
|                     if sub_item is None:
 | |
|                         yield None
 | |
|                     elif _item_is_fn(sub_item):
 | |
|                         yield from sub_item(context)
 | |
|                     else:
 | |
|                         yield sub_item
 | |
|             else:
 | |
|                 if _item_is_fn(item):
 | |
|                     yield from item(context)
 | |
|                 else:
 | |
|                     yield item
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tools_flatten_with_tool_index(tools):
 | |
|         for item in tools:
 | |
|             if item is None:
 | |
|                 yield None, -1
 | |
|             elif type(item) is tuple:
 | |
|                 i = 0
 | |
|                 for sub_item in item:
 | |
|                     if sub_item is None:
 | |
|                         yield None
 | |
|                     elif _item_is_fn(sub_item):
 | |
|                         for item_dyn in sub_item(context):
 | |
|                             yield item_dyn, i
 | |
|                             i += 1
 | |
|                     else:
 | |
|                         yield sub_item, i
 | |
|                         i += 1
 | |
|             else:
 | |
|                 if _item_is_fn(item):
 | |
|                     for item_dyn in item(context):
 | |
|                         yield item_dyn, -1
 | |
|                 else:
 | |
|                     yield item, -1
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_get_active(context, space_type, mode, with_icon=False):
 | |
|         """
 | |
|         Return the active Python tool definition and icon name.
 | |
|         """
 | |
|         workspace = context.workspace
 | |
|         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
 | |
|         if cls is not None:
 | |
|             tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type, mode)
 | |
|             tool_active_text = getattr(tool_active, "name", None)
 | |
|             for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context, mode)):
 | |
|                 if item is not None:
 | |
|                     if item.text == tool_active_text:
 | |
|                         if with_icon:
 | |
|                             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
 | |
|                         else:
 | |
|                             icon_value = 0
 | |
|                         return (item, tool_active, icon_value)
 | |
|         return None, None, 0
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_get_by_name(context, space_type, text):
 | |
|         """
 | |
|         Return the active Python tool definition and index (if in sub-group, else -1).
 | |
|         """
 | |
|         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
 | |
|         if cls is not None:
 | |
|             for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)):
 | |
|                 if item is not None:
 | |
|                     if item.text == text:
 | |
|                         return (cls, item, index)
 | |
|         return None, None, -1
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_active_from_context(context, space_type, mode=None, create=False):
 | |
|         if space_type == 'VIEW_3D':
 | |
|             if mode is None:
 | |
|                 mode = context.mode
 | |
|             tool = context.workspace.tools.from_space_view3d_mode(mode, create=create)
 | |
|             if tool is not None:
 | |
|                 tool.refresh_from_context()
 | |
|                 return tool
 | |
|         elif space_type == 'IMAGE_EDITOR':
 | |
|             space_data = context.space_data
 | |
|             if mode is None:
 | |
|                 if space_data is None:
 | |
|                     mode = 'VIEW'
 | |
|                 else:
 | |
|                     mode = space_data.mode
 | |
|             tool = context.workspace.tools.from_space_image_mode(mode, create=create)
 | |
|             if tool is not None:
 | |
|                 tool.refresh_from_context()
 | |
|                 return tool
 | |
|         return None
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_text_from_button(context):
 | |
|         return context.button_operator.name
 | |
| 
 | |
|     @classmethod
 | |
|     def _km_action_simple(cls, kc, context_mode, text, keymap_fn):
 | |
|         if context_mode is None:
 | |
|             context_mode = "All"
 | |
|         km_idname = f"{cls.keymap_prefix:s} {context_mode:s}, {text:s}"
 | |
|         km = kc.keymaps.get(km_idname)
 | |
|         if km is None:
 | |
|             km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW')
 | |
|             keymap_fn[0](km)
 | |
|         keymap_fn[0] = km
 | |
| 
 | |
|     @classmethod
 | |
|     def register(cls):
 | |
|         wm = bpy.context.window_manager
 | |
| 
 | |
|         # XXX, should we be manipulating the user-keyconfig on load?
 | |
|         # Perhaps this should only add when keymap items don't already exist.
 | |
|         #
 | |
|         # This needs some careful consideration.
 | |
|         kc = wm.keyconfigs.user
 | |
| 
 | |
|         # Track which tool-group was last used for non-active groups.
 | |
|         # Blender stores the active tool-group index.
 | |
|         #
 | |
|         # {tool_name_first: index_in_group, ...}
 | |
|         cls._tool_group_active = {}
 | |
| 
 | |
|         # ignore in background mode
 | |
|         if kc is None:
 | |
|             return
 | |
| 
 | |
|         for context_mode, tools in cls.tools_all():
 | |
|             for item_parent in tools:
 | |
|                 if item_parent is None:
 | |
|                     continue
 | |
|                 for item in item_parent if (type(item_parent) is tuple) else (item_parent,):
 | |
|                     # skip None or generator function
 | |
|                     if item is None or _item_is_fn(item):
 | |
|                         continue
 | |
|                     keymap_data = item.keymap
 | |
|                     if keymap_data is not None and callable(keymap_data[0]):
 | |
|                         text = item.text
 | |
|                         icon_name = item.icon
 | |
|                         cls._km_action_simple(kc, context_mode, text, keymap_data)
 | |
| 
 | |
|     # -------------------------------------------------------------------------
 | |
|     # Layout Generators
 | |
|     #
 | |
|     # Meaning of recieved values:
 | |
|     # - Bool: True for a separator, otherwise False for regular tools.
 | |
|     # - None: Signal to finish (complete any final operations, e.g. add padding).
 | |
| 
 | |
|     @staticmethod
 | |
|     def _layout_generator_single_column(layout, scale_y):
 | |
|         col = layout.column(align=True)
 | |
|         col.scale_y = scale_y
 | |
|         is_sep = False
 | |
|         while True:
 | |
|             if is_sep is True:
 | |
|                 col = layout.column(align=True)
 | |
|                 col.scale_y = scale_y
 | |
|             elif is_sep is None:
 | |
|                 yield None
 | |
|                 return
 | |
|             is_sep = yield col
 | |
| 
 | |
|     @staticmethod
 | |
|     def _layout_generator_multi_columns(layout, column_count, scale_y):
 | |
|         scale_x = scale_y * 1.1
 | |
|         column_last = column_count - 1
 | |
| 
 | |
|         col = layout.column(align=True)
 | |
| 
 | |
|         row = col.row(align=True)
 | |
| 
 | |
|         row.scale_x = scale_x
 | |
|         row.scale_y = scale_y
 | |
| 
 | |
|         is_sep = False
 | |
|         column_index = 0
 | |
|         while True:
 | |
|             if is_sep is True:
 | |
|                 if column_index != column_last:
 | |
|                     row.label(text="")
 | |
|                 col = layout.column(align=True)
 | |
|                 row = col.row(align=True)
 | |
|                 row.scale_x = scale_x
 | |
|                 row.scale_y = scale_y
 | |
|                 column_index = 0
 | |
| 
 | |
|             is_sep = yield row
 | |
|             if is_sep is None:
 | |
|                 if column_index == column_last:
 | |
|                     row.label(text="")
 | |
|                     yield None
 | |
|                     return
 | |
| 
 | |
|             if column_index == column_count:
 | |
|                 column_index = 0
 | |
|                 row = col.row(align=True)
 | |
|                 row.scale_x = scale_x
 | |
|                 row.scale_y = scale_y
 | |
|             column_index += 1
 | |
| 
 | |
|     @staticmethod
 | |
|     def _layout_generator_detect_from_region(layout, region, scale_y):
 | |
|         """
 | |
|         Choose an appropriate layout for the toolbar.
 | |
|         """
 | |
|         # Currently this just checks the width,
 | |
|         # we could have different layouts as preferences too.
 | |
|         system = bpy.context.user_preferences.system
 | |
|         view2d = region.view2d
 | |
|         view2d_scale = (
 | |
|             view2d.region_to_view(1.0, 0.0)[0] -
 | |
|             view2d.region_to_view(0.0, 0.0)[0]
 | |
|         )
 | |
|         width_scale = region.width * view2d_scale / system.ui_scale
 | |
| 
 | |
|         if width_scale > 120.0:
 | |
|             show_text = True
 | |
|             column_count = 1
 | |
|         else:
 | |
|             show_text = False
 | |
|             # 2 column layout, disabled
 | |
|             if width_scale > 80.0:
 | |
|                 column_count = 2
 | |
|                 use_columns = True
 | |
|             else:
 | |
|                 column_count = 1
 | |
| 
 | |
|         if column_count == 1:
 | |
|             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y=scale_y)
 | |
|         else:
 | |
|             ui_gen = ToolSelectPanelHelper._layout_generator_multi_columns(layout, column_count=column_count, scale_y=scale_y)
 | |
| 
 | |
|         return ui_gen, show_text
 | |
| 
 | |
|     @classmethod
 | |
|     def draw_cls(cls, layout, context, detect_layout=True, scale_y=1.75):
 | |
|         # Use a classmethod so it can be called outside of a panel context.
 | |
| 
 | |
|         # XXX, this UI isn't very nice.
 | |
|         # We might need to create new button types for this.
 | |
|         # Since we probably want:
 | |
|         # - tool-tips that include multiple key shortcuts.
 | |
|         # - ability to click and hold to expose sub-tools.
 | |
| 
 | |
|         space_type = context.space_data.type
 | |
|         tool_active_text = getattr(
 | |
|             ToolSelectPanelHelper._tool_active_from_context(context, space_type),
 | |
|             "name", None,
 | |
|         )
 | |
| 
 | |
|         if detect_layout:
 | |
|             ui_gen, show_text = cls._layout_generator_detect_from_region(layout, context.region, scale_y)
 | |
|         else:
 | |
|             ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y)
 | |
|             show_text = True
 | |
| 
 | |
|         # Start iteration
 | |
|         ui_gen.send(None)
 | |
| 
 | |
|         for item in cls.tools_from_context(context):
 | |
|             if item is None:
 | |
|                 ui_gen.send(True)
 | |
|                 continue
 | |
| 
 | |
|             if type(item) is tuple:
 | |
|                 is_active = False
 | |
|                 i = 0
 | |
|                 for i, sub_item in enumerate(item):
 | |
|                     if sub_item is None:
 | |
|                         continue
 | |
|                     is_active = (sub_item.text == tool_active_text)
 | |
|                     if is_active:
 | |
|                         index = i
 | |
|                         break
 | |
|                 del i, sub_item
 | |
| 
 | |
|                 if is_active:
 | |
|                     # not ideal, write this every time :S
 | |
|                     cls._tool_group_active[item[0].text] = index
 | |
|                 else:
 | |
|                     index = cls._tool_group_active.get(item[0].text, 0)
 | |
| 
 | |
|                 item = item[index]
 | |
|                 use_menu = True
 | |
|             else:
 | |
|                 index = -1
 | |
|                 use_menu = False
 | |
| 
 | |
|             is_active = (item.text == tool_active_text)
 | |
|             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
 | |
| 
 | |
|             sub = ui_gen.send(False)
 | |
| 
 | |
|             if use_menu:
 | |
|                 sub.operator_menu_hold(
 | |
|                     "wm.tool_set_by_name",
 | |
|                     text=item.text if show_text else "",
 | |
|                     depress=is_active,
 | |
|                     menu="WM_MT_toolsystem_submenu",
 | |
|                     icon_value=icon_value,
 | |
|                 ).name = item.text
 | |
|             else:
 | |
|                 sub.operator(
 | |
|                     "wm.tool_set_by_name",
 | |
|                     text=item.text if show_text else "",
 | |
|                     depress=is_active,
 | |
|                     icon_value=icon_value,
 | |
|                 ).name = item.text
 | |
|         # Signal to finish any remaining layout edits.
 | |
|         ui_gen.send(None)
 | |
| 
 | |
|     def draw(self, context):
 | |
|         self.draw_cls(self.layout, context)
 | |
| 
 | |
|     @staticmethod
 | |
|     def draw_active_tool_header(
 | |
|             context, layout,
 | |
|             *,
 | |
|             show_tool_name=False,
 | |
|     ):
 | |
|         # BAD DESIGN WARNING: last used tool
 | |
|         workspace = context.workspace
 | |
|         space_type = workspace.tools_space_type
 | |
|         mode = workspace.tools_mode
 | |
|         item, tool, icon_value = ToolSelectPanelHelper._tool_get_active(context, space_type, mode, with_icon=True)
 | |
|         if item is None:
 | |
|             return None
 | |
|         # Note: we could show 'item.text' here but it makes the layout jitter when switching tools.
 | |
|         # Add some spacing since the icon is currently assuming regular small icon size.
 | |
|         layout.label(text="    " + item.text if show_tool_name else " ", icon_value=icon_value)
 | |
|         draw_settings = item.draw_settings
 | |
|         if draw_settings is not None:
 | |
|             draw_settings(context, layout, tool)
 | |
|         return tool
 | |
| 
 | |
| 
 | |
| # The purpose of this menu is to be a generic popup to select between tools
 | |
| # in cases when a single tool allows to select alternative tools.
 | |
| class WM_MT_toolsystem_submenu(Menu):
 | |
|     bl_label = ""
 | |
| 
 | |
|     @staticmethod
 | |
|     def _tool_group_from_button(context):
 | |
|         # Lookup the tool definitions based on the space-type.
 | |
|         cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type)
 | |
|         if cls is not None:
 | |
|             button_text = ToolSelectPanelHelper._tool_text_from_button(context)
 | |
|             for item_group in cls.tools_from_context(context):
 | |
|                 if type(item_group) is tuple:
 | |
|                     for sub_item in item_group:
 | |
|                         if sub_item.text == button_text:
 | |
|                             return cls, item_group
 | |
|         return None, None
 | |
| 
 | |
|     def draw(self, context):
 | |
|         layout = self.layout
 | |
|         layout.scale_y = 2.0
 | |
| 
 | |
|         cls, item_group = self._tool_group_from_button(context)
 | |
|         if item_group is None:
 | |
|             # Should never happen, just in case
 | |
|             layout.label(text="Unable to find toolbar group")
 | |
|             return
 | |
| 
 | |
|         for item in item_group:
 | |
|             if item is None:
 | |
|                 layout.separator()
 | |
|                 continue
 | |
|             icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon)
 | |
|             layout.operator(
 | |
|                 "wm.tool_set_by_name",
 | |
|                 text=item.text,
 | |
|                 icon_value=icon_value,
 | |
|             ).name = item.text
 | |
| 
 | |
| 
 | |
| def _activate_by_item(context, space_type, item, index):
 | |
|     tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True)
 | |
|     tool.setup(
 | |
|         name=item.text,
 | |
|         keymap=item.keymap[0].name if item.keymap is not None else "",
 | |
|         cursor=item.cursor or 'DEFAULT',
 | |
|         gizmo_group=item.widget or "",
 | |
|         data_block=item.data_block or "",
 | |
|         operator=item.operator or "",
 | |
|         index=index,
 | |
|     )
 | |
| 
 | |
| 
 | |
| def activate_by_name(context, space_type, text):
 | |
|     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
 | |
|     if item is None:
 | |
|         return False
 | |
|     _activate_by_item(context, space_type, item, index)
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def activate_by_name_or_cycle(context, space_type, text, offset=1):
 | |
| 
 | |
|     # Only cycle when the active tool is activated again.
 | |
|     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
 | |
|     if item is None:
 | |
|         return False
 | |
| 
 | |
|     tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type)
 | |
|     text_active = getattr(tool_active, "name", None)
 | |
| 
 | |
|     text_current = ""
 | |
|     for item_group in cls.tools_from_context(context):
 | |
|         if type(item_group) is tuple:
 | |
|             index_current = cls._tool_group_active.get(item_group[0].text, 0)
 | |
|             ok = False
 | |
|             for i, sub_item in enumerate(item_group):
 | |
|                 if sub_item.text == text:
 | |
|                     text_current = item_group[index_current].text
 | |
|                     break
 | |
|             if text_current:
 | |
|                 break
 | |
| 
 | |
|     if text_current == "":
 | |
|         return activate_by_name(context, space_type, text)
 | |
|     if text_active != text_current:
 | |
|         return activate_by_name(context, space_type, text_current)
 | |
| 
 | |
|     index_found = (tool_active.index + offset) % len(item_group)
 | |
| 
 | |
|     cls._tool_group_active[item_group[0].text] = index_found
 | |
| 
 | |
|     item_found = item_group[index_found]
 | |
|     _activate_by_item(context, space_type, item_found, index_found)
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def description_from_name(context, space_type, text, *, use_operator=True):
 | |
|     # Used directly for tooltips.
 | |
|     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
 | |
|     if item is None:
 | |
|         return False
 | |
| 
 | |
|     # Custom description.
 | |
|     description = item.description
 | |
|     if description is not None:
 | |
|         return description
 | |
| 
 | |
|     # Extract from the operator.
 | |
|     if use_operator:
 | |
|         operator = item.operator
 | |
| 
 | |
|         if operator is None:
 | |
|             if item.keymap is not None:
 | |
|                 operator = item.keymap[0].keymap_items[0].idname
 | |
| 
 | |
|         if operator is not None:
 | |
|             import _bpy
 | |
|             return _bpy.ops.get_rna(operator).bl_rna.description
 | |
|     return ""
 | |
| 
 | |
| 
 | |
| def keymap_from_name(context, space_type, text):
 | |
|     # Used directly for tooltips.
 | |
|     cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text)
 | |
|     if item is None:
 | |
|         return False
 | |
| 
 | |
|     keymap = item.keymap
 | |
|     # List container of one.
 | |
|     if keymap:
 | |
|         return keymap[0]
 | |
|     return ""
 | |
| 
 | |
| 
 | |
| def keymap_from_context(context, space_type):
 | |
|     """
 | |
|     Keymap for popup toolbar, currently generated each time.
 | |
|     """
 | |
| 
 | |
|     def modifier_keywords_from_item(kmi):
 | |
|         kw = {}
 | |
|         for (attr, default) in (
 | |
|                 ("any", False),
 | |
|                 ("shift", False),
 | |
|                 ("ctrl", False),
 | |
|                 ("alt", False),
 | |
|                 ("oskey", False),
 | |
|                 ("key_modifier", 'NONE'),
 | |
|         ):
 | |
|             val = getattr(kmi, attr)
 | |
|             if val != default:
 | |
|                 kw[attr] = val
 | |
|         return kw
 | |
| 
 | |
|     def dict_as_tuple(d):
 | |
|         return tuple((k, v) for (k, v) in sorted(d.items()))
 | |
| 
 | |
|     use_simple_keymap = False
 | |
| 
 | |
|     # Generate items when no keys are mapped.
 | |
|     use_auto_keymap = True
 | |
| 
 | |
|     # Temporary, only create so we can pass 'properties' to find_item_from_operator.
 | |
|     use_hack_properties = True
 | |
| 
 | |
|     km_name = "Toolbar Popup"
 | |
|     wm = context.window_manager
 | |
|     keyconf = wm.keyconfigs.active
 | |
|     keymap = keyconf.keymaps.get(km_name)
 | |
|     if keymap is None:
 | |
|         keymap = keyconf.keymaps.new(km_name, space_type='EMPTY', region_type='TEMPORARY')
 | |
|     for kmi in keymap.keymap_items:
 | |
|         keymap.keymap_items.remove(kmi)
 | |
| 
 | |
|     kmi_unique_args = set()
 | |
| 
 | |
|     cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
 | |
| 
 | |
|     items_all = [
 | |
|         # 0: tool
 | |
|         # 1: keymap item (direct access)
 | |
|         # 2: keymap item (newly calculated for toolbar)
 | |
|         [item, None, None]
 | |
|         for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context))
 | |
|         if item is not None
 | |
|     ]
 | |
| 
 | |
|     if use_hack_properties:
 | |
|         kmi_hack = keymap.keymap_items.new("wm.tool_set_by_name", 'A', 'PRESS')
 | |
|         kmi_hack_properties = kmi_hack.properties
 | |
| 
 | |
|         kmi_hack_brush_select = keymap.keymap_items.new("paint.brush_select", 'A', 'PRESS')
 | |
|         kmi_hack_brush_select_properties = kmi_hack_brush_select.properties
 | |
| 
 | |
|     if use_simple_keymap:
 | |
|         # Simply assign a key from A-Z.
 | |
|         for i, (item, _, _) in enumerate(items_all):
 | |
|             key = chr(ord('A') + i)
 | |
|             kmi = keymap.keymap_items.new("wm.tool_set_by_name", key, 'PRESS')
 | |
|             kmi.properties.name = item.text
 | |
|     else:
 | |
|         for item_container in items_all:
 | |
|             item = item_container[0]
 | |
|             # Only check the first item in the tools key-map (a little arbitrary).
 | |
| 
 | |
|             if use_hack_properties:
 | |
|                 # First check for direct assignment.
 | |
|                 kmi_hack_properties.name = item.text
 | |
|                 kmi_found = wm.keyconfigs.find_item_from_operator(
 | |
|                     idname="wm.tool_set_by_name",
 | |
|                     context='INVOKE_REGION_WIN',
 | |
|                     # properties={"name": item.text},
 | |
|                     properties=kmi_hack_properties,
 | |
|                 )[1]
 | |
| 
 | |
|                 if kmi_found is None:
 | |
|                     if item.data_block:
 | |
|                         # PAINT_OT_brush_select
 | |
|                         brush = bpy.data.brushes.get(item.data_block)
 | |
|                         if brush is not None:
 | |
|                             mode = context.active_object.mode
 | |
|                             attr_op, attr_brush = {
 | |
|                                 'SCULPT': ("sculpt_tool", "sculpt_tool"),
 | |
|                                 'WEIGHT_PAINT': ("weight_paint_tool", "vertex_tool"),
 | |
|                                 'VERTEX_PAINT': ("vertex_paint_tool", "vertex_tool"),
 | |
|                                 'TEXTURE_PAINT': ("texture_paint_tool", "image_tool"),
 | |
|                             }.get(mode, (None, None))
 | |
|                             if attr_op is not None:
 | |
|                                 kmi_hack_brush_select_properties.paint_mode = mode
 | |
|                                 setattr(kmi_hack_brush_select_properties, attr_op, getattr(brush, attr_brush))
 | |
|                                 kmi_found = wm.keyconfigs.find_item_from_operator(
 | |
|                                     idname="paint.brush_select",
 | |
|                                     context='INVOKE_REGION_WIN',
 | |
|                                     properties=kmi_hack_brush_select_properties,
 | |
|                                 )[1]
 | |
|                             elif mode == 'GPENCIL_PAINT':
 | |
|                                 # TODO: gpencil.brush_select
 | |
|                                 # By default no keys are mapped to this, pass.
 | |
|                                 pass
 | |
|                             else:
 | |
|                                 print("Unsupported mode:", mode)
 | |
|                             del mode, attr_op, attr_brush
 | |
| 
 | |
|             else:
 | |
|                 kmi_found = None
 | |
| 
 | |
|             if kmi_found is not None:
 | |
|                 pass
 | |
|             elif item.operator is not None:
 | |
|                 kmi_found = wm.keyconfigs.find_item_from_operator(
 | |
|                     idname=item.operator,
 | |
|                     context='INVOKE_REGION_WIN',
 | |
|                 )[1]
 | |
|             elif item.keymap is not None:
 | |
|                 kmi_first = item.keymap[0].keymap_items[0]
 | |
|                 kmi_found = wm.keyconfigs.find_item_from_operator(
 | |
|                     idname=kmi_first.idname,
 | |
|                     # properties=kmi_first.properties,  # prevents matches, don't use.
 | |
|                     context='INVOKE_REGION_WIN',
 | |
|                 )[1]
 | |
|                 del kmi_first
 | |
|             else:
 | |
|                 kmi_found = None
 | |
|             item_container[1] = kmi_found
 | |
| 
 | |
|         # More complex multi-pass test.
 | |
|         for item_container in items_all:
 | |
|             item, kmi_found = item_container[:2]
 | |
|             if kmi_found is None:
 | |
|                 continue
 | |
|             kmi_found_type = kmi_found.type
 | |
| 
 | |
|             # Only for single keys.
 | |
|             if (
 | |
|                     (len(kmi_found_type) == 1) or
 | |
|                     # When a tool is being activated instead of running an operator, just copy the shortcut.
 | |
|                     (kmi_found.idname in {"wm.tool_set_by_name", "WM_OT_tool_set_by_name"})
 | |
|             ):
 | |
|                 kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
 | |
|                 kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
 | |
|                 kmi.properties.name = item.text
 | |
|                 item_container[2] = kmi
 | |
|                 if use_auto_keymap:
 | |
|                     kmi_unique_args.add(dict_as_tuple(kmi_args))
 | |
| 
 | |
|         # Test for key_modifier, where alpha key is used as a 'key_modifier'
 | |
|         # (grease pencil holding 'D' for example).
 | |
|         for item_container in items_all:
 | |
|             item, kmi_found, kmi_exist = item_container
 | |
|             if kmi_found is None or kmi_exist:
 | |
|                 continue
 | |
| 
 | |
|             kmi_found_type = kmi_found.type
 | |
|             if kmi_found_type in {
 | |
|                     'LEFTMOUSE',
 | |
|                     'RIGHTMOUSE',
 | |
|                     'MIDDLEMOUSE',
 | |
|                     'BUTTON4MOUSE',
 | |
|                     'BUTTON5MOUSE',
 | |
|                     'BUTTON6MOUSE',
 | |
|                     'BUTTON7MOUSE',
 | |
|                     'ACTIONMOUSE',
 | |
|                     'SELECTMOUSE',
 | |
|             }:
 | |
|                 kmi_found_type = kmi_found.key_modifier
 | |
|                 # excludes 'NONE'
 | |
|                 if len(kmi_found_type) == 1:
 | |
|                     kmi_args = {"type": kmi_found_type, **modifier_keywords_from_item(kmi_found)}
 | |
|                     del kmi_args["key_modifier"]
 | |
|                     kmi_tuple = dict_as_tuple(kmi_args)
 | |
|                     if kmi_tuple in kmi_unique_args:
 | |
|                         continue
 | |
|                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
 | |
|                     kmi.properties.name = item.text
 | |
|                     item_container[2] = kmi
 | |
|                     if use_auto_keymap:
 | |
|                         kmi_unique_args.add(kmi_tuple)
 | |
| 
 | |
|         if use_auto_keymap:
 | |
|             # Map all unmapped keys to numbers,
 | |
|             # while this is a bit strange it means users will not confuse regular key bindings to ordered bindings.
 | |
| 
 | |
|             # Free events (last used first).
 | |
|             kmi_type_auto = ('ONE', 'TWO', 'THREE', 'FOUR', 'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE', 'ZERO')
 | |
|             # Map both numbers and num-pad.
 | |
|             kmi_type_dupe = {
 | |
|                 'ONE': 'NUMPAD_1',
 | |
|                 'TWO': 'NUMPAD_2',
 | |
|                 'THREE': 'NUMPAD_3',
 | |
|                 'FOUR': 'NUMPAD_4',
 | |
|                 'FIVE': 'NUMPAD_5',
 | |
|                 'SIX': 'NUMPAD_6',
 | |
|                 'SEVEN': 'NUMPAD_7',
 | |
|                 'EIGHT': 'NUMPAD_8',
 | |
|                 'NINE': 'NUMPAD_9',
 | |
|                 'ZERO': 'NUMPAD_0',
 | |
|             }
 | |
| 
 | |
|             def iter_free_events():
 | |
|                 for mod in ({}, {"shift": True}, {"ctrl": True}, {"alt": True}):
 | |
|                     for e in kmi_type_auto:
 | |
|                         yield (e, mod)
 | |
| 
 | |
|             iter_events = iter(iter_free_events())
 | |
| 
 | |
|             for item_container in items_all:
 | |
|                 item, kmi_found, kmi_exist = item_container
 | |
|                 if kmi_exist:
 | |
|                     continue
 | |
|                 kmi_args = None
 | |
|                 while True:
 | |
|                     key, mod = next(iter_events, (None, None))
 | |
|                     if key is None:
 | |
|                         break
 | |
|                     kmi_args = {"type": key, **mod}
 | |
|                     kmi_tuple = dict_as_tuple(kmi_args)
 | |
|                     if kmi_tuple in kmi_unique_args:
 | |
|                         kmi_args = None
 | |
|                     else:
 | |
|                         break
 | |
| 
 | |
|                 if kmi_args is not None:
 | |
|                     kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
 | |
|                     kmi.properties.name = item.text
 | |
|                     item_container[2] = kmi
 | |
|                     if use_auto_keymap:
 | |
|                         kmi_unique_args.add(kmi_tuple)
 | |
| 
 | |
|                     key = kmi_type_dupe.get(kmi_args["type"])
 | |
|                     if key is not None:
 | |
|                         kmi_args["type"] = key
 | |
|                         kmi_tuple = dict_as_tuple(kmi_args)
 | |
|                         if not kmi_tuple in kmi_unique_args:
 | |
|                             kmi = keymap.keymap_items.new(idname="wm.tool_set_by_name", value='PRESS', **kmi_args)
 | |
|                             kmi.properties.name = item.text
 | |
|                             if use_auto_keymap:
 | |
|                                 kmi_unique_args.add(kmi_tuple)
 | |
| 
 | |
|     if use_hack_properties:
 | |
|         keymap.keymap_items.remove(kmi_hack)
 | |
| 
 | |
|     if True:
 | |
|         # The shortcut will show, so we better support running it.
 | |
|         kmi_search = wm.keyconfigs.find_item_from_operator(
 | |
|             idname="wm.search_menu",
 | |
|             context='INVOKE_REGION_WIN',
 | |
|         )[1]
 | |
|         if kmi_search:
 | |
|             keymap.keymap_items.new(
 | |
|                 "wm.search_menu",
 | |
|                 type=kmi_search.type,
 | |
|                 value='PRESS',
 | |
|                 **modifier_keywords_from_item(kmi_search),
 | |
|             )
 | |
| 
 | |
|     wm.keyconfigs.update()
 | |
|     return keymap
 | |
| 
 | |
| 
 | |
| classes = (
 | |
|     WM_MT_toolsystem_submenu,
 | |
| )
 | |
| 
 | |
| if __name__ == "__main__":  # only for live edit.
 | |
|     from bpy.utils import register_class
 | |
|     for cls in classes:
 | |
|         register_class(cls)
 |