1# ##### BEGIN GPL LICENSE BLOCK #####
2#
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.
7#
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.
12#
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.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# Copyright 2011, Ryan Inch
20
21from . import persistent_data
22
23import bpy
24
25from bpy.types import (
26    PropertyGroup,
27    Operator,
28)
29
30from bpy.props import (
31    StringProperty,
32    IntProperty,
33)
34
35move_triggered = False
36move_selection = []
37move_active = None
38
39layer_collections = {}
40collection_tree = []
41collection_state = {}
42qcd_collection_state = {}
43expanded = set()
44row_index = 0
45max_lvl = 0
46
47rto_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": {},
62}
63
64qcd_history = {}
65
66expand_history = {
67    "target": "",
68    "history": [],
69    }
70
71phantom_history = {
72    "view_layer": "",
73    "initial_state": {},
74
75    "exclude_history": {},
76    "select_history": {},
77    "hide_history": {},
78    "disable_history": {},
79    "render_history": {},
80    "holdout_history": {},
81    "indirect_history": {},
82
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": [],
90                   }
91
92copy_buffer = {
93    "RTO": "",
94    "values": []
95    }
96
97swap_buffer = {
98    "A": {
99        "RTO": "",
100        "values": []
101        },
102    "B": {
103        "RTO": "",
104        "values": []
105        }
106    }
107
108
109def get_max_lvl():
110    return max_lvl
111
112
113class QCDSlots():
114    _slots = {}
115    overrides = set()
116    allow_update = True
117
118    def __init__(self):
119        self._slots = persistent_data.slots
120        self.overrides = persistent_data.overrides
121
122    def __iter__(self):
123        return self._slots.items().__iter__()
124
125    def __repr__(self):
126        return self._slots.__repr__()
127
128    def contains(self, *, idx=None, name=None):
129        if idx:
130            return idx in self._slots.keys()
131
132        if name:
133            return name in self._slots.values()
134
135        raise
136
137    def object_in_slots(self, obj):
138        for collection in obj.users_collection:
139            if self.contains(name=collection.name):
140                return True
141
142        return False
143
144    def get_data_for_blend(self):
145        return f"{self._slots.__repr__()}\n{self.overrides.__repr__()}"
146
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])
151
152        self._slots.clear()
153        self.overrides.clear()
154
155        for key, value in blend_slots.items():
156            self._slots[key] = value
157
158        for key in blend_overrides:
159            self.overrides.add(key)
160
161    def length(self):
162        return len(self._slots)
163
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
168
169        return r_value
170
171    def get_name(self, idx, r_value=None):
172        if idx in self._slots:
173            return self._slots[idx]
174
175        return r_value
176
177    def add_slot(self, idx, name):
178        self._slots[idx] = name
179
180        if name in self.overrides:
181            self.overrides.remove(name)
182
183    def update_slot(self, idx, name):
184        self.add_slot(idx, name)
185
186    def del_slot(self, *, idx=None, name=None):
187        if idx and not name:
188            del self._slots[idx]
189            return
190
191        if name and not idx:
192            slot_idx = self.get_idx(name)
193            del self._slots[slot_idx]
194            return
195
196        raise
197
198    def add_override(self, name):
199        qcd_slots.del_slot(name=name)
200        qcd_slots.overrides.add(name)
201
202    def clear_slots(self):
203        self._slots.clear()
204
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)
209
210    def auto_numerate(self):
211        if self.length() < 20:
212            laycol = bpy.context.view_layer.layer_collection
213
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))
218
219                if layer_collection.name in qcd_slots.overrides:
220                    continue
221
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)
226
227
228                if self.length() > 20:
229                    break
230
231    def renumerate(self, *, beginning=False, depth_first=False, constrain=False):
232        if beginning:
233            self.clear_slots()
234            self.overrides.clear()
235
236        starting_laycol_name = self.get_name("1")
237
238        if not starting_laycol_name:
239            laycol = bpy.context.view_layer.layer_collection
240            starting_laycol_name = laycol.children[0].name
241
242        self.clear_slots()
243        self.overrides.clear()
244
245        if depth_first:
246            parent = layer_collections[starting_laycol_name]["parent"]
247            x = 1
248
249            for laycol in layer_collections.values():
250                if self.length() == 0 and starting_laycol_name != laycol["name"]:
251                    continue
252
253                if constrain:
254                    if self.length():
255                        if laycol["parent"]["name"] == parent["name"]:
256                            break
257
258                self.add_slot(f"{x}", laycol["name"])
259
260                x += 1
261
262                if self.length() > 20:
263                    break
264
265        else:
266            laycol = layer_collections[starting_laycol_name]["parent"]["ptr"]
267
268            laycol_iter_list = []
269            for laycol in laycol.children:
270                if laycol.name == starting_laycol_name:
271                    laycol_iter_list.append(laycol)
272
273                elif not constrain and laycol_iter_list:
274                    laycol_iter_list.append(laycol)
275
276            x = 1
277            while laycol_iter_list:
278                layer_collection = laycol_iter_list.pop(0)
279
280                self.add_slot(f"{x}", layer_collection.name)
281
282                laycol_iter_list.extend(list(layer_collection.children))
283
284                x += 1
285
286                if self.length() > 20:
287                    break
288
289
290        for laycol in layer_collections.values():
291            if not self.contains(name=laycol["name"]):
292                self.overrides.add(laycol["name"])
293
294qcd_slots = QCDSlots()
295
296
297def update_col_name(self, context):
298    global layer_collections
299    global qcd_slots
300    global rto_history
301    global expand_history
302
303    if self.name != self.last_name:
304        if self.name == '':
305            self.name = self.last_name
306            return
307
308        # if statement prevents update on list creation
309        if self.last_name != '':
310            view_layer_name = context.view_layer.name
311
312            # update collection name
313            layer_collections[self.last_name]["ptr"].collection.name = self.name
314
315            # update expanded
316            orig_expanded = {x for x in expanded}
317
318            if self.last_name in orig_expanded:
319                expanded.remove(self.last_name)
320                expanded.add(self.name)
321
322            # update qcd_slot
323            idx = qcd_slots.get_idx(self.last_name)
324            if idx:
325                qcd_slots.update_slot(idx, self.name)
326
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)
331
332            # update history
333            rtos = [
334                "exclude",
335                "select",
336                "hide",
337                "disable",
338                "render",
339                "holdout",
340                "indirect",
341                ]
342
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)
347                }
348
349            for rto in rtos:
350                history = rto_history[rto].get(view_layer_name)
351
352                if history and orig_targets[rto] == self.last_name:
353                    history["target"] = self.name
354
355            # update expand history
356            orig_expand_target = expand_history["target"]
357            orig_expand_history = [x for x in expand_history["history"]]
358
359            if orig_expand_target == self.last_name:
360                expand_history["target"] = self.name
361
362            for x, name in enumerate(orig_expand_history):
363                if name == self.last_name:
364                    expand_history["history"][x] = self.name
365
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)
371
372            while laycol_iter_list:
373                layer_collection = laycol_iter_list[0]
374                cm_list_item = cm_list_collection[count]
375
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)
381
382                        expanded.add(layer_collection.name)
383
384                    # update qcd_slot
385                    idx = cm_list_item.qcd_slot_idx
386                    if idx:
387                        qcd_slots.update_slot(idx, layer_collection.name)
388
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)
393
394                        qcd_slots.overrides.add(layer_collection.name)
395
396                    # update history
397                    for rto in rtos:
398                        history = rto_history[rto].get(view_layer_name)
399
400                        if history and orig_targets[rto] == cm_list_item.last_name:
401                            history["target"] = layer_collection.name
402
403                    # update expand history
404                    if orig_expand_target == cm_list_item.last_name:
405                        expand_history["target"] = layer_collection.name
406
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
410
411                if layer_collection.children:
412                    laycol_iter_list[0:0] = list(layer_collection.children)
413
414
415                laycol_iter_list.remove(layer_collection)
416                count += 1
417
418
419            update_property_group(context)
420
421
422        self.last_name = self.name
423
424
425def update_qcd_slot(self, context):
426    global qcd_slots
427
428    if not qcd_slots.allow_update:
429        return
430
431    update_needed = False
432
433    try:
434        int(self.qcd_slot_idx)
435
436    except ValueError:
437        if self.qcd_slot_idx == "":
438            qcd_slots.add_override(self.name)
439
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
444
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
449
450        return
451
452    if qcd_slots.contains(name=self.name):
453        qcd_slots.del_slot(name=self.name)
454        update_needed = True
455
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
459
460    if int(self.qcd_slot_idx) > 20:
461        self.qcd_slot_idx = "20"
462
463    if int(self.qcd_slot_idx) < 1:
464        self.qcd_slot_idx = "1"
465
466    qcd_slots.add_slot(self.qcd_slot_idx, self.name)
467
468    if update_needed:
469        update_property_group(context)
470
471
472class 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)
476
477
478def update_collection_tree(context):
479    global max_lvl
480    global row_index
481    global collection_tree
482    global layer_collections
483    global qcd_slots
484
485    collection_tree.clear()
486    layer_collections.clear()
487
488    max_lvl = 0
489    row_index = 0
490    layer_collection = context.view_layer.layer_collection
491    init_laycol_list = layer_collection.children
492
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
503                     }
504
505    get_all_collections(context, init_laycol_list, master_laycol, master_laycol["children"], visible=True)
506
507    for laycol in master_laycol["children"]:
508        collection_tree.append(laycol)
509
510    qcd_slots.update_qcd()
511
512    qcd_slots.auto_numerate()
513
514
515def get_all_collections(context, collections, parent, tree, level=0, visible=False):
516    global row_index
517    global max_lvl
518
519    if level > max_lvl:
520        max_lvl = level
521
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
533                  }
534
535        row_index += 1
536
537        layer_collections[item.name] = laycol
538        tree.append(laycol)
539
540        if len(item.children) > 0:
541            laycol["has_children"] = True
542
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)
546
547            else:
548                get_all_collections(context, item.children, laycol, laycol["children"], level+1)
549
550
551def update_property_group(context):
552    global collection_tree
553    global qcd_slots
554
555    qcd_slots.allow_update = False
556
557    update_collection_tree(context)
558    context.scene.collection_manager.cm_list_collection.clear()
559    create_property_group(context, collection_tree)
560
561    qcd_slots.allow_update = True
562
563
564def create_property_group(context, tree):
565    global in_filter
566    global qcd_slots
567
568    cm = context.scene.collection_manager
569
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"], "")
574
575        if laycol["has_children"]:
576            create_property_group(context, laycol["children"])
577
578
579def get_modifiers(event):
580    modifiers = []
581
582    if event.alt:
583        modifiers.append("alt")
584
585    if event.ctrl:
586        modifiers.append("ctrl")
587
588    if event.oskey:
589        modifiers.append("oskey")
590
591    if event.shift:
592        modifiers.append("shift")
593
594    return set(modifiers)
595
596
597def generate_state(*, qcd=False):
598    global layer_collections
599    global qcd_slots
600
601    state = {
602        "name": [],
603        "exclude": [],
604        "select": [],
605        "hide": [],
606        "disable": [],
607        "render": [],
608        "holdout": [],
609        "indirect": [],
610        }
611
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)
621
622    if qcd:
623        state["qcd"] = dict(qcd_slots)
624
625    return state
626
627
628def get_move_selection(*, names_only=False):
629    global move_selection
630
631    if not move_selection:
632        move_selection = {obj.name for obj in bpy.context.selected_objects}
633
634    if names_only:
635        return move_selection
636
637    else:
638        if len(move_selection) <= 5:
639            return {bpy.data.objects[name] for name in move_selection}
640
641        else:
642            return {obj for obj in bpy.data.objects if obj.name in move_selection}
643
644
645def get_move_active():
646    global move_active
647    global move_selection
648
649    if not move_active:
650        move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
651
652    if move_active not in get_move_selection(names_only=True):
653        move_active = None
654
655    return bpy.data.objects[move_active] if move_active else None
656
657
658def 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"
663
664
665class CMSendReport(Operator):
666    bl_label = "Send Report"
667    bl_idname = "view3d.cm_send_report"
668
669    message: StringProperty()
670
671    def draw(self, context):
672        layout = self.layout
673        col = layout.column(align=True)
674
675        first = True
676        string = ""
677
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')
685
686                string = ""
687                continue
688
689            string = string + char
690
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')
695
696    def invoke(self, context, event):
697        wm = context.window_manager
698
699        max_len = 0
700        length = 0
701
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
709
710        if length > max_len:
711            max_len = length
712
713        return wm.invoke_popup(self, width=(30 + (max_len*5.5)))
714
715    def execute(self, context):
716        self.report({'INFO'}, self.message)
717        print(self.message)
718        return {'FINISHED'}
719
720def 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)
725
726    bpy.app.timers.register(report)
727