diff options
author | Campbell Barton <ideasman42@gmail.com> | 2019-03-14 06:15:29 +0300 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2019-03-15 09:05:18 +0300 |
commit | f29b80ff79ec7c6a2532d0b0eb5841d00cc7af1b (patch) | |
tree | fdb3ea22965f327a403d1ea3fb662796f16d9966 /release | |
parent | d4156b46d7a4acb22f3abfde14b05c46ead34b43 (diff) |
Tool System: add tool registration API
This mimics RNA style class registration,
keeping the same internal data types.
Currently there is a template which shows an example of adding a tool
group with a keymap.
Icon generation still needs to be exposed for general use.
Diffstat (limited to 'release')
-rw-r--r-- | release/scripts/modules/bpy/utils/__init__.py | 206 | ||||
-rw-r--r-- | release/scripts/templates_py/ui_tool_simple.py | 65 |
2 files changed, 271 insertions, 0 deletions
diff --git a/release/scripts/modules/bpy/utils/__init__.py b/release/scripts/modules/bpy/utils/__init__.py index ce7781517b8..81b62b59fa6 100644 --- a/release/scripts/modules/bpy/utils/__init__.py +++ b/release/scripts/modules/bpy/utils/__init__.py @@ -39,6 +39,7 @@ __all__ = ( "unregister_manual_map", "register_classes_factory", "register_submodule_factory", + "register_tool", "make_rna_paths", "manual_map", "previews", @@ -50,6 +51,7 @@ __all__ = ( "smpte_from_seconds", "units", "unregister_class", + "unregister_tool", "user_resource", "execfile", ) @@ -716,6 +718,210 @@ def register_submodule_factory(module_name, submodule_names): # ----------------------------------------------------------------------------- +# Tool Registration + + +def register_tool(tool_cls, *, after=None, separator=False, group=False): + """ + Register a tool in the toolbar. + + :arg tool: A tool subclass. + :type tool: :class:`bpy.types.WorkSpaceTool` subclass. + :arg space_type: Space type identifier. + :type space_type: string + :arg after: Optional identifiers this tool will be added after. + :type after: collection of strings or None. + :arg separator: When true, add a separator before this tool. + :type separator: bool + :arg group: When true, add a new nested group of tools. + :type group: bool + """ + space_type = tool_cls.bl_space_type + context_mode = tool_cls.bl_context_mode + + from bl_ui.space_toolsystem_common import ( + ToolSelectPanelHelper, + ToolDef, + ) + + cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type) + if cls is None: + raise Exception(f"Space type {space_type!r} has no toolbar") + tools = cls._tools[context_mode] + + # First sanity check + from bpy.types import WorkSpaceTool + tools_id = { + item.idname for item in ToolSelectPanelHelper._tools_flatten(tools) + if item is not None + } + if not issubclass(tool_cls, WorkSpaceTool): + raise Exception(f"Expected WorkSpaceTool subclass, not {type(tool_cls)!r}") + if tool_cls.bl_idname in tools_id: + raise Exception(f"Tool {tool_cls.bl_idname!r} already exists!") + del tools_id, WorkSpaceTool + + # Convert the class into a ToolDef. + def tool_from_class(tool_cls): + # Convert class to tuple, store in the class for removal. + tool_def = ToolDef.from_dict({ + "idname": tool_cls.bl_idname, + "label": tool_cls.bl_label, + "description": getattr(tool_cls, "bl_description", tool_cls.__doc__), + "icon": getattr(tool_cls, "bl_icon", None), + "cursor": getattr(tool_cls, "bl_cursor", None), + "widget": getattr(tool_cls, "bl_widget", None), + "keymap": getattr(tool_cls, "bl_keymap", None), + "data_block": getattr(tool_cls, "bl_data_block", None), + "operator": getattr(tool_cls, "bl_operator", None), + "draw_settings": getattr(tool_cls, "draw_settings", None), + "draw_cursor": getattr(tool_cls, "draw_cursor", None), + }) + tool_cls._bl_tool = tool_def + + keymap_data = tool_def.keymap + if keymap_data is not None: + if context_mode is None: + context_descr = "All" + else: + context_descr = context_mode.replace("_", " ").title() + from bpy import context + wm = context.window_manager + kc = wm.keyconfigs.default + if callable(keymap_data[0]): + cls._km_action_simple(kc, context_descr, tool_def.label, keymap_data) + return tool_def + + tool_converted = tool_from_class(tool_cls) + + if group: + # Create a new group + tool_converted = (tool_converted,) + + + tool_def_insert = ( + (None, tool_converted) if separator else + (tool_converted,) + ) + + def skip_to_end_of_group(seq, i): + i_prev = i + while i < len(seq) and seq[i] is not None: + i_prev = i + i += 1 + return i_prev + + changed = False + if after is not None: + for i, item in enumerate(tools): + if item is None: + pass + elif isinstance(item, ToolDef): + if item.idname in after: + i = skip_to_end_of_group(item, i) + tools[i + 1:i + 1] = tool_def_insert + changed = True + break + elif isinstance(item, tuple): + for j, sub_item in enumerate(item, 1): + if isinstance(sub_item, ToolDef): + if sub_item.idname in after: + if group: + # Can't add a group within a group, + # add a new group after this group. + i = skip_to_end_of_group(tools, i) + tools[i + 1:i + 1] = tool_def_insert + else: + j = skip_to_end_of_group(item, j) + item = item[:j + 1] + tool_def_insert + item[j + 1:] + tools[i] = item + changed = True + break + if changed: + break + + if not changed: + print("bpy.utils.register_tool: could not find 'after'", after) + if not changed: + tools.extend(tool_def_insert) + + +def unregister_tool(tool_cls): + space_type = tool_cls.bl_space_type + context_mode = tool_cls.bl_context_mode + + from bl_ui.space_toolsystem_common import ToolSelectPanelHelper + cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type) + if cls is None: + raise Exception(f"Space type {space_type!r} has no toolbar") + tools = cls._tools[context_mode] + + tool_def = tool_cls._bl_tool + try: + i = tools.index(tool_def) + except ValueError: + i = -1 + + def tool_list_clean(tool_list): + # Trim separators. + while tool_list and tool_list[-1] is None: + del tool_list[-1] + while tool_list and tool_list[0] is None: + del tool_list[0] + is_none_prev = False + # Remove duplicate separators. + for i in range(len(tool_list) - 1, -1, -1): + is_none = tool_list[i] is None + if is_none and prev_is_none: + del tool_list[i] + prev_is_none = is_none + + changed = False + if i != -1: + del tools[i] + tool_list_clean(tools) + changed = True + + if not changed: + for i, item in enumerate(tools): + if isinstance(item, tuple): + try: + j = item.index(tool_def) + except ValueError: + j = -1 + + if j != -1: + item_clean = list(item) + del item_clean[j] + tool_list_clean(item_clean) + if item_clean: + tools[i] = tuple(item_clean) + else: + del tools[i] + tool_list_clean(tools) + del item_clean + + # tuple(sub_item for sub_item in items if sub_item is not tool_def) + changed = True + break + + if not changed: + raise Exception(f"Unable to remove {tool_cls!r}") + del tool_cls._bl_tool + + keymap_data = tool_def.keymap + if keymap_data is not None: + from bpy import context + wm = context.window_manager + kc = wm.keyconfigs.default + km = kc.keymaps.get(keymap_data[0]) + if km is None: + print("Warning keymap {keymap_data[0]!r} not found!") + else: + kc.keymaps.remove(km) + + +# ----------------------------------------------------------------------------- # Manual lookups, each function has to return a basepath and a sequence # of... diff --git a/release/scripts/templates_py/ui_tool_simple.py b/release/scripts/templates_py/ui_tool_simple.py new file mode 100644 index 00000000000..9cd71d0160c --- /dev/null +++ b/release/scripts/templates_py/ui_tool_simple.py @@ -0,0 +1,65 @@ +# This example adds an object mode tool to the toolbar. +# This is just the circle-select and lasso tools tool. +import bpy +from bpy.utils.toolsystem import ToolDef +from bpy.types import WorkSpaceTool + +class MyTool(WorkSpaceTool): + bl_space_type='VIEW_3D' + bl_context_mode='OBJECT' + + # The prefix of the idname should be your add-on name. + bl_idname = "my_template.my_circle_select" + bl_label = "My Circle Select" + bl_description = ( + "This is a tooltip\n" + "with multiple lines" + ) + bl_icon = "ops.generic.select_circle" + bl_widget = None + bl_keymap = ( + ("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS'}, + {"properties": [("wait_for_input", False)]}), + ("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, + {"properties": [("mode", 'SUB'), ("wait_for_input", False)]}), + ) + + def draw_settings(context, layout, tool): + props = tool.operator_properties("view3d.select_circle") + layout.prop(props, "mode") + layout.prop(props, "radius") + + +class MyOtherTool(WorkSpaceTool): + bl_space_type='VIEW_3D' + bl_context_mode='OBJECT' + + bl_idname = "my_template.my_other_select" + bl_label = "My Lasso Tool Select" + bl_description = ( + "This is a tooltip\n" + "with multiple lines" + ) + bl_icon = "ops.generic.select_lasso" + bl_widget = None + bl_keymap = ( + ("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), + ("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, + {"properties": [("mode", 'SUB')]}), + ) + + def draw_settings(context, layout, tool): + props = tool.operator_properties("view3d.select_lasso") + layout.prop(props, "mode") + + +def register(): + bpy.utils.register_tool(MyTool, after={"builtin.scale_cage"}, separator=True, group=True) + bpy.utils.register_tool(MyOtherTool, after={MyTool.bl_idname}) + +def unregister(): + bpy.utils.unregister_tool(MyTool) + bpy.utils.unregister_tool(MyOtherTool) + +if __name__ == "__main__": + register() |