Sun position: Fix T80379 - Custom startup breaks the add-on
[blender-addons.git] / object_collection_manager / internals.py
blobceef1560b2a463f3400b4da8339163d24e2de92b
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 from . import persistent_data
23 import bpy
25 from bpy.types import (
26 PropertyGroup,
27 Operator,
30 from bpy.props import (
31 StringProperty,
32 IntProperty,
35 move_triggered = False
36 move_selection = []
37 move_active = None
39 layer_collections = {}
40 collection_tree = []
41 collection_state = {}
42 qcd_collection_state = {}
43 expanded = set()
44 row_index = 0
45 max_lvl = 0
47 rto_history = {
48 "exclude": {},
49 "exclude_all": {},
50 "select": {},
51 "select_all": {},
52 "hide": {},
53 "hide_all": {},
54 "disable": {},
55 "disable_all": {},
56 "render": {},
57 "render_all": {},
58 "holdout": {},
59 "holdout_all": {},
60 "indirect": {},
61 "indirect_all": {},
64 qcd_history = {}
66 expand_history = {
67 "target": "",
68 "history": [],
71 phantom_history = {
72 "view_layer": "",
73 "initial_state": {},
75 "exclude_history": {},
76 "select_history": {},
77 "hide_history": {},
78 "disable_history": {},
79 "render_history": {},
80 "holdout_history": {},
81 "indirect_history": {},
83 "exclude_all_history": [],
84 "select_all_history": [],
85 "hide_all_history": [],
86 "disable_all_history": [],
87 "render_all_history": [],
88 "holdout_all_history": [],
89 "indirect_all_history": [],
92 copy_buffer = {
93 "RTO": "",
94 "values": []
97 swap_buffer = {
98 "A": {
99 "RTO": "",
100 "values": []
102 "B": {
103 "RTO": "",
104 "values": []
109 def get_max_lvl():
110 return max_lvl
113 class QCDSlots():
114 _slots = {}
115 overrides = set()
116 allow_update = True
118 def __init__(self):
119 self._slots = persistent_data.slots
120 self.overrides = persistent_data.overrides
122 def __iter__(self):
123 return self._slots.items().__iter__()
125 def __repr__(self):
126 return self._slots.__repr__()
128 def contains(self, *, idx=None, name=None):
129 if idx:
130 return idx in self._slots.keys()
132 if name:
133 return name in self._slots.values()
135 raise
137 def object_in_slots(self, obj):
138 for collection in obj.users_collection:
139 if self.contains(name=collection.name):
140 return True
142 return False
144 def get_data_for_blend(self):
145 return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}"
147 def load_blend_data(self, data):
148 decoupled_data = data.split("\n")
149 blend_slots = eval(decoupled_data[0])
150 blend_overrides = eval(decoupled_data[1])
152 self._slots.clear()
153 self.overrides.clear()
155 for key, value in blend_slots.items():
156 self._slots[key] = value
158 for key in blend_overrides:
159 self.overrides.add(key)
161 def length(self):
162 return len(self._slots)
164 def get_idx(self, name, r_value=None):
165 for idx, slot_name in self._slots.items():
166 if slot_name == name:
167 return idx
169 return r_value
171 def get_name(self, idx, r_value=None):
172 if idx in self._slots:
173 return self._slots[idx]
175 return r_value
177 def add_slot(self, idx, name):
178 self._slots[idx] = name
180 if name in self.overrides:
181 self.overrides.remove(name)
183 def update_slot(self, idx, name):
184 self.add_slot(idx, name)
186 def del_slot(self, *, idx=None, name=None):
187 if idx and not name:
188 del self._slots[idx]
189 return
191 if name and not idx:
192 slot_idx = self.get_idx(name)
193 del self._slots[slot_idx]
194 return
196 raise
198 def add_override(self, name):
199 qcd_slots.del_slot(name=name)
200 qcd_slots.overrides.add(name)
202 def clear_slots(self):
203 self._slots.clear()
205 def update_qcd(self):
206 for idx, name in list(self._slots.items()):
207 if not layer_collections.get(name, None):
208 qcd_slots.del_slot(name=name)
210 def auto_numerate(self):
211 if self.length() < 20:
212 laycol = bpy.context.view_layer.layer_collection
214 laycol_iter_list = list(laycol.children)
215 while laycol_iter_list:
216 layer_collection = laycol_iter_list.pop(0)
217 laycol_iter_list.extend(list(layer_collection.children))
219 if layer_collection.name in qcd_slots.overrides:
220 continue
222 for x in range(20):
223 if (not self.contains(idx=str(x+1)) and
224 not self.contains(name=layer_collection.name)):
225 self.add_slot(str(x+1), layer_collection.name)
228 if self.length() > 20:
229 break
231 def renumerate(self, *, beginning=False, depth_first=False, constrain=False):
232 if beginning:
233 self.clear_slots()
234 self.overrides.clear()
236 starting_laycol_name = self.get_name("1")
238 if not starting_laycol_name:
239 laycol = bpy.context.view_layer.layer_collection
240 starting_laycol_name = laycol.children[0].name
242 self.clear_slots()
243 self.overrides.clear()
245 if depth_first:
246 parent = layer_collections[starting_laycol_name]["parent"]
247 x = 1
249 for laycol in layer_collections.values():
250 if self.length() == 0 and starting_laycol_name != laycol["name"]:
251 continue
253 if constrain:
254 if self.length():
255 if laycol["parent"]["name"] == parent["name"]:
256 break
258 self.add_slot(f"{x}", laycol["name"])
260 x += 1
262 if self.length() > 20:
263 break
265 else:
266 laycol = layer_collections[starting_laycol_name]["parent"]["ptr"]
268 laycol_iter_list = []
269 for laycol in laycol.children:
270 if laycol.name == starting_laycol_name:
271 laycol_iter_list.append(laycol)
273 elif not constrain and laycol_iter_list:
274 laycol_iter_list.append(laycol)
276 x = 1
277 while laycol_iter_list:
278 layer_collection = laycol_iter_list.pop(0)
280 self.add_slot(f"{x}", layer_collection.name)
282 laycol_iter_list.extend(list(layer_collection.children))
284 x += 1
286 if self.length() > 20:
287 break
290 for laycol in layer_collections.values():
291 if not self.contains(name=laycol["name"]):
292 self.overrides.add(laycol["name"])
294 qcd_slots = QCDSlots()
297 def update_col_name(self, context):
298 global layer_collections
299 global qcd_slots
300 global rto_history
301 global expand_history
303 if self.name != self.last_name:
304 if self.name == '':
305 self.name = self.last_name
306 return
308 # if statement prevents update on list creation
309 if self.last_name != '':
310 view_layer_name = context.view_layer.name
312 # update collection name
313 layer_collections[self.last_name]["ptr"].collection.name = self.name
315 # update expanded
316 orig_expanded = {x for x in expanded}
318 if self.last_name in orig_expanded:
319 expanded.remove(self.last_name)
320 expanded.add(self.name)
322 # update qcd_slot
323 idx = qcd_slots.get_idx(self.last_name)
324 if idx:
325 qcd_slots.update_slot(idx, self.name)
327 # update qcd_overrides
328 if self.last_name in qcd_slots.overrides:
329 qcd_slots.overrides.remove(self.last_name)
330 qcd_slots.overrides.add(self.name)
332 # update history
333 rtos = [
334 "exclude",
335 "select",
336 "hide",
337 "disable",
338 "render",
339 "holdout",
340 "indirect",
343 orig_targets = {
344 rto: rto_history[rto][view_layer_name]["target"]
345 for rto in rtos
346 if rto_history[rto].get(view_layer_name)
349 for rto in rtos:
350 history = rto_history[rto].get(view_layer_name)
352 if history and orig_targets[rto] == self.last_name:
353 history["target"] = self.name
355 # update expand history
356 orig_expand_target = expand_history["target"]
357 orig_expand_history = [x for x in expand_history["history"]]
359 if orig_expand_target == self.last_name:
360 expand_history["target"] = self.name
362 for x, name in enumerate(orig_expand_history):
363 if name == self.last_name:
364 expand_history["history"][x] = self.name
366 # update names in expanded, qcd slots, and rto_history for any other
367 # collection names that changed as a result of this name change
368 cm_list_collection = context.scene.collection_manager.cm_list_collection
369 count = 0
370 laycol_iter_list = list(context.view_layer.layer_collection.children)
372 while laycol_iter_list:
373 layer_collection = laycol_iter_list[0]
374 cm_list_item = cm_list_collection[count]
376 if cm_list_item.name != layer_collection.name:
377 # update expanded
378 if cm_list_item.last_name in orig_expanded:
379 if not cm_list_item.last_name in layer_collections:
380 expanded.remove(cm_list_item.name)
382 expanded.add(layer_collection.name)
384 # update qcd_slot
385 idx = cm_list_item.qcd_slot_idx
386 if idx:
387 qcd_slots.update_slot(idx, layer_collection.name)
389 # update qcd_overrides
390 if cm_list_item.name in qcd_slots.overrides:
391 if not cm_list_item.name in layer_collections:
392 qcd_slots.overrides.remove(cm_list_item.name)
394 qcd_slots.overrides.add(layer_collection.name)
396 # update history
397 for rto in rtos:
398 history = rto_history[rto].get(view_layer_name)
400 if history and orig_targets[rto] == cm_list_item.last_name:
401 history["target"] = layer_collection.name
403 # update expand history
404 if orig_expand_target == cm_list_item.last_name:
405 expand_history["target"] = layer_collection.name
407 for x, name in enumerate(orig_expand_history):
408 if name == cm_list_item.last_name:
409 expand_history["history"][x] = layer_collection.name
411 if layer_collection.children:
412 laycol_iter_list[0:0] = list(layer_collection.children)
415 laycol_iter_list.remove(layer_collection)
416 count += 1
419 update_property_group(context)
422 self.last_name = self.name
425 def update_qcd_slot(self, context):
426 global qcd_slots
428 if not qcd_slots.allow_update:
429 return
431 update_needed = False
433 try:
434 int(self.qcd_slot_idx)
436 except ValueError:
437 if self.qcd_slot_idx == "":
438 qcd_slots.add_override(self.name)
440 if qcd_slots.contains(name=self.name):
441 qcd_slots.allow_update = False
442 self.qcd_slot_idx = qcd_slots.get_idx(self.name)
443 qcd_slots.allow_update = True
445 if self.name in qcd_slots.overrides:
446 qcd_slots.allow_update = False
447 self.qcd_slot_idx = ""
448 qcd_slots.allow_update = True
450 return
452 if qcd_slots.contains(name=self.name):
453 qcd_slots.del_slot(name=self.name)
454 update_needed = True
456 if qcd_slots.contains(idx=self.qcd_slot_idx):
457 qcd_slots.add_override(qcd_slots.get_name(self.qcd_slot_idx))
458 update_needed = True
460 if int(self.qcd_slot_idx) > 20:
461 self.qcd_slot_idx = "20"
463 if int(self.qcd_slot_idx) < 1:
464 self.qcd_slot_idx = "1"
466 qcd_slots.add_slot(self.qcd_slot_idx, self.name)
468 if update_needed:
469 update_property_group(context)
472 class CMListCollection(PropertyGroup):
473 name: StringProperty(update=update_col_name)
474 last_name: StringProperty()
475 qcd_slot_idx: StringProperty(name="QCD Slot", update=update_qcd_slot)
478 def update_collection_tree(context):
479 global max_lvl
480 global row_index
481 global collection_tree
482 global layer_collections
483 global qcd_slots
485 collection_tree.clear()
486 layer_collections.clear()
488 max_lvl = 0
489 row_index = 0
490 layer_collection = context.view_layer.layer_collection
491 init_laycol_list = layer_collection.children
493 master_laycol = {"id": 0,
494 "name": layer_collection.name,
495 "lvl": -1,
496 "row_index": -1,
497 "visible": True,
498 "has_children": True,
499 "expanded": True,
500 "parent": None,
501 "children": [],
502 "ptr": layer_collection
505 get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True)
507 for laycol in master_laycol["children"]:
508 collection_tree.append(laycol)
510 qcd_slots.update_qcd()
512 qcd_slots.auto_numerate()
515 def get_all_collections(context, collections, parent, tree, level=0, visible=False):
516 global row_index
517 global max_lvl
519 if level > max_lvl:
520 max_lvl = level
522 for item in collections:
523 laycol = {"id": len(layer_collections) +1,
524 "name": item.name,
525 "lvl": level,
526 "row_index": row_index,
527 "visible": visible,
528 "has_children": False,
529 "expanded": False,
530 "parent": parent,
531 "children": [],
532 "ptr": item
535 row_index += 1
537 layer_collections[item.name] = laycol
538 tree.append(laycol)
540 if len(item.children) > 0:
541 laycol["has_children"] = True
543 if item.name in expanded and laycol["visible"]:
544 laycol["expanded"] = True
545 get_all_collections(context, item.children, laycol, laycol["children"], level+1, visible=True)
547 else:
548 get_all_collections(context, item.children, laycol, laycol["children"], level+1)
551 def update_property_group(context):
552 global collection_tree
553 global qcd_slots
555 qcd_slots.allow_update = False
557 update_collection_tree(context)
558 context.scene.collection_manager.cm_list_collection.clear()
559 create_property_group(context, collection_tree)
561 qcd_slots.allow_update = True
564 def create_property_group(context, tree):
565 global in_filter
566 global qcd_slots
568 cm = context.scene.collection_manager
570 for laycol in tree:
571 new_cm_listitem = cm.cm_list_collection.add()
572 new_cm_listitem.name = laycol["name"]
573 new_cm_listitem.qcd_slot_idx = qcd_slots.get_idx(laycol["name"], "")
575 if laycol["has_children"]:
576 create_property_group(context, laycol["children"])
579 def get_modifiers(event):
580 modifiers = []
582 if event.alt:
583 modifiers.append("alt")
585 if event.ctrl:
586 modifiers.append("ctrl")
588 if event.oskey:
589 modifiers.append("oskey")
591 if event.shift:
592 modifiers.append("shift")
594 return set(modifiers)
597 def generate_state(*, qcd=False):
598 global layer_collections
599 global qcd_slots
601 state = {
602 "name": [],
603 "exclude": [],
604 "select": [],
605 "hide": [],
606 "disable": [],
607 "render": [],
608 "holdout": [],
609 "indirect": [],
612 for name, laycol in layer_collections.items():
613 state["name"].append(name)
614 state["exclude"].append(laycol["ptr"].exclude)
615 state["select"].append(laycol["ptr"].collection.hide_select)
616 state["hide"].append(laycol["ptr"].hide_viewport)
617 state["disable"].append(laycol["ptr"].collection.hide_viewport)
618 state["render"].append(laycol["ptr"].collection.hide_render)
619 state["holdout"].append(laycol["ptr"].holdout)
620 state["indirect"].append(laycol["ptr"].indirect_only)
622 if qcd:
623 state["qcd"] = dict(qcd_slots)
625 return state
628 def get_move_selection(*, names_only=False):
629 global move_selection
631 if not move_selection:
632 move_selection = {obj.name for obj in bpy.context.selected_objects}
634 if names_only:
635 return move_selection
637 else:
638 if len(move_selection) <= 5:
639 return {bpy.data.objects[name] for name in move_selection}
641 else:
642 return {obj for obj in bpy.data.objects if obj.name in move_selection}
645 def get_move_active():
646 global move_active
647 global move_selection
649 if not move_active:
650 move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
652 if move_active not in get_move_selection(names_only=True):
653 move_active = None
655 return bpy.data.objects[move_active] if move_active else None
658 def update_qcd_header():
659 cm = bpy.context.scene.collection_manager
660 cm.update_header.clear()
661 new_update_header = cm.update_header.add()
662 new_update_header.name = "updated"
665 class CMSendReport(Operator):
666 bl_label = "Send Report"
667 bl_idname = "view3d.cm_send_report"
669 message: StringProperty()
671 def draw(self, context):
672 layout = self.layout
673 col = layout.column(align=True)
675 first = True
676 string = ""
678 for num, char in enumerate(self.message):
679 if char == "\n":
680 if first:
681 col.row(align=True).label(text=string, icon='ERROR')
682 first = False
683 else:
684 col.row(align=True).label(text=string, icon='BLANK1')
686 string = ""
687 continue
689 string = string + char
691 if first:
692 col.row(align=True).label(text=string, icon='ERROR')
693 else:
694 col.row(align=True).label(text=string, icon='BLANK1')
696 def invoke(self, context, event):
697 wm = context.window_manager
699 max_len = 0
700 length = 0
702 for char in self.message:
703 if char == "\n":
704 if length > max_len:
705 max_len = length
706 length = 0
707 else:
708 length += 1
710 if length > max_len:
711 max_len = length
713 return wm.invoke_popup(self, width=(30 + (max_len*5.5)))
715 def execute(self, context):
716 self.report({'INFO'}, self.message)
717 print(self.message)
718 return {'FINISHED'}
720 def send_report(message):
721 def report():
722 window = bpy.context.window_manager.windows[0]
723 ctx = {'window': window, 'screen': window.screen, }
724 bpy.ops.view3d.cm_send_report(ctx, 'INVOKE_DEFAULT', message=message)
726 bpy.app.timers.register(report)