Cleanup: quiet character escape warnings
[blender-addons.git] / object_collection_manager / ui.py
blobec2c7354f55206283e740e2b319ba5a09de70db8
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # Copyright 2011, Ryan Inch
21 import bpy
23 from bpy.types import (
24 Menu,
25 Operator,
26 Panel,
27 UIList,
30 from bpy.props import (
31 BoolProperty,
32 StringProperty,
35 # For VARS
36 from . import internals
38 # For FUNCTIONS
39 from .internals import (
40 update_collection_tree,
41 update_property_group,
42 generate_state,
43 check_state,
44 get_move_selection,
45 get_move_active,
46 update_qcd_header,
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"
60 last_view_layer = ""
62 window_open = False
64 master_collection: StringProperty(
65 default='Scene Collection',
66 name="",
67 description="Scene Collection"
70 def __init__(self):
71 self.window_open = True
73 def draw(self, context):
74 cls = CollectionManager
75 layout = self.layout
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:
82 if prefs.enable_qcd:
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="")
97 view.separator()
99 window = context.window
100 scene = window.scene
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()
110 # buttons
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"
122 else:
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
130 break
132 if prefs.enable_qcd:
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')
142 # menu & filter
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)
158 # collection icon
159 c_icon = master_collection_row.row()
160 highlight = False
161 if (context.view_layer.active_layer_collection ==
162 context.view_layer.layer_collection):
163 highlight = True
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()
172 # name
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
178 # set selection
179 setsel = name_row.row(align=True)
180 icon = 'DOT'
181 some_selected = False
183 if not collection.objects:
184 icon = 'BLANK1'
185 setsel.active = False
187 else:
188 all_selected = None
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
196 else:
197 all_unreachable = False
199 if obj.select_get() == False:
200 # some objects remain unselected
201 icon = 'KEYFRAME'
202 all_selected = False
204 else:
205 some_selected = True
207 if all_selected == False:
208 break
210 all_selected = True
213 if all_selected:
214 # all objects are selected
215 icon = 'KEYFRAME_HLT'
217 if all_unreachable:
218 if collection.objects:
219 icon = 'DOT'
221 setsel.active = False
223 prop = setsel.operator("view3d.select_collection_objects",
224 text="",
225 icon=icon,
226 depress=some_selected,
228 prop.is_master_collection = True
229 prop.collection_name = 'Scene Collection'
232 # global rtos
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()
240 # set collection
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
252 icon = 'IMPORT'
254 if collection.objects:
255 icon = 'MESH_CUBE'
257 if selected_objects:
258 if active_object and active_object.name in collection.objects:
259 icon = 'SNAP_VOLUME'
261 elif not selected_objects.isdisjoint(collection.objects):
262 icon = 'STICKY_UVS_LOC'
264 else:
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",
273 text="",
274 icon='BLANK1',
277 # add operator
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",
288 text="",
289 icon='BLANK1',
292 copy_icon = 'COPYDOWN'
293 swap_icon = 'ARROW_LEFTRIGHT'
294 copy_swap_icon = 'SELECT_INTERSECT'
296 if cm.show_exclude:
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":
303 icon = copy_icon
304 buffers[0] = True
306 if internals.swap_buffer["A"]["RTO"] == "exclude":
307 icon = swap_icon
308 buffers[1] = True
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":
322 icon = copy_icon
323 buffers[0] = True
325 if internals.swap_buffer["A"]["RTO"] == "select":
326 icon = swap_icon
327 buffers[1] = True
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
337 icon = 'HIDE_OFF'
338 buffers = [False, False]
340 if internals.copy_buffer["RTO"] == "hide":
341 icon = copy_icon
342 buffers[0] = True
344 if internals.swap_buffer["A"]["RTO"] == "hide":
345 icon = swap_icon
346 buffers[1] = True
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":
360 icon = copy_icon
361 buffers[0] = True
363 if internals.swap_buffer["A"]["RTO"] == "disable":
364 icon = swap_icon
365 buffers[1] = True
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)
372 if cm.show_render:
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":
379 icon = copy_icon
380 buffers[0] = True
382 if internals.swap_buffer["A"]["RTO"] == "render":
383 icon = swap_icon
384 buffers[1] = True
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)
391 if cm.show_holdout:
392 holdout_all_history = internals.rto_history["holdout_all"].get(view_layer.name, [])
393 depress = True if len(holdout_all_history) else False
394 icon = 'HOLDOUT_ON'
395 buffers = [False, False]
397 if internals.copy_buffer["RTO"] == "holdout":
398 icon = copy_icon
399 buffers[0] = True
401 if internals.swap_buffer["A"]["RTO"] == "holdout":
402 icon = swap_icon
403 buffers[1] = True
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":
417 icon = copy_icon
418 buffers[0] = True
420 if internals.swap_buffer["A"]["RTO"] == "indirect":
421 icon = swap_icon
422 buffers[1] = True
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)
429 # treeview
430 layout.row().template_list("CM_UL_items", "",
431 cm, "cm_list_collection",
432 cm, "cm_list_index",
433 rows=15,
434 sort_lock=True)
436 # add collections
437 button_row_2 = layout.row()
438 prop = button_row_2.operator("view3d.add_collection", text="Add Collection",
439 icon='COLLECTION_NEW')
440 prop.child = False
442 prop = button_row_2.operator("view3d.add_collection", text="Add SubCollection",
443 icon='COLLECTION_NEW')
444 prop.child = True
447 button_row_3 = layout.row()
449 # phantom mode
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:
456 view.enabled = False
458 if prefs.enable_qcd:
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
487 max_width = 960
488 min_width = 456
489 row_indent_width = 15
490 width_step = 21
491 qcd_width = 30
492 scrollbar_width = 21
494 width = min_width + row_indent_width + (width_step * internals.max_lvl)
496 if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
497 width += qcd_width
499 if len(internals.layer_collections) > 14:
500 width += scrollbar_width
502 if width > max_width:
503 width = max_width
505 return wm.invoke_popup(self, width=width)
507 def __del__(self):
508 if not self.window_open:
509 # prevent destructor execution when changing templates
510 return
512 internals.collection_state.clear()
513 internals.collection_state.update(generate_state())
516 class CM_UL_items(UIList):
517 filtering = False
518 last_filter_value = ""
520 selected_objects = set()
521 active_object = None
523 visible_items = []
524 new_collections = []
526 filter_name: StringProperty(
527 name="Filter By Name",
528 default="",
529 description="Filter collections by name",
530 update=lambda self, context:
531 CM_UL_items.new_collections.clear(),
534 use_filter_invert: BoolProperty(
535 name="Invert",
536 default=False,
537 description="Invert filtering (show hidden items, and vice-versa)",
540 filter_by_selected: BoolProperty(
541 name="Filter By Selected",
542 default=False,
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",
549 default=False,
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'
576 row = s1
578 # allow room to select the row from the beginning
579 row.separator()
581 # indent child items
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)
594 prop.expand = False
595 prop.name = item.name
596 prop.index = index
598 else:
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)
604 prop.expand = True
605 prop.name = item.name
606 prop.index = index
608 else:
609 row.label(icon='BLANK1')
612 # collection icon
613 c_icon = row.row()
614 highlight = False
615 if (context.view_layer.active_layer_collection == laycol["ptr"]):
616 highlight = True
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
624 if prefs.enable_qcd:
625 QCD = row.row()
626 QCD.scale_x = 0.4
627 QCD.prop(item, "qcd_slot_idx", text="")
629 # collection name
630 c_name = row.row(align=True)
632 #if rename[0] and index == cm.cm_list_index:
633 #c_name.activate_init = True
634 #rename[0] = False
636 c_name.prop(item, "name", text="", expand=True)
638 # set selection
639 setsel = c_name.row(align=True)
640 icon = 'DOT'
641 some_selected = False
643 if not collection.objects:
644 icon = 'BLANK1'
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
654 else:
655 all_selected = None
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
663 else:
664 all_unreachable = False
666 if obj.select_get() == False:
667 # some objects remain unselected
668 icon = 'KEYFRAME'
669 all_selected = False
671 else:
672 some_selected = True
674 if all_selected == False:
675 break
677 all_selected = True
680 if all_selected:
681 # all objects are selected
682 icon = 'KEYFRAME_HLT'
684 if all_unreachable:
685 if collection.objects:
686 icon = 'DOT'
688 setsel.active = False
691 prop = setsel.operator("view3d.select_collection_objects",
692 text="",
693 icon=icon,
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)
700 row.label()
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'
712 icon = 'IMPORT'
714 if collection.objects:
715 icon = 'MESH_CUBE'
717 if selected_objects:
718 if active_object and active_object.name in collection.objects:
719 icon = 'SNAP_VOLUME'
721 elif not selected_objects.isdisjoint(collection.objects):
722 icon = 'STICKY_UVS_LOC'
724 else:
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)
736 if cm.show_exclude:
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
780 'RESTRICT_VIEW_OFF')
782 prop = row.operator("view3d.disable_viewport_collection", text="", icon=icon,
783 emboss=highlight, depress=highlight)
784 prop.name = item.name
786 if cm.show_render:
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
799 if cm.show_holdout:
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
806 'HOLDOUT_OFF')
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
819 'INDIRECT_ONLY_OFF')
821 prop = row.operator("view3d.indirect_only_collection", text="", icon=icon,
822 emboss=highlight, depress=highlight)
823 prop.name = item.name
827 row = s2
829 row.separator()
830 row.separator()
832 rm_op = row.row()
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
857 if prefs.enable_qcd:
858 QCD.enabled = False
861 def draw_filter(self, context, layout):
862 row = layout.row()
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
877 flt_flags = []
878 flt_neworder = []
879 list_items = getattr(data, propname)
882 if self.filter_name:
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):
947 pass
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
963 layout = self.layout
965 panel_header = layout.row()
966 panel_header.alignment = 'CENTER'
967 panel_header.label(text="Display Options")
969 layout.separator()
971 section_header = layout.row()
972 section_header.alignment = 'LEFT'
973 section_header.label(text="Restriction Toggles")
975 row = layout.row()
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)
984 layout.separator()
986 section_header = layout.row()
987 section_header.label(text="Layout")
989 row = layout.row()
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):
998 layout = self.layout
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
1007 layout.separator()
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")
1021 layout.separator()
1023 layout.operator("view3d.enable_all_qcd_slots")
1024 layout.operator("view3d.enable_all_qcd_slots_isolated")
1026 layout.separator()
1028 layout.operator("view3d.isolate_selected_objects_collections")
1029 if context.mode == 'OBJECT':
1030 layout.operator("view3d.disable_selected_objects_collections")
1032 layout.separator()
1034 layout.operator("view3d.disable_all_non_qcd_slots")
1035 layout.operator("view3d.disable_all_collections")
1037 if context.mode == 'OBJECT':
1038 layout.separator()
1039 layout.operator("view3d.select_all_qcd_objects")
1041 layout.separator()
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
1051 idx = 1
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",
1060 text="",
1061 icon='HIDE_OFF',
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)
1069 row.scale_y = 0.5
1071 selected_objects = get_move_selection()
1072 active_object = get_move_active()
1074 for x in range(20):
1075 qcd_slot_name = internals.qcd_slots.get_name(str(x+1))
1077 if qcd_slot_name:
1078 qcd_laycol = internals.layer_collections[qcd_slot_name]["ptr"]
1079 collection_objects = qcd_laycol.collection.objects
1081 icon_value = 0
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):
1090 icon = 'LAYER_USED'
1092 # If there are objects use LAYER_USED
1093 elif collection_objects:
1094 icon = 'NONE'
1095 active_icon = get_active_icon(context, qcd_laycol)
1096 icon_value = active_icon.icon_id
1098 else:
1099 icon = 'BLANK1'
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)
1106 else:
1107 prop = row.operator("view3d.unassigned_qcd_slot", text="", icon='X', emboss=False)
1108 prop.slot = str(x+1)
1111 if idx%5==0:
1112 row.separator()
1114 if idx == 10:
1115 row = col.row(align=True)
1116 row.scale_y = 0.5
1118 idx += 1
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"]
1139 else:
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
1150 else:
1151 last_icon_theme_text_sel = theme_color.hsv
1153 return icon
1156 def update_icon(base, icon, theme_color):
1157 icon.icon_pixels = base.icon_pixels
1158 colored_icon = []
1160 for offset in range(len(icon.icon_pixels)):
1161 idx = offset * 4
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
1169 r = min(r+r*0.2,1)
1170 g = min(g+g*0.2,1)
1171 b = min(b+b*0.2,1)
1172 a = min(a+a*0.2,1)
1174 # make the icon follow the theme color (assuming the icon is white)
1175 r *= theme_color.r
1176 g *= theme_color.g
1177 b *= theme_color.b
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.
1196 import fnmatch
1198 if not pattern or not items: # Empty pattern or list = no filtering!
1199 return flags or []
1201 if flags is None:
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
1214 name = name.lower()
1216 # This is similar to a logical xor
1217 if bool(name and fnmatch.fnmatch(name, pattern)) is not bool(reverse):
1218 flags[i] |= bitflag
1220 # add in any recently created collections
1221 if item.name in CM_UL_items.new_collections:
1222 flags[i] |= bitflag
1224 return flags
1226 def merge_flt_flags(l1, l2):
1227 for idx, _ in enumerate(l1):
1228 l1[idx] &= l2.pop(0)
1230 return l1 + l2