Collection Manager: Add Operator. Task: T69577

Adds a Remove Empty Collections operator in a new specials menu
in the main Collection Manager popup.

This operator has two modes:
Mode one only removes collections if they don't have subcollections
or objects.
Mode two removes all collections that don't contain objects.

Both of these modes are accessible via the new specials menu.
This commit is contained in:
Ryan Inch 2020-07-22 02:28:41 -04:00
parent 39b8dbb572
commit 711efc3e2c
4 changed files with 174 additions and 83 deletions

View File

@ -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,
)

View File

@ -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"]

View File

@ -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)
link_child_collections_to_parent(laycol, collection, parent_collection)
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)
# 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)
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 -= 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"]
# update qcd
if qcd_slots.contains(name=self.collection_name):
qcd_slots.del_slot(name=self.collection_name)
if self.collection_name in qcd_slots.overrides:
qcd_slots.overrides.remove(self.collection_name)
# reset history
for rto in rto_history.values():
rto.clear()
# 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"
)
return tooltip
def execute(self, context):
global rto_history
global expand_history
global qcd_slots
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
# 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")
return {'FINISHED'}
rename = [False]
class CMNewCollectionOperator(Operator):
bl_label = "Add New Collection"

View File

@ -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