diff options
-rw-r--r-- | object_collection_manager/__init__.py | 4 | ||||
-rw-r--r-- | object_collection_manager/operator_utils.py | 82 | ||||
-rw-r--r-- | object_collection_manager/operators.py | 106 | ||||
-rw-r--r-- | object_collection_manager/ui.py | 39 |
4 files changed, 161 insertions, 70 deletions
diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py index f6a31695..90783e7e 100644 --- a/object_collection_manager/__init__.py +++ b/object_collection_manager/__init__.py @@ -22,7 +22,7 @@ bl_info = { "name": "Collection Manager", "description": "Manage collections and their objects", "author": "Ryan Inch", - "version": (2, 9, 5), + "version": (2, 10, 0), "blender": (2, 80, 0), "location": "View3D - Object Mode (Shortcut - M)", "warning": '', # used for warning icon and text in addons panel @@ -110,12 +110,14 @@ classes = ( operators.CMUnDisableRenderAllOperator, operators.CMNewCollectionOperator, operators.CMRemoveCollectionOperator, + operators.CMRemoveEmptyCollectionsOperator, operators.CMSetCollectionOperator, operators.CMPhantomModeOperator, preferences.CMPreferences, ui.CM_UL_items, ui.CollectionManager, ui.CMDisplayOptionsPanel, + ui.SpecialsMenu, CollectionManagerProperties, ) diff --git a/object_collection_manager/operator_utils.py b/object_collection_manager/operator_utils.py index f99d870b..d86a534f 100644 --- a/object_collection_manager/operator_utils.py +++ b/object_collection_manager/operator_utils.py @@ -17,12 +17,17 @@ # ##### END GPL LICENSE BLOCK ##### # Copyright 2011, Ryan Inch +import bpy from .internals import ( layer_collections, + qcd_slots, + expanded, + expand_history, rto_history, copy_buffer, swap_buffer, + update_property_group, ) rto_path = { @@ -289,3 +294,80 @@ def clear_swap(rto): swap_buffer["A"]["values"].clear() swap_buffer["B"]["RTO"] = "" swap_buffer["B"]["values"].clear() + + +def link_child_collections_to_parent(laycol, collection, parent_collection): + # store view layer RTOs for all children of the to be deleted collection + child_states = {} + def get_child_states(layer_collection): + child_states[layer_collection.name] = (layer_collection.exclude, + layer_collection.hide_viewport, + layer_collection.holdout, + layer_collection.indirect_only) + + apply_to_children(laycol["ptr"], get_child_states) + + # link any subcollections of the to be deleted collection to it's parent + for subcollection in collection.children: + if not subcollection.name in parent_collection.children: + parent_collection.children.link(subcollection) + + # apply the stored view layer RTOs to the newly linked collections and their + # children + def restore_child_states(layer_collection): + state = child_states.get(layer_collection.name) + + if state: + layer_collection.exclude = state[0] + layer_collection.hide_viewport = state[1] + layer_collection.holdout = state[2] + layer_collection.indirect_only = state[3] + + apply_to_children(laycol["parent"]["ptr"], restore_child_states) + + +def remove_collection(laycol, collection, context): + # get selected row + cm = context.scene.collection_manager + selected_row_name = cm.cm_list_collection[cm.cm_list_index].name + + # delete collection + bpy.data.collections.remove(collection) + + # update references + expanded.discard(laycol["name"]) + + if expand_history["target"] == laycol["name"]: + expand_history["target"] = "" + + if laycol["name"] in expand_history["history"]: + expand_history["history"].remove(laycol["name"]) + + if qcd_slots.contains(name=laycol["name"]): + qcd_slots.del_slot(name=laycol["name"]) + + if laycol["name"] in qcd_slots.overrides: + qcd_slots.overrides.remove(laycol["name"]) + + # reset history + for rto in rto_history.values(): + rto.clear() + + # update tree view + update_property_group(context) + + # update selected row + laycol = layer_collections.get(selected_row_name, None) + if laycol: + cm.cm_list_index = laycol["row_index"] + + elif len(cm.cm_list_collection) <= cm.cm_list_index: + cm.cm_list_index = len(cm.cm_list_collection) - 1 + + if cm.cm_list_index > -1: + name = cm.cm_list_collection[cm.cm_list_index].name + laycol = layer_collections[name] + while not laycol["visible"]: + laycol = laycol["parent"] + + cm.cm_list_index = laycol["row_index"] diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py index 77882c97..642860fa 100644 --- a/object_collection_manager/operators.py +++ b/object_collection_manager/operators.py @@ -63,6 +63,8 @@ from .operator_utils import ( swap_rtos, clear_copy, clear_swap, + link_child_collections_to_parent, + remove_collection, ) class SetActiveCollection(Operator): @@ -869,12 +871,9 @@ class CMRemoveCollectionOperator(Operator): global expand_history global qcd_slots - cm = context.scene.collection_manager - laycol = layer_collections[self.collection_name] collection = laycol["ptr"].collection parent_collection = laycol["parent"]["ptr"].collection - selected_row_name = cm.cm_list_collection[cm.cm_list_index].name # shift all objects in this collection to the parent collection @@ -885,78 +884,69 @@ class CMRemoveCollectionOperator(Operator): # shift all child collections to the parent collection preserving view layer RTOs if collection.children: - # store view layer RTOs for all children of the to be deleted collection - child_states = {} - def get_child_states(layer_collection): - child_states[layer_collection.name] = (layer_collection.exclude, - layer_collection.hide_viewport, - layer_collection.holdout, - layer_collection.indirect_only) - - apply_to_children(laycol["ptr"], get_child_states) - - # link any subcollections of the to be deleted collection to it's parent - for subcollection in collection.children: - if not subcollection.name in parent_collection.children: - parent_collection.children.link(subcollection) + link_child_collections_to_parent(laycol, collection, parent_collection) - # apply the stored view layer RTOs to the newly linked collections and their - # children - def restore_child_states(layer_collection): - state = child_states.get(layer_collection.name) - - if state: - layer_collection.exclude = state[0] - layer_collection.hide_viewport = state[1] - layer_collection.holdout = state[2] - layer_collection.indirect_only = state[3] - - apply_to_children(laycol["parent"]["ptr"], restore_child_states) + # remove collection, update references, and update tree view + remove_collection(laycol, collection, context) + return {'FINISHED'} - # remove collection, update expanded, and update tree view - bpy.data.collections.remove(collection) - expanded.discard(self.collection_name) - if expand_history["target"] == self.collection_name: - expand_history["target"] = "" - - if self.collection_name in expand_history["history"]: - expand_history["history"].remove(self.collection_name) +class CMRemoveEmptyCollectionsOperator(Operator): + bl_label = "Remove Empty Collections" + bl_idname = "view3d.remove_empty_collections" + bl_options = {'UNDO'} - update_property_group(context) + 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" + ) - # update selected row - laycol = layer_collections.get(selected_row_name, None) - if laycol: - cm.cm_list_index = laycol["row_index"] + else: + tooltip = ( + "Remove Empty Collections.\n" + "Delete collections that don't have any subcollections or objects" + ) - elif len(cm.cm_list_collection) == cm.cm_list_index: - cm.cm_list_index -= 1 + return tooltip - if cm.cm_list_index > -1: - name = cm.cm_list_collection[cm.cm_list_index].name - laycol = layer_collections[name] - while not laycol["visible"]: - laycol = laycol["parent"] + def execute(self, context): + global rto_history + global expand_history + global qcd_slots - cm.cm_list_index = laycol["row_index"] + if self.without_objects: + empty_collections = [laycol["name"] + for laycol in layer_collections.values() + if not laycol["ptr"].collection.objects] + else: + empty_collections = [laycol["name"] + for laycol in layer_collections.values() + if not laycol["children"] and + not laycol["ptr"].collection.objects] + for name in empty_collections: + laycol = layer_collections[name] + collection = laycol["ptr"].collection + parent_collection = laycol["parent"]["ptr"].collection - # update qcd - if qcd_slots.contains(name=self.collection_name): - qcd_slots.del_slot(name=self.collection_name) + # link all child collections to the parent collection preserving view layer RTOs + if collection.children: + link_child_collections_to_parent(laycol, collection, parent_collection) - if self.collection_name in qcd_slots.overrides: - qcd_slots.overrides.remove(self.collection_name) + # remove collection, update references, and update tree view + remove_collection(laycol, collection, context) - # reset history - for rto in rto_history.values(): - rto.clear() + self.report({"INFO"}, f"Removed {len(empty_collections)} collections") return {'FINISHED'} + rename = [False] class CMNewCollectionOperator(Operator): bl_label = "Add New Collection" diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py index 838c5d18..fab65c67 100644 --- a/object_collection_manager/ui.py +++ b/object_collection_manager/ui.py @@ -21,6 +21,7 @@ import bpy from bpy.types import ( + Menu, Operator, Panel, UIList, @@ -112,9 +113,9 @@ class CollectionManager(Operator): layout.row().separator() # buttons - button_row = layout.row() + button_row_1 = layout.row() - op_sec = button_row.row() + op_sec = button_row_1.row() op_sec.alignment = 'LEFT' collapse_sec = op_sec.row() @@ -138,11 +139,12 @@ class CollectionManager(Operator): renum_sec.alignment = 'LEFT' renum_sec.operator("view3d.renumerate_qcd_slots") - # filter - filter_sec = button_row.row() - filter_sec.alignment = 'RIGHT' + # menu & filter + right_sec = button_row_1.row() + right_sec.alignment = 'RIGHT' - filter_sec.popover(panel="COLLECTIONMANAGER_PT_display_options", + right_sec.menu("VIEW3D_MT_CM_specials_menu") + right_sec.popover(panel="COLLECTIONMANAGER_PT_display_options", text="", icon='FILTER') mc_box = layout.box() @@ -304,19 +306,19 @@ class CollectionManager(Operator): sort_lock=True) # add collections - addcollec_row = layout.row() - prop = addcollec_row.operator("view3d.add_collection", text="Add Collection", + button_row_2 = layout.row() + prop = button_row_2.operator("view3d.add_collection", text="Add Collection", icon='COLLECTION_NEW') prop.child = False - prop = addcollec_row.operator("view3d.add_collection", text="Add SubCollection", + prop = button_row_2.operator("view3d.add_collection", text="Add SubCollection", icon='COLLECTION_NEW') prop.child = True # phantom mode - phantom_row = layout.row() + button_row_3 = layout.row() toggle_text = "Disable " if cm.in_phantom_mode else "Enable " - phantom_row.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") + button_row_3.operator("view3d.toggle_phantom_mode", text=toggle_text+"Phantom Mode") if cm.in_phantom_mode: view.enabled = False @@ -748,6 +750,21 @@ class CMDisplayOptionsPanel(Panel): row.prop(cm, "align_local_ops") +class SpecialsMenu(Menu): + bl_label = "Specials" + bl_idname = "VIEW3D_MT_CM_specials_menu" + + def draw(self, context): + layout = self.layout + + prop = layout.operator("view3d.remove_empty_collections") + prop.without_objects = False + + prop = layout.operator("view3d.remove_empty_collections", + text="Purge All Collections Without Objects") + prop.without_objects = True + + def view3d_header_qcd_slots(self, context): layout = self.layout |