Merge branch 'master' into blender2.8
[blender-addons.git] / object_edit_linked.py
blob0077fb509503e2e15f9a26becad46f5f11199fb3
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, 1),
24 "blender": (2, 80, 0),
25 "location": "File > External Data > Edit Linked Library",
26 "description": "Allows editing of objects linked from a .blend library.",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Object/Edit_Linked_Library",
29 "category": "Object",
32 import bpy
33 import logging
34 import os
36 from bpy.app.handlers import persistent
38 logger = logging.getLogger('object_edit_linked')
40 settings = {
41 "original_file": "",
42 "linked_file": "",
43 "linked_objects": [],
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)
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})
94 elif target.library:
95 targetpath = target.library.filepath
96 settings["linked_objects"].append(target.name)
97 elif target.proxy:
98 target = target.proxy
99 targetpath = target.library.filepath
100 settings["linked_objects"].append(target.name)
102 if targetpath:
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")
109 return {'CANCELLED'}
110 bpy.ops.wm.save_mainfile()
112 settings["original_file"] = bpy.data.filepath
113 settings["linked_file"] = bpy.path.abspath(targetpath)
115 if self.use_instance:
116 import subprocess
117 try:
118 subprocess.Popen([bpy.app.binary_path, settings["linked_file"]])
119 except:
120 logger.error("Error on the new Blender instance")
121 import traceback
122 logger.error(traceback.print_exc())
123 else:
124 bpy.ops.wm.open_mainfile(filepath=settings["linked_file"])
126 logger.info("Opened linked file!")
127 else:
128 self.report({'WARNING'}, target.name + " is not linked")
129 logger.warning(target.name + " is not linked")
131 return {'FINISHED'}
134 class WM_OT_ReturnToOriginal(bpy.types.Operator):
135 """Load the original file"""
136 bl_idname = "wm.return_to_original"
137 bl_label = "Return to Original File"
139 use_autosave: bpy.props.BoolProperty(
140 name="Autosave",
141 description="Save the current file before opening original file",
142 default=True)
144 @classmethod
145 def poll(cls, context: bpy.context):
146 return (settings["original_file"] != "")
148 def execute(self, context: bpy.context):
149 if self.use_autosave:
150 bpy.ops.wm.save_mainfile()
152 bpy.ops.wm.open_mainfile(filepath=settings["original_file"])
154 settings["original_file"] = ""
155 settings["linked_objects"] = []
156 logger.info("Back to the original!")
157 return {'FINISHED'}
160 class VIEW3D_PT_PanelLinkedEdit(bpy.types.Panel):
161 bl_label = "Edit Linked Library"
162 bl_space_type = "VIEW_3D"
163 bl_region_type = 'UI'
164 bl_category = "View"
165 bl_context = 'objectmode'
167 @classmethod
168 def poll(cls, context: bpy.context):
169 return (context.active_object is not None) or (settings["original_file"] != "")
171 def draw_common(self, scene, layout, props):
172 props.use_autosave = scene.use_autosave
173 props.use_instance = scene.use_instance
175 layout.prop(scene, "use_autosave")
176 layout.prop(scene, "use_instance")
178 def draw(self, context: bpy.context):
179 layout = self.layout
180 scene = context.scene
181 icon = "OUTLINER_DATA_" + context.active_object.type
183 target = None
185 if context.active_object.proxy:
186 target = context.active_object.proxy
187 else:
188 target = context.active_object.instance_collection
190 if settings["original_file"] == "" and (
191 (target and
192 target.library is not None) or
193 context.active_object.library is not None):
195 if (target is not None):
196 props = layout.operator("object.edit_linked", icon="LINK_BLEND",
197 text="Edit Library: %s" % target.name)
198 else:
199 props = layout.operator("object.edit_linked", icon="LINK_BLEND",
200 text="Edit Library: %s" % context.active_object.name)
202 self.draw_common(scene, layout, props)
204 if (target is not None):
205 layout.label(text="Path: %s" %
206 target.library.filepath)
207 else:
208 layout.label(text="Path: %s" %
209 context.active_object.library.filepath)
211 elif settings["original_file"] != "":
213 if scene.use_instance:
214 layout.operator("wm.return_to_original",
215 text="Reload Current File",
216 icon="FILE_REFRESH").use_autosave = False
218 layout.separator()
220 # XXX - This is for nested linked assets... but it only works
221 # when launching a new Blender instance. Nested links don't
222 # currently work when using a single instance of Blender.
223 props = layout.operator("object.edit_linked",
224 text="Edit Library: %s" % context.active_object.instance_collection.name,
225 icon="LINK_BLEND")
227 self.draw_common(scene, layout, props)
229 layout.label(text="Path: %s" %
230 context.active_object.instance_collection.library.filepath)
232 else:
233 props = layout.operator("wm.return_to_original", icon="LOOP_BACK")
234 props.use_autosave = scene.use_autosave
236 layout.prop(scene, "use_autosave")
238 else:
239 layout.label(text="%s is not linked" % context.active_object.name,
240 icon=icon)
243 class TOPBAR_MT_edit_linked_submenu(bpy.types.Menu):
244 bl_label = 'Edit Linked Library'
245 bl_idname = 'view3d.TOPBAR_MT_edit_linked_submenu'
247 def draw(self, context):
248 self.layout.separator()
249 self.layout.operator(OBJECT_OT_EditLinked.bl_idname)
250 self.layout.operator(WM_OT_ReturnToOriginal.bl_idname)
253 addon_keymaps = []
254 classes = (
255 OBJECT_OT_EditLinked,
256 WM_OT_ReturnToOriginal,
257 VIEW3D_PT_PanelLinkedEdit,
258 TOPBAR_MT_edit_linked_submenu
262 def register():
263 bpy.app.handlers.load_post.append(linked_file_check)
265 for c in classes:
266 bpy.utils.register_class(c)
268 bpy.types.Scene.use_autosave = bpy.props.BoolProperty(
269 name="Autosave",
270 description="Save the current file before opening a linked file",
271 default=True)
273 bpy.types.Scene.use_instance = bpy.props.BoolProperty(
274 name="New Blender Instance",
275 description="Open in a new Blender instance",
276 default=False)
278 # add the function to the file menu
279 bpy.types.TOPBAR_MT_file_external_data.append(TOPBAR_MT_edit_linked_submenu.draw)
281 # Keymapping (deactivated by default; activated when a library object is selected)
282 kc = bpy.context.window_manager.keyconfigs.addon
283 if kc: # don't register keymaps from command line
284 km = kc.keymaps.new(name="3D View", space_type='VIEW_3D')
285 kmi = km.keymap_items.new("object.edit_linked", 'NUMPAD_SLASH', 'PRESS', shift=True)
286 kmi.active = True
287 addon_keymaps.append((km, kmi))
288 kmi = km.keymap_items.new("wm.return_to_original", 'NUMPAD_SLASH', 'PRESS', shift=True)
289 kmi.active = True
290 addon_keymaps.append((km, kmi))
293 def unregister():
295 bpy.app.handlers.load_post.remove(linked_file_check)
296 bpy.types.TOPBAR_MT_file_external_data.remove(TOPBAR_MT_edit_linked_submenu)
298 del bpy.types.Scene.use_autosave
299 del bpy.types.Scene.use_instance
301 # handle the keymap
302 for km, kmi in addon_keymaps:
303 km.keymap_items.remove(kmi)
304 addon_keymaps.clear()
306 for c in reversed(classes):
307 bpy.utils.unregister_class(c)
310 if __name__ == "__main__":
311 register()