This repository has been archived on 2023-10-09. You can view files and clone it, but cannot push or open issues or pull requests.
Files
blender-archive/release/scripts/startup/bl_ui/space_toolsystem_common.py
Campbell Barton 9504652687 UI: move tool definitions to classes
Originally it was nice to have a small list of definitions
with tools inline.

However we need to be able to define drawing functions for tools
which Python can't easily inline.

Use function for keymap definition,
support creating a function from a tuple as well
(handy for simple key-maps).
2018-04-27 13:55:04 +02:00

414 lines
15 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__ = (
"ToolSelectPanelHelper",
"ToolDef",
)
# Support reloading icons.
if "_icon_cache" in locals():
release = bpy.app.icons.release
for icon_value in _icon_cache.values():
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
class ToolDef:
"""
Tool definition,
This class is never instanced, it's used as definition for tool types.
Since we want to define functions here, it's more convenient to declare class-methods
then functions in a dict or tuple.
"""
__slots__ = ()
def __new__(cls, *args, **kwargs):
raise RuntimeError("%s should not be instantiated" % cls)
def __init_subclass__(cls):
# All classes must have a name
assert(cls.text is not None)
# We must have a key-map or widget (otherwise the tool does nothing!)
assert(cls.keymap is not None or cls.widget is not None)
if type(cls.keymap) is tuple:
cls.keymap = _keymap_fn_from_seq(cls.keymap)
# The name to display in the interface.
text = None
# The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon.
icon = None
# An optional manipulator group to activate when the tool is set or None for no widget.
widget = None
# An optional triple of: ``(operator_id, operator_properties, keymap_item_args)``.
keymap = None
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):
Returns tools available in this context.
Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``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 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 os.path.exists(filename):
print("Missing icons:", filename, ex)
else:
print("Corrupt icon:", filename, ex)
icon_value = 0
_icon_cache[icon_name] = icon_value
return icon_value
else:
return 0
@staticmethod
def _tools_flatten(tools):
for item in tools:
if item is not None:
if type(item) is tuple:
for sub_item in item:
if sub_item is not None:
yield sub_item
else:
yield item
@classmethod
def _tool_vars_from_def(cls, item, context_mode):
# For now be strict about whats in this dict
# prevent accidental adding unknown keys.
text = item.text
icon_name = item.icon
mp_idname = item.widget
keymap_fn = item.keymap
if keymap_fn is None:
km, km_idname = (None, None)
else:
km_test = cls._tool_keymap.get((context_mode, text))
if km_test is None and context_mode is not None:
km_test = cls._tool_keymap[None, text]
km, km_idname = km_test
return (km_idname, mp_idname), icon_name
@staticmethod
def _tool_vars_from_active_with_index(context):
workspace = context.workspace
return (
(workspace.tool_keymap or None, workspace.tool_manipulator_group or None),
workspace.tool_index,
)
@staticmethod
def _tool_vars_from_button_with_index(context):
props = context.button_operator
return (
(props.keymap or None or None, props.manipulator_group or None),
props.index,
)
@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} {context_mode}, {text}"
km = kc.keymaps.get(km_idname)
if km is not None:
return km, km_idname
km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW')
keymap_fn(km)
return km, km_idname
@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
# {context_mode: {tool_name: (keymap, keymap_idname, manipulator_group_idname), ...}, ...}
cls._tool_keymap = {}
# 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,):
if item is None:
continue
keymap_data = item.keymap
if keymap_data is not None:
text = item.text
icon_name = item.icon
km, km_idname = cls._km_action_simple(kc, context_mode, text, keymap_data)
cls._tool_keymap[context_mode, text] = km, km_idname
def draw(self, 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.
context_mode = context.mode
tool_def_active, index_active = self._tool_vars_from_active_with_index(context)
layout = self.layout
scale_y = 2.0
# TODO(campbell): expose ui_scale.
view2d = context.region.view2d
ui_scale = (
view2d.region_to_view(1.0, 0.0)[0] -
view2d.region_to_view(0.0, 0.0)[0]
)
show_text = (context.region.width / ui_scale) > 100.0
del view2d, ui_scale
for tool_items in self.tools_from_context(context):
if tool_items:
col = layout.column(align=True)
col.scale_y = scale_y
for item in tool_items:
if item is None:
col = layout.column(align=True)
col.scale_y = scale_y
continue
if type(item) is tuple:
is_active = False
i = 0
for i, sub_item in enumerate(item):
if sub_item is None:
continue
tool_def, icon_name = self._tool_vars_from_def(sub_item, context_mode)
is_active = (tool_def == tool_def_active)
if is_active:
index = i
break
del i, sub_item
if is_active:
# not ideal, write this every time :S
self._tool_group_active[item[0].text] = index
else:
index = self._tool_group_active.get(item[0].text, 0)
item = item[index]
use_menu = True
else:
index = -1
use_menu = False
tool_def, icon_name = self._tool_vars_from_def(item, context_mode)
is_active = (tool_def == tool_def_active)
icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(icon_name)
if use_menu:
props = col.operator_menu_hold(
"wm.tool_set",
text=item.text if show_text else "",
depress=is_active,
menu="WM_MT_toolsystem_submenu",
icon_value=icon_value,
)
else:
props = col.operator(
"wm.tool_set",
text=item.text if show_text else "",
depress=is_active,
icon_value=icon_value,
)
props.keymap = tool_def[0] or ""
props.manipulator_group = tool_def[1] or ""
props.index = index
def tools_from_context(cls, context):
return (cls._tools[None], cls._tools.get(context.mode, ()))
@staticmethod
def _active_tool(context, with_icon=False):
"""
Return the active Python tool definition and icon name.
"""
workspace = context.workspace
space_type = workspace.tool_space_type
cls = next(
(cls for cls in ToolSelectPanelHelper.__subclasses__()
if cls.bl_space_type == space_type),
None
)
if cls is not None:
tool_def_active, index_active = ToolSelectPanelHelper._tool_vars_from_active_with_index(context)
context_mode = context.mode
for tool_items in cls.tools_from_context(context):
for item in cls._tools_flatten(tool_items):
tool_def, icon_name = cls._tool_vars_from_def(item, context_mode)
if (tool_def == tool_def_active):
if with_icon:
icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(icon_name)
else:
icon_value = 0
return (item, icon_value)
return None, 0
@staticmethod
def draw_active_tool_header(context, layout):
item, icon_value = ToolSelectPanelHelper._active_tool(context, with_icon=True)
if item is None:
layout.label("No Tool Found")
return
# Indent until we have better icon scaling.
layout.label(" " + item.text, icon_value=icon_value)
# 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):
context_mode = context.mode
# Lookup the tool definitions based on the space-type.
space_type = context.space_data.type
cls = next(
(cls for cls in ToolSelectPanelHelper.__subclasses__()
if cls.bl_space_type == space_type),
None
)
if cls is not None:
tool_def_button, index_button = cls._tool_vars_from_button_with_index(context)
for item_items in cls.tools_from_context(context):
for item_group in item_items:
if type(item_group) is tuple:
if index_button < len(item_group):
item = item_group[index_button]
tool_def, icon_name = cls._tool_vars_from_def(item, context_mode)
is_active = (tool_def == tool_def_button)
if is_active:
return cls, item_group, index_button
return None, None, -1
def draw(self, context):
context_mode = context.mode
layout = self.layout
layout.scale_y = 2.0
cls, item_group, index_active = self._tool_group_from_button(context)
if item_group is None:
# Should never happen, just in case
layout.label("Unable to find toolbar group")
return
index = 0
for item in item_group:
if item is None:
layout.separator()
continue
tool_def, icon_name = cls._tool_vars_from_def(item, context_mode)
icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(icon_name)
props = layout.operator(
"wm.tool_set",
text=item.text,
icon_value=icon_value,
)
props.keymap = tool_def[0] or ""
props.manipulator_group = tool_def[1] or ""
props.index = index
index += 1
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)