439 lines
16 KiB
Python
439 lines
16 KiB
Python
# 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",
|
|
"version": (0, 9, 2),
|
|
"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",
|
|
"category": "Object",
|
|
}
|
|
|
|
import bpy
|
|
import logging
|
|
import os
|
|
|
|
from bpy.app.handlers import persistent
|
|
|
|
logger = logging.getLogger('object_edit_linked')
|
|
|
|
settings = {
|
|
"original_file": "",
|
|
"linked_file": "",
|
|
"linked_objects": [],
|
|
"linked_nodes": []
|
|
}
|
|
|
|
|
|
@persistent
|
|
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_category = "Item"
|
|
bl_context = 'objectmode'
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
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 = self.layout
|
|
layout.use_property_split = False
|
|
layout.use_property_decorate = False
|
|
icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")
|
|
|
|
target = None
|
|
|
|
target = context.active_object.instance_collection
|
|
|
|
if settings["original_file"] == "" and (
|
|
(target 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)):
|
|
|
|
if (target is not None):
|
|
props = layout.operator("object.edit_linked", icon="LINK_BLEND",
|
|
text="Edit Library: %s" % target.name)
|
|
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)
|
|
|
|
if (target is not None):
|
|
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"] != "":
|
|
|
|
if scene.use_instance:
|
|
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,
|
|
icon=icon)
|
|
|
|
|
|
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,
|
|
NODE_OT_EditLinked,
|
|
WM_OT_ReturnToOriginal,
|
|
VIEW3D_PT_PanelLinkedEdit,
|
|
NODE_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()
|