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)
88 def execute(self
, context
: bpy
.context
):
89 target
= context
.active_object
91 if target
.instance_collection
and target
.instance_collection
.library
:
92 targetpath
= target
.instance_collection
.library
.filepath
93 settings
["linked_objects"].extend({ob
.name
for ob
in target
.instance_collection
.objects
})
95 targetpath
= target
.library
.filepath
96 settings
["linked_objects"].append(target
.name
)
99 targetpath
= target
.library
.filepath
100 settings
["linked_objects"].append(target
.name
)
103 logger
.debug(target
.name
+ " is linked to " + targetpath
)
105 if self
.use_autosave
:
106 if not bpy
.data
.filepath
:
107 # File is not saved on disk, better to abort!
108 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
110 bpy
.ops
.wm
.save_mainfile()
112 settings
["original_file"] = bpy
.data
.filepath
113 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
114 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
116 if self
.use_instance
:
119 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
121 logger
.error("Error on the new Blender instance")
123 logger
.error(traceback
.print_exc())
125 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
127 logger
.info("Opened linked file!")
129 self
.report({'WARNING'}, target
.name
+ " is not linked")
130 logger
.warning(target
.name
+ " is not linked")
135 class NODE_OT_EditLinked(bpy
.types
.Operator
):
136 """Edit Linked Library"""
137 bl_idname
= "node.edit_linked"
138 bl_label
= "Edit Linked Library"
140 use_autosave
: bpy
.props
.BoolProperty(
142 description
="Save the current file before opening the linked library",
144 use_instance
: bpy
.props
.BoolProperty(
145 name
="New Blender Instance",
146 description
="Open in a new Blender instance",
150 def poll(cls
, context
: bpy
.context
):
151 return settings
["original_file"] == "" and context
.active_node
is not None and (
152 context
.active_node
.type == 'GROUP' and
153 hasattr(context
.active_node
.node_tree
, "library") and
154 context
.active_node
.node_tree
.library
is not None)
156 def execute(self
, context
: bpy
.context
):
157 target
= context
.active_node
.node_tree
159 targetpath
= target
.library
.filepath
160 settings
["linked_nodes"].append(target
.name
)
163 logger
.debug(target
.name
+ " is linked to " + targetpath
)
165 if self
.use_autosave
:
166 if not bpy
.data
.filepath
:
167 # File is not saved on disk, better to abort!
168 self
.report({'ERROR'}, "Current file does not exist on disk, we cannot autosave it, aborting")
170 bpy
.ops
.wm
.save_mainfile()
172 settings
["original_file"] = bpy
.data
.filepath
173 # Using both bpy and os abspath functions because Windows doesn't like relative routes as part of an absolute path
174 settings
["linked_file"] = os
.path
.abspath(bpy
.path
.abspath(targetpath
))
176 if self
.use_instance
:
179 subprocess
.Popen([bpy
.app
.binary_path
, settings
["linked_file"]])
181 logger
.error("Error on the new Blender instance")
183 logger
.error(traceback
.print_exc())
185 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["linked_file"])
187 logger
.info("Opened linked file!")
189 self
.report({'WARNING'}, target
.name
+ " is not linked")
190 logger
.warning(target
.name
+ " is not linked")
195 class WM_OT_ReturnToOriginal(bpy
.types
.Operator
):
196 """Load the original file"""
197 bl_idname
= "wm.return_to_original"
198 bl_label
= "Return to Original File"
200 use_autosave
: bpy
.props
.BoolProperty(
202 description
="Save the current file before opening original file",
206 def poll(cls
, context
: bpy
.context
):
207 return (settings
["original_file"] != "")
209 def execute(self
, context
: bpy
.context
):
210 if self
.use_autosave
:
211 bpy
.ops
.wm
.save_mainfile()
213 bpy
.ops
.wm
.open_mainfile(filepath
=settings
["original_file"])
215 settings
["original_file"] = ""
216 settings
["linked_objects"] = []
217 logger
.info("Back to the original!")
221 class VIEW3D_PT_PanelLinkedEdit(bpy
.types
.Panel
):
222 bl_label
= "Edit Linked Library"
223 bl_space_type
= "VIEW_3D"
224 bl_region_type
= 'UI'
226 bl_context
= 'objectmode'
227 bl_options
= {'DEFAULT_CLOSED'}
230 def poll(cls
, context
: bpy
.context
):
231 return (context
.active_object
is not None) or (settings
["original_file"] != "")
233 def draw_common(self
, scene
, layout
, props
):
234 if props
is not None:
235 props
.use_autosave
= scene
.use_autosave
236 props
.use_instance
= scene
.use_instance
238 layout
.prop(scene
, "use_autosave")
239 layout
.prop(scene
, "use_instance")
241 def draw(self
, context
: bpy
.context
):
242 scene
= context
.scene
244 layout
.use_property_split
= False
245 layout
.use_property_decorate
= False
246 icon
= "OUTLINER_DATA_" + context
.active_object
.type.replace("LIGHT_PROBE", "LIGHTPROBE")
250 if context
.active_object
.proxy
:
251 target
= context
.active_object
.proxy
253 target
= context
.active_object
.instance_collection
255 if settings
["original_file"] == "" and (
257 target
.library
is not None) or
258 context
.active_object
.library
is not None):
260 if (target
is not None):
261 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
262 text
="Edit Library: %s" % target
.name
)
264 props
= layout
.operator("object.edit_linked", icon
="LINK_BLEND",
265 text
="Edit Library: %s" % context
.active_object
.name
)
267 self
.draw_common(scene
, layout
, props
)
269 if (target
is not None):
270 layout
.label(text
="Path: %s" %
271 target
.library
.filepath
)
273 layout
.label(text
="Path: %s" %
274 context
.active_object
.library
.filepath
)
276 elif settings
["original_file"] != "":
278 if scene
.use_instance
:
279 layout
.operator("wm.return_to_original",
280 text
="Reload Current File",
281 icon
="FILE_REFRESH").use_autosave
= False
285 # XXX - This is for nested linked assets... but it only works
286 # when launching a new Blender instance. Nested links don't
287 # currently work when using a single instance of Blender.
288 if context
.active_object
.instance_collection
is not None:
289 props
= layout
.operator("object.edit_linked",
290 text
="Edit Library: %s" % context
.active_object
.instance_collection
.name
,
295 self
.draw_common(scene
, layout
, props
)
297 if context
.active_object
.instance_collection
is not None:
298 layout
.label(text
="Path: %s" %
299 context
.active_object
.instance_collection
.library
.filepath
)
302 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
303 props
.use_autosave
= scene
.use_autosave
305 layout
.prop(scene
, "use_autosave")
308 layout
.label(text
="%s is not linked" % context
.active_object
.name
,
312 class NODE_PT_PanelLinkedEdit(bpy
.types
.Panel
):
313 bl_label
= "Edit Linked Library"
314 bl_space_type
= 'NODE_EDITOR'
315 bl_region_type
= 'UI'
316 if bpy
.app
.version
>= (2, 93, 0):
320 bl_options
= {'DEFAULT_CLOSED'}
323 def poll(cls
, context
):
324 return context
.active_node
is not None
326 def draw_common(self
, scene
, layout
, props
):
327 if props
is not None:
328 props
.use_autosave
= scene
.use_autosave
329 props
.use_instance
= scene
.use_instance
331 layout
.prop(scene
, "use_autosave")
332 layout
.prop(scene
, "use_instance")
334 def draw(self
, context
):
335 scene
= context
.scene
337 layout
.use_property_split
= False
338 layout
.use_property_decorate
= False
341 target
= context
.active_node
343 if settings
["original_file"] == "" and (
344 target
.type == 'GROUP' and hasattr(target
.node_tree
, "library") and
345 target
.node_tree
.library
is not None):
347 props
= layout
.operator("node.edit_linked", icon
="LINK_BLEND",
348 text
="Edit Library: %s" % target
.name
)
350 self
.draw_common(scene
, layout
, props
)
352 layout
.label(text
="Path: %s" % target
.node_tree
.library
.filepath
)
354 elif settings
["original_file"] != "":
356 if scene
.use_instance
:
357 layout
.operator("wm.return_to_original",
358 text
="Reload Current File",
359 icon
="FILE_REFRESH").use_autosave
= False
365 self
.draw_common(scene
, layout
, props
)
367 #layout.label(text="Path: %s" %
368 # context.active_object.instance_collection.library.filepath)
371 props
= layout
.operator("wm.return_to_original", icon
="LOOP_BACK")
372 props
.use_autosave
= scene
.use_autosave
374 layout
.prop(scene
, "use_autosave")
377 layout
.label(text
="%s is not linked" % target
.name
, icon
=icon
)
380 class TOPBAR_MT_edit_linked_submenu(bpy
.types
.Menu
):
381 bl_label
= 'Edit Linked Library'
383 def draw(self
, context
):
384 self
.layout
.separator()
385 self
.layout
.operator(OBJECT_OT_EditLinked
.bl_idname
)
386 self
.layout
.operator(WM_OT_ReturnToOriginal
.bl_idname
)
391 OBJECT_OT_EditLinked
,
393 WM_OT_ReturnToOriginal
,
394 VIEW3D_PT_PanelLinkedEdit
,
395 NODE_PT_PanelLinkedEdit
,
396 TOPBAR_MT_edit_linked_submenu
401 bpy
.app
.handlers
.load_post
.append(linked_file_check
)
404 bpy
.utils
.register_class(c
)
406 bpy
.types
.Scene
.use_autosave
= bpy
.props
.BoolProperty(
408 description
="Save the current file before opening a linked file",
411 bpy
.types
.Scene
.use_instance
= bpy
.props
.BoolProperty(
412 name
="New Blender Instance",
413 description
="Open in a new Blender instance",
416 # add the function to the file menu
417 bpy
.types
.TOPBAR_MT_file_external_data
.append(TOPBAR_MT_edit_linked_submenu
.draw
)
424 bpy
.app
.handlers
.load_post
.remove(linked_file_check
)
425 bpy
.types
.TOPBAR_MT_file_external_data
.remove(TOPBAR_MT_edit_linked_submenu
)
427 del bpy
.types
.Scene
.use_autosave
428 del bpy
.types
.Scene
.use_instance
431 for c
in reversed(classes
):
432 bpy
.utils
.unregister_class(c
)
435 if __name__
== "__main__":