Cleanup: quiet character escape warnings
[blender-addons.git] / object_edit_linked.py
blob9299a575348772fdaf6c2805ea7414fb3e3f55a4
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 *****
20 bl_info = {
21 "name": "Edit Linked Library",
22 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
23 "version": (0, 9, 2),
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",
28 "category": "Object",
31 import bpy
32 import logging
33 import os
35 from bpy.app.handlers import persistent
37 logger = logging.getLogger('object_edit_linked')
39 settings = {
40 "original_file": "",
41 "linked_file": "",
42 "linked_objects": [],
43 "linked_nodes": []
47 @persistent
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]]
57 else:
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(
71 name="Autosave",
72 description="Save the current file before opening the linked library",
73 default=True)
74 use_instance: bpy.props.BoolProperty(
75 name="New Blender Instance",
76 description="Open in a new Blender instance",
77 default=False)
79 @classmethod
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})
96 elif target.library:
97 targetpath = target.library.filepath
98 settings["linked_objects"].append(target.name)
99 elif target.proxy:
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)
108 if targetpath:
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")
115 return {'CANCELLED'}
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:
123 import subprocess
124 try:
125 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
126 except:
127 logger.error("Error on the new Blender instance")
128 import traceback
129 logger.error(traceback.print_exc())
130 else:
131 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
133 logger.info("Opened linked file!")
134 else:
135 self.report({'WARNING'}, target.name + " is not linked")
136 logger.warning(target.name + " is not linked")
138 return {'FINISHED'}
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(
147 name="Autosave",
148 description="Save the current file before opening the linked library",
149 default=True)
150 use_instance: bpy.props.BoolProperty(
151 name="New Blender Instance",
152 description="Open in a new Blender instance",
153 default=False)
155 @classmethod
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
168 else:
169 target = target.monad
171 targetpath = target.library.filepath
172 settings["linked_nodes"].append(target.name)
174 if targetpath:
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")
181 return {'CANCELLED'}
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:
189 import subprocess
190 try:
191 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
192 except:
193 logger.error("Error on the new Blender instance")
194 import traceback
195 logger.error(traceback.print_exc())
196 else:
197 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
199 logger.info("Opened linked file!")
200 else:
201 self.report({'WARNING'}, target.name + " is not linked")
202 logger.warning(target.name + " is not linked")
204 return {'FINISHED'}
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(
213 name="Autosave",
214 description="Save the current file before opening original file",
215 default=True)
217 @classmethod
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!")
230 return {'FINISHED'}
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'
237 bl_category = "Item"
238 bl_context = 'objectmode'
239 bl_options = {'DEFAULT_CLOSED'}
241 @classmethod
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
255 layout = self.layout
256 layout.use_property_split = False
257 layout.use_property_decorate = False
258 icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")
260 target = None
262 if context.active_object.proxy:
263 target = context.active_object.proxy
264 else:
265 target = context.active_object.instance_collection
267 if settings["original_file"] == "" and (
268 (target 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)
280 else:
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)
292 else:
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
303 layout.separator()
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,
311 icon="LINK_BLEND")
312 else:
313 props = None
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)
321 else:
322 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
323 props.use_autosave = scene.use_autosave
325 layout.prop(scene, "use_autosave")
327 else:
328 layout.label(text="%s is not linked" % context.active_object.name,
329 icon=icon)
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):
337 bl_category = "Node"
338 else:
339 bl_category = "Item"
340 bl_options = {'DEFAULT_CLOSED'}
342 @classmethod
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
356 layout = self.layout
357 layout.use_property_split = False
358 layout.use_property_decorate = False
359 icon = 'NODETREE'
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)
371 else:
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)
379 else:
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
389 layout.separator()
391 props = None
393 self.draw_common(scene, layout, props)
395 #layout.label(text="Path: %s" %
396 # context.active_object.instance_collection.library.filepath)
398 else:
399 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
400 props.use_autosave = scene.use_autosave
402 layout.prop(scene, "use_autosave")
404 else:
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)
417 addon_keymaps = []
418 classes = (
419 OBJECT_OT_EditLinked,
420 NODE_OT_EditLinked,
421 WM_OT_ReturnToOriginal,
422 VIEW3D_PT_PanelLinkedEdit,
423 NODE_PT_PanelLinkedEdit,
424 TOPBAR_MT_edit_linked_submenu
428 def register():
429 bpy.app.handlers.load_post.append(linked_file_check)
431 for c in classes:
432 bpy.utils.register_class(c)
434 bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
435 name="Autosave",
436 description="Save the current file before opening a linked file",
437 default=True)
439 bpy.types.Scene.use_instance = bpy.props.BoolProperty(
440 name="New Blender Instance",
441 description="Open in a new Blender instance",
442 default=False)
444 # add the function to the file menu
445 bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)
450 def unregister():
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__":
464 register()