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.
This commit is contained in:
Campbell Barton 2019-03-14 14:15:29 +11:00
parent d4156b46d7
commit f29b80ff79
2 changed files with 271 additions and 0 deletions

View 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...

View File

@ -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()