Files
blender/release/scripts/startup/bl_ui/space_toolsystem_common.py

423 lines
15 KiB
Python
Raw Normal View History

# ##### 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
2017-11-02 23:05:13 +11:00
from bpy.types import (
Menu,
)
__all__ = (
"ToolSelectPanelHelper",
"ToolDef",
)
2018-04-26 15:18:47 +02:00
# 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
2018-04-24 09:19:28 +02:00
# (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
# Optional keymap for tool, either:
# - A function that populates a keymaps passed in as an argument.
# - A tuple filled with triple's of:
# ``(operator_id, operator_properties, keymap_item_args)``.
keymap = None
# Optional draw settings (operator options, toolsettings).
draw_settings = 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``.
"""
2018-04-24 09:19:28 +02:00
@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')
2018-04-24 09:19:28 +02:00
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 not os.path.exists(filename):
2018-04-24 09:19:28 +02:00
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
2017-11-02 23:05:13 +11:00
@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
2018-04-24 09:19:28 +02:00
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,
)
2017-11-02 23:05:13 +11:00
@classmethod
def _km_action_simple(cls, kc, context_mode, text, keymap_fn):
2018-04-26 08:10:52 +02:00
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
2018-04-26 08:10:52 +02:00
# {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
2018-04-24 09:19:28 +02:00
scale_y = 2.0
2018-04-24 16:33:38 +02:00
# 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:
2017-11-02 15:52:16 +11:00
col = layout.column(align=True)
2018-04-24 09:19:28 +02:00
col.scale_y = scale_y
for item in tool_items:
if item is None:
2017-11-02 15:52:16 +11:00
col = layout.column(align=True)
2018-04-24 09:19:28 +02:00
col.scale_y = scale_y
continue
if type(item) is tuple:
2017-11-02 23:05:13 +11:00
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)
2017-11-02 23:05:13 +11:00
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)
2017-11-02 23:05:13 +11:00
item = item[index]
use_menu = True
else:
2017-11-02 23:05:13 +11:00
index = -1
use_menu = False
tool_def, icon_name = self._tool_vars_from_def(item, context_mode)
is_active = (tool_def == tool_def_active)
2018-04-24 09:19:28 +02:00
icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(icon_name)
2017-11-02 23:05:13 +11:00
if use_menu:
props = col.operator_menu_hold(
"wm.tool_set",
text=item.text if show_text else "",
2017-11-02 23:05:13 +11:00
depress=is_active,
menu="WM_MT_toolsystem_submenu",
2018-04-24 09:19:28 +02:00
icon_value=icon_value,
2017-11-02 23:05:13 +11:00
)
else:
props = col.operator(
"wm.tool_set",
text=item.text if show_text else "",
2017-11-02 23:05:13 +11:00
depress=is_active,
2018-04-24 09:19:28 +02:00
icon_value=icon_value,
2017-11-02 23:05:13 +11:00
)
props.keymap = tool_def[0] or ""
props.manipulator_group = tool_def[1] or ""
2017-11-02 23:05:13 +11:00
props.index = index
def tools_from_context(cls, context):
return (cls._tools[None], cls._tools.get(context.mode, ()))
2018-04-27 10:49:20 +02:00
@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)
2018-04-27 10:49:20 +02:00
draw_settings = item.draw_settings
if draw_settings is not None:
draw_settings(context, layout)
2018-04-27 10:49:20 +02:00
2017-11-02 23:05:13 +11:00
# 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
2017-11-02 23:05:13 +11:00
# 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)
2017-11-02 23:05:13 +11:00
for item_items in cls.tools_from_context(context):
for item_group in item_items:
if type(item_group) is tuple:
2017-11-02 23:05:13 +11:00
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)
2017-11-02 23:05:13 +11:00
if is_active:
return cls, item_group, index_button
return None, None, -1
def draw(self, context):
context_mode = context.mode
2017-11-02 23:05:13 +11:00
layout = self.layout
2018-04-24 09:19:28 +02:00
layout.scale_y = 2.0
2017-11-02 23:05:13 +11:00
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")
2017-11-02 23:05:13 +11:00
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)
2018-04-24 09:19:28 +02:00
icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(icon_name)
2017-11-02 23:05:13 +11:00
props = layout.operator(
"wm.tool_set",
text=item.text,
2018-04-24 09:19:28 +02:00
icon_value=icon_value,
2017-11-02 23:05:13 +11:00
)
props.keymap = tool_def[0] or ""
props.manipulator_group = tool_def[1] or ""
2017-11-02 23:05:13 +11:00
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)