1 # ***** BEGIN GPL LICENSE BLOCK *****
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # ***** END GPL LICENCE BLOCK *****
21 "name": "Edit Linked Library",
22 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
24 "blender": (2, 80, 0),
25 "location": "File > External Data / View3D > Sidebar > Item Tab / Node Editor > Sidebar > Node Tab",
26 "description": "Allows editing of objects, collections, and node groups linked from a .blend library.",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/edit_linked_library.html",
35 from bpy
.app
.handlers
import persistent
37 logger
= logging
.getLogger('object_edit_linked')
48 def linked_file_check(context
: bpy
.context
):
49 if settings
["linked_file"] != "":
50 if os
.path
.samefile(settings
["linked_file"], bpy
.data
.filepath
):
51 logger
.info("Editing a linked library.")
52 bpy
.ops
.object.select_all(action
='DESELECT')
53 for ob_name
in settings
["linked_objects"]:
54 bpy
.data
.objects
[ob_name
].select_set(True) # XXX Assumes selected object is in the active scene
55 if len(settings
["linked_objects"]) == 1:
56 context
.view_layer
.objects
.active
= bpy
.data
.objects
[settings
["linked_objects"][0]]
58 # For some reason, the linked editing session ended
59 # (failed to find a file or opened a different file
60 # before returning to the originating .blend)
61 settings
["original_file"] = ""
62 settings
["linked_file"] = ""
65 class OBJECT_OT_EditLinked(bpy
.types
.Operator
):
66 """Edit Linked Library"""
67 bl_idname
= "object.edit_linked"
68 bl_label
= "Edit Linked Library"
70 use_autosave
: bpy
.props
.BoolProperty(
72 description
="Save the current file before opening the linked library",
74 use_instance
: bpy
.props
.BoolProperty(
75 name
="New Blender Instance",
76 description
="Open in a new Blender instance",
80 def poll(cls
, context
: bpy
.context
):
81 return settings
["original_file"] == "" and context
.active_object
is not None and (
82 (context
.active_object
.instance_collection
and
83 context
.active_object
.instance_collection
.library
is not None) or
84 (context
.active_object
.proxy
and
85 context
.active_object
.proxy
.library
is not None) or
86 context
.active_object
.library
is not None or
87 (context
.active_object
.override_library
and
88 context
.active_object
.override_library
.reference
.library
is not None))
90 def execute(self
, context
: bpy
.context
):
91 target
= context
.active_object
93 if target
.instance_collection
and target
.instance_collection
.library
:
94 targetpath
= target
.instance_collection
.library
.filepath
95 settings
["linked_objects"].extend({ob
.name
for ob
in target
.instance_collection
.objects
})
97 targetpath
= target
.library
.filepath
98 settings
["linked_objects"].append(target
.name
)
100 target
= target
.proxy
101 targetpath
= target
.library
.filepath
102 settings
["linked_objects"].append(target
.name
)
103 elif target
.override_library
:
104 target
= target
.override_library
.reference
105 targetpath
= target
.library
.filepath
106 settings
["linked_objects"].append(target
.name
)
109 logger
.debug(target
.name
+ " is linked to " + targetpath
)
111 if self
.use_autosave
:
112 if not bpy
.data
.filepath
:
113 # File is not saved on disk, better to abort!
114 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
116 bpy
.ops
.wm
.save_mainfile()
118 settings
["original_file"] = bpy
.data
.filepath
119 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
120 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
122 if self
.use_instance
:
125 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
127 logger
.error("Error on the new Blender instance")
129 logger
.error(traceback
.print_exc())
131 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
133 logger
.info("Opened linked file!")
135 self
.report({'WARNING'}, target
.name
+ " is not linked")
136 logger
.warning(target
.name
+ " is not linked")
141 class NODE_OT_EditLinked(bpy
.types
.Operator
):
142 """Edit Linked Library"""
143 bl_idname
= "node.edit_linked"
144 bl_label
= "Edit Linked Library"
146 use_autosave
: bpy
.props
.BoolProperty(
148 description
="Save the current file before opening the linked library",
150 use_instance
: bpy
.props
.BoolProperty(
151 name
="New Blender Instance",
152 description
="Open in a new Blender instance",
156 def poll(cls
, context
: bpy
.context
):
157 return settings
["original_file"] == "" and context
.active_node
is not None and (
158 (context
.active_node
.type == 'GROUP' and
159 hasattr(context
.active_node
.node_tree
, "library") and
160 context
.active_node
.node_tree
.library
is not None) or
161 (hasattr(context
.active_node
, "monad") and
162 context
.active_node
.monad
.library
is not None))
164 def execute(self
, context
: bpy
.context
):
165 target
= context
.active_node
166 if (target
.type == "GROUP"):
167 target
= target
.node_tree
169 target
= target
.monad
171 targetpath
= target
.library
.filepath
172 settings
["linked_nodes"].append(target
.name
)
175 logger
.debug(target
.name
+ " is linked to " + targetpath
)
177 if self
.use_autosave
:
178 if not bpy
.data
.filepath
:
179 # File is not saved on disk, better to abort!
180 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
182 bpy
.ops
.wm
.save_mainfile()
184 settings
["original_file"] = bpy
.data
.filepath
185 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
186 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
188 if self
.use_instance
:
191 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
193 logger
.error("Error on the new Blender instance")
195 logger
.error(traceback
.print_exc())
197 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
199 logger
.info("Opened linked file!")
201 self
.report({'WARNING'}, target
.name
+ " is not linked")
202 logger
.warning(target
.name
+ " is not linked")
207 class WM_OT_ReturnToOriginal(bpy
.types
.Operator
):
208 """Load the original file"""
209 bl_idname
= "wm.return_to_original"
210 bl_label
= "Return to Original File"
212 use_autosave
: bpy
.props
.BoolProperty(
214 description
="Save the current file before opening original file",
218 def poll(cls
, context
: bpy
.context
):
219 return (settings
["original_file"] != "")
221 def execute(self
, context
: bpy
.context
):
222 if self
.use_autosave
:
223 bpy
.ops
.wm
.save_mainfile()
225 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["original_file"])
227 settings
["original_file"] = ""
228 settings
["linked_objects"] = []
229 logger
.info("Back to the original!")
233 class VIEW3D_PT_PanelLinkedEdit(bpy
.types
.Panel
):
234 bl_label
= "Edit Linked Library"
235 bl_space_type
= "VIEW_3D"
236 bl_region_type
= 'UI'
238 bl_context
= 'objectmode'
239 bl_options
= {'DEFAULT_CLOSED'}
242 def poll(cls
, context
: bpy
.context
):
243 return (context
.active_object
is not None) or (settings
["original_file"] != "")
245 def draw_common(self
, scene
, layout
, props
):
246 if props
is not None:
247 props
.use_autosave
= scene
.use_autosave
248 props
.use_instance
= scene
.use_instance
250 layout
.prop(scene
, "use_autosave")
251 layout
.prop(scene
, "use_instance")
253 def draw(self
, context
: bpy
.context
):
254 scene
= context
.scene
256 layout
.use_property_split
= False
257 layout
.use_property_decorate
= False
258 icon
= "OUTLINER_DATA_" + context
.active_object
.type.replace("LIGHT_PROBE", "LIGHTPROBE")
262 if context
.active_object
.proxy
:
263 target
= context
.active_object
.proxy
265 target
= context
.active_object
.instance_collection
267 if settings
["original_file"] == "" and (
269 target
.library
is not None) or
270 context
.active_object
.library
is not None or
271 (context
.active_object
.override_library
is not None and
272 context
.active_object
.override_library
.reference
is not None)):
274 if (target
is not None):
275 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
276 text
="Edit Library: %s" % target
.name
)
277 elif (context
.active_object
.library
):
278 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
279 text
="Edit Library: %s" % context
.active_object
.name
)
281 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
282 text
="Edit Override Library: %s" % context
.active_object
.override_library
.reference
.name
)
284 self
.draw_common(scene
, layout
, props
)
286 if (target
is not None):
287 layout
.label(text
="Path: %s" %
288 target
.library
.filepath
)
289 elif (context
.active_object
.library
):
290 layout
.label(text
="Path: %s" %
291 context
.active_object
.library
.filepath
)
293 layout
.label(text
="Path: %s" %
294 context
.active_object
.override_library
.reference
.library
.filepath
)
296 elif settings
["original_file"] != "":
298 if scene
.use_instance
:
299 layout
.operator("wm.return_to_original",
300 text
="Reload Current File",
301 icon
="FILE_REFRESH").use_autosave
= False
305 # XXX - This is for nested linked assets... but it only works
306 # when launching a new Blender instance. Nested links don't
307 # currently work when using a single instance of Blender.
308 if context
.active_object
.instance_collection
is not None:
309 props
= layout
.operator("object.edit_linked",
310 text
="Edit Library: %s" % context
.active_object
.instance_collection
.name
,
315 self
.draw_common(scene
, layout
, props
)
317 if context
.active_object
.instance_collection
is not None:
318 layout
.label(text
="Path: %s" %
319 context
.active_object
.instance_collection
.library
.filepath
)
322 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
323 props
.use_autosave
= scene
.use_autosave
325 layout
.prop(scene
, "use_autosave")
328 layout
.label(text
="%s is not linked" % context
.active_object
.name
,
332 class NODE_PT_PanelLinkedEdit(bpy
.types
.Panel
):
333 bl_label
= "Edit Linked Library"
334 bl_space_type
= 'NODE_EDITOR'
335 bl_region_type
= 'UI'
336 if bpy
.app
.version
>= (2, 93, 0):
340 bl_options
= {'DEFAULT_CLOSED'}
343 def poll(cls
, context
):
344 return context
.active_node
is not None
346 def draw_common(self
, scene
, layout
, props
):
347 if props
is not None:
348 props
.use_autosave
= scene
.use_autosave
349 props
.use_instance
= scene
.use_instance
351 layout
.prop(scene
, "use_autosave")
352 layout
.prop(scene
, "use_instance")
354 def draw(self
, context
):
355 scene
= context
.scene
357 layout
.use_property_split
= False
358 layout
.use_property_decorate
= False
361 target
= context
.active_node
363 if settings
["original_file"] == "" and (
364 (target
.type == 'GROUP' and hasattr(target
.node_tree
, "library") and
365 target
.node_tree
.library
is not None) or
366 (hasattr(target
, "monad") and target
.monad
.library
is not None)):
368 if (target
.type == "GROUP"):
369 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
370 text
="Edit Library: %s" % target
.name
)
372 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
373 text
="Edit Library: %s" % target
.monad
.name
)
375 self
.draw_common(scene
, layout
, props
)
377 if (target
.type == "GROUP"):
378 layout
.label(text
="Path: %s" % target
.node_tree
.library
.filepath
)
380 layout
.label(text
="Path: %s" % target
.monad
.library
.filepath
)
382 elif settings
["original_file"] != "":
384 if scene
.use_instance
:
385 layout
.operator("wm.return_to_original",
386 text
="Reload Current File",
387 icon
="FILE_REFRESH").use_autosave
= False
393 self
.draw_common(scene
, layout
, props
)
395 #layout.label(text="Path: %s" %
396 # context.active_object.instance_collection.library.filepath)
399 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
400 props
.use_autosave
= scene
.use_autosave
402 layout
.prop(scene
, "use_autosave")
405 layout
.label(text
="%s is not linked" % target
.name
, icon
=icon
)
408 class TOPBAR_MT_edit_linked_submenu(bpy
.types
.Menu
):
409 bl_label
= 'Edit Linked Library'
411 def draw(self
, context
):
412 self
.layout
.separator()
413 self
.layout
.operator(OBJECT_OT_EditLinked
.bl_idname
)
414 self
.layout
.operator(WM_OT_ReturnToOriginal
.bl_idname
)
419 OBJECT_OT_EditLinked
,
421 WM_OT_ReturnToOriginal
,
422 VIEW3D_PT_PanelLinkedEdit
,
423 NODE_PT_PanelLinkedEdit
,
424 TOPBAR_MT_edit_linked_submenu
429 bpy
.app
.handlers
.load_post
.append(linked_file_check
)
432 bpy
.utils
.register_class(c
)
434 bpy
.types
.Scene
.use_autosave
= bpy
.props
.BoolProperty(
436 description
="Save the current file before opening a linked file",
439 bpy
.types
.Scene
.use_instance
= bpy
.props
.BoolProperty(
440 name
="New Blender Instance",
441 description
="Open in a new Blender instance",
444 # add the function to the file menu
445 bpy
.types
.TOPBAR_MT_file_external_data
.append(TOPBAR_MT_edit_linked_submenu
.draw
)
452 bpy
.app
.handlers
.load_post
.remove(linked_file_check
)
453 bpy
.types
.TOPBAR_MT_file_external_data
.remove(TOPBAR_MT_edit_linked_submenu
)
455 del bpy
.types
.Scene
.use_autosave
456 del bpy
.types
.Scene
.use_instance
459 for c
in reversed(classes
):
460 bpy
.utils
.unregister_class(c
)
463 if __name__
== "__main__":