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
,
48 from .qcd_operators
import (
53 preview_collections
= {}
54 last_icon_theme_text
= None
55 last_icon_theme_text_sel
= None
58 class CollectionManager(Operator
):
59 '''Manage and control collections, with advanced features, in a popup UI'''
60 bl_label
= "Collection Manager"
61 bl_idname
= "view3d.collection_manager"
67 master_collection
: StringProperty(
68 default
='Scene Collection',
70 description
="Scene Collection"
74 self
.window_open
= True
76 def draw(self
, context
):
77 cls
= CollectionManager
79 cm
= context
.scene
.collection_manager
80 prefs
= context
.preferences
.addons
[__package__
].preferences
81 view_layer
= context
.view_layer
83 if view_layer
.name
!= cls
.last_view_layer
:
85 bpy
.app
.timers
.register(update_qcd_header
)
87 update_collection_tree(context
)
88 cls
.last_view_layer
= view_layer
.name
90 # title and view layer
91 title_row
= layout
.split(factor
=0.5)
92 main
= title_row
.row()
93 view
= title_row
.row(align
=True)
94 view
.alignment
= 'RIGHT'
96 main
.label(text
="Collection Manager")
98 view
.prop(view_layer
, "use", text
="")
101 window
= context
.window
103 view
.template_search(
104 window
, "view_layer",
105 scene
, "view_layers",
106 new
="scene.view_layer_add",
107 unlink
="scene.view_layer_remove")
109 layout
.row().separator()
110 layout
.row().separator()
113 button_row_1
= layout
.row()
115 op_sec
= button_row_1
.row()
116 op_sec
.alignment
= 'LEFT'
118 collapse_sec
= op_sec
.row()
119 collapse_sec
.alignment
= 'LEFT'
120 collapse_sec
.enabled
= False
122 if len(internals
.expanded
) > 0:
123 text
= "Collapse All Items"
125 text
= "Expand All Items"
127 collapse_sec
.operator("view3d.expand_all_items", text
=text
)
129 for laycol
in internals
.collection_tree
:
130 if laycol
["has_children"]:
131 collapse_sec
.enabled
= True
135 renum_sec
= op_sec
.row()
136 renum_sec
.alignment
= 'LEFT'
137 renum_sec
.operator("view3d.renumerate_qcd_slots")
140 right_sec
= button_row_1
.row()
141 right_sec
.alignment
= 'RIGHT'
143 right_sec
.menu("VIEW3D_MT_CM_specials_menu")
144 right_sec
.popover(panel
="COLLECTIONMANAGER_PT_display_options",
145 text
="", icon
='FILTER')
147 mc_box
= layout
.box()
148 master_collection_row
= mc_box
.row(align
=True)
151 c_icon
= master_collection_row
.row()
153 if (context
.view_layer
.active_layer_collection
==
154 context
.view_layer
.layer_collection
):
157 prop
= c_icon
.operator("view3d.set_active_collection",
158 text
='', icon
='GROUP', depress
=highlight
)
159 prop
.is_master_collection
= True
160 prop
.collection_name
= 'Master Collection'
162 master_collection_row
.separator()
165 name_row
= master_collection_row
.row()
166 name_row
.prop(self
, "master_collection", text
='')
167 name_row
.enabled
= False
169 master_collection_row
.separator()
172 global_rto_row
= master_collection_row
.row()
173 global_rto_row
.alignment
= 'RIGHT'
175 row_setcol
= global_rto_row
.row()
176 row_setcol
.alignment
= 'LEFT'
177 row_setcol
.operator_context
= 'INVOKE_DEFAULT'
179 selected_objects
= get_move_selection()
180 active_object
= get_move_active()
181 CM_UL_items
.selected_objects
= selected_objects
182 CM_UL_items
.active_object
= active_object
184 collection
= context
.view_layer
.layer_collection
.collection
189 if active_object
and active_object
.name
in collection
.objects
:
192 elif not selected_objects
.isdisjoint(collection
.objects
):
193 icon
= 'STICKY_UVS_LOC'
196 row_setcol
.enabled
= False
198 prop
= row_setcol
.operator("view3d.set_collection", text
="",
199 icon
=icon
, emboss
=False)
200 prop
.is_master_collection
= True
201 prop
.collection_name
= 'Master Collection'
203 copy_icon
= 'COPYDOWN'
204 swap_icon
= 'ARROW_LEFTRIGHT'
205 copy_swap_icon
= 'SELECT_INTERSECT'
208 exclude_all_history
= internals
.rto_history
["exclude_all"].get(view_layer
.name
, [])
209 depress
= True if len(exclude_all_history
) else False
210 icon
= 'CHECKBOX_HLT'
211 buffers
= [False, False]
213 if internals
.copy_buffer
["RTO"] == "exclude":
217 if internals
.swap_buffer
["A"]["RTO"] == "exclude":
221 if buffers
[0] and buffers
[1]:
222 icon
= copy_swap_icon
224 global_rto_row
.operator("view3d.un_exclude_all_collections", text
="", icon
=icon
, depress
=depress
)
226 if cm
.show_selectable
:
227 select_all_history
= internals
.rto_history
["select_all"].get(view_layer
.name
, [])
228 depress
= True if len(select_all_history
) else False
229 icon
= 'RESTRICT_SELECT_OFF'
230 buffers
= [False, False]
232 if internals
.copy_buffer
["RTO"] == "select":
236 if internals
.swap_buffer
["A"]["RTO"] == "select":
240 if buffers
[0] and buffers
[1]:
241 icon
= copy_swap_icon
243 global_rto_row
.operator("view3d.un_restrict_select_all_collections", text
="", icon
=icon
, depress
=depress
)
245 if cm
.show_hide_viewport
:
246 hide_all_history
= internals
.rto_history
["hide_all"].get(view_layer
.name
, [])
247 depress
= True if len(hide_all_history
) else False
249 buffers
= [False, False]
251 if internals
.copy_buffer
["RTO"] == "hide":
255 if internals
.swap_buffer
["A"]["RTO"] == "hide":
259 if buffers
[0] and buffers
[1]:
260 icon
= copy_swap_icon
262 global_rto_row
.operator("view3d.un_hide_all_collections", text
="", icon
=icon
, depress
=depress
)
264 if cm
.show_disable_viewport
:
265 disable_all_history
= internals
.rto_history
["disable_all"].get(view_layer
.name
, [])
266 depress
= True if len(disable_all_history
) else False
267 icon
= 'RESTRICT_VIEW_OFF'
268 buffers
= [False, False]
270 if internals
.copy_buffer
["RTO"] == "disable":
274 if internals
.swap_buffer
["A"]["RTO"] == "disable":
278 if buffers
[0] and buffers
[1]:
279 icon
= copy_swap_icon
281 global_rto_row
.operator("view3d.un_disable_viewport_all_collections", text
="", icon
=icon
, depress
=depress
)
284 render_all_history
= internals
.rto_history
["render_all"].get(view_layer
.name
, [])
285 depress
= True if len(render_all_history
) else False
286 icon
= 'RESTRICT_RENDER_OFF'
287 buffers
= [False, False]
289 if internals
.copy_buffer
["RTO"] == "render":
293 if internals
.swap_buffer
["A"]["RTO"] == "render":
297 if buffers
[0] and buffers
[1]:
298 icon
= copy_swap_icon
300 global_rto_row
.operator("view3d.un_disable_render_all_collections", text
="", icon
=icon
, depress
=depress
)
303 holdout_all_history
= internals
.rto_history
["holdout_all"].get(view_layer
.name
, [])
304 depress
= True if len(holdout_all_history
) else False
306 buffers
= [False, False]
308 if internals
.copy_buffer
["RTO"] == "holdout":
312 if internals
.swap_buffer
["A"]["RTO"] == "holdout":
316 if buffers
[0] and buffers
[1]:
317 icon
= copy_swap_icon
319 global_rto_row
.operator("view3d.un_holdout_all_collections", text
="", icon
=icon
, depress
=depress
)
321 if cm
.show_indirect_only
:
322 indirect_all_history
= internals
.rto_history
["indirect_all"].get(view_layer
.name
, [])
323 depress
= True if len(indirect_all_history
) else False
324 icon
= 'INDIRECT_ONLY_ON'
325 buffers
= [False, False]
327 if internals
.copy_buffer
["RTO"] == "indirect":
331 if internals
.swap_buffer
["A"]["RTO"] == "indirect":
335 if buffers
[0] and buffers
[1]:
336 icon
= copy_swap_icon
338 global_rto_row
.operator("view3d.un_indirect_only_all_collections", text
="", icon
=icon
, depress
=depress
)
341 layout
.row().template_list("CM_UL_items", "",
342 cm
, "cm_list_collection",
348 button_row_2
= layout
.row()
349 prop
= button_row_2
.operator("view3d.add_collection", text
="Add Collection",
350 icon
='COLLECTION_NEW')
353 prop
= button_row_2
.operator("view3d.add_collection", text
="Add SubCollection",
354 icon
='COLLECTION_NEW')
358 button_row_3
= layout
.row()
361 phantom_mode
= button_row_3
.row(align
=True)
362 toggle_text
= "Disable " if cm
.in_phantom_mode
else "Enable "
363 phantom_mode
.operator("view3d.toggle_phantom_mode", text
=toggle_text
+"Phantom Mode")
364 phantom_mode
.operator("view3d.apply_phantom_mode", text
="", icon
='CHECKMARK')
366 if cm
.in_phantom_mode
:
369 renum_sec
.enabled
= False
371 c_icon
.enabled
= False
372 row_setcol
.enabled
= False
373 addcollec_row
.enabled
= False
376 def execute(self
, context
):
377 wm
= context
.window_manager
379 update_property_group(context
)
381 cm
= context
.scene
.collection_manager
382 view_layer
= context
.view_layer
384 self
.view_layer
= view_layer
.name
386 # make sure list index is valid
387 if cm
.cm_list_index
>= len(cm
.cm_list_collection
):
388 cm
.cm_list_index
= -1
390 # check if expanded & history/buffer state still correct
391 if internals
.collection_state
:
392 new_state
= generate_state()
394 if new_state
["name"] != internals
.collection_state
["name"]:
395 internals
.copy_buffer
["RTO"] = ""
396 internals
.copy_buffer
["values"].clear()
398 internals
.swap_buffer
["A"]["RTO"] = ""
399 internals
.swap_buffer
["A"]["values"].clear()
400 internals
.swap_buffer
["B"]["RTO"] = ""
401 internals
.swap_buffer
["B"]["values"].clear()
403 for name
in list(internals
.expanded
):
404 laycol
= internals
.layer_collections
.get(name
)
405 if not laycol
or not laycol
["has_children"]:
406 internals
.expanded
.remove(name
)
408 for name
in list(internals
.expand_history
["history"]):
409 laycol
= internals
.layer_collections
.get(name
)
410 if not laycol
or not laycol
["has_children"]:
411 internals
.expand_history
["history"].remove(name
)
413 for rto
, history
in internals
.rto_history
.items():
414 if view_layer
.name
in history
:
415 del history
[view_layer
.name
]
419 for rto
in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
420 if new_state
[rto
] != internals
.collection_state
[rto
]:
421 if view_layer
.name
in internals
.rto_history
[rto
]:
422 del internals
.rto_history
[rto
][view_layer
.name
]
424 if view_layer
.name
in internals
.rto_history
[rto
+"_all"]:
425 del internals
.rto_history
[rto
+"_all"][view_layer
.name
]
427 # check if in phantom mode and if it's still viable
428 if cm
.in_phantom_mode
:
429 if internals
.layer_collections
.keys() != internals
.phantom_history
["initial_state"].keys():
430 cm
.in_phantom_mode
= False
432 if view_layer
.name
!= internals
.phantom_history
["view_layer"]:
433 cm
.in_phantom_mode
= False
435 if not cm
.in_phantom_mode
:
436 for key
, value
in internals
.phantom_history
.items():
439 except AttributeError:
440 if key
== "view_layer":
441 internals
.phantom_history
["view_layer"] = ""
443 # handle window sizing
446 row_indent_width
= 15
451 width
= min_width
+ row_indent_width
+ (width_step
* internals
.max_lvl
)
453 if bpy
.context
.preferences
.addons
[__package__
].preferences
.enable_qcd
:
456 if len(internals
.layer_collections
) > 14:
457 width
+= scrollbar_width
459 if width
> max_width
:
462 return wm
.invoke_popup(self
, width
=width
)
465 if not self
.window_open
:
466 # prevent destructor execution when changing templates
469 internals
.collection_state
.clear()
470 internals
.collection_state
.update(generate_state())
473 class CM_UL_items(UIList
):
475 last_filter_value
= ""
477 selected_objects
= set()
483 filter_name
: StringProperty(
484 name
="Filter By Name",
486 description
="Filter collections by name",
487 update
=lambda self
, context
:
488 CM_UL_items
.new_collections
.clear(),
491 use_filter_invert
: BoolProperty(
494 description
="Invert filtering (show hidden items, and vice-versa)",
497 filter_by_selected
: BoolProperty(
498 name
="Filter By Selected",
500 description
="Filter collections by selected items",
501 update
=lambda self
, context
:
502 CM_UL_items
.new_collections
.clear(),
504 filter_by_qcd
: BoolProperty(
505 name
="Filter By QCD",
507 description
="Filter collections to only show QCD slots",
508 update
=lambda self
, context
:
509 CM_UL_items
.new_collections
.clear(),
512 def draw_item(self
, context
, layout
, data
, item
, icon
, active_data
,active_propname
, index
):
513 self
.use_filter_show
= True
515 cm
= context
.scene
.collection_manager
516 prefs
= context
.preferences
.addons
[__package__
].preferences
517 view_layer
= context
.view_layer
518 laycol
= internals
.layer_collections
[item
.name
]
519 collection
= laycol
["ptr"].collection
520 selected_objects
= CM_UL_items
.selected_objects
521 active_object
= CM_UL_items
.active_object
523 column
= layout
.column(align
=True)
525 main_row
= column
.row()
527 s1
= main_row
.row(align
=True)
528 s1
.alignment
= 'LEFT'
530 s2
= main_row
.row(align
=True)
531 s2
.alignment
= 'RIGHT'
535 # allow room to select the row from the beginning
539 if laycol
["lvl"] > 0:
540 for _
in range(laycol
["lvl"]):
541 row
.label(icon
='BLANK1')
543 # add expander if collection has children to make UIList act like tree view
544 if laycol
["has_children"]:
545 if laycol
["expanded"]:
546 highlight
= True if internals
.expand_history
["target"] == item
.name
else False
548 prop
= row
.operator("view3d.expand_sublevel", text
="",
549 icon
='DISCLOSURE_TRI_DOWN',
550 emboss
=highlight
, depress
=highlight
)
552 prop
.name
= item
.name
556 highlight
= True if internals
.expand_history
["target"] == item
.name
else False
558 prop
= row
.operator("view3d.expand_sublevel", text
="",
559 icon
='DISCLOSURE_TRI_RIGHT',
560 emboss
=highlight
, depress
=highlight
)
562 prop
.name
= item
.name
566 row
.label(icon
='BLANK1')
572 if (context
.view_layer
.active_layer_collection
== laycol
["ptr"]):
575 prop
= c_icon
.operator("view3d.set_active_collection", text
='', icon
='GROUP',
576 emboss
=highlight
, depress
=highlight
)
578 prop
.is_master_collection
= False
579 prop
.collection_name
= item
.name
584 QCD
.prop(item
, "qcd_slot_idx", text
="")
588 #if rename[0] and index == cm.cm_list_index:
589 #c_name.activate_init = True
592 c_name
.prop(item
, "name", text
="", expand
=True)
594 # used as a separator (actual separator not wide enough)
597 row
= s2
if cm
.align_local_ops
else s1
599 # add set_collection op
600 set_obj_col
= row
.row()
601 set_obj_col
.operator_context
= 'INVOKE_DEFAULT'
606 if active_object
and active_object
.name
in collection
.objects
:
609 elif not selected_objects
.isdisjoint(collection
.objects
):
610 icon
= 'STICKY_UVS_LOC'
613 set_obj_col
.enabled
= False
616 prop
= set_obj_col
.operator("view3d.set_collection", text
="",
617 icon
=icon
, emboss
=False)
618 prop
.is_master_collection
= False
619 prop
.collection_name
= item
.name
623 exclude_history_base
= internals
.rto_history
["exclude"].get(view_layer
.name
, {})
624 exclude_target
= exclude_history_base
.get("target", "")
625 exclude_history
= exclude_history_base
.get("history", [])
627 highlight
= bool(exclude_history
and exclude_target
== item
.name
)
628 icon
= 'CHECKBOX_DEHLT' if laycol
["ptr"].exclude
else 'CHECKBOX_HLT'
630 prop
= row
.operator("view3d.exclude_collection", text
="", icon
=icon
,
631 emboss
=highlight
, depress
=highlight
)
632 prop
.name
= item
.name
634 if cm
.show_selectable
:
635 select_history_base
= internals
.rto_history
["select"].get(view_layer
.name
, {})
636 select_target
= select_history_base
.get("target", "")
637 select_history
= select_history_base
.get("history", [])
639 highlight
= bool(select_history
and select_target
== item
.name
)
640 icon
= ('RESTRICT_SELECT_ON' if laycol
["ptr"].collection
.hide_select
else
641 'RESTRICT_SELECT_OFF')
643 prop
= row
.operator("view3d.restrict_select_collection", text
="", icon
=icon
,
644 emboss
=highlight
, depress
=highlight
)
645 prop
.name
= item
.name
647 if cm
.show_hide_viewport
:
648 hide_history_base
= internals
.rto_history
["hide"].get(view_layer
.name
, {})
649 hide_target
= hide_history_base
.get("target", "")
650 hide_history
= hide_history_base
.get("history", [])
652 highlight
= bool(hide_history
and hide_target
== item
.name
)
653 icon
= 'HIDE_ON' if laycol
["ptr"].hide_viewport
else 'HIDE_OFF'
655 prop
= row
.operator("view3d.hide_collection", text
="", icon
=icon
,
656 emboss
=highlight
, depress
=highlight
)
657 prop
.name
= item
.name
659 if cm
.show_disable_viewport
:
660 disable_history_base
= internals
.rto_history
["disable"].get(view_layer
.name
, {})
661 disable_target
= disable_history_base
.get("target", "")
662 disable_history
= disable_history_base
.get("history", [])
664 highlight
= bool(disable_history
and disable_target
== item
.name
)
665 icon
= ('RESTRICT_VIEW_ON' if laycol
["ptr"].collection
.hide_viewport
else
668 prop
= row
.operator("view3d.disable_viewport_collection", text
="", icon
=icon
,
669 emboss
=highlight
, depress
=highlight
)
670 prop
.name
= item
.name
673 render_history_base
= internals
.rto_history
["render"].get(view_layer
.name
, {})
674 render_target
= render_history_base
.get("target", "")
675 render_history
= render_history_base
.get("history", [])
677 highlight
= bool(render_history
and render_target
== item
.name
)
678 icon
= ('RESTRICT_RENDER_ON' if laycol
["ptr"].collection
.hide_render
else
679 'RESTRICT_RENDER_OFF')
681 prop
= row
.operator("view3d.disable_render_collection", text
="", icon
=icon
,
682 emboss
=highlight
, depress
=highlight
)
683 prop
.name
= item
.name
686 holdout_history_base
= internals
.rto_history
["holdout"].get(view_layer
.name
, {})
687 holdout_target
= holdout_history_base
.get("target", "")
688 holdout_history
= holdout_history_base
.get("history", [])
690 highlight
= bool(holdout_history
and holdout_target
== item
.name
)
691 icon
= ('HOLDOUT_ON' if laycol
["ptr"].holdout
else
694 prop
= row
.operator("view3d.holdout_collection", text
="", icon
=icon
,
695 emboss
=highlight
, depress
=highlight
)
696 prop
.name
= item
.name
698 if cm
.show_indirect_only
:
699 indirect_history_base
= internals
.rto_history
["indirect"].get(view_layer
.name
, {})
700 indirect_target
= indirect_history_base
.get("target", "")
701 indirect_history
= indirect_history_base
.get("history", [])
703 highlight
= bool(indirect_history
and indirect_target
== item
.name
)
704 icon
= ('INDIRECT_ONLY_ON' if laycol
["ptr"].indirect_only
else
707 prop
= row
.operator("view3d.indirect_only_collection", text
="", icon
=icon
,
708 emboss
=highlight
, depress
=highlight
)
709 prop
.name
= item
.name
719 prop
= rm_op
.operator("view3d.remove_collection", text
="", icon
='X', emboss
=False)
720 prop
.collection_name
= item
.name
723 if len(data
.cm_list_collection
) > index
+ 1:
724 line_separator
= column
.row(align
=True)
725 line_separator
.ui_units_y
= 0.01
726 line_separator
.scale_y
= 0.1
727 line_separator
.enabled
= False
729 line_separator
.separator()
730 line_separator
.label(icon
='BLANK1')
732 for _
in range(laycol
["lvl"] + 1):
733 line_separator
.label(icon
='BLANK1')
735 line_separator
.prop(cm
, "ui_separator")
737 if cm
.in_phantom_mode
:
738 c_icon
.enabled
= False
739 c_name
.enabled
= False
740 set_obj_col
.enabled
= False
741 rm_op
.enabled
= False
747 def draw_filter(self
, context
, layout
):
750 subrow
= row
.row(align
=True)
751 subrow
.prop(self
, "filter_name", text
="")
753 icon
= 'ZOOM_OUT' if self
.use_filter_invert
else 'ZOOM_IN'
754 subrow
.prop(self
, "use_filter_invert", text
="", icon
=icon
)
756 subrow
= row
.row(align
=True)
757 subrow
.prop(self
, "filter_by_selected", text
="", icon
='SNAP_VOLUME')
759 if context
.preferences
.addons
[__package__
].preferences
.enable_qcd
:
760 subrow
.prop(self
, "filter_by_qcd", text
="", icon
='EVENT_Q')
762 def filter_items(self
, context
, data
, propname
):
763 CM_UL_items
.filtering
= False
767 list_items
= getattr(data
, propname
)
771 CM_UL_items
.filtering
= True
773 new_flt_flags
= filter_items_by_name_custom(self
.filter_name
, self
.bitflag_filter_item
, list_items
)
775 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
778 if self
.filter_by_selected
:
779 CM_UL_items
.filtering
= True
780 new_flt_flags
= [0] * len(list_items
)
782 for idx
, item
in enumerate(list_items
):
783 collection
= internals
.layer_collections
[item
.name
]["ptr"].collection
785 # check if any of the selected objects are in the collection
786 if not set(context
.selected_objects
).isdisjoint(collection
.objects
):
787 new_flt_flags
[idx
] = self
.bitflag_filter_item
789 # add in any recently created collections
790 if item
.name
in CM_UL_items
.new_collections
:
791 new_flt_flags
[idx
] = self
.bitflag_filter_item
793 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
796 if self
.filter_by_qcd
:
797 CM_UL_items
.filtering
= True
798 new_flt_flags
= [0] * len(list_items
)
800 for idx
, item
in enumerate(list_items
):
801 if item
.qcd_slot_idx
:
802 new_flt_flags
[idx
] = self
.bitflag_filter_item
804 # add in any recently created collections
805 if item
.name
in CM_UL_items
.new_collections
:
806 new_flt_flags
[idx
] = self
.bitflag_filter_item
808 flt_flags
= merge_flt_flags(flt_flags
, new_flt_flags
)
811 if not CM_UL_items
.filtering
: # display as treeview
812 CM_UL_items
.new_collections
.clear()
813 flt_flags
= [0] * len(list_items
)
815 for idx
, item
in enumerate(list_items
):
816 if internals
.layer_collections
[item
.name
]["visible"]:
817 flt_flags
[idx
] = self
.bitflag_filter_item
820 if self
.use_filter_invert
:
821 CM_UL_items
.filtering
= True # invert can act as pseudo filtering
822 for idx
, flag
in enumerate(flt_flags
):
823 flt_flags
[idx
] = 0 if flag
else self
.bitflag_filter_item
826 # update visible items list
827 CM_UL_items
.visible_items
.clear()
828 CM_UL_items
.visible_items
.extend(flt_flags
)
830 return flt_flags
, flt_neworder
834 def invoke(self
, context
, event
):
838 class CMDisplayOptionsPanel(Panel
):
839 bl_label
= "Display Options"
840 bl_idname
= "COLLECTIONMANAGER_PT_display_options"
842 # set space type to VIEW_3D and region type to HEADER
843 # because we only need it in a popover in the 3D View
844 # and don't want it always present in the UI/N-Panel
845 bl_space_type
= 'VIEW_3D'
846 bl_region_type
= 'HEADER'
848 def draw(self
, context
):
849 cm
= context
.scene
.collection_manager
853 panel_header
= layout
.row()
854 panel_header
.alignment
= 'CENTER'
855 panel_header
.label(text
="Display Options")
859 section_header
= layout
.row()
860 section_header
.alignment
= 'LEFT'
861 section_header
.label(text
="Restriction Toggles")
864 row
.prop(cm
, "show_exclude", icon
='CHECKBOX_HLT', icon_only
=True)
865 row
.prop(cm
, "show_selectable", icon
='RESTRICT_SELECT_OFF', icon_only
=True)
866 row
.prop(cm
, "show_hide_viewport", icon
='HIDE_OFF', icon_only
=True)
867 row
.prop(cm
, "show_disable_viewport", icon
='RESTRICT_VIEW_OFF', icon_only
=True)
868 row
.prop(cm
, "show_render", icon
='RESTRICT_RENDER_OFF', icon_only
=True)
869 row
.prop(cm
, "show_holdout", icon
='HOLDOUT_ON', icon_only
=True)
870 row
.prop(cm
, "show_indirect_only", icon
='INDIRECT_ONLY_ON', icon_only
=True)
874 section_header
= layout
.row()
875 section_header
.label(text
="Layout")
878 row
.prop(cm
, "align_local_ops")
881 class SpecialsMenu(Menu
):
882 bl_label
= "Specials"
883 bl_idname
= "VIEW3D_MT_CM_specials_menu"
885 def draw(self
, context
):
888 prop
= layout
.operator("view3d.remove_empty_collections")
889 prop
.without_objects
= False
891 prop
= layout
.operator("view3d.remove_empty_collections",
892 text
="Purge All Collections Without Objects")
893 prop
.without_objects
= True
896 class EnableAllQCDSlotsMenu(Menu
):
897 bl_label
= "Global QCD Slot Actions"
898 bl_idname
= "VIEW3D_MT_CM_qcd_enable_all_menu"
900 def draw(self
, context
):
903 layout
.operator("view3d.enable_all_qcd_slots")
904 layout
.operator("view3d.enable_all_qcd_slots_isolated")
908 layout
.operator("view3d.isolate_selected_objects_collections")
909 if context
.mode
== 'OBJECT':
910 layout
.operator("view3d.disable_selected_objects_collections")
914 layout
.operator("view3d.disable_all_non_qcd_slots")
915 layout
.operator("view3d.disable_all_collections")
917 if context
.mode
== 'OBJECT':
919 layout
.operator("view3d.select_all_qcd_objects")
923 layout
.operator("view3d.discard_qcd_history")
926 def view3d_header_qcd_slots(self
, context
):
927 update_collection_tree(context
)
929 view_layer
= context
.view_layer
933 if internals
.qcd_collection_state
:
934 view_layer
= context
.view_layer
935 new_state
= generate_state(qcd
=True)
937 if (new_state
["name"] != internals
.qcd_collection_state
["name"]
938 or new_state
["exclude"] != internals
.qcd_collection_state
["exclude"]
939 or new_state
["qcd"] != internals
.qcd_collection_state
["qcd"]):
940 if view_layer
.name
in internals
.qcd_history
:
941 del internals
.qcd_history
[view_layer
.name
]
942 internals
.qcd_collection_state
.clear()
946 main_row
= layout
.row(align
=True)
947 current_qcd_history
= internals
.qcd_history
.get(context
.view_layer
.name
, [])
949 main_row
.operator_menu_hold("view3d.enable_all_qcd_slots_meta",
952 depress
=bool(current_qcd_history
),
953 menu
="VIEW3D_MT_CM_qcd_enable_all_menu")
956 split
= main_row
.split()
957 col
= split
.column(align
=True)
958 row
= col
.row(align
=True)
961 selected_objects
= get_move_selection()
962 active_object
= get_move_active()
965 qcd_slot_name
= internals
.qcd_slots
.get_name(str(x
+1))
968 qcd_laycol
= internals
.layer_collections
[qcd_slot_name
]["ptr"]
969 collection_objects
= qcd_laycol
.collection
.objects
973 # if the active object is in the current collection use a custom icon
974 if (active_object
and active_object
in selected_objects
and
975 active_object
.name
in collection_objects
):
976 icon
= 'LAYER_ACTIVE'
978 # if there are selected objects use LAYER_ACTIVE
979 elif not selected_objects
.isdisjoint(collection_objects
):
982 # If there are objects use LAYER_USED
983 elif collection_objects
:
985 active_icon
= get_active_icon(context
, qcd_laycol
)
986 icon_value
= active_icon
.icon_id
992 prop
= row
.operator("view3d.view_move_qcd_slot", text
="", icon
=icon
,
993 icon_value
=icon_value
, depress
=not qcd_laycol
.exclude
)
997 row
.label(text
="", icon
='X')
1004 row
= col
.row(align
=True)
1010 def view_layer_update(self
, context
):
1011 if context
.view_layer
.name
!= CollectionManager
.last_view_layer
:
1012 bpy
.app
.timers
.register(update_qcd_header
)
1013 CollectionManager
.last_view_layer
= context
.view_layer
.name
1016 def get_active_icon(context
, qcd_laycol
):
1017 global last_icon_theme_text
1018 global last_icon_theme_text_sel
1020 tool_theme
= context
.preferences
.themes
[0].user_interface
.wcol_tool
1021 pcoll
= preview_collections
["icons"]
1023 if qcd_laycol
.exclude
:
1024 theme_color
= tool_theme
.text
1025 last_theme_color
= last_icon_theme_text
1026 icon
= pcoll
["active_icon_text"]
1029 theme_color
= tool_theme
.text_sel
1030 last_theme_color
= last_icon_theme_text_sel
1031 icon
= pcoll
["active_icon_text_sel"]
1033 if last_theme_color
== None or theme_color
.hsv
!= last_theme_color
:
1034 update_icon(pcoll
["active_icon_base"], icon
, theme_color
)
1036 if qcd_laycol
.exclude
:
1037 last_icon_theme_text
= theme_color
.hsv
1040 last_icon_theme_text_sel
= theme_color
.hsv
1045 def update_icon(base
, icon
, theme_color
):
1046 icon
.icon_pixels
= base
.icon_pixels
1049 for offset
in range(len(icon
.icon_pixels
)):
1052 r
= icon
.icon_pixels_float
[idx
]
1053 g
= icon
.icon_pixels_float
[idx
+1]
1054 b
= icon
.icon_pixels_float
[idx
+2]
1055 a
= icon
.icon_pixels_float
[idx
+3]
1057 # add back some brightness and opacity blender takes away from the custom icon
1063 # make the icon follow the theme color (assuming the icon is white)
1068 colored_icon
.append(r
)
1069 colored_icon
.append(g
)
1070 colored_icon
.append(b
)
1071 colored_icon
.append(a
)
1073 icon
.icon_pixels_float
= colored_icon
1076 def filter_items_by_name_custom(pattern
, bitflag
, items
, propname
="name", flags
=None, reverse
=False):
1078 Set FILTER_ITEM for items which name matches filter_name one (case-insensitive).
1079 pattern is the filtering pattern.
1080 propname is the name of the string property to use for filtering.
1081 flags must be a list of integers the same length as items, or None!
1082 return a list of flags (based on given flags if not None),
1083 or an empty list if no flags were given and no filtering has been done.
1087 if not pattern
or not items
: # Empty pattern or list = no filtering!
1091 flags
= [0] * len(items
)
1093 # Make pattern case-insensitive
1094 pattern
= pattern
.lower()
1096 # Implicitly add heading/trailing wildcards.
1097 pattern
= "*" + pattern
+ "*"
1099 for i
, item
in enumerate(items
):
1100 name
= getattr(item
, propname
, None)
1102 # Make name case-insensitive
1105 # This is similar to a logical xor
1106 if bool(name
and fnmatch
.fnmatch(name
, pattern
)) is not bool(reverse
):
1109 # add in any recently created collections
1110 if item
.name
in CM_UL_items
.new_collections
:
1115 def merge_flt_flags(l1
, l2
):
1116 for idx
, _
in enumerate(l1
):
1117 l1
[idx
] &= l2
.pop(0)