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
23 from bpy
.types
import (
30 from bpy
.props
import (
36 from . import internals
39 from .internals
import (
40 update_collection_tree
,
41 update_property_group
,
47 add_vertical_separator_line
,
50 preview_collections
= {}
51 last_icon_theme_text
= None
52 last_icon_theme_text_sel
= None
55 class CollectionManager(Operator
):
56 '''Manage and control collections, with advanced features, in a popup UI'''
57 bl_label
= "Collection Manager"
58 bl_idname
= "view3d.collection_manager"
64 master_collection
: StringProperty(
65 default
='Scene Collection',
67 description
="Scene Collection"
71 self
.window_open
= True
73 def draw(self
, context
):
74 cls
= CollectionManager
76 cm
= context
.scene
.collection_manager
77 prefs
= context
.preferences
.addons
[__package__
].preferences
78 view_layer
= context
.view_layer
79 collection
= context
.view_layer
.layer_collection
.collection
81 if view_layer
.name
!= cls
.last_view_layer
:
83 bpy
.app
.timers
.register(update_qcd_header
)
85 update_collection_tree(context
)
86 cls
.last_view_layer
= view_layer
.name
88 # title and view layer
89 title_row
= layout
.split(factor
=0.5)
90 main
= title_row
.row()
91 view
= title_row
.row(align
=True)
92 view
.alignment
= 'RIGHT'
94 main
.label(text
="Collection Manager")
96 view
.prop(view_layer
, "use", text
="")
99 window
= context
.window
101 view
.template_search(
102 window
, "view_layer",
103 scene
, "view_layers",
104 new
="scene.view_layer_add",
105 unlink
="scene.view_layer_remove")
107 layout
.row().separator()
108 layout
.row().separator()
111 button_row_1
= layout
.row()
113 op_sec
= button_row_1
.row()
114 op_sec
.alignment
= 'LEFT'
116 collapse_sec
= op_sec
.row()
117 collapse_sec
.alignment
= 'LEFT'
118 collapse_sec
.enabled
= False
120 if len(internals
.expanded
) > 0:
121 text
= "Collapse All Items"
123 text
= "Expand All Items"
125 collapse_sec
.operator("view3d.expand_all_items", text
=text
)
127 for laycol
in internals
.collection_tree
:
128 if laycol
["has_children"]:
129 collapse_sec
.enabled
= True
133 renum_sec
= op_sec
.row()
134 renum_sec
.alignment
= 'LEFT'
135 renum_sec
.operator("view3d.renumerate_qcd_slots")
137 undo_sec
= op_sec
.row(align
=True)
138 undo_sec
.alignment
= 'LEFT'
139 undo_sec
.operator("view3d.undo_wrapper", text
="", icon
='LOOP_BACK')
140 undo_sec
.operator("view3d.redo_wrapper", text
="", icon
='LOOP_FORWARDS')
143 right_sec
= button_row_1
.row()
144 right_sec
.alignment
= 'RIGHT'
146 specials_menu
= right_sec
.row()
147 specials_menu
.alignment
= 'RIGHT'
148 specials_menu
.menu("VIEW3D_MT_CM_specials_menu")
150 display_options
= right_sec
.row()
151 display_options
.alignment
= 'RIGHT'
152 display_options
.popover(panel
="COLLECTIONMANAGER_PT_display_options",
153 text
="", icon
='FILTER')
155 mc_box
= layout
.box()
156 master_collection_row
= mc_box
.row(align
=True)
159 c_icon
= master_collection_row
.row()
161 if (context
.view_layer
.active_layer_collection
==
162 context
.view_layer
.layer_collection
):
165 prop
= c_icon
.operator("view3d.set_active_collection",
166 text
='', icon
='GROUP', depress
=highlight
)
167 prop
.is_master_collection
= True
168 prop
.collection_name
= 'Scene Collection'
170 master_collection_row
.separator()
173 name_row
= master_collection_row
.row(align
=True)
174 name_field
= name_row
.row(align
=True)
175 name_field
.prop(self
, "master_collection", text
='')
176 name_field
.enabled
= False
179 setsel
= name_row
.row(align
=True)
181 some_selected
= False
183 if not collection
.objects
:
185 setsel
.active
= False
189 all_unreachable
= None
191 for obj
in collection
.objects
:
192 if not obj
.visible_get() or obj
.hide_select
:
193 if all_unreachable
!= False:
194 all_unreachable
= True
197 all_unreachable
= False
199 if obj
.select_get() == False:
200 # some objects remain unselected
207 if all_selected
== False:
214 # all objects are selected
215 icon
= 'KEYFRAME_HLT'
218 if collection
.objects
:
221 setsel
.active
= False
223 prop
= setsel
.operator("view3d.select_collection_objects",
226 depress
=some_selected
,
228 prop
.is_master_collection
= True
229 prop
.collection_name
= 'Scene Collection'
233 global_rto_row
= master_collection_row
.row()
234 global_rto_row
.alignment
= 'RIGHT'
236 # used as a separator (actual separator not wide enough)
237 global_rto_row
.label()
241 row_setcol
= global_rto_row
.row()
242 row_setcol
.alignment
= 'LEFT'
243 row_setcol
.operator_context
= 'INVOKE_DEFAULT'
245 selected_objects
= get_move_selection()
246 active_object
= get_move_active()
247 CM_UL_items
.selected_objects
= selected_objects
248 CM_UL_items
.active_object
= active_object
250 collection
= context
.view_layer
.layer_collection
.collection
254 if collection
.objects
:
258 if active_object
and active_object
.name
in collection
.objects
:
261 elif not selected_objects
.isdisjoint(collection
.objects
):
262 icon
= 'STICKY_UVS_LOC'
265 row_setcol
.active
= False
268 # add vertical separator line
269 separator
= row_setcol
.row()
270 separator
.scale_x
= 0.2
271 separator
.enabled
= False
272 separator
.operator("view3d.cm_ui_separator_button",
278 prop
= row_setcol
.operator("view3d.send_objects_to_collection", text
="",
279 icon
=icon
, emboss
=False)
280 prop
.is_master_collection
= True
281 prop
.collection_name
= 'Scene Collection'
283 # add vertical separator line
284 separator
= row_setcol
.row()
285 separator
.scale_x
= 0.2
286 separator
.enabled
= False
287 separator
.operator("view3d.cm_ui_separator_button",
292 copy_icon
= 'COPYDOWN'
293 swap_icon
= 'ARROW_LEFTRIGHT'
294 copy_swap_icon
= 'SELECT_INTERSECT'
297 exclude_all_history
= internals
.rto_history
["exclude_all"].get(view_layer
.name
, [])
298 depress
= True if len(exclude_all_history
) else False
299 icon
= 'CHECKBOX_HLT'
300 buffers
= [False, False]
302 if internals
.copy_buffer
["RTO"] == "exclude":
306 if internals
.swap_buffer
["A"]["RTO"] == "exclude":
310 if buffers
[0] and buffers
[1]:
311 icon
= copy_swap_icon
313 global_rto_row
.operator("view3d.un_exclude_all_collections", text
="", icon
=icon
, depress
=depress
)
315 if cm
.show_selectable
:
316 select_all_history
= internals
.rto_history
["select_all"].get(view_layer
.name
, [])
317 depress
= True if len(select_all_history
) else False
318 icon
= 'RESTRICT_SELECT_OFF'
319 buffers
= [False, False]
321 if internals
.copy_buffer
["RTO"] == "select":
325 if internals
.swap_buffer
["A"]["RTO"] == "select":
329 if buffers
[0] and buffers
[1]:
330 icon
= copy_swap_icon
332 global_rto_row
.operator("view3d.un_restrict_select_all_collections", text
="", icon
=icon
, depress
=depress
)
334 if cm
.show_hide_viewport
:
335 hide_all_history
= internals
.rto_history
["hide_all"].get(view_layer
.name
, [])
336 depress
= True if len(hide_all_history
) else False
338 buffers
= [False, False]
340 if internals
.copy_buffer
["RTO"] == "hide":
344 if internals
.swap_buffer
["A"]["RTO"] == "hide":
348 if buffers
[0] and buffers
[1]:
349 icon
= copy_swap_icon
351 global_rto_row
.operator("view3d.un_hide_all_collections", text
="", icon
=icon
, depress
=depress
)
353 if cm
.show_disable_viewport
:
354 disable_all_history
= internals
.rto_history
["disable_all"].get(view_layer
.name
, [])
355 depress
= True if len(disable_all_history
) else False
356 icon
= 'RESTRICT_VIEW_OFF'
357 buffers
= [False, False]
359 if internals
.copy_buffer
["RTO"] == "disable":
363 if internals
.swap_buffer
["A"]["RTO"] == "disable":
367 if buffers
[0] and buffers
[1]:
368 icon
= copy_swap_icon
370 global_rto_row
.operator("view3d.un_disable_viewport_all_collections", text
="", icon
=icon
, depress
=depress
)
373 render_all_history
= internals
.rto_history
["render_all"].get(view_layer
.name
, [])
374 depress
= True if len(render_all_history
) else False
375 icon
= 'RESTRICT_RENDER_OFF'
376 buffers
= [False, False]
378 if internals
.copy_buffer
["RTO"] == "render":
382 if internals
.swap_buffer
["A"]["RTO"] == "render":
386 if buffers
[0] and buffers
[1]:
387 icon
= copy_swap_icon
389 global_rto_row
.operator("view3d.un_disable_render_all_collections", text
="", icon
=icon
, depress
=depress
)
392 holdout_all_history
= internals
.rto_history
["holdout_all"].get(view_layer
.name
, [])
393 depress
= True if len(holdout_all_history
) else False
395 buffers
= [False, False]
397 if internals
.copy_buffer
["RTO"] == "holdout":
401 if internals
.swap_buffer
["A"]["RTO"] == "holdout":
405 if buffers
[0] and buffers
[1]:
406 icon
= copy_swap_icon
408 global_rto_row
.operator("view3d.un_holdout_all_collections", text
="", icon
=icon
, depress
=depress
)
410 if cm
.show_indirect_only
:
411 indirect_all_history
= internals
.rto_history
["indirect_all"].get(view_layer
.name
, [])
412 depress
= True if len(indirect_all_history
) else False
413 icon
= 'INDIRECT_ONLY_ON'
414 buffers
= [False, False]
416 if internals
.copy_buffer
["RTO"] == "indirect":
420 if internals
.swap_buffer
["A"]["RTO"] == "indirect":
424 if buffers
[0] and buffers
[1]:
425 icon
= copy_swap_icon
427 global_rto_row
.operator("view3d.un_indirect_only_all_collections", text
="", icon
=icon
, depress
=depress
)
430 layout
.row().template_list("CM_UL_items", "",
431 cm
, "cm_list_collection",
437 button_row_2
= layout
.row()
438 prop
= button_row_2
.operator("view3d.add_collection", text
="Add Collection",
439 icon
='COLLECTION_NEW')
442 prop
= button_row_2
.operator("view3d.add_collection", text
="Add SubCollection",
443 icon
='COLLECTION_NEW')
447 button_row_3
= layout
.row()
450 phantom_mode
= button_row_3
.row(align
=True)
451 toggle_text
= "Disable " if cm
.in_phantom_mode
else "Enable "
452 phantom_mode
.operator("view3d.toggle_phantom_mode", text
=toggle_text
+"Phantom Mode")
453 phantom_mode
.operator("view3d.apply_phantom_mode", text
="", icon
='CHECKMARK')
455 if cm
.in_phantom_mode
:
459 renum_sec
.enabled
= False
461 undo_sec
.enabled
= False
462 specials_menu
.enabled
= False
464 c_icon
.enabled
= False
465 row_setcol
.enabled
= False
466 button_row_2
.enabled
= False
469 def execute(self
, context
):
470 wm
= context
.window_manager
472 update_property_group(context
)
474 cm
= context
.scene
.collection_manager
475 view_layer
= context
.view_layer
477 self
.view_layer
= view_layer
.name
479 # make sure list index is valid
480 if cm
.cm_list_index
>= len(cm
.cm_list_collection
):
481 cm
.cm_list_index
= -1
483 # check if history/buffer/phantom state still correct
484 check_state(context
, cm_popup
=True, phantom_mode
=True)
486 # handle window sizing
489 row_indent_width
= 15
494 width
= min_width
+ row_indent_width
+ (width_step
* internals
.max_lvl
)
496 if bpy
.context
.preferences
.addons
[__package__
].preferences
.enable_qcd
:
499 if len(internals
.layer_collections
) > 14:
500 width
+= scrollbar_width
502 if width
> max_width
:
505 return wm
.invoke_popup(self
, width
=width
)
508 if not self
.window_open
:
509 # prevent destructor execution when changing templates
512 internals
.collection_state
.clear()
513 internals
.collection_state
.update(generate_state())
516 class CM_UL_items(UIList
):
518 last_filter_value
= ""
520 selected_objects
= set()
526 filter_name
: StringProperty(
527 name
="Filter By Name",
529 description
="Filter collections by name",
530 update
=lambda self
, context
:
531 CM_UL_items
.new_collections
.clear(),
534 use_filter_invert
: BoolProperty(
537 description
="Invert filtering (show hidden items, and vice-versa)",
540 filter_by_selected
: BoolProperty(
541 name
="Filter By Selected",
543 description
="Filter collections to only show the ones that contain the selected objects",
544 update
=lambda self
, context
:
545 CM_UL_items
.new_collections
.clear(),
547 filter_by_qcd
: BoolProperty(
548 name
="Filter By QCD",
550 description
="Filter collections to only show the ones that are QCD slots",
551 update
=lambda self
, context
:
552 CM_UL_items
.new_collections
.clear(),
555 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
,active_propname
, index
):
556 self
.use_filter_show
= True
558 cm
= context
.scene
.collection_manager
559 prefs
= context
.preferences
.addons
[__package__
].preferences
560 view_layer
= context
.view_layer
561 laycol
= internals
.layer_collections
[item
.name
]
562 collection
= laycol
["ptr"].collection
563 selected_objects
= CM_UL_items
.selected_objects
564 active_object
= CM_UL_items
.active_object
566 column
= layout
.column(align
=True)
568 main_row
= column
.row()
570 s1
= main_row
.row(align
=True)
571 s1
.alignment
= 'LEFT'
573 s2
= main_row
.row(align
=True)
574 s2
.alignment
= 'RIGHT'
578 # allow room to select the row from the beginning
582 if laycol
["lvl"] > 0:
583 for _
in range(laycol
["lvl"]):
584 row
.label(icon
='BLANK1')
586 # add expander if collection has children to make UIList act like tree view
587 if laycol
["has_children"]:
588 if laycol
["expanded"]:
589 highlight
= True if internals
.expand_history
["target"] == item
.name
else False
591 prop
= row
.operator("view3d.expand_sublevel", text
="",
592 icon
='DISCLOSURE_TRI_DOWN',
593 emboss
=highlight
, depress
=highlight
)
595 prop
.name
= item
.name
599 highlight
= True if internals
.expand_history
["target"] == item
.name
else False
601 prop
= row
.operator("view3d.expand_sublevel", text
="",
602 icon
='DISCLOSURE_TRI_RIGHT',
603 emboss
=highlight
, depress
=highlight
)
605 prop
.name
= item
.name
609 row
.label(icon
='BLANK1')
615 if (context
.view_layer
.active_layer_collection
== laycol
["ptr"]):
618 prop
= c_icon
.operator("view3d.set_active_collection", text
='', icon
='GROUP',
619 emboss
=highlight
, depress
=highlight
)
621 prop
.is_master_collection
= False
622 prop
.collection_name
= item
.name
627 QCD
.prop(item
, "qcd_slot_idx", text
="")
630 c_name
= row
.row(align
=True)
632 #if rename[0] and index == cm.cm_list_index:
633 #c_name.activate_init = True
636 c_name
.prop(item
, "name", text
="", expand
=True)
639 setsel
= c_name
.row(align
=True)
641 some_selected
= False
643 if not collection
.objects
:
645 setsel
.active
= False
647 if any((laycol
["ptr"].exclude
,
648 collection
.hide_select
,
649 collection
.hide_viewport
,
650 laycol
["ptr"].hide_viewport
,)):
651 # objects cannot be selected
652 setsel
.active
= False
656 all_unreachable
= None
658 for obj
in collection
.objects
:
659 if not obj
.visible_get() or obj
.hide_select
:
660 if all_unreachable
!= False:
661 all_unreachable
= True
664 all_unreachable
= False
666 if obj
.select_get() == False:
667 # some objects remain unselected
674 if all_selected
== False:
681 # all objects are selected
682 icon
= 'KEYFRAME_HLT'
685 if collection
.objects
:
688 setsel
.active
= False
691 prop
= setsel
.operator("view3d.select_collection_objects",
694 depress
=some_selected
696 prop
.is_master_collection
= False
697 prop
.collection_name
= item
.name
699 # used as a separator (actual separator not wide enough)
702 row
= s2
if cm
.align_local_ops
else s1
705 add_vertical_separator_line(row
)
708 # add send_objects_to_collection op
709 set_obj_col
= row
.row()
710 set_obj_col
.operator_context
= 'INVOKE_DEFAULT'
714 if collection
.objects
:
718 if active_object
and active_object
.name
in collection
.objects
:
721 elif not selected_objects
.isdisjoint(collection
.objects
):
722 icon
= 'STICKY_UVS_LOC'
725 set_obj_col
.enabled
= False
728 prop
= set_obj_col
.operator("view3d.send_objects_to_collection", text
="",
729 icon
=icon
, emboss
=False)
730 prop
.is_master_collection
= False
731 prop
.collection_name
= item
.name
733 add_vertical_separator_line(row
)
737 exclude_history_base
= internals
.rto_history
["exclude"].get(view_layer
.name
, {})
738 exclude_target
= exclude_history_base
.get("target", "")
739 exclude_history
= exclude_history_base
.get("history", [])
741 highlight
= bool(exclude_history
and exclude_target
== item
.name
)
742 icon
= 'CHECKBOX_DEHLT' if laycol
["ptr"].exclude
else 'CHECKBOX_HLT'
744 prop
= row
.operator("view3d.exclude_collection", text
="", icon
=icon
,
745 emboss
=highlight
, depress
=highlight
)
746 prop
.name
= item
.name
748 if cm
.show_selectable
:
749 select_history_base
= internals
.rto_history
["select"].get(view_layer
.name
, {})
750 select_target
= select_history_base
.get("target", "")
751 select_history
= select_history_base
.get("history", [])
753 highlight
= bool(select_history
and select_target
== item
.name
)
754 icon
= ('RESTRICT_SELECT_ON' if laycol
["ptr"].collection
.hide_select
else
755 'RESTRICT_SELECT_OFF')
757 prop
= row
.operator("view3d.restrict_select_collection", text
="", icon
=icon
,
758 emboss
=highlight
, depress
=highlight
)
759 prop
.name
= item
.name
761 if cm
.show_hide_viewport
:
762 hide_history_base
= internals
.rto_history
["hide"].get(view_layer
.name
, {})
763 hide_target
= hide_history_base
.get("target", "")
764 hide_history
= hide_history_base
.get("history", [])
766 highlight
= bool(hide_history
and hide_target
== item
.name
)
767 icon
= 'HIDE_ON' if laycol
["ptr"].hide_viewport
else 'HIDE_OFF'
769 prop
= row
.operator("view3d.hide_collection", text
="", icon
=icon
,
770 emboss
=highlight
, depress
=highlight
)
771 prop
.name
= item
.name
773 if cm
.show_disable_viewport
:
774 disable_history_base
= internals
.rto_history
["disable"].get(view_layer
.name
, {})
775 disable_target
= disable_history_base
.get("target", "")
776 disable_history
= disable_history_base
.get("history", [])
778 highlight
= bool(disable_history
and disable_target
== item
.name
)
779 icon
= ('RESTRICT_VIEW_ON' if laycol
["ptr"].collection
.hide_viewport
else
782 prop
= row
.operator("view3d.disable_viewport_collection", text
="", icon
=icon
,
783 emboss
=highlight
, depress
=highlight
)
784 prop
.name
= item
.name
787 render_history_base
= internals
.rto_history
["render"].get(view_layer
.name
, {})
788 render_target
= render_history_base
.get("target", "")
789 render_history
= render_history_base
.get("history", [])
791 highlight
= bool(render_history
and render_target
== item
.name
)
792 icon
= ('RESTRICT_RENDER_ON' if laycol
["ptr"].collection
.hide_render
else
793 'RESTRICT_RENDER_OFF')
795 prop
= row
.operator("view3d.disable_render_collection", text
="", icon
=icon
,
796 emboss
=highlight
, depress
=highlight
)
797 prop
.name
= item
.name
800 holdout_history_base
= internals
.rto_history
["holdout"].get(view_layer
.name
, {})
801 holdout_target
= holdout_history_base
.get("target", "")
802 holdout_history
= holdout_history_base
.get("history", [])
804 highlight
= bool(holdout_history
and holdout_target
== item
.name
)
805 icon
= ('HOLDOUT_ON' if laycol
["ptr"].holdout
else
808 prop
= row
.operator("view3d.holdout_collection", text
="", icon
=icon
,
809 emboss
=highlight
, depress
=highlight
)
810 prop
.name
= item
.name
812 if cm
.show_indirect_only
:
813 indirect_history_base
= internals
.rto_history
["indirect"].get(view_layer
.name
, {})
814 indirect_target
= indirect_history_base
.get("target", "")
815 indirect_history
= indirect_history_base
.get("history", [])
817 highlight
= bool(indirect_history
and indirect_target
== item
.name
)
818 icon
= ('INDIRECT_ONLY_ON' if laycol
["ptr"].indirect_only
else
821 prop
= row
.operator("view3d.indirect_only_collection", text
="", icon
=icon
,
822 emboss
=highlight
, depress
=highlight
)
823 prop
.name
= item
.name
833 prop
= rm_op
.operator("view3d.remove_collection", text
="", icon
='X', emboss
=False)
834 prop
.collection_name
= item
.name
837 if len(data
.cm_list_collection
) > index
+ 1:
838 line_separator
= column
.row(align
=True)
839 line_separator
.ui_units_y
= 0.01
840 line_separator
.scale_y
= 0.1
841 line_separator
.enabled
= False
843 line_separator
.separator()
844 line_separator
.label(icon
='BLANK1')
846 for _
in range(laycol
["lvl"] + 1):
847 line_separator
.label(icon
='BLANK1')
849 line_separator
.prop(cm
, "ui_separator")
851 if cm
.in_phantom_mode
:
852 c_icon
.enabled
= False
853 c_name
.enabled
= False
854 set_obj_col
.enabled
= False
855 rm_op
.enabled
= False
861 def draw_filter(self
, context
, layout
):
864 subrow
= row
.row(align
=True)
865 subrow
.prop(self
, "filter_name", text
="")
866 subrow
.prop(self
, "use_filter_invert", text
="", icon
='ARROW_LEFTRIGHT')
868 subrow
= row
.row(align
=True)
869 subrow
.prop(self
, "filter_by_selected", text
="", icon
='STICKY_UVS_LOC')
871 if context
.preferences
.addons
[__package__
].preferences
.enable_qcd
:
872 subrow
.prop(self
, "filter_by_qcd", text
="", icon
='EVENT_Q')
874 def filter_items(self
, context
, data
, propname
):
875 CM_UL_items
.filtering
= False
879 list_items
= getattr(data
, propname
)
883 CM_UL_items
.filtering
= True
885 new_flt_flags
= filter_items_by_name_custom(self
.filter_name
, self
.bitflag_filter_item
, list_items
)
887 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
890 if self
.filter_by_selected
:
891 CM_UL_items
.filtering
= True
892 new_flt_flags
= [0] * len(list_items
)
894 for idx
, item
in enumerate(list_items
):
895 collection
= internals
.layer_collections
[item
.name
]["ptr"].collection
897 # check if any of the selected objects are in the collection
898 if not set(context
.selected_objects
).isdisjoint(collection
.objects
):
899 new_flt_flags
[idx
] = self
.bitflag_filter_item
901 # add in any recently created collections
902 if item
.name
in CM_UL_items
.new_collections
:
903 new_flt_flags
[idx
] = self
.bitflag_filter_item
905 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
908 if self
.filter_by_qcd
:
909 CM_UL_items
.filtering
= True
910 new_flt_flags
= [0] * len(list_items
)
912 for idx
, item
in enumerate(list_items
):
913 if item
.qcd_slot_idx
:
914 new_flt_flags
[idx
] = self
.bitflag_filter_item
916 # add in any recently created collections
917 if item
.name
in CM_UL_items
.new_collections
:
918 new_flt_flags
[idx
] = self
.bitflag_filter_item
920 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
923 if not CM_UL_items
.filtering
: # display as treeview
924 CM_UL_items
.new_collections
.clear()
925 flt_flags
= [0] * len(list_items
)
927 for idx
, item
in enumerate(list_items
):
928 if internals
.layer_collections
[item
.name
]["visible"]:
929 flt_flags
[idx
] = self
.bitflag_filter_item
932 if self
.use_filter_invert
:
933 CM_UL_items
.filtering
= True # invert can act as pseudo filtering
934 for idx
, flag
in enumerate(flt_flags
):
935 flt_flags
[idx
] = 0 if flag
else self
.bitflag_filter_item
938 # update visible items list
939 CM_UL_items
.visible_items
.clear()
940 CM_UL_items
.visible_items
.extend(flt_flags
)
942 return flt_flags
, flt_neworder
946 def invoke(self
, context
, event
):
950 class CMDisplayOptionsPanel(Panel
):
951 bl_label
= "Display Options"
952 bl_idname
= "COLLECTIONMANAGER_PT_display_options"
954 # set space type to VIEW_3D and region type to HEADER
955 # because we only need it in a popover in the 3D View
956 # and don't want it always present in the UI/N-Panel
957 bl_space_type
= 'VIEW_3D'
958 bl_region_type
= 'HEADER'
960 def draw(self
, context
):
961 cm
= context
.scene
.collection_manager
965 panel_header
= layout
.row()
966 panel_header
.alignment
= 'CENTER'
967 panel_header
.label(text
="Display Options")
971 section_header
= layout
.row()
972 section_header
.alignment
= 'LEFT'
973 section_header
.label(text
="Restriction Toggles")
976 row
.prop(cm
, "show_exclude", icon
='CHECKBOX_HLT', icon_only
=True)
977 row
.prop(cm
, "show_selectable", icon
='RESTRICT_SELECT_OFF', icon_only
=True)
978 row
.prop(cm
, "show_hide_viewport", icon
='HIDE_OFF', icon_only
=True)
979 row
.prop(cm
, "show_disable_viewport", icon
='RESTRICT_VIEW_OFF', icon_only
=True)
980 row
.prop(cm
, "show_render", icon
='RESTRICT_RENDER_OFF', icon_only
=True)
981 row
.prop(cm
, "show_holdout", icon
='HOLDOUT_ON', icon_only
=True)
982 row
.prop(cm
, "show_indirect_only", icon
='INDIRECT_ONLY_ON', icon_only
=True)
986 section_header
= layout
.row()
987 section_header
.label(text
="Layout")
990 row
.prop(cm
, "align_local_ops")
993 class SpecialsMenu(Menu
):
994 bl_label
= "Specials"
995 bl_idname
= "VIEW3D_MT_CM_specials_menu"
997 def draw(self
, context
):
1000 prop
= layout
.operator("view3d.remove_empty_collections")
1001 prop
.without_objects
= False
1003 prop
= layout
.operator("view3d.remove_empty_collections",
1004 text
="Purge All Collections Without Objects")
1005 prop
.without_objects
= True
1009 layout
.operator("view3d.select_all_cumulative_objects")
1012 class EnableAllQCDSlotsMenu(Menu
):
1013 bl_label
= "Global QCD Slot Actions"
1014 bl_idname
= "VIEW3D_MT_CM_qcd_enable_all_menu"
1016 def draw(self
, context
):
1017 layout
= self
.layout
1019 layout
.operator("view3d.create_all_qcd_slots")
1023 layout
.operator("view3d.enable_all_qcd_slots")
1024 layout
.operator("view3d.enable_all_qcd_slots_isolated")
1028 layout
.operator("view3d.isolate_selected_objects_collections")
1029 if context
.mode
== 'OBJECT':
1030 layout
.operator("view3d.disable_selected_objects_collections")
1034 layout
.operator("view3d.disable_all_non_qcd_slots")
1035 layout
.operator("view3d.disable_all_collections")
1037 if context
.mode
== 'OBJECT':
1039 layout
.operator("view3d.select_all_qcd_objects")
1043 layout
.operator("view3d.discard_qcd_history")
1046 def view3d_header_qcd_slots(self
, context
):
1047 update_collection_tree(context
)
1049 view_layer
= context
.view_layer
1050 layout
= self
.layout
1053 check_state(context
, qcd
=True)
1056 main_row
= layout
.row(align
=True)
1057 current_qcd_history
= internals
.qcd_history
.get(context
.view_layer
.name
, [])
1059 main_row
.operator_menu_hold("view3d.enable_all_qcd_slots_meta",
1062 depress
=bool(current_qcd_history
),
1063 menu
="VIEW3D_MT_CM_qcd_enable_all_menu")
1066 split
= main_row
.split()
1067 col
= split
.column(align
=True)
1068 row
= col
.row(align
=True)
1071 selected_objects
= get_move_selection()
1072 active_object
= get_move_active()
1075 qcd_slot_name
= internals
.qcd_slots
.get_name(str(x
+1))
1078 qcd_laycol
= internals
.layer_collections
[qcd_slot_name
]["ptr"]
1079 collection_objects
= qcd_laycol
.collection
.objects
1083 # if the active object is in the current collection use a custom icon
1084 if (active_object
and active_object
in selected_objects
and
1085 active_object
.name
in collection_objects
):
1086 icon
= 'LAYER_ACTIVE'
1088 # if there are selected objects use LAYER_ACTIVE
1089 elif not selected_objects
.isdisjoint(collection_objects
):
1092 # If there are objects use LAYER_USED
1093 elif collection_objects
:
1095 active_icon
= get_active_icon(context
, qcd_laycol
)
1096 icon_value
= active_icon
.icon_id
1102 prop
= row
.operator("view3d.view_move_qcd_slot", text
="", icon
=icon
,
1103 icon_value
=icon_value
, depress
=not qcd_laycol
.exclude
)
1104 prop
.slot
= str(x
+1)
1107 prop
= row
.operator("view3d.unassigned_qcd_slot", text
="", icon
='X', emboss
=False)
1108 prop
.slot
= str(x
+1)
1115 row
= col
.row(align
=True)
1121 def view_layer_update(self
, context
):
1122 if context
.view_layer
.name
!= CollectionManager
.last_view_layer
:
1123 bpy
.app
.timers
.register(update_qcd_header
)
1124 CollectionManager
.last_view_layer
= context
.view_layer
.name
1127 def get_active_icon(context
, qcd_laycol
):
1128 global last_icon_theme_text
1129 global last_icon_theme_text_sel
1131 tool_theme
= context
.preferences
.themes
[0].user_interface
.wcol_tool
1132 pcoll
= preview_collections
["icons"]
1134 if qcd_laycol
.exclude
:
1135 theme_color
= tool_theme
.text
1136 last_theme_color
= last_icon_theme_text
1137 icon
= pcoll
["active_icon_text"]
1140 theme_color
= tool_theme
.text_sel
1141 last_theme_color
= last_icon_theme_text_sel
1142 icon
= pcoll
["active_icon_text_sel"]
1144 if last_theme_color
== None or theme_color
.hsv
!= last_theme_color
:
1145 update_icon(pcoll
["active_icon_base"], icon
, theme_color
)
1147 if qcd_laycol
.exclude
:
1148 last_icon_theme_text
= theme_color
.hsv
1151 last_icon_theme_text_sel
= theme_color
.hsv
1156 def update_icon(base
, icon
, theme_color
):
1157 icon
.icon_pixels
= base
.icon_pixels
1160 for offset
in range(len(icon
.icon_pixels
)):
1163 r
= icon
.icon_pixels_float
[idx
]
1164 g
= icon
.icon_pixels_float
[idx
+1]
1165 b
= icon
.icon_pixels_float
[idx
+2]
1166 a
= icon
.icon_pixels_float
[idx
+3]
1168 # add back some brightness and opacity blender takes away from the custom icon
1174 # make the icon follow the theme color (assuming the icon is white)
1179 colored_icon
.append(r
)
1180 colored_icon
.append(g
)
1181 colored_icon
.append(b
)
1182 colored_icon
.append(a
)
1184 icon
.icon_pixels_float
= colored_icon
1187 def filter_items_by_name_custom(pattern
, bitflag
, items
, propname
="name", flags
=None, reverse
=False):
1189 Set FILTER_ITEM for items which name matches filter_name one (case-insensitive).
1190 pattern is the filtering pattern.
1191 propname is the name of the string property to use for filtering.
1192 flags must be a list of integers the same length as items, or None!
1193 return a list of flags (based on given flags if not None),
1194 or an empty list if no flags were given and no filtering has been done.
1198 if not pattern
or not items
: # Empty pattern or list = no filtering!
1202 flags
= [0] * len(items
)
1204 # Make pattern case-insensitive
1205 pattern
= pattern
.lower()
1207 # Implicitly add heading/trailing wildcards.
1208 pattern
= "*" + pattern
+ "*"
1210 for i
, item
in enumerate(items
):
1211 name
= getattr(item
, propname
, None)
1213 # Make name case-insensitive
1216 # This is similar to a logical xor
1217 if bool(name
and fnmatch
.fnmatch(name
, pattern
)) is not bool(reverse
):
1220 # add in any recently created collections
1221 if item
.name
in CM_UL_items
.new_collections
:
1226 def merge_flt_flags(l1
, l2
):
1227 for idx
, _
in enumerate(l1
):
1228 l1
[idx
] &= l2
.pop(0)