Cleanup: quiet warnings with descriptions ending with a '.'
[blender-addons.git] / object_edit_linked.py
blobf7177766fbfa6b81681d46babea1b78f7b15759c
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Edit Linked Library",
5 "author": "Jason van Gumster (Fweeb), Bassam Kurdali, Pablo Vazquez, Rainer Trummer",
6 "version": (0, 9, 2),
7 "blender": (2, 80, 0),
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",
11 "category": "Object",
14 import bpy
15 import logging
16 import os
18 from bpy.app.handlers import persistent
20 logger = logging.getLogger('object_edit_linked')
22 settings = {
23 "original_file": "",
24 "linked_file": "",
25 "linked_objects": [],
26 "linked_nodes": []
30 @persistent
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]]
40 else:
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(
54 name="Autosave",
55 description="Save the current file before opening the linked library",
56 default=True)
57 use_instance: bpy.props.BoolProperty(
58 name="New Blender Instance",
59 description="Open in a new Blender instance",
60 default=False)
62 @classmethod
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})
77 elif target.library:
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)
85 if targetpath:
86 logger.debug(target.name + " is linked to " + targetpath)
88 if self.use_autosave:
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")
92 return {'CANCELLED'}
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))
99 if self.use_instance:
100 import subprocess
101 try:
102 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
103 except:
104 logger.error("Error on the new Blender instance")
105 import traceback
106 logger.error(traceback.print_exc())
107 else:
108 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
110 logger.info("Opened linked file!")
111 else:
112 self.report({'WARNING'}, target.name + " is not linked")
113 logger.warning(target.name + " is not linked")
115 return {'FINISHED'}
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(
124 name="Autosave",
125 description="Save the current file before opening the linked library",
126 default=True)
127 use_instance: bpy.props.BoolProperty(
128 name="New Blender Instance",
129 description="Open in a new Blender instance",
130 default=False)
132 @classmethod
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
145 else:
146 target = target.monad
148 targetpath = target.library.filepath
149 settings["linked_nodes"].append(target.name)
151 if targetpath:
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")
158 return {'CANCELLED'}
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:
166 import subprocess
167 try:
168 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
169 except:
170 logger.error("Error on the new Blender instance")
171 import traceback
172 logger.error(traceback.print_exc())
173 else:
174 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
176 logger.info("Opened linked file!")
177 else:
178 self.report({'WARNING'}, target.name + " is not linked")
179 logger.warning(target.name + " is not linked")
181 return {'FINISHED'}
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(
190 name="Autosave",
191 description="Save the current file before opening original file",
192 default=True)
194 @classmethod
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!")
207 return {'FINISHED'}
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'
214 bl_category = "Item"
215 bl_context = 'objectmode'
216 bl_options = {'DEFAULT_CLOSED'}
218 @classmethod
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
232 layout = self.layout
233 layout.use_property_split = False
234 layout.use_property_decorate = False
235 icon = "OUTLINER_DATA_" + context.active_object.type.replace("LIGHT_PROBE", "LIGHTPROBE")
237 target = None
239 target = context.active_object.instance_collection
241 if settings["original_file"] == "" and (
242 (target 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)
254 else:
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)
266 else:
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
277 layout.separator()
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,
285 icon="LINK_BLEND")
286 else:
287 props = None
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)
295 else:
296 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
297 props.use_autosave = scene.use_autosave
299 layout.prop(scene, "use_autosave")
301 else:
302 layout.label(text="%s is not linked" % context.active_object.name,
303 icon=icon)
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):
311 bl_category = "Node"
312 else:
313 bl_category = "Item"
314 bl_options = {'DEFAULT_CLOSED'}
316 @classmethod
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
330 layout = self.layout
331 layout.use_property_split = False
332 layout.use_property_decorate = False
333 icon = 'NODETREE'
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)
345 else:
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)
353 else:
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
363 layout.separator()
365 props = None
367 self.draw_common(scene, layout, props)
369 #layout.label(text="Path: %s" %
370 # context.active_object.instance_collection.library.filepath)
372 else:
373 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
374 props.use_autosave = scene.use_autosave
376 layout.prop(scene, "use_autosave")
378 else:
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)
391 addon_keymaps = []
392 classes = (
393 OBJECT_OT_EditLinked,
394 NODE_OT_EditLinked,
395 WM_OT_ReturnToOriginal,
396 VIEW3D_PT_PanelLinkedEdit,
397 NODE_PT_PanelLinkedEdit,
398 TOPBAR_MT_edit_linked_submenu
402 def register():
403 bpy.app.handlers.load_post.append(linked_file_check)
405 for c in classes:
406 bpy.utils.register_class(c)
408 bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
409 name="Autosave",
410 description="Save the current file before opening a linked file",
411 default=True)
413 bpy.types.Scene.use_instance = bpy.props.BoolProperty(
414 name="New Blender Instance",
415 description="Open in a new Blender instance",
416 default=False)
418 # add the function to the file menu
419 bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)
424 def unregister():
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__":
438 register()