Cleanup: simplify file name incrementing logic
[blender-addons.git] / object_collection_manager / internals.py
blob77e801f6c7e170349345455ebe79556470b2f16a
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 class QCDSlots():
110 _slots = {}
111 overrides = set()
112 allow_update = True
114 def __init__(self):
115 self._slots = persistent_data.slots
116 self.overrides = persistent_data.overrides
118 def __iter__(self):
119 return self._slots.items().__iter__()
121 def __repr__(self):
122 return self._slots.__repr__()
124 def contains(self, *, idx=None, name=None):
125 if idx:
126 return idx in self._slots.keys()
128 if name:
129 return name in self._slots.values()
131 raise
133 def object_in_slots(self, obj):
134 for collection in obj.users_collection:
135 if self.contains(name=collection.name):
136 return True
138 return False
140 def get_data_for_blend(self):
141 return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}"
143 def load_blend_data(self, data):
144 decoupled_data = data.split("\n")
145 blend_slots = eval(decoupled_data[0])
146 blend_overrides = eval(decoupled_data[1])
148 self._slots.clear()
149 self.overrides.clear()
151 for key, value in blend_slots.items():
152 self._slots[key] = value
154 for key in blend_overrides:
155 self.overrides.add(key)
157 def length(self):
158 return len(self._slots)
160 def get_idx(self, name, r_value=None):
161 for idx, slot_name in self._slots.items():
162 if slot_name == name:
163 return idx
165 return r_value
167 def get_name(self, idx, r_value=None):
168 if idx in self._slots:
169 return self._slots[idx]
171 return r_value
173 def add_slot(self, idx, name):
174 self._slots[idx] = name
176 if name in self.overrides:
177 self.overrides.remove(name)
179 def update_slot(self, idx, name):
180 self.add_slot(idx, name)
182 def del_slot(self, *, idx=None, name=None):
183 if idx and not name:
184 del self._slots[idx]
185 return
187 if name and not idx:
188 slot_idx = self.get_idx(name)
189 del self._slots[slot_idx]
190 return
192 raise
194 def add_override(self, name):
195 qcd_slots.del_slot(name=name)
196 qcd_slots.overrides.add(name)
198 def clear_slots(self):
199 self._slots.clear()
201 def update_qcd(self):
202 for idx, name in list(self._slots.items()):
203 if not layer_collections.get(name, None):
204 qcd_slots.del_slot(name=name)
206 def auto_numerate(self):
207 if self.length() < 20:
208 laycol = bpy.context.view_layer.layer_collection
210 laycol_iter_list = list(laycol.children)
211 while laycol_iter_list:
212 layer_collection = laycol_iter_list.pop(0)
213 laycol_iter_list.extend(list(layer_collection.children))
215 if layer_collection.name in qcd_slots.overrides:
216 continue
218 for x in range(20):
219 if (not self.contains(idx=str(x+1)) and
220 not self.contains(name=layer_collection.name)):
221 self.add_slot(str(x+1), layer_collection.name)
224 if self.length() > 20:
225 break
227 def renumerate(self, *, beginning=False, depth_first=False, constrain=False):
228 if beginning:
229 self.clear_slots()
230 self.overrides.clear()
232 starting_laycol_name = self.get_name("1")
234 if not starting_laycol_name:
235 laycol = bpy.context.view_layer.layer_collection
236 starting_laycol_name = laycol.children[0].name
238 self.clear_slots()
239 self.overrides.clear()
241 if depth_first:
242 parent = layer_collections[starting_laycol_name]["parent"]
243 x = 1
245 for laycol in layer_collections.values():
246 if self.length() == 0 and starting_laycol_name != laycol["name"]:
247 continue
249 if constrain:
250 if self.length():
251 if laycol["parent"]["name"] == parent["name"]:
252 break
254 self.add_slot(f"{x}", laycol["name"])
256 x += 1
258 if self.length() > 20:
259 break
261 else:
262 laycol = layer_collections[starting_laycol_name]["parent"]["ptr"]
264 laycol_iter_list = []
265 for laycol in laycol.children:
266 if laycol.name == starting_laycol_name:
267 laycol_iter_list.append(laycol)
269 elif not constrain and laycol_iter_list:
270 laycol_iter_list.append(laycol)
272 x = 1
273 while laycol_iter_list:
274 layer_collection = laycol_iter_list.pop(0)
276 self.add_slot(f"{x}", layer_collection.name)
278 laycol_iter_list.extend(list(layer_collection.children))
280 x += 1
282 if self.length() > 20:
283 break
286 for laycol in layer_collections.values():
287 if not self.contains(name=laycol["name"]):
288 self.overrides.add(laycol["name"])
290 qcd_slots = QCDSlots()
293 def update_col_name(self, context):
294 global layer_collections
295 global qcd_slots
296 global rto_history
297 global expand_history
299 if self.name != self.last_name:
300 if self.name == '':
301 self.name = self.last_name
302 return
304 # if statement prevents update on list creation
305 if self.last_name != '':
306 view_layer_name = context.view_layer.name
308 # update collection name
309 layer_collections[self.last_name]["ptr"].collection.name = self.name
311 # update expanded
312 orig_expanded = {x for x in expanded}
314 if self.last_name in orig_expanded:
315 expanded.remove(self.last_name)
316 expanded.add(self.name)
318 # update qcd_slot
319 idx = qcd_slots.get_idx(self.last_name)
320 if idx:
321 qcd_slots.update_slot(idx, self.name)
323 # update qcd_overrides
324 if self.last_name in qcd_slots.overrides:
325 qcd_slots.overrides.remove(self.last_name)
326 qcd_slots.overrides.add(self.name)
328 # update history
329 rtos = [
330 "exclude",
331 "select",
332 "hide",
333 "disable",
334 "render",
335 "holdout",
336 "indirect",
339 orig_targets = {
340 rto: rto_history[rto][view_layer_name]["target"]
341 for rto in rtos
342 if rto_history[rto].get(view_layer_name)
345 for rto in rtos:
346 history = rto_history[rto].get(view_layer_name)
348 if history and orig_targets[rto] == self.last_name:
349 history["target"] = self.name
351 # update expand history
352 orig_expand_target = expand_history["target"]
353 orig_expand_history = [x for x in expand_history["history"]]
355 if orig_expand_target == self.last_name:
356 expand_history["target"] = self.name
358 for x, name in enumerate(orig_expand_history):
359 if name == self.last_name:
360 expand_history["history"][x] = self.name
362 # update names in expanded, qcd slots, and rto_history for any other
363 # collection names that changed as a result of this name change
364 cm_list_collection = context.scene.collection_manager.cm_list_collection
365 count = 0
366 laycol_iter_list = list(context.view_layer.layer_collection.children)
368 while laycol_iter_list:
369 layer_collection = laycol_iter_list[0]
370 cm_list_item = cm_list_collection[count]
372 if cm_list_item.name != layer_collection.name:
373 # update expanded
374 if cm_list_item.last_name in orig_expanded:
375 if not cm_list_item.last_name in layer_collections:
376 expanded.remove(cm_list_item.name)
378 expanded.add(layer_collection.name)
380 # update qcd_slot
381 idx = cm_list_item.qcd_slot_idx
382 if idx:
383 qcd_slots.update_slot(idx, layer_collection.name)
385 # update qcd_overrides
386 if cm_list_item.name in qcd_slots.overrides:
387 if not cm_list_item.name in layer_collections:
388 qcd_slots.overrides.remove(cm_list_item.name)
390 qcd_slots.overrides.add(layer_collection.name)
392 # update history
393 for rto in rtos:
394 history = rto_history[rto].get(view_layer_name)
396 if history and orig_targets[rto] == cm_list_item.last_name:
397 history["target"] = layer_collection.name
399 # update expand history
400 if orig_expand_target == cm_list_item.last_name:
401 expand_history["target"] = layer_collection.name
403 for x, name in enumerate(orig_expand_history):
404 if name == cm_list_item.last_name:
405 expand_history["history"][x] = layer_collection.name
407 if layer_collection.children:
408 laycol_iter_list[0:0] = list(layer_collection.children)
411 laycol_iter_list.remove(layer_collection)
412 count += 1
415 update_property_group(context)
418 self.last_name = self.name
421 def update_qcd_slot(self, context):
422 global qcd_slots
424 if not qcd_slots.allow_update:
425 return
427 update_needed = False
429 try:
430 int(self.qcd_slot_idx)
432 except ValueError:
433 if self.qcd_slot_idx == "":
434 qcd_slots.add_override(self.name)
436 if qcd_slots.contains(name=self.name):
437 qcd_slots.allow_update = False
438 self.qcd_slot_idx = qcd_slots.get_idx(self.name)
439 qcd_slots.allow_update = True
441 if self.name in qcd_slots.overrides:
442 qcd_slots.allow_update = False
443 self.qcd_slot_idx = ""
444 qcd_slots.allow_update = True
446 return
448 if qcd_slots.contains(name=self.name):
449 qcd_slots.del_slot(name=self.name)
450 update_needed = True
452 if qcd_slots.contains(idx=self.qcd_slot_idx):
453 qcd_slots.add_override(qcd_slots.get_name(self.qcd_slot_idx))
454 update_needed = True
456 if int(self.qcd_slot_idx) > 20:
457 self.qcd_slot_idx = "20"
459 if int(self.qcd_slot_idx) < 1:
460 self.qcd_slot_idx = "1"
462 qcd_slots.add_slot(self.qcd_slot_idx, self.name)
464 if update_needed:
465 update_property_group(context)
468 class CMListCollection(PropertyGroup):
469 name: StringProperty(update=update_col_name)
470 last_name: StringProperty()
471 qcd_slot_idx: StringProperty(name="QCD Slot", update=update_qcd_slot)
474 def update_collection_tree(context):
475 global max_lvl
476 global row_index
477 global collection_tree
478 global layer_collections
479 global qcd_slots
481 collection_tree.clear()
482 layer_collections.clear()
484 max_lvl = 0
485 row_index = 0
486 layer_collection = context.view_layer.layer_collection
487 init_laycol_list = layer_collection.children
489 master_laycol = {"id": 0,
490 "name": layer_collection.name,
491 "lvl": -1,
492 "row_index": -1,
493 "visible": True,
494 "has_children": True,
495 "expanded": True,
496 "parent": None,
497 "children": [],
498 "ptr": layer_collection
501 get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True)
503 for laycol in master_laycol["children"]:
504 collection_tree.append(laycol)
506 qcd_slots.update_qcd()
508 qcd_slots.auto_numerate()
511 def get_all_collections(context, collections, parent, tree, level=0, visible=False):
512 global row_index
513 global max_lvl
515 if level > max_lvl:
516 max_lvl = level
518 for item in collections:
519 laycol = {"id": len(layer_collections) +1,
520 "name": item.name,
521 "lvl": level,
522 "row_index": row_index,
523 "visible": visible,
524 "has_children": False,
525 "expanded": False,
526 "parent": parent,
527 "children": [],
528 "ptr": item
531 row_index += 1
533 layer_collections[item.name] = laycol
534 tree.append(laycol)
536 if len(item.children) > 0:
537 laycol["has_children"] = True
539 if item.name in expanded and laycol["visible"]:
540 laycol["expanded"] = True
541 get_all_collections(context, item.children, laycol, laycol["children"], level+1, visible=True)
543 else:
544 get_all_collections(context, item.children, laycol, laycol["children"], level+1)
547 def update_property_group(context):
548 global collection_tree
549 global qcd_slots
551 qcd_slots.allow_update = False
553 update_collection_tree(context)
554 context.scene.collection_manager.cm_list_collection.clear()
555 create_property_group(context, collection_tree)
557 qcd_slots.allow_update = True
560 def create_property_group(context, tree):
561 global in_filter
562 global qcd_slots
564 cm = context.scene.collection_manager
566 for laycol in tree:
567 new_cm_listitem = cm.cm_list_collection.add()
568 new_cm_listitem.name = laycol["name"]
569 new_cm_listitem.qcd_slot_idx = qcd_slots.get_idx(laycol["name"], "")
571 if laycol["has_children"]:
572 create_property_group(context, laycol["children"])
575 def get_modifiers(event):
576 modifiers = []
578 if event.alt:
579 modifiers.append("alt")
581 if event.ctrl:
582 modifiers.append("ctrl")
584 if event.oskey:
585 modifiers.append("oskey")
587 if event.shift:
588 modifiers.append("shift")
590 return set(modifiers)
593 def generate_state(*, qcd=False):
594 global layer_collections
595 global qcd_slots
597 state = {
598 "name": [],
599 "exclude": [],
600 "select": [],
601 "hide": [],
602 "disable": [],
603 "render": [],
604 "holdout": [],
605 "indirect": [],
608 for name, laycol in layer_collections.items():
609 state["name"].append(name)
610 state["exclude"].append(laycol["ptr"].exclude)
611 state["select"].append(laycol["ptr"].collection.hide_select)
612 state["hide"].append(laycol["ptr"].hide_viewport)
613 state["disable"].append(laycol["ptr"].collection.hide_viewport)
614 state["render"].append(laycol["ptr"].collection.hide_render)
615 state["holdout"].append(laycol["ptr"].holdout)
616 state["indirect"].append(laycol["ptr"].indirect_only)
618 if qcd:
619 state["qcd"] = dict(qcd_slots)
621 return state
624 def get_move_selection(*, names_only=False):
625 global move_selection
627 if not move_selection:
628 move_selection = {obj.name for obj in bpy.context.selected_objects}
630 if names_only:
631 return move_selection
633 else:
634 if len(move_selection) <= 5:
635 return {bpy.data.objects[name] for name in move_selection}
637 else:
638 return {obj for obj in bpy.data.objects if obj.name in move_selection}
641 def get_move_active(*, always=False):
642 global move_active
643 global move_selection
645 if not move_active:
646 move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
648 if not always and move_active not in get_move_selection(names_only=True):
649 move_active = None
651 return bpy.data.objects[move_active] if move_active else None
654 def update_qcd_header():
655 cm = bpy.context.scene.collection_manager
656 cm.update_header.clear()
657 new_update_header = cm.update_header.add()
658 new_update_header.name = "updated"
661 class CMSendReport(Operator):
662 bl_label = "Send Report"
663 bl_idname = "view3d.cm_send_report"
665 message: StringProperty()
667 def draw(self, context):
668 layout = self.layout
669 col = layout.column(align=True)
671 first = True
672 string = ""
674 for num, char in enumerate(self.message):
675 if char == "\n":
676 if first:
677 col.row(align=True).label(text=string, icon='ERROR')
678 first = False
679 else:
680 col.row(align=True).label(text=string, icon='BLANK1')
682 string = ""
683 continue
685 string = string + char
687 if first:
688 col.row(align=True).label(text=string, icon='ERROR')
689 else:
690 col.row(align=True).label(text=string, icon='BLANK1')
692 def invoke(self, context, event):
693 wm = context.window_manager
695 max_len = 0
696 length = 0
698 for char in self.message:
699 if char == "\n":
700 if length > max_len:
701 max_len = length
702 length = 0
703 else:
704 length += 1
706 if length > max_len:
707 max_len = length
709 return wm.invoke_popup(self, width=(30 + (max_len*5.5)))
711 def execute(self, context):
712 self.report({'INFO'}, self.message)
713 print(self.message)
714 return {'FINISHED'}
716 def send_report(message):
717 def report():
718 window = bpy.context.window_manager.windows[0]
719 ctx = {'window': window, 'screen': window.screen, }
720 bpy.ops.view3d.cm_send_report(ctx, 'INVOKE_DEFAULT', message=message)
722 bpy.app.timers.register(report)