Update for API change: scene.cursor_location -> scene.cursor.location
[blender-addons.git] / ui_layer_manager.py
blobc5768ce0472a6f9c6dbd7575166f4aeb5c40deb0
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 bl_info = {
22 "name": "Layer Management",
23 "author": "Alfonso Annarumma, Bastien Montagne",
24 "version": (1, 5, 4),
25 "blender": (2, 76, 0),
26 "location": "Toolshelf > Layers Tab",
27 "warning": "",
28 "description": "Display and Edit Layer Name",
29 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
30 "Scripts/3D_interaction/layer_manager",
31 "category": "3D View",
34 import bpy
35 from bpy.types import (
36 Operator,
37 Panel,
38 UIList,
39 PropertyGroup,
40 AddonPreferences,
42 from bpy.props import (
43 StringProperty,
44 BoolProperty,
45 IntProperty,
46 CollectionProperty,
47 BoolVectorProperty,
48 PointerProperty,
50 from bpy.app.handlers import persistent
52 EDIT_MODES = {'EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_METABALL', 'EDIT_TEXT', 'EDIT_ARMATURE'}
54 NUM_LAYERS = 20
56 FAKE_LAYER_GROUP = [True] * NUM_LAYERS
59 class NamedLayer(PropertyGroup):
60 name: StringProperty(
61 name="Layer Name"
63 use_lock: BoolProperty(
64 name="Lock Layer",
65 default=False
67 use_object_select: BoolProperty(
68 name="Object Select",
69 default=True
71 use_wire: BoolProperty(
72 name="Wire Layer",
73 default=False
77 class NamedLayers(PropertyGroup):
78 layers: CollectionProperty(type=NamedLayer)
80 use_hide_empty_layers: BoolProperty(
81 name="Hide Empty Layer",
82 default=False
84 use_extra_options: BoolProperty(
85 name="Show Extra Options",
86 default=True
88 use_layer_indices: BoolProperty(
89 name="Show Layer Indices",
90 default=False
92 use_classic: BoolProperty(
93 name="Classic",
94 default=False,
95 description="Use a classic layer selection visibility"
97 use_init: BoolProperty(
98 default=True,
99 options={'HIDDEN'}
103 # Stupid, but only solution currently is to use a handler to init that layers collection...
104 @persistent
105 def check_init_data(scene):
106 namedlayers = scene.namedlayers
107 if namedlayers.use_init:
108 while namedlayers.layers:
109 namedlayers.layers.remove(0)
110 for i in range(NUM_LAYERS):
111 layer = namedlayers.layers.add()
112 layer.name = "Layer%.2d" % (i + 1) # Blender use layer nums starting from 1, not 0.
113 namedlayers.use_init = False
116 class LayerGroup(PropertyGroup):
117 use_toggle: BoolProperty(name="", default=False)
118 use_wire: BoolProperty(name="", default=False)
119 use_lock: BoolProperty(name="", default=False)
121 layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS, subtype='LAYER')
124 class SCENE_OT_namedlayer_group_add(Operator):
125 """Add and select a new layer group"""
126 bl_idname = "scene.namedlayer_group_add"
127 bl_label = "Add Layer Group"
129 layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS)
131 @classmethod
132 def poll(cls, context):
133 return bool(context.scene)
135 def execute(self, context):
136 scene = context.scene
137 layergroups = scene.layergroups
138 layers = self.layers
140 group_idx = len(layergroups)
141 layer_group = layergroups.add()
142 layer_group.name = "LayerGroup.%.3d" % group_idx
143 layer_group.layers = layers
144 scene.layergroups_index = group_idx
146 return {'FINISHED'}
149 class SCENE_OT_namedlayer_group_remove(Operator):
150 """Remove selected layer group"""
151 bl_idname = "scene.namedlayer_group_remove"
152 bl_label = "Remove Layer Group"
154 group_idx: bpy.props.IntProperty()
156 @classmethod
157 def poll(cls, context):
158 return bool(context.scene)
160 def execute(self, context):
161 scene = context.scene
162 group_idx = self.group_idx
164 scene.layergroups.remove(group_idx)
165 if scene.layergroups_index > len(scene.layergroups) - 1:
166 scene.layergroups_index = len(scene.layergroups) - 1
168 return {'FINISHED'}
171 class SCENE_OT_namedlayer_toggle_visibility(Operator):
172 """Show or hide given layer (shift to extend)"""
173 bl_idname = "scene.namedlayer_toggle_visibility"
174 bl_label = "Show/Hide Layer"
176 layer_idx: IntProperty()
177 group_idx: IntProperty()
178 use_spacecheck: BoolProperty()
179 extend: BoolProperty(options={'SKIP_SAVE'})
181 @classmethod
182 def poll(cls, context):
183 return context.scene and (context.area.spaces.active.type == 'VIEW_3D')
185 def execute(self, context):
186 scene = context.scene
187 layer_cont = context.area.spaces.active if self.use_spacecheck else context.scene
188 layer_idx = self.layer_idx
190 if layer_idx == -1:
191 group_idx = self.group_idx
192 layergroups = scene.layergroups[group_idx]
193 group_layers = layergroups.layers
194 layers = layer_cont.layers
196 if layergroups.use_toggle:
197 layer_cont.layers = [not group_layer and layer for group_layer, layer in zip(group_layers, layers)]
198 layergroups.use_toggle = False
199 else:
200 layer_cont.layers = [group_layer or layer for group_layer, layer in zip(group_layers, layers)]
201 layergroups.use_toggle = True
202 else:
203 if self.extend:
204 layer_cont.layers[layer_idx] = not layer_cont.layers[layer_idx]
205 else:
206 layers = [False] * NUM_LAYERS
207 layers[layer_idx] = True
208 layer_cont.layers = layers
209 return {'FINISHED'}
211 def invoke(self, context, event):
212 self.extend = event.shift
213 return self.execute(context)
216 class SCENE_OT_namedlayer_move_to_layer(Operator):
217 """Move selected objects to this Layer (shift to extend)"""
218 bl_idname = "scene.namedlayer_move_to_layer"
219 bl_label = "Move Objects To Layer"
221 layer_idx: IntProperty()
222 extend: BoolProperty(options={'SKIP_SAVE'})
224 @classmethod
225 def poll(cls, context):
226 return context.scene
228 def execute(self, context):
229 layer_idx = self.layer_idx
230 scene = context.scene
232 # Cycle all objects in the layer
233 for obj in scene.objects:
234 if obj.select_get():
235 # If object is in at least one of the scene's visible layers...
236 if True in {ob_layer and sce_layer for ob_layer, sce_layer in zip(obj.layers, scene.layers)}:
237 if self.extend:
238 obj.layers[layer_idx] = not obj.layers[layer_idx]
239 else:
240 layer = [False] * NUM_LAYERS
241 layer[layer_idx] = True
242 obj.layers = layer
243 return {'FINISHED'}
245 def invoke(self, context, event):
246 self.extend = event.shift
247 return self.execute(context)
250 class SCENE_OT_namedlayer_toggle_wire(Operator):
251 """Toggle all objects on this layer draw as wire"""
252 bl_idname = "scene.namedlayer_toggle_wire"
253 bl_label = "Toggle Objects Draw Wire"
255 layer_idx: IntProperty()
256 use_wire: BoolProperty()
257 group_idx: IntProperty()
259 @classmethod
260 def poll(cls, context):
261 return context.scene and (context.area.spaces.active.type == 'VIEW_3D')
263 def execute(self, context):
264 scene = context.scene
265 layer_idx = self.layer_idx
266 use_wire = self.use_wire
268 view_3d = context.area.spaces.active
270 # Check if layer have some thing
271 if view_3d.layers_used[layer_idx] or layer_idx == -1:
272 display = 'WIRE' if use_wire else 'TEXTURED'
273 # Cycle all objects in the layer.
274 for obj in context.scene.objects:
275 if layer_idx == -1:
276 group_idx = self.group_idx
277 group_layers = scene.layergroups[group_idx].layers
278 layers = obj.layers
279 if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
280 obj.display_type = display
281 scene.layergroups[group_idx].use_wire = use_wire
282 else:
283 if obj.layers[layer_idx]:
284 obj.display_type = display
285 scene.namedlayers.layers[layer_idx].use_wire = use_wire
287 return {'FINISHED'}
290 class SCENE_OT_namedlayer_lock_all(Operator):
291 """Lock all objects on this layer"""
292 bl_idname = "scene.namedlayer_lock_all"
293 bl_label = "Lock Objects"
295 layer_idx: IntProperty()
296 use_lock: BoolProperty()
297 group_idx: IntProperty()
299 @classmethod
300 def poll(cls, context):
301 return context.scene and (context.area.spaces.active.type == 'VIEW_3D')
303 def execute(self, context):
304 scene = context.scene
305 view_3d = context.area.spaces.active
306 layer_idx = self.layer_idx
307 group_idx = self.group_idx
308 group_layers = FAKE_LAYER_GROUP if group_idx < 0 else scene.layergroups[group_idx].layers
309 use_lock = self.use_lock
311 # check if layer have some thing
312 if layer_idx == -1 or view_3d.layers_used[layer_idx]:
313 # Cycle all objects in the layer.
314 for obj in context.scene.objects:
315 if layer_idx == -1:
316 layers = obj.layers
317 if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
318 obj.hide_select = not use_lock
319 obj.select_set(False)
320 scene.layergroups[group_idx].use_lock = not use_lock
321 else:
322 if obj.layers[layer_idx]:
323 obj.hide_select = not use_lock
324 obj.select_set(False)
325 scene.namedlayers.layers[layer_idx].use_lock = not use_lock
327 return {'FINISHED'}
330 class SCENE_OT_namedlayer_select_objects_by_layer(Operator):
331 """Select all the objects on this Layer (shift for multi selection, ctrl to make active the last selected object)"""
332 bl_idname = "scene.namedlayer_select_objects_by_layer"
333 bl_label = "Select Objects In Layer"
335 select_obj: BoolProperty()
336 layer_idx: IntProperty()
338 extend: BoolProperty(options={'SKIP_SAVE'})
339 active: BoolProperty(options={'SKIP_SAVE'})
341 @classmethod
342 def poll(cls, context):
343 return context.scene and (context.area.spaces.active.type == 'VIEW_3D')
345 def execute(self, context):
346 scene = context.scene
347 view_3d = context.area.spaces.active
348 select_obj = self.select_obj
349 layer_idx = self.layer_idx
351 not_all_selected = 0
352 # check if layer have some thing
353 if view_3d.layers_used[layer_idx]:
354 objects = []
355 for obj in context.scene.objects:
356 if obj.layers[layer_idx]:
357 objects.append(obj)
358 not_all_selected -= 1
359 if self.active:
360 context.view_layer.objects.active = obj
361 if obj.select_get():
362 not_all_selected += 1
363 if not not_all_selected:
364 for obj in objects:
365 obj.select_set(False)
366 else:
367 bpy.ops.object.select_by_layer(match='SHARED', extend=self.extend, layers=layer_idx + 1)
369 return {'FINISHED'}
371 def invoke(self, context, event):
372 self.extend = event.shift
373 self.active = event.ctrl
374 return self.execute(context)
377 class SCENE_OT_namedlayer_show_all(Operator):
378 """Show or hide all layers in the scene"""
379 bl_idname = "scene.namedlayer_show_all"
380 bl_label = "Select All Layers"
382 show: BoolProperty()
384 @classmethod
385 def poll(cls, context):
386 return context.scene and (context.area.spaces.active.type == 'VIEW_3D')
388 def execute(self, context):
389 scene = context.scene
390 view_3d = context.area.spaces.active
391 show = self.show
392 active_layer = scene.active_layer
394 # check for lock camera and layer is active
395 layer_cont = scene if view_3d.lock_camera_and_layers else view_3d
397 if show:
398 layer_cont.layers[:] = [True] * NUM_LAYERS
399 # Restore active layer (stupid, but Scene.active_layer is readonly).
400 layer_cont.layers[active_layer] = False
401 layer_cont.layers[active_layer] = True
402 else:
403 layers = [False] * NUM_LAYERS
404 # Keep selection of active layer
405 layers[active_layer] = True
406 layer_cont.layers[:] = layers
408 return {'FINISHED'}
411 class SCENE_PT_namedlayer_layers(Panel):
412 bl_space_type = 'VIEW_3D'
413 bl_region_type = 'TOOLS'
414 bl_label = "Layer Management"
415 bl_category = "Layers"
416 bl_context = "objectmode"
418 @classmethod
419 def poll(self, context):
420 return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
421 (context.area.spaces.active.type == 'VIEW_3D'))
423 def draw(self, context):
424 scene = context.scene
425 view_3d = context.area.spaces.active
426 actob = context.object
427 namedlayers = scene.namedlayers
428 use_extra = namedlayers.use_extra_options
429 use_hide = namedlayers.use_hide_empty_layers
430 use_indices = namedlayers.use_layer_indices
431 use_classic = namedlayers.use_classic
433 # Check for lock camera and layer is active
434 if view_3d.lock_camera_and_layers:
435 layer_cont = scene
436 use_spacecheck = False
437 else:
438 layer_cont = view_3d
439 use_spacecheck = True
441 layout = self.layout
442 row = layout.row()
443 col = row.column()
444 col.prop(view_3d, "lock_camera_and_layers", text="")
445 # Check if there is a layer off
446 show = (False in {layer for layer in layer_cont.layers})
447 icon = 'RESTRICT_VIEW_ON' if show else 'RESTRICT_VIEW_OFF'
448 col.operator("scene.namedlayer_show_all", emboss=False, icon=icon, text="").show = show
450 col = row.column()
451 col.prop(namedlayers, "use_classic")
452 col.prop(namedlayers, "use_extra_options", text="Options")
454 col = row.column()
455 col.prop(namedlayers, "use_layer_indices", text="Indices")
456 col.prop(namedlayers, "use_hide_empty_layers", text="Hide Empty")
458 col = layout.column()
459 for layer_idx in range(NUM_LAYERS):
460 namedlayer = namedlayers.layers[layer_idx]
461 is_layer_used = view_3d.layers_used[layer_idx]
463 if (use_hide and not is_layer_used):
464 # Hide unused layers and this one is unused, skip.
465 continue
467 row = col.row(align=True)
469 # layer index
470 if use_indices:
471 sub = row.row(align=True)
472 sub.alignment = 'LEFT'
473 sub.label(text="%.2d." % (layer_idx + 1))
475 # visualization
476 icon = 'RESTRICT_VIEW_OFF' if layer_cont.layers[layer_idx] else 'RESTRICT_VIEW_ON'
477 if use_classic:
478 op = row.operator("scene.namedlayer_toggle_visibility", text="", icon=icon, emboss=True)
479 op.layer_idx = layer_idx
480 op.use_spacecheck = use_spacecheck
481 else:
482 row.prop(layer_cont, "layers", index=layer_idx, emboss=True, icon=icon, toggle=True, text="")
484 # Name (use special icon for active layer)
485 icon = 'FILE_TICK' if (getattr(layer_cont, "active_layer", -1) == layer_idx) else 'NONE'
486 row.prop(namedlayer, "name", text="", icon=icon)
488 if use_extra:
489 use_lock = namedlayer.use_lock
491 # Select by type operator
492 sub = row.column(align=True)
493 sub.enabled = not use_lock
494 sub.operator("scene.namedlayer_select_objects_by_layer", icon='RESTRICT_SELECT_OFF',
495 text="", emboss=True).layer_idx = layer_idx
497 # Lock operator
498 icon = 'LOCKED' if use_lock else 'UNLOCKED'
499 op = row.operator("scene.namedlayer_lock_all", text="", emboss=True, icon=icon)
500 op.layer_idx = layer_idx
501 op.group_idx = -1
502 op.use_lock = use_lock
504 # Merge layer
505 # check if layer has something
506 has_active = (actob and actob.layers[layer_idx])
507 icon = ('LAYER_ACTIVE' if has_active else 'LAYER_USED') if is_layer_used else 'RADIOBUT_OFF'
508 row.operator("scene.namedlayer_move_to_layer", text="", emboss=True, icon=icon).layer_idx = layer_idx
510 # Wire view
511 use_wire = namedlayer.use_wire
512 icon = 'WIRE' if use_wire else 'POTATO'
513 op = row.operator("scene.namedlayer_toggle_wire", text="", emboss=True, icon=icon)
514 op.layer_idx = layer_idx
515 op.use_wire = not use_wire
517 if not (layer_idx + 1) % 5:
518 col.separator()
520 if len(scene.objects) == 0:
521 layout.label(text="No objects in scene")
524 class SCENE_UL_namedlayer_groups(UIList):
525 def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
526 layer_group = item
528 # check for lock camera and layer is active
529 view_3d = context.area.spaces.active # Ensured it is a 'VIEW_3D' in panel's poll(), weak... :/
530 use_spacecheck = False if view_3d.lock_camera_and_layers else True
532 if self.layout_type in {'DEFAULT', 'COMPACT'}:
533 layout.prop(layer_group, "name", text="", emboss=False)
534 # lock operator
535 use_lock = layer_group.use_lock
536 icon = 'LOCKED' if use_lock else 'UNLOCKED'
537 op = layout.operator("scene.namedlayer_lock_all", text="", emboss=False, icon=icon)
538 op.use_lock = use_lock
539 op.group_idx = index
540 op.layer_idx = -1
542 # view operator
543 icon = 'RESTRICT_VIEW_OFF' if layer_group.use_toggle else 'RESTRICT_VIEW_ON'
544 op = layout.operator("scene.namedlayer_toggle_visibility", text="", emboss=False, icon=icon)
545 op.use_spacecheck = use_spacecheck
546 op.group_idx = index
547 op.layer_idx = -1
549 # wire operator
550 use_wire = layer_group.use_wire
551 icon = 'WIRE' if use_wire else 'POTATO'
552 op = layout.operator("scene.namedlayer_toggle_wire", text="", emboss=False, icon=icon)
553 op.use_wire = not use_wire
554 op.group_idx = index
555 op.layer_idx = -1
557 elif self.layout_type in {'GRID'}:
558 layout.alignment = 'CENTER'
561 class SCENE_PT_namedlayer_groups(Panel):
562 bl_space_type = 'VIEW_3D'
563 bl_region_type = 'TOOLS'
564 bl_context = "objectmode"
565 bl_category = "Layers"
566 bl_label = "Layer Groups"
567 bl_options = {'DEFAULT_CLOSED'}
569 @classmethod
570 def poll(self, context):
571 return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
572 (context.area.spaces.active.type == 'VIEW_3D'))
574 def draw(self, context):
575 scene = context.scene
576 group_idx = scene.layergroups_index
578 layout = self.layout
579 row = layout.row()
580 row.template_list("SCENE_UL_namedlayer_groups", "", scene, "layergroups", scene, "layergroups_index")
582 col = row.column(align=True)
583 col.operator("scene.namedlayer_group_add", icon='ADD', text="").layers = scene.layers
584 col.operator("scene.namedlayer_group_remove", icon='REMOVE', text="").group_idx = group_idx
586 if bool(scene.layergroups):
587 layout.prop(scene.layergroups[group_idx], "layers", text="", toggle=True)
588 layout.prop(scene.layergroups[group_idx], "name", text="Name:")
591 # Add-ons Preferences Update Panel
593 # Define Panel classes for updating
594 panels = (
595 SCENE_PT_namedlayer_layers,
596 SCENE_PT_namedlayer_groups,
600 def update_panel(self, context):
601 message = "Layer Management: Updating Panel locations has failed"
602 try:
603 for panel in panels:
604 if "bl_rna" in panel.__dict__:
605 bpy.utils.unregister_class(panel)
607 for panel in panels:
608 panel.bl_category = context.preferences.addons[__name__].preferences.category
609 bpy.utils.register_class(panel)
611 except Exception as e:
612 print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
613 pass
616 class LayerMAddonPreferences(AddonPreferences):
617 # this must match the addon name, use '__package__'
618 # when defining this in a submodule of a python package.
619 bl_idname = __name__
621 category: StringProperty(
622 name="Tab Category",
623 description="Choose a name for the category of the panel",
624 default="Layers",
625 update=update_panel
628 def draw(self, context):
629 layout = self.layout
631 row = layout.row()
632 col = row.column()
633 col.label(text="Tab Category:")
634 col.prop(self, "category", text="")
637 def register():
638 bpy.utils.register_module(__name__)
639 bpy.types.Scene.layergroups = CollectionProperty(type=LayerGroup)
640 # Unused, but this is needed for the TemplateList to work...
641 bpy.types.Scene.layergroups_index = IntProperty(default=-1)
642 bpy.types.Scene.namedlayers = PointerProperty(type=NamedLayers)
643 bpy.app.handlers.scene_update_post.append(check_init_data)
644 update_panel(None, bpy.context)
647 def unregister():
648 bpy.app.handlers.scene_update_post.remove(check_init_data)
649 del bpy.types.Scene.layergroups
650 del bpy.types.Scene.layergroups_index
651 del bpy.types.Scene.namedlayers
652 bpy.utils.unregister_module(__name__)
655 if __name__ == "__main__":
656 register()