1 # SPDX-FileCopyrightText: 2011 Ryan Inch
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import (
11 from bpy
.props
import (
18 from . import internals
21 from .internals
import (
22 update_property_group
,
30 from .operator_utils
import (
33 select_collection_objects
,
35 isolate_sel_objs_collections
,
36 disable_sel_objs_collections
,
39 class LockedObjects():
44 def get_locked_objs(context
):
45 # get objects not in object mode
46 locked
= LockedObjects()
47 if context
.mode
== 'OBJECT':
50 if context
.view_layer
.objects
.active
:
51 active
= context
.view_layer
.objects
.active
52 locked
.mode
= mode_converter
[context
.mode
]
54 for obj
in context
.view_layer
.objects
:
55 if obj
.mode
!= 'OBJECT':
56 if obj
.mode
not in ['POSE', 'WEIGHT_PAINT'] or obj
== active
:
57 if obj
.mode
== active
.mode
:
58 locked
.objs
.append(obj
)
71 orig_active_collection
= None
72 orig_active_object
= None
76 def init(cls
, context
):
78 cls
.orig_active_collection
= context
.view_layer
.active_layer_collection
79 cls
.view_layer
= context
.view_layer
.name
80 cls
.orig_active_object
= context
.view_layer
.objects
.active
82 if not cls
.view_layer
in internals
.qcd_history
:
83 internals
.qcd_history
[cls
.view_layer
] = []
85 cls
.history
= internals
.qcd_history
[cls
.view_layer
]
87 cls
.locked
= get_locked_objs(context
)
90 def apply_history(cls
):
91 for x
, item
in enumerate(internals
.layer_collections
.values()):
92 item
["ptr"].exclude
= cls
.history
[x
]
95 del internals
.qcd_history
[cls
.view_layer
]
97 internals
.qcd_collection_state
.clear()
102 # restore active collection
103 cls
.context
.view_layer
.active_layer_collection
= cls
.orig_active_collection
105 # restore active object if possible
106 if cls
.orig_active_object
:
107 if cls
.orig_active_object
.name
in cls
.context
.view_layer
.objects
:
108 cls
.context
.view_layer
.objects
.active
= cls
.orig_active_object
110 # restore locked objects back to their original mode
111 # needed because of exclude child updates
112 if cls
.context
.view_layer
.objects
.active
:
114 bpy
.ops
.object.mode_set(mode
=cls
.locked
.mode
)
121 cls
.orig_active_collection
= None
122 cls
.orig_active_object
= None
126 class EnableAllQCDSlotsMeta(Operator
):
127 '''QCD All Meta Operator'''
128 bl_label
= "Quick View Toggles"
129 bl_idname
= "view3d.enable_all_qcd_slots_meta"
132 def description(cls
, context
, properties
):
134 " * LMB - Enable all slots/Restore.\n"
135 " * Alt+LMB - Discard History.\n"
141 def invoke(self
, context
, event
):
144 modifiers
= get_modifiers(event
)
148 if modifiers
== {"alt"}:
149 bpy
.ops
.view3d
.discard_qcd_history()
155 bpy
.ops
.view3d
.enable_all_qcd_slots()
163 self
.report({"INFO"}, qab
.meta_report
)
164 qab
.meta_report
= None
172 class EnableAllQCDSlots(Operator
):
173 '''Toggles between the current state and all enabled'''
174 bl_label
= "Enable All QCD Slots"
175 bl_idname
= "view3d.enable_all_qcd_slots"
176 bl_options
= {'REGISTER', 'UNDO'}
178 def execute(self
, context
):
182 if not dict(internals
.qcd_slots
):
184 qab
.meta_report
= "No QCD slots."
186 self
.report({"INFO"}, "No QCD slots.")
195 for laycol
in internals
.layer_collections
.values():
196 is_qcd_slot
= internals
.qcd_slots
.contains(name
=laycol
["name"])
198 qab
.history
.append(laycol
["ptr"].exclude
)
200 if is_qcd_slot
and laycol
["ptr"].exclude
:
202 set_exclude_state(laycol
["ptr"], False)
207 del internals
.qcd_history
[qab
.view_layer
]
211 qab
.meta_report
= "All QCD slots are already enabled."
214 self
.report({"INFO"}, "All QCD slots are already enabled.")
218 internals
.qcd_collection_state
.clear()
219 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
229 class EnableAllQCDSlotsIsolated(Operator
):
230 '''Toggles between the current state and all enabled (non-QCD collections disabled)'''
231 bl_label
= "Enable All QCD Slots Isolated"
232 bl_idname
= "view3d.enable_all_qcd_slots_isolated"
233 bl_options
= {'REGISTER', 'UNDO'}
235 def execute(self
, context
):
239 if not dict(internals
.qcd_slots
):
240 self
.report({"INFO"}, "No QCD slots.")
246 if qab
.locked
.objs
and not internals
.qcd_slots
.object_in_slots(qab
.orig_active_object
):
248 del internals
.qcd_history
[qab
.view_layer
]
251 self
.report({"WARNING"}, "Cannot execute. The active object would be lost.")
258 for laycol
in internals
.layer_collections
.values():
259 is_qcd_slot
= internals
.qcd_slots
.contains(name
=laycol
["name"])
261 qab
.history
.append(laycol
["ptr"].exclude
)
263 if is_qcd_slot
and laycol
["ptr"].exclude
:
265 set_exclude_state(laycol
["ptr"], False)
267 if not is_qcd_slot
and not laycol
["ptr"].exclude
:
269 set_exclude_state(laycol
["ptr"], True)
274 del internals
.qcd_history
[qab
.view_layer
]
277 self
.report({"INFO"}, "All QCD slots are already enabled and isolated.")
280 internals
.qcd_collection_state
.clear()
281 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
290 class IsolateSelectedObjectsCollections(Operator
):
291 '''Isolate collections (via EC) that contain the selected objects'''
292 bl_label
= "Isolate Selected Objects Collections"
293 bl_idname
= "view3d.isolate_selected_objects_collections"
295 def execute(self
, context
):
299 use_active
= bool(context
.mode
!= 'OBJECT')
302 error
= isolate_sel_objs_collections(qab
.view_layer
, "exclude", "QCD", use_active
=use_active
)
306 self
.report({"WARNING"}, error
)
311 internals
.qcd_collection_state
.clear()
312 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
317 class DisableSelectedObjectsCollections(Operator
):
318 '''Disable all collections that contain the selected objects'''
319 bl_label
= "Disable Selected Objects Collections"
320 bl_idname
= "view3d.disable_selected_objects_collections"
322 def execute(self
, context
):
328 del internals
.qcd_history
[qab
.view_layer
]
331 self
.report({"WARNING"}, "Can only be executed in Object Mode")
335 error
= disable_sel_objs_collections(qab
.view_layer
, "exclude", "QCD")
339 self
.report({"WARNING"}, error
)
344 internals
.qcd_collection_state
.clear()
345 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
350 class DisableAllNonQCDSlots(Operator
):
351 '''Toggles between the current state and all non-QCD collections disabled'''
352 bl_label
= "Disable All Non QCD Slots"
353 bl_idname
= "view3d.disable_all_non_qcd_slots"
354 bl_options
= {'REGISTER', 'UNDO'}
357 def execute(self
, context
):
361 if not dict(internals
.qcd_slots
):
362 self
.report({"INFO"}, "No QCD slots.")
368 if qab
.locked
.objs
and not internals
.qcd_slots
.object_in_slots(qab
.orig_active_object
):
370 del internals
.qcd_history
[qab
.view_layer
]
373 self
.report({"WARNING"}, "Cannot execute. The active object would be lost.")
380 for laycol
in internals
.layer_collections
.values():
381 is_qcd_slot
= internals
.qcd_slots
.contains(name
=laycol
["name"])
383 qab
.history
.append(laycol
["ptr"].exclude
)
385 if not is_qcd_slot
and not laycol
["ptr"].exclude
:
387 set_exclude_state(laycol
["ptr"], True)
391 del internals
.qcd_history
[qab
.view_layer
]
394 self
.report({"INFO"}, "All non QCD slots are already disabled.")
397 internals
.qcd_collection_state
.clear()
398 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
408 class DisableAllCollections(Operator
):
409 '''Toggles between the current state and all collections disabled'''
410 bl_label
= "Disable All collections"
411 bl_idname
= "view3d.disable_all_collections"
412 bl_options
= {'REGISTER', 'UNDO'}
415 def execute(self
, context
):
422 del internals
.qcd_history
[qab
.view_layer
]
425 self
.report({"WARNING"}, "Cannot execute. The active object would be lost.")
430 for laycol
in internals
.layer_collections
.values():
432 qab
.history
.append(laycol
["ptr"].exclude
)
434 if all(qab
.history
): # no collections are enabled
436 del internals
.qcd_history
[qab
.view_layer
]
439 self
.report({"INFO"}, "All collections are already disabled.")
442 for laycol
in internals
.layer_collections
.values():
443 laycol
["ptr"].exclude
= True
445 internals
.qcd_collection_state
.clear()
446 internals
.qcd_collection_state
.update(internals
.generate_state(qcd
=True))
456 class SelectAllQCDObjects(Operator
):
457 '''Select all objects in QCD slots'''
458 bl_label
= "Select All QCD Objects"
459 bl_idname
= "view3d.select_all_qcd_objects"
460 bl_options
= {'REGISTER', 'UNDO'}
463 def execute(self
, context
):
466 if context
.mode
!= 'OBJECT':
467 self
.report({"WARNING"}, "Can only be executed in Object Mode")
470 if not context
.selectable_objects
:
472 qab
.meta_report
= "No objects present to select."
475 self
.report({"INFO"}, "No objects present to select.")
479 orig_selected_objects
= context
.selected_objects
481 bpy
.ops
.object.select_all(action
='DESELECT')
483 for slot
, collection_name
in internals
.qcd_slots
:
484 select_collection_objects(
485 is_master_collection
=False,
486 collection_name
=collection_name
,
492 if context
.selected_objects
== orig_selected_objects
:
493 for slot
, collection_name
in internals
.qcd_slots
:
494 select_collection_objects(
495 is_master_collection
=False,
496 collection_name
=collection_name
,
499 selection_state
=False
506 class DiscardQCDHistory(Operator
):
507 '''Discard QCD History'''
508 bl_label
= "Discard History"
509 bl_idname
= "view3d.discard_qcd_history"
511 def execute(self
, context
):
514 view_layer
= context
.view_layer
.name
516 if view_layer
in internals
.qcd_history
:
517 del internals
.qcd_history
[view_layer
]
526 class MoveToQCDSlot(Operator
):
527 '''Move object(s) to QCD slot'''
528 bl_label
= "Move To QCD Slot"
529 bl_idname
= "view3d.move_to_qcd_slot"
530 bl_options
= {'REGISTER', 'UNDO'}
532 slot
: StringProperty()
533 toggle
: BoolProperty()
535 def execute(self
, context
):
537 selected_objects
= get_move_selection()
538 active_object
= get_move_active()
539 internals
.move_triggered
= True
542 slot_name
= internals
.qcd_slots
.get_name(self
.slot
)
545 qcd_laycol
= internals
.layer_collections
[slot_name
]["ptr"]
551 if not selected_objects
:
554 # adds object to slot
556 if not active_object
:
557 active_object
= tuple(selected_objects
)[0]
559 if not active_object
.name
in qcd_laycol
.collection
.objects
:
560 for obj
in selected_objects
:
561 if obj
.name
not in qcd_laycol
.collection
.objects
:
562 qcd_laycol
.collection
.objects
.link(obj
)
565 for obj
in selected_objects
:
566 if obj
.name
in qcd_laycol
.collection
.objects
:
568 if len(obj
.users_collection
) == 1:
571 qcd_laycol
.collection
.objects
.unlink(obj
)
574 # moves object to slot
576 for obj
in selected_objects
:
577 if obj
.name
not in qcd_laycol
.collection
.objects
:
578 qcd_laycol
.collection
.objects
.link(obj
)
580 for collection
in obj
.users_collection
:
581 qcd_idx
= internals
.qcd_slots
.get_idx(collection
.name
)
582 if qcd_idx
!= self
.slot
:
583 collection
.objects
.unlink(obj
)
586 # update the active object if needed
587 if not context
.active_object
:
589 context
.view_layer
.objects
.active
= active_object
591 except RuntimeError: # object not in visible slot
600 class ViewMoveQCDSlot(Operator
):
602 bl_idname
= "view3d.view_move_qcd_slot"
603 bl_options
= {'REGISTER', 'UNDO'}
605 slot
: StringProperty()
608 def description(cls
, context
, properties
):
609 slot_name
= internals
.qcd_slots
.get_name(properties
.slot
)
610 slot_string
= f
"QCD Slot {properties.slot}: \"{slot_name}\"\n"
611 selection_hotkeys
= ""
613 if context
.mode
== 'OBJECT':
614 selection_hotkeys
= (
616 " * Alt+LMB - Select objects in slot.\n"
617 " * Alt+Shift+LMB - Toggle objects' selection for slot"
621 " * LMB - Isolate slot.\n"
622 " * Shift+LMB - Toggle slot.\n"
623 " * Ctrl+LMB - Move objects to slot.\n"
624 " * Ctrl+Shift+LMB - Toggle objects' slot"
628 return f
"{slot_string}{hotkey_string}"
630 def invoke(self
, context
, event
):
631 modifiers
= get_modifiers(event
)
633 if modifiers
== {"shift"}:
634 bpy
.ops
.view3d
.view_qcd_slot(slot
=self
.slot
, toggle
=True)
636 elif modifiers
== {"ctrl"}:
637 bpy
.ops
.view3d
.move_to_qcd_slot(slot
=self
.slot
, toggle
=False)
639 elif modifiers
== {"ctrl", "shift"}:
640 bpy
.ops
.view3d
.move_to_qcd_slot(slot
=self
.slot
, toggle
=True)
642 elif modifiers
== {"alt"}:
643 select_collection_objects(
644 is_master_collection
=False,
645 collection_name
=internals
.qcd_slots
.get_name(self
.slot
),
650 elif modifiers
== {"alt", "shift"}:
651 select_collection_objects(
652 is_master_collection
=False,
653 collection_name
=internals
.qcd_slots
.get_name(self
.slot
),
659 bpy
.ops
.view3d
.view_qcd_slot(slot
=self
.slot
, toggle
=False)
663 class ViewQCDSlot(Operator
):
664 '''View objects in QCD slot'''
665 bl_label
= "View QCD Slot"
666 bl_idname
= "view3d.view_qcd_slot"
667 bl_options
= {'UNDO'}
669 slot
: StringProperty()
670 toggle
: BoolProperty()
672 def execute(self
, context
):
674 slot_name
= internals
.qcd_slots
.get_name(self
.slot
)
677 qcd_laycol
= internals
.layer_collections
[slot_name
]["ptr"]
683 orig_active_object
= context
.view_layer
.objects
.active
684 locked
= get_locked_objs(context
)
688 # check if slot can be toggled off.
689 if not qcd_laycol
.exclude
:
690 if not set(locked
.objs
).isdisjoint(qcd_laycol
.collection
.objects
):
693 # toggle exclusion of qcd_laycol
694 set_exclude_state(qcd_laycol
, not qcd_laycol
.exclude
)
697 # exclude all collections
698 for laycol
in internals
.layer_collections
.values():
699 if laycol
["name"] != qcd_laycol
.name
:
700 # prevent exclusion if locked objects in this collection
701 if set(locked
.objs
).isdisjoint(laycol
["ptr"].collection
.objects
):
702 laycol
["ptr"].exclude
= True
704 laycol
["ptr"].exclude
= False
706 # un-exclude target collection
707 qcd_laycol
.exclude
= False
709 # exclude all children
710 def exclude_all_children(layer_collection
):
711 # prevent exclusion if locked objects in this collection
712 if set(locked
.objs
).isdisjoint(layer_collection
.collection
.objects
):
713 layer_collection
.exclude
= True
715 layer_collection
.exclude
= False
717 apply_to_children(qcd_laycol
, exclude_all_children
)
719 if orig_active_object
:
721 if orig_active_object
.name
in context
.view_layer
.objects
:
722 context
.view_layer
.objects
.active
= orig_active_object
724 # Blender appears to have a race condition here for versions 3.4+,
725 # so if the active object is no longer in the view layer when
726 # attempting to set it just do nothing.
729 # restore locked objects back to their original mode
730 # needed because of exclude child updates
731 if context
.view_layer
.objects
.active
:
733 bpy
.ops
.object.mode_set(mode
=locked
.mode
)
735 # set layer as active layer collection
736 context
.view_layer
.active_layer_collection
= qcd_laycol
742 view_layer
= context
.view_layer
.name
743 if view_layer
in internals
.rto_history
["exclude"]:
744 del internals
.rto_history
["exclude"][view_layer
]
745 if view_layer
in internals
.rto_history
["exclude_all"]:
746 del internals
.rto_history
["exclude_all"][view_layer
]
752 class UnassignedQCDSlot(Operator
):
754 bl_idname
= "view3d.unassigned_qcd_slot"
755 bl_options
= {'REGISTER', 'UNDO'}
757 slot
: StringProperty()
760 def description(cls
, context
, properties
):
761 slot_string
= f
"Unassigned QCD Slot {properties.slot}:\n"
764 " * LMB - Create slot.\n"
765 " * Shift+LMB - Create and isolate slot.\n"
766 " * Ctrl+LMB - Create and move objects to slot.\n"
767 " * Ctrl+Shift+LMB - Create and add objects to slot"
770 return f
"{slot_string}{hotkey_string}"
772 def invoke(self
, context
, event
):
773 modifiers
= get_modifiers(event
)
775 new_collection
= bpy
.data
.collections
.new(f
"Collection {self.slot}")
776 context
.scene
.collection
.children
.link(new_collection
)
777 internals
.qcd_slots
.add_slot(f
"{self.slot}", new_collection
.name
)
779 # update tree view property
780 update_property_group(context
)
782 if modifiers
== {"shift"}:
783 bpy
.ops
.view3d
.view_qcd_slot(slot
=self
.slot
, toggle
=False)
785 elif modifiers
== {"ctrl"}:
786 bpy
.ops
.view3d
.move_to_qcd_slot(slot
=self
.slot
, toggle
=False)
788 elif modifiers
== {"ctrl", "shift"}:
789 bpy
.ops
.view3d
.move_to_qcd_slot(slot
=self
.slot
, toggle
=True)
797 class CreateAllQCDSlots(Operator
):
798 bl_label
= "Create All QCD Slots"
799 bl_description
= "Create any missing QCD slots so you have a full 20"
800 bl_idname
= "view3d.create_all_qcd_slots"
801 bl_options
= {'REGISTER', 'UNDO'}
803 def execute(self
, context
):
804 for slot_number
in range(1, 21):
805 if not internals
.qcd_slots
.get_name(f
"{slot_number}"):
806 new_collection
= bpy
.data
.collections
.new(f
"Collection {slot_number}")
807 context
.scene
.collection
.children
.link(new_collection
)
808 internals
.qcd_slots
.add_slot(f
"{slot_number}", new_collection
.name
)
810 # update tree view property
811 update_property_group(context
)
816 class RenumerateQCDSlots(Operator
):
817 bl_label
= "Renumber QCD Slots"
819 "Renumber QCD slots.\n"
820 " * LMB - Renumber (breadth first) from slot 1.\n"
821 " * +Ctrl - Linear.\n"
823 " * +Shift - Constrain to branch"
825 bl_idname
= "view3d.renumerate_qcd_slots"
826 bl_options
= {'REGISTER', 'UNDO'}
828 def invoke(self
, context
, event
):
829 modifiers
= get_modifiers(event
)
835 if 'alt' in modifiers
:
838 if 'ctrl' in modifiers
:
841 if 'shift' in modifiers
:
844 internals
.qcd_slots
.renumerate(beginning
=beginning
,
845 depth_first
=depth_first
,
848 update_property_group(context
)