Merge branch 'blender-v4.0-release'
[blender-addons.git] / object_collection_manager / qcd_operators.py
blobc492c62a214b2bb7b92cac04242d81f491b5e1e4
1 # SPDX-FileCopyrightText: 2011 Ryan Inch
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 import bpy
7 from bpy.types import (
8 Operator,
11 from bpy.props import (
12 BoolProperty,
13 StringProperty,
14 IntProperty
17 # For VARS
18 from . import internals
20 # For FUNCTIONS
21 from .internals import (
22 update_property_group,
23 generate_state,
24 get_modifiers,
25 get_move_selection,
26 get_move_active,
27 update_qcd_header,
30 from .operator_utils import (
31 mode_converter,
32 apply_to_children,
33 select_collection_objects,
34 set_exclude_state,
35 isolate_sel_objs_collections,
36 disable_sel_objs_collections,
39 class LockedObjects():
40 def __init__(self):
41 self.objs = []
42 self.mode = ""
44 def get_locked_objs(context):
45 # get objects not in object mode
46 locked = LockedObjects()
47 if context.mode == 'OBJECT':
48 return locked
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)
60 return locked
64 class QCDAllBase():
65 meta_op = False
66 meta_report = None
68 context = None
69 view_layer = ""
70 history = None
71 orig_active_collection = None
72 orig_active_object = None
73 locked = None
75 @classmethod
76 def init(cls, context):
77 cls.context = 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)
89 @classmethod
90 def apply_history(cls):
91 for x, item in enumerate(internals.layer_collections.values()):
92 item["ptr"].exclude = cls.history[x]
94 # clear rto history
95 del internals.qcd_history[cls.view_layer]
97 internals.qcd_collection_state.clear()
98 cls.history = None
100 @classmethod
101 def finalize(cls):
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:
113 if cls.locked.objs:
114 bpy.ops.object.mode_set(mode=cls.locked.mode)
116 @classmethod
117 def clear(cls):
118 cls.context = None
119 cls.view_layer = ""
120 cls.history = None
121 cls.orig_active_collection = None
122 cls.orig_active_object = None
123 cls.locked = 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"
131 @classmethod
132 def description(cls, context, properties):
133 hotkey_string = (
134 " * LMB - Enable all slots/Restore.\n"
135 " * Alt+LMB - Discard History.\n"
136 " * LMB+Hold - Menu"
139 return hotkey_string
141 def invoke(self, context, event):
142 qab = QCDAllBase
144 modifiers = get_modifiers(event)
146 qab.meta_op = True
148 if modifiers == {"alt"}:
149 bpy.ops.view3d.discard_qcd_history()
151 else:
152 qab.init(context)
154 if not qab.history:
155 bpy.ops.view3d.enable_all_qcd_slots()
157 else:
158 qab.apply_history()
159 qab.finalize()
162 if qab.meta_report:
163 self.report({"INFO"}, qab.meta_report)
164 qab.meta_report = None
166 qab.meta_op = False
169 return {'FINISHED'}
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):
179 qab = QCDAllBase
181 # validate qcd slots
182 if not dict(internals.qcd_slots):
183 if qab.meta_op:
184 qab.meta_report = "No QCD slots."
185 else:
186 self.report({"INFO"}, "No QCD slots.")
188 return {'CANCELLED'}
190 qab.init(context)
192 if not qab.history:
193 keep_history = False
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:
201 keep_history = True
202 set_exclude_state(laycol["ptr"], False)
205 if not keep_history:
206 # clear rto history
207 del internals.qcd_history[qab.view_layer]
208 qab.clear()
210 if qab.meta_op:
211 qab.meta_report = "All QCD slots are already enabled."
213 else:
214 self.report({"INFO"}, "All QCD slots are already enabled.")
216 return {'CANCELLED'}
218 internals.qcd_collection_state.clear()
219 internals.qcd_collection_state.update(internals.generate_state(qcd=True))
221 else:
222 qab.apply_history()
224 qab.finalize()
226 return {'FINISHED'}
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):
236 qab = QCDAllBase
238 # validate qcd slots
239 if not dict(internals.qcd_slots):
240 self.report({"INFO"}, "No QCD slots.")
242 return {'CANCELLED'}
244 qab.init(context)
246 if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
247 # clear rto history
248 del internals.qcd_history[qab.view_layer]
249 qab.clear()
251 self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
253 return {'CANCELLED'}
255 if not qab.history:
256 keep_history = False
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:
264 keep_history = True
265 set_exclude_state(laycol["ptr"], False)
267 if not is_qcd_slot and not laycol["ptr"].exclude:
268 keep_history = True
269 set_exclude_state(laycol["ptr"], True)
272 if not keep_history:
273 # clear rto history
274 del internals.qcd_history[qab.view_layer]
275 qab.clear()
277 self.report({"INFO"}, "All QCD slots are already enabled and isolated.")
278 return {'CANCELLED'}
280 internals.qcd_collection_state.clear()
281 internals.qcd_collection_state.update(internals.generate_state(qcd=True))
283 else:
284 qab.apply_history()
286 qab.finalize()
288 return {'FINISHED'}
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):
296 qab = QCDAllBase
297 qab.init(context)
299 use_active = bool(context.mode != 'OBJECT')
301 # isolate
302 error = isolate_sel_objs_collections(qab.view_layer, "exclude", "QCD", use_active=use_active)
304 if error:
305 qab.clear()
306 self.report({"WARNING"}, error)
307 return {'CANCELLED'}
309 qab.finalize()
311 internals.qcd_collection_state.clear()
312 internals.qcd_collection_state.update(internals.generate_state(qcd=True))
314 return {'FINISHED'}
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):
323 qab = QCDAllBase
324 qab.init(context)
326 if qab.locked.objs:
327 # clear rto history
328 del internals.qcd_history[qab.view_layer]
329 qab.clear()
331 self.report({"WARNING"}, "Can only be executed in Object Mode")
332 return {'CANCELLED'}
334 # disable
335 error = disable_sel_objs_collections(qab.view_layer, "exclude", "QCD")
337 if error:
338 qab.clear()
339 self.report({"WARNING"}, error)
340 return {'CANCELLED'}
342 qab.finalize()
344 internals.qcd_collection_state.clear()
345 internals.qcd_collection_state.update(internals.generate_state(qcd=True))
347 return {'FINISHED'}
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):
358 qab = QCDAllBase
360 # validate qcd slots
361 if not dict(internals.qcd_slots):
362 self.report({"INFO"}, "No QCD slots.")
364 return {'CANCELLED'}
366 qab.init(context)
368 if qab.locked.objs and not internals.qcd_slots.object_in_slots(qab.orig_active_object):
369 # clear rto history
370 del internals.qcd_history[qab.view_layer]
371 qab.clear()
373 self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
375 return {'CANCELLED'}
377 if not qab.history:
378 keep_history = False
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:
386 keep_history = True
387 set_exclude_state(laycol["ptr"], True)
389 if not keep_history:
390 # clear rto history
391 del internals.qcd_history[qab.view_layer]
392 qab.clear()
394 self.report({"INFO"}, "All non QCD slots are already disabled.")
395 return {'CANCELLED'}
397 internals.qcd_collection_state.clear()
398 internals.qcd_collection_state.update(internals.generate_state(qcd=True))
400 else:
401 qab.apply_history()
403 qab.finalize()
405 return {'FINISHED'}
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):
416 qab = QCDAllBase
418 qab.init(context)
420 if qab.locked.objs:
421 # clear rto history
422 del internals.qcd_history[qab.view_layer]
423 qab.clear()
425 self.report({"WARNING"}, "Cannot execute. The active object would be lost.")
427 return {'CANCELLED'}
429 if not qab.history:
430 for laycol in internals.layer_collections.values():
432 qab.history.append(laycol["ptr"].exclude)
434 if all(qab.history): # no collections are enabled
435 # clear rto history
436 del internals.qcd_history[qab.view_layer]
437 qab.clear()
439 self.report({"INFO"}, "All collections are already disabled.")
440 return {'CANCELLED'}
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))
448 else:
449 qab.apply_history()
451 qab.finalize()
453 return {'FINISHED'}
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):
464 qab = QCDAllBase
466 if context.mode != 'OBJECT':
467 self.report({"WARNING"}, "Can only be executed in Object Mode")
468 return {'CANCELLED'}
470 if not context.selectable_objects:
471 if qab.meta_op:
472 qab.meta_report = "No objects present to select."
474 else:
475 self.report({"INFO"}, "No objects present to select.")
477 return {'CANCELLED'}
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,
487 replace=False,
488 nested=False,
489 selection_state=True
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,
497 replace=False,
498 nested=False,
499 selection_state=False
503 return {'FINISHED'}
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):
512 qab = QCDAllBase
514 view_layer = context.view_layer.name
516 if view_layer in internals.qcd_history:
517 del internals.qcd_history[view_layer]
518 qab.clear()
520 # update header UI
521 update_qcd_header()
523 return {'FINISHED'}
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
541 qcd_laycol = None
542 slot_name = internals.qcd_slots.get_name(self.slot)
544 if slot_name:
545 qcd_laycol = internals.layer_collections[slot_name]["ptr"]
547 else:
548 return {'CANCELLED'}
551 if not selected_objects:
552 return {'CANCELLED'}
554 # adds object to slot
555 if self.toggle:
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)
564 else:
565 for obj in selected_objects:
566 if obj.name in qcd_laycol.collection.objects:
568 if len(obj.users_collection) == 1:
569 continue
571 qcd_laycol.collection.objects.unlink(obj)
574 # moves object to slot
575 else:
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:
588 try:
589 context.view_layer.objects.active = active_object
591 except RuntimeError: # object not in visible slot
592 pass
594 # update header UI
595 update_qcd_header()
597 return {'FINISHED'}
600 class ViewMoveQCDSlot(Operator):
601 bl_label = ""
602 bl_idname = "view3d.view_move_qcd_slot"
603 bl_options = {'REGISTER', 'UNDO'}
605 slot: StringProperty()
607 @classmethod
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 = (
615 ".\n"
616 " * Alt+LMB - Select objects in slot.\n"
617 " * Alt+Shift+LMB - Toggle objects' selection for slot"
620 hotkey_string = (
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"
625 + selection_hotkeys
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),
646 replace=True,
647 nested=False
650 elif modifiers == {"alt", "shift"}:
651 select_collection_objects(
652 is_master_collection=False,
653 collection_name=internals.qcd_slots.get_name(self.slot),
654 replace=False,
655 nested=False
658 else:
659 bpy.ops.view3d.view_qcd_slot(slot=self.slot, toggle=False)
661 return {'FINISHED'}
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):
673 qcd_laycol = None
674 slot_name = internals.qcd_slots.get_name(self.slot)
676 if slot_name:
677 qcd_laycol = internals.layer_collections[slot_name]["ptr"]
679 else:
680 return {'CANCELLED'}
683 orig_active_object = context.view_layer.objects.active
684 locked = get_locked_objs(context)
687 if self.toggle:
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):
691 return {'CANCELLED'}
693 # toggle exclusion of qcd_laycol
694 set_exclude_state(qcd_laycol, not qcd_laycol.exclude)
696 else:
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
703 else:
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
714 else:
715 layer_collection.exclude = False
717 apply_to_children(qcd_laycol, exclude_all_children)
719 if orig_active_object:
720 try:
721 if orig_active_object.name in context.view_layer.objects:
722 context.view_layer.objects.active = orig_active_object
723 except RuntimeError:
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.
727 pass
729 # restore locked objects back to their original mode
730 # needed because of exclude child updates
731 if context.view_layer.objects.active:
732 if locked.objs:
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
739 # update header UI
740 update_qcd_header()
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]
749 return {'FINISHED'}
752 class UnassignedQCDSlot(Operator):
753 bl_label = ""
754 bl_idname = "view3d.unassigned_qcd_slot"
755 bl_options = {'REGISTER', 'UNDO'}
757 slot: StringProperty()
759 @classmethod
760 def description(cls, context, properties):
761 slot_string = f"Unassigned QCD Slot {properties.slot}:\n"
763 hotkey_string = (
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)
791 else:
792 pass
794 return {'FINISHED'}
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)
813 return {'FINISHED'}
816 class RenumerateQCDSlots(Operator):
817 bl_label = "Renumber QCD Slots"
818 bl_description = (
819 "Renumber QCD slots.\n"
820 " * LMB - Renumber (breadth first) from slot 1.\n"
821 " * +Ctrl - Linear.\n"
822 " * +Alt - Reset.\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)
831 beginning = False
832 depth_first = False
833 constrain = False
835 if 'alt' in modifiers:
836 beginning=True
838 if 'ctrl' in modifiers:
839 depth_first=True
841 if 'shift' in modifiers:
842 constrain=True
844 internals.qcd_slots.renumerate(beginning=beginning,
845 depth_first=depth_first,
846 constrain=constrain)
848 update_property_group(context)
850 return {'FINISHED'}