Tool System: add tool registration API
authorCampbell Barton <ideasman42@gmail.com>
Thu, 14 Mar 2019 03:15:29 +0000 (14:15 +1100)
committerCampbell Barton <ideasman42@gmail.com>
Fri, 15 Mar 2019 06:05:18 +0000 (17:05 +1100)
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.

release/scripts/modules/bpy/utils/__init__.py
release/scripts/templates_py/ui_tool_simple.py [new file with mode: 0644]

index ce7781517b85aa17a74284d3967b4c10bd680c72..81b62b59fa6a23f7778ca76d33770fe3ccd1a9de 100644 (file)
@@ -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",
 )
@@ -715,6 +717,210 @@ def register_submodule_factory(module_name, submodule_names):
     return register, unregister
 
 
+# -----------------------------------------------------------------------------
+# 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 (file)
index 0000000..9cd71d0
--- /dev/null
@@ -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()