From b752de9e0da4e8ad694de25497275f66168a2df4 Mon Sep 17 00:00:00 2001 From: Ryan Inch Date: Mon, 16 Mar 2020 02:48:02 -0400 Subject: Collection Manager: Add QCD System. Task: T69577 Adds a Quick Content Display (QCD) system to the Collection Manager. This consists of a 3D View Header widget and a floating panel similar to the layers system in blender 2.7x, along with hotkeys to view/move objects to QCD slots, and additions to the main Collection Manager popup to allow you to manage which collections correspond to which slots. --- object_collection_manager/__init__.py | 118 +++- object_collection_manager/icons/minus.png | Bin 0 -> 1934 bytes object_collection_manager/internals.py | 195 +++++- object_collection_manager/operators.py | 9 + object_collection_manager/preferences.py | 512 ++++++++++++++ object_collection_manager/qcd_move_widget.py | 969 +++++++++++++++++++++++++++ object_collection_manager/qcd_operators.py | 286 ++++++++ object_collection_manager/ui.py | 246 +++++-- 8 files changed, 2277 insertions(+), 58 deletions(-) create mode 100644 object_collection_manager/icons/minus.png create mode 100644 object_collection_manager/preferences.py create mode 100644 object_collection_manager/qcd_move_widget.py create mode 100644 object_collection_manager/qcd_operators.py diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index 4d895df7..37bf9c3a 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (1,10,0), + "version": (2,0,0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -36,19 +36,30 @@ if "bpy" in locals(): importlib.reload(internals) importlib.reload(operators) + importlib.reload(preferences) + importlib.reload(qcd_move_widget) + importlib.reload(qcd_operators) importlib.reload(ui) else: from . import internals from . import operators + from . import preferences + from . import qcd_move_widget + from . import qcd_operators from . import ui +import os import bpy +import bpy.utils.previews +from bpy.app.handlers import persistent from bpy.types import PropertyGroup from bpy.props import ( CollectionProperty, + EnumProperty, IntProperty, BoolProperty, + StringProperty, PointerProperty, ) @@ -65,6 +76,10 @@ class CollectionManagerProperties(PropertyGroup): in_phantom_mode: BoolProperty(default=False) + update_header: CollectionProperty(type=internals.CMListCollection) + + qcd_slots_blend_data: StringProperty() + addon_keymaps = [] @@ -87,30 +102,131 @@ classes = ( operators.CMRemoveCollectionOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, + preferences.CMPreferences, + qcd_move_widget.QCDMoveWidget, + qcd_operators.MoveToQCDSlot, + qcd_operators.ViewQCDSlot, + qcd_operators.ViewMoveQCDSlot, + qcd_operators.RenumerateQCDSlots, ui.CM_UL_items, ui.CollectionManager, ui.CMRestrictionTogglesPanel, CollectionManagerProperties, ) +@persistent +def depsgraph_update_post_handler(dummy): + if qcd_operators.move_triggered: + qcd_operators.move_triggered = False + return + + qcd_operators.move_selection.clear() + qcd_operators.move_active = None + qcd_operators.get_move_selection() + qcd_operators.get_move_active() + +@persistent +def save_internal_data(dummy): + cm = bpy.context.scene.collection_manager + + cm.qcd_slots_blend_data = internals.qcd_slots.get_data_for_blend() + +@persistent +def load_internal_data(dummy): + cm = bpy.context.scene.collection_manager + data = cm.qcd_slots_blend_data + + if not data: + return + + internals.qcd_slots.load_blend_data(data) + def register(): for cls in classes: bpy.utils.register_class(cls) + + pcoll = bpy.utils.previews.new() + icons_dir = os.path.join(os.path.dirname(__file__), "icons") + pcoll.load("active_icon_base", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + pcoll.load("active_icon_text", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + pcoll.load("active_icon_text_sel", os.path.join(icons_dir, "minus.png"), 'IMAGE', True) + ui.preview_collections["icons"] = pcoll + + bpy.types.Scene.collection_manager = PointerProperty(type=CollectionManagerProperties) + bpy.types.VIEW3D_HT_header.append(ui.view3d_header_qcd_slots) + # create the global menu hotkey wm = bpy.context.window_manager km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS') addon_keymaps.append((km, kmi)) + # create qcd hotkeys + qcd_hotkeys = [ + ["ONE", False, "1"], + ["TWO", False, "2"], + ["THREE", False, "3"], + ["FOUR", False, "4"], + ["FIVE", False, "5"], + ["SIX", False, "6"], + ["SEVEN", False, "7"], + ["EIGHT", False, "8"], + ["NINE", False, "9"], + ["ZERO", False, "10"], + ["ONE", True, "11"], + ["TWO", True, "12"], + ["THREE", True, "13"], + ["FOUR", True, "14"], + ["FIVE", True, "15"], + ["SIX", True, "16"], + ["SEVEN", True, "17"], + ["EIGHT", True, "18"], + ["NINE", True, "19"], + ["ZERO", True, "20"], + ] + + for key in qcd_hotkeys: + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.view_qcd_slot', key[0], 'PRESS', alt=key[1]) + kmi.properties.slot = key[2] + kmi.properties.toggle = False + addon_keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.view_qcd_slot', key[0], 'PRESS',shift=True, alt=key[1]) + kmi.properties.slot = key[2] + kmi.properties.toggle = True + addon_keymaps.append((km, kmi)) + + km = wm.keyconfigs.addon.keymaps.new(name='Object Mode') + kmi = km.keymap_items.new('view3d.qcd_move_widget', 'V', 'PRESS') + addon_keymaps.append((km, kmi)) + + bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler) + bpy.app.handlers.save_pre.append(save_internal_data) + bpy.app.handlers.load_post.append(load_internal_data) + def unregister(): + bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler) + bpy.app.handlers.save_pre.remove(save_internal_data) + bpy.app.handlers.load_post.remove(load_internal_data) + for cls in classes: bpy.utils.unregister_class(cls) + for pcoll in ui.preview_collections.values(): + bpy.utils.previews.remove(pcoll) + ui.preview_collections.clear() + ui.last_icon_theme_text = None + ui.last_icon_theme_text_sel = None + del bpy.types.Scene.collection_manager + bpy.types.VIEW3D_HT_header.remove(ui.view3d_header_qcd_slots) + # remove keymaps when add-on is deactivated for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) diff --git a/object_collection_manager/icons/minus.png b/object_collection_manager/icons/minus.png new file mode 100644 index 00000000..dff25acd Binary files /dev/null and b/object_collection_manager/icons/minus.png differ diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py index e7f63884..5ebc6025 100644 --- a/object_collection_manager/internals.py +++ b/object_collection_manager/internals.py @@ -25,46 +25,183 @@ from bpy.types import ( Operator, ) -from bpy.props import StringProperty +from bpy.props import ( + StringProperty, + IntProperty, +) layer_collections = {} - collection_tree = [] - expanded = [] - -max_lvl = 0 row_index = 0 +max_lvl = 0 def get_max_lvl(): return max_lvl + +class QCDSlots(): + _slots = {} + overrides = {} + allow_update = True + + def __iter__(self): + return self._slots.items().__iter__() + + def __repr__(self): + return self._slots.__repr__() + + def __contains__(self, key): + try: + int(key) + return key in self._slots + + except ValueError: + return key in self._slots.values() + + return False + + def get_data_for_blend(self): + return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}" + + def load_blend_data(self, data): + decoupled_data = data.split("\n") + blend_slots = eval(decoupled_data[0]) + blend_overrides = eval(decoupled_data[1]) + + self._slots = blend_slots + self.overrides = blend_overrides + + def length(self): + return len(self._slots) + + def get_idx(self, name, r_value=None): + for k, v in self._slots.items(): + if v == name: + return k + + return r_value + + def get_name(self, idx, r_value=None): + if idx in self._slots: + return self._slots[idx] + + return r_value + + def add_slot(self, idx, name): + self._slots[idx] = name + + def update_slot(self, idx, name): + self._slots[idx] = name + + def del_slot(self, slot): + try: + int(slot) + del self._slots[slot] + + except ValueError: + idx = self.get_idx(slot) + del self._slots[idx] + + def clear(self): + self._slots.clear() + +qcd_slots = QCDSlots() + + def update_col_name(self, context): + global layer_collections + global qcd_slots + if self.name != self.last_name: if self.name == '': self.name = self.last_name return if self.last_name != '': + # update collection name layer_collections[self.last_name]["ptr"].collection.name = self.name + # update qcd_slot + idx = qcd_slots.get_idx(self.last_name) + if idx: + qcd_slots.update_slot(idx, self.name) + update_property_group(context) self.last_name = self.name +def update_qcd_slot(self, context): + global qcd_slots + + if not qcd_slots.allow_update: + return + + update_needed = False + + try: + int(self.qcd_slot) + except: + + if self.qcd_slot == "": + qcd_slots.del_slot(self.name) + qcd_slots.overrides[self.name] = True + + if self.name in qcd_slots: + qcd_slots.allow_update = False + self.qcd_slot = qcd_slots.get_idx(self.name) + qcd_slots.allow_update = True + + if self.name in qcd_slots.overrides: + qcd_slots.allow_update = False + self.qcd_slot = "" + qcd_slots.allow_update = True + + return + + if self.name in qcd_slots: + qcd_slots.del_slot(self.name) + update_needed = True + + if self.qcd_slot in qcd_slots: + qcd_slots.overrides[qcd_slots.get_name(self.qcd_slot)] = True + qcd_slots.del_slot(self.qcd_slot) + update_needed = True + + if int(self.qcd_slot) > 20: + self.qcd_slot = "20" + + if int(self.qcd_slot) < 1: + self.qcd_slot = "1" + + qcd_slots.add_slot(self.qcd_slot, self.name) + + if self.name in qcd_slots.overrides: + del qcd_slots.overrides[self.name] + + + if update_needed: + update_property_group(context) + + class CMListCollection(PropertyGroup): name: StringProperty(update=update_col_name) last_name: StringProperty() + qcd_slot: StringProperty(name="QCD Slot", update=update_qcd_slot) -def update_collection_tree(context): +def update_collection_tree(context, renumerate=False): global max_lvl global row_index + global collection_tree + global layer_collections + global qcd_slots + collection_tree.clear() layer_collections.clear() + max_lvl = 0 row_index = 0 - layer_collection = context.view_layer.layer_collection init_laycol_list = layer_collection.children @@ -85,6 +222,37 @@ def update_collection_tree(context): for laycol in master_laycol["children"]: collection_tree.append(laycol) + # update qcd + for x in range(20): + qcd_slot = qcd_slots.get_name(str(x+1)) + if qcd_slot and not layer_collections.get(qcd_slot, None): + qcd_slots.del_slot(qcd_slot) + + # update autonumeration + if qcd_slots.length() < 20: + lvl = 0 + num = 1 + while lvl <= max_lvl: + if num > 20: + break + + for laycol in layer_collections.values(): + if num > 20: + break + + if int(laycol["lvl"]) == lvl: + if laycol["name"] in qcd_slots.overrides: + if not renumerate: + num += 1 + continue + + if str(num) not in qcd_slots and laycol["name"] not in qcd_slots: + qcd_slots.add_slot(str(num), laycol["name"]) + + num += 1 + + lvl += 1 + def get_all_collections(context, collections, parent, tree, level=0, visible=False): global row_index @@ -122,20 +290,29 @@ def get_all_collections(context, collections, parent, tree, level=0, visible=Fal get_all_collections(context, item.children, laycol, laycol["children"], level+1) -def update_property_group(context): - update_collection_tree(context) +def update_property_group(context, renumerate=False): + global collection_tree + global qcd_slots + + qcd_slots.allow_update = False + + update_collection_tree(context, renumerate) context.scene.collection_manager.cm_list_collection.clear() create_property_group(context, collection_tree) + qcd_slots.allow_update = True + def create_property_group(context, tree): global in_filter + global qcd_slots cm = context.scene.collection_manager for laycol in tree: new_cm_listitem = cm.cm_list_collection.add() new_cm_listitem.name = laycol["name"] + new_cm_listitem.qcd_slot = qcd_slots.get_idx(laycol["name"], "") if laycol["has_children"]: create_property_group(context, laycol["children"]) diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 473a5908..067a5277 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -35,6 +35,7 @@ from bpy.props import ( from .internals import ( expanded, layer_collections, + qcd_slots, update_property_group, get_modifiers, send_report, @@ -1418,6 +1419,7 @@ class CMRemoveCollectionOperator(Operator): def execute(self, context): global rto_history + global qcd_slots cm = context.scene.collection_manager @@ -1448,6 +1450,13 @@ class CMRemoveCollectionOperator(Operator): update_property_group(context) + # update qcd + if self.collection_name in qcd_slots: + qcd_slots.del_slot(self.collection_name) + + if self.collection_name in qcd_slots.overrides: + del qcd_slots.overrides[self.collection_name] + # reset history for rto in rto_history.values(): rto.clear() diff --git a/object_collection_manager/preferences.py b/object_collection_manager/preferences.py new file mode 100644 index 00000000..154ee3ee --- /dev/null +++ b/object_collection_manager/preferences.py @@ -0,0 +1,512 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import bpy +from bpy.types import AddonPreferences +from bpy.props import ( + BoolProperty, + FloatProperty, + FloatVectorProperty, + ) + +def get_tool_text(self): + if self.tool_text_override: + return self["tool_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.text + self["tool_text_color"] = color.r, color.g, color.b + return self["tool_text_color"] + +def set_tool_text(self, values): + self["tool_text_color"] = values[0], values[1], values[2] + + +def get_tool_text_sel(self): + if self.tool_text_sel_override: + return self["tool_text_sel_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.text_sel + self["tool_text_sel_color"] = color.r, color.g, color.b + return self["tool_text_sel_color"] + +def set_tool_text_sel(self, values): + self["tool_text_sel_color"] = values[0], values[1], values[2] + + +def get_tool_inner(self): + if self.tool_inner_override: + return self["tool_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.inner + self["tool_inner_color"] = color[0], color[1], color[2], color[3] + return self["tool_inner_color"] + +def set_tool_inner(self, values): + self["tool_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_tool_inner_sel(self): + if self.tool_inner_sel_override: + return self["tool_inner_sel_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.inner_sel + self["tool_inner_sel_color"] = color[0], color[1], color[2], color[3] + return self["tool_inner_sel_color"] + +def set_tool_inner_sel(self, values): + self["tool_inner_sel_color"] = values[0], values[1], values[2], values[3] + + +def get_tool_outline(self): + if self.tool_outline_override: + return self["tool_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tool.outline + self["tool_outline_color"] = color.r, color.g, color.b + return self["tool_outline_color"] + +def set_tool_outline(self, values): + self["tool_outline_color"] = values[0], values[1], values[2] + + +def get_menu_back_text(self): + if self.menu_back_text_override: + return self["menu_back_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.text + self["menu_back_text_color"] = color.r, color.g, color.b + return self["menu_back_text_color"] + +def set_menu_back_text(self, values): + self["menu_back_text_color"] = values[0], values[1], values[2] + + +def get_menu_back_inner(self): + if self.menu_back_inner_override: + return self["menu_back_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.inner + self["menu_back_inner_color"] = color[0], color[1], color[2], color[3] + return self["menu_back_inner_color"] + +def set_menu_back_inner(self, values): + self["menu_back_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_menu_back_outline(self): + if self.menu_back_outline_override: + return self["menu_back_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_menu_back.outline + self["menu_back_outline_color"] = color.r, color.g, color.b + return self["menu_back_outline_color"] + +def set_menu_back_outline(self, values): + self["menu_back_outline_color"] = values[0], values[1], values[2] + + +def get_tooltip_text(self): + if self.tooltip_text_override: + return self["tooltip_text_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text + self["tooltip_text_color"] = color.r, color.g, color.b + return self["tooltip_text_color"] + +def set_tooltip_text(self, values): + self["tooltip_text_color"] = values[0], values[1], values[2] + + +def get_tooltip_inner(self): + if self.tooltip_inner_override: + return self["tooltip_inner_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner + self["tooltip_inner_color"] = color[0], color[1], color[2], color[3] + return self["tooltip_inner_color"] + +def set_tooltip_inner(self, values): + self["tooltip_inner_color"] = values[0], values[1], values[2], values[3] + + +def get_tooltip_outline(self): + if self.tooltip_outline_override: + return self["tooltip_outline_color"] + else: + color = bpy.context.preferences.themes[0].user_interface.wcol_tooltip.outline + self["tooltip_outline_color"] = color.r, color.g, color.b + return self["tooltip_outline_color"] + +def set_tooltip_outline(self, values): + self["tooltip_outline_color"] = values[0], values[1], values[2] + + +class CMPreferences(AddonPreferences): + bl_idname = __package__ + + # OVERRIDE BOOLS + tool_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + tool_text_sel_override: BoolProperty( + name="Selection", + description="Override Theme Text Selection Color", + default=False, + ) + + tool_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + tool_inner_sel_override: BoolProperty( + name="Selection", + description="Override Theme Inner Selection Color", + default=False, + ) + + tool_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + menu_back_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + menu_back_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + menu_back_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + tooltip_text_override: BoolProperty( + name="Text", + description="Override Theme Text Color", + default=False, + ) + + tooltip_inner_override: BoolProperty( + name="Inner", + description="Override Theme Inner Color", + default=False, + ) + + tooltip_outline_override: BoolProperty( + name="Outline", + description="Override Theme Outline Color", + default=False, + ) + + + # OVERRIDE COLORS + qcd_ogl_widget_tool_text: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_text, + set=set_tool_text, + ) + + qcd_ogl_widget_tool_text_sel: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Text Selection Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.text_sel, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_text_sel, + set=set_tool_text_sel, + ) + + qcd_ogl_widget_tool_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tool_inner, + set=set_tool_inner, + ) + + qcd_ogl_widget_tool_inner_sel: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Inner Selection Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.inner_sel, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tool_inner_sel, + set=set_tool_inner_sel, + ) + + qcd_ogl_widget_tool_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Tool Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tool.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tool_outline, + set=set_tool_outline, + ) + + qcd_ogl_widget_menu_back_text: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_menu_back_text, + set=set_menu_back_text, + ) + + qcd_ogl_widget_menu_back_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_menu_back_inner, + set=set_menu_back_inner, + ) + + qcd_ogl_widget_menu_back_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Menu Back Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_menu_back.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_menu_back_outline, + set=set_menu_back_outline, + ) + + qcd_ogl_widget_tooltip_text: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Text Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.text, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tooltip_text, + set=set_tooltip_text, + ) + + qcd_ogl_widget_tooltip_inner: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Inner Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.inner, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + size=4, + get=get_tooltip_inner, + set=set_tooltip_inner, + ) + + qcd_ogl_widget_tooltip_outline: FloatVectorProperty( + name="", + description="QCD Move Widget Tooltip Outline Color", + default=bpy.context.preferences.themes[0].user_interface.wcol_tooltip.outline, + subtype='COLOR_GAMMA', + min=0.0, + max=1.0, + get=get_tooltip_outline, + set=set_tooltip_outline, + ) + + # NON ACTIVE ICON ALPHA + qcd_ogl_selected_icon_alpha: FloatProperty( + name="Selected Icon Alpha", + description="Set the 'Selected' icon's alpha value", + default=0.9, + min=0.0, + max=1.0, + ) + + qcd_ogl_objects_icon_alpha: FloatProperty( + name="Objects Icon Alpha", + description="Set the 'Objects' icon's alpha value", + default=0.5, + min=0.0, + max=1.0, + ) + + def draw(self, context): + layout = self.layout + box = layout.box() + + box.row().label(text="QCD Move Widget") + + tool_box = box.box() + tool_box.row().label(text="Tool Theme Overrides:") + tool_box.use_property_split = True + + flow = tool_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tool_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_text_override + row.prop(self, "qcd_ogl_widget_tool_text") + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tool_text_sel_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_text_sel_override + row.prop(self, "qcd_ogl_widget_tool_text_sel") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_inner_override + row.prop(self, "qcd_ogl_widget_tool_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_inner_sel_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_inner_sel_override + row.prop(self, "qcd_ogl_widget_tool_inner_sel") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tool_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tool_outline_override + row.prop(self, "qcd_ogl_widget_tool_outline") + + tool_box.use_property_split = False + tool_box.row().label(text="Icon Alpha:") + icon_fade_row = tool_box.row() + icon_fade_row.alignment = 'EXPAND' + icon_fade_row.prop(self, "qcd_ogl_selected_icon_alpha", text="Selected") + icon_fade_row.prop(self, "qcd_ogl_objects_icon_alpha", text="Objects") + + + menu_back_box = box.box() + menu_back_box.use_property_split = True + menu_back_box.row().label(text="Menu Back Theme Overrides:") + + flow = menu_back_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "menu_back_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_text_override + row.prop(self, "qcd_ogl_widget_menu_back_text") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "menu_back_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_inner_override + row.prop(self, "qcd_ogl_widget_menu_back_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "menu_back_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.menu_back_outline_override + row.prop(self, "qcd_ogl_widget_menu_back_outline") + + + tooltip_box = box.box() + tooltip_box.use_property_split = True + tooltip_box.row().label(text="Tooltip Theme Overrides:") + + flow = tooltip_box.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.alignment = 'LEFT' + + row = col.row(align=True) + row.alignment = 'RIGHT' + row.prop(self, "tooltip_text_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_text_override + row.prop(self, "qcd_ogl_widget_tooltip_text") + + col = flow.column() + col.alignment = 'RIGHT' + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tooltip_inner_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_inner_override + row.prop(self, "qcd_ogl_widget_tooltip_inner") + + row = col.row() + row.alignment = 'RIGHT' + row.prop(self, "tooltip_outline_override") + row = row.row(align=True) + row.alignment = 'RIGHT' + row.enabled = self.tooltip_outline_override + row.prop(self, "qcd_ogl_widget_tooltip_outline") diff --git a/object_collection_manager/qcd_move_widget.py b/object_collection_manager/qcd_move_widget.py new file mode 100644 index 00000000..442d8cfb --- /dev/null +++ b/object_collection_manager/qcd_move_widget.py @@ -0,0 +1,969 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import time +from math import cos, sin, pi, floor +import bpy +import bgl +import blf +import gpu +from gpu_extras.batch import batch_for_shader + +from bpy.types import Operator +from .internals import ( + layer_collections, + qcd_slots, + ) +from . import qcd_operators + +def spacer(): + spacer = 10 + return round(spacer * scale_factor()) + +def scale_factor(): + return bpy.context.preferences.system.ui_scale + +def get_coords(area): + x = area["vert"][0] + y = area["vert"][1] + w = area["width"] + h = area["height"] + + vertices = ( + (x, y-h), # bottom left + (x+w, y-h), # bottom right + (x, y), # top left + (x+w, y)) # top right + + indices = ( + (0, 1, 2), (2, 1, 3)) + + return vertices, indices + +def get_x_coords(area): + x = area["vert"][0] + y = area["vert"][1] + w = area["width"] + h = area["height"] + + vertices = ( + (x, y), # top left A + (x+(w*0.1), y), # top left B + (x+w, y), # top right A + (x+w-(w*0.1), y), # top right B + (x, y-h), # bottom left A + (x+(w*0.1), y-h), # bottom left B + (x+w, y-h), # bottom right A + (x+w-(w*0.1), y-h), # bottom right B + (x+(w/2)-(w*0.05), y-(h/2)), # center left + (x+(w/2)+(w*0.05), y-(h/2)) # center right + ) + + indices = ( + (0,1,8), (1,8,9), # top left bar + (2,3,9), (3,9,8), # top right bar + (4,5,8), (5,8,9), # bottom left bar + (6,7,8), (6,9,8) # bottom right bar + ) + + return vertices, indices + +def get_circle_coords(area): + # set x, y to center + x = area["vert"][0] + area["width"] / 2 + y = area["vert"][1] - area["width"] / 2 + radius = area["width"] / 2 + sides = 32 + vertices = [(radius * cos(side * 2 * pi / sides) + x, + radius * sin(side * 2 * pi / sides) + y) + for side in range(sides + 1)] + + return vertices + +def draw_rounded_rect(area, shader, color, tl=5, tr=5, bl=5, br=5, outline=False): + sides = 32 + + tl = round(tl * scale_factor()) + tr = round(tr * scale_factor()) + bl = round(bl * scale_factor()) + br = round(br * scale_factor()) + + bgl.glEnable(bgl.GL_BLEND) + + if outline: + thickness = round(2 * scale_factor()) + thickness = max(thickness, 2) + + bgl.glLineWidth(thickness) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST) + + draw_type = 'TRI_FAN' if not outline else 'LINE_STRIP' + + # top left corner + vert_x = area["vert"][0] + tl + vert_y = area["vert"][1] - tl + tl_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (8<=side<=16): + cosine = tl * cos(side * 2 * pi / sides) + vert_x + sine = tl * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # top right corner + vert_x = area["vert"][0] + area["width"] - tr + vert_y = area["vert"][1] - tr + tr_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (0<=side<=8): + cosine = tr * cos(side * 2 * pi / sides) + vert_x + sine = tr * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # bottom left corner + vert_x = area["vert"][0] + bl + vert_y = area["vert"][1] - area["height"] + bl + bl_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (16<=side<=24): + cosine = bl * cos(side * 2 * pi / sides) + vert_x + sine = bl * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # bottom right corner + vert_x = area["vert"][0] + area["width"] - br + vert_y = area["vert"][1] - area["height"] + br + br_vert = (vert_x, vert_y) + vertices = [(vert_x, vert_y)] if not outline else [] + + for side in range(sides+1): + if (24<=side<=32): + cosine = br * cos(side * 2 * pi / sides) + vert_x + sine = br * sin(side * 2 * pi / sides) + vert_y + vertices.append((cosine,sine)) + + batch = batch_for_shader(shader, draw_type, {"pos": vertices}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + if not outline: + vertices = [] + indices = [] + base_ind = 0 + + # left edge + width = max(tl, bl) + le_x = tl_vert[0]-tl + vertices.extend([ + (le_x, tl_vert[1]), + (le_x+width, tl_vert[1]), + (le_x, bl_vert[1]), + (le_x+width, bl_vert[1]) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # right edge + width = max(tr, br) + re_x = tr_vert[0]+tr + vertices.extend([ + (re_x, tr_vert[1]), + (re_x-width, tr_vert[1]), + (re_x, br_vert[1]), + (re_x-width, br_vert[1]) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # top edge + width = max(tl, tr) + te_y = tl_vert[1]+tl + vertices.extend([ + (tl_vert[0], te_y), + (tl_vert[0], te_y-width), + (tr_vert[0], te_y), + (tr_vert[0], te_y-width) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # bottom edge + width = max(bl, br) + be_y = bl_vert[1]-bl + vertices.extend([ + (bl_vert[0], be_y), + (bl_vert[0], be_y+width), + (br_vert[0], be_y), + (br_vert[0], be_y+width) + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + base_ind += 4 + + # middle + vertices.extend([ + tl_vert, + tr_vert, + bl_vert, + br_vert + ]) + indices.extend([ + (base_ind,base_ind+1,base_ind+2), + (base_ind+2,base_ind+3,base_ind+1) + ]) + + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + shader.uniform_float("color", color) + batch.draw(shader) + + else: + overlap = round(thickness / 2 - scale_factor() / 2) + + # left edge + le_x = tl_vert[0]-tl + vertices = [ + (le_x, tl_vert[1] + (overlap if tl == 0 else 0)), + (le_x, bl_vert[1] - (overlap if bl == 0 else 0)) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # right edge + re_x = tr_vert[0]+tr + vertices = [ + (re_x, tr_vert[1] + (overlap if tr == 0 else 0)), + (re_x, br_vert[1] - (overlap if br == 0 else 0)) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # top edge + te_y = tl_vert[1]+tl + vertices = [ + (tl_vert[0] - (overlap if tl == 0 else 0), te_y), + (tr_vert[0] + (overlap if tr == 0 else 0), te_y) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + # bottom edge + be_y = bl_vert[1]-bl + vertices = [ + (bl_vert[0] - (overlap if bl == 0 else 0), be_y), + (br_vert[0] + (overlap if br == 0 else 0), be_y) + ] + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + batch.draw(shader) + + bgl.glDisable(bgl.GL_LINE_SMOOTH) + + bgl.glDisable(bgl.GL_BLEND) + +def mouse_in_area(mouse_pos, area, buf = 0): + x = mouse_pos[0] + y = mouse_pos[1] + + # check left + if x+buf < area["vert"][0]: + return False + + # check right + if x-buf > area["vert"][0] + area["width"]: + return False + + # check top + if y-buf > area["vert"][1]: + return False + + # check bottom + if y+buf < area["vert"][1] - area["height"]: + return False + + # if we reach here we're in the area + return True + +def account_for_view_bounds(area): + # make sure it renders in the 3d view + # left + if area["vert"][0] < 0: + x = 0 + y = area["vert"][1] + + area["vert"] = (x, y) + + # right + if area["vert"][0] + area["width"] > bpy.context.region.width: + x = bpy.context.region.width - area["width"] + y = area["vert"][1] + + area["vert"] = (x, y) + + # top + if area["vert"][1] > bpy.context.region.height: + x = area["vert"][0] + y = bpy.context.region.height + + area["vert"] = (x, y) + + # bottom + if area["vert"][1] - area["height"] < 0: + x = area["vert"][0] + y = area["height"] + + area["vert"] = (x, y) + +def update_area_dimensions(area, w=0, h=0): + area["width"] += w + area["height"] += h + +class QCDMoveWidget(Operator): + """QCD Move Widget""" + bl_idname = "view3d.qcd_move_widget" + bl_label = "QCD Move Widget" + + slots = { + "ONE":1, + "TWO":2, + "THREE":3, + "FOUR":4, + "FIVE":5, + "SIX":6, + "SEVEN":7, + "EIGHT":8, + "NINE":9, + "ZERO":10, + } + + last_type = '' + moved = False + + def modal(self, context, event): + if event.type == 'TIMER': + if self.hover_time and self.hover_time + 0.5 < time.time(): + self.draw_tooltip = True + + context.area.tag_redraw() + return {'RUNNING_MODAL'} + + + context.area.tag_redraw() + + if len(self.areas) == 1: + return {'RUNNING_MODAL'} + + if self.last_type == 'LEFTMOUSE' and event.value == 'PRESS' and event.type == 'MOUSEMOVE': + if mouse_in_area(self.mouse_pos, self.areas["Grab Bar"]): + x_offset = self.areas["Main Window"]["vert"][0] - self.mouse_pos[0] + x = event.mouse_region_x + x_offset + + y_offset = self.areas["Main Window"]["vert"][1] - self.mouse_pos[1] + y = event.mouse_region_y + y_offset + + self.areas["Main Window"]["vert"] = (x, y) + + self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) + + elif event.type == 'MOUSEMOVE': + self.draw_tooltip = False + self.hover_time = None + self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) + + if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 50 * scale_factor()): + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + if self.moved: + bpy.ops.ed.undo_push() + + return {'FINISHED'} + + elif event.value == 'PRESS' and event.type == 'LEFTMOUSE': + if not mouse_in_area(self.mouse_pos, self.areas["Main Window"], 10 * scale_factor()): + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + if self.moved: + bpy.ops.ed.undo_push() + + return {'FINISHED'} + + for num in range(20): + if not self.areas.get(f"Button {num + 1}", None): + break + + if mouse_in_area(self.mouse_pos, self.areas[f"Button {num + 1}"]): + bpy.ops.view3d.move_to_qcd_slot(slot=str(num + 1), toggle=event.shift) + self.moved = True + + elif event.type in {'RIGHTMOUSE', 'ESC'}: + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + + return {'CANCELLED'} + + if event.value == 'PRESS' and event.type in self.slots: + move_to = self.slots[event.type] + + if event.alt: + move_to += 10 + + if event.shift: + bpy.ops.view3d.move_to_qcd_slot(slot=str(move_to), toggle=True) + else: + bpy.ops.view3d.move_to_qcd_slot(slot=str(move_to), toggle=False) + + self.moved = True + + if event.type != 'MOUSEMOVE' and event.type != 'INBETWEEN_MOUSEMOVE': + self.last_type = event.type + + return {'RUNNING_MODAL'} + + def invoke(self, context, event): + if context.area.type == 'VIEW_3D': + # the arguments we pass the the callback + args = (self, context) + # Add the region OpenGL drawing callback + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL') + self._timer = context.window_manager.event_timer_add(0.1, window=context.window) + + self.mouse_pos = (event.mouse_region_x, event.mouse_region_y) + + self.draw_tooltip = False + + self.hover_time = None + + self.areas = {} + + # MAIN WINDOW BACKGROUND + x = self.mouse_pos[0] - spacer()*2 + y = self.mouse_pos[1] + spacer()*2 + main_window = { + # Top Left Vertex + "vert": (x,y), + "width": 0, + "height": 0, + "value": None + } + account_for_view_bounds(main_window) + + # add main window background to areas + self.areas["Main Window"] = main_window + + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + else: + self.report({'WARNING'}, "View3D not found, cannot run operator") + return {'CANCELLED'} + + +def allocate_main_ui(self, context): + main_window = self.areas["Main Window"] + self.areas.clear() + main_window["width"] = 0 + main_window["height"] = 0 + self.areas["Main Window"] = main_window + + cur_width_pos = main_window["vert"][0] + cur_height_pos = main_window["vert"][1] + + # GRAB BAR + grab_bar = { + "vert": main_window["vert"], + "width": 0, + "height": round(23 * scale_factor()), + "value": None + } + + # add grab bar to areas + self.areas["Grab Bar"] = grab_bar + + + # WINDOW TITLE + wt_indent_x = spacer()*2 + wt_y_offset = round(spacer()/2) + window_title = { + "vert": main_window["vert"], + "width": 0, + "height": round(13 * scale_factor()), + "value": "Move Objects to QCD Slots" + } + + x = main_window["vert"][0] + wt_indent_x + y = main_window["vert"][1] - window_title["height"] - wt_y_offset + window_title["vert"] = (x, y) + + # add window title to areas + self.areas["Window Title"] = window_title + + cur_height_pos = window_title["vert"][1] + + + # MAIN BUTTON AREA + button_size = round(20 * scale_factor()) + button_gap = round(1 * scale_factor()) + button_group = 5 + button_group_gap = round(20 * scale_factor()) + button_group_width = button_size * button_group + button_gap * (button_group - 1) + + mba_indent_x = spacer()*2 + mba_outdent_x = spacer()*2 + mba_indent_y = spacer() + x = cur_width_pos + mba_indent_x + y = cur_height_pos - mba_indent_y + main_button_area = { + "vert": (x, y), + "width": 0, + "height": 0, + "value": None + } + + # add main button area to areas + self.areas["Main Button Area"] = main_button_area + + # update current position + cur_width_pos = main_button_area["vert"][0] + cur_height_pos = main_button_area["vert"][1] + + + # BUTTON ROW 1 A + button_row_1_a = { + "vert": main_button_area["vert"], + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 1 A to areas + self.areas["Button Row 1 A"] = button_row_1_a + + # advance width pos to start of next row + cur_width_pos += button_row_1_a["width"] + cur_width_pos += button_group_gap + + # BUTTON ROW 1 B + x = cur_width_pos + y = cur_height_pos + button_row_1_b = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 1 B to areas + self.areas["Button Row 1 B"] = button_row_1_b + + # reset width pos to start of main button area + cur_width_pos = main_button_area["vert"][0] + # update height pos + cur_height_pos -= button_row_1_a["height"] + # add gap between button rows + cur_height_pos -= button_gap + + + # BUTTON ROW 2 A + x = cur_width_pos + y = cur_height_pos + button_row_2_a = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 2 A to areas + self.areas["Button Row 2 A"] = button_row_2_a + + # advance width pos to start of next row + cur_width_pos += button_row_2_a["width"] + cur_width_pos += button_group_gap + + # BUTTON ROW 2 B + x = cur_width_pos + y = cur_height_pos + button_row_2_b = { + "vert": (x, y), + "width": button_group_width, + "height": button_size, + "value": None + } + + # add button row 2 B to areas + self.areas["Button Row 2 B"] = button_row_2_b + + + # BUTTONS + def get_buttons(button_row, row_num): + cur_width_pos = button_row["vert"][0] + cur_height_pos = button_row["vert"][1] + for num in range(button_group): + slot_num = row_num + num + + qcd_slot = qcd_slots.get_name(f"{slot_num}") + + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["ptr"] + collection_objects = qcd_laycol.collection.objects + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + + # BUTTON + x = cur_width_pos + y = cur_height_pos + button = { + "vert": (x, y), + "width": button_size, + "height": button_size, + "value": slot_num + } + + self.areas[f"Button {slot_num}"] = button + + # ACTIVE OBJECT ICON + if active_object and active_object in selected_objects and active_object.name in collection_objects: + x = cur_width_pos + round(button_size / 4) + y = cur_height_pos - round(button_size / 4) + active_object_indicator = { + "vert": (x, y), + "width": floor(button_size / 2), + "height": floor(button_size / 2), + "value": None + } + + self.areas[f"Button {slot_num} Active Object Indicator"] = active_object_indicator + + elif not set(selected_objects).isdisjoint(collection_objects): + x = cur_width_pos + round(button_size / 4) + floor(1 * scale_factor()) + y = cur_height_pos - round(button_size / 4) - floor(1 * scale_factor()) + selected_object_indicator = { + "vert": (x, y), + "width": floor(button_size / 2) - floor(1 * scale_factor()), + "height": floor(button_size / 2) - floor(1 * scale_factor()), + "value": None + } + + self.areas[f"Button {slot_num} Selected Object Indicator"] = selected_object_indicator + + elif collection_objects: + x = cur_width_pos + floor(button_size / 4) + y = cur_height_pos - button_size / 2 + 1 * scale_factor() + object_indicator = { + "vert": (x, y), + "width": round(button_size / 2), + "height": round(2 * scale_factor()), + "value": None + } + self.areas[f"Button {slot_num} Object Indicator"] = object_indicator + + else: + x = cur_width_pos + 2 * scale_factor() + y = cur_height_pos - 2 * scale_factor() + X_icon = { + "vert": (x, y), + "width": button_size - 4 * scale_factor(), + "height": button_size - 4 * scale_factor(), + "value": None + } + + self.areas[f"X_icon {slot_num}"] = X_icon + + cur_width_pos += button_size + cur_width_pos += button_gap + + get_buttons(button_row_1_a, 1) + get_buttons(button_row_1_b, 6) + get_buttons(button_row_2_a, 11) + get_buttons(button_row_2_b, 16) + + + # UPDATE DYNAMIC DIMENSIONS + width = button_row_1_a["width"] + button_group_gap + button_row_1_b["width"] + height = button_row_1_a["height"] + button_gap + button_row_2_a["height"] + update_area_dimensions(main_button_area, width, height) + + width = main_button_area["width"] + mba_indent_x + mba_outdent_x + height = main_button_area["height"] + mba_indent_y * 2 + window_title["height"] + wt_y_offset + update_area_dimensions(main_window, width, height) + + update_area_dimensions(grab_bar, main_window["width"]) + + +def draw_callback_px(self, context): + allocate_main_ui(self, context) + + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + shader.bind() + + addon_prefs = context.preferences.addons[__package__].preferences + + # main window background + main_window = self.areas["Main Window"] + outline_color = addon_prefs.qcd_ogl_widget_menu_back_outline + background_color = addon_prefs.qcd_ogl_widget_menu_back_inner + draw_rounded_rect(main_window, shader, outline_color[:] + (1,), outline=True) + draw_rounded_rect(main_window, shader, background_color) + + # draw window title + window_title = self.areas["Window Title"] + x = window_title["vert"][0] + y = window_title["vert"][1] + h = window_title["height"] + text = window_title["value"] + text_color = addon_prefs.qcd_ogl_widget_menu_back_text + font_id = 0 + blf.position(font_id, x, y, 0) + blf.size(font_id, int(h), 72) + blf.color(font_id, text_color[0], text_color[1], text_color[2], 1) + blf.draw(font_id, text) + + # refresh shader - not sure why this is needed + shader.bind() + + in_tooltip_area = False + + for num in range(20): + slot_num = num + 1 + qcd_slot = qcd_slots.get_name(f"{slot_num}") + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["ptr"] + collection_objects = qcd_laycol.collection.objects + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + button_area = self.areas[f"Button {slot_num}"] + + # colors + button_color = addon_prefs.qcd_ogl_widget_tool_inner + icon_color = addon_prefs.qcd_ogl_widget_tool_text + if not qcd_laycol.exclude: + button_color = addon_prefs.qcd_ogl_widget_tool_inner_sel + icon_color = addon_prefs.qcd_ogl_widget_tool_text_sel + + if mouse_in_area(self.mouse_pos, button_area): + in_tooltip_area = True + + mod = 0.1 + + if button_color[0] + mod > 1 or button_color[1] + mod > 1 or button_color[2] + mod > 1: + mod = -mod + + button_color = ( + button_color[0] + mod, + button_color[1] + mod, + button_color[2] + mod, + button_color[3] + ) + + + # button roundness + tl = tr = bl = br = 0 + rounding = 5 + + if num < 10: + if not f"{num+2}" in qcd_slots: + tr = rounding + + if not f"{num}" in qcd_slots: + tl = rounding + else: + if not f"{num+2}" in qcd_slots: + br = rounding + + if not f"{num}" in qcd_slots: + bl = rounding + + if num in [0,5]: + tl = rounding + elif num in [4,9]: + tr = rounding + elif num in [10,15]: + bl = rounding + elif num in [14,19]: + br = rounding + + # draw button + outline_color = addon_prefs.qcd_ogl_widget_tool_outline + draw_rounded_rect(button_area, shader, outline_color[:] + (1,), tl, tr, bl, br, outline=True) + draw_rounded_rect(button_area, shader, button_color, tl, tr, bl, br) + + # ACTIVE OBJECT + if active_object and active_object in selected_objects and active_object.name in collection_objects: + active_object_indicator = self.areas[f"Button {slot_num} Active Object Indicator"] + + vertices = get_circle_coords(active_object_indicator) + shader.uniform_float("color", icon_color[:] + (1,)) + batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices}) + + bgl.glEnable(bgl.GL_BLEND) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_BLEND) + + # SELECTED OBJECTS + elif not set(selected_objects).isdisjoint(collection_objects): + selected_object_indicator = self.areas[f"Button {slot_num} Selected Object Indicator"] + + alpha = addon_prefs.qcd_ogl_selected_icon_alpha + vertices = get_circle_coords(selected_object_indicator) + shader.uniform_float("color", icon_color[:] + (alpha,)) + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices}) + + bgl.glLineWidth(2 * scale_factor()) + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_LINE_SMOOTH) + bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_LINE_SMOOTH) + bgl.glDisable(bgl.GL_BLEND) + + # OBJECTS + elif collection_objects: + object_indicator = self.areas[f"Button {slot_num} Object Indicator"] + + alpha = addon_prefs.qcd_ogl_objects_icon_alpha + vertices, indices = get_coords(object_indicator) + shader.uniform_float("color", icon_color[:] + (alpha,)) + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + bgl.glEnable(bgl.GL_BLEND) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_BLEND) + + + # X ICON + else: + X_icon = self.areas[f"X_icon {slot_num}"] + X_icon_color = addon_prefs.qcd_ogl_widget_menu_back_text + + vertices, indices = get_x_coords(X_icon) + shader.uniform_float("color", X_icon_color[:] + (1,)) + batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices) + + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_POLYGON_SMOOTH) + bgl.glHint(bgl.GL_POLYGON_SMOOTH_HINT, bgl.GL_NICEST) + + batch.draw(shader) + + bgl.glDisable(bgl.GL_POLYGON_SMOOTH) + bgl.glDisable(bgl.GL_BLEND) + + if in_tooltip_area: + if self.draw_tooltip: + draw_tooltip(self, context, shader,"Move Object To QCD Slot\n * Shift-Click to toggle objects\' slot") + self.hover_time = None + + else: + if not self.hover_time: + self.hover_time = time.time() + + +def draw_tooltip(self, context, shader, message): + addon_prefs = context.preferences.addons[__package__].preferences + + font_id = 0 + line_height = 11 * scale_factor() + text_color = addon_prefs.qcd_ogl_widget_tooltip_text + blf.size(font_id, int(line_height), 72) + blf.color(font_id, text_color[0], text_color[1], text_color[2], 1) + + lines = message.split("\n") + longest = [0,""] + num_lines = len(lines) + + for line in lines: + if len(line) > longest[0]: + longest[0] = len(line) + longest[1] = line + + w, h = blf.dimensions(font_id, longest[1]) + + line_spacer = 1 * scale_factor() + padding = 4 * scale_factor() + + # draw background + tooltip = { + "vert": self.mouse_pos, + "width": w + spacer()*2, + "height": (line_height * num_lines + line_spacer * num_lines) + padding*3, + "value": None + } + + x = tooltip["vert"][0] - spacer()*2 + y = tooltip["vert"][1] + tooltip["height"] + round(5 * scale_factor()) + tooltip["vert"] = (x, y) + + account_for_view_bounds(tooltip) + + outline_color = addon_prefs.qcd_ogl_widget_tooltip_outline + background_color = addon_prefs.qcd_ogl_widget_tooltip_inner + draw_rounded_rect(tooltip, shader, outline_color[:] + (1,), outline=True) + draw_rounded_rect(tooltip, shader, background_color) + + line_pos = padding + line_height + # draw text + for num, line in enumerate(lines): + x = tooltip["vert"][0] + spacer() + y = tooltip["vert"][1] - line_pos + blf.position(font_id, x, y, 0) + blf.draw(font_id, line) + + line_pos += line_height + line_spacer diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py new file mode 100644 index 00000000..db58dc4b --- /dev/null +++ b/object_collection_manager/qcd_operators.py @@ -0,0 +1,286 @@ +# ##### 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 ##### + +# Copyright 2011, Ryan Inch + +import bpy + +from bpy.types import ( + Operator, +) + +from bpy.props import ( + BoolProperty, + StringProperty, + IntProperty +) + +from .internals import ( + layer_collections, + qcd_slots, + update_property_group, + get_modifiers, +) + +from .operators import rto_history + +move_triggered = False +move_selection = [] +move_active = None + +def get_move_selection(): + global move_selection + + if not move_selection: + move_selection = bpy.context.selected_objects + + return move_selection + +def get_move_active(): + global move_active + global move_selection + + if not move_active: + move_active = bpy.context.view_layer.objects.active + + if move_active not in get_move_selection(): + move_active = None + + try: + move_active.name + + except: + move_active = None + move_selection = [] + + # update header widget + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + return move_active + +class MoveToQCDSlot(Operator): + '''Move object(s) to QCD slot''' + bl_label = "Move To QCD Slot" + bl_idname = "view3d.move_to_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + toggle: BoolProperty() + + def execute(self, context): + global qcd_slots + global layer_collections + global move_triggered + + selected_objects = get_move_selection() + active_object = get_move_active() + move_triggered = True + qcd_laycol = qcd_slots.get_name(self.slot) + + if qcd_laycol: + qcd_laycol = layer_collections[qcd_laycol]["ptr"] + + else: + return {'CANCELLED'} + + + if not selected_objects: + return {'CANCELLED'} + + # adds object to slot + if self.toggle: + if not active_object: + active_object = selected_objects[0] + + if not active_object.name in qcd_laycol.collection.objects: + for obj in selected_objects: + if obj.name not in qcd_laycol.collection.objects: + qcd_laycol.collection.objects.link(obj) + + else: + for obj in selected_objects: + if obj.name in qcd_laycol.collection.objects: + + if len(obj.users_collection) == 1: + continue + + qcd_laycol.collection.objects.unlink(obj) + + + # moves object to slot + else: + for obj in selected_objects: + if obj.name not in qcd_laycol.collection.objects: + qcd_laycol.collection.objects.link(obj) + + for collection in obj.users_collection: + qcd_idx = qcd_slots.get_idx(collection.name) + if qcd_idx != self.slot: + collection.objects.unlink(obj) + + + if not context.active_object: + try: + context.view_layer.objects.active = active_object + except: + pass + + # update header UI + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + return {'FINISHED'} + + +class ViewMoveQCDSlot(Operator): + ''' * Shift-Click to toggle QCD slots\n * Ctrl-Click to move objects to QCD slot\n * Ctrl-Shift-Click to toggle objects\' slot''' + bl_label = "View QCD Slot" + bl_idname = "view3d.view_move_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + + def invoke(self, context, event): + global layer_collections + global qcd_history + + modifiers = get_modifiers(event) + + if modifiers == {"shift"}: + bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=True) + + return {'FINISHED'} + + elif modifiers == {"ctrl"}: + bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=False) + return {'FINISHED'} + + elif modifiers == {"ctrl", "shift"}: + bpy.ops.view3d.move_to_qcd_slot(slot=self.slot, toggle=True) + return {'FINISHED'} + + else: + bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False) + return {'FINISHED'} + +class ViewQCDSlot(Operator): + '''View objects in QCD slot''' + bl_label = "View QCD Slot" + bl_idname = "view3d.view_qcd_slot" + bl_options = {'REGISTER', 'UNDO'} + + slot: StringProperty() + toggle: BoolProperty() + + def execute(self, context): + global qcd_slots + global layer_collections + global rto_history + + qcd_laycol = qcd_slots.get_name(self.slot) + + if qcd_laycol: + qcd_laycol = layer_collections[qcd_laycol]["ptr"] + + else: + return {'CANCELLED'} + + if self.toggle: + # get current child exclusion state + child_exclusion = [] + + laycol_iter_list = [qcd_laycol.children] + while len(laycol_iter_list) > 0: + new_laycol_iter_list = [] + for laycol_iter in laycol_iter_list: + for layer_collection in laycol_iter: + child_exclusion.append([layer_collection, layer_collection.exclude]) + if len(layer_collection.children) > 0: + new_laycol_iter_list.append(layer_collection.children) + + laycol_iter_list = new_laycol_iter_list + + # toggle exclusion of qcd_laycol + qcd_laycol.exclude = not qcd_laycol.exclude + + # set correct state for all children + for laycol in child_exclusion: + laycol[0].exclude = laycol[1] + + # set layer as active layer collection + context.view_layer.active_layer_collection = qcd_laycol + + else: + for laycol in layer_collections.values(): + if laycol["name"] != qcd_laycol.name: + laycol["ptr"].exclude = True + + qcd_laycol.exclude = False + + # exclude all children + laycol_iter_list = [qcd_laycol.children] + while len(laycol_iter_list) > 0: + new_laycol_iter_list = [] + for laycol_iter in laycol_iter_list: + for layer_collection in laycol_iter: + layer_collection.exclude = True + if len(layer_collection.children) > 0: + new_laycol_iter_list.append(layer_collection.children) + + laycol_iter_list = new_laycol_iter_list + + # set layer as active layer collection + context.view_layer.active_layer_collection = qcd_laycol + + # update header UI + cm = bpy.context.scene.collection_manager + cm.update_header.clear() + new_update_header = cm.update_header.add() + new_update_header.name = "updated" + + view_layer = context.view_layer.name + if view_layer in rto_history["exclude"]: + del rto_history["exclude"][view_layer] + if view_layer in rto_history["exclude_all"]: + del rto_history["exclude_all"][view_layer] + + return {'FINISHED'} + + +class RenumerateQCDSlots(Operator): + '''Re-numerate QCD slots\n * Ctrl-Click to include collections marked by the user as non QCD slots''' + bl_label = "Re-numerate QCD Slots" + bl_idname = "view3d.renumerate_qcd_slots" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + global qcd_slots + + qcd_slots.clear() + + if event.ctrl: + qcd_slots.overrides.clear() + + update_property_group(context, renumerate=True) + + return {'FINISHED'} diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 88e9d0cc..b29f5c59 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -31,6 +31,7 @@ from .internals import ( expanded, get_max_lvl, layer_collections, + qcd_slots, update_collection_tree, update_property_group, ) @@ -41,6 +42,13 @@ from .operators import ( phantom_history, ) +from . import qcd_operators + + +preview_collections = {} +last_icon_theme_text = None +last_icon_theme_text_sel = None + class CollectionManager(Operator): bl_label = "Collection Manager" @@ -97,6 +105,10 @@ class CollectionManager(Operator): sec1.operator("view3d.expand_all_items", text=text) + renum = toggle_row.row() + renum.alignment = 'LEFT' + renum.operator("view3d.renumerate_qcd_slots") + for laycol in collection_tree: if laycol["has_children"]: sec1.enabled = True @@ -201,54 +213,6 @@ class CollectionManager(Operator): return wm.invoke_popup(self, width=width) -def update_selection(self, context): - cm = context.scene.collection_manager - - if cm.cm_list_index == -1: - return - - selected_item = cm.cm_list_collection[cm.cm_list_index] - layer_collection = layer_collections[selected_item.name]["ptr"] - - context.view_layer.active_layer_collection = layer_collection - - -def filter_items_by_name_insensitive(pattern, bitflag, items, propname="name", flags=None, reverse=False): - """ - Set FILTER_ITEM for items which name matches filter_name one (case-insensitive). - pattern is the filtering pattern. - propname is the name of the string property to use for filtering. - flags must be a list of integers the same length as items, or None! - return a list of flags (based on given flags if not None), - or an empty list if no flags were given and no filtering has been done. - """ - import fnmatch - - if not pattern or not items: # Empty pattern or list = no filtering! - return flags or [] - - if flags is None: - flags = [0] * len(items) - - # Make pattern case-insensitive - pattern = pattern.lower() - - # Implicitly add heading/trailing wildcards. - pattern = "*" + pattern + "*" - - for i, item in enumerate(items): - name = getattr(item, propname, None) - - # Make name case-insensitive - name = name.lower() - - # This is similar to a logical xor - if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse): - flags[i] |= bitflag - - return flags - - class CM_UL_items(UIList): last_filter_value = "" @@ -257,6 +221,11 @@ class CM_UL_items(UIList): default=False, description="Filter collections by selected items" ) + filter_by_qcd: BoolProperty( + name="Filter By QCD", + default=False, + description="Filter collections to only show QCD slots" + ) def draw_item(self, context, layout, data, item, icon, active_data,active_propname, index): self.use_filter_show = True @@ -299,6 +268,10 @@ class CM_UL_items(UIList): row.label(icon='GROUP') + QCD = row.row() + QCD.scale_x = 0.4 + QCD.prop(item, "qcd_slot", text="") + name_row = row.row() #if rename[0] and index == cm.cm_list_index: @@ -410,6 +383,7 @@ class CM_UL_items(UIList): subrow = row.row(align=True) subrow.prop(self, "filter_by_selected", text="", icon='SNAP_VOLUME') + subrow.prop(self, "filter_by_qcd", text="", icon='EVENT_Q') def filter_items(self, context, data, propname): flt_flags = [] @@ -430,6 +404,13 @@ class CM_UL_items(UIList): if not set(context.selected_objects).isdisjoint(collection.objects): flt_flags[idx] |= self.bitflag_filter_item + elif self.filter_by_qcd: + flt_flags = [0] * len(list_items) + + for idx, item in enumerate(list_items): + if item.qcd_slot: + flt_flags[idx] |= self.bitflag_filter_item + else: # display as treeview flt_flags = [self.bitflag_filter_item] * len(list_items) @@ -462,3 +443,172 @@ class CMRestrictionTogglesPanel(Panel): row.prop(cm, "show_hide_viewport", icon='HIDE_OFF', icon_only=True) row.prop(cm, "show_disable_viewport", icon='RESTRICT_VIEW_OFF', icon_only=True) row.prop(cm, "show_render", icon='RESTRICT_RENDER_OFF', icon_only=True) + + +def view3d_header_qcd_slots(self, context): + layout = self.layout + + idx = 1 + + split = layout.split() + col = split.column(align=True) + row = col.row(align=True) + row.scale_y = 0.5 + + update_collection_tree(context) + + for x in range(20): + qcd_slot = qcd_slots.get_name(str(x+1)) + + if qcd_slot: + qcd_laycol = layer_collections[qcd_slot]["ptr"] + collection_objects = qcd_laycol.collection.objects + selected_objects = qcd_operators.get_move_selection() + active_object = qcd_operators.get_move_active() + + icon_value = 0 + + # if the active object is in the current collection use a custom icon + if (active_object and active_object in selected_objects and + active_object.name in collection_objects): + icon = 'LAYER_ACTIVE' + + + # if there are selected objects use LAYER_ACTIVE + elif not set(selected_objects).isdisjoint(collection_objects): + icon = 'LAYER_USED' + + # If there are objects use LAYER_USED + elif collection_objects: + icon = 'NONE' + active_icon = get_active_icon(context, qcd_laycol) + icon_value = active_icon.icon_id + + else: + icon = 'BLANK1' + + + prop = row.operator("view3d.view_move_qcd_slot", text="", icon=icon, + icon_value=icon_value, depress=not qcd_laycol.exclude) + prop.slot = str(x+1) + + else: + row.label(text="", icon='X') + + + if idx%5==0: + row.separator() + + if idx == 10: + row = col.row(align=True) + row.scale_y = 0.5 + + idx += 1 + + +def get_active_icon(context, qcd_laycol): + global last_icon_theme_text + global last_icon_theme_text_sel + + tool_theme = context.preferences.themes[0].user_interface.wcol_tool + pcoll = preview_collections["icons"] + + if qcd_laycol.exclude: + theme_color = tool_theme.text + last_theme_color = last_icon_theme_text + icon = pcoll["active_icon_text"] + + else: + theme_color = tool_theme.text_sel + last_theme_color = last_icon_theme_text_sel + icon = pcoll["active_icon_text_sel"] + + if last_theme_color == None or theme_color.hsv != last_theme_color: + update_icon(pcoll["active_icon_base"], icon, theme_color) + + if qcd_laycol.exclude: + last_icon_theme_text = theme_color.hsv + + else: + last_icon_theme_text_sel = theme_color.hsv + + return icon + + +def update_icon(base, icon, theme_color): + icon.icon_pixels = base.icon_pixels + colored_icon = [] + + for offset in range(len(icon.icon_pixels)): + idx = offset * 4 + + r = icon.icon_pixels_float[idx] + g = icon.icon_pixels_float[idx+1] + b = icon.icon_pixels_float[idx+2] + a = icon.icon_pixels_float[idx+3] + + # add back some brightness and opacity blender takes away from the custom icon + r = min(r+r*0.2,1) + g = min(g+g*0.2,1) + b = min(b+b*0.2,1) + a = min(a+a*0.2,1) + + # make the icon follow the theme color (assuming the icon is white) + r *= theme_color.r + g *= theme_color.g + b *= theme_color.b + + colored_icon.append(r) + colored_icon.append(g) + colored_icon.append(b) + colored_icon.append(a) + + icon.icon_pixels_float = colored_icon + + +def update_selection(self, context): + cm = context.scene.collection_manager + + if cm.cm_list_index == -1: + return + + selected_item = cm.cm_list_collection[cm.cm_list_index] + layer_collection = layer_collections[selected_item.name]["ptr"] + + context.view_layer.active_layer_collection = layer_collection + + +def filter_items_by_name_insensitive(pattern, bitflag, items, propname="name", flags=None, reverse=False): + """ + Set FILTER_ITEM for items which name matches filter_name one (case-insensitive). + pattern is the filtering pattern. + propname is the name of the string property to use for filtering. + flags must be a list of integers the same length as items, or None! + return a list of flags (based on given flags if not None), + or an empty list if no flags were given and no filtering has been done. + """ + import fnmatch + + if not pattern or not items: # Empty pattern or list = no filtering! + return flags or [] + + if flags is None: + flags = [0] * len(items) + + # Make pattern case-insensitive + pattern = pattern.lower() + + # Implicitly add heading/trailing wildcards. + pattern = "*" + pattern + "*" + + for i, item in enumerate(items): + name = getattr(item, propname, None) + + # Make name case-insensitive + name = name.lower() + + # This is similar to a logical xor + if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse): + flags[i] |= bitflag + + return flags -- cgit v1.2.3