Skip to content
Snippets Groups Projects
operators.py 49.6 KiB
Newer Older
# SPDX-License-Identifier: GPL-2.0-or-later
from copy import deepcopy

from bpy.types import (
    Operator,
)

from bpy.props import (
    BoolProperty,
    StringProperty,
    IntProperty
from .internals import (
    update_property_group,
    generate_state,
    check_state,
    get_move_selection,
    get_move_active,
from .operator_utils import (
    apply_to_children,
    isolate_rto,
    toggle_children,
    activate_all_rtos,
    invert_rtos,
    copy_rtos,
    swap_rtos,
    clear_copy,
    clear_swap,
    link_child_collections_to_parent,
    remove_collection,
    select_collection_objects,
    set_exclude_state,
    isolate_sel_objs_collections,
    disable_sel_objs_collections,
class SetActiveCollection(Operator):
    '''Set the active collection'''
    bl_label = "Set Active Collection"
    bl_idname = "view3d.set_active_collection"
    bl_options = {'UNDO'}
    is_master_collection: BoolProperty()
    collection_name: StringProperty()

    def execute(self, context):
        if self.is_master_collection:
            layer_collection = context.view_layer.layer_collection

        else:
            laycol = internals.layer_collections[self.collection_name]
            layer_collection = laycol["ptr"]

            # set selection to this row
            cm = context.scene.collection_manager
            cm.cm_list_index = laycol["row_index"]

        context.view_layer.active_layer_collection = layer_collection

        if context.view_layer.active_layer_collection != layer_collection:
            self.report({'WARNING'}, "Can't set excluded collection as active")

class ExpandAllOperator(Operator):
    '''Expand/Collapse all collections'''
    bl_label = "Expand All Items"
    bl_idname = "view3d.expand_all_items"
    def execute(self, context):
        if len(internals.expanded) > 0:
            internals.expanded.clear()
            context.scene.collection_manager.cm_list_index = 0
            for laycol in internals.layer_collections.values():
                if laycol["ptr"].children:
                    internals.expanded.add(laycol["name"])
        # clear expand history
        internals.expand_history["target"] = ""
        internals.expand_history["history"].clear()
        # update tree view
        update_property_group(context)
class ExpandSublevelOperator(Operator):
    bl_label = "Expand Sublevel Items"
    bl_description = (
        "  * Ctrl+LMB - Expand/Collapse all sublevels\n"
        "  * Shift+LMB - Isolate tree/Restore\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.expand_sublevel"
    expand: BoolProperty()
    name: StringProperty()
    index: IntProperty()
    def invoke(self, context, event):
        cls = ExpandSublevelOperator

        modifiers = get_modifiers(event)

        if modifiers == {"alt"}:
            internals.expand_history["target"] = ""
            internals.expand_history["history"].clear()
            # expand/collapse all subcollections
            expand = None
            # check whether to expand or collapse
            if self.name in internals.expanded:
                internals.expanded.remove(self.name)
                internals.expanded.add(self.name)
            # do expanding/collapsing
            def set_expanded(layer_collection):
                if expand:
                    internals.expanded.add(layer_collection.name)
                    internals.expanded.discard(layer_collection.name)
            apply_to_children(internals.layer_collections[self.name]["ptr"], set_expanded)
            internals.expand_history["target"] = ""
            internals.expand_history["history"].clear()

        elif modifiers == {"shift"}:
            def isolate_tree(current_laycol):
                parent = current_laycol["parent"]

                for laycol in parent["children"]:
                    if (laycol["name"] != current_laycol["name"]
                    and laycol["name"] in internals.expanded):
                        internals.expanded.remove(laycol["name"])
                        internals.expand_history["history"].append(laycol["name"])

                if parent["parent"]:
                    isolate_tree(parent)

            if self.name == internals.expand_history["target"]:
                for item in internals.expand_history["history"]:
                    internals.expanded.add(item)
                internals.expand_history["target"] = ""
                internals.expand_history["history"].clear()
                internals.expand_history["target"] = ""
                internals.expand_history["history"].clear()
                isolate_tree(internals.layer_collections[self.name])
                internals.expand_history["target"] = self.name
        else:
            # expand/collapse collection
            if self.expand:
                internals.expanded.add(self.name)
                internals.expanded.remove(self.name)
            internals.expand_history["target"] = ""
            internals.expand_history["history"].clear()
        # set the selected row to the collection you're expanding/collapsing to
        # preserve the tree view's scrolling
        context.scene.collection_manager.cm_list_index = self.index
        update_property_group(context)
class CMSelectCollectionObjectsOperator(Operator):
    bl_label = "Select All Objects in the Collection"
    bl_description = (
        "  * LMB - Select all objects in collection.\n"
        "  * Shift+LMB - Add/Remove collection objects from selection.\n"
        "  * Ctrl+LMB - Isolate nested selection.\n"
        "  * Ctrl+Shift+LMB - Add/Remove nested from selection"
        )
    bl_idname = "view3d.select_collection_objects"
    bl_options = {'REGISTER', 'UNDO'}

    is_master_collection: BoolProperty()
    collection_name: StringProperty()

    def invoke(self, context, event):
        modifiers = get_modifiers(event)

        if modifiers == {"shift"}:
            select_collection_objects(
                is_master_collection=self.is_master_collection,
                collection_name=self.collection_name,
                replace=False,
                nested=False
                )

        elif modifiers == {"ctrl"}:
            select_collection_objects(
                is_master_collection=self.is_master_collection,
                collection_name=self.collection_name,
                replace=True,
                nested=True
                )

        elif modifiers == {"ctrl", "shift"}:
            select_collection_objects(
                is_master_collection=self.is_master_collection,
                collection_name=self.collection_name,
                replace=False,
                nested=True
                )

        else:
            select_collection_objects(
                is_master_collection=self.is_master_collection,
                collection_name=self.collection_name,
                replace=True,
                nested=False
                )

        return {'FINISHED'}


class SelectAllCumulativeObjectsOperator(Operator):
    '''Select all objects that are present in more than one collection'''
    bl_label = "Select All Cumulative Objects"
    bl_idname = "view3d.select_all_cumulative_objects"

    def execute(self, context):
        selected_cumulative_objects = 0
        total_cumulative_objects = 0

        bpy.ops.object.select_all(action='DESELECT')

        for obj in bpy.data.objects:
            if len(obj.users_collection) > 1:
                if obj.visible_get():
                    obj.select_set(True)
                    if obj.select_get() == True: # needed because obj.select_set can fail silently
                        selected_cumulative_objects +=1

                total_cumulative_objects += 1

        self.report({'INFO'}, f"{selected_cumulative_objects}/{total_cumulative_objects} Cumulative Objects Selected")

        return {'FINISHED'}


class CMSendObjectsToCollectionOperator(Operator):
    bl_label = "Send Objects to Collection"
    bl_description = (
        "  * LMB - Move objects to collection.\n"
        "  * Shift+LMB - Add/Remove objects from collection"
    bl_idname = "view3d.send_objects_to_collection"
    bl_options = {'REGISTER', 'UNDO'}
    is_master_collection: BoolProperty()
    collection_name: StringProperty()
    def invoke(self, context, event):
        if self.is_master_collection:
            target_collection = context.view_layer.layer_collection.collection

        else:
            laycol = internals.layer_collections[self.collection_name]
            target_collection = laycol["ptr"].collection

        selected_objects = get_move_selection()
        active_object = get_move_active()

        internals.move_triggered = True

        if not selected_objects:
            return {'CANCELLED'}
            # add objects to collection

            # make sure there is an active object
            if not active_object:
                active_object = tuple(selected_objects)[0]
            # check if in collection
            if not active_object.name in target_collection.objects:
                # add to collection
                for obj in selected_objects:
                    if obj.name not in target_collection.objects:
                        target_collection.objects.link(obj)
                warnings = False
                master_warning = False

                # remove from collections
                for obj in selected_objects:
                    if obj.name in target_collection.objects:

                        # disallow removing if only one
                        if len(obj.users_collection) == 1:
                            warnings = True
                            master_laycol = context.view_layer.layer_collection
                            master_collection = master_laycol.collection

                            if obj.name not in master_collection.objects:
                                master_collection.objects.link(obj)

                            else:
                                master_warning = True
                                continue


                        # remove from collection
                        target_collection.objects.unlink(obj)
                if warnings:
                    if master_warning:
                        send_report(
                        "Error removing 1 or more objects from the Scene Collection.\n"
                        "Objects would be left without a collection."
                        )
                        self.report({"WARNING"},
                        "Error removing 1 or more objects from the Scene Collection."
                        "  Objects would be left without a collection."
                        )

                    else:
                        self.report({"INFO"}, "1 or more objects moved to Scene Collection.")
            # move objects to collection
            for obj in selected_objects:
                if obj.name not in target_collection.objects:
                    target_collection.objects.link(obj)

                # remove from all other collections
                for collection in obj.users_collection:
                    if collection != target_collection:
                        collection.objects.unlink(obj)

        # update the active object if needed
        if not context.active_object:
            try:
                context.view_layer.objects.active = active_object

            except RuntimeError: # object not in visible collection
                pass

        # update qcd header UI
class CMExcludeOperator(Operator):
    bl_label = "[EC] Exclude from View Layer"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.exclude_collection"
    bl_options = {'REGISTER', 'UNDO'}
    name: StringProperty()
    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMExcludeOperator
        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        orig_active_collection = context.view_layer.active_layer_collection
        orig_active_object = context.view_layer.objects.active
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["exclude"]:
            internals.rto_history["exclude"][view_layer] = {"target": "", "history": []}
        if modifiers == {"alt"}:
            del internals.rto_history["exclude"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "exclude")
        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "exclude")
            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "exclude", children=True)
        else:
            # toggle exclusion
            # reset exclude history
            del internals.rto_history["exclude"][view_layer]
            set_exclude_state(laycol_ptr, not laycol_ptr.exclude)
            cls.isolated = False
        # restore active collection
        context.view_layer.active_layer_collection = orig_active_collection

        # restore active object if possible
        if orig_active_object:
            if orig_active_object.name in context.view_layer.objects:
                context.view_layer.objects.active = orig_active_object

        # reset exclude all history
        if view_layer in internals.rto_history["exclude_all"]:
            del internals.rto_history["exclude_all"][view_layer]
class CMUnExcludeAllOperator(Operator):
    bl_label = "[EC Global] Exclude from View Layer"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_exclude_all_collections"
    bl_options = {'REGISTER', 'UNDO'}
    def invoke(self, context, event):
        orig_active_collection = context.view_layer.active_layer_collection
        orig_active_object = context.view_layer.objects.active
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)
        if not view_layer in internals.rto_history["exclude_all"]:
            internals.rto_history["exclude_all"][view_layer] = []
        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["exclude_all"][view_layer]
            clear_copy("exclude")
            clear_swap("exclude")
        elif modifiers == {"ctrl"}:
        elif modifiers == {"ctrl", "alt"}:
        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "exclude")
        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "exclude", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "exclude", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

            activate_all_rtos(view_layer, "exclude")
        # restore active collection
        context.view_layer.active_layer_collection = orig_active_collection

        # restore active object if possible
        if orig_active_object:
            if orig_active_object.name in context.view_layer.objects:
                context.view_layer.objects.active = orig_active_object

class CMRestrictSelectOperator(Operator):
    bl_label = "[SS] Disable Selection"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.restrict_select_collection"
    bl_options = {'REGISTER', 'UNDO'}
    name: StringProperty()
    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMRestrictSelectOperator
        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["select"]:
            internals.rto_history["select"][view_layer] = {"target": "", "history": []}
        if modifiers == {"alt"}:
            del internals.rto_history["select"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "select")
        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "select")
            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "select", children=True)
            # reset select history
            del internals.rto_history["select"][view_layer]
            # toggle selectability of collection
            laycol_ptr.collection.hide_select = not laycol_ptr.collection.hide_select
            cls.isolated = False
        # reset select all history
        if view_layer in internals.rto_history["select_all"]:
            del internals.rto_history["select_all"][view_layer]
class CMUnRestrictSelectAllOperator(Operator):
    bl_label = "[SS Global] Disable Selection"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_restrict_select_all_collections"
    bl_options = {'REGISTER', 'UNDO'}
    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)
        if not view_layer in internals.rto_history["select_all"]:
            internals.rto_history["select_all"][view_layer] = []
        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["select_all"][view_layer]
            clear_copy("select")
            clear_swap("select")
        elif modifiers == {"ctrl"}:
        elif modifiers == {"ctrl", "alt"}:
        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "select")
        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "select", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "select", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

            activate_all_rtos(view_layer, "select")
class CMHideOperator(Operator):
    bl_label = "[VV] Hide in Viewport"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.hide_collection"
    bl_options = {'REGISTER', 'UNDO'}
    name: StringProperty()
    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMHideOperator
        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["hide"]:
            internals.rto_history["hide"][view_layer] = {"target": "", "history": []}
        if modifiers == {"alt"}:
            del internals.rto_history["hide"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "hide")

        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "hide")
            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "hide", children=True)
            # reset hide history
            del internals.rto_history["hide"][view_layer]
            # toggle view of collection
            laycol_ptr.hide_viewport = not laycol_ptr.hide_viewport
            cls.isolated = False
        # reset hide all history
        if view_layer in internals.rto_history["hide_all"]:
            del internals.rto_history["hide_all"][view_layer]
class CMUnHideAllOperator(Operator):
    bl_label = "[VV Global] Hide in Viewport"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_hide_all_collections"
    bl_options = {'REGISTER', 'UNDO'}
    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)
        if not view_layer in internals.rto_history["hide_all"]:
            internals.rto_history["hide_all"][view_layer] = []
        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["hide_all"][view_layer]
            clear_copy("hide")
            clear_swap("hide")
        elif modifiers == {"ctrl"}:
        elif modifiers == {"ctrl", "alt"}:
        elif modifiers == {"shift"}:
        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "hide", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "hide", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

            activate_all_rtos(view_layer, "hide")
class CMDisableViewportOperator(Operator):
    bl_label = "[DV] Disable in Viewports"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.disable_viewport_collection"
    bl_options = {'REGISTER', 'UNDO'}
    name: StringProperty()
    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMDisableViewportOperator
        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["disable"]:
            internals.rto_history["disable"][view_layer] = {"target": "", "history": []}
        if modifiers == {"alt"}:
            del internals.rto_history["disable"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "disable")

        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "disable")
            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "disable", children=True)
            # reset disable history
            del internals.rto_history["disable"][view_layer]
            # toggle disable of collection in viewport
            laycol_ptr.collection.hide_viewport = not laycol_ptr.collection.hide_viewport
            cls.isolated = False
        # reset disable all history
        if view_layer in internals.rto_history["disable_all"]:
            del internals.rto_history["disable_all"][view_layer]
class CMUnDisableViewportAllOperator(Operator):
    bl_label = "[DV Global] Disable in Viewports"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_disable_viewport_all_collections"
    bl_options = {'REGISTER', 'UNDO'}
    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)
        if not view_layer in internals.rto_history["disable_all"]:
            internals.rto_history["disable_all"][view_layer] = []
        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["disable_all"][view_layer]
            clear_copy("disable")
            clear_swap("disable")
        elif modifiers == {"ctrl"}:
        elif modifiers == {"ctrl", "alt"}:
        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "disable")
        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "disable", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "disable", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

            activate_all_rtos(view_layer, "disable")
class CMDisableRenderOperator(Operator):
    bl_label = "[RR] Disable in Renders"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.disable_render_collection"
    bl_options = {'REGISTER', 'UNDO'}
    name: StringProperty()
    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMDisableRenderOperator
        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["render"]:
            internals.rto_history["render"][view_layer] = {"target": "", "history": []}
            del internals.rto_history["render"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "render")

        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "render")
            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "render", children=True)
            # reset render history
            del internals.rto_history["render"][view_layer]
            # toggle renderability of collection
            laycol_ptr.collection.hide_render = not laycol_ptr.collection.hide_render
            cls.isolated = False
        # reset render all history
        if view_layer in internals.rto_history["render_all"]:
            del internals.rto_history["render_all"][view_layer]
class CMUnDisableRenderAllOperator(Operator):
    bl_label = "[RR Global] Disable in Renders"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_disable_render_all_collections"
    bl_options = {'REGISTER', 'UNDO'}
    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)
        if not view_layer in internals.rto_history["render_all"]:
            internals.rto_history["render_all"][view_layer] = []
        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["render_all"][view_layer]
            clear_copy("render")
            clear_swap("render")
        elif modifiers == {"ctrl"}:
        elif modifiers == {"ctrl", "alt"}:
        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "render")
        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "render", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "render", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

            activate_all_rtos(view_layer, "render")
class CMHoldoutOperator(Operator):
    bl_label = "[HH] Holdout"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.holdout_collection"
    bl_options = {'REGISTER', 'UNDO'}

    name: StringProperty()

    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMHoldoutOperator

        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["holdout"]:
            internals.rto_history["holdout"][view_layer] = {"target": "", "history": []}
            del internals.rto_history["holdout"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "holdout")

        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "holdout")

            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "holdout", children=True)

        else:
            # toggle holdout

            # reset holdout history
            del internals.rto_history["holdout"][view_layer]

            # toggle holdout of collection in viewport
            laycol_ptr.holdout = not laycol_ptr.holdout

            cls.isolated = False

        # reset holdout all history
        if view_layer in internals.rto_history["holdout_all"]:
            del internals.rto_history["holdout_all"][view_layer]

        return {'FINISHED'}


class CMUnHoldoutAllOperator(Operator):
    bl_label = "[HH Global] Holdout"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_holdout_all_collections"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)

        if not view_layer in internals.rto_history["holdout_all"]:
            internals.rto_history["holdout_all"][view_layer] = []

        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["holdout_all"][view_layer]
            clear_copy("holdout")
            clear_swap("holdout")

        elif modifiers == {"ctrl"}:
            copy_rtos(view_layer, "holdout")

        elif modifiers == {"ctrl", "alt"}:
            swap_rtos(view_layer, "holdout")

        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "holdout")

        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "holdout", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "holdout", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        else:
            activate_all_rtos(view_layer, "holdout")

        return {'FINISHED'}


class CMIndirectOnlyOperator(Operator):
    bl_label = "[IO] Indirect Only"
    bl_description = (
        "  * Shift+LMB - Isolate/Restore.\n"
        "  * Shift+Ctrl+LMB - Isolate nested/Restore.\n"
        "  * Ctrl+LMB - Toggle nested.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.indirect_only_collection"
    bl_options = {'REGISTER', 'UNDO'}

    name: StringProperty()

    # static class var
    isolated = False

    def invoke(self, context, event):
        cls = CMIndirectOnlyOperator

        modifiers = get_modifiers(event)
        view_layer = context.view_layer.name
        laycol_ptr = internals.layer_collections[self.name]["ptr"]
        if not view_layer in internals.rto_history["indirect"]:
            internals.rto_history["indirect"][view_layer] = {"target": "", "history": []}
            del internals.rto_history["indirect"][view_layer]
            cls.isolated = False

        elif modifiers == {"shift"}:
            isolate_rto(cls, self, view_layer, "indirect")

        elif modifiers == {"ctrl"}:
            toggle_children(self, view_layer, "indirect")

            cls.isolated = False

        elif modifiers == {"ctrl", "shift"}:
            isolate_rto(cls, self, view_layer, "indirect", children=True)

        else:
            # toggle indirect only

            # reset indirect history
            del internals.rto_history["indirect"][view_layer]

            # toggle indirect only of collection
            laycol_ptr.indirect_only = not laycol_ptr.indirect_only

            cls.isolated = False

        # reset indirect all history
        if view_layer in internals.rto_history["indirect_all"]:
            del internals.rto_history["indirect_all"][view_layer]

        return {'FINISHED'}


class CMUnIndirectOnlyAllOperator(Operator):
    bl_label = "[IO Global] Indirect Only"
    bl_description = (
        "  * LMB - Enable all/Restore.\n"
        "  * Shift+LMB - Invert.\n"
        "  * Shift+Ctrl+LMB - Isolate collections w/ selected objects.\n"
        "  * Shift+Alt+LMB - Disable collections w/ selected objects.\n"
        "  * Ctrl+LMB - Copy/Paste RTOs.\n"
        "  * Ctrl+Alt+LMB - Swap RTOs.\n"
        "  * Alt+LMB - Discard history"
        )
    bl_idname = "view3d.un_indirect_only_all_collections"
    bl_options = {'REGISTER', 'UNDO'}

    def invoke(self, context, event):
        view_layer = context.view_layer.name
        modifiers = get_modifiers(event)

        if not view_layer in internals.rto_history["indirect_all"]:
            internals.rto_history["indirect_all"][view_layer] = []

        if modifiers == {"alt"}:
            # clear all states
            del internals.rto_history["indirect_all"][view_layer]
            clear_copy("indirect")
            clear_swap("indirect")

        elif modifiers == {"ctrl"}:
            copy_rtos(view_layer, "indirect")

        elif modifiers == {"ctrl", "alt"}:
            swap_rtos(view_layer, "indirect")

        elif modifiers == {"shift"}:
            invert_rtos(view_layer, "indirect")

        elif modifiers == {"shift", "ctrl"}:
            error = isolate_sel_objs_collections(view_layer, "indirect", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        elif modifiers == {"shift", "alt"}:
            error = disable_sel_objs_collections(view_layer, "indirect", "CM")

            if error:
                self.report({"WARNING"}, error)
                return {'CANCELLED'}

        else:
            activate_all_rtos(view_layer, "indirect")

        return {'FINISHED'}


class CMRemoveCollectionOperator(Operator):
    '''Remove Collection'''
    bl_label = "Remove Collection"
    bl_idname = "view3d.remove_collection"
    bl_options = {'UNDO'}
    collection_name: StringProperty()
    def execute(self, context):
        laycol = internals.layer_collections[self.collection_name]
        collection = laycol["ptr"].collection
        parent_collection = laycol["parent"]["ptr"].collection
        # shift all objects in this collection to the parent collection
        for obj in collection.objects:
            if obj.name not in parent_collection.objects:
                parent_collection.objects.link(obj)
        # shift all child collections to the parent collection preserving view layer RTOs
        if collection.children:
            link_child_collections_to_parent(laycol, collection, parent_collection)
        # remove collection, update references, and update tree view
        remove_collection(laycol, collection, context)
        return {'FINISHED'}
class CMRemoveEmptyCollectionsOperator(Operator):
    bl_label = "Remove Empty Collections"
    bl_idname = "view3d.remove_empty_collections"
    bl_options = {'UNDO'}
    without_objects: BoolProperty()
    @classmethod
    def description(cls, context, properties):
        if properties.without_objects:
            tooltip = (
                "Purge All Collections Without Objects.\n"
                "Deletes all collections that don't contain objects even if they have subcollections"
                )
        else:
            tooltip = (
                "Remove Empty Collections.\n"
                "Delete collections that don't have any subcollections or objects"
                )
    def execute(self, context):
        if self.without_objects:
            empty_collections = [laycol["name"]
                                for laycol in internals.layer_collections.values()
                                if not laycol["ptr"].collection.objects]
        else:
            empty_collections = [laycol["name"]
                                for laycol in internals.layer_collections.values()
                                if not laycol["children"] and
                                not laycol["ptr"].collection.objects]
        for name in empty_collections:
            laycol = internals.layer_collections[name]
            collection = laycol["ptr"].collection
            parent_collection = laycol["parent"]["ptr"].collection
            # link all child collections to the parent collection preserving view layer RTOs
            if collection.children:
                link_child_collections_to_parent(laycol, collection, parent_collection)
            # remove collection, update references, and update tree view
            remove_collection(laycol, collection, context)
        self.report({"INFO"}, f"Removed {len(empty_collections)} collections")
rename = [False]
class CMNewCollectionOperator(Operator):
    bl_label = "Add New Collection"
    bl_idname = "view3d.add_collection"
    bl_options = {'UNDO'}
    child: BoolProperty()
    @classmethod
    def description(cls, context, properties):
        if properties.child:
            tooltip = (
                "Add New SubCollection.\n"
                "Add a new subcollection to the currently selected collection"
                )

        else:
            tooltip = (
                "Add New Collection.\n"
                "Add a new collection as a sibling of the currently selected collection"
                )

        return tooltip

    def execute(self, context):
        new_collection = bpy.data.collections.new("New Collection")
        cm = context.scene.collection_manager

        # prevent adding collections when collections are filtered
        # and the selection is ambiguous
        if cm.cm_list_index == -1 and ui.CM_UL_items.filtering:
            send_report("Cannot create new collection.\n"
                        "No collection is selected and collections are filtered."
                       )
            return {'CANCELLED'}

        if cm.cm_list_index > -1 and not ui.CM_UL_items.visible_items[cm.cm_list_index]:
            send_report("Cannot create new collection.\n"
                        "The selected collection isn't visible."
                       )
            return {'CANCELLED'}

        # if there are collections
        if len(cm.cm_list_collection) > 0:
            if not cm.cm_list_index == -1:
                # get selected collection
                laycol = internals.layer_collections[cm.cm_list_collection[cm.cm_list_index].name]
                # add new collection
                if self.child:
                    laycol["ptr"].collection.children.link(new_collection)
                    internals.expanded.add(laycol["name"])
                    # update tree view property
                    update_property_group(context)

                    cm.cm_list_index = internals.layer_collections[new_collection.name]["row_index"]

                else:
                    laycol["parent"]["ptr"].collection.children.link(new_collection)

                    # update tree view property
                    update_property_group(context)
                    cm.cm_list_index = internals.layer_collections[new_collection.name]["row_index"]
                context.scene.collection.children.link(new_collection)
                # update tree view property
                update_property_group(context)
                cm.cm_list_index = internals.layer_collections[new_collection.name]["row_index"]
        # if no collections add top level collection and select it
        else:
            context.scene.collection.children.link(new_collection)
            # update tree view property
            update_property_group(context)
            cm.cm_list_index = 0

        # set new collection to active
        layer_collection = internals.layer_collections[new_collection.name]["ptr"]
        context.view_layer.active_layer_collection = layer_collection

        # show the new collection when collections are filtered.
        ui.CM_UL_items.new_collections.append(new_collection.name)

        global rename
        rename[0] = True
        for rto in internals.rto_history.values():
        return {'FINISHED'}
class CMPhantomModeOperator(Operator):
    bl_label = "Toggle Phantom Mode"
    bl_idname = "view3d.toggle_phantom_mode"
    bl_description = (
        "Phantom Mode\n"
        "Saves the state of all RTOs and only allows changes to them, on exit all RTOs are returned to their saved state.\n"
        "Note: modifying collections (except RTOs) externally will exit Phantom Mode and your initial state will be lost"
        )
    def execute(self, context):
        cm = context.scene.collection_manager
        view_layer = context.view_layer
        # enter Phantom Mode
        if not cm.in_phantom_mode:
            cm.in_phantom_mode = True
            # save current visibility state
            internals.phantom_history["view_layer"] = view_layer.name
            def save_visibility_state(layer_collection):
                internals.phantom_history["initial_state"][layer_collection.name] = {
                            "exclude": layer_collection.exclude,
                            "select": layer_collection.collection.hide_select,
                            "hide": layer_collection.hide_viewport,
                            "disable": layer_collection.collection.hide_viewport,
                            "render": layer_collection.collection.hide_render,
                            "holdout": layer_collection.holdout,
                            "indirect": layer_collection.indirect_only,
            apply_to_children(view_layer.layer_collection, save_visibility_state)
            # save current rto history
            for rto, history, in internals.rto_history.items():
                if history.get(view_layer.name, None):
                    internals.phantom_history[rto+"_history"] = deepcopy(history[view_layer.name])
        else: # return to normal mode
            def restore_visibility_state(layer_collection):
                phantom_laycol = internals.phantom_history["initial_state"][layer_collection.name]
                layer_collection.exclude = phantom_laycol["exclude"]
                layer_collection.collection.hide_select = phantom_laycol["select"]
                layer_collection.hide_viewport = phantom_laycol["hide"]
                layer_collection.collection.hide_viewport = phantom_laycol["disable"]
                layer_collection.collection.hide_render = phantom_laycol["render"]
                layer_collection.holdout = phantom_laycol["holdout"]
                layer_collection.indirect_only = phantom_laycol["indirect"]
            apply_to_children(view_layer.layer_collection, restore_visibility_state)
            # restore previous rto history
            for rto, history, in internals.rto_history.items():
                if view_layer.name in history:
                    del history[view_layer.name]
                if internals.phantom_history[rto+"_history"]:
                    history[view_layer.name] = deepcopy(internals.phantom_history[rto+"_history"])
                internals.phantom_history[rto+"_history"].clear()
            cm.in_phantom_mode = False
        return {'FINISHED'}


class CMApplyPhantomModeOperator(Operator):
    '''Apply changes and quit Phantom Mode'''
    bl_label = "Apply Phantom Mode"
    bl_idname = "view3d.apply_phantom_mode"

    def execute(self, context):
        cm = context.scene.collection_manager
        cm.in_phantom_mode = False

        return {'FINISHED'}


class CMDisableObjectsOperator(Operator):
    '''Disable selected objects in viewports'''
    bl_label = "Disable Selected"
    bl_idname = "view3d.disable_selected_objects"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        for obj in context.selected_objects:
            obj.hide_viewport = True

        return {'FINISHED'}


class CMDisableUnSelectedObjectsOperator(Operator):
    '''Disable unselected objects in viewports'''
    bl_label = "Disable Unselected"
    bl_idname = "view3d.disable_unselected_objects"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        for obj in bpy.data.objects:
            if obj in context.visible_objects and not obj in context.selected_objects:
                obj.hide_viewport = True

        return {'FINISHED'}


class CMRestoreDisabledObjectsOperator(Operator):
    '''Restore disabled objects in viewports'''
    bl_label = "Restore Disabled Objects"
    bl_idname = "view3d.restore_disabled_objects"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        for obj in bpy.data.objects:
            if obj.hide_viewport:
                obj.hide_viewport = False
                obj.select_set(True)

        return {'FINISHED'}


class CMUndoWrapper(Operator):
    bl_label = "Undo"
    bl_description = "Undo previous action"
    bl_idname = "view3d.undo_wrapper"

    @classmethod
    def poll(self, context):
        return bpy.ops.ed.undo.poll()

    def execute(self, context):
        internals.collection_state.clear()
        internals.collection_state.update(generate_state())
        bpy.ops.ed.undo()
        update_property_group(context)

        check_state(context, cm_popup=True)

        # clear buffers
        internals.copy_buffer["RTO"] = ""
        internals.copy_buffer["values"].clear()

        internals.swap_buffer["A"]["RTO"] = ""
        internals.swap_buffer["A"]["values"].clear()
        internals.swap_buffer["B"]["RTO"] = ""
        internals.swap_buffer["B"]["values"].clear()

        return {'FINISHED'}


class CMRedoWrapper(Operator):
    bl_label = "Redo"
    bl_description = "Redo previous action"
    bl_idname = "view3d.redo_wrapper"

    @classmethod
    def poll(self, context):
        return bpy.ops.ed.redo.poll()

    def execute(self, context):
        bpy.ops.ed.redo()
        update_property_group(context)

        return {'FINISHED'}