Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRyan Inch <mythologylover75@gmail.com>2020-03-16 09:48:02 +0300
committerRyan Inch <mythologylover75@gmail.com>2020-03-16 09:48:02 +0300
commitb752de9e0da4e8ad694de25497275f66168a2df4 (patch)
tree8a8f5471e22cf2a1037f0fea1a74149dbff461cc
parenta35d66c1337ae5e3f86f11a760ad24770386f9a9 (diff)
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.
-rw-r--r--object_collection_manager/__init__.py118
-rw-r--r--object_collection_manager/icons/minus.pngbin0 -> 1934 bytes
-rw-r--r--object_collection_manager/internals.py195
-rw-r--r--object_collection_manager/operators.py9
-rw-r--r--object_collection_manager/preferences.py512
-rw-r--r--object_collection_manager/qcd_move_widget.py969
-rw-r--r--object_collection_manager/qcd_operators.py286
-rw-r--r--object_collection_manager/ui.py246
8 files changed, 2277 insertions, 58 deletions
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
--- /dev/null
+++ b/object_collection_manager/icons/minus.png
Binary files 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