diff options
Diffstat (limited to 'release/scripts/startup/bl_ui/space_toolsystem_common.py')
-rw-r--r-- | release/scripts/startup/bl_ui/space_toolsystem_common.py | 683 |
1 files changed, 683 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_ui/space_toolsystem_common.py b/release/scripts/startup/bl_ui/space_toolsystem_common.py new file mode 100644 index 00000000000..20db56af416 --- /dev/null +++ b/release/scripts/startup/bl_ui/space_toolsystem_common.py @@ -0,0 +1,683 @@ +# ##### 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(): + if icon_value != 0: + 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 + + +def _item_is_fn(item): + return (not (type(item) is ToolDef) and callable(item)) + + +from collections import namedtuple +ToolDef = namedtuple( + "ToolDef", + ( + # The name to display in the interface. + "text", + # The name of the icon to use (found in ``release/datafiles/icons``) or None for no icon. + "icon", + # An optional cursor to use when this tool is active. + "cursor", + # An optional manipulator group to activate when the tool is set or None for no widget. + "widget", + # 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)``. + # + # Warning: currently 'from_dict' this is a list of one item, + # so internally we can swap the keymap function for the keymap it's self. + # This isn't very nice and may change, tool definitions shouldn't care about this. + "keymap", + # Optional data-block assosiated with this tool. + # (Typically brush name, usage depends on mode, we could use for non-brush ID's in other modes). + "data_block", + # Optional draw settings (operator options, toolsettings). + "draw_settings", + ) +) +del namedtuple + + +def from_dict(kw_args): + """ + Use so each tool can avoid defining all members of the named tuple. + Also convert the keymap from a tuple into a function + (since keymap is a callback). + """ + kw = { + "icon": None, + "cursor": None, + "widget": None, + "keymap": None, + "data_block": None, + "draw_settings": None, + } + kw.update(kw_args) + + keymap = kw["keymap"] + if kw["keymap"] is None: + pass + elif type(keymap) is tuple: + keymap = [_keymap_fn_from_seq(keymap)] + else: + keymap = [keymap] + kw["keymap"] = keymap + return ToolDef(**kw) + + +def from_fn(fn): + """ + Use as decorator so we can define functions. + """ + return ToolDef.from_dict(fn()) + + +ToolDef.from_dict = from_dict +ToolDef.from_fn = from_fn +del from_dict +del from_fn + + +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, mode=None): + Returns tools available in this context. + + Each tool is a 'ToolDef' or None for a separator in the toolbar, use ``None``. + """ + + @staticmethod + def _tool_class_from_space_type(space_type): + return next( + (cls for cls in ToolSelectPanelHelper.__subclasses__() + if cls.bl_space_type == space_type), + 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 os.path.exists(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 not os.path.exists(filename): + print("Missing icons:", filename, ex) + else: + print("Corrupt icon:", filename, ex) + # Use none as a fallback (avoids layout issues). + if icon_name != "none": + icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle("none") + else: + icon_value = 0 + _icon_cache[icon_name] = icon_value + return icon_value + else: + return 0 + + @staticmethod + def _tools_flatten(tools): + """ + Flattens, skips None and calls generators. + """ + for item in tools: + if item is None: + yield None + elif type(item) is tuple: + for sub_item in item: + if sub_item is None: + yield None + elif _item_is_fn(sub_item): + yield from sub_item(context) + else: + yield sub_item + else: + if _item_is_fn(item): + yield from item(context) + else: + yield item + + @staticmethod + def _tools_flatten_with_tool_index(tools): + for item in tools: + if item is None: + yield None, -1 + elif type(item) is tuple: + i = 0 + for sub_item in item: + if sub_item is None: + yield None + elif _item_is_fn(sub_item): + for item_dyn in sub_item(context): + yield item_dyn, i + i += 1 + else: + yield sub_item, i + i += 1 + else: + if _item_is_fn(item): + for item_dyn in item(context): + yield item_dyn, -1 + else: + yield item, -1 + + @staticmethod + def _tool_get_active(context, space_type, mode, with_icon=False): + """ + Return the active Python tool definition and icon name. + """ + workspace = context.workspace + cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type) + if cls is not None: + tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type, mode) + tool_active_text = getattr(tool_active, "name", None) + for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context, mode)): + if item is not None: + if item.text == tool_active_text: + if with_icon: + icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon) + else: + icon_value = 0 + return (item, tool_active, icon_value) + return None, None, 0 + + @staticmethod + def _tool_get_by_name(context, space_type, text): + """ + Return the active Python tool definition and index (if in sub-group, else -1). + """ + cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type) + if cls is not None: + for item, index in ToolSelectPanelHelper._tools_flatten_with_tool_index(cls.tools_from_context(context)): + if item is not None: + if item.text == text: + return (cls, item, index) + return None, None, -1 + + @staticmethod + def _tool_active_from_context(context, space_type, mode=None, create=False): + if space_type == 'VIEW_3D': + if mode is None: + mode = context.mode + tool = context.workspace.tools.from_space_view3d_mode(mode, create) + if tool is not None: + return tool + elif space_type == 'IMAGE_EDITOR': + space_data = context.space_data + if mode is None: + mode = space_data.mode + tool = context.workspace.tools.from_space_image_mode(mode, create) + if tool is not None: + return tool + return None + + @staticmethod + def _tool_text_from_button(context): + return context.button_operator.name + + @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 None: + km = kc.keymaps.new(km_idname, space_type=cls.bl_space_type, region_type='WINDOW') + keymap_fn[0](km) + keymap_fn[0] = km + + @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 + + # 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,): + # skip None or generator function + if item is None or _item_is_fn(item): + continue + keymap_data = item.keymap + if keymap_data is not None and callable(keymap_data[0]): + text = item.text + icon_name = item.icon + cls._km_action_simple(kc, context_mode, text, keymap_data) + + # ------------------------------------------------------------------------- + # Layout Generators + # + # Meaning of recieved values: + # - Bool: True for a separator, otherwise False for regular tools. + # - None: Signal to finish (complete any final operations, e.g. add padding). + + @staticmethod + def _layout_generator_single_column(layout, scale_y): + col = layout.column(align=True) + col.scale_y = scale_y + is_sep = False + while True: + if is_sep is True: + col = layout.column(align=True) + col.scale_y = scale_y + elif is_sep is None: + yield None + return + is_sep = yield col + + @staticmethod + def _layout_generator_multi_columns(layout, column_count, scale_y): + scale_x = scale_y * 1.1 + column_last = column_count - 1 + + col = layout.column(align=True) + + row = col.row(align=True) + + row.scale_x = scale_x + row.scale_y = scale_y + + is_sep = False + column_index = 0 + while True: + if is_sep is True: + if column_index != column_last: + row.label("") + col = layout.column(align=True) + row = col.row(align=True) + row.scale_x = scale_x + row.scale_y = scale_y + column_index = 0 + + is_sep = yield row + if is_sep is None: + if column_index == column_last: + row.label("") + yield None + return + + if column_index == column_count: + column_index = 0 + row = col.row(align=True) + row.scale_x = scale_x + row.scale_y = scale_y + column_index += 1 + + @staticmethod + def _layout_generator_detect_from_region(layout, region, scale_y): + """ + Choose an appropriate layout for the toolbar. + """ + # Currently this just checks the width, + # we could have different layouts as preferences too. + system = bpy.context.user_preferences.system + view2d = region.view2d + view2d_scale = ( + view2d.region_to_view(1.0, 0.0)[0] - + view2d.region_to_view(0.0, 0.0)[0] + ) + width_scale = region.width * view2d_scale / system.ui_scale + + if width_scale > 120.0: + show_text = True + column_count = 1 + else: + show_text = False + # 2 column layout, disabled + if width_scale > 80.0: + column_count = 2 + use_columns = True + else: + column_count = 1 + + if column_count == 1: + ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y=scale_y) + else: + ui_gen = ToolSelectPanelHelper._layout_generator_multi_columns(layout, column_count=column_count, scale_y=scale_y) + + return ui_gen, show_text + + @classmethod + def draw_cls(cls, layout, context, detect_layout=True, scale_y=1.75): + # Use a classmethod so it can be called outside of a panel 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. + + space_type = context.space_data.type + tool_active_text = getattr( + ToolSelectPanelHelper._tool_active_from_context(context, space_type), + "name", None, + ) + + if detect_layout: + ui_gen, show_text = cls._layout_generator_detect_from_region(layout, context.region, scale_y) + else: + ui_gen = ToolSelectPanelHelper._layout_generator_single_column(layout, scale_y) + show_text = True + + # Start iteration + ui_gen.send(None) + + for item in cls.tools_from_context(context): + if item is None: + ui_gen.send(True) + continue + + if type(item) is tuple: + is_active = False + i = 0 + for i, sub_item in enumerate(item): + if sub_item is None: + continue + is_active = (sub_item.text == tool_active_text) + if is_active: + index = i + break + del i, sub_item + + if is_active: + # not ideal, write this every time :S + cls._tool_group_active[item[0].text] = index + else: + index = cls._tool_group_active.get(item[0].text, 0) + + item = item[index] + use_menu = True + else: + index = -1 + use_menu = False + + is_active = (item.text == tool_active_text) + icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon) + + sub = ui_gen.send(False) + + if use_menu: + sub.operator_menu_hold( + "wm.tool_set_by_name", + text=item.text if show_text else "", + depress=is_active, + menu="WM_MT_toolsystem_submenu", + icon_value=icon_value, + ).name = item.text + else: + sub.operator( + "wm.tool_set_by_name", + text=item.text if show_text else "", + depress=is_active, + icon_value=icon_value, + ).name = item.text + # Signal to finish any remaining layout edits. + ui_gen.send(None) + + def draw(self, context): + self.draw_cls(self.layout, context) + + @staticmethod + def draw_active_tool_header(context, layout): + # BAD DESIGN WARNING: last used tool + workspace = context.workspace + space_type = workspace.tools_space_type + mode = workspace.tools_mode + item, tool, icon_value = ToolSelectPanelHelper._tool_get_active(context, space_type, mode, with_icon=True) + if item is None: + return + # Note: we could show 'item.text' here but it makes the layout jitter when switcuing tools. + layout.label(" ", icon_value=icon_value) + draw_settings = item.draw_settings + if draw_settings is not None: + draw_settings(context, layout, tool) + + +# 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): + # Lookup the tool definitions based on the space-type. + cls = ToolSelectPanelHelper._tool_class_from_space_type(context.space_data.type) + if cls is not None: + button_text = ToolSelectPanelHelper._tool_text_from_button(context) + for item_group in cls.tools_from_context(context): + if type(item_group) is tuple: + for sub_item in item_group: + if sub_item.text == button_text: + return cls, item_group + return None, None + + def draw(self, context): + layout = self.layout + layout.scale_y = 2.0 + + cls, item_group = 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 + + for item in item_group: + if item is None: + layout.separator() + continue + icon_value = ToolSelectPanelHelper._icon_value_from_icon_handle(item.icon) + layout.operator( + "wm.tool_set_by_name", + text=item.text, + icon_value=icon_value, + ).name = item.text + + +def _activate_by_item(context, space_type, item, index): + tool = ToolSelectPanelHelper._tool_active_from_context(context, space_type, create=True) + tool.setup( + name=item.text, + keymap=item.keymap[0].name if item.keymap is not None else "", + cursor=item.cursor or 'DEFAULT', + manipulator_group=item.widget or "", + data_block=item.data_block or "", + index=index, + ) + + +def activate_by_name(context, space_type, text): + cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text) + if item is None: + return False + _activate_by_item(context, space_type, item, index) + return True + + +def activate_by_name_or_cycle(context, space_type, text, offset=1): + + # Only cycle when the active tool is activated again. + cls, item, index = ToolSelectPanelHelper._tool_get_by_name(context, space_type, text) + if item is None: + return False + + tool_active = ToolSelectPanelHelper._tool_active_from_context(context, space_type) + text_active = getattr(tool_active, "name", None) + + text_current = "" + for item_group in cls.tools_from_context(context): + if type(item_group) is tuple: + index_current = cls._tool_group_active.get(item_group[0].text, 0) + ok = False + for i, sub_item in enumerate(item_group): + if sub_item.text == text: + text_current = item_group[index_current].text + break + if text_current: + break + + if text_current == "": + return activate_by_name(context, space_type, text) + if text_active != text_current: + return activate_by_name(context, space_type, text_current) + + index_found = (tool_active.index + offset) % len(item_group) + + cls._tool_group_active[item_group[0].text] = index_found + + item_found = item_group[index_found] + _activate_by_item(context, space_type, item_found, index_found) + return True + + +def keymap_from_context(context, space_type): + """ + Keymap for popup toolbar, currently generated each time. + """ + use_simple_keymap = False + km_name = "Toolbar Popup" + wm = context.window_manager + keyconf = wm.keyconfigs.active + keymap = keyconf.keymaps.get(km_name) + if keymap is None: + keymap = keyconf.keymaps.new(km_name, space_type='EMPTY', region_type='TEMPORARY') + for kmi in keymap.keymap_items: + keymap.keymap_items.remove(kmi) + + items = [] + cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type) + for i, item in enumerate( + ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context)) + ): + if item is not None: + if use_simple_keymap: + # Simply assign a key from A-Z + items.append(((chr(ord('A') + i)), item.text)) + kmi = keymap.keymap_items.new("wm.tool_set_by_name", key, 'PRESS') + kmi.properties.name = item.text + continue + + if not item.keymap: + continue + + # Only check the first item in the tools key-map (a little arbitrary). + kmi_first = item.keymap[0].keymap_items[0] + kmi_found = wm.keyconfigs.find_item_from_operator( + idname=kmi_first.idname, + # properties=kmi_first.properties, # prevents matches, don't use. + )[1] + if kmi_found is not None: + kmi_found_type = kmi_found.type + # Only for single keys. + if len(kmi_found_type) == 1: + kmi = keymap.keymap_items.new( + idname="wm.tool_set_by_name", + type=kmi_found_type, + value='PRESS', + any=kmi_found.any, + shift=kmi_found.shift, + ctrl=kmi_found.ctrl, + alt=kmi_found.alt, + oskey=kmi_found.oskey, + key_modifier=kmi_found.key_modifier, + ) + kmi.properties.name = item.text + + wm.keyconfigs.update() + return keymap + + +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) |