diff --git a/object_collection_manager/__init__.py b/object_collection_manager/__init__.py
index 2aa38c5bd4a8af15fc428e1caa2703b69b72d21b..1f3fdef145fb974898d290b4e1ab082dcc364916 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,4,12),
+    "version": (2,5,0),
     "blender": (2, 80, 0),
     "location": "View3D - Object Mode (Shortcut - M)",
     "warning": '',  # used for warning icon and text in addons panel
@@ -52,6 +52,7 @@ else:
     from . import preferences
 
 import bpy
+from bpy.app.handlers import persistent
 from bpy.types import PropertyGroup
 from bpy.props import (
     CollectionProperty,
@@ -108,7 +109,19 @@ classes = (
     CollectionManagerProperties,
     )
 
+@persistent
+def depsgraph_update_post_handler(dummy):
+    if internals.move_triggered:
+        internals.move_triggered = False
+        return
 
+    internals.move_selection.clear()
+    internals.move_active = None
+
+@persistent
+def undo_redo_post_handler(dummy):
+    internals.move_selection.clear()
+    internals.move_active = None
 
 def register():
     for cls in classes:
@@ -122,6 +135,10 @@ def register():
     kmi = km.keymap_items.new('view3d.collection_manager', 'M', 'PRESS')
     addon_keymaps.append((km, kmi))
 
+    bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler)
+    bpy.app.handlers.undo_post.append(undo_redo_post_handler)
+    bpy.app.handlers.redo_post.append(undo_redo_post_handler)
+
     if bpy.context.preferences.addons[__package__].preferences.enable_qcd:
         qcd_init.register_qcd()
 
@@ -132,6 +149,10 @@ def unregister():
     for cls in classes:
         bpy.utils.unregister_class(cls)
 
+    bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler)
+    bpy.app.handlers.undo_post.remove(undo_redo_post_handler)
+    bpy.app.handlers.redo_post.remove(undo_redo_post_handler)
+
     del bpy.types.Scene.collection_manager
 
     # remove keymaps when add-on is deactivated
diff --git a/object_collection_manager/internals.py b/object_collection_manager/internals.py
index 292d8cfd6d1bc6370a75b32099a9cfb4c90768db..594756c952a7c3a3e8c5893ff5b64e0f60fc9922 100644
--- a/object_collection_manager/internals.py
+++ b/object_collection_manager/internals.py
@@ -32,6 +32,10 @@ from bpy.props import (
     IntProperty,
 )
 
+move_triggered = False
+move_selection = []
+move_active = None
+
 layer_collections = {}
 collection_tree = []
 collection_state = {}
@@ -186,6 +190,7 @@ def update_col_name(self, context):
 
         self.last_name = self.name
 
+
 def update_qcd_slot(self, context):
     global qcd_slots
 
@@ -357,6 +362,7 @@ def get_modifiers(event):
 
     return set(modifiers)
 
+
 def generate_state():
     global layer_collections
 
@@ -380,6 +386,27 @@ def generate_state():
     return state
 
 
+def get_move_selection():
+    global move_selection
+
+    if not move_selection:
+        move_selection = [obj.name for obj in bpy.context.selected_objects]
+
+    return [bpy.data.objects[name] for name in move_selection]
+
+
+def get_move_active():
+    global move_active
+    global move_selection
+
+    if not move_active:
+        move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
+
+    if move_active not in [obj.name for obj in get_move_selection()]:
+        move_active = None
+
+    return bpy.data.objects[move_active] if move_active else None
+
 
 class CMSendReport(Operator):
     bl_label = "Send Report"
diff --git a/object_collection_manager/operators.py b/object_collection_manager/operators.py
index 30cf17a9540d2947e35332a1608e9638a89a4781..1094e57889c633fb2e54910d69bd5418b2191926 100644
--- a/object_collection_manager/operators.py
+++ b/object_collection_manager/operators.py
@@ -32,12 +32,16 @@ from bpy.props import (
     IntProperty
 )
 
+from . import internals
+
 from .internals import (
     expanded,
     layer_collections,
     qcd_slots,
     update_property_group,
     get_modifiers,
+    get_move_selection,
+    get_move_active,
     send_report,
 )
 
@@ -187,30 +191,74 @@ class CMSetCollectionOperator(Operator):
     collection_name: StringProperty()
 
     def invoke(self, context, event):
-        collection = layer_collections[self.collection_name]["ptr"].collection
+        laycol = 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'}
 
         if event.shift:
-            # add object to collection
+            # add objects to collection
+
+            # make sure there is an active object
+            if not active_object:
+                active_object = selected_objects[0]
 
             # check if in collection
-            if context.active_object.name not in collection.objects:
+            if not active_object.name in target_collection.objects:
                 # add to collection
-                bpy.ops.object.link_to_collection(collection_index=self.collection_index)
+                for obj in selected_objects:
+                    if obj.name not in target_collection.objects:
+                        target_collection.objects.link(obj)
 
             else:
-                # check and disallow removing from all collections
-                for obj in context.selected_objects:
-                    if len(obj.users_collection) == 1:
-                        send_report("Error removing 1 or more objects from this collection.\nObjects would be left without a collection")
+                errors = 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:
+                            errors = True
+                            continue
+
+                        # remove from collection
+                        target_collection.objects.unlink(obj)
 
-                        return {'FINISHED'}
+                if errors:
+                    send_report("Error removing 1 or more objects from this collection.\nObjects would be left without a collection")
 
-                # remove from collection
-                bpy.ops.collection.objects_remove(collection=collection.name)
 
         else:
-            # move object to collection
-            bpy.ops.object.move_to_collection(collection_index=self.collection_index)
+            # 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.name != target_collection.name:
+                        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
+        cm = bpy.context.scene.collection_manager
+        cm.update_header.clear()
+        new_update_header = cm.update_header.add()
+        new_update_header.name = "updated"
 
         return {'FINISHED'}
 
diff --git a/object_collection_manager/qcd_init.py b/object_collection_manager/qcd_init.py
index 87cbcd161f6bc7761a1cf2284f99df8da02ca4f7..0bef6e4461bc191229205251736be04eb43270d6 100644
--- a/object_collection_manager/qcd_init.py
+++ b/object_collection_manager/qcd_init.py
@@ -20,20 +20,6 @@ qcd_classes = (
     qcd_operators.RenumerateQCDSlots,
     )
 
-@persistent
-def depsgraph_update_post_handler(dummy):
-    if qcd_operators.move_triggered:
-        qcd_operators.move_triggered = False
-        return
-
-    qcd_operators.move_selection.clear()
-    qcd_operators.move_active = None
-
-@persistent
-def undo_redo_post_handler(dummy):
-    qcd_operators.move_selection.clear()
-    qcd_operators.move_active = None
-
 @persistent
 def save_internal_data(dummy):
     cm = bpy.context.scene.collection_manager
@@ -66,9 +52,6 @@ def register_qcd():
     kmi = km.keymap_items.new('view3d.qcd_move_widget', 'V', 'PRESS')
     addon_qcd_keymaps.append((km, kmi))
 
-    bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_post_handler)
-    bpy.app.handlers.undo_post.append(undo_redo_post_handler)
-    bpy.app.handlers.redo_post.append(undo_redo_post_handler)
     bpy.app.handlers.save_pre.append(save_internal_data)
     bpy.app.handlers.load_post.append(load_internal_data)
 
@@ -122,9 +105,6 @@ def unregister_qcd():
     for cls in qcd_classes:
         bpy.utils.unregister_class(cls)
 
-    bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_post_handler)
-    bpy.app.handlers.undo_post.remove(undo_redo_post_handler)
-    bpy.app.handlers.redo_post.remove(undo_redo_post_handler)
     bpy.app.handlers.save_pre.remove(save_internal_data)
     bpy.app.handlers.load_post.remove(load_internal_data)
 
diff --git a/object_collection_manager/qcd_operators.py b/object_collection_manager/qcd_operators.py
index 98d3b4556fd631c046e5ac6af3b3c64ef4d390b4..a7a6079e3af77cf237328e0f6827d0d445cdda98 100644
--- a/object_collection_manager/qcd_operators.py
+++ b/object_collection_manager/qcd_operators.py
@@ -30,39 +30,19 @@ from bpy.props import (
     IntProperty
 )
 
+from . import internals
+
 from .internals import (
     layer_collections,
     qcd_slots,
     update_property_group,
     get_modifiers,
+    get_move_selection,
+    get_move_active
 )
 
 from .operators import rto_history
 
-move_triggered = False
-move_selection = []
-move_active = None
-
-def get_move_selection():
-    global move_selection
-
-    if not move_selection:
-        move_selection = [obj.name for obj in bpy.context.selected_objects]
-
-    return [bpy.data.objects[name] for name in move_selection]
-
-def get_move_active():
-    global move_active
-    global move_selection
-
-    if not move_active:
-        move_active = getattr(bpy.context.view_layer.objects.active, "name", None)
-
-    if move_active not in [obj.name for obj in get_move_selection()]:
-        move_active = None
-
-    return bpy.data.objects[move_active] if move_active else None
-
 
 class MoveToQCDSlot(Operator):
     '''Move object(s) to QCD slot'''
@@ -76,11 +56,11 @@ class MoveToQCDSlot(Operator):
     def execute(self, context):
         global qcd_slots
         global layer_collections
-        global move_triggered
 
         selected_objects = get_move_selection()
         active_object = get_move_active()
-        move_triggered = True
+        internals.move_triggered = True
+
         qcd_laycol = None
         slot_name = qcd_slots.get_name(self.slot)
 
diff --git a/object_collection_manager/ui.py b/object_collection_manager/ui.py
index a90a619f1cb41a1de1ea9adb4ffb8bb122afe0bd..2209f18f215b7f62e5e0c3d4433883466aa6489a 100644
--- a/object_collection_manager/ui.py
+++ b/object_collection_manager/ui.py
@@ -36,6 +36,8 @@ from .internals import (
     update_collection_tree,
     update_property_group,
     generate_state,
+    get_move_selection,
+    get_move_active
 )
 
 from .operators import (
@@ -46,8 +48,6 @@ from .operators import (
     phantom_history,
     )
 
-from . import qcd_operators
-
 
 preview_collections = {}
 last_icon_theme_text = None
@@ -353,6 +353,8 @@ class CM_UL_items(UIList):
         view_layer = context.view_layer
         laycol = layer_collections[item.name]
         collection = laycol["ptr"].collection
+        selected_objects = get_move_selection()
+        active_object = get_move_active()
 
         split = layout.split(factor=0.96)
         row = split.row(align=True)
@@ -409,9 +411,13 @@ class CM_UL_items(UIList):
 
         icon = 'MESH_CUBE'
 
-        if len(context.selected_objects) > 0 and context.active_object:
-            if context.active_object.name in collection.objects:
+        if selected_objects:
+            if active_object and active_object.name in collection.objects:
                 icon = 'SNAP_VOLUME'
+
+            elif not set(selected_objects).isdisjoint(collection.objects):
+                icon = 'STICKY_UVS_LOC'
+
         else:
             row_setcol.enabled = False
 
@@ -588,8 +594,8 @@ def view3d_header_qcd_slots(self, context):
         if qcd_slot_name:
             qcd_laycol = layer_collections[qcd_slot_name]["ptr"]
             collection_objects = qcd_laycol.collection.objects
-            selected_objects = qcd_operators.get_move_selection()
-            active_object = qcd_operators.get_move_active()
+            selected_objects = get_move_selection()
+            active_object = get_move_active()
 
             icon_value = 0