Merge branch 'blender-v2.92-release'
[blender-addons.git] / object_collection_manager / ui.py
blobb11a08bc7d8067afb320b8564c21b65ce5427ad2
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # Copyright 2011, Ryan Inch
21 import bpy
23 from bpy.types import (
24 Menu,
25 Operator,
26 Panel,
27 UIList,
30 from bpy.props import (
31 BoolProperty,
32 StringProperty,
35 # For VARS
36 from . import internals
38 # For FUNCTIONS
39 from .internals import (
40 update_collection_tree,
41 update_property_group,
42 generate_state,
43 get_move_selection,
44 get_move_active,
45 update_qcd_header,
48 from .qcd_operators import (
49 QCDAllBase,
53 preview_collections = {}
54 last_icon_theme_text = None
55 last_icon_theme_text_sel = None
58 class CollectionManager(Operator):
59 '''Manage and control collections, with advanced features, in a popup UI'''
60 bl_label = "Collection Manager"
61 bl_idname = "view3d.collection_manager"
63 last_view_layer = ""
65 window_open = False
67 master_collection: StringProperty(
68 default='Scene Collection',
69 name="",
70 description="Scene Collection"
73 def __init__(self):
74 self.window_open = True
76 def draw(self, context):
77 cls = CollectionManager
78 layout = self.layout
79 cm = context.scene.collection_manager
80 prefs = context.preferences.addons[__package__].preferences
81 view_layer = context.view_layer
83 if view_layer.name != cls.last_view_layer:
84 if prefs.enable_qcd:
85 bpy.app.timers.register(update_qcd_header)
87 update_collection_tree(context)
88 cls.last_view_layer = view_layer.name
90 # title and view layer
91 title_row = layout.split(factor=0.5)
92 main = title_row.row()
93 view = title_row.row(align=True)
94 view.alignment = 'RIGHT'
96 main.label(text="Collection Manager")
98 view.prop(view_layer, "use", text="")
99 view.separator()
101 window = context.window
102 scene = window.scene
103 view.template_search(
104 window, "view_layer",
105 scene, "view_layers",
106 new="scene.view_layer_add",
107 unlink="scene.view_layer_remove")
109 layout.row().separator()
110 layout.row().separator()
112 # buttons
113 button_row_1 = layout.row()
115 op_sec = button_row_1.row()
116 op_sec.alignment = 'LEFT'
118 collapse_sec = op_sec.row()
119 collapse_sec.alignment = 'LEFT'
120 collapse_sec.enabled = False
122 if len(internals.expanded) > 0:
123 text = "Collapse All Items"
124 else:
125 text = "Expand All Items"
127 collapse_sec.operator("view3d.expand_all_items", text=text)
129 for laycol in internals.collection_tree:
130 if laycol["has_children"]:
131 collapse_sec.enabled = True
132 break
134 if prefs.enable_qcd:
135 renum_sec = op_sec.row()
136 renum_sec.alignment = 'LEFT'
137 renum_sec.operator("view3d.renumerate_qcd_slots")
139 # menu & filter
140 right_sec = button_row_1.row()
141 right_sec.alignment = 'RIGHT'
143 right_sec.menu("VIEW3D_MT_CM_specials_menu")
144 right_sec.popover(panel="COLLECTIONMANAGER_PT_display_options",
145 text="", icon='FILTER')
147 mc_box = layout.box()
148 master_collection_row = mc_box.row(align=True)
150 # collection icon
151 c_icon = master_collection_row.row()
152 highlight = False
153 if (context.view_layer.active_layer_collection ==
154 context.view_layer.layer_collection):
155 highlight = True
157 prop = c_icon.operator("view3d.set_active_collection",
158 text='', icon='GROUP', depress=highlight)
159 prop.is_master_collection = True
160 prop.collection_name = 'Master Collection'
162 master_collection_row.separator()
164 # name
165 name_row = master_collection_row.row()
166 name_row.prop(self, "master_collection", text='')
167 name_row.enabled = False
169 master_collection_row.separator()
171 # global rtos
172 global_rto_row = master_collection_row.row()
173 global_rto_row.alignment = 'RIGHT'
175 row_setcol = global_rto_row.row()
176 row_setcol.alignment = 'LEFT'
177 row_setcol.operator_context = 'INVOKE_DEFAULT'
179 selected_objects = get_move_selection()
180 active_object = get_move_active()
181 CM_UL_items.selected_objects = selected_objects
182 CM_UL_items.active_object = active_object
184 collection = context.view_layer.layer_collection.collection
186 icon = 'MESH_CUBE'
188 if selected_objects:
189 if active_object and active_object.name in collection.objects:
190 icon = 'SNAP_VOLUME'
192 elif not selected_objects.isdisjoint(collection.objects):
193 icon = 'STICKY_UVS_LOC'
195 else:
196 row_setcol.enabled = False
198 prop = row_setcol.operator("view3d.set_collection", text="",
199 icon=icon, emboss=False)
200 prop.is_master_collection = True
201 prop.collection_name = 'Master Collection'
203 copy_icon = 'COPYDOWN'
204 swap_icon = 'ARROW_LEFTRIGHT'
205 copy_swap_icon = 'SELECT_INTERSECT'
207 if cm.show_exclude:
208 exclude_all_history = internals.rto_history["exclude_all"].get(view_layer.name, [])
209 depress = True if len(exclude_all_history) else False
210 icon = 'CHECKBOX_HLT'
211 buffers = [False, False]
213 if internals.copy_buffer["RTO"] == "exclude":
214 icon = copy_icon
215 buffers[0] = True
217 if internals.swap_buffer["A"]["RTO"] == "exclude":
218 icon = swap_icon
219 buffers[1] = True
221 if buffers[0] and buffers[1]:
222 icon = copy_swap_icon
224 global_rto_row.operator("view3d.un_exclude_all_collections", text="", icon=icon, depress=depress)
226 if cm.show_selectable:
227 select_all_history = internals.rto_history["select_all"].get(view_layer.name, [])
228 depress = True if len(select_all_history) else False
229 icon = 'RESTRICT_SELECT_OFF'
230 buffers = [False, False]
232 if internals.copy_buffer["RTO"] == "select":
233 icon = copy_icon
234 buffers[0] = True
236 if internals.swap_buffer["A"]["RTO"] == "select":
237 icon = swap_icon
238 buffers[1] = True
240 if buffers[0] and buffers[1]:
241 icon = copy_swap_icon
243 global_rto_row.operator("view3d.un_restrict_select_all_collections", text="", icon=icon, depress=depress)
245 if cm.show_hide_viewport:
246 hide_all_history = internals.rto_history["hide_all"].get(view_layer.name, [])
247 depress = True if len(hide_all_history) else False
248 icon = 'HIDE_OFF'
249 buffers = [False, False]
251 if internals.copy_buffer["RTO"] == "hide":
252 icon = copy_icon
253 buffers[0] = True
255 if internals.swap_buffer["A"]["RTO"] == "hide":
256 icon = swap_icon
257 buffers[1] = True
259 if buffers[0] and buffers[1]:
260 icon = copy_swap_icon
262 global_rto_row.operator("view3d.un_hide_all_collections", text="", icon=icon, depress=depress)
264 if cm.show_disable_viewport:
265 disable_all_history = internals.rto_history["disable_all"].get(view_layer.name, [])
266 depress = True if len(disable_all_history) else False
267 icon = 'RESTRICT_VIEW_OFF'
268 buffers = [False, False]
270 if internals.copy_buffer["RTO"] == "disable":
271 icon = copy_icon
272 buffers[0] = True
274 if internals.swap_buffer["A"]["RTO"] == "disable":
275 icon = swap_icon
276 buffers[1] = True
278 if buffers[0] and buffers[1]:
279 icon = copy_swap_icon
281 global_rto_row.operator("view3d.un_disable_viewport_all_collections", text="", icon=icon, depress=depress)
283 if cm.show_render:
284 render_all_history = internals.rto_history["render_all"].get(view_layer.name, [])
285 depress = True if len(render_all_history) else False
286 icon = 'RESTRICT_RENDER_OFF'
287 buffers = [False, False]
289 if internals.copy_buffer["RTO"] == "render":
290 icon = copy_icon
291 buffers[0] = True
293 if internals.swap_buffer["A"]["RTO"] == "render":
294 icon = swap_icon
295 buffers[1] = True
297 if buffers[0] and buffers[1]:
298 icon = copy_swap_icon
300 global_rto_row.operator("view3d.un_disable_render_all_collections", text="", icon=icon, depress=depress)
302 if cm.show_holdout:
303 holdout_all_history = internals.rto_history["holdout_all"].get(view_layer.name, [])
304 depress = True if len(holdout_all_history) else False
305 icon = 'HOLDOUT_ON'
306 buffers = [False, False]
308 if internals.copy_buffer["RTO"] == "holdout":
309 icon = copy_icon
310 buffers[0] = True
312 if internals.swap_buffer["A"]["RTO"] == "holdout":
313 icon = swap_icon
314 buffers[1] = True
316 if buffers[0] and buffers[1]:
317 icon = copy_swap_icon
319 global_rto_row.operator("view3d.un_holdout_all_collections", text="", icon=icon, depress=depress)
321 if cm.show_indirect_only:
322 indirect_all_history = internals.rto_history["indirect_all"].get(view_layer.name, [])
323 depress = True if len(indirect_all_history) else False
324 icon = 'INDIRECT_ONLY_ON'
325 buffers = [False, False]
327 if internals.copy_buffer["RTO"] == "indirect":
328 icon = copy_icon
329 buffers[0] = True
331 if internals.swap_buffer["A"]["RTO"] == "indirect":
332 icon = swap_icon
333 buffers[1] = True
335 if buffers[0] and buffers[1]:
336 icon = copy_swap_icon
338 global_rto_row.operator("view3d.un_indirect_only_all_collections", text="", icon=icon, depress=depress)
340 # treeview
341 layout.row().template_list("CM_UL_items", "",
342 cm, "cm_list_collection",
343 cm, "cm_list_index",
344 rows=15,
345 sort_lock=True)
347 # add collections
348 button_row_2 = layout.row()
349 prop = button_row_2.operator("view3d.add_collection", text="Add Collection",
350 icon='COLLECTION_NEW')
351 prop.child = False
353 prop = button_row_2.operator("view3d.add_collection", text="Add SubCollection",
354 icon='COLLECTION_NEW')
355 prop.child = True
358 button_row_3 = layout.row()
360 # phantom mode
361 phantom_mode = button_row_3.row(align=True)
362 toggle_text = "Disable " if cm.in_phantom_mode else "Enable "
363 phantom_mode.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode")
364 phantom_mode.operator("view3d.apply_phantom_mode", text="", icon='CHECKMARK')
366 if cm.in_phantom_mode:
367 view.enabled = False
368 if prefs.enable_qcd:
369 renum_sec.enabled = False
371 c_icon.enabled = False
372 row_setcol.enabled = False
373 addcollec_row.enabled = False
376 def execute(self, context):
377 wm = context.window_manager
379 update_property_group(context)
381 cm = context.scene.collection_manager
382 view_layer = context.view_layer
384 self.view_layer = view_layer.name
386 # make sure list index is valid
387 if cm.cm_list_index >= len(cm.cm_list_collection):
388 cm.cm_list_index = -1
390 # check if expanded & history/buffer state still correct
391 if internals.collection_state:
392 new_state = generate_state()
394 if new_state["name"] != internals.collection_state["name"]:
395 internals.copy_buffer["RTO"] = ""
396 internals.copy_buffer["values"].clear()
398 internals.swap_buffer["A"]["RTO"] = ""
399 internals.swap_buffer["A"]["values"].clear()
400 internals.swap_buffer["B"]["RTO"] = ""
401 internals.swap_buffer["B"]["values"].clear()
403 for name in list(internals.expanded):
404 laycol = internals.layer_collections.get(name)
405 if not laycol or not laycol["has_children"]:
406 internals.expanded.remove(name)
408 for name in list(internals.expand_history["history"]):
409 laycol = internals.layer_collections.get(name)
410 if not laycol or not laycol["has_children"]:
411 internals.expand_history["history"].remove(name)
413 for rto, history in internals.rto_history.items():
414 if view_layer.name in history:
415 del history[view_layer.name]
418 else:
419 for rto in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
420 if new_state[rto] != internals.collection_state[rto]:
421 if view_layer.name in internals.rto_history[rto]:
422 del internals.rto_history[rto][view_layer.name]
424 if view_layer.name in internals.rto_history[rto+"_all"]:
425 del internals.rto_history[rto+"_all"][view_layer.name]
427 # check if in phantom mode and if it's still viable
428 if cm.in_phantom_mode:
429 if internals.layer_collections.keys() != internals.phantom_history["initial_state"].keys():
430 cm.in_phantom_mode = False
432 if view_layer.name != internals.phantom_history["view_layer"]:
433 cm.in_phantom_mode = False
435 if not cm.in_phantom_mode:
436 for key, value in internals.phantom_history.items():
437 try:
438 value.clear()
439 except AttributeError:
440 if key == "view_layer":
441 internals.phantom_history["view_layer"] = ""
443 # handle window sizing
444 max_width = 960
445 min_width = 456
446 row_indent_width = 15
447 width_step = 21
448 qcd_width = 30
449 scrollbar_width = 21
451 width = min_width + row_indent_width + (width_step * internals.max_lvl)
453 if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
454 width += qcd_width
456 if len(internals.layer_collections) > 14:
457 width += scrollbar_width
459 if width > max_width:
460 width = max_width
462 return wm.invoke_popup(self, width=width)
464 def __del__(self):
465 if not self.window_open:
466 # prevent destructor execution when changing templates
467 return
469 internals.collection_state.clear()
470 internals.collection_state.update(generate_state())
473 class CM_UL_items(UIList):
474 filtering = False
475 last_filter_value = ""
477 selected_objects = set()
478 active_object = None
480 visible_items = []
481 new_collections = []
483 filter_name: StringProperty(
484 name="Filter By Name",
485 default="",
486 description="Filter collections by name",
487 update=lambda self, context:
488 CM_UL_items.new_collections.clear(),
491 use_filter_invert: BoolProperty(
492 name="Invert",
493 default=False,
494 description="Invert filtering (show hidden items, and vice-versa)",
497 filter_by_selected: BoolProperty(
498 name="Filter By Selected",
499 default=False,
500 description="Filter collections by selected items",
501 update=lambda self, context:
502 CM_UL_items.new_collections.clear(),
504 filter_by_qcd: BoolProperty(
505 name="Filter By QCD",
506 default=False,
507 description="Filter collections to only show QCD slots",
508 update=lambda self, context:
509 CM_UL_items.new_collections.clear(),
512 def draw_item(self, context, layout, data, item, icon, active_data,active_propname, index):
513 self.use_filter_show = True
515 cm = context.scene.collection_manager
516 prefs = context.preferences.addons[__package__].preferences
517 view_layer = context.view_layer
518 laycol = internals.layer_collections[item.name]
519 collection = laycol["ptr"].collection
520 selected_objects = CM_UL_items.selected_objects
521 active_object = CM_UL_items.active_object
523 column = layout.column(align=True)
525 main_row = column.row()
527 s1 = main_row.row(align=True)
528 s1.alignment = 'LEFT'
530 s2 = main_row.row(align=True)
531 s2.alignment = 'RIGHT'
533 row = s1
535 # allow room to select the row from the beginning
536 row.separator()
538 # indent child items
539 if laycol["lvl"] > 0:
540 for _ in range(laycol["lvl"]):
541 row.label(icon='BLANK1')
543 # add expander if collection has children to make UIList act like tree view
544 if laycol["has_children"]:
545 if laycol["expanded"]:
546 highlight = True if internals.expand_history["target"] == item.name else False
548 prop = row.operator("view3d.expand_sublevel", text="",
549 icon='DISCLOSURE_TRI_DOWN',
550 emboss=highlight, depress=highlight)
551 prop.expand = False
552 prop.name = item.name
553 prop.index = index
555 else:
556 highlight = True if internals.expand_history["target"] == item.name else False
558 prop = row.operator("view3d.expand_sublevel", text="",
559 icon='DISCLOSURE_TRI_RIGHT',
560 emboss=highlight, depress=highlight)
561 prop.expand = True
562 prop.name = item.name
563 prop.index = index
565 else:
566 row.label(icon='BLANK1')
569 # collection icon
570 c_icon = row.row()
571 highlight = False
572 if (context.view_layer.active_layer_collection == laycol["ptr"]):
573 highlight = True
575 prop = c_icon.operator("view3d.set_active_collection", text='', icon='GROUP',
576 emboss=highlight, depress=highlight)
578 prop.is_master_collection = False
579 prop.collection_name = item.name
581 if prefs.enable_qcd:
582 QCD = row.row()
583 QCD.scale_x = 0.4
584 QCD.prop(item, "qcd_slot_idx", text="")
586 c_name = row.row()
588 #if rename[0] and index == cm.cm_list_index:
589 #c_name.activate_init = True
590 #rename[0] = False
592 c_name.prop(item, "name", text="", expand=True)
594 # used as a separator (actual separator not wide enough)
595 row.label()
597 row = s2 if cm.align_local_ops else s1
599 # add set_collection op
600 set_obj_col = row.row()
601 set_obj_col.operator_context = 'INVOKE_DEFAULT'
603 icon = 'MESH_CUBE'
605 if selected_objects:
606 if active_object and active_object.name in collection.objects:
607 icon = 'SNAP_VOLUME'
609 elif not selected_objects.isdisjoint(collection.objects):
610 icon = 'STICKY_UVS_LOC'
612 else:
613 set_obj_col.enabled = False
616 prop = set_obj_col.operator("view3d.set_collection", text="",
617 icon=icon, emboss=False)
618 prop.is_master_collection = False
619 prop.collection_name = item.name
622 if cm.show_exclude:
623 exclude_history_base = internals.rto_history["exclude"].get(view_layer.name, {})
624 exclude_target = exclude_history_base.get("target", "")
625 exclude_history = exclude_history_base.get("history", [])
627 highlight = bool(exclude_history and exclude_target == item.name)
628 icon = 'CHECKBOX_DEHLT' if laycol["ptr"].exclude else 'CHECKBOX_HLT'
630 prop = row.operator("view3d.exclude_collection", text="", icon=icon,
631 emboss=highlight, depress=highlight)
632 prop.name = item.name
634 if cm.show_selectable:
635 select_history_base = internals.rto_history["select"].get(view_layer.name, {})
636 select_target = select_history_base.get("target", "")
637 select_history = select_history_base.get("history", [])
639 highlight = bool(select_history and select_target == item.name)
640 icon = ('RESTRICT_SELECT_ON' if laycol["ptr"].collection.hide_select else
641 'RESTRICT_SELECT_OFF')
643 prop = row.operator("view3d.restrict_select_collection", text="", icon=icon,
644 emboss=highlight, depress=highlight)
645 prop.name = item.name
647 if cm.show_hide_viewport:
648 hide_history_base = internals.rto_history["hide"].get(view_layer.name, {})
649 hide_target = hide_history_base.get("target", "")
650 hide_history = hide_history_base.get("history", [])
652 highlight = bool(hide_history and hide_target == item.name)
653 icon = 'HIDE_ON' if laycol["ptr"].hide_viewport else 'HIDE_OFF'
655 prop = row.operator("view3d.hide_collection", text="", icon=icon,
656 emboss=highlight, depress=highlight)
657 prop.name = item.name
659 if cm.show_disable_viewport:
660 disable_history_base = internals.rto_history["disable"].get(view_layer.name, {})
661 disable_target = disable_history_base.get("target", "")
662 disable_history = disable_history_base.get("history", [])
664 highlight = bool(disable_history and disable_target == item.name)
665 icon = ('RESTRICT_VIEW_ON' if laycol["ptr"].collection.hide_viewport else
666 'RESTRICT_VIEW_OFF')
668 prop = row.operator("view3d.disable_viewport_collection", text="", icon=icon,
669 emboss=highlight, depress=highlight)
670 prop.name = item.name
672 if cm.show_render:
673 render_history_base = internals.rto_history["render"].get(view_layer.name, {})
674 render_target = render_history_base.get("target", "")
675 render_history = render_history_base.get("history", [])
677 highlight = bool(render_history and render_target == item.name)
678 icon = ('RESTRICT_RENDER_ON' if laycol["ptr"].collection.hide_render else
679 'RESTRICT_RENDER_OFF')
681 prop = row.operator("view3d.disable_render_collection", text="", icon=icon,
682 emboss=highlight, depress=highlight)
683 prop.name = item.name
685 if cm.show_holdout:
686 holdout_history_base = internals.rto_history["holdout"].get(view_layer.name, {})
687 holdout_target = holdout_history_base.get("target", "")
688 holdout_history = holdout_history_base.get("history", [])
690 highlight = bool(holdout_history and holdout_target == item.name)
691 icon = ('HOLDOUT_ON' if laycol["ptr"].holdout else
692 'HOLDOUT_OFF')
694 prop = row.operator("view3d.holdout_collection", text="", icon=icon,
695 emboss=highlight, depress=highlight)
696 prop.name = item.name
698 if cm.show_indirect_only:
699 indirect_history_base = internals.rto_history["indirect"].get(view_layer.name, {})
700 indirect_target = indirect_history_base.get("target", "")
701 indirect_history = indirect_history_base.get("history", [])
703 highlight = bool(indirect_history and indirect_target == item.name)
704 icon = ('INDIRECT_ONLY_ON' if laycol["ptr"].indirect_only else
705 'INDIRECT_ONLY_OFF')
707 prop = row.operator("view3d.indirect_only_collection", text="", icon=icon,
708 emboss=highlight, depress=highlight)
709 prop.name = item.name
713 row = s2
715 row.separator()
716 row.separator()
718 rm_op = row.row()
719 prop = rm_op.operator("view3d.remove_collection", text="", icon='X', emboss=False)
720 prop.collection_name = item.name
723 if len(data.cm_list_collection) > index + 1:
724 line_separator = column.row(align=True)
725 line_separator.ui_units_y = 0.01
726 line_separator.scale_y = 0.1
727 line_separator.enabled = False
729 line_separator.separator()
730 line_separator.label(icon='BLANK1')
732 for _ in range(laycol["lvl"] + 1):
733 line_separator.label(icon='BLANK1')
735 line_separator.prop(cm, "ui_separator")
737 if cm.in_phantom_mode:
738 c_icon.enabled = False
739 c_name.enabled = False
740 set_obj_col.enabled = False
741 rm_op.enabled = False
743 if prefs.enable_qcd:
744 QCD.enabled = False
747 def draw_filter(self, context, layout):
748 row = layout.row()
750 subrow = row.row(align=True)
751 subrow.prop(self, "filter_name", text="")
753 icon = 'ZOOM_OUT' if self.use_filter_invert else 'ZOOM_IN'
754 subrow.prop(self, "use_filter_invert", text="", icon=icon)
756 subrow = row.row(align=True)
757 subrow.prop(self, "filter_by_selected", text="", icon='SNAP_VOLUME')
759 if context.preferences.addons[__package__].preferences.enable_qcd:
760 subrow.prop(self, "filter_by_qcd", text="", icon='EVENT_Q')
762 def filter_items(self, context, data, propname):
763 CM_UL_items.filtering = False
765 flt_flags = []
766 flt_neworder = []
767 list_items = getattr(data, propname)
770 if self.filter_name:
771 CM_UL_items.filtering = True
773 new_flt_flags = filter_items_by_name_custom(self.filter_name, self.bitflag_filter_item, list_items)
775 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
778 if self.filter_by_selected:
779 CM_UL_items.filtering = True
780 new_flt_flags = [0] * len(list_items)
782 for idx, item in enumerate(list_items):
783 collection = internals.layer_collections[item.name]["ptr"].collection
785 # check if any of the selected objects are in the collection
786 if not set(context.selected_objects).isdisjoint(collection.objects):
787 new_flt_flags[idx] = self.bitflag_filter_item
789 # add in any recently created collections
790 if item.name in CM_UL_items.new_collections:
791 new_flt_flags[idx] = self.bitflag_filter_item
793 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
796 if self.filter_by_qcd:
797 CM_UL_items.filtering = True
798 new_flt_flags = [0] * len(list_items)
800 for idx, item in enumerate(list_items):
801 if item.qcd_slot_idx:
802 new_flt_flags[idx] = self.bitflag_filter_item
804 # add in any recently created collections
805 if item.name in CM_UL_items.new_collections:
806 new_flt_flags[idx] = self.bitflag_filter_item
808 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
811 if not CM_UL_items.filtering: # display as treeview
812 CM_UL_items.new_collections.clear()
813 flt_flags = [0] * len(list_items)
815 for idx, item in enumerate(list_items):
816 if internals.layer_collections[item.name]["visible"]:
817 flt_flags[idx] = self.bitflag_filter_item
820 if self.use_filter_invert:
821 CM_UL_items.filtering = True # invert can act as pseudo filtering
822 for idx, flag in enumerate(flt_flags):
823 flt_flags[idx] = 0 if flag else self.bitflag_filter_item
826 # update visible items list
827 CM_UL_items.visible_items.clear()
828 CM_UL_items.visible_items.extend(flt_flags)
830 return flt_flags, flt_neworder
834 def invoke(self, context, event):
835 pass
838 class CMDisplayOptionsPanel(Panel):
839 bl_label = "Display Options"
840 bl_idname = "COLLECTIONMANAGER_PT_display_options"
842 # set space type to VIEW_3D and region type to HEADER
843 # because we only need it in a popover in the 3D View
844 # and don't want it always present in the UI/N-Panel
845 bl_space_type = 'VIEW_3D'
846 bl_region_type = 'HEADER'
848 def draw(self, context):
849 cm = context.scene.collection_manager
851 layout = self.layout
853 panel_header = layout.row()
854 panel_header.alignment = 'CENTER'
855 panel_header.label(text="Display Options")
857 layout.separator()
859 section_header = layout.row()
860 section_header.alignment = 'LEFT'
861 section_header.label(text="Restriction Toggles")
863 row = layout.row()
864 row.prop(cm, "show_exclude", icon='CHECKBOX_HLT', icon_only=True)
865 row.prop(cm, "show_selectable", icon='RESTRICT_SELECT_OFF', icon_only=True)
866 row.prop(cm, "show_hide_viewport", icon='HIDE_OFF', icon_only=True)
867 row.prop(cm, "show_disable_viewport", icon='RESTRICT_VIEW_OFF', icon_only=True)
868 row.prop(cm, "show_render", icon='RESTRICT_RENDER_OFF', icon_only=True)
869 row.prop(cm, "show_holdout", icon='HOLDOUT_ON', icon_only=True)
870 row.prop(cm, "show_indirect_only", icon='INDIRECT_ONLY_ON', icon_only=True)
872 layout.separator()
874 section_header = layout.row()
875 section_header.label(text="Layout")
877 row = layout.row()
878 row.prop(cm, "align_local_ops")
881 class SpecialsMenu(Menu):
882 bl_label = "Specials"
883 bl_idname = "VIEW3D_MT_CM_specials_menu"
885 def draw(self, context):
886 layout = self.layout
888 prop = layout.operator("view3d.remove_empty_collections")
889 prop.without_objects = False
891 prop = layout.operator("view3d.remove_empty_collections",
892 text="Purge All Collections Without Objects")
893 prop.without_objects = True
896 class EnableAllQCDSlotsMenu(Menu):
897 bl_label = "Global QCD Slot Actions"
898 bl_idname = "VIEW3D_MT_CM_qcd_enable_all_menu"
900 def draw(self, context):
901 layout = self.layout
903 layout.operator("view3d.enable_all_qcd_slots")
904 layout.operator("view3d.enable_all_qcd_slots_isolated")
906 layout.separator()
908 layout.operator("view3d.isolate_selected_objects_collections")
909 if context.mode == 'OBJECT':
910 layout.operator("view3d.disable_selected_objects_collections")
912 layout.separator()
914 layout.operator("view3d.disable_all_non_qcd_slots")
915 layout.operator("view3d.disable_all_collections")
917 if context.mode == 'OBJECT':
918 layout.separator()
919 layout.operator("view3d.select_all_qcd_objects")
921 layout.separator()
923 layout.operator("view3d.discard_qcd_history")
926 def view3d_header_qcd_slots(self, context):
927 update_collection_tree(context)
929 view_layer = context.view_layer
930 layout = self.layout
931 idx = 1
933 if internals.qcd_collection_state:
934 view_layer = context.view_layer
935 new_state = generate_state(qcd=True)
937 if (new_state["name"] != internals.qcd_collection_state["name"]
938 or new_state["exclude"] != internals.qcd_collection_state["exclude"]
939 or new_state["qcd"] != internals.qcd_collection_state["qcd"]):
940 if view_layer.name in internals.qcd_history:
941 del internals.qcd_history[view_layer.name]
942 internals.qcd_collection_state.clear()
943 QCDAllBase.clear()
946 main_row = layout.row(align=True)
947 current_qcd_history = internals.qcd_history.get(context.view_layer.name, [])
949 main_row.operator_menu_hold("view3d.enable_all_qcd_slots_meta",
950 text="",
951 icon='HIDE_OFF',
952 depress=bool(current_qcd_history),
953 menu="VIEW3D_MT_CM_qcd_enable_all_menu")
956 split = main_row.split()
957 col = split.column(align=True)
958 row = col.row(align=True)
959 row.scale_y = 0.5
961 selected_objects = get_move_selection()
962 active_object = get_move_active()
964 for x in range(20):
965 qcd_slot_name = internals.qcd_slots.get_name(str(x+1))
967 if qcd_slot_name:
968 qcd_laycol = internals.layer_collections[qcd_slot_name]["ptr"]
969 collection_objects = qcd_laycol.collection.objects
971 icon_value = 0
973 # if the active object is in the current collection use a custom icon
974 if (active_object and active_object in selected_objects and
975 active_object.name in collection_objects):
976 icon = 'LAYER_ACTIVE'
978 # if there are selected objects use LAYER_ACTIVE
979 elif not selected_objects.isdisjoint(collection_objects):
980 icon = 'LAYER_USED'
982 # If there are objects use LAYER_USED
983 elif collection_objects:
984 icon = 'NONE'
985 active_icon = get_active_icon(context, qcd_laycol)
986 icon_value = active_icon.icon_id
988 else:
989 icon = 'BLANK1'
992 prop = row.operator("view3d.view_move_qcd_slot", text="", icon=icon,
993 icon_value=icon_value, depress=not qcd_laycol.exclude)
994 prop.slot = str(x+1)
996 else:
997 row.label(text="", icon='X')
1000 if idx%5==0:
1001 row.separator()
1003 if idx == 10:
1004 row = col.row(align=True)
1005 row.scale_y = 0.5
1007 idx += 1
1010 def view_layer_update(self, context):
1011 if context.view_layer.name != CollectionManager.last_view_layer:
1012 bpy.app.timers.register(update_qcd_header)
1013 CollectionManager.last_view_layer = context.view_layer.name
1016 def get_active_icon(context, qcd_laycol):
1017 global last_icon_theme_text
1018 global last_icon_theme_text_sel
1020 tool_theme = context.preferences.themes[0].user_interface.wcol_tool
1021 pcoll = preview_collections["icons"]
1023 if qcd_laycol.exclude:
1024 theme_color = tool_theme.text
1025 last_theme_color = last_icon_theme_text
1026 icon = pcoll["active_icon_text"]
1028 else:
1029 theme_color = tool_theme.text_sel
1030 last_theme_color = last_icon_theme_text_sel
1031 icon = pcoll["active_icon_text_sel"]
1033 if last_theme_color == None or theme_color.hsv != last_theme_color:
1034 update_icon(pcoll["active_icon_base"], icon, theme_color)
1036 if qcd_laycol.exclude:
1037 last_icon_theme_text = theme_color.hsv
1039 else:
1040 last_icon_theme_text_sel = theme_color.hsv
1042 return icon
1045 def update_icon(base, icon, theme_color):
1046 icon.icon_pixels = base.icon_pixels
1047 colored_icon = []
1049 for offset in range(len(icon.icon_pixels)):
1050 idx = offset * 4
1052 r = icon.icon_pixels_float[idx]
1053 g = icon.icon_pixels_float[idx+1]
1054 b = icon.icon_pixels_float[idx+2]
1055 a = icon.icon_pixels_float[idx+3]
1057 # add back some brightness and opacity blender takes away from the custom icon
1058 r = min(r+r*0.2,1)
1059 g = min(g+g*0.2,1)
1060 b = min(b+b*0.2,1)
1061 a = min(a+a*0.2,1)
1063 # make the icon follow the theme color (assuming the icon is white)
1064 r *= theme_color.r
1065 g *= theme_color.g
1066 b *= theme_color.b
1068 colored_icon.append(r)
1069 colored_icon.append(g)
1070 colored_icon.append(b)
1071 colored_icon.append(a)
1073 icon.icon_pixels_float = colored_icon
1076 def filter_items_by_name_custom(pattern, bitflag, items, propname="name", flags=None, reverse=False):
1078 Set FILTER_ITEM for items which name matches filter_name one (case-insensitive).
1079 pattern is the filtering pattern.
1080 propname is the name of the string property to use for filtering.
1081 flags must be a list of integers the same length as items, or None!
1082 return a list of flags (based on given flags if not None),
1083 or an empty list if no flags were given and no filtering has been done.
1085 import fnmatch
1087 if not pattern or not items: # Empty pattern or list = no filtering!
1088 return flags or []
1090 if flags is None:
1091 flags = [0] * len(items)
1093 # Make pattern case-insensitive
1094 pattern = pattern.lower()
1096 # Implicitly add heading/trailing wildcards.
1097 pattern = "*" + pattern + "*"
1099 for i, item in enumerate(items):
1100 name = getattr(item, propname, None)
1102 # Make name case-insensitive
1103 name = name.lower()
1105 # This is similar to a logical xor
1106 if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse):
1107 flags[i] |= bitflag
1109 # add in any recently created collections
1110 if item.name in CM_UL_items.new_collections:
1111 flags[i] |= bitflag
1113 return flags
1115 def merge_flt_flags(l1, l2):
1116 for idx, _ in enumerate(l1):
1117 l1[idx] &= l2.pop(0)
1119 return l1 + l2