Skinify: fix shape generation
[blender-addons.git] / object_collection_manager / ui.py
blob25987b25ae179b290ea7adc8dd0bad8a0d13f6a7
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Copyright 2011, Ryan Inch
5 import bpy
7 from bpy.types import (
8 Menu,
9 Operator,
10 Panel,
11 UIList,
14 from bpy.props import (
15 BoolProperty,
16 StringProperty,
19 # For VARS
20 from . import internals
22 # For FUNCTIONS
23 from .internals import (
24 update_collection_tree,
25 update_property_group,
26 generate_state,
27 check_state,
28 get_move_selection,
29 get_move_active,
30 update_qcd_header,
31 add_vertical_separator_line,
34 preview_collections = {}
35 last_icon_theme_text = None
36 last_icon_theme_text_sel = None
39 class CollectionManager(Operator):
40 '''Manage and control collections, with advanced features, in a popup UI'''
41 bl_label = "Collection Manager"
42 bl_idname = "view3d.collection_manager"
44 last_view_layer = ""
46 window_open = False
48 master_collection: StringProperty(
49 default='Scene Collection',
50 name="",
51 description="Scene Collection"
54 def __init__(self):
55 self.window_open = True
57 def draw(self, context):
58 cls = CollectionManager
59 layout = self.layout
60 cm = context.scene.collection_manager
61 prefs = context.preferences.addons[__package__].preferences
62 view_layer = context.view_layer
63 collection = context.view_layer.layer_collection.collection
65 if view_layer.name != cls.last_view_layer:
66 if prefs.enable_qcd:
67 bpy.app.timers.register(update_qcd_header)
69 update_collection_tree(context)
70 cls.last_view_layer = view_layer.name
72 # title and view layer
73 title_row = layout.split(factor=0.5)
74 main = title_row.row()
75 view = title_row.row(align=True)
76 view.alignment = 'RIGHT'
78 main.label(text="Collection Manager")
80 view.prop(view_layer, "use", text="")
81 view.separator()
83 window = context.window
84 scene = window.scene
85 view.template_search(
86 window, "view_layer",
87 scene, "view_layers",
88 new="scene.view_layer_add",
89 unlink="scene.view_layer_remove")
91 layout.row().separator()
92 layout.row().separator()
94 # buttons
95 button_row_1 = layout.row()
97 op_sec = button_row_1.row()
98 op_sec.alignment = 'LEFT'
100 collapse_sec = op_sec.row()
101 collapse_sec.alignment = 'LEFT'
102 collapse_sec.enabled = False
104 if len(internals.expanded) > 0:
105 text = "Collapse All Items"
106 else:
107 text = "Expand All Items"
109 collapse_sec.operator("view3d.expand_all_items", text=text)
111 for laycol in internals.collection_tree:
112 if laycol["has_children"]:
113 collapse_sec.enabled = True
114 break
116 if prefs.enable_qcd:
117 renum_sec = op_sec.row()
118 renum_sec.alignment = 'LEFT'
119 renum_sec.operator("view3d.renumerate_qcd_slots")
121 undo_sec = op_sec.row(align=True)
122 undo_sec.alignment = 'LEFT'
123 undo_sec.operator("view3d.undo_wrapper", text="", icon='LOOP_BACK')
124 undo_sec.operator("view3d.redo_wrapper", text="", icon='LOOP_FORWARDS')
126 # menu & filter
127 right_sec = button_row_1.row()
128 right_sec.alignment = 'RIGHT'
130 specials_menu = right_sec.row()
131 specials_menu.alignment = 'RIGHT'
132 specials_menu.menu("VIEW3D_MT_CM_specials_menu")
134 display_options = right_sec.row()
135 display_options.alignment = 'RIGHT'
136 display_options.popover(panel="COLLECTIONMANAGER_PT_display_options",
137 text="", icon='FILTER')
139 mc_box = layout.box()
140 master_collection_row = mc_box.row(align=True)
142 # collection icon
143 c_icon = master_collection_row.row()
144 highlight = False
145 if (context.view_layer.active_layer_collection ==
146 context.view_layer.layer_collection):
147 highlight = True
149 prop = c_icon.operator("view3d.set_active_collection",
150 text='', icon='GROUP', depress=highlight)
151 prop.is_master_collection = True
152 prop.collection_name = 'Scene Collection'
154 master_collection_row.separator()
156 # name
157 name_row = master_collection_row.row(align=True)
158 name_field = name_row.row(align=True)
159 name_field.prop(self, "master_collection", text='')
160 name_field.enabled = False
162 # set selection
163 setsel = name_row.row(align=True)
164 icon = 'DOT'
165 some_selected = False
167 if not collection.objects:
168 icon = 'BLANK1'
169 setsel.active = False
171 else:
172 all_selected = None
173 all_unreachable = None
175 for obj in collection.objects:
176 if not obj.visible_get() or obj.hide_select:
177 if all_unreachable != False:
178 all_unreachable = True
180 else:
181 all_unreachable = False
183 if obj.select_get() == False:
184 # some objects remain unselected
185 icon = 'KEYFRAME'
186 all_selected = False
188 else:
189 some_selected = True
191 if all_selected == False:
192 break
194 all_selected = True
197 if all_selected:
198 # all objects are selected
199 icon = 'KEYFRAME_HLT'
201 if all_unreachable:
202 if collection.objects:
203 icon = 'DOT'
205 setsel.active = False
207 prop = setsel.operator("view3d.select_collection_objects",
208 text="",
209 icon=icon,
210 depress=some_selected,
212 prop.is_master_collection = True
213 prop.collection_name = 'Scene Collection'
216 # global rtos
217 global_rto_row = master_collection_row.row()
218 global_rto_row.alignment = 'RIGHT'
220 # used as a separator (actual separator not wide enough)
221 global_rto_row.label()
224 # set collection
225 row_setcol = global_rto_row.row()
226 row_setcol.alignment = 'LEFT'
227 row_setcol.operator_context = 'INVOKE_DEFAULT'
229 selected_objects = get_move_selection()
230 active_object = get_move_active()
231 CM_UL_items.selected_objects = selected_objects
232 CM_UL_items.active_object = active_object
234 collection = context.view_layer.layer_collection.collection
236 icon = 'IMPORT'
238 if collection.objects:
239 icon = 'MESH_CUBE'
241 if selected_objects:
242 if active_object and active_object.name in collection.objects:
243 icon = 'SNAP_VOLUME'
245 elif not selected_objects.isdisjoint(collection.objects):
246 icon = 'STICKY_UVS_LOC'
248 else:
249 row_setcol.active = False
252 # add vertical separator line
253 separator = row_setcol.row()
254 separator.scale_x = 0.2
255 separator.enabled = False
256 separator.operator("view3d.cm_ui_separator_button",
257 text="",
258 icon='BLANK1',
261 # add operator
262 prop = row_setcol.operator("view3d.send_objects_to_collection", text="",
263 icon=icon, emboss=False)
264 prop.is_master_collection = True
265 prop.collection_name = 'Scene Collection'
267 # add vertical separator line
268 separator = row_setcol.row()
269 separator.scale_x = 0.2
270 separator.enabled = False
271 separator.operator("view3d.cm_ui_separator_button",
272 text="",
273 icon='BLANK1',
276 copy_icon = 'COPYDOWN'
277 swap_icon = 'ARROW_LEFTRIGHT'
278 copy_swap_icon = 'SELECT_INTERSECT'
280 if cm.show_exclude:
281 exclude_all_history = internals.rto_history["exclude_all"].get(view_layer.name, [])
282 depress = True if len(exclude_all_history) else False
283 icon = 'CHECKBOX_HLT'
284 buffers = [False, False]
286 if internals.copy_buffer["RTO"] == "exclude":
287 icon = copy_icon
288 buffers[0] = True
290 if internals.swap_buffer["A"]["RTO"] == "exclude":
291 icon = swap_icon
292 buffers[1] = True
294 if buffers[0] and buffers[1]:
295 icon = copy_swap_icon
297 global_rto_row.operator("view3d.un_exclude_all_collections", text="", icon=icon, depress=depress)
299 if cm.show_selectable:
300 select_all_history = internals.rto_history["select_all"].get(view_layer.name, [])
301 depress = True if len(select_all_history) else False
302 icon = 'RESTRICT_SELECT_OFF'
303 buffers = [False, False]
305 if internals.copy_buffer["RTO"] == "select":
306 icon = copy_icon
307 buffers[0] = True
309 if internals.swap_buffer["A"]["RTO"] == "select":
310 icon = swap_icon
311 buffers[1] = True
313 if buffers[0] and buffers[1]:
314 icon = copy_swap_icon
316 global_rto_row.operator("view3d.un_restrict_select_all_collections", text="", icon=icon, depress=depress)
318 if cm.show_hide_viewport:
319 hide_all_history = internals.rto_history["hide_all"].get(view_layer.name, [])
320 depress = True if len(hide_all_history) else False
321 icon = 'HIDE_OFF'
322 buffers = [False, False]
324 if internals.copy_buffer["RTO"] == "hide":
325 icon = copy_icon
326 buffers[0] = True
328 if internals.swap_buffer["A"]["RTO"] == "hide":
329 icon = swap_icon
330 buffers[1] = True
332 if buffers[0] and buffers[1]:
333 icon = copy_swap_icon
335 global_rto_row.operator("view3d.un_hide_all_collections", text="", icon=icon, depress=depress)
337 if cm.show_disable_viewport:
338 disable_all_history = internals.rto_history["disable_all"].get(view_layer.name, [])
339 depress = True if len(disable_all_history) else False
340 icon = 'RESTRICT_VIEW_OFF'
341 buffers = [False, False]
343 if internals.copy_buffer["RTO"] == "disable":
344 icon = copy_icon
345 buffers[0] = True
347 if internals.swap_buffer["A"]["RTO"] == "disable":
348 icon = swap_icon
349 buffers[1] = True
351 if buffers[0] and buffers[1]:
352 icon = copy_swap_icon
354 global_rto_row.operator("view3d.un_disable_viewport_all_collections", text="", icon=icon, depress=depress)
356 if cm.show_render:
357 render_all_history = internals.rto_history["render_all"].get(view_layer.name, [])
358 depress = True if len(render_all_history) else False
359 icon = 'RESTRICT_RENDER_OFF'
360 buffers = [False, False]
362 if internals.copy_buffer["RTO"] == "render":
363 icon = copy_icon
364 buffers[0] = True
366 if internals.swap_buffer["A"]["RTO"] == "render":
367 icon = swap_icon
368 buffers[1] = True
370 if buffers[0] and buffers[1]:
371 icon = copy_swap_icon
373 global_rto_row.operator("view3d.un_disable_render_all_collections", text="", icon=icon, depress=depress)
375 if cm.show_holdout:
376 holdout_all_history = internals.rto_history["holdout_all"].get(view_layer.name, [])
377 depress = True if len(holdout_all_history) else False
378 icon = 'HOLDOUT_ON'
379 buffers = [False, False]
381 if internals.copy_buffer["RTO"] == "holdout":
382 icon = copy_icon
383 buffers[0] = True
385 if internals.swap_buffer["A"]["RTO"] == "holdout":
386 icon = swap_icon
387 buffers[1] = True
389 if buffers[0] and buffers[1]:
390 icon = copy_swap_icon
392 global_rto_row.operator("view3d.un_holdout_all_collections", text="", icon=icon, depress=depress)
394 if cm.show_indirect_only:
395 indirect_all_history = internals.rto_history["indirect_all"].get(view_layer.name, [])
396 depress = True if len(indirect_all_history) else False
397 icon = 'INDIRECT_ONLY_ON'
398 buffers = [False, False]
400 if internals.copy_buffer["RTO"] == "indirect":
401 icon = copy_icon
402 buffers[0] = True
404 if internals.swap_buffer["A"]["RTO"] == "indirect":
405 icon = swap_icon
406 buffers[1] = True
408 if buffers[0] and buffers[1]:
409 icon = copy_swap_icon
411 global_rto_row.operator("view3d.un_indirect_only_all_collections", text="", icon=icon, depress=depress)
413 # treeview
414 layout.row().template_list("CM_UL_items", "",
415 cm, "cm_list_collection",
416 cm, "cm_list_index",
417 rows=15,
418 sort_lock=True)
420 # add collections
421 button_row_2 = layout.row()
422 prop = button_row_2.operator("view3d.add_collection", text="Add Collection",
423 icon='COLLECTION_NEW')
424 prop.child = False
426 prop = button_row_2.operator("view3d.add_collection", text="Add SubCollection",
427 icon='COLLECTION_NEW')
428 prop.child = True
431 button_row_3 = layout.row()
433 # phantom mode
434 phantom_mode = button_row_3.row(align=True)
435 toggle_text = "Disable " if cm.in_phantom_mode else "Enable "
436 phantom_mode.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode")
437 phantom_mode.operator("view3d.apply_phantom_mode", text="", icon='CHECKMARK')
439 if cm.in_phantom_mode:
440 view.enabled = False
442 if prefs.enable_qcd:
443 renum_sec.enabled = False
445 undo_sec.enabled = False
446 specials_menu.enabled = False
448 c_icon.enabled = False
449 row_setcol.enabled = False
450 button_row_2.enabled = False
453 def execute(self, context):
454 wm = context.window_manager
456 update_property_group(context)
458 cm = context.scene.collection_manager
459 view_layer = context.view_layer
461 self.view_layer = view_layer.name
463 # make sure list index is valid
464 if cm.cm_list_index >= len(cm.cm_list_collection):
465 cm.cm_list_index = -1
467 # check if history/buffer/phantom state still correct
468 check_state(context, cm_popup=True, phantom_mode=True)
470 # handle window sizing
471 max_width = 960
472 min_width = 456
473 row_indent_width = 15
474 width_step = 21
475 qcd_width = 30
476 scrollbar_width = 21
478 width = min_width + row_indent_width + (width_step * internals.max_lvl)
480 if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
481 width += qcd_width
483 if len(internals.layer_collections) > 14:
484 width += scrollbar_width
486 if width > max_width:
487 width = max_width
489 return wm.invoke_popup(self, width=width)
491 def __del__(self):
492 if not self.window_open:
493 # prevent destructor execution when changing templates
494 return
496 internals.collection_state.clear()
497 internals.collection_state.update(generate_state())
500 class CM_UL_items(UIList):
501 filtering = False
502 last_filter_value = ""
504 selected_objects = set()
505 active_object = None
507 visible_items = []
508 new_collections = []
510 filter_name: StringProperty(
511 name="Filter By Name",
512 default="",
513 description="Filter collections by name",
514 update=lambda self, context:
515 CM_UL_items.new_collections.clear(),
518 use_filter_invert: BoolProperty(
519 name="Invert",
520 default=False,
521 description="Invert filtering (show hidden items, and vice-versa)",
524 filter_by_selected: BoolProperty(
525 name="Filter By Selected",
526 default=False,
527 description="Filter collections to only show the ones that contain the selected objects",
528 update=lambda self, context:
529 CM_UL_items.new_collections.clear(),
531 filter_by_qcd: BoolProperty(
532 name="Filter By QCD",
533 default=False,
534 description="Filter collections to only show the ones that are QCD slots",
535 update=lambda self, context:
536 CM_UL_items.new_collections.clear(),
539 def draw_item(self, context, layout, data, item, icon, active_data,active_propname, index):
540 self.use_filter_show = True
542 cm = context.scene.collection_manager
543 prefs = context.preferences.addons[__package__].preferences
544 view_layer = context.view_layer
545 laycol = internals.layer_collections[item.name]
546 collection = laycol["ptr"].collection
547 selected_objects = CM_UL_items.selected_objects
548 active_object = CM_UL_items.active_object
550 column = layout.column(align=True)
552 main_row = column.row()
554 s1 = main_row.row(align=True)
555 s1.alignment = 'LEFT'
557 s2 = main_row.row(align=True)
558 s2.alignment = 'RIGHT'
560 row = s1
562 # allow room to select the row from the beginning
563 row.separator()
565 # indent child items
566 if laycol["lvl"] > 0:
567 for _ in range(laycol["lvl"]):
568 row.label(icon='BLANK1')
570 # add expander if collection has children to make UIList act like tree view
571 if laycol["has_children"]:
572 if laycol["expanded"]:
573 highlight = True if internals.expand_history["target"] == item.name else False
575 prop = row.operator("view3d.expand_sublevel", text="",
576 icon='DISCLOSURE_TRI_DOWN',
577 emboss=highlight, depress=highlight)
578 prop.expand = False
579 prop.name = item.name
580 prop.index = index
582 else:
583 highlight = True if internals.expand_history["target"] == item.name else False
585 prop = row.operator("view3d.expand_sublevel", text="",
586 icon='DISCLOSURE_TRI_RIGHT',
587 emboss=highlight, depress=highlight)
588 prop.expand = True
589 prop.name = item.name
590 prop.index = index
592 else:
593 row.label(icon='BLANK1')
596 # collection icon
597 c_icon = row.row()
598 highlight = False
599 if (context.view_layer.active_layer_collection == laycol["ptr"]):
600 highlight = True
602 prop = c_icon.operator("view3d.set_active_collection", text='', icon='GROUP',
603 emboss=highlight, depress=highlight)
605 prop.is_master_collection = False
606 prop.collection_name = item.name
608 if prefs.enable_qcd:
609 QCD = row.row()
610 QCD.scale_x = 0.4
611 QCD.prop(item, "qcd_slot_idx", text="")
613 # collection name
614 c_name = row.row(align=True)
616 #if rename[0] and index == cm.cm_list_index:
617 #c_name.activate_init = True
618 #rename[0] = False
620 c_name.prop(item, "name", text="", expand=True)
622 # set selection
623 setsel = c_name.row(align=True)
624 icon = 'DOT'
625 some_selected = False
627 if not collection.objects:
628 icon = 'BLANK1'
629 setsel.active = False
631 if any((laycol["ptr"].exclude,
632 collection.hide_select,
633 collection.hide_viewport,
634 laycol["ptr"].hide_viewport,)):
635 # objects cannot be selected
636 setsel.active = False
638 else:
639 all_selected = None
640 all_unreachable = None
642 for obj in collection.objects:
643 if not obj.visible_get() or obj.hide_select:
644 if all_unreachable != False:
645 all_unreachable = True
647 else:
648 all_unreachable = False
650 if obj.select_get() == False:
651 # some objects remain unselected
652 icon = 'KEYFRAME'
653 all_selected = False
655 else:
656 some_selected = True
658 if all_selected == False:
659 break
661 all_selected = True
664 if all_selected:
665 # all objects are selected
666 icon = 'KEYFRAME_HLT'
668 if all_unreachable:
669 if collection.objects:
670 icon = 'DOT'
672 setsel.active = False
675 prop = setsel.operator("view3d.select_collection_objects",
676 text="",
677 icon=icon,
678 depress=some_selected
680 prop.is_master_collection = False
681 prop.collection_name = item.name
683 # used as a separator (actual separator not wide enough)
684 row.label()
686 row = s2 if cm.align_local_ops else s1
689 add_vertical_separator_line(row)
692 # add send_objects_to_collection op
693 set_obj_col = row.row()
694 set_obj_col.operator_context = 'INVOKE_DEFAULT'
696 icon = 'IMPORT'
698 if collection.objects:
699 icon = 'MESH_CUBE'
701 if selected_objects:
702 if active_object and active_object.name in collection.objects:
703 icon = 'SNAP_VOLUME'
705 elif not selected_objects.isdisjoint(collection.objects):
706 icon = 'STICKY_UVS_LOC'
708 else:
709 set_obj_col.enabled = False
712 prop = set_obj_col.operator("view3d.send_objects_to_collection", text="",
713 icon=icon, emboss=False)
714 prop.is_master_collection = False
715 prop.collection_name = item.name
717 add_vertical_separator_line(row)
720 if cm.show_exclude:
721 exclude_history_base = internals.rto_history["exclude"].get(view_layer.name, {})
722 exclude_target = exclude_history_base.get("target", "")
723 exclude_history = exclude_history_base.get("history", [])
725 highlight = bool(exclude_history and exclude_target == item.name)
726 icon = 'CHECKBOX_DEHLT' if laycol["ptr"].exclude else 'CHECKBOX_HLT'
728 prop = row.operator("view3d.exclude_collection", text="", icon=icon,
729 emboss=highlight, depress=highlight)
730 prop.name = item.name
732 if cm.show_selectable:
733 select_history_base = internals.rto_history["select"].get(view_layer.name, {})
734 select_target = select_history_base.get("target", "")
735 select_history = select_history_base.get("history", [])
737 highlight = bool(select_history and select_target == item.name)
738 icon = ('RESTRICT_SELECT_ON' if laycol["ptr"].collection.hide_select else
739 'RESTRICT_SELECT_OFF')
741 prop = row.operator("view3d.restrict_select_collection", text="", icon=icon,
742 emboss=highlight, depress=highlight)
743 prop.name = item.name
745 if cm.show_hide_viewport:
746 hide_history_base = internals.rto_history["hide"].get(view_layer.name, {})
747 hide_target = hide_history_base.get("target", "")
748 hide_history = hide_history_base.get("history", [])
750 highlight = bool(hide_history and hide_target == item.name)
751 icon = 'HIDE_ON' if laycol["ptr"].hide_viewport else 'HIDE_OFF'
753 prop = row.operator("view3d.hide_collection", text="", icon=icon,
754 emboss=highlight, depress=highlight)
755 prop.name = item.name
757 if cm.show_disable_viewport:
758 disable_history_base = internals.rto_history["disable"].get(view_layer.name, {})
759 disable_target = disable_history_base.get("target", "")
760 disable_history = disable_history_base.get("history", [])
762 highlight = bool(disable_history and disable_target == item.name)
763 icon = ('RESTRICT_VIEW_ON' if laycol["ptr"].collection.hide_viewport else
764 'RESTRICT_VIEW_OFF')
766 prop = row.operator("view3d.disable_viewport_collection", text="", icon=icon,
767 emboss=highlight, depress=highlight)
768 prop.name = item.name
770 if cm.show_render:
771 render_history_base = internals.rto_history["render"].get(view_layer.name, {})
772 render_target = render_history_base.get("target", "")
773 render_history = render_history_base.get("history", [])
775 highlight = bool(render_history and render_target == item.name)
776 icon = ('RESTRICT_RENDER_ON' if laycol["ptr"].collection.hide_render else
777 'RESTRICT_RENDER_OFF')
779 prop = row.operator("view3d.disable_render_collection", text="", icon=icon,
780 emboss=highlight, depress=highlight)
781 prop.name = item.name
783 if cm.show_holdout:
784 holdout_history_base = internals.rto_history["holdout"].get(view_layer.name, {})
785 holdout_target = holdout_history_base.get("target", "")
786 holdout_history = holdout_history_base.get("history", [])
788 highlight = bool(holdout_history and holdout_target == item.name)
789 icon = ('HOLDOUT_ON' if laycol["ptr"].holdout else
790 'HOLDOUT_OFF')
792 prop = row.operator("view3d.holdout_collection", text="", icon=icon,
793 emboss=highlight, depress=highlight)
794 prop.name = item.name
796 if cm.show_indirect_only:
797 indirect_history_base = internals.rto_history["indirect"].get(view_layer.name, {})
798 indirect_target = indirect_history_base.get("target", "")
799 indirect_history = indirect_history_base.get("history", [])
801 highlight = bool(indirect_history and indirect_target == item.name)
802 icon = ('INDIRECT_ONLY_ON' if laycol["ptr"].indirect_only else
803 'INDIRECT_ONLY_OFF')
805 prop = row.operator("view3d.indirect_only_collection", text="", icon=icon,
806 emboss=highlight, depress=highlight)
807 prop.name = item.name
811 row = s2
813 row.separator()
814 row.separator()
816 rm_op = row.row()
817 prop = rm_op.operator("view3d.remove_collection", text="", icon='X', emboss=False)
818 prop.collection_name = item.name
821 if len(data.cm_list_collection) > index + 1:
822 line_separator = column.row(align=True)
823 line_separator.ui_units_y = 0.01
824 line_separator.scale_y = 0.1
825 line_separator.enabled = False
827 line_separator.separator()
828 line_separator.label(icon='BLANK1')
830 for _ in range(laycol["lvl"] + 1):
831 line_separator.label(icon='BLANK1')
833 line_separator.prop(cm, "ui_separator")
835 if cm.in_phantom_mode:
836 c_icon.enabled = False
837 c_name.enabled = False
838 set_obj_col.enabled = False
839 rm_op.enabled = False
841 if prefs.enable_qcd:
842 QCD.enabled = False
845 def draw_filter(self, context, layout):
846 row = layout.row()
848 subrow = row.row(align=True)
849 subrow.prop(self, "filter_name", text="")
850 subrow.prop(self, "use_filter_invert", text="", icon='ARROW_LEFTRIGHT')
852 subrow = row.row(align=True)
853 subrow.prop(self, "filter_by_selected", text="", icon='STICKY_UVS_LOC')
855 if context.preferences.addons[__package__].preferences.enable_qcd:
856 subrow.prop(self, "filter_by_qcd", text="", icon='EVENT_Q')
858 def filter_items(self, context, data, propname):
859 CM_UL_items.filtering = False
861 flt_flags = []
862 flt_neworder = []
863 list_items = getattr(data, propname)
866 if self.filter_name:
867 CM_UL_items.filtering = True
869 new_flt_flags = filter_items_by_name_custom(self.filter_name, self.bitflag_filter_item, list_items)
871 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
874 if self.filter_by_selected:
875 CM_UL_items.filtering = True
876 new_flt_flags = [0] * len(list_items)
878 for idx, item in enumerate(list_items):
879 collection = internals.layer_collections[item.name]["ptr"].collection
881 # check if any of the selected objects are in the collection
882 if not set(context.selected_objects).isdisjoint(collection.objects):
883 new_flt_flags[idx] = self.bitflag_filter_item
885 # add in any recently created collections
886 if item.name in CM_UL_items.new_collections:
887 new_flt_flags[idx] = self.bitflag_filter_item
889 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
892 if self.filter_by_qcd:
893 CM_UL_items.filtering = True
894 new_flt_flags = [0] * len(list_items)
896 for idx, item in enumerate(list_items):
897 if item.qcd_slot_idx:
898 new_flt_flags[idx] = self.bitflag_filter_item
900 # add in any recently created collections
901 if item.name in CM_UL_items.new_collections:
902 new_flt_flags[idx] = self.bitflag_filter_item
904 flt_flags = merge_flt_flags(flt_flags, new_flt_flags)
907 if not CM_UL_items.filtering: # display as treeview
908 CM_UL_items.new_collections.clear()
909 flt_flags = [0] * len(list_items)
911 for idx, item in enumerate(list_items):
912 if internals.layer_collections[item.name]["visible"]:
913 flt_flags[idx] = self.bitflag_filter_item
916 if self.use_filter_invert:
917 CM_UL_items.filtering = True # invert can act as pseudo filtering
918 for idx, flag in enumerate(flt_flags):
919 flt_flags[idx] = 0 if flag else self.bitflag_filter_item
922 # update visible items list
923 CM_UL_items.visible_items.clear()
924 CM_UL_items.visible_items.extend(flt_flags)
926 return flt_flags, flt_neworder
930 def invoke(self, context, event):
931 pass
934 class CMDisplayOptionsPanel(Panel):
935 bl_label = "Display Options"
936 bl_idname = "COLLECTIONMANAGER_PT_display_options"
938 # set space type to VIEW_3D and region type to HEADER
939 # because we only need it in a popover in the 3D View
940 # and don't want it always present in the UI/N-Panel
941 bl_space_type = 'VIEW_3D'
942 bl_region_type = 'HEADER'
944 def draw(self, context):
945 cm = context.scene.collection_manager
947 layout = self.layout
949 panel_header = layout.row()
950 panel_header.alignment = 'CENTER'
951 panel_header.label(text="Display Options")
953 layout.separator()
955 section_header = layout.row()
956 section_header.alignment = 'LEFT'
957 section_header.label(text="Restriction Toggles")
959 row = layout.row()
960 row.prop(cm, "show_exclude", icon='CHECKBOX_HLT', icon_only=True)
961 row.prop(cm, "show_selectable", icon='RESTRICT_SELECT_OFF', icon_only=True)
962 row.prop(cm, "show_hide_viewport", icon='HIDE_OFF', icon_only=True)
963 row.prop(cm, "show_disable_viewport", icon='RESTRICT_VIEW_OFF', icon_only=True)
964 row.prop(cm, "show_render", icon='RESTRICT_RENDER_OFF', icon_only=True)
965 row.prop(cm, "show_holdout", icon='HOLDOUT_ON', icon_only=True)
966 row.prop(cm, "show_indirect_only", icon='INDIRECT_ONLY_ON', icon_only=True)
968 layout.separator()
970 section_header = layout.row()
971 section_header.label(text="Layout")
973 row = layout.row()
974 row.prop(cm, "align_local_ops")
977 class SpecialsMenu(Menu):
978 bl_label = "Specials"
979 bl_idname = "VIEW3D_MT_CM_specials_menu"
981 def draw(self, context):
982 layout = self.layout
984 prop = layout.operator("view3d.remove_empty_collections")
985 prop.without_objects = False
987 prop = layout.operator("view3d.remove_empty_collections",
988 text="Purge All Collections Without Objects")
989 prop.without_objects = True
991 layout.separator()
993 layout.operator("view3d.select_all_cumulative_objects")
996 class EnableAllQCDSlotsMenu(Menu):
997 bl_label = "Global QCD Slot Actions"
998 bl_idname = "VIEW3D_MT_CM_qcd_enable_all_menu"
1000 def draw(self, context):
1001 layout = self.layout
1003 layout.operator("view3d.create_all_qcd_slots")
1005 layout.separator()
1007 layout.operator("view3d.enable_all_qcd_slots")
1008 layout.operator("view3d.enable_all_qcd_slots_isolated")
1010 layout.separator()
1012 layout.operator("view3d.isolate_selected_objects_collections")
1013 if context.mode == 'OBJECT':
1014 layout.operator("view3d.disable_selected_objects_collections")
1016 layout.separator()
1018 layout.operator("view3d.disable_all_non_qcd_slots")
1019 layout.operator("view3d.disable_all_collections")
1021 if context.mode == 'OBJECT':
1022 layout.separator()
1023 layout.operator("view3d.select_all_qcd_objects")
1025 layout.separator()
1027 layout.operator("view3d.discard_qcd_history")
1030 def view3d_header_qcd_slots(self, context):
1031 update_collection_tree(context)
1033 view_layer = context.view_layer
1034 layout = self.layout
1035 idx = 1
1037 check_state(context, qcd=True)
1040 main_row = layout.row(align=True)
1041 current_qcd_history = internals.qcd_history.get(context.view_layer.name, [])
1043 main_row.operator_menu_hold("view3d.enable_all_qcd_slots_meta",
1044 text="",
1045 icon='HIDE_OFF',
1046 depress=bool(current_qcd_history),
1047 menu="VIEW3D_MT_CM_qcd_enable_all_menu")
1050 split = main_row.split()
1051 col = split.column(align=True)
1052 row = col.row(align=True)
1053 row.scale_y = 0.5
1055 selected_objects = get_move_selection()
1056 active_object = get_move_active()
1058 for x in range(20):
1059 qcd_slot_name = internals.qcd_slots.get_name(str(x+1))
1061 if qcd_slot_name:
1062 qcd_laycol = internals.layer_collections[qcd_slot_name]["ptr"]
1063 collection_objects = qcd_laycol.collection.objects
1065 icon_value = 0
1067 # if the active object is in the current collection use a custom icon
1068 if (active_object and active_object in selected_objects and
1069 active_object.name in collection_objects):
1070 icon = 'LAYER_ACTIVE'
1072 # if there are selected objects use LAYER_ACTIVE
1073 elif not selected_objects.isdisjoint(collection_objects):
1074 icon = 'LAYER_USED'
1076 # If there are objects use LAYER_USED
1077 elif collection_objects:
1078 icon = 'NONE'
1079 active_icon = get_active_icon(context, qcd_laycol)
1080 icon_value = active_icon.icon_id
1082 else:
1083 icon = 'BLANK1'
1086 prop = row.operator("view3d.view_move_qcd_slot", text="", icon=icon,
1087 icon_value=icon_value, depress=not qcd_laycol.exclude)
1088 prop.slot = str(x+1)
1090 else:
1091 prop = row.operator("view3d.unassigned_qcd_slot", text="", icon='X', emboss=False)
1092 prop.slot = str(x+1)
1095 if idx%5==0:
1096 row.separator()
1098 if idx == 10:
1099 row = col.row(align=True)
1100 row.scale_y = 0.5
1102 idx += 1
1105 def view_layer_update(self, context):
1106 if context.view_layer.name != CollectionManager.last_view_layer:
1107 bpy.app.timers.register(update_qcd_header)
1108 CollectionManager.last_view_layer = context.view_layer.name
1111 def get_active_icon(context, qcd_laycol):
1112 global last_icon_theme_text
1113 global last_icon_theme_text_sel
1115 tool_theme = context.preferences.themes[0].user_interface.wcol_tool
1116 pcoll = preview_collections["icons"]
1118 if qcd_laycol.exclude:
1119 theme_color = tool_theme.text
1120 last_theme_color = last_icon_theme_text
1121 icon = pcoll["active_icon_text"]
1123 else:
1124 theme_color = tool_theme.text_sel
1125 last_theme_color = last_icon_theme_text_sel
1126 icon = pcoll["active_icon_text_sel"]
1128 if last_theme_color == None or theme_color.hsv != last_theme_color:
1129 update_icon(pcoll["active_icon_base"], icon, theme_color)
1131 if qcd_laycol.exclude:
1132 last_icon_theme_text = theme_color.hsv
1134 else:
1135 last_icon_theme_text_sel = theme_color.hsv
1137 return icon
1140 def update_icon(base, icon, theme_color):
1141 icon.icon_pixels = base.icon_pixels
1142 colored_icon = []
1144 for offset in range(len(icon.icon_pixels)):
1145 idx = offset * 4
1147 r = icon.icon_pixels_float[idx]
1148 g = icon.icon_pixels_float[idx+1]
1149 b = icon.icon_pixels_float[idx+2]
1150 a = icon.icon_pixels_float[idx+3]
1152 # add back some brightness and opacity blender takes away from the custom icon
1153 r = min(r+r*0.2,1)
1154 g = min(g+g*0.2,1)
1155 b = min(b+b*0.2,1)
1156 a = min(a+a*0.2,1)
1158 # make the icon follow the theme color (assuming the icon is white)
1159 r *= theme_color.r
1160 g *= theme_color.g
1161 b *= theme_color.b
1163 colored_icon.append(r)
1164 colored_icon.append(g)
1165 colored_icon.append(b)
1166 colored_icon.append(a)
1168 icon.icon_pixels_float = colored_icon
1171 def filter_items_by_name_custom(pattern, bitflag, items, propname="name", flags=None, reverse=False):
1173 Set FILTER_ITEM for items which name matches filter_name one (case-insensitive).
1174 pattern is the filtering pattern.
1175 propname is the name of the string property to use for filtering.
1176 flags must be a list of integers the same length as items, or None!
1177 return a list of flags (based on given flags if not None),
1178 or an empty list if no flags were given and no filtering has been done.
1180 import fnmatch
1182 if not pattern or not items: # Empty pattern or list = no filtering!
1183 return flags or []
1185 if flags is None:
1186 flags = [0] * len(items)
1188 # Make pattern case-insensitive
1189 pattern = pattern.lower()
1191 # Implicitly add heading/trailing wildcards.
1192 pattern = "*" + pattern + "*"
1194 for i, item in enumerate(items):
1195 name = getattr(item, propname, None)
1197 # Make name case-insensitive
1198 name = name.lower()
1200 # This is similar to a logical xor
1201 if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse):
1202 flags[i] |= bitflag
1204 # add in any recently created collections
1205 if item.name in CM_UL_items.new_collections:
1206 flags[i] |= bitflag
1208 return flags
1210 def merge_flt_flags(l1, l2):
1211 for idx, _ in enumerate(l1):
1212 l1[idx] &= l2.pop(0)
1214 return l1 + l2