1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Copyright 2011, Ryan Inch
7 from bpy
.types
import (
14 from bpy
.props
import (
20 from . import internals
23 from .internals
import (
24 update_collection_tree
,
25 update_property_group
,
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"
48 master_collection
: StringProperty(
49 default
='Scene Collection',
51 description
="Scene Collection"
55 self
.window_open
= True
57 def draw(self
, context
):
58 cls
= CollectionManager
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
:
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
="")
83 window
= context
.window
88 new
="scene.view_layer_add",
89 unlink
="scene.view_layer_remove")
91 layout
.row().separator()
92 layout
.row().separator()
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"
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
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')
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)
143 c_icon
= master_collection_row
.row()
145 if (context
.view_layer
.active_layer_collection
==
146 context
.view_layer
.layer_collection
):
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()
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
163 setsel
= name_row
.row(align
=True)
165 some_selected
= False
167 if not collection
.objects
:
169 setsel
.active
= False
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
181 all_unreachable
= False
183 if obj
.select_get() == False:
184 # some objects remain unselected
191 if all_selected
== False:
198 # all objects are selected
199 icon
= 'KEYFRAME_HLT'
202 if collection
.objects
:
205 setsel
.active
= False
207 prop
= setsel
.operator("view3d.select_collection_objects",
210 depress
=some_selected
,
212 prop
.is_master_collection
= True
213 prop
.collection_name
= 'Scene Collection'
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()
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
238 if collection
.objects
:
242 if active_object
and active_object
.name
in collection
.objects
:
245 elif not selected_objects
.isdisjoint(collection
.objects
):
246 icon
= 'STICKY_UVS_LOC'
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",
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",
276 copy_icon
= 'COPYDOWN'
277 swap_icon
= 'ARROW_LEFTRIGHT'
278 copy_swap_icon
= 'SELECT_INTERSECT'
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":
290 if internals
.swap_buffer
["A"]["RTO"] == "exclude":
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":
309 if internals
.swap_buffer
["A"]["RTO"] == "select":
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
322 buffers
= [False, False]
324 if internals
.copy_buffer
["RTO"] == "hide":
328 if internals
.swap_buffer
["A"]["RTO"] == "hide":
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":
347 if internals
.swap_buffer
["A"]["RTO"] == "disable":
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
)
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":
366 if internals
.swap_buffer
["A"]["RTO"] == "render":
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
)
376 holdout_all_history
= internals
.rto_history
["holdout_all"].get(view_layer
.name
, [])
377 depress
= True if len(holdout_all_history
) else False
379 buffers
= [False, False]
381 if internals
.copy_buffer
["RTO"] == "holdout":
385 if internals
.swap_buffer
["A"]["RTO"] == "holdout":
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":
404 if internals
.swap_buffer
["A"]["RTO"] == "indirect":
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
)
414 layout
.row().template_list("CM_UL_items", "",
415 cm
, "cm_list_collection",
421 button_row_2
= layout
.row()
422 prop
= button_row_2
.operator("view3d.add_collection", text
="Add Collection",
423 icon
='COLLECTION_NEW')
426 prop
= button_row_2
.operator("view3d.add_collection", text
="Add SubCollection",
427 icon
='COLLECTION_NEW')
431 button_row_3
= layout
.row()
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
:
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
473 row_indent_width
= 15
478 width
= min_width
+ row_indent_width
+ (width_step
* internals
.max_lvl
)
480 if bpy
.context
.preferences
.addons
[__package__
].preferences
.enable_qcd
:
483 if len(internals
.layer_collections
) > 14:
484 width
+= scrollbar_width
486 if width
> max_width
:
489 return wm
.invoke_popup(self
, width
=width
)
492 if not self
.window_open
:
493 # prevent destructor execution when changing templates
496 internals
.collection_state
.clear()
497 internals
.collection_state
.update(generate_state())
500 class CM_UL_items(UIList
):
502 last_filter_value
= ""
504 selected_objects
= set()
510 filter_name
: StringProperty(
511 name
="Filter By Name",
513 description
="Filter collections by name",
514 update
=lambda self
, context
:
515 CM_UL_items
.new_collections
.clear(),
518 use_filter_invert
: BoolProperty(
521 description
="Invert filtering (show hidden items, and vice-versa)",
524 filter_by_selected
: BoolProperty(
525 name
="Filter By Selected",
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",
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'
562 # allow room to select the row from the beginning
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
)
579 prop
.name
= item
.name
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
)
589 prop
.name
= item
.name
593 row
.label(icon
='BLANK1')
599 if (context
.view_layer
.active_layer_collection
== laycol
["ptr"]):
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
611 QCD
.prop(item
, "qcd_slot_idx", text
="")
614 c_name
= row
.row(align
=True)
616 #if rename[0] and index == cm.cm_list_index:
617 #c_name.activate_init = True
620 c_name
.prop(item
, "name", text
="", expand
=True)
623 setsel
= c_name
.row(align
=True)
625 some_selected
= False
627 if not collection
.objects
:
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
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
648 all_unreachable
= False
650 if obj
.select_get() == False:
651 # some objects remain unselected
658 if all_selected
== False:
665 # all objects are selected
666 icon
= 'KEYFRAME_HLT'
669 if collection
.objects
:
672 setsel
.active
= False
675 prop
= setsel
.operator("view3d.select_collection_objects",
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)
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'
698 if collection
.objects
:
702 if active_object
and active_object
.name
in collection
.objects
:
705 elif not selected_objects
.isdisjoint(collection
.objects
):
706 icon
= 'STICKY_UVS_LOC'
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
)
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
766 prop
= row
.operator("view3d.disable_viewport_collection", text
="", icon
=icon
,
767 emboss
=highlight
, depress
=highlight
)
768 prop
.name
= item
.name
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
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
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
805 prop
= row
.operator("view3d.indirect_only_collection", text
="", icon
=icon
,
806 emboss
=highlight
, depress
=highlight
)
807 prop
.name
= item
.name
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
845 def draw_filter(self
, context
, layout
):
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
863 list_items
= getattr(data
, propname
)
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
):
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
949 panel_header
= layout
.row()
950 panel_header
.alignment
= 'CENTER'
951 panel_header
.label(text
="Display Options")
955 section_header
= layout
.row()
956 section_header
.alignment
= 'LEFT'
957 section_header
.label(text
="Restriction Toggles")
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)
970 section_header
= layout
.row()
971 section_header
.label(text
="Layout")
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
):
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
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")
1007 layout
.operator("view3d.enable_all_qcd_slots")
1008 layout
.operator("view3d.enable_all_qcd_slots_isolated")
1012 layout
.operator("view3d.isolate_selected_objects_collections")
1013 if context
.mode
== 'OBJECT':
1014 layout
.operator("view3d.disable_selected_objects_collections")
1018 layout
.operator("view3d.disable_all_non_qcd_slots")
1019 layout
.operator("view3d.disable_all_collections")
1021 if context
.mode
== 'OBJECT':
1023 layout
.operator("view3d.select_all_qcd_objects")
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
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",
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)
1055 selected_objects
= get_move_selection()
1056 active_object
= get_move_active()
1059 qcd_slot_name
= internals
.qcd_slots
.get_name(str(x
+1))
1062 qcd_laycol
= internals
.layer_collections
[qcd_slot_name
]["ptr"]
1063 collection_objects
= qcd_laycol
.collection
.objects
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
):
1076 # If there are objects use LAYER_USED
1077 elif collection_objects
:
1079 active_icon
= get_active_icon(context
, qcd_laycol
)
1080 icon_value
= active_icon
.icon_id
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)
1091 prop
= row
.operator("view3d.unassigned_qcd_slot", text
="", icon
='X', emboss
=False)
1092 prop
.slot
= str(x
+1)
1099 row
= col
.row(align
=True)
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"]
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
1135 last_icon_theme_text_sel
= theme_color
.hsv
1140 def update_icon(base
, icon
, theme_color
):
1141 icon
.icon_pixels
= base
.icon_pixels
1144 for offset
in range(len(icon
.icon_pixels
)):
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
1158 # make the icon follow the theme color (assuming the icon is white)
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.
1182 if not pattern
or not items
: # Empty pattern or list = no filtering!
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
1200 # This is similar to a logical xor
1201 if bool(name
and fnmatch
.fnmatch(name
, pattern
)) is not bool(reverse
):
1204 # add in any recently created collections
1205 if item
.name
in CM_UL_items
.new_collections
:
1210 def merge_flt_flags(l1
, l2
):
1211 for idx
, _
in enumerate(l1
):
1212 l1
[idx
] &= l2
.pop(0)