1# ##### BEGIN GPL LICENSE BLOCK #####
2#
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.
7#
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.
12#
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.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19bl_info = {
20    "name": "Stored Views",
21    "description": "Save and restore User defined views, pov, layers and display configs",
22    "author": "nfloyd, Francesco Siddi",
23    "version": (0, 3, 7),
24    "blender": (2, 80, 0),
25    "location": "View3D > Properties > Stored Views",
26    "warning": "",
27    "doc_url": "https://wiki.blender.org/index.php/Extensions:2.5/"
28               "Py/Scripts/3D_interaction/stored_views",
29    "category": "3D View"
30}
31
32"""
33ACKNOWLEDGMENT
34==============
35import/export functionality is mostly based
36on Bart Crouch's Theme Manager Addon
37
38TODO: quadview complete support : investigate. Where's the data?
39TODO: lock_camera_and_layers. investigate usage
40TODO: list reordering
41
42NOTE: logging setup has to be provided by the user in a separate config file
43    as Blender will not try to configure logging by default in an add-on
44    The Config File should be in the Blender Config folder > /scripts/startup/config_logging.py
45    For setting up /location of the config folder see:
46    https://docs.blender.org/manual/en/latest/getting_started/
47    installing/configuration/directories.html
48    For configuring logging itself in the file, general Python documentation should work
49    As the logging calls are not configured, they can be kept in the other modules of this add-on
50    and will not have output until the logging configuration is set up
51"""
52
53
54import bpy
55from bpy.props import (
56    BoolProperty,
57    IntProperty,
58    PointerProperty,
59)
60from bpy.types import (
61    AddonPreferences,
62    Operator,
63    Panel
64)
65
66import logging
67module_logger = logging.getLogger(__name__)
68
69import gzip
70import os
71import pickle
72import shutil
73
74from bpy_extras.io_utils import (
75    ExportHelper,
76    ImportHelper,
77)
78
79import blf
80
81import hashlib
82import bpy
83
84
85# Utility function get preferences setting for exporters
86def get_preferences():
87    # replace the key if the add-on name changes
88    addon = bpy.context.preferences.addons[__package__]
89    show_warn = (addon.preferences.show_exporters if addon else False)
90
91    return show_warn
92
93
94class StoredView():
95    def __init__(self, mode, index=None):
96        self.logger = logging.getLogger('%s.StoredView' % __name__)
97        self.scene = bpy.context.scene
98        self.view3d = bpy.context.space_data
99        self.index = index
100        self.data_store = DataStore(mode=mode)
101
102    def save(self):
103        if self.index == -1:
104            stored_view, self.index = self.data_store.create()
105        else:
106            stored_view = self.data_store.get(self.index)
107        self.from_v3d(stored_view)
108        self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name))
109
110    def set(self):
111        stored_view = self.data_store.get(self.index)
112        self.update_v3d(stored_view)
113        self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name))
114
115    def from_v3d(self, stored_view):
116        raise NotImplementedError("Subclass must implement abstract method")
117
118    def update_v3d(self, stored_view):
119        raise NotImplementedError("Subclass must implement abstract method")
120
121    @staticmethod
122    def is_modified(context, stored_view):
123        raise NotImplementedError("Subclass must implement abstract method")
124
125
126class POV(StoredView):
127    def __init__(self, index=None):
128        super().__init__(mode='POV', index=index)
129        self.logger = logging.getLogger('%s.POV' % __name__)
130
131    def from_v3d(self, stored_view):
132        view3d = self.view3d
133        region3d = view3d.region_3d
134
135        stored_view.distance = region3d.view_distance
136        stored_view.location = region3d.view_location
137        stored_view.rotation = region3d.view_rotation
138        stored_view.perspective_matrix_md5 = POV._get_perspective_matrix_md5(region3d)
139        stored_view.perspective = region3d.view_perspective
140        stored_view.lens = view3d.lens
141        stored_view.clip_start = view3d.clip_start
142        stored_view.clip_end = view3d.clip_end
143
144        if region3d.view_perspective == 'CAMERA':
145            stored_view.camera_type = view3d.camera.type  # type : 'CAMERA' or 'MESH'
146            stored_view.camera_name = view3d.camera.name  # store string instead of object
147        if view3d.lock_object is not None:
148            stored_view.lock_object_name = view3d.lock_object.name  # idem
149        else:
150            stored_view.lock_object_name = ""
151        stored_view.lock_cursor = view3d.lock_cursor
152        stored_view.cursor_location = view3d.cursor_location
153
154    def update_v3d(self, stored_view):
155        view3d = self.view3d
156        region3d = view3d.region_3d
157        region3d.view_distance = stored_view.distance
158        region3d.view_location = stored_view.location
159        region3d.view_rotation = stored_view.rotation
160        region3d.view_perspective = stored_view.perspective
161        view3d.lens = stored_view.lens
162        view3d.clip_start = stored_view.clip_start
163        view3d.clip_end = stored_view.clip_end
164        view3d.lock_cursor = stored_view.lock_cursor
165        if stored_view.lock_cursor is True:
166            # update cursor only if view is locked to cursor
167            view3d.cursor_location = stored_view.cursor_location
168
169        if stored_view.perspective == "CAMERA":
170
171            lock_obj = self._get_object(stored_view.lock_object_name)
172            if lock_obj:
173                view3d.lock_object = lock_obj
174            else:
175                cam = self._get_object(stored_view.camera_name)
176                if cam:
177                    view3d.camera = cam
178
179    @staticmethod
180    def _get_object(name, pointer=None):
181        return bpy.data.objects.get(name)
182
183    @staticmethod
184    def is_modified(context, stored_view):
185        # TODO: check for others param, currently only perspective
186        # and perspective_matrix are checked
187        POV.logger = logging.getLogger('%s.POV' % __name__)
188        view3d = context.space_data
189        region3d = view3d.region_3d
190        if region3d.view_perspective != stored_view.perspective:
191            POV.logger.debug('view_perspective')
192            return True
193
194        md5 = POV._get_perspective_matrix_md5(region3d)
195        if (md5 != stored_view.perspective_matrix_md5 and
196          region3d.view_perspective != "CAMERA"):
197            POV.logger.debug('perspective_matrix')
198            return True
199
200        return False
201
202    @staticmethod
203    def _get_perspective_matrix_md5(region3d):
204        md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest()
205        return md5
206
207
208class Layers(StoredView):
209    def __init__(self, index=None):
210        super().__init__(mode='LAYERS', index=index)
211        self.logger = logging.getLogger('%s.Layers' % __name__)
212
213    def from_v3d(self, stored_view):
214        view3d = self.view3d
215        stored_view.view_layers = view3d.layers
216        stored_view.scene_layers = self.scene.layers
217        stored_view.lock_camera_and_layers = view3d.lock_camera_and_layers
218
219    def update_v3d(self, stored_view):
220        view3d = self.view3d
221        view3d.lock_camera_and_layers = stored_view.lock_camera_and_layers
222        if stored_view.lock_camera_and_layers is True:
223            self.scene.layers = stored_view.scene_layers
224        else:
225            view3d.layers = stored_view.view_layers
226
227    @staticmethod
228    def is_modified(context, stored_view):
229        Layers.logger = logging.getLogger('%s.Layers' % __name__)
230        if stored_view.lock_camera_and_layers != context.space_data.lock_camera_and_layers:
231            Layers.logger.debug('lock_camera_and_layers')
232            return True
233        if stored_view.lock_camera_and_layers is True:
234            for i in range(20):
235                if stored_view.scene_layers[i] != context.scene.layers[i]:
236                    Layers.logger.debug('scene_layers[%s]' % (i, ))
237                    return True
238        else:
239            for i in range(20):
240                if stored_view.view_layers[i] != context.space_data.view3d.layers[i]:
241                    return True
242        return False
243
244
245class Display(StoredView):
246    def __init__(self, index=None):
247        super().__init__(mode='DISPLAY', index=index)
248        self.logger = logging.getLogger('%s.Display' % __name__)
249
250    def from_v3d(self, stored_view):
251        view3d = self.view3d
252        stored_view.viewport_shade = view3d.viewport_shade
253        stored_view.show_only_render = view3d.show_only_render
254        stored_view.show_outline_selected = view3d.show_outline_selected
255        stored_view.show_all_objects_origin = view3d.show_all_objects_origin
256        stored_view.show_relationship_lines = view3d.show_relationship_lines
257        stored_view.show_floor = view3d.show_floor
258        stored_view.show_axis_x = view3d.show_axis_x
259        stored_view.show_axis_y = view3d.show_axis_y
260        stored_view.show_axis_z = view3d.show_axis_z
261        stored_view.grid_lines = view3d.grid_lines
262        stored_view.grid_scale = view3d.grid_scale
263        stored_view.grid_subdivisions = view3d.grid_subdivisions
264        stored_view.material_mode = self.scene.game_settings.material_mode
265        stored_view.show_textured_solid = view3d.show_textured_solid
266
267    def update_v3d(self, stored_view):
268        view3d = self.view3d
269        view3d.viewport_shade = stored_view.viewport_shade
270        view3d.show_only_render = stored_view.show_only_render
271        view3d.show_outline_selected = stored_view.show_outline_selected
272        view3d.show_all_objects_origin = stored_view.show_all_objects_origin
273        view3d.show_relationship_lines = stored_view.show_relationship_lines
274        view3d.show_floor = stored_view.show_floor
275        view3d.show_axis_x = stored_view.show_axis_x
276        view3d.show_axis_y = stored_view.show_axis_y
277        view3d.show_axis_z = stored_view.show_axis_z
278        view3d.grid_lines = stored_view.grid_lines
279        view3d.grid_scale = stored_view.grid_scale
280        view3d.grid_subdivisions = stored_view.grid_subdivisions
281        self.scene.game_settings.material_mode = stored_view.material_mode
282        view3d.show_textured_solid = stored_view.show_textured_solid
283
284    @staticmethod
285    def is_modified(context, stored_view):
286        Display.logger = logging.getLogger('%s.Display' % __name__)
287        view3d = context.space_data
288        excludes = ["material_mode", "quad_view", "lock_rotation", "show_sync_view", "use_box_clip", "name"]
289        for k, v in stored_view.items():
290            if k not in excludes:
291                if getattr(view3d, k) != getattr(stored_view, k):
292                    return True
293
294        if stored_view.material_mode != context.scene.game_settings.material_mode:
295            Display.logger.debug('material_mode')
296            return True
297
298
299class View(StoredView):
300    def __init__(self, index=None):
301        super().__init__(mode='VIEW', index=index)
302        self.logger = logging.getLogger('%s.View' % __name__)
303        self.pov = POV()
304        self.layers = Layers()
305        self.display = Display()
306
307    def from_v3d(self, stored_view):
308        self.pov.from_v3d(stored_view.pov)
309        self.layers.from_v3d(stored_view.layers)
310        self.display.from_v3d(stored_view.display)
311
312    def update_v3d(self, stored_view):
313        self.pov.update_v3d(stored_view.pov)
314        self.layers.update_v3d(stored_view.layers)
315        self.display.update_v3d(stored_view.display)
316
317    @staticmethod
318    def is_modified(context, stored_view):
319        if POV.is_modified(context, stored_view.pov) or \
320           Layers.is_modified(context, stored_view.layers) or \
321           Display.is_modified(context, stored_view.display):
322            return True
323        return False
324
325
326class DataStore():
327    def __init__(self, scene=None, mode=None):
328        if scene is None:
329            scene = bpy.context.scene
330        stored_views = scene.stored_views
331        self.mode = mode
332
333        if mode is None:
334            self.mode = stored_views.mode
335
336        if self.mode == 'VIEW':
337            self.list = stored_views.view_list
338            self.current_index = stored_views.current_indices[0]
339        elif self.mode == 'POV':
340            self.list = stored_views.pov_list
341            self.current_index = stored_views.current_indices[1]
342        elif self.mode == 'LAYERS':
343            self.list = stored_views.layers_list
344            self.current_index = stored_views.current_indices[2]
345        elif self.mode == 'DISPLAY':
346            self.list = stored_views.display_list
347            self.current_index = stored_views.current_indices[3]
348
349    def create(self):
350        item = self.list.add()
351        item.name = self._generate_name()
352        index = len(self.list) - 1
353        self._set_current_index(index)
354        return item, index
355
356    def get(self, index):
357        self._set_current_index(index)
358        return self.list[index]
359
360    def delete(self, index):
361        if self.current_index > index:
362            self._set_current_index(self.current_index - 1)
363        elif self.current_index == index:
364            self._set_current_index(-1)
365
366        self.list.remove(index)
367
368    def _set_current_index(self, index):
369        self.current_index = index
370        mode = self.mode
371        stored_views = bpy.context.scene.stored_views
372        if mode == 'VIEW':
373            stored_views.current_indices[0] = index
374        elif mode == 'POV':
375            stored_views.current_indices[1] = index
376        elif mode == 'LAYERS':
377            stored_views.current_indices[2] = index
378        elif mode == 'DISPLAY':
379            stored_views.current_indices[3] = index
380
381    def _generate_name(self):
382        default_name = str(self.mode)
383        names = []
384        for i in self.list:
385            i_name = i.name
386            if i_name.startswith(default_name):
387                names.append(i_name)
388        names.sort()
389        try:
390            l_name = names[-1]
391            post_fix = l_name.rpartition('.')[2]
392            if post_fix.isnumeric():
393                post_fix = str(int(post_fix) + 1).zfill(3)
394            else:
395                if post_fix == default_name:
396                    post_fix = "001"
397            return default_name + "." + post_fix
398        except:
399            return default_name
400
401    @staticmethod
402    def sanitize_data(scene):
403
404        def check_objects_references(mode, list):
405            to_remove = []
406            for i, list_item in enumerate(list.items()):
407                key, item = list_item
408                if mode == 'POV' or mode == 'VIEWS':
409                    if mode == 'VIEWS':
410                        item = item.pov
411
412                    if item.perspective == "CAMERA":
413
414                        camera = bpy.data.objects.get(item.camera_name)
415                        if camera is None:
416                            try:  # pick a default camera TODO: ask to pick?
417                                camera = bpy.data.cameras[0]
418                                item.camera_name = camera.name
419                            except:  # couldn't find a camera in the scene
420                                pass
421
422                        obj = bpy.data.objects.get(item.lock_object_name)
423                        if obj is None and camera is None:
424                            to_remove.append(i)
425
426            for i in reversed(to_remove):
427                list.remove(i)
428
429        modes = ['POV', 'VIEW', 'DISPLAY', 'LAYERS']
430        for mode in modes:
431            data = DataStore(scene=scene, mode=mode)
432            check_objects_references(mode, data.list)
433
434
435def stored_view_factory(mode, *args, **kwargs):
436    if mode == 'POV':
437        return POV(*args, **kwargs)
438    elif mode == 'LAYERS':
439        return Layers(*args, **kwargs)
440    elif mode == 'DISPLAY':
441        return Display(*args, **kwargs)
442    elif mode == 'VIEW':
443        return View(*args, **kwargs)
444
445"""
446  If view name display is enabled,
447  it will check periodically if the view has been modified
448  since last set.
449  get_preferences_timer() is the time in seconds between these checks.
450  It can be increased, if the view become sluggish
451  It is set in the add-on preferences
452"""
453
454
455# Utility function get_preferences_timer for update of 3d view draw
456def get_preferences_timer():
457    # replace the key if the add-on name changes
458    # TODO: expose refresh rate to ui???
459    addon = bpy.context.preferences.addons[__package__]
460    timer_update = (addon.preferences.view_3d_update_rate if addon else False)
461
462    return timer_update
463
464
465def init_draw(context=None):
466    if context is None:
467        context = bpy.context
468
469    if "stored_views_osd" not in context.window_manager:
470        context.window_manager["stored_views_osd"] = False
471
472    if not context.window_manager["stored_views_osd"]:
473        context.window_manager["stored_views_osd"] = True
474        bpy.ops.stored_views.draw()
475
476
477def _draw_callback_px(self, context):
478    if context.area and context.area.type == 'VIEW_3D':
479        r_width = text_location = context.region.width
480        r_height = context.region.height
481        font_id = 0  # TODO: need to find out how best to get font_id
482
483        blf.size(font_id, 11, context.preferences.system.dpi)
484        text_size = blf.dimensions(0, self.view_name)
485
486        # compute the text location
487        text_location = 0
488        overlap = context.preferences.system.use_region_overlap
489        if overlap:
490            for region in context.area.regions:
491                if region.type == "UI":
492                    text_location = r_width - region.width
493
494        text_x = text_location - text_size[0] - 10
495        text_y = r_height - text_size[1] - 8
496        blf.position(font_id, text_x, text_y, 0)
497        blf.draw(font_id, self.view_name)
498
499
500class VIEW3D_OT_stored_views_draw(Operator):
501    bl_idname = "stored_views.draw"
502    bl_label = "Show current"
503    bl_description = "Toggle the display current view name in the view 3D"
504
505    _handle = None
506    _timer = None
507
508    @staticmethod
509    def handle_add(self, context):
510        VIEW3D_OT_stored_views_draw._handle = bpy.types.SpaceView3D.draw_handler_add(
511            _draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
512        VIEW3D_OT_stored_views_draw._timer = \
513            context.window_manager.event_timer_add(get_preferences_timer(), context.window)
514
515    @staticmethod
516    def handle_remove(context):
517        if VIEW3D_OT_stored_views_draw._handle is not None:
518            bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_OT_stored_views_draw._handle, 'WINDOW')
519        if VIEW3D_OT_stored_views_draw._timer is not None:
520            context.window_manager.event_timer_remove(VIEW3D_OT_stored_views_draw._timer)
521        VIEW3D_OT_stored_views_draw._handle = None
522        VIEW3D_OT_stored_views_draw._timer = None
523
524    @classmethod
525    def poll(cls, context):
526        # return context.mode == 'OBJECT'
527        return True
528
529    def modal(self, context, event):
530        if context.area:
531            context.area.tag_redraw()
532
533        if not context.area or context.area.type != "VIEW_3D":
534            return {"PASS_THROUGH"}
535
536        data = DataStore()
537        stored_views = context.scene.stored_views
538
539        if len(data.list) > 0 and \
540           data.current_index >= 0 and \
541           not stored_views.view_modified:
542
543            if not stored_views.view_modified:
544                sv = data.list[data.current_index]
545                self.view_name = sv.name
546                if event.type == 'TIMER':
547                    is_modified = False
548                    if data.mode == 'VIEW':
549                        is_modified = View.is_modified(context, sv)
550                    elif data.mode == 'POV':
551                        is_modified = POV.is_modified(context, sv)
552                    elif data.mode == 'LAYERS':
553                        is_modified = Layers.is_modified(context, sv)
554                    elif data.mode == 'DISPLAY':
555                        is_modified = Display.is_modified(context, sv)
556                    if is_modified:
557                        module_logger.debug(
558                                'view modified - index: %s name: %s' % (data.current_index, sv.name)
559                                )
560                        self.view_name = ""
561                        stored_views.view_modified = is_modified
562
563                return {"PASS_THROUGH"}
564        else:
565            module_logger.debug('exit')
566            context.window_manager["stored_views_osd"] = False
567            VIEW3D_OT_stored_views_draw.handle_remove(context)
568
569            return {'FINISHED'}
570
571    def execute(self, context):
572        if context.area.type == "VIEW_3D":
573            self.view_name = ""
574            VIEW3D_OT_stored_views_draw.handle_add(self, context)
575            context.window_manager.modal_handler_add(self)
576
577            return {"RUNNING_MODAL"}
578        else:
579            self.report({"WARNING"}, "View3D not found. Operation Cancelled")
580
581            return {"CANCELLED"}
582
583class VIEW3D_OT_stored_views_initialize(Operator):
584    bl_idname = "view3d.stored_views_initialize"
585    bl_label = "Initialize"
586
587    @classmethod
588    def poll(cls, context):
589        return not hasattr(bpy.types.Scene, 'stored_views')
590
591    def execute(self, context):
592        bpy.types.Scene.stored_views: PointerProperty(
593                                            type=properties.StoredViewsData
594                                            )
595        scenes = bpy.data.scenes
596        data = DataStore()
597        for scene in scenes:
598            DataStore.sanitize_data(scene)
599        return {'FINISHED'}
600
601
602from bpy.types import PropertyGroup
603from bpy.props import (
604        BoolProperty,
605        BoolVectorProperty,
606        CollectionProperty,
607        FloatProperty,
608        FloatVectorProperty,
609        EnumProperty,
610        IntProperty,
611        IntVectorProperty,
612        PointerProperty,
613        StringProperty,
614        )
615
616
617class POVData(PropertyGroup):
618    distance: FloatProperty()
619    location: FloatVectorProperty(
620            subtype='TRANSLATION'
621            )
622    rotation: FloatVectorProperty(
623            subtype='QUATERNION',
624            size=4
625            )
626    name: StringProperty()
627    perspective: EnumProperty(
628            items=[('PERSP', '', ''),
629                   ('ORTHO', '', ''),
630                   ('CAMERA', '', '')]
631            )
632    lens: FloatProperty()
633    clip_start: FloatProperty()
634    clip_end: FloatProperty()
635    lock_cursor: BoolProperty()
636    cursor_location: FloatVectorProperty()
637    perspective_matrix_md5: StringProperty()
638    camera_name: StringProperty()
639    camera_type: StringProperty()
640    lock_object_name: StringProperty()
641
642
643class LayersData(PropertyGroup):
644    view_layers: BoolVectorProperty(size=20)
645    scene_layers: BoolVectorProperty(size=20)
646    lock_camera_and_layers: BoolProperty()
647    name: StringProperty()
648
649
650class DisplayData(PropertyGroup):
651    name: StringProperty()
652    viewport_shade: EnumProperty(
653            items=[('BOUNDBOX', 'BOUNDBOX', 'BOUNDBOX'),
654                   ('WIREFRAME', 'WIREFRAME', 'WIREFRAME'),
655                   ('SOLID', 'SOLID', 'SOLID'),
656                   ('TEXTURED', 'TEXTURED', 'TEXTURED'),
657                   ('MATERIAL', 'MATERIAL', 'MATERIAL'),
658                   ('RENDERED', 'RENDERED', 'RENDERED')]
659            )
660    show_only_render: BoolProperty()
661    show_outline_selected: BoolProperty()
662    show_all_objects_origin: BoolProperty()
663    show_relationship_lines: BoolProperty()
664    show_floor: BoolProperty()
665    show_axis_x: BoolProperty()
666    show_axis_y: BoolProperty()
667    show_axis_z: BoolProperty()
668    grid_lines: IntProperty()
669    grid_scale: FloatProperty()
670    grid_subdivisions: IntProperty()
671    material_mode: StringProperty()
672    show_textured_solid: BoolProperty()
673    quad_view: BoolProperty()
674    lock_rotation: BoolProperty()
675    show_sync_view: BoolProperty()
676    use_box_clip: BoolProperty()
677
678
679class ViewData(PropertyGroup):
680    pov: PointerProperty(
681            type=POVData
682            )
683    layers: PointerProperty(
684            type=LayersData
685            )
686    display: PointerProperty(
687            type=DisplayData
688            )
689    name: StringProperty()
690
691
692class StoredViewsData(PropertyGroup):
693    pov_list: CollectionProperty(
694            type=POVData
695            )
696    layers_list: CollectionProperty(
697            type=LayersData
698            )
699    display_list: CollectionProperty(
700            type=DisplayData
701            )
702    view_list: CollectionProperty(
703            type=ViewData
704            )
705    mode: EnumProperty(
706            name="Mode",
707            items=[('VIEW', "View", "3D View settings"),
708                   ('POV', "POV", "POV settings"),
709                   ('LAYERS', "Layers", "Layers settings"),
710                   ('DISPLAY', "Display", "Display settings")],
711            default='VIEW'
712            )
713    current_indices: IntVectorProperty(
714            size=4,
715            default=[-1, -1, -1, -1]
716            )
717    view_modified: BoolProperty(
718            default=False
719            )
720
721class VIEW3D_OT_stored_views_save(Operator):
722    bl_idname = "stored_views.save"
723    bl_label = "Save Current"
724    bl_description = "Save the view 3d current state"
725
726    index: IntProperty()
727
728    def execute(self, context):
729        mode = context.scene.stored_views.mode
730        sv = stored_view_factory(mode, self.index)
731        sv.save()
732        context.scene.stored_views.view_modified = False
733        init_draw(context)
734
735        return {'FINISHED'}
736
737
738class VIEW3D_OT_stored_views_set(Operator):
739    bl_idname = "stored_views.set"
740    bl_label = "Set"
741    bl_description = "Update the view 3D according to this view"
742
743    index: IntProperty()
744
745    def execute(self, context):
746        mode = context.scene.stored_views.mode
747        sv = stored_view_factory(mode, self.index)
748        sv.set()
749        context.scene.stored_views.view_modified = False
750        init_draw(context)
751
752        return {'FINISHED'}
753
754
755class VIEW3D_OT_stored_views_delete(Operator):
756    bl_idname = "stored_views.delete"
757    bl_label = "Delete"
758    bl_description = "Delete this view"
759
760    index: IntProperty()
761
762    def execute(self, context):
763        data = DataStore()
764        data.delete(self.index)
765
766        return {'FINISHED'}
767
768
769class VIEW3D_OT_New_Camera_to_View(Operator):
770    bl_idname = "stored_views.newcamera"
771    bl_label = "New Camera To View"
772    bl_description = "Add a new Active Camera and align it to this view"
773
774    @classmethod
775    def poll(cls, context):
776        return (
777            context.space_data is not None and
778            context.space_data.type == 'VIEW_3D' and
779            context.space_data.region_3d.view_perspective != 'CAMERA'
780            )
781
782    def execute(self, context):
783
784        if bpy.ops.object.mode_set.poll():
785            bpy.ops.object.mode_set(mode='OBJECT')
786
787        bpy.ops.object.camera_add()
788        cam = context.active_object
789        cam.name = "View_Camera"
790        # make active camera by hand
791        context.scene.camera = cam
792
793        bpy.ops.view3d.camera_to_view()
794        return {'FINISHED'}
795
796
797# Camera marker & switcher by Fsiddi
798class VIEW3D_OT_SetSceneCamera(Operator):
799    bl_idname = "cameraselector.set_scene_camera"
800    bl_label = "Set Scene Camera"
801    bl_description = "Set chosen camera as the scene's active camera"
802
803    hide_others = False
804
805    def execute(self, context):
806        chosen_camera = context.active_object
807        scene = context.scene
808
809        if self.hide_others:
810            for c in [o for o in scene.objects if o.type == 'CAMERA']:
811                c.hide = (c != chosen_camera)
812        scene.camera = chosen_camera
813        bpy.ops.object.select_all(action='DESELECT')
814        chosen_camera.select_set(True)
815        return {'FINISHED'}
816
817    def invoke(self, context, event):
818        if event.ctrl:
819            self.hide_others = True
820
821        return self.execute(context)
822
823
824class VIEW3D_OT_PreviewSceneCamera(Operator):
825    bl_idname = "cameraselector.preview_scene_camera"
826    bl_label = "Preview Camera"
827    bl_description = "Preview chosen camera and make scene's active camera"
828
829    def execute(self, context):
830        chosen_camera = context.active_object
831        bpy.ops.view3d.object_as_camera()
832        bpy.ops.object.select_all(action="DESELECT")
833        chosen_camera.select_set(True)
834        return {'FINISHED'}
835
836
837class VIEW3D_OT_AddCameraMarker(Operator):
838    bl_idname = "cameraselector.add_camera_marker"
839    bl_label = "Add Camera Marker"
840    bl_description = "Add a timeline marker bound to chosen camera"
841
842    def execute(self, context):
843        chosen_camera = context.active_object
844        scene = context.scene
845
846        current_frame = scene.frame_current
847        marker = None
848        for m in reversed(sorted(filter(lambda m: m.frame <= current_frame,
849                                        scene.timeline_markers),
850                                 key=lambda m: m.frame)):
851            marker = m
852            break
853        if marker and (marker.camera == chosen_camera):
854            # Cancel if the last marker at or immediately before
855            # current frame is already bound to the camera.
856            return {'CANCELLED'}
857
858        marker_name = "F_%02d_%s" % (current_frame, chosen_camera.name)
859        if marker and (marker.frame == current_frame):
860            # Reuse existing marker at current frame to avoid
861            # overlapping bound markers.
862            marker.name = marker_name
863        else:
864            marker = scene.timeline_markers.new(marker_name)
865        marker.frame = scene.frame_current
866        marker.camera = chosen_camera
867        marker.select = True
868
869        for other_marker in [m for m in scene.timeline_markers if m != marker]:
870            other_marker.select = False
871
872        return {'FINISHED'}
873
874# gpl authors: nfloyd, Francesco Siddi
875
876
877
878
879
880# TODO: reinstate filters?
881class IO_Utils():
882
883    @staticmethod
884    def get_preset_path():
885        # locate stored_views preset folder
886        paths = bpy.utils.preset_paths("stored_views")
887        if not paths:
888            # stored_views preset folder doesn't exist, so create it
889            paths = [os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets",
890                    "stored_views")]
891            if not os.path.exists(paths[0]):
892                os.makedirs(paths[0])
893
894        return(paths)
895
896    @staticmethod
897    def stored_views_apply_from_scene(scene_name, replace=True):
898        scene = bpy.context.scene
899        scene_exists = True if scene_name in bpy.data.scenes.keys() else False
900
901        if scene_exists:
902            sv = bpy.context.scene.stored_views
903            # io_filters = sv.settings.io_filters
904
905            structs = [sv.view_list, sv.pov_list, sv.layers_list, sv.display_list]
906            if replace is True:
907                for st in structs:  # clear swap and list
908                    while len(st) > 0:
909                        st.remove(0)
910
911            f_sv = bpy.data.scenes[scene_name].stored_views
912            # f_sv = bpy.data.scenes[scene_name].stored_views
913            f_structs = [f_sv.view_list, f_sv.pov_list, f_sv.layers_list, f_sv.display_list]
914            """
915            is_filtered = [io_filters.views, io_filters.point_of_views,
916                           io_filters.layers, io_filters.displays]
917            """
918            for i in range(len(f_structs)):
919                """
920                if is_filtered[i] is False:
921                    continue
922                """
923                for j in f_structs[i]:
924                    item = structs[i].add()
925                    # stored_views_copy_item(j, item)
926                    for k, v in j.items():
927                        item[k] = v
928            DataStore.sanitize_data(scene)
929            return True
930        else:
931            return False
932
933    @staticmethod
934    def stored_views_export_to_blsv(filepath, name='Custom Preset'):
935        # create dictionary with all information
936        dump = {"info": {}, "data": {}}
937        dump["info"]["script"] = bl_info['name']
938        dump["info"]["script_version"] = bl_info['version']
939        dump["info"]["version"] = bpy.app.version
940        dump["info"]["preset_name"] = name
941
942        # get current stored views settings
943        scene = bpy.context.scene
944        sv = scene.stored_views
945
946        def dump_view_list(dict, list):
947            if str(type(list)) == "<class 'bpy_prop_collection_idprop'>":
948                for i, struct_dict in enumerate(list):
949                    dict[i] = {"name": str,
950                               "pov": {},
951                               "layers": {},
952                               "display": {}}
953                    dict[i]["name"] = struct_dict.name
954                    dump_item(dict[i]["pov"], struct_dict.pov)
955                    dump_item(dict[i]["layers"], struct_dict.layers)
956                    dump_item(dict[i]["display"], struct_dict.display)
957
958        def dump_list(dict, list):
959            if str(type(list)) == "<class 'bpy_prop_collection_idprop'>":
960                for i, struct in enumerate(list):
961                    dict[i] = {}
962                    dump_item(dict[i], struct)
963
964        def dump_item(dict, struct):
965            for prop in struct.bl_rna.properties:
966                if prop.identifier == "rna_type":
967                    # not a setting, so skip
968                    continue
969
970                val = getattr(struct, prop.identifier)
971                if str(type(val)) in ["<class 'bpy_prop_array'>"]:
972                    # array
973                    dict[prop.identifier] = [v for v in val]
974                # address the pickle limitations of dealing with the Vector class
975                elif str(type(val)) in ["<class 'Vector'>",
976                                       "<class 'Quaternion'>"]:
977                    dict[prop.identifier] = [v for v in val]
978                else:
979                    # single value
980                    dict[prop.identifier] = val
981
982        # io_filters = sv.settings.io_filters
983        dump["data"] = {"point_of_views": {},
984                        "layers": {},
985                        "displays": {},
986                        "views": {}}
987
988        others_data = [(dump["data"]["point_of_views"], sv.pov_list),  # , io_filters.point_of_views),
989                       (dump["data"]["layers"], sv.layers_list),       # , io_filters.layers),
990                       (dump["data"]["displays"], sv.display_list)]    # , io_filters.displays)]
991        for list_data in others_data:
992            # if list_data[2] is True:
993            dump_list(list_data[0], list_data[1])
994
995        views_data = (dump["data"]["views"], sv.view_list)
996        # if io_filters.views is True:
997        dump_view_list(views_data[0], views_data[1])
998
999        # save to file
1000        filepath = filepath
1001        filepath = bpy.path.ensure_ext(filepath, '.blsv')
1002        file = gzip.open(filepath, mode='wb')
1003        pickle.dump(dump, file, protocol=pickle.HIGHEST_PROTOCOL)
1004        file.close()
1005
1006    @staticmethod
1007    def stored_views_apply_preset(filepath, replace=True):
1008        if not filepath:
1009            return False
1010
1011        file = gzip.open(filepath, mode='rb')
1012        dump = pickle.load(file)
1013        file.close()
1014        # apply preset
1015        scene = bpy.context.scene
1016        sv = getattr(scene, "stored_views", None)
1017
1018        if not sv:
1019            return False
1020
1021        # io_filters = sv.settings.io_filters
1022        sv_data = {
1023            "point_of_views": sv.pov_list,
1024            "views": sv.view_list,
1025            "layers": sv.layers_list,
1026            "displays": sv.display_list
1027        }
1028        for sv_struct, props in dump["data"].items():
1029            """
1030            is_filtered = getattr(io_filters, sv_struct)
1031            if is_filtered is False:
1032                continue
1033            """
1034            sv_list = sv_data[sv_struct]  # .list
1035            if replace is True:  # clear swap and list
1036                while len(sv_list) > 0:
1037                    sv_list.remove(0)
1038            for key, prop_struct in props.items():
1039                sv_item = sv_list.add()
1040
1041                for subprop, subval in prop_struct.items():
1042                    if isinstance(subval, dict):  # views : pov, layers, displays
1043                        v_subprop = getattr(sv_item, subprop)
1044                        for v_subkey, v_subval in subval.items():
1045                            if isinstance(v_subval, list):  # array like of pov,...
1046                                v_array_like = getattr(v_subprop, v_subkey)
1047                                for i in range(len(v_array_like)):
1048                                    v_array_like[i] = v_subval[i]
1049                            else:
1050                                setattr(v_subprop, v_subkey, v_subval)  # others
1051                    elif isinstance(subval, list):
1052                        array_like = getattr(sv_item, subprop)
1053                        for i in range(len(array_like)):
1054                            array_like[i] = subval[i]
1055                    else:
1056                        setattr(sv_item, subprop, subval)
1057
1058        DataStore.sanitize_data(scene)
1059
1060        return True
1061
1062
1063class VIEW3D_OT_stored_views_import(Operator, ImportHelper):
1064    bl_idname = "stored_views.import_blsv"
1065    bl_label = "Import Stored Views preset"
1066    bl_description = "Import a .blsv preset file to the current Stored Views"
1067
1068    filename_ext = ".blsv"
1069    filter_glob: StringProperty(
1070        default="*.blsv",
1071        options={'HIDDEN'}
1072    )
1073    replace: BoolProperty(
1074        name="Replace",
1075        default=True,
1076        description="Replace current stored views, otherwise append"
1077    )
1078
1079    @classmethod
1080    def poll(cls, context):
1081        return get_preferences()
1082
1083    def execute(self, context):
1084        # the usual way is to not select the file in the file browser
1085        exists = os.path.isfile(self.filepath) if self.filepath else False
1086        if not exists:
1087            self.report({'WARNING'},
1088                        "No filepath specified or file could not be found. Operation Cancelled")
1089            return {'CANCELLED'}
1090
1091        # apply chosen preset
1092        apply_preset = IO_Utils.stored_views_apply_preset(
1093                            filepath=self.filepath, replace=self.replace
1094                            )
1095        if not apply_preset:
1096            self.report({'WARNING'},
1097                        "Please Initialize Stored Views first (in the 3D View Properties Area)")
1098            return {'CANCELLED'}
1099
1100        # copy preset to presets folder
1101        filename = os.path.basename(self.filepath)
1102        try:
1103            shutil.copyfile(self.filepath,
1104                            os.path.join(IO_Utils.get_preset_path()[0], filename))
1105        except:
1106            self.report({'WARNING'},
1107                        "Stored Views: preset applied, but installing failed (preset already exists?)")
1108            return{'CANCELLED'}
1109
1110        return{'FINISHED'}
1111
1112
1113class VIEW3D_OT_stored_views_import_from_scene(Operator):
1114    bl_idname = "stored_views.import_from_scene"
1115    bl_label = "Import stored views from scene"
1116    bl_description = "Import currently stored views from an another scene"
1117
1118    scene_name: StringProperty(
1119        name="Scene Name",
1120        description="A current blend scene",
1121        default=""
1122    )
1123    replace: BoolProperty(
1124        name="Replace",
1125        default=True,
1126        description="Replace current stored views, otherwise append"
1127    )
1128
1129    @classmethod
1130    def poll(cls, context):
1131        return get_preferences()
1132
1133    def draw(self, context):
1134        layout = self.layout
1135
1136        layout.prop_search(self, "scene_name", bpy.data, "scenes")
1137        layout.prop(self, "replace")
1138
1139    def invoke(self, context, event):
1140        return context.window_manager.invoke_props_dialog(self)
1141
1142    def execute(self, context):
1143        # filepath should always be given
1144        if not self.scene_name:
1145            self.report({"WARNING"},
1146                        "No scene name was given. Operation Cancelled")
1147            return{'CANCELLED'}
1148
1149        is_finished = IO_Utils.stored_views_apply_from_scene(
1150                            self.scene_name, replace=self.replace
1151                            )
1152        if not is_finished:
1153            self.report({"WARNING"},
1154                        "Could not find the specified scene. Operation Cancelled")
1155            return {"CANCELLED"}
1156
1157        return{'FINISHED'}
1158
1159
1160class VIEW3D_OT_stored_views_export(Operator, ExportHelper):
1161    bl_idname = "stored_views.export_blsv"
1162    bl_label = "Export Stored Views preset"
1163    bl_description = "Export the current Stored Views to a .blsv preset file"
1164
1165    filename_ext = ".blsv"
1166    filepath: StringProperty(
1167        default=os.path.join(IO_Utils.get_preset_path()[0], "untitled")
1168    )
1169    filter_glob: StringProperty(
1170        default="*.blsv",
1171        options={'HIDDEN'}
1172    )
1173    preset_name: StringProperty(
1174        name="Preset name",
1175        default="",
1176        description="Name of the stored views preset"
1177    )
1178
1179    @classmethod
1180    def poll(cls, context):
1181        return get_preferences()
1182
1183    def execute(self, context):
1184        IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name)
1185
1186        return{'FINISHED'}
1187
1188
1189class VIEW3D_PT_properties_stored_views(Panel):
1190    bl_label = "Stored Views"
1191    bl_space_type = "VIEW_3D"
1192    bl_region_type = "UI"
1193    bl_category = "View"
1194
1195    def draw(self, context):
1196        self.logger = logging.getLogger('%s Properties panel' % __name__)
1197        layout = self.layout
1198
1199        if bpy.ops.view3d.stored_views_initialize.poll():
1200            layout.operator("view3d.stored_views_initialize")
1201            return
1202
1203        stored_views = context.scene.stored_views
1204
1205        # UI : mode
1206        col = layout.column(align=True)
1207        col.prop_enum(stored_views, "mode", 'VIEW')
1208        row = layout.row(align=True)
1209        row.operator("view3d.camera_to_view", text="Camera To view")
1210        row.operator("stored_views.newcamera")
1211
1212        row = col.row(align=True)
1213        row.prop_enum(stored_views, "mode", 'POV')
1214        row.prop_enum(stored_views, "mode", 'LAYERS')
1215        row.prop_enum(stored_views, "mode", 'DISPLAY')
1216
1217        # UI : operators
1218        row = layout.row()
1219        row.operator("stored_views.save").index = -1
1220
1221        # IO Operators
1222        if core.get_preferences():
1223            row = layout.row(align=True)
1224            row.operator("stored_views.import_from_scene", text="Import from Scene")
1225            row.operator("stored_views.import_blsv", text="", icon="IMPORT")
1226            row.operator("stored_views.export_blsv", text="", icon="EXPORT")
1227
1228        data_store = DataStore()
1229        list = data_store.list
1230        # UI : items list
1231        if len(list) > 0:
1232            row = layout.row()
1233            box = row.box()
1234            # items list
1235            mode = stored_views.mode
1236            for i in range(len(list)):
1237                # associated icon
1238                icon_string = "MESH_CUBE"  # default icon
1239                # TODO: icons for view
1240                if mode == 'POV':
1241                    persp = list[i].perspective
1242                    if persp == 'PERSP':
1243                        icon_string = "MESH_CUBE"
1244                    elif persp == 'ORTHO':
1245                        icon_string = "MESH_PLANE"
1246                    elif persp == 'CAMERA':
1247                        if list[i].camera_type != 'CAMERA':
1248                            icon_string = 'OBJECT_DATAMODE'
1249                        else:
1250                            icon_string = "OUTLINER_DATA_CAMERA"
1251                if mode == 'LAYERS':
1252                    if list[i].lock_camera_and_layers is True:
1253                        icon_string = 'SCENE_DATA'
1254                    else:
1255                        icon_string = 'RENDERLAYERS'
1256                if mode == 'DISPLAY':
1257                    shade = list[i].viewport_shade
1258                    if shade == 'TEXTURED':
1259                        icon_string = 'TEXTURE_SHADED'
1260                    if shade == 'MATERIAL':
1261                        icon_string = 'MATERIAL_DATA'
1262                    elif shade == 'SOLID':
1263                        icon_string = 'SOLID'
1264                    elif shade == 'WIREFRAME':
1265                        icon_string = "WIRE"
1266                    elif shade == 'BOUNDBOX':
1267                        icon_string = 'BBOX'
1268                    elif shade == 'RENDERED':
1269                        icon_string = 'MATERIAL'
1270                # stored view row
1271                subrow = box.row(align=True)
1272                # current view indicator
1273                if data_store.current_index == i and context.scene.stored_views.view_modified is False:
1274                    subrow.label(text="", icon='SMALL_TRI_RIGHT_VEC')
1275                subrow.operator("stored_views.set",
1276                                text="", icon=icon_string).index = i
1277                subrow.prop(list[i], "name", text="")
1278                subrow.operator("stored_views.save",
1279                                text="", icon="REC").index = i
1280                subrow.operator("stored_views.delete",
1281                                text="", icon="PANEL_CLOSE").index = i
1282
1283        layout = self.layout
1284        scene = context.scene
1285        layout.label(text="Camera Selector")
1286        cameras = sorted([o for o in scene.objects if o.type == 'CAMERA'],
1287                         key=lambda o: o.name)
1288
1289        if len(cameras) > 0:
1290            for camera in cameras:
1291                row = layout.row(align=True)
1292                row.context_pointer_set("active_object", camera)
1293                row.operator("cameraselector.set_scene_camera",
1294                                   text=camera.name, icon='OUTLINER_DATA_CAMERA')
1295                row.operator("cameraselector.preview_scene_camera",
1296                                   text='', icon='RESTRICT_VIEW_OFF')
1297                row.operator("cameraselector.add_camera_marker",
1298                                   text='', icon='MARKER')
1299        else:
1300            layout.label(text="No cameras in this scene")
1301# Addon Preferences
1302
1303class VIEW3D_OT_stored_views_preferences(AddonPreferences):
1304    bl_idname = __name__
1305
1306    show_exporters: BoolProperty(
1307        name="Enable I/O Operators",
1308        default=False,
1309        description="Enable Import/Export Operations in the UI:\n"
1310                    "Import Stored Views preset,\n"
1311                    "Export Stored Views preset and \n"
1312                    "Import stored views from scene",
1313    )
1314    view_3d_update_rate: IntProperty(
1315        name="3D view update",
1316        description="Update rate of the 3D view redraw\n"
1317                    "Increse the value if the UI feels sluggish",
1318        min=1, max=10,
1319        default=1
1320    )
1321
1322    def draw(self, context):
1323        layout = self.layout
1324
1325        row = layout.row(align=True)
1326        row.prop(self, "view_3d_update_rate", toggle=True)
1327        row.prop(self, "show_exporters", toggle=True)
1328
1329
1330# Register
1331classes = [
1332    VIEW3D_OT_stored_views_initialize,
1333    VIEW3D_OT_stored_views_preferences,
1334    VIEW3D_PT_properties_stored_views,
1335    POVData,
1336    LayersData,
1337    DisplayData,
1338    ViewData,
1339    StoredViewsData,
1340    VIEW3D_OT_stored_views_draw,
1341    VIEW3D_OT_stored_views_save,
1342    VIEW3D_OT_stored_views_set,
1343    VIEW3D_OT_stored_views_delete,
1344    VIEW3D_OT_New_Camera_to_View,
1345    VIEW3D_OT_SetSceneCamera,
1346    VIEW3D_OT_PreviewSceneCamera,
1347    VIEW3D_OT_AddCameraMarker,
1348#    IO_Utils,
1349    VIEW3D_OT_stored_views_import,
1350    VIEW3D_OT_stored_views_import_from_scene,
1351    VIEW3D_OT_stored_views_export
1352    ]
1353
1354
1355def register():
1356    from bpy.utils import register_class
1357    for cls in classes:
1358        register_class(cls)
1359
1360
1361def unregister():
1362    ui.VIEW3D_OT_stored_views_draw.handle_remove(bpy.context)
1363    from bpy.utils import unregister_class
1364    for cls in reversed(classes):
1365        unregister_class(cls)
1366    if hasattr(bpy.types.Scene, "stored_views"):
1367        del bpy.types.Scene.stored_views
1368
1369
1370if __name__ == "__main__":
1371    register()
1372