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:
parent
d4156b46d7
commit
f29b80ff79
|
@ -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...
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue