1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Edit Linked Library",
5 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
8 "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
9 "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
10 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
18 from bpy
.app
.handlers
import persistent
20 logger
= logging
.getLogger('object_edit_linked')
31 def linked_file_check(context
: bpy
.context
):
32 if settings
["linked_file"] != "":
33 if os
.path
.samefile(settings
["linked_file"], bpy
.data
.filepath
):
34 logger
.info("Editing a linked library.")
35 bpy
.ops
.object.select_all(action
='DESELECT')
36 for ob_name
in settings
["linked_objects"]:
37 bpy
.data
.objects
[ob_name
].select_set(True) # XXX Assumes selected object is in the active scene
38 if len(settings
["linked_objects"]) == 1:
39 context
.view_layer
.objects
.active
= bpy
.data
.objects
[settings
["linked_objects"][0]]
41 # For some reason, the linked editing session ended
42 # (failed to find a file or opened a different file
43 # before returning to the originating .blend)
44 settings
["original_file"] = ""
45 settings
["linked_file"] = ""
48 class OBJECT_OT_EditLinked(bpy
.types
.Operator
):
49 """Edit Linked Library"""
50 bl_idname
= "object.edit_linked"
51 bl_label
= "Edit Linked Library"
53 use_autosave
: bpy
.props
.BoolProperty(
55 description
="Save the current file before opening the linked library",
57 use_instance
: bpy
.props
.BoolProperty(
58 name
="New Blender Instance",
59 description
="Open in a new Blender instance",
63 def poll(cls
, context
: bpy
.context
):
64 return settings
["original_file"] == "" and context
.active_object
is not None and (
65 (context
.active_object
.instance_collection
and
66 context
.active_object
.instance_collection
.library
is not None) or
67 context
.active_object
.library
is not None or
68 (context
.active_object
.override_library
and
69 context
.active_object
.override_library
.reference
.library
is not None))
71 def execute(self
, context
: bpy
.context
):
72 target
= context
.active_object
74 if target
.instance_collection
and target
.instance_collection
.library
:
75 targetpath
= target
.instance_collection
.library
.filepath
76 settings
["linked_objects"].extend({ob
.name
for ob
in target
.instance_collection
.objects
})
78 targetpath
= target
.library
.filepath
79 settings
["linked_objects"].append(target
.name
)
80 elif target
.override_library
:
81 target
= target
.override_library
.reference
82 targetpath
= target
.library
.filepath
83 settings
["linked_objects"].append(target
.name
)
86 logger
.debug(target
.name
+ " is linked to " + targetpath
)
89 if not bpy
.data
.filepath
:
90 # File is not saved on disk, better to abort!
91 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
93 bpy
.ops
.wm
.save_mainfile()
95 settings
["original_file"] = bpy
.data
.filepath
96 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
97 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
102 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
104 logger
.error("Error on the new Blender instance")
106 logger
.error(traceback
.print_exc())
108 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
110 logger
.info("Opened linked file!")
112 self
.report({'WARNING'}, target
.name
+ " is not linked")
113 logger
.warning(target
.name
+ " is not linked")
118 class NODE_OT_EditLinked(bpy
.types
.Operator
):
119 """Edit Linked Library"""
120 bl_idname
= "node.edit_linked"
121 bl_label
= "Edit Linked Library"
123 use_autosave
: bpy
.props
.BoolProperty(
125 description
="Save the current file before opening the linked library",
127 use_instance
: bpy
.props
.BoolProperty(
128 name
="New Blender Instance",
129 description
="Open in a new Blender instance",
133 def poll(cls
, context
: bpy
.context
):
134 return settings
["original_file"] == "" and context
.active_node
is not None and (
135 (context
.active_node
.type == 'GROUP' and
136 hasattr(context
.active_node
.node_tree
, "library") and
137 context
.active_node
.node_tree
.library
is not None) or
138 (hasattr(context
.active_node
, "monad") and
139 context
.active_node
.monad
.library
is not None))
141 def execute(self
, context
: bpy
.context
):
142 target
= context
.active_node
143 if (target
.type == "GROUP"):
144 target
= target
.node_tree
146 target
= target
.monad
148 targetpath
= target
.library
.filepath
149 settings
["linked_nodes"].append(target
.name
)
152 logger
.debug(target
.name
+ " is linked to " + targetpath
)
154 if self
.use_autosave
:
155 if not bpy
.data
.filepath
:
156 # File is not saved on disk, better to abort!
157 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
159 bpy
.ops
.wm
.save_mainfile()
161 settings
["original_file"] = bpy
.data
.filepath
162 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
163 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
165 if self
.use_instance
:
168 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
170 logger
.error("Error on the new Blender instance")
172 logger
.error(traceback
.print_exc())
174 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
176 logger
.info("Opened linked file!")
178 self
.report({'WARNING'}, target
.name
+ " is not linked")
179 logger
.warning(target
.name
+ " is not linked")
184 class WM_OT_ReturnToOriginal(bpy
.types
.Operator
):
185 """Load the original file"""
186 bl_idname
= "wm.return_to_original"
187 bl_label
= "Return to Original File"
189 use_autosave
: bpy
.props
.BoolProperty(
191 description
="Save the current file before opening original file",
195 def poll(cls
, context
: bpy
.context
):
196 return (settings
["original_file"] != "")
198 def execute(self
, context
: bpy
.context
):
199 if self
.use_autosave
:
200 bpy
.ops
.wm
.save_mainfile()
202 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["original_file"])
204 settings
["original_file"] = ""
205 settings
["linked_objects"] = []
206 logger
.info("Back to the original!")
210 class VIEW3D_PT_PanelLinkedEdit(bpy
.types
.Panel
):
211 bl_label
= "Edit Linked Library"
212 bl_space_type
= "VIEW_3D"
213 bl_region_type
= 'UI'
215 bl_context
= 'objectmode'
216 bl_options
= {'DEFAULT_CLOSED'}
219 def poll(cls
, context
: bpy
.context
):
220 return (context
.active_object
is not None) or (settings
["original_file"] != "")
222 def draw_common(self
, scene
, layout
, props
):
223 if props
is not None:
224 props
.use_autosave
= scene
.use_autosave
225 props
.use_instance
= scene
.use_instance
227 layout
.prop(scene
, "use_autosave")
228 layout
.prop(scene
, "use_instance")
230 def draw(self
, context
: bpy
.context
):
231 scene
= context
.scene
233 layout
.use_property_split
= False
234 layout
.use_property_decorate
= False
235 icon
= "OUTLINER_DATA_" + context
.active_object
.type.replace("LIGHT_PROBE", "LIGHTPROBE")
239 target
= context
.active_object
.instance_collection
241 if settings
["original_file"] == "" and (
243 target
.library
is not None) or
244 context
.active_object
.library
is not None or
245 (context
.active_object
.override_library
is not None and
246 context
.active_object
.override_library
.reference
is not None)):
248 if (target
is not None):
249 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
250 text
="Edit Library: %s" % target
.name
)
251 elif (context
.active_object
.library
):
252 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
253 text
="Edit Library: %s" % context
.active_object
.name
)
255 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
256 text
="Edit Override Library: %s" % context
.active_object
.override_library
.reference
.name
)
258 self
.draw_common(scene
, layout
, props
)
260 if (target
is not None):
261 layout
.label(text
="Path: %s" %
262 target
.library
.filepath
)
263 elif (context
.active_object
.library
):
264 layout
.label(text
="Path: %s" %
265 context
.active_object
.library
.filepath
)
267 layout
.label(text
="Path: %s" %
268 context
.active_object
.override_library
.reference
.library
.filepath
)
270 elif settings
["original_file"] != "":
272 if scene
.use_instance
:
273 layout
.operator("wm.return_to_original",
274 text
="Reload Current File",
275 icon
="FILE_REFRESH").use_autosave
= False
279 # XXX - This is for nested linked assets... but it only works
280 # when launching a new Blender instance. Nested links don't
281 # currently work when using a single instance of Blender.
282 if context
.active_object
.instance_collection
is not None:
283 props
= layout
.operator("object.edit_linked",
284 text
="Edit Library: %s" % context
.active_object
.instance_collection
.name
,
289 self
.draw_common(scene
, layout
, props
)
291 if context
.active_object
.instance_collection
is not None:
292 layout
.label(text
="Path: %s" %
293 context
.active_object
.instance_collection
.library
.filepath
)
296 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
297 props
.use_autosave
= scene
.use_autosave
299 layout
.prop(scene
, "use_autosave")
302 layout
.label(text
="%s is not linked" % context
.active_object
.name
,
306 class NODE_PT_PanelLinkedEdit(bpy
.types
.Panel
):
307 bl_label
= "Edit Linked Library"
308 bl_space_type
= 'NODE_EDITOR'
309 bl_region_type
= 'UI'
310 if bpy
.app
.version
>= (2, 93, 0):
314 bl_options
= {'DEFAULT_CLOSED'}
317 def poll(cls
, context
):
318 return context
.active_node
is not None
320 def draw_common(self
, scene
, layout
, props
):
321 if props
is not None:
322 props
.use_autosave
= scene
.use_autosave
323 props
.use_instance
= scene
.use_instance
325 layout
.prop(scene
, "use_autosave")
326 layout
.prop(scene
, "use_instance")
328 def draw(self
, context
):
329 scene
= context
.scene
331 layout
.use_property_split
= False
332 layout
.use_property_decorate
= False
335 target
= context
.active_node
337 if settings
["original_file"] == "" and (
338 (target
.type == 'GROUP' and hasattr(target
.node_tree
, "library") and
339 target
.node_tree
.library
is not None) or
340 (hasattr(target
, "monad") and target
.monad
.library
is not None)):
342 if (target
.type == "GROUP"):
343 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
344 text
="Edit Library: %s" % target
.name
)
346 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
347 text
="Edit Library: %s" % target
.monad
.name
)
349 self
.draw_common(scene
, layout
, props
)
351 if (target
.type == "GROUP"):
352 layout
.label(text
="Path: %s" % target
.node_tree
.library
.filepath
)
354 layout
.label(text
="Path: %s" % target
.monad
.library
.filepath
)
356 elif settings
["original_file"] != "":
358 if scene
.use_instance
:
359 layout
.operator("wm.return_to_original",
360 text
="Reload Current File",
361 icon
="FILE_REFRESH").use_autosave
= False
367 self
.draw_common(scene
, layout
, props
)
369 #layout.label(text="Path: %s" %
370 # context.active_object.instance_collection.library.filepath)
373 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
374 props
.use_autosave
= scene
.use_autosave
376 layout
.prop(scene
, "use_autosave")
379 layout
.label(text
="%s is not linked" % target
.name
, icon
=icon
)
382 class TOPBAR_MT_edit_linked_submenu(bpy
.types
.Menu
):
383 bl_label
= 'Edit Linked Library'
385 def draw(self
, context
):
386 self
.layout
.separator()
387 self
.layout
.operator(OBJECT_OT_EditLinked
.bl_idname
)
388 self
.layout
.operator(WM_OT_ReturnToOriginal
.bl_idname
)
393 OBJECT_OT_EditLinked
,
395 WM_OT_ReturnToOriginal
,
396 VIEW3D_PT_PanelLinkedEdit
,
397 NODE_PT_PanelLinkedEdit
,
398 TOPBAR_MT_edit_linked_submenu
403 bpy
.app
.handlers
.load_post
.append(linked_file_check
)
406 bpy
.utils
.register_class(c
)
408 bpy
.types
.Scene
.use_autosave
= bpy
.props
.BoolProperty(
410 description
="Save the current file before opening a linked file",
413 bpy
.types
.Scene
.use_instance
= bpy
.props
.BoolProperty(
414 name
="New Blender Instance",
415 description
="Open in a new Blender instance",
418 # add the function to the file menu
419 bpy
.types
.TOPBAR_MT_file_external_data
.append(TOPBAR_MT_edit_linked_submenu
.draw
)
426 bpy
.app
.handlers
.load_post
.remove(linked_file_check
)
427 bpy
.types
.TOPBAR_MT_file_external_data
.remove(TOPBAR_MT_edit_linked_submenu
)
429 del bpy
.types
.Scene
.use_autosave
430 del bpy
.types
.Scene
.use_instance
433 for c
in reversed(classes
):
434 bpy
.utils
.unregister_class(c
)
437 if __name__
== "__main__":