AnimAll: rename the "Animate" tab back to "Animation"
[blender-addons.git] / object_collection_manager / internals.py
blob5bd2d732f0443c21a536c7ec468f94d0140175bd
1 # SPDX-FileCopyrightText: 2011 Ryan Inch
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 from . import persistent_data
7 import bpy
9 from bpy.types import (
10 PropertyGroup,
11 Operator,
14 from bpy.props import (
15 StringProperty,
16 IntProperty,
19 move_triggered = False
20 move_selection = []
21 move_active = None
23 layer_collections = {}
24 collection_tree = []
25 collection_state = {}
26 qcd_collection_state = {}
27 expanded = set()
28 row_index = 0
29 max_lvl = 0
31 rto_history = {
32 "exclude": {},
33 "exclude_all": {},
34 "select": {},
35 "select_all": {},
36 "hide": {},
37 "hide_all": {},
38 "disable": {},
39 "disable_all": {},
40 "render": {},
41 "render_all": {},
42 "holdout": {},
43 "holdout_all": {},
44 "indirect": {},
45 "indirect_all": {},
48 qcd_history = {}
50 expand_history = {
51 "target": "",
52 "history": [],
55 phantom_history = {
56 "view_layer": "",
57 "initial_state": {},
59 "exclude_history": {},
60 "select_history": {},
61 "hide_history": {},
62 "disable_history": {},
63 "render_history": {},
64 "holdout_history": {},
65 "indirect_history": {},
67 "exclude_all_history": [],
68 "select_all_history": [],
69 "hide_all_history": [],
70 "disable_all_history": [],
71 "render_all_history": [],
72 "holdout_all_history": [],
73 "indirect_all_history": [],
76 copy_buffer = {
77 "RTO": "",
78 "values": []
81 swap_buffer = {
82 "A": {
83 "RTO": "",
84 "values": []
86 "B": {
87 "RTO": "",
88 "values": []
93 class QCDSlots():
94 _slots = {}
95 overrides = set()
96 allow_update = True
98 def __init__(self):
99 self._slots = persistent_data.slots
100 self.overrides = persistent_data.overrides
102 def __iter__(self):
103 return self._slots.items().__iter__()
105 def __repr__(self):
106 return self._slots.__repr__()
108 def contains(self, *, idx=None, name=None):
109 if idx:
110 return idx in self._slots.keys()
112 if name:
113 return name in self._slots.values()
115 raise
117 def object_in_slots(self, obj):
118 for collection in obj.users_collection:
119 if self.contains(name=collection.name):
120 return True
122 return False
124 def get_data_for_blend(self):
125 return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}"
127 def load_blend_data(self, data):
128 decoupled_data = data.split("\n")
129 blend_slots = eval(decoupled_data[0])
130 blend_overrides = eval(decoupled_data[1])
132 self._slots.clear()
133 self.overrides.clear()
135 for key, value in blend_slots.items():
136 self._slots[key] = value
138 for key in blend_overrides:
139 self.overrides.add(key)
141 def length(self):
142 return len(self._slots)
144 def get_idx(self, name, r_value=None):
145 for idx, slot_name in self._slots.items():
146 if slot_name == name:
147 return idx
149 return r_value
151 def get_name(self, idx, r_value=None):
152 if idx in self._slots:
153 return self._slots[idx]
155 return r_value
157 def add_slot(self, idx, name):
158 self._slots[idx] = name
160 if name in self.overrides:
161 self.overrides.remove(name)
163 def update_slot(self, idx, name):
164 self.add_slot(idx, name)
166 def del_slot(self, *, idx=None, name=None):
167 if idx and not name:
168 del self._slots[idx]
169 return
171 if name and not idx:
172 slot_idx = self.get_idx(name)
173 del self._slots[slot_idx]
174 return
176 raise
178 def add_override(self, name):
179 qcd_slots.del_slot(name=name)
180 qcd_slots.overrides.add(name)
182 def clear_slots(self):
183 self._slots.clear()
185 def update_qcd(self):
186 for idx, name in list(self._slots.items()):
187 if not layer_collections.get(name, None):
188 qcd_slots.del_slot(name=name)
190 def auto_numerate(self):
191 if self.length() < 20:
192 laycol = bpy.context.view_layer.layer_collection
194 laycol_iter_list = list(laycol.children)
195 while laycol_iter_list:
196 layer_collection = laycol_iter_list.pop(0)
197 laycol_iter_list.extend(list(layer_collection.children))
199 if layer_collection.name in qcd_slots.overrides:
200 continue
202 for x in range(20):
203 if (not self.contains(idx=str(x+1)) and
204 not self.contains(name=layer_collection.name)):
205 self.add_slot(str(x+1), layer_collection.name)
208 if self.length() > 20:
209 break
211 def renumerate(self, *, beginning=False, depth_first=False, constrain=False):
212 if beginning:
213 self.clear_slots()
214 self.overrides.clear()
216 starting_laycol_name = self.get_name("1")
218 if not starting_laycol_name:
219 laycol = bpy.context.view_layer.layer_collection
220 starting_laycol_name = laycol.children[0].name
222 self.clear_slots()
223 self.overrides.clear()
225 if depth_first:
226 parent = layer_collections[starting_laycol_name]["parent"]
227 x = 1
229 for laycol in layer_collections.values():
230 if self.length() == 0 and starting_laycol_name != laycol["name"]:
231 continue
233 if constrain:
234 if self.length():
235 if laycol["parent"]["name"] == parent["name"]:
236 break
238 self.add_slot(f"{x}", laycol["name"])
240 x += 1
242 if self.length() == 20:
243 break
245 else:
246 laycol = layer_collections[starting_laycol_name]["parent"]["ptr"]
248 laycol_iter_list = []
249 for laycol in laycol.children:
250 if laycol.name == starting_laycol_name:
251 laycol_iter_list.append(laycol)
253 elif not constrain and laycol_iter_list:
254 laycol_iter_list.append(laycol)
256 x = 1
257 while laycol_iter_list:
258 layer_collection = laycol_iter_list.pop(0)
260 self.add_slot(f"{x}", layer_collection.name)
262 laycol_iter_list.extend(list(layer_collection.children))
264 x += 1
266 if self.length() == 20:
267 break
270 for laycol in layer_collections.values():
271 if not self.contains(name=laycol["name"]):
272 self.overrides.add(laycol["name"])
274 qcd_slots = QCDSlots()
277 def update_col_name(self, context):
278 global layer_collections
279 global qcd_slots
280 global rto_history
281 global expand_history
283 if self.name != self.last_name:
284 if self.name == '':
285 self.name = self.last_name
286 return
288 # if statement prevents update on list creation
289 if self.last_name != '':
290 view_layer_name = context.view_layer.name
292 # update collection name
293 layer_collections[self.last_name]["ptr"].collection.name = self.name
295 # update expanded
296 orig_expanded = {x for x in expanded}
298 if self.last_name in orig_expanded:
299 expanded.remove(self.last_name)
300 expanded.add(self.name)
302 # update qcd_slot
303 idx = qcd_slots.get_idx(self.last_name)
304 if idx:
305 qcd_slots.update_slot(idx, self.name)
307 # update qcd_overrides
308 if self.last_name in qcd_slots.overrides:
309 qcd_slots.overrides.remove(self.last_name)
310 qcd_slots.overrides.add(self.name)
312 # update history
313 rtos = [
314 "exclude",
315 "select",
316 "hide",
317 "disable",
318 "render",
319 "holdout",
320 "indirect",
323 orig_targets = {
324 rto: rto_history[rto][view_layer_name]["target"]
325 for rto in rtos
326 if rto_history[rto].get(view_layer_name)
329 for rto in rtos:
330 history = rto_history[rto].get(view_layer_name)
332 if history and orig_targets[rto] == self.last_name:
333 history["target"] = self.name
335 # update expand history
336 orig_expand_target = expand_history["target"]
337 orig_expand_history = [x for x in expand_history["history"]]
339 if orig_expand_target == self.last_name:
340 expand_history["target"] = self.name
342 for x, name in enumerate(orig_expand_history):
343 if name == self.last_name:
344 expand_history["history"][x] = self.name
346 # update names in expanded, qcd slots, and rto_history for any other
347 # collection names that changed as a result of this name change
348 cm_list_collection = context.scene.collection_manager.cm_list_collection
349 count = 0
350 laycol_iter_list = list(context.view_layer.layer_collection.children)
352 while laycol_iter_list:
353 layer_collection = laycol_iter_list[0]
354 cm_list_item = cm_list_collection[count]
356 if cm_list_item.name != layer_collection.name:
357 # update expanded
358 if cm_list_item.last_name in orig_expanded:
359 if not cm_list_item.last_name in layer_collections:
360 expanded.remove(cm_list_item.name)
362 expanded.add(layer_collection.name)
364 # update qcd_slot
365 idx = cm_list_item.qcd_slot_idx
366 if idx:
367 qcd_slots.update_slot(idx, layer_collection.name)
369 # update qcd_overrides
370 if cm_list_item.name in qcd_slots.overrides:
371 if not cm_list_item.name in layer_collections:
372 qcd_slots.overrides.remove(cm_list_item.name)
374 qcd_slots.overrides.add(layer_collection.name)
376 # update history
377 for rto in rtos:
378 history = rto_history[rto].get(view_layer_name)
380 if history and orig_targets[rto] == cm_list_item.last_name:
381 history["target"] = layer_collection.name
383 # update expand history
384 if orig_expand_target == cm_list_item.last_name:
385 expand_history["target"] = layer_collection.name
387 for x, name in enumerate(orig_expand_history):
388 if name == cm_list_item.last_name:
389 expand_history["history"][x] = layer_collection.name
391 if layer_collection.children:
392 laycol_iter_list[0:0] = list(layer_collection.children)
395 laycol_iter_list.remove(layer_collection)
396 count += 1
399 update_property_group(context)
402 self.last_name = self.name
405 def update_qcd_slot(self, context):
406 global qcd_slots
408 if not qcd_slots.allow_update:
409 return
411 update_needed = False
413 try:
414 int(self.qcd_slot_idx)
416 except ValueError:
417 if self.qcd_slot_idx == "":
418 qcd_slots.add_override(self.name)
420 if qcd_slots.contains(name=self.name):
421 qcd_slots.allow_update = False
422 self.qcd_slot_idx = qcd_slots.get_idx(self.name)
423 qcd_slots.allow_update = True
425 if self.name in qcd_slots.overrides:
426 qcd_slots.allow_update = False
427 self.qcd_slot_idx = ""
428 qcd_slots.allow_update = True
430 return
432 if qcd_slots.contains(name=self.name):
433 qcd_slots.del_slot(name=self.name)
434 update_needed = True
436 if qcd_slots.contains(idx=self.qcd_slot_idx):
437 qcd_slots.add_override(qcd_slots.get_name(self.qcd_slot_idx))
438 update_needed = True
440 if int(self.qcd_slot_idx) > 20:
441 self.qcd_slot_idx = "20"
443 if int(self.qcd_slot_idx) < 1:
444 self.qcd_slot_idx = "1"
446 qcd_slots.add_slot(self.qcd_slot_idx, self.name)
448 if update_needed:
449 update_property_group(context)
452 class CMListCollection(PropertyGroup):
453 name: StringProperty(update=update_col_name)
454 last_name: StringProperty()
455 qcd_slot_idx: StringProperty(name="QCD Slot", update=update_qcd_slot)
458 def update_collection_tree(context):
459 global max_lvl
460 global row_index
461 global collection_tree
462 global layer_collections
463 global qcd_slots
465 collection_tree.clear()
466 layer_collections.clear()
468 max_lvl = 0
469 row_index = 0
470 layer_collection = context.view_layer.layer_collection
471 init_laycol_list = layer_collection.children
473 master_laycol = {"id": 0,
474 "name": layer_collection.name,
475 "lvl": -1,
476 "row_index": -1,
477 "visible": True,
478 "has_children": True,
479 "expanded": True,
480 "parent": None,
481 "children": [],
482 "ptr": layer_collection
485 get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True)
487 for laycol in master_laycol["children"]:
488 collection_tree.append(laycol)
490 qcd_slots.update_qcd()
492 qcd_slots.auto_numerate()
495 def get_all_collections(context, collections, parent, tree, level=0, visible=False):
496 global row_index
497 global max_lvl
499 if level > max_lvl:
500 max_lvl = level
502 for item in collections:
503 laycol = {"id": len(layer_collections) +1,
504 "name": item.name,
505 "lvl": level,
506 "row_index": row_index,
507 "visible": visible,
508 "has_children": False,
509 "expanded": False,
510 "parent": parent,
511 "children": [],
512 "ptr": item
515 row_index += 1
517 layer_collections[item.name] = laycol
518 tree.append(laycol)
520 if len(item.children) > 0:
521 laycol["has_children"] = True
523 if item.name in expanded and laycol["visible"]:
524 laycol["expanded"] = True
525 get_all_collections(context, item.children, laycol, laycol["children"], level+1, visible=True)
527 else:
528 get_all_collections(context, item.children, laycol, laycol["children"], level+1)
531 def update_property_group(context):
532 global collection_tree
533 global qcd_slots
535 qcd_slots.allow_update = False
537 update_collection_tree(context)
538 context.scene.collection_manager.cm_list_collection.clear()
539 create_property_group(context, collection_tree)
541 qcd_slots.allow_update = True
544 def create_property_group(context, tree):
545 global in_filter
546 global qcd_slots
548 cm = context.scene.collection_manager
550 for laycol in tree:
551 new_cm_listitem = cm.cm_list_collection.add()
552 new_cm_listitem.name = laycol["name"]
553 new_cm_listitem.qcd_slot_idx = qcd_slots.get_idx(laycol["name"], "")
555 if laycol["has_children"]:
556 create_property_group(context, laycol["children"])
559 def get_modifiers(event):
560 modifiers = []
562 if event.alt:
563 modifiers.append("alt")
565 if event.ctrl:
566 modifiers.append("ctrl")
568 if event.oskey:
569 modifiers.append("oskey")
571 if event.shift:
572 modifiers.append("shift")
574 return set(modifiers)
577 def generate_state(*, qcd=False):
578 global layer_collections
579 global qcd_slots
581 state = {
582 "name": [],
583 "exclude": [],
584 "select": [],
585 "hide": [],
586 "disable": [],
587 "render": [],
588 "holdout": [],
589 "indirect": [],
592 for name, laycol in layer_collections.items():
593 state["name"].append(name)
594 state["exclude"].append(laycol["ptr"].exclude)
595 state["select"].append(laycol["ptr"].collection.hide_select)
596 state["hide"].append(laycol["ptr"].hide_viewport)
597 state["disable"].append(laycol["ptr"].collection.hide_viewport)
598 state["render"].append(laycol["ptr"].collection.hide_render)
599 state["holdout"].append(laycol["ptr"].holdout)
600 state["indirect"].append(laycol["ptr"].indirect_only)
602 if qcd:
603 state["qcd"] = dict(qcd_slots)
605 return state
608 def check_state(context, *, cm_popup=False, phantom_mode=False, qcd=False):
609 view_layer = context.view_layer
611 # check if expanded & history/buffer state still correct
612 if cm_popup and collection_state:
613 new_state = generate_state()
615 if new_state["name"] != collection_state["name"]:
616 copy_buffer["RTO"] = ""
617 copy_buffer["values"].clear()
619 swap_buffer["A"]["RTO"] = ""
620 swap_buffer["A"]["values"].clear()
621 swap_buffer["B"]["RTO"] = ""
622 swap_buffer["B"]["values"].clear()
624 for name in list(expanded):
625 laycol = layer_collections.get(name)
626 if not laycol or not laycol["has_children"]:
627 expanded.remove(name)
629 for name in list(expand_history["history"]):
630 laycol = layer_collections.get(name)
631 if not laycol or not laycol["has_children"]:
632 expand_history["history"].remove(name)
634 for rto, history in rto_history.items():
635 if view_layer.name in history:
636 del history[view_layer.name]
639 else:
640 for rto in ["exclude", "select", "hide", "disable", "render", "holdout", "indirect"]:
641 if new_state[rto] != collection_state[rto]:
642 if view_layer.name in rto_history[rto]:
643 del rto_history[rto][view_layer.name]
645 if view_layer.name in rto_history[rto+"_all"]:
646 del rto_history[rto+"_all"][view_layer.name]
649 if phantom_mode:
650 cm = context.scene.collection_manager
652 # check if in phantom mode and if it's still viable
653 if cm.in_phantom_mode:
654 if layer_collections.keys() != phantom_history["initial_state"].keys():
655 cm.in_phantom_mode = False
657 if view_layer.name != phantom_history["view_layer"]:
658 cm.in_phantom_mode = False
660 if not cm.in_phantom_mode:
661 for key, value in phantom_history.items():
662 try:
663 value.clear()
664 except AttributeError:
665 if key == "view_layer":
666 phantom_history["view_layer"] = ""
669 if qcd and qcd_collection_state:
670 from .qcd_operators import QCDAllBase
671 new_state = generate_state(qcd=True)
673 if (new_state["name"] != qcd_collection_state["name"]
674 or new_state["exclude"] != qcd_collection_state["exclude"]
675 or new_state["qcd"] != qcd_collection_state["qcd"]):
676 if view_layer.name in qcd_history:
677 del qcd_history[view_layer.name]
678 qcd_collection_state.clear()
679 QCDAllBase.clear()
682 def get_move_selection(*, names_only=False):
683 global move_selection
685 if not move_selection:
686 move_selection = {obj.name for obj in bpy.context.selected_objects}
688 if names_only:
689 return move_selection
691 else:
692 if len(move_selection) <= 5:
693 return {bpy.data.objects[name] for name in move_selection}
695 else:
696 return {obj for obj in bpy.data.objects if obj.name in move_selection}
699 def get_move_active(*, always=False):
700 global move_active
701 global move_selection
703 if not move_active:
704 move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
706 if not always and move_active not in get_move_selection(names_only=True):
707 move_active = None
709 return bpy.data.objects[move_active] if move_active else None
712 def update_qcd_header():
713 cm = bpy.context.scene.collection_manager
714 cm.update_header.clear()
715 new_update_header = cm.update_header.add()
716 new_update_header.name = "updated"
719 class CMSendReport(Operator):
720 bl_label = "Send Report"
721 bl_idname = "view3d.cm_send_report"
723 message: StringProperty()
725 def draw(self, context):
726 layout = self.layout
727 col = layout.column(align=True)
729 first = True
730 string = ""
732 for num, char in enumerate(self.message):
733 if char == "\n":
734 if first:
735 col.row(align=True).label(text=string, icon='ERROR')
736 first = False
737 else:
738 col.row(align=True).label(text=string, icon='BLANK1')
740 string = ""
741 continue
743 string = string + char
745 if first:
746 col.row(align=True).label(text=string, icon='ERROR')
747 else:
748 col.row(align=True).label(text=string, icon='BLANK1')
750 def invoke(self, context, event):
751 wm = context.window_manager
753 max_len = 0
754 length = 0
756 for char in self.message:
757 if char == "\n":
758 if length > max_len:
759 max_len = length
760 length = 0
761 else:
762 length += 1
764 if length > max_len:
765 max_len = length
767 return wm.invoke_popup(self, width=int(30 + (max_len*5.5)))
769 def execute(self, context):
770 self.report({'INFO'}, self.message)
771 print(self.message)
772 return {'FINISHED'}
774 def send_report(message):
775 def report():
776 window = bpy.context.window_manager.windows[0]
777 ctx = {'window': window, 'screen': window.screen, }
778 bpy.ops.view3d.cm_send_report(ctx, 'INVOKE_DEFAULT', message=message)
780 bpy.app.timers.register(report)
783 class CMUISeparatorButton(Operator):
784 bl_label = "UI Separator Button"
785 bl_idname = "view3d.cm_ui_separator_button"
787 def execute(self, context):
788 return {'CANCELED'}
790 def add_vertical_separator_line(row):
791 # add buffer before to account for scaling
792 separator = row.row()
793 separator.scale_x = 0.1
794 separator.label()
796 # add separator line
797 separator = row.row()
798 separator.scale_x = 0.2
799 separator.enabled = False
800 separator.operator("view3d.cm_ui_separator_button",
801 text="",
802 icon='BLANK1',
804 # add buffer after to account for scaling
805 separator = row.row()
806 separator.scale_x = 0.1
807 separator.label()