1 # SPDX-FileCopyrightText: 2013-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Edit Linked Library",
7 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
10 "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
11 "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
20 from bpy
.app
.handlers
import persistent
22 logger
= logging
.getLogger('object_edit_linked')
33 def linked_file_check(context
: bpy
.context
):
34 if settings
["linked_file"] != "":
35 if os
.path
.samefile(settings
["linked_file"], bpy
.data
.filepath
):
36 logger
.info("Editing a linked library.")
37 bpy
.ops
.object.select_all(action
='DESELECT')
38 for ob_name
in settings
["linked_objects"]:
39 bpy
.data
.objects
[ob_name
].select_set(True) # XXX Assumes selected object is in the active scene
40 if len(settings
["linked_objects"]) == 1:
41 context
.view_layer
.objects
.active
= bpy
.data
.objects
[settings
["linked_objects"][0]]
43 # For some reason, the linked editing session ended
44 # (failed to find a file or opened a different file
45 # before returning to the originating .blend)
46 settings
["original_file"] = ""
47 settings
["linked_file"] = ""
50 class OBJECT_OT_EditLinked(bpy
.types
.Operator
):
51 """Edit Linked Library"""
52 bl_idname
= "object.edit_linked"
53 bl_label
= "Edit Linked Library"
55 use_autosave
: bpy
.props
.BoolProperty(
57 description
="Save the current file before opening the linked library",
59 use_instance
: bpy
.props
.BoolProperty(
60 name
="New Blender Instance",
61 description
="Open in a new Blender instance",
65 def poll(cls
, context
: bpy
.context
):
66 return settings
["original_file"] == "" and context
.active_object
is not None and (
67 (context
.active_object
.instance_collection
and
68 context
.active_object
.instance_collection
.library
is not None) or
69 context
.active_object
.library
is not None or
70 (context
.active_object
.override_library
and
71 context
.active_object
.override_library
.reference
.library
is not None))
73 def execute(self
, context
: bpy
.context
):
74 target
= context
.active_object
76 if target
.instance_collection
and target
.instance_collection
.library
:
77 targetpath
= target
.instance_collection
.library
.filepath
78 settings
["linked_objects"].extend({ob
.name
for ob
in target
.instance_collection
.objects
})
80 targetpath
= target
.library
.filepath
81 settings
["linked_objects"].append(target
.name
)
82 elif target
.override_library
:
83 target
= target
.override_library
.reference
84 targetpath
= target
.library
.filepath
85 settings
["linked_objects"].append(target
.name
)
88 logger
.debug(target
.name
+ " is linked to " + targetpath
)
91 if not bpy
.data
.filepath
:
92 # File is not saved on disk, better to abort!
93 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
95 bpy
.ops
.wm
.save_mainfile()
97 settings
["original_file"] = bpy
.data
.filepath
98 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
99 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
101 if self
.use_instance
:
104 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
106 logger
.error("Error on the new Blender instance")
108 logger
.error(traceback
.print_exc())
110 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
112 logger
.info("Opened linked file!")
114 self
.report({'WARNING'}, target
.name
+ " is not linked")
115 logger
.warning(target
.name
+ " is not linked")
120 class NODE_OT_EditLinked(bpy
.types
.Operator
):
121 """Edit Linked Library"""
122 bl_idname
= "node.edit_linked"
123 bl_label
= "Edit Linked Library"
125 use_autosave
: bpy
.props
.BoolProperty(
127 description
="Save the current file before opening the linked library",
129 use_instance
: bpy
.props
.BoolProperty(
130 name
="New Blender Instance",
131 description
="Open in a new Blender instance",
135 def poll(cls
, context
: bpy
.context
):
136 return settings
["original_file"] == "" and context
.active_node
is not None and (
137 (context
.active_node
.type == 'GROUP' and
138 hasattr(context
.active_node
.node_tree
, "library") and
139 context
.active_node
.node_tree
.library
is not None) or
140 (hasattr(context
.active_node
, "monad") and
141 context
.active_node
.monad
.library
is not None))
143 def execute(self
, context
: bpy
.context
):
144 target
= context
.active_node
145 if (target
.type == "GROUP"):
146 target
= target
.node_tree
148 target
= target
.monad
150 targetpath
= target
.library
.filepath
151 settings
["linked_nodes"].append(target
.name
)
154 logger
.debug(target
.name
+ " is linked to " + targetpath
)
156 if self
.use_autosave
:
157 if not bpy
.data
.filepath
:
158 # File is not saved on disk, better to abort!
159 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
161 bpy
.ops
.wm
.save_mainfile()
163 settings
["original_file"] = bpy
.data
.filepath
164 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
165 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
167 if self
.use_instance
:
170 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
172 logger
.error("Error on the new Blender instance")
174 logger
.error(traceback
.print_exc())
176 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
178 logger
.info("Opened linked file!")
180 self
.report({'WARNING'}, target
.name
+ " is not linked")
181 logger
.warning(target
.name
+ " is not linked")
186 class WM_OT_ReturnToOriginal(bpy
.types
.Operator
):
187 """Load the original file"""
188 bl_idname
= "wm.return_to_original"
189 bl_label
= "Return to Original File"
191 use_autosave
: bpy
.props
.BoolProperty(
193 description
="Save the current file before opening original file",
197 def poll(cls
, context
: bpy
.context
):
198 return (settings
["original_file"] != "")
200 def execute(self
, context
: bpy
.context
):
201 if self
.use_autosave
:
202 bpy
.ops
.wm
.save_mainfile()
204 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["original_file"])
206 settings
["original_file"] = ""
207 settings
["linked_objects"] = []
208 logger
.info("Back to the original!")
212 class VIEW3D_PT_PanelLinkedEdit(bpy
.types
.Panel
):
213 bl_label
= "Edit Linked Library"
214 bl_space_type
= "VIEW_3D"
215 bl_region_type
= 'UI'
217 bl_context
= 'objectmode'
218 bl_options
= {'DEFAULT_CLOSED'}
221 def poll(cls
, context
: bpy
.context
):
222 return (context
.active_object
is not None) or (settings
["original_file"] != "")
224 def draw_common(self
, scene
, layout
, props
):
225 if props
is not None:
226 props
.use_autosave
= scene
.use_autosave
227 props
.use_instance
= scene
.use_instance
229 layout
.prop(scene
, "use_autosave")
230 layout
.prop(scene
, "use_instance")
232 def draw(self
, context
: bpy
.context
):
233 scene
= context
.scene
235 layout
.use_property_split
= False
236 layout
.use_property_decorate
= False
237 icon
= "OUTLINER_DATA_" + context
.active_object
.type.replace("LIGHT_PROBE", "LIGHTPROBE")
241 target
= context
.active_object
.instance_collection
243 if settings
["original_file"] == "" and (
245 target
.library
is not None) or
246 context
.active_object
.library
is not None or
247 (context
.active_object
.override_library
is not None and
248 context
.active_object
.override_library
.reference
is not None)):
250 if (target
is not None):
251 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
252 text
="Edit Library: %s" % target
.name
)
253 elif (context
.active_object
.library
):
254 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
255 text
="Edit Library: %s" % context
.active_object
.name
)
257 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
258 text
="Edit Override Library: %s" % context
.active_object
.override_library
.reference
.name
)
260 self
.draw_common(scene
, layout
, props
)
262 if (target
is not None):
263 layout
.label(text
="Path: %s" %
264 target
.library
.filepath
)
265 elif (context
.active_object
.library
):
266 layout
.label(text
="Path: %s" %
267 context
.active_object
.library
.filepath
)
269 layout
.label(text
="Path: %s" %
270 context
.active_object
.override_library
.reference
.library
.filepath
)
272 elif settings
["original_file"] != "":
274 if scene
.use_instance
:
275 layout
.operator("wm.return_to_original",
276 text
="Reload Current File",
277 icon
="FILE_REFRESH").use_autosave
= False
281 # XXX - This is for nested linked assets... but it only works
282 # when launching a new Blender instance. Nested links don't
283 # currently work when using a single instance of Blender.
284 if context
.active_object
.instance_collection
is not None:
285 props
= layout
.operator("object.edit_linked",
286 text
="Edit Library: %s" % context
.active_object
.instance_collection
.name
,
291 self
.draw_common(scene
, layout
, props
)
293 if context
.active_object
.instance_collection
is not None:
294 layout
.label(text
="Path: %s" %
295 context
.active_object
.instance_collection
.library
.filepath
)
298 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
299 props
.use_autosave
= scene
.use_autosave
301 layout
.prop(scene
, "use_autosave")
304 layout
.label(text
="%s is not linked" % context
.active_object
.name
,
308 class NODE_PT_PanelLinkedEdit(bpy
.types
.Panel
):
309 bl_label
= "Edit Linked Library"
310 bl_space_type
= 'NODE_EDITOR'
311 bl_region_type
= 'UI'
312 if bpy
.app
.version
>= (2, 93, 0):
316 bl_options
= {'DEFAULT_CLOSED'}
319 def poll(cls
, context
):
320 return context
.active_node
is not None
322 def draw_common(self
, scene
, layout
, props
):
323 if props
is not None:
324 props
.use_autosave
= scene
.use_autosave
325 props
.use_instance
= scene
.use_instance
327 layout
.prop(scene
, "use_autosave")
328 layout
.prop(scene
, "use_instance")
330 def draw(self
, context
):
331 scene
= context
.scene
333 layout
.use_property_split
= False
334 layout
.use_property_decorate
= False
337 target
= context
.active_node
339 if settings
["original_file"] == "" and (
340 (target
.type == 'GROUP' and hasattr(target
.node_tree
, "library") and
341 target
.node_tree
.library
is not None) or
342 (hasattr(target
, "monad") and target
.monad
.library
is not None)):
344 if (target
.type == "GROUP"):
345 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
346 text
="Edit Library: %s" % target
.name
)
348 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
349 text
="Edit Library: %s" % target
.monad
.name
)
351 self
.draw_common(scene
, layout
, props
)
353 if (target
.type == "GROUP"):
354 layout
.label(text
="Path: %s" % target
.node_tree
.library
.filepath
)
356 layout
.label(text
="Path: %s" % target
.monad
.library
.filepath
)
358 elif settings
["original_file"] != "":
360 if scene
.use_instance
:
361 layout
.operator("wm.return_to_original",
362 text
="Reload Current File",
363 icon
="FILE_REFRESH").use_autosave
= False
369 self
.draw_common(scene
, layout
, props
)
371 #layout.label(text="Path: %s" %
372 # context.active_object.instance_collection.library.filepath)
375 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
376 props
.use_autosave
= scene
.use_autosave
378 layout
.prop(scene
, "use_autosave")
381 layout
.label(text
="%s is not linked" % target
.name
, icon
=icon
)
384 class TOPBAR_MT_edit_linked_submenu(bpy
.types
.Menu
):
385 bl_label
= 'Edit Linked Library'
387 def draw(self
, context
):
388 self
.layout
.separator()
389 self
.layout
.operator(OBJECT_OT_EditLinked
.bl_idname
)
390 self
.layout
.operator(WM_OT_ReturnToOriginal
.bl_idname
)
395 OBJECT_OT_EditLinked
,
397 WM_OT_ReturnToOriginal
,
398 VIEW3D_PT_PanelLinkedEdit
,
399 NODE_PT_PanelLinkedEdit
,
400 TOPBAR_MT_edit_linked_submenu
405 bpy
.app
.handlers
.load_post
.append(linked_file_check
)
408 bpy
.utils
.register_class(c
)
410 bpy
.types
.Scene
.use_autosave
= bpy
.props
.BoolProperty(
412 description
="Save the current file before opening a linked file",
415 bpy
.types
.Scene
.use_instance
= bpy
.props
.BoolProperty(
416 name
="New Blender Instance",
417 description
="Open in a new Blender instance",
420 # add the function to the file menu
421 bpy
.types
.TOPBAR_MT_file_external_data
.append(TOPBAR_MT_edit_linked_submenu
.draw
)
428 bpy
.app
.handlers
.load_post
.remove(linked_file_check
)
429 bpy
.types
.TOPBAR_MT_file_external_data
.remove(TOPBAR_MT_edit_linked_submenu
)
431 del bpy
.types
.Scene
.use_autosave
432 del bpy
.types
.Scene
.use_instance
435 for c
in reversed(classes
):
436 bpy
.utils
.unregister_class(c
)
439 if __name__
== "__main__":