Skip to content
Snippets Groups Projects
object_edit_linked.py 16 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    bl_info = {
        "name": "Edit Linked Library",
    
        "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
    
        "blender": (2, 80, 0),
    
        "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
        "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
    
        "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
    
    
    import bpy
    
    from bpy.app.handlers import persistent
    
    logger = logging.getLogger('object_edit_linked')
    
    
    settings = {
        "original_file": "",
        "linked_file": "",
        "linked_objects": [],
    
    def linked_file_check(context: bpy.context):
    
        if settings["linked_file"] != "":
            if os.path.samefile(settings["linked_file"], bpy.data.filepath):
    
                logger.info("Editing a linked library.")
    
                bpy.ops.object.select_all(action='DESELECT')
                for ob_name in settings["linked_objects"]:
    
                    bpy.data.objects[ob_name].select_set(True) # XXX Assumes selected object is in the active scene
    
                if len(settings["linked_objects"]) == 1:
    
                    context.view_layer.objects.active = bpy.data.objects[settings["linked_objects"][0]]
    
            else:
                # For some reason, the linked editing session ended
                # (failed to find a file or opened a different file
                # before returning to the originating .blend)
                settings["original_file"] = ""
                settings["linked_file"] = ""
    
    
    
    class OBJECT_OT_EditLinked(bpy.types.Operator):
    
        """Edit Linked Library"""
        bl_idname = "object.edit_linked"
        bl_label = "Edit Linked Library"
    
    
        use_autosave: bpy.props.BoolProperty(
    
                name="Autosave",
                description="Save the current file before opening the linked library",
                default=True)
    
        use_instance: bpy.props.BoolProperty(
    
                name="New Blender Instance",
                description="Open in a new Blender instance",
                default=False)
    
        @classmethod
    
        def poll(cls, context: bpy.context):
    
            return settings["original_file"] == "" and context.active_object is not None and (
    
                    (context.active_object.instance_collection and
    
                    context.active_object.instance_collection.library is not None) or
    
                    context.active_object.library is not None or
                    (context.active_object.override_library and
                    context.active_object.override_library.reference.library is not None))
    
        def execute(self, context: bpy.context):
    
            target = context.active_object
    
    
            if target.instance_collection and target.instance_collection.library:
                targetpath = target.instance_collection.library.filepath
                settings["linked_objects"].extend({ob.name for ob in target.instance_collection.objects})
    
            elif target.library:
                targetpath = target.library.filepath
                settings["linked_objects"].append(target.name)
    
            elif target.override_library:
                target = target.override_library.reference
                targetpath = target.library.filepath
                settings["linked_objects"].append(target.name)
    
    
            if targetpath:
    
                logger.debug(target.name + " is linked to " + targetpath)
    
    
                if self.use_autosave:
    
                    if not bpy.data.filepath:
                        # File is not saved on disk, better to abort!
                        self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
                        return {'CANCELLED'}
    
                    bpy.ops.wm.save_mainfile()
    
                settings["original_file"] = bpy.data.filepath
    
                # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
                settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))
    
    
                if self.use_instance:
    
                    import subprocess
    
                    try:
                        subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
                    except:
    
                        logger.error("Error on the new Blender instance")
    
                        import traceback
    
                        logger.error(traceback.print_exc())
    
                else:
                    bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
    
    
                logger.info("Opened linked file!")
    
            else:
                self.report({'WARNING'}, target.name + " is not linked")
    
                logger.warning(target.name + " is not linked")
    
    
            return {'FINISHED'}
    
    
    
    class NODE_OT_EditLinked(bpy.types.Operator):
        """Edit Linked Library"""
        bl_idname = "node.edit_linked"
        bl_label = "Edit Linked Library"
    
        use_autosave: bpy.props.BoolProperty(
                name="Autosave",
                description="Save the current file before opening the linked library",
                default=True)
        use_instance: bpy.props.BoolProperty(
                name="New Blender Instance",
                description="Open in a new Blender instance",
                default=False)
    
        @classmethod
        def poll(cls, context: bpy.context):
            return settings["original_file"] == "" and context.active_node is not None and (
    
                    (context.active_node.type == 'GROUP' and
    
                    hasattr(context.active_node.node_tree, "library") and
    
                    context.active_node.node_tree.library is not None) or
                    (hasattr(context.active_node, "monad") and
                    context.active_node.monad.library is not None))
    
    
        def execute(self, context: bpy.context):
    
            target = context.active_node
            if (target.type == "GROUP"):
                target = target.node_tree
            else:
                target = target.monad
    
    
            targetpath = target.library.filepath
            settings["linked_nodes"].append(target.name)
    
            if targetpath:
                logger.debug(target.name + " is linked to " + targetpath)
    
                if self.use_autosave:
                    if not bpy.data.filepath:
                        # File is not saved on disk, better to abort!
                        self.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
                        return {'CANCELLED'}
                    bpy.ops.wm.save_mainfile()
    
                settings["original_file"] = bpy.data.filepath
    
                # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
                settings["linked_file"] = os.path.abspath(bpy.path.abspath(targetpath))
    
    
                if self.use_instance:
                    import subprocess
                    try:
                        subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
                    except:
                        logger.error("Error on the new Blender instance")
                        import traceback
                        logger.error(traceback.print_exc())
                else:
                    bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
    
                logger.info("Opened linked file!")
            else:
                self.report({'WARNING'}, target.name + " is not linked")
                logger.warning(target.name + " is not linked")
    
            return {'FINISHED'}
    
    
    
    class WM_OT_ReturnToOriginal(bpy.types.Operator):
    
        """Load the original file"""
        bl_idname = "wm.return_to_original"
        bl_label = "Return to Original File"
    
    
        use_autosave: bpy.props.BoolProperty(
    
                name="Autosave",
                description="Save the current file before opening original file",
                default=True)
    
        @classmethod
    
        def poll(cls, context: bpy.context):
    
            return (settings["original_file"] != "")
    
    
        def execute(self, context: bpy.context):
    
            if self.use_autosave:
                bpy.ops.wm.save_mainfile()
    
            bpy.ops.wm.open_mainfile(filepath=settings["original_file"])
    
            settings["original_file"] = ""
            settings["linked_objects"] = []
    
            logger.info("Back to the original!")
    
            return {'FINISHED'}
    
    
    
    class VIEW3D_PT_PanelLinkedEdit(bpy.types.Panel):
    
        bl_label = "Edit Linked Library"
        bl_space_type = "VIEW_3D"
    
        bl_region_type = 'UI'
    
        bl_context = 'objectmode'
    
        bl_options = {'DEFAULT_CLOSED'}
    
        def poll(cls, context: bpy.context):
    
            return (context.active_object is not None) or (settings["original_file"] != "")
    
    
        def draw_common(self, scene, layout, props):
    
            if props is not None:
                props.use_autosave = scene.use_autosave
                props.use_instance = scene.use_instance
    
                layout.prop(scene, "use_autosave")
                layout.prop(scene, "use_instance")
    
    
        def draw(self, context: bpy.context):
    
            scene = context.scene
    
            layout.use_property_split = False
    
            layout.use_property_decorate = False
    
            icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")
    
            target = context.active_object.instance_collection
    
            if settings["original_file"] == "" and (
    
                    target.library is not None) or
    
                    context.active_object.library is not None or
                    (context.active_object.override_library is not None and
                    context.active_object.override_library.reference is not None)):
    
                    props = layout.operator("object.edit_linked", icon="LINK_BLEND",
    
                elif (context.active_object.library):
    
                    props = layout.operator("object.edit_linked", icon="LINK_BLEND",
                                            text="Edit Library: %s" % context.active_object.name)
    
                else:
                    props = layout.operator("object.edit_linked", icon="LINK_BLEND",
                                            text="Edit Override Library: %s" % context.active_object.override_library.reference.name)
    
                self.draw_common(scene, layout, props)
    
                    layout.label(text="Path: %s" %
    
                                target.library.filepath)
    
                elif (context.active_object.library):
    
                    layout.label(text="Path: %s" %
    
                                context.active_object.library.filepath)
    
                else:
                    layout.label(text="Path: %s" %
                                context.active_object.override_library.reference.library.filepath)
    
    
            elif settings["original_file"] != "":
    
    
                    layout.operator("wm.return_to_original",
                                    text="Reload Current File",
                                    icon="FILE_REFRESH").use_autosave = False
    
                    layout.separator()
    
    
                    # XXX - This is for nested linked assets... but it only works
                    # when launching a new Blender instance. Nested links don't
                    # currently work when using a single instance of Blender.
    
                    if context.active_object.instance_collection is not None:
                        props = layout.operator("object.edit_linked",
                                text="Edit Library: %s" % context.active_object.instance_collection.name,
                                icon="LINK_BLEND")
                    else:
                        props = None
    
    
                    self.draw_common(scene, layout, props)
    
                    if context.active_object.instance_collection is not None:
                        layout.label(text="Path: %s" %
    
                                context.active_object.instance_collection.library.filepath)
    
    
                else:
                    props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
                    props.use_autosave = scene.use_autosave
    
                    layout.prop(scene, "use_autosave")
    
            else:
                layout.label(text="%s is not linked" % context.active_object.name,
    
    class NODE_PT_PanelLinkedEdit(bpy.types.Panel):
        bl_label = "Edit Linked Library"
        bl_space_type = 'NODE_EDITOR'
        bl_region_type = 'UI'
    
        if bpy.app.version >= (2, 93, 0):
            bl_category = "Node"
        else:
            bl_category = "Item"
    
        bl_options = {'DEFAULT_CLOSED'}
    
        @classmethod
        def poll(cls, context):
            return context.active_node is not None
    
        def draw_common(self, scene, layout, props):
            if props is not None:
                props.use_autosave = scene.use_autosave
                props.use_instance = scene.use_instance
    
                layout.prop(scene, "use_autosave")
                layout.prop(scene, "use_instance")
    
        def draw(self, context):
            scene = context.scene
            layout = self.layout
            layout.use_property_split = False
            layout.use_property_decorate = False
            icon = 'NODETREE'
    
            target = context.active_node
    
            if settings["original_file"] == "" and (
    
                    (target.type == 'GROUP' and hasattr(target.node_tree, "library") and
                    target.node_tree.library is not None) or
                    (hasattr(target, "monad") and target.monad.library is not None)):
    
                if (target.type == "GROUP"):
                    props = layout.operator("node.edit_linked", icon="LINK_BLEND",
                                            text="Edit Library: %s" % target.name)
                else:
                    props = layout.operator("node.edit_linked", icon="LINK_BLEND",
                                            text="Edit Library: %s" % target.monad.name)
    
    
                self.draw_common(scene, layout, props)
    
    
                if (target.type == "GROUP"):
                    layout.label(text="Path: %s" % target.node_tree.library.filepath)
                else:
                    layout.label(text="Path: %s" % target.monad.library.filepath)
    
    
            elif settings["original_file"] != "":
    
                if scene.use_instance:
                    layout.operator("wm.return_to_original",
                                    text="Reload Current File",
                                    icon="FILE_REFRESH").use_autosave = False
    
                    layout.separator()
    
                    props = None
    
                    self.draw_common(scene, layout, props)
    
                    #layout.label(text="Path: %s" %
                    #            context.active_object.instance_collection.library.filepath)
    
                else:
                    props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
                    props.use_autosave = scene.use_autosave
    
                    layout.prop(scene, "use_autosave")
    
            else:
                layout.label(text="%s is not linked" % target.name, icon=icon)
    
    
    
    class TOPBAR_MT_edit_linked_submenu(bpy.types.Menu):
        bl_label = 'Edit Linked Library'
    
        def draw(self, context):
            self.layout.separator()
            self.layout.operator(OBJECT_OT_EditLinked.bl_idname)
            self.layout.operator(WM_OT_ReturnToOriginal.bl_idname)
    
    
    
    addon_keymaps = []
    
    classes = (
        OBJECT_OT_EditLinked,
    
        WM_OT_ReturnToOriginal,
        VIEW3D_PT_PanelLinkedEdit,
    
        TOPBAR_MT_edit_linked_submenu
        )
    
    
    
    def register():
    
        bpy.app.handlers.load_post.append(linked_file_check)
    
    
        for c in classes:
            bpy.utils.register_class(c)
    
    
        bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
                name="Autosave",
                description="Save the current file before opening a linked file",
                default=True)
    
        bpy.types.Scene.use_instance = bpy.props.BoolProperty(
                name="New Blender Instance",
                description="Open in a new Blender instance",
                default=False)
    
    
        # add the function to the file menu
    
        bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)
    
    
    
    def unregister():
    
        bpy.app.handlers.load_post.remove(linked_file_check)
        bpy.types.TOPBAR_MT_file_external_data.remove(TOPBAR_MT_edit_linked_submenu)
    
    
        del bpy.types.Scene.use_autosave
        del bpy.types.Scene.use_instance
    
    
    
        for c in reversed(classes):
            bpy.utils.unregister_class(c)
    
    
    
    if __name__ == "__main__":
        register()