OutlinerTweaks.py (Source)

#-------------------------------------------------------------------------------
#                      Outliner Tweaks - Addon for Blender
#
# Auxiliary Groups
# - #DEFAULT_GROUP - to collect ungrouped objects
# - #NEW_GROUP
#
# Commands:
# - Update Default Group (U)
# - Add to New Group (N)
# - Remove from Groups (R)
# - Expand/Collapse One Level (E)
# - Isolate Selection (Q)
#
#-------------------------------------------------------------------------------
# Version: 0.1
# Revised: 2.1.2018
# Author: Miki (meshlogic)
#-------------------------------------------------------------------------------
bl_info = {
    "name": "Outliner Tweaks",
    "author": "Miki (meshlogic)",
    "category": "3D View",
    "description": "Make organizing objects and groups in Outliner easier.",
    "location": "Outliner > Header",
    "version": (0, 1),
    "blender": (2, 79, 0)
}

import bpy
from bpy.props import *
from bpy.types import Menu, Operator, Panel, UIList


#--- Set default groups names
DEFAULT_GROUP = "#DEFAULT_GROUP"
NEW_GROUP = "#NEW_GROUP"

#--- Print debug msg into console
DEBUG = False
def dprint(*msg):
    if DEBUG: print(*msg)
    

#-------------------------------------------------------------------------------
# MISC FUNC
#-------------------------------------------------------------------------------
def activate_all_layers():
    for i in range(20):
        bpy.context.scene.layers[i] = True
    return

def cache_layers():
    layer_cache = [False]*20
    for i in range(20):
        layer_cache[i] = bpy.context.scene.layers[i]   
    return layer_cache

def restore_layers(layer_cache):
    for (i, state) in enumerate(layer_cache):
        bpy.context.scene.layers[i] = state
    return


#-------------------------------------------------------------------------------
# IsolateSelection_OT
#-------------------------------------------------------------------------------
class IsolateSelection_OT(Operator):
    """Isolate the selected objects in the 3D viewport
    - 1. Enable visibility and render for the selected objects
    - 2. Restrict visibility and render for all other objects
    - 3. Make visible only the layers of the selected objects"""
    
    bl_idname = "outliner.isolate_selection"
    bl_label = "Isolate Selection"

    #--- Available only in object mode
    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'

    def execute(self, context):
        dprint("\n*** outliner.isolate_selection ***")

        #--- All layers must be active (you cannot get selected_objects on inactive layers)
        activate_all_layers()
        layer_mask = [False]*20
        
        #--- Loop all objects
        for obj in bpy.data.objects:
            
            # 1. Make selected objects visible
            if obj in bpy.context.selected_objects:
                
                # Add object's layer to the mask
                layer_mask = [a or b for a,b in zip(obj.layers, layer_mask)]
            
                obj.hide = False
                obj.hide_render = False
                
            # 2. Hide all other objects
            else:
                obj.hide = True
                obj.hide_render = True
        
        #--- 3. Make visible only the layers of the selected objects
        restore_layers(layer_mask)
        
        return{'FINISHED'}


#-------------------------------------------------------------------------------
# ExpandCollapse_OT
#-------------------------------------------------------------------------------
class ExpandCollapse_OT(Operator):
    """Toggle Expand/Collapse one level for all items"""

    bl_idname = "outliner.expand_collapse_one_level"
    bl_label = "Expand/Collapse One Level"
    
    toggle_expand = BoolProperty(default = False)
    
    #--- Available only in object mode
    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'

    def execute(self, context):
              
        # Collapse all items
        for i in range(5):
            bpy.ops.outliner.show_one_level(open=False)
        
        # Toggle expand
        if not self.toggle_expand:
            bpy.ops.outliner.show_one_level(open=True)
        
        self.toggle_expand = not self.toggle_expand 
        
        return{'FINISHED'}
        
        
#-------------------------------------------------------------------------------
# UpdateDefaultGroup_OT
#-------------------------------------------------------------------------------
class UpdateDefaultGroup_OT(Operator):
    """Update Default Group
    - 1. Group all unsorted objects (add them to the DEFAULT_GROUP)
    - 2. Clean the DEFAULT_GROUP from the objects that already belong to other groups"""

    bl_idname = "outliner.update_default_group"
    bl_label = "Update Default Group"

    #--- Available only in object mode
    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'

    def execute(self, context):
        dprint("\n*** outliner.update_default_group ***")
        
        #--- Create DEFAULT_GROUP if it doesn't exist yet
        if DEFAULT_GROUP not in bpy.data.groups:
            bpy.ops.group.create(name = DEFAULT_GROUP)
        
        #--- Loop all objects
        for obj in bpy.data.objects:
            
            # Get names of all groups the obj belongs to
            obj_groups = [g.name for g in obj.users_group]
            
            # 1. Add ungrouped object to DEFAULT_GROUP
            if len(obj_groups) == 0:
                bpy.data.groups.get(DEFAULT_GROUP).objects.link(obj)
                dprint(" ", obj.name, "added to", DEFAULT_GROUP)
            
            # 2. Remove grouped obj from DEFAULT_GROUP
            elif len(obj.users_group) > 1 and DEFAULT_GROUP in obj_groups:
                bpy.data.groups.get(DEFAULT_GROUP).objects.unlink(obj)
                dprint(" ", obj.name, "removed from", DEFAULT_GROUP)
        
        return{'FINISHED'}
        

#-------------------------------------------------------------------------------
# AddToNewGroup_OT
#-------------------------------------------------------------------------------
class AddToNew_OT(Operator):
    """Add to New Group
    - 1. Add selected objects to the NEW_GROUP
    - 2. Remove them from all other groups"""

    bl_idname = "outliner.add_to_new_group"
    bl_label = "Add to New Group"
    
    #--- Available only in object mode
    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'
    
    def execute(self, context):
        dprint("\n*** outliner.add_to_new_group ***")
        
        #--- All layers must be active (you cannot get selected_objects on inactive layers)
        layer_cache = cache_layers()
        activate_all_layers()
        
        #--- Create NEW_GROUP, if it doesn't exist yet
        if NEW_GROUP not in bpy.data.groups:
            bpy.ops.group.create(name = NEW_GROUP)
        
        #--- Loop the selected objects
        for obj in bpy.context.selected_objects:
            
            # 2. Remove from all groups
            for group in obj.users_group:
                group.objects.unlink(obj)
                dprint(" ", obj.name, "removed from", group.name)
            
            # 1. Add obj to the NEW_GROUP
            bpy.data.groups.get(NEW_GROUP).objects.link(obj)
            dprint(" ", obj.name, "added to", NEW_GROUP)
            
        #--- Restore layers visibility
        restore_layers(layer_cache) 
    
        return{'FINISHED'}
   
   
#-------------------------------------------------------------------------------
# RemoveFromGroups_OT
#-------------------------------------------------------------------------------
class RemoveFromGroups_OT(Operator):
    """Remove from Groups
    - 1. Remove selected objects from all groups
    - 2. Add them to the DEFAULT_GROUP"""

    bl_idname = "outliner.remove_from_groups"
    bl_label = "Remove from Groups"
    
    #--- Available only in object mode
    @classmethod
    def poll(cls, context):
        return context.mode == 'OBJECT'
    
    def execute(self, context):
        dprint("\n*** outliner.remove_from_groups ***") 
        
        #--- All layers must be active (you cannot get selected_objects on inactive layers)
        layer_cache = cache_layers()
        activate_all_layers()

        #--- Loop the selected objects
        for obj in bpy.context.selected_objects:
            
            # 1. Remove obj from all groups
            for group in obj.users_group:
                group.objects.unlink(obj)
                dprint(" ", obj.name, "removed from", group.name)
                
            # 2. Add obj to DEFAULT_GROUP
            bpy.data.groups.get(DEFAULT_GROUP).objects.link(obj)
            dprint(" ", obj.name, "added to", DEFAULT_GROUP)

        #--- Restore layers visibility
        restore_layers(layer_cache)
    
        return{'FINISHED'} 
    

#-------------------------------------------------------------------------------
# MENU ITEMS
#-------------------------------------------------------------------------------
def draw_menu(self, context):
    layout = self.layout
    layout.menu("OutlinerTweaksMenu", icon='COLLAPSEMENU', text="")
    
class OutlinerTweaksMenu(Menu):
    bl_idname = "OutlinerTweaksMenu"
    bl_label = ""
    
    def draw(self, context):
        layout = self.layout
        layout.operator("outliner.isolate_selection", icon='RESTRICT_VIEW_OFF')
        layout.operator("outliner.expand_collapse_one_level", icon='OOPS')
                
        layout.separator()
        layout.operator("outliner.remove_from_groups", icon='X')
        layout.operator("outliner.add_to_new_group", icon='GROUP')
        layout.operator("outliner.update_default_group", icon='FILE_REFRESH')


#-------------------------------------------------------------------------------
# REGISTER/UNREGISTER KEYMAPS
#-------------------------------------------------------------------------------
addon_keymaps = []

def register_keymaps():
    wm = bpy.context.window_manager
    
    km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
    kmi = km.keymap_items.new("outliner.isolate_selection", "Q", "PRESS", shift=False)
    addon_keymaps.append((km, kmi))
    
    km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
    kmi = km.keymap_items.new("outliner.expand_collapse_one_level", "E", "PRESS", shift=False)
    addon_keymaps.append((km, kmi))
    
    km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
    kmi = km.keymap_items.new("outliner.update_default_group", "U", "PRESS", shift=False)
    addon_keymaps.append((km, kmi))
    
    km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
    kmi = km.keymap_items.new("outliner.add_to_new_group", "N", "PRESS", shift=False)
    addon_keymaps.append((km, kmi))
    
    km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
    kmi = km.keymap_items.new("outliner.remove_from_groups", "R", "PRESS", shift=False)
    addon_keymaps.append((km, kmi))
    
def unregister_keymaps():
    for km, kmi in addon_keymaps:
        km.keymap_items.remove(kmi) 


#-------------------------------------------------------------------------------
# REGISTER/UNREGISTER CLASSES
#-------------------------------------------------------------------------------
def register():
    bpy.utils.register_module(__name__)
    bpy.types.OUTLINER_HT_header.prepend(draw_menu)
    register_keymaps()

def unregister():
    unregister_keymaps()
    bpy.types.OUTLINER_HT_header.remove(draw_menu)
    bpy.utils.unregister_module(__name__)
 
if __name__ == "__main__":
    register()