space_view3d_stored_views: test file for single file script
authormeta-androcto <meta.androcto1@gmail.com>
Sat, 8 Jun 2019 07:03:16 +0000 (17:03 +1000)
committermeta-androcto <meta.androcto1@gmail.com>
Sat, 8 Jun 2019 07:03:16 +0000 (17:03 +1000)
space_view3d_stored_views/stored_views_test.py [new file with mode: 0644]

diff --git a/space_view3d_stored_views/stored_views_test.py b/space_view3d_stored_views/stored_views_test.py
new file mode 100644 (file)
index 0000000..fa4866f
--- /dev/null
@@ -0,0 +1,1371 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+bl_info = {
+    "name": "Stored Views",
+    "description": "Save and restore User defined views, pov, layers and display configs",
+    "author": "nfloyd, Francesco Siddi",
+    "version": (0, 3, 7),
+    "blender": (2, 80, 0),
+    "location": "View3D > Properties > Stored Views",
+    "warning": "",
+    "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.5/"
+                "Py/Scripts/3D_interaction/stored_views",
+    "category": "3D View"
+}
+
+"""
+ACKNOWLEDGMENT
+==============
+import/export functionality is mostly based
+on Bart Crouch's Theme Manager Addon
+
+TODO: quadview complete support : investigate. Where's the data?
+TODO: lock_camera_and_layers. investigate usage
+TODO: list reordering
+
+NOTE: logging setup has to be provided by the user in a separate config file
+    as Blender will not try to configure logging by default in an add-on
+    The Config File should be in the Blender Config folder > /scripts/startup/config_logging.py
+    For setting up /location of the config folder see:
+    https://docs.blender.org/manual/en/dev/getting_started/
+    installing/configuration/directories.html
+    For configuring logging itself in the file, general Python documentation should work
+    As the logging calls are not configured, they can be kept in the other modules of this add-on
+    and will not have output until the logging configuration is set up
+"""
+
+
+import bpy
+from bpy.props import (
+    BoolProperty,
+    IntProperty,
+    PointerProperty,
+)
+from bpy.types import (
+    AddonPreferences,
+    Operator,
+    Panel
+)
+
+import logging
+module_logger = logging.getLogger(__name__)
+
+import gzip
+import os
+import pickle
+import shutil
+
+from bpy_extras.io_utils import (
+    ExportHelper,
+    ImportHelper,
+)
+
+import blf
+
+import hashlib
+import bpy
+
+
+# Utility function get preferences setting for exporters
+def get_preferences():
+    # replace the key if the add-on name changes
+    addon = bpy.context.preferences.addons[__package__]
+    show_warn = (addon.preferences.show_exporters if addon else False)
+
+    return show_warn
+
+
+class StoredView():
+    def __init__(self, mode, index=None):
+        self.logger = logging.getLogger('%s.StoredView' % __name__)
+        self.scene = bpy.context.scene
+        self.view3d = bpy.context.space_data
+        self.index = index
+        self.data_store = DataStore(mode=mode)
+
+    def save(self):
+        if self.index == -1:
+            stored_view, self.index = self.data_store.create()
+        else:
+            stored_view = self.data_store.get(self.index)
+        self.from_v3d(stored_view)
+        self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name))
+
+    def set(self):
+        stored_view = self.data_store.get(self.index)
+        self.update_v3d(stored_view)
+        self.logger.debug('index: %s name: %s' % (self.data_store.current_index, stored_view.name))
+
+    def from_v3d(self, stored_view):
+        raise NotImplementedError("Subclass must implement abstract method")
+
+    def update_v3d(self, stored_view):
+        raise NotImplementedError("Subclass must implement abstract method")
+
+    @staticmethod
+    def is_modified(context, stored_view):
+        raise NotImplementedError("Subclass must implement abstract method")
+
+
+class POV(StoredView):
+    def __init__(self, index=None):
+        super().__init__(mode='POV', index=index)
+        self.logger = logging.getLogger('%s.POV' % __name__)
+
+    def from_v3d(self, stored_view):
+        view3d = self.view3d
+        region3d = view3d.region_3d
+
+        stored_view.distance = region3d.view_distance
+        stored_view.location = region3d.view_location
+        stored_view.rotation = region3d.view_rotation
+        stored_view.perspective_matrix_md5 = POV._get_perspective_matrix_md5(region3d)
+        stored_view.perspective = region3d.view_perspective
+        stored_view.lens = view3d.lens
+        stored_view.clip_start = view3d.clip_start
+        stored_view.clip_end = view3d.clip_end
+
+        if region3d.view_perspective == 'CAMERA':
+            stored_view.camera_type = view3d.camera.type  # type : 'CAMERA' or 'MESH'
+            stored_view.camera_name = view3d.camera.name  # store string instead of object
+        if view3d.lock_object is not None:
+            stored_view.lock_object_name = view3d.lock_object.name  # idem
+        else:
+            stored_view.lock_object_name = ""
+        stored_view.lock_cursor = view3d.lock_cursor
+        stored_view.cursor_location = view3d.cursor_location
+
+    def update_v3d(self, stored_view):
+        view3d = self.view3d
+        region3d = view3d.region_3d
+        region3d.view_distance = stored_view.distance
+        region3d.view_location = stored_view.location
+        region3d.view_rotation = stored_view.rotation
+        region3d.view_perspective = stored_view.perspective
+        view3d.lens = stored_view.lens
+        view3d.clip_start = stored_view.clip_start
+        view3d.clip_end = stored_view.clip_end
+        view3d.lock_cursor = stored_view.lock_cursor
+        if stored_view.lock_cursor is True:
+            # update cursor only if view is locked to cursor
+            view3d.cursor_location = stored_view.cursor_location
+
+        if stored_view.perspective == "CAMERA":
+
+            lock_obj = self._get_object(stored_view.lock_object_name)
+            if lock_obj:
+                view3d.lock_object = lock_obj
+            else:
+                cam = self._get_object(stored_view.camera_name)
+                if cam:
+                    view3d.camera = cam
+
+    @staticmethod
+    def _get_object(name, pointer=None):
+        return bpy.data.objects.get(name)
+
+    @staticmethod
+    def is_modified(context, stored_view):
+        # TODO: check for others param, currently only perspective
+        # and perspective_matrix are checked
+        POV.logger = logging.getLogger('%s.POV' % __name__)
+        view3d = context.space_data
+        region3d = view3d.region_3d
+        if region3d.view_perspective != stored_view.perspective:
+            POV.logger.debug('view_perspective')
+            return True
+
+        md5 = POV._get_perspective_matrix_md5(region3d)
+        if (md5 != stored_view.perspective_matrix_md5 and
+          region3d.view_perspective != "CAMERA"):
+            POV.logger.debug('perspective_matrix')
+            return True
+
+        return False
+
+    @staticmethod
+    def _get_perspective_matrix_md5(region3d):
+        md5 = hashlib.md5(str(region3d.perspective_matrix).encode('utf-8')).hexdigest()
+        return md5
+
+
+class Layers(StoredView):
+    def __init__(self, index=None):
+        super().__init__(mode='LAYERS', index=index)
+        self.logger = logging.getLogger('%s.Layers' % __name__)
+
+    def from_v3d(self, stored_view):
+        view3d = self.view3d
+        stored_view.view_layers = view3d.layers
+        stored_view.scene_layers = self.scene.layers
+        stored_view.lock_camera_and_layers = view3d.lock_camera_and_layers
+
+    def update_v3d(self, stored_view):
+        view3d = self.view3d
+        view3d.lock_camera_and_layers = stored_view.lock_camera_and_layers
+        if stored_view.lock_camera_and_layers is True:
+            self.scene.layers = stored_view.scene_layers
+        else:
+            view3d.layers = stored_view.view_layers
+
+    @staticmethod
+    def is_modified(context, stored_view):
+        Layers.logger = logging.getLogger('%s.Layers' % __name__)
+        if stored_view.lock_camera_and_layers != context.space_data.lock_camera_and_layers:
+            Layers.logger.debug('lock_camera_and_layers')
+            return True
+        if stored_view.lock_camera_and_layers is True:
+            for i in range(20):
+                if stored_view.scene_layers[i] != context.scene.layers[i]:
+                    Layers.logger.debug('scene_layers[%s]' % (i, ))
+                    return True
+        else:
+            for i in range(20):
+                if stored_view.view_layers[i] != context.space_data.view3d.layers[i]:
+                    return True
+        return False
+
+
+class Display(StoredView):
+    def __init__(self, index=None):
+        super().__init__(mode='DISPLAY', index=index)
+        self.logger = logging.getLogger('%s.Display' % __name__)
+
+    def from_v3d(self, stored_view):
+        view3d = self.view3d
+        stored_view.viewport_shade = view3d.viewport_shade
+        stored_view.show_only_render = view3d.show_only_render
+        stored_view.show_outline_selected = view3d.show_outline_selected
+        stored_view.show_all_objects_origin = view3d.show_all_objects_origin
+        stored_view.show_relationship_lines = view3d.show_relationship_lines
+        stored_view.show_floor = view3d.show_floor
+        stored_view.show_axis_x = view3d.show_axis_x
+        stored_view.show_axis_y = view3d.show_axis_y
+        stored_view.show_axis_z = view3d.show_axis_z
+        stored_view.grid_lines = view3d.grid_lines
+        stored_view.grid_scale = view3d.grid_scale
+        stored_view.grid_subdivisions = view3d.grid_subdivisions
+        stored_view.material_mode = self.scene.game_settings.material_mode
+        stored_view.show_textured_solid = view3d.show_textured_solid
+
+    def update_v3d(self, stored_view):
+        view3d = self.view3d
+        view3d.viewport_shade = stored_view.viewport_shade
+        view3d.show_only_render = stored_view.show_only_render
+        view3d.show_outline_selected = stored_view.show_outline_selected
+        view3d.show_all_objects_origin = stored_view.show_all_objects_origin
+        view3d.show_relationship_lines = stored_view.show_relationship_lines
+        view3d.show_floor = stored_view.show_floor
+        view3d.show_axis_x = stored_view.show_axis_x
+        view3d.show_axis_y = stored_view.show_axis_y
+        view3d.show_axis_z = stored_view.show_axis_z
+        view3d.grid_lines = stored_view.grid_lines
+        view3d.grid_scale = stored_view.grid_scale
+        view3d.grid_subdivisions = stored_view.grid_subdivisions
+        self.scene.game_settings.material_mode = stored_view.material_mode
+        view3d.show_textured_solid = stored_view.show_textured_solid
+
+    @staticmethod
+    def is_modified(context, stored_view):
+        Display.logger = logging.getLogger('%s.Display' % __name__)
+        view3d = context.space_data
+        excludes = ["material_mode", "quad_view", "lock_rotation", "show_sync_view", "use_box_clip", "name"]
+        for k, v in stored_view.items():
+            if k not in excludes:
+                if getattr(view3d, k) != getattr(stored_view, k):
+                    return True
+
+        if stored_view.material_mode != context.scene.game_settings.material_mode:
+            Display.logger.debug('material_mode')
+            return True
+
+
+class View(StoredView):
+    def __init__(self, index=None):
+        super().__init__(mode='VIEW', index=index)
+        self.logger = logging.getLogger('%s.View' % __name__)
+        self.pov = POV()
+        self.layers = Layers()
+        self.display = Display()
+
+    def from_v3d(self, stored_view):
+        self.pov.from_v3d(stored_view.pov)
+        self.layers.from_v3d(stored_view.layers)
+        self.display.from_v3d(stored_view.display)
+
+    def update_v3d(self, stored_view):
+        self.pov.update_v3d(stored_view.pov)
+        self.layers.update_v3d(stored_view.layers)
+        self.display.update_v3d(stored_view.display)
+
+    @staticmethod
+    def is_modified(context, stored_view):
+        if POV.is_modified(context, stored_view.pov) or \
+           Layers.is_modified(context, stored_view.layers) or \
+           Display.is_modified(context, stored_view.display):
+            return True
+        return False
+
+
+class DataStore():
+    def __init__(self, scene=None, mode=None):
+        if scene is None:
+            scene = bpy.context.scene
+        stored_views = scene.stored_views
+        self.mode = mode
+
+        if mode is None:
+            self.mode = stored_views.mode
+
+        if self.mode == 'VIEW':
+            self.list = stored_views.view_list
+            self.current_index = stored_views.current_indices[0]
+        elif self.mode == 'POV':
+            self.list = stored_views.pov_list
+            self.current_index = stored_views.current_indices[1]
+        elif self.mode == 'LAYERS':
+            self.list = stored_views.layers_list
+            self.current_index = stored_views.current_indices[2]
+        elif self.mode == 'DISPLAY':
+            self.list = stored_views.display_list
+            self.current_index = stored_views.current_indices[3]
+
+    def create(self):
+        item = self.list.add()
+        item.name = self._generate_name()
+        index = len(self.list) - 1
+        self._set_current_index(index)
+        return item, index
+
+    def get(self, index):
+        self._set_current_index(index)
+        return self.list[index]
+
+    def delete(self, index):
+        if self.current_index > index:
+            self._set_current_index(self.current_index - 1)
+        elif self.current_index == index:
+            self._set_current_index(-1)
+
+        self.list.remove(index)
+
+    def _set_current_index(self, index):
+        self.current_index = index
+        mode = self.mode
+        stored_views = bpy.context.scene.stored_views
+        if mode == 'VIEW':
+            stored_views.current_indices[0] = index
+        elif mode == 'POV':
+            stored_views.current_indices[1] = index
+        elif mode == 'LAYERS':
+            stored_views.current_indices[2] = index
+        elif mode == 'DISPLAY':
+            stored_views.current_indices[3] = index
+
+    def _generate_name(self):
+        default_name = str(self.mode)
+        names = []
+        for i in self.list:
+            i_name = i.name
+            if i_name.startswith(default_name):
+                names.append(i_name)
+        names.sort()
+        try:
+            l_name = names[-1]
+            post_fix = l_name.rpartition('.')[2]
+            if post_fix.isnumeric():
+                post_fix = str(int(post_fix) + 1).zfill(3)
+            else:
+                if post_fix == default_name:
+                    post_fix = "001"
+            return default_name + "." + post_fix
+        except:
+            return default_name
+
+    @staticmethod
+    def sanitize_data(scene):
+
+        def check_objects_references(mode, list):
+            to_remove = []
+            for i, list_item in enumerate(list.items()):
+                key, item = list_item
+                if mode == 'POV' or mode == 'VIEWS':
+                    if mode == 'VIEWS':
+                        item = item.pov
+
+                    if item.perspective == "CAMERA":
+
+                        camera = bpy.data.objects.get(item.camera_name)
+                        if camera is None:
+                            try:  # pick a default camera TODO: ask to pick?
+                                camera = bpy.data.cameras[0]
+                                item.camera_name = camera.name
+                            except:  # couldn't find a camera in the scene
+                                pass
+
+                        obj = bpy.data.objects.get(item.lock_object_name)
+                        if obj is None and camera is None:
+                            to_remove.append(i)
+
+            for i in reversed(to_remove):
+                list.remove(i)
+
+        modes = ['POV', 'VIEW', 'DISPLAY', 'LAYERS']
+        for mode in modes:
+            data = DataStore(scene=scene, mode=mode)
+            check_objects_references(mode, data.list)
+
+
+def stored_view_factory(mode, *args, **kwargs):
+    if mode == 'POV':
+        return POV(*args, **kwargs)
+    elif mode == 'LAYERS':
+        return Layers(*args, **kwargs)
+    elif mode == 'DISPLAY':
+        return Display(*args, **kwargs)
+    elif mode == 'VIEW':
+        return View(*args, **kwargs)
+
+"""
+  If view name display is enabled,
+  it will check periodically if the view has been modified
+  since last set.
+  get_preferences_timer() is the time in seconds between these checks.
+  It can be increased, if the view become sluggish
+  It is set in the add-on preferences
+"""
+
+
+# Utility function get_preferences_timer for update of 3d view draw
+def get_preferences_timer():
+    # replace the key if the add-on name changes
+    # TODO: expose refresh rate to ui???
+    addon = bpy.context.preferences.addons[__package__]
+    timer_update = (addon.preferences.view_3d_update_rate if addon else False)
+
+    return timer_update
+
+
+def init_draw(context=None):
+    if context is None:
+        context = bpy.context
+
+    if "stored_views_osd" not in context.window_manager:
+        context.window_manager["stored_views_osd"] = False
+
+    if not context.window_manager["stored_views_osd"]:
+        context.window_manager["stored_views_osd"] = True
+        bpy.ops.stored_views.draw()
+
+
+def _draw_callback_px(self, context):
+    if context.area and context.area.type == 'VIEW_3D':
+        r_width = text_location = context.region.width
+        r_height = context.region.height
+        font_id = 0  # TODO: need to find out how best to get font_id
+
+        blf.size(font_id, 11, context.preferences.system.dpi)
+        text_size = blf.dimensions(0, self.view_name)
+
+        # compute the text location
+        text_location = 0
+        overlap = context.preferences.system.use_region_overlap
+        if overlap:
+            for region in context.area.regions:
+                if region.type == "UI":
+                    text_location = r_width - region.width
+
+        text_x = text_location - text_size[0] - 10
+        text_y = r_height - text_size[1] - 8
+        blf.position(font_id, text_x, text_y, 0)
+        blf.draw(font_id, self.view_name)
+
+
+class VIEW3D_OT_stored_views_draw(Operator):
+    bl_idname = "stored_views.draw"
+    bl_label = "Show current"
+    bl_description = "Toggle the display current view name in the view 3D"
+
+    _handle = None
+    _timer = None
+
+    @staticmethod
+    def handle_add(self, context):
+        VIEW3D_OT_stored_views_draw._handle = bpy.types.SpaceView3D.draw_handler_add(
+            _draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
+        VIEW3D_OT_stored_views_draw._timer = \
+            context.window_manager.event_timer_add(get_preferences_timer(), context.window)
+
+    @staticmethod
+    def handle_remove(context):
+        if VIEW3D_OT_stored_views_draw._handle is not None:
+            bpy.types.SpaceView3D.draw_handler_remove(VIEW3D_OT_stored_views_draw._handle, 'WINDOW')
+        if VIEW3D_OT_stored_views_draw._timer is not None:
+            context.window_manager.event_timer_remove(VIEW3D_OT_stored_views_draw._timer)
+        VIEW3D_OT_stored_views_draw._handle = None
+        VIEW3D_OT_stored_views_draw._timer = None
+
+    @classmethod
+    def poll(cls, context):
+        # return context.mode == 'OBJECT'
+        return True
+
+    def modal(self, context, event):
+        if context.area:
+            context.area.tag_redraw()
+
+        if not context.area or context.area.type != "VIEW_3D":
+            return {"PASS_THROUGH"}
+
+        data = DataStore()
+        stored_views = context.scene.stored_views
+
+        if len(data.list) > 0 and \
+           data.current_index >= 0 and \
+           not stored_views.view_modified:
+
+            if not stored_views.view_modified:
+                sv = data.list[data.current_index]
+                self.view_name = sv.name
+                if event.type == 'TIMER':
+                    is_modified = False
+                    if data.mode == 'VIEW':
+                        is_modified = View.is_modified(context, sv)
+                    elif data.mode == 'POV':
+                        is_modified = POV.is_modified(context, sv)
+                    elif data.mode == 'LAYERS':
+                        is_modified = Layers.is_modified(context, sv)
+                    elif data.mode == 'DISPLAY':
+                        is_modified = Display.is_modified(context, sv)
+                    if is_modified:
+                        module_logger.debug(
+                                'view modified - index: %s name: %s' % (data.current_index, sv.name)
+                                )
+                        self.view_name = ""
+                        stored_views.view_modified = is_modified
+
+                return {"PASS_THROUGH"}
+        else:
+            module_logger.debug('exit')
+            context.window_manager["stored_views_osd"] = False
+            VIEW3D_OT_stored_views_draw.handle_remove(context)
+
+            return {'FINISHED'}
+
+    def execute(self, context):
+        if context.area.type == "VIEW_3D":
+            self.view_name = ""
+            VIEW3D_OT_stored_views_draw.handle_add(self, context)
+            context.window_manager.modal_handler_add(self)
+
+            return {"RUNNING_MODAL"}
+        else:
+            self.report({"WARNING"}, "View3D not found. Operation Cancelled")
+
+            return {"CANCELLED"}
+
+class VIEW3D_OT_stored_views_initialize(Operator):
+    bl_idname = "view3d.stored_views_initialize"
+    bl_label = "Initialize"
+
+    @classmethod
+    def poll(cls, context):
+        return not hasattr(bpy.types.Scene, 'stored_views')
+
+    def execute(self, context):
+        bpy.types.Scene.stored_views: PointerProperty(
+                                            type=properties.StoredViewsData
+                                            )
+        scenes = bpy.data.scenes
+        data = DataStore()
+        for scene in scenes:
+            DataStore.sanitize_data(scene)
+        return {'FINISHED'}
+
+
+from bpy.types import PropertyGroup
+from bpy.props import (
+        BoolProperty,
+        BoolVectorProperty,
+        CollectionProperty,
+        FloatProperty,
+        FloatVectorProperty,
+        EnumProperty,
+        IntProperty,
+        IntVectorProperty,
+        PointerProperty,
+        StringProperty,
+        )
+
+
+class POVData(PropertyGroup):
+    distance: FloatProperty()
+    location: FloatVectorProperty(
+            subtype='TRANSLATION'
+            )
+    rotation: FloatVectorProperty(
+            subtype='QUATERNION',
+            size=4
+            )
+    name: StringProperty()
+    perspective: EnumProperty(
+            items=[('PERSP', '', ''),
+                   ('ORTHO', '', ''),
+                   ('CAMERA', '', '')]
+            )
+    lens: FloatProperty()
+    clip_start: FloatProperty()
+    clip_end: FloatProperty()
+    lock_cursor: BoolProperty()
+    cursor_location: FloatVectorProperty()
+    perspective_matrix_md5: StringProperty()
+    camera_name: StringProperty()
+    camera_type: StringProperty()
+    lock_object_name: StringProperty()
+
+
+class LayersData(PropertyGroup):
+    view_layers: BoolVectorProperty(size=20)
+    scene_layers: BoolVectorProperty(size=20)
+    lock_camera_and_layers: BoolProperty()
+    name: StringProperty()
+
+
+class DisplayData(PropertyGroup):
+    name: StringProperty()
+    viewport_shade: EnumProperty(
+            items=[('BOUNDBOX', 'BOUNDBOX', 'BOUNDBOX'),
+                   ('WIREFRAME', 'WIREFRAME', 'WIREFRAME'),
+                   ('SOLID', 'SOLID', 'SOLID'),
+                   ('TEXTURED', 'TEXTURED', 'TEXTURED'),
+                   ('MATERIAL', 'MATERIAL', 'MATERIAL'),
+                   ('RENDERED', 'RENDERED', 'RENDERED')]
+            )
+    show_only_render: BoolProperty()
+    show_outline_selected: BoolProperty()
+    show_all_objects_origin: BoolProperty()
+    show_relationship_lines: BoolProperty()
+    show_floor: BoolProperty()
+    show_axis_x: BoolProperty()
+    show_axis_y: BoolProperty()
+    show_axis_z: BoolProperty()
+    grid_lines: IntProperty()
+    grid_scale: FloatProperty()
+    grid_subdivisions: IntProperty()
+    material_mode: StringProperty()
+    show_textured_solid: BoolProperty()
+    quad_view: BoolProperty()
+    lock_rotation: BoolProperty()
+    show_sync_view: BoolProperty()
+    use_box_clip: BoolProperty()
+
+
+class ViewData(PropertyGroup):
+    pov: PointerProperty(
+            type=POVData
+            )
+    layers: PointerProperty(
+            type=LayersData
+            )
+    display: PointerProperty(
+            type=DisplayData
+            )
+    name: StringProperty()
+
+
+class StoredViewsData(PropertyGroup):
+    pov_list: CollectionProperty(
+            type=POVData
+            )
+    layers_list: CollectionProperty(
+            type=LayersData
+            )
+    display_list: CollectionProperty(
+            type=DisplayData
+            )
+    view_list: CollectionProperty(
+            type=ViewData
+            )
+    mode: EnumProperty(
+            name="Mode",
+            items=[('VIEW', "View", "3D View settings"),
+                   ('POV', "POV", "POV settings"),
+                   ('LAYERS', "Layers", "Layers settings"),
+                   ('DISPLAY', "Display", "Display settings")],
+            default='VIEW'
+            )
+    current_indices: IntVectorProperty(
+            size=4,
+            default=[-1, -1, -1, -1]
+            )
+    view_modified: BoolProperty(
+            default=False
+            )
+
+class VIEW3D_OT_stored_views_save(Operator):
+    bl_idname = "stored_views.save"
+    bl_label = "Save Current"
+    bl_description = "Save the view 3d current state"
+
+    index: IntProperty()
+
+    def execute(self, context):
+        mode = context.scene.stored_views.mode
+        sv = stored_view_factory(mode, self.index)
+        sv.save()
+        context.scene.stored_views.view_modified = False
+        init_draw(context)
+
+        return {'FINISHED'}
+
+
+class VIEW3D_OT_stored_views_set(Operator):
+    bl_idname = "stored_views.set"
+    bl_label = "Set"
+    bl_description = "Update the view 3D according to this view"
+
+    index: IntProperty()
+
+    def execute(self, context):
+        mode = context.scene.stored_views.mode
+        sv = stored_view_factory(mode, self.index)
+        sv.set()
+        context.scene.stored_views.view_modified = False
+        init_draw(context)
+
+        return {'FINISHED'}
+
+
+class VIEW3D_OT_stored_views_delete(Operator):
+    bl_idname = "stored_views.delete"
+    bl_label = "Delete"
+    bl_description = "Delete this view"
+
+    index: IntProperty()
+
+    def execute(self, context):
+        data = DataStore()
+        data.delete(self.index)
+
+        return {'FINISHED'}
+
+
+class VIEW3D_OT_New_Camera_to_View(Operator):
+    bl_idname = "stored_views.newcamera"
+    bl_label = "New Camera To View"
+    bl_description = "Add a new Active Camera and align it to this view"
+
+    @classmethod
+    def poll(cls, context):
+        return (
+            context.space_data is not None and
+            context.space_data.type == 'VIEW_3D' and
+            context.space_data.region_3d.view_perspective != 'CAMERA'
+            )
+
+    def execute(self, context):
+
+        if bpy.ops.object.mode_set.poll():
+            bpy.ops.object.mode_set(mode='OBJECT')
+
+        bpy.ops.object.camera_add()
+        cam = context.active_object
+        cam.name = "View_Camera"
+        # make active camera by hand
+        context.scene.camera = cam
+
+        bpy.ops.view3d.camera_to_view()
+        return {'FINISHED'}
+
+
+# Camera marker & switcher by Fsiddi
+class VIEW3D_OT_SetSceneCamera(Operator):
+    bl_idname = "cameraselector.set_scene_camera"
+    bl_label = "Set Scene Camera"
+    bl_description = "Set chosen camera as the scene's active camera"
+
+    hide_others = False
+
+    def execute(self, context):
+        chosen_camera = context.active_object
+        scene = context.scene
+
+        if self.hide_others:
+            for c in [o for o in scene.objects if o.type == 'CAMERA']:
+                c.hide = (c != chosen_camera)
+        scene.camera = chosen_camera
+        bpy.ops.object.select_all(action='DESELECT')
+        chosen_camera.select_set(True)
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        if event.ctrl:
+            self.hide_others = True
+
+        return self.execute(context)
+
+
+class VIEW3D_OT_PreviewSceneCamera(Operator):
+    bl_idname = "cameraselector.preview_scene_camera"
+    bl_label = "Preview Camera"
+    bl_description = "Preview chosen camera and make scene's active camera"
+
+    def execute(self, context):
+        chosen_camera = context.active_object
+        bpy.ops.view3d.object_as_camera()
+        bpy.ops.object.select_all(action="DESELECT")
+        chosen_camera.select_set(True)
+        return {'FINISHED'}
+
+
+class VIEW3D_OT_AddCameraMarker(Operator):
+    bl_idname = "cameraselector.add_camera_marker"
+    bl_label = "Add Camera Marker"
+    bl_description = "Add a timeline marker bound to chosen camera"
+
+    def execute(self, context):
+        chosen_camera = context.active_object
+        scene = context.scene
+
+        current_frame = scene.frame_current
+        marker = None
+        for m in reversed(sorted(filter(lambda m: m.frame <= current_frame,
+                                        scene.timeline_markers),
+                                 key=lambda m: m.frame)):
+            marker = m
+            break
+        if marker and (marker.camera == chosen_camera):
+            # Cancel if the last marker at or immediately before
+            # current frame is already bound to the camera.
+            return {'CANCELLED'}
+
+        marker_name = "F_%02d_%s" % (current_frame, chosen_camera.name)
+        if marker and (marker.frame == current_frame):
+            # Reuse existing marker at current frame to avoid
+            # overlapping bound markers.
+            marker.name = marker_name
+        else:
+            marker = scene.timeline_markers.new(marker_name)
+        marker.frame = scene.frame_current
+        marker.camera = chosen_camera
+        marker.select = True
+
+        for other_marker in [m for m in scene.timeline_markers if m != marker]:
+            other_marker.select = False
+
+        return {'FINISHED'}
+
+# gpl authors: nfloyd, Francesco Siddi
+
+
+
+
+
+# TODO: reinstate filters?
+class IO_Utils():
+
+    @staticmethod
+    def get_preset_path():
+        # locate stored_views preset folder
+        paths = bpy.utils.preset_paths("stored_views")
+        if not paths:
+            # stored_views preset folder doesn't exist, so create it
+            paths = [os.path.join(bpy.utils.user_resource('SCRIPTS'), "presets",
+                    "stored_views")]
+            if not os.path.exists(paths[0]):
+                os.makedirs(paths[0])
+
+        return(paths)
+
+    @staticmethod
+    def stored_views_apply_from_scene(scene_name, replace=True):
+        scene = bpy.context.scene
+        scene_exists = True if scene_name in bpy.data.scenes.keys() else False
+
+        if scene_exists:
+            sv = bpy.context.scene.stored_views
+            # io_filters = sv.settings.io_filters
+
+            structs = [sv.view_list, sv.pov_list, sv.layers_list, sv.display_list]
+            if replace is True:
+                for st in structs:  # clear swap and list
+                    while len(st) > 0:
+                        st.remove(0)
+
+            f_sv = bpy.data.scenes[scene_name].stored_views
+            # f_sv = bpy.data.scenes[scene_name].stored_views
+            f_structs = [f_sv.view_list, f_sv.pov_list, f_sv.layers_list, f_sv.display_list]
+            """
+            is_filtered = [io_filters.views, io_filters.point_of_views,
+                           io_filters.layers, io_filters.displays]
+            """
+            for i in range(len(f_structs)):
+                """
+                if is_filtered[i] is False:
+                    continue
+                """
+                for j in f_structs[i]:
+                    item = structs[i].add()
+                    # stored_views_copy_item(j, item)
+                    for k, v in j.items():
+                        item[k] = v
+            DataStore.sanitize_data(scene)
+            return True
+        else:
+            return False
+
+    @staticmethod
+    def stored_views_export_to_blsv(filepath, name='Custom Preset'):
+        # create dictionary with all information
+        dump = {"info": {}, "data": {}}
+        dump["info"]["script"] = bl_info['name']
+        dump["info"]["script_version"] = bl_info['version']
+        dump["info"]["version"] = bpy.app.version
+        dump["info"]["preset_name"] = name
+
+        # get current stored views settings
+        scene = bpy.context.scene
+        sv = scene.stored_views
+
+        def dump_view_list(dict, list):
+            if str(type(list)) == "<class 'bpy_prop_collection_idprop'>":
+                for i, struct_dict in enumerate(list):
+                    dict[i] = {"name": str,
+                               "pov": {},
+                               "layers": {},
+                               "display": {}}
+                    dict[i]["name"] = struct_dict.name
+                    dump_item(dict[i]["pov"], struct_dict.pov)
+                    dump_item(dict[i]["layers"], struct_dict.layers)
+                    dump_item(dict[i]["display"], struct_dict.display)
+
+        def dump_list(dict, list):
+            if str(type(list)) == "<class 'bpy_prop_collection_idprop'>":
+                for i, struct in enumerate(list):
+                    dict[i] = {}
+                    dump_item(dict[i], struct)
+
+        def dump_item(dict, struct):
+            for prop in struct.bl_rna.properties:
+                if prop.identifier == "rna_type":
+                    # not a setting, so skip
+                    continue
+
+                val = getattr(struct, prop.identifier)
+                if str(type(val)) in ["<class 'bpy_prop_array'>"]:
+                    # array
+                    dict[prop.identifier] = [v for v in val]
+                # address the pickle limitations of dealing with the Vector class
+                elif str(type(val)) in ["<class 'Vector'>",
+                                       "<class 'Quaternion'>"]:
+                    dict[prop.identifier] = [v for v in val]
+                else:
+                    # single value
+                    dict[prop.identifier] = val
+
+        # io_filters = sv.settings.io_filters
+        dump["data"] = {"point_of_views": {},
+                        "layers": {},
+                        "displays": {},
+                        "views": {}}
+
+        others_data = [(dump["data"]["point_of_views"], sv.pov_list),  # , io_filters.point_of_views),
+                       (dump["data"]["layers"], sv.layers_list),       # , io_filters.layers),
+                       (dump["data"]["displays"], sv.display_list)]    # , io_filters.displays)]
+        for list_data in others_data:
+            # if list_data[2] is True:
+            dump_list(list_data[0], list_data[1])
+
+        views_data = (dump["data"]["views"], sv.view_list)
+        # if io_filters.views is True:
+        dump_view_list(views_data[0], views_data[1])
+
+        # save to file
+        filepath = filepath
+        filepath = bpy.path.ensure_ext(filepath, '.blsv')
+        file = gzip.open(filepath, mode='wb')
+        pickle.dump(dump, file, protocol=pickle.HIGHEST_PROTOCOL)
+        file.close()
+
+    @staticmethod
+    def stored_views_apply_preset(filepath, replace=True):
+        if not filepath:
+            return False
+
+        file = gzip.open(filepath, mode='rb')
+        dump = pickle.load(file)
+        file.close()
+        # apply preset
+        scene = bpy.context.scene
+        sv = getattr(scene, "stored_views", None)
+
+        if not sv:
+            return False
+
+        # io_filters = sv.settings.io_filters
+        sv_data = {
+            "point_of_views": sv.pov_list,
+            "views": sv.view_list,
+            "layers": sv.layers_list,
+            "displays": sv.display_list
+        }
+        for sv_struct, props in dump["data"].items():
+            """
+            is_filtered = getattr(io_filters, sv_struct)
+            if is_filtered is False:
+                continue
+            """
+            sv_list = sv_data[sv_struct]  # .list
+            if replace is True:  # clear swap and list
+                while len(sv_list) > 0:
+                    sv_list.remove(0)
+            for key, prop_struct in props.items():
+                sv_item = sv_list.add()
+
+                for subprop, subval in prop_struct.items():
+                    if isinstance(subval, dict):  # views : pov, layers, displays
+                        v_subprop = getattr(sv_item, subprop)
+                        for v_subkey, v_subval in subval.items():
+                            if isinstance(v_subval, list):  # array like of pov,...
+                                v_array_like = getattr(v_subprop, v_subkey)
+                                for i in range(len(v_array_like)):
+                                    v_array_like[i] = v_subval[i]
+                            else:
+                                setattr(v_subprop, v_subkey, v_subval)  # others
+                    elif isinstance(subval, list):
+                        array_like = getattr(sv_item, subprop)
+                        for i in range(len(array_like)):
+                            array_like[i] = subval[i]
+                    else:
+                        setattr(sv_item, subprop, subval)
+
+        DataStore.sanitize_data(scene)
+
+        return True
+
+
+class VIEW3D_OT_stored_views_import(Operator, ImportHelper):
+    bl_idname = "stored_views.import_blsv"
+    bl_label = "Import Stored Views preset"
+    bl_description = "Import a .blsv preset file to the current Stored Views"
+
+    filename_ext = ".blsv"
+    filter_glob: StringProperty(
+        default="*.blsv",
+        options={'HIDDEN'}
+    )
+    replace: BoolProperty(
+        name="Replace",
+        default=True,
+        description="Replace current stored views, otherwise append"
+    )
+
+    @classmethod
+    def poll(cls, context):
+        return get_preferences()
+
+    def execute(self, context):
+        # the usual way is to not select the file in the file browser
+        exists = os.path.isfile(self.filepath) if self.filepath else False
+        if not exists:
+            self.report({'WARNING'},
+                        "No filepath specified or file could not be found. Operation Cancelled")
+            return {'CANCELLED'}
+
+        # apply chosen preset
+        apply_preset = IO_Utils.stored_views_apply_preset(
+                            filepath=self.filepath, replace=self.replace
+                            )
+        if not apply_preset:
+            self.report({'WARNING'},
+                        "Please Initialize Stored Views first (in the 3D View Properties Area)")
+            return {'CANCELLED'}
+
+        # copy preset to presets folder
+        filename = os.path.basename(self.filepath)
+        try:
+            shutil.copyfile(self.filepath,
+                            os.path.join(IO_Utils.get_preset_path()[0], filename))
+        except:
+            self.report({'WARNING'},
+                        "Stored Views: preset applied, but installing failed (preset already exists?)")
+            return{'CANCELLED'}
+
+        return{'FINISHED'}
+
+
+class VIEW3D_OT_stored_views_import_from_scene(Operator):
+    bl_idname = "stored_views.import_from_scene"
+    bl_label = "Import stored views from scene"
+    bl_description = "Import currently stored views from an another scene"
+
+    scene_name: StringProperty(
+        name="Scene Name",
+        description="A current blend scene",
+        default=""
+    )
+    replace: BoolProperty(
+        name="Replace",
+        default=True,
+        description="Replace current stored views, otherwise append"
+    )
+
+    @classmethod
+    def poll(cls, context):
+        return get_preferences()
+
+    def draw(self, context):
+        layout = self.layout
+
+        layout.prop_search(self, "scene_name", bpy.data, "scenes")
+        layout.prop(self, "replace")
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_props_dialog(self)
+
+    def execute(self, context):
+        # filepath should always be given
+        if not self.scene_name:
+            self.report({"WARNING"},
+                        "No scene name was given. Operation Cancelled")
+            return{'CANCELLED'}
+
+        is_finished = IO_Utils.stored_views_apply_from_scene(
+                            self.scene_name, replace=self.replace
+                            )
+        if not is_finished:
+            self.report({"WARNING"},
+                        "Could not find the specified scene. Operation Cancelled")
+            return {"CANCELLED"}
+
+        return{'FINISHED'}
+
+
+class VIEW3D_OT_stored_views_export(Operator, ExportHelper):
+    bl_idname = "stored_views.export_blsv"
+    bl_label = "Export Stored Views preset"
+    bl_description = "Export the current Stored Views to a .blsv preset file"
+
+    filename_ext = ".blsv"
+    filepath: StringProperty(
+        default=os.path.join(IO_Utils.get_preset_path()[0], "untitled")
+    )
+    filter_glob: StringProperty(
+        default="*.blsv",
+        options={'HIDDEN'}
+    )
+    preset_name: StringProperty(
+        name="Preset name",
+        default="",
+        description="Name of the stored views preset"
+    )
+
+    @classmethod
+    def poll(cls, context):
+        return get_preferences()
+
+    def execute(self, context):
+        IO_Utils.stored_views_export_to_blsv(self.filepath, self.preset_name)
+
+        return{'FINISHED'}
+
+
+class VIEW3D_PT_properties_stored_views(Panel):
+    bl_label = "Stored Views"
+    bl_space_type = "VIEW_3D"
+    bl_region_type = "UI"
+    bl_category = "View"
+
+    def draw(self, context):
+        self.logger = logging.getLogger('%s Properties panel' % __name__)
+        layout = self.layout
+
+        if bpy.ops.view3d.stored_views_initialize.poll():
+            layout.operator("view3d.stored_views_initialize")
+            return
+
+        stored_views = context.scene.stored_views
+
+        # UI : mode
+        col = layout.column(align=True)
+        col.prop_enum(stored_views, "mode", 'VIEW')
+        row = layout.row(align=True)
+        row.operator("view3d.camera_to_view", text="Camera To view")
+        row.operator("stored_views.newcamera")
+
+        row = col.row(align=True)
+        row.prop_enum(stored_views, "mode", 'POV')
+        row.prop_enum(stored_views, "mode", 'LAYERS')
+        row.prop_enum(stored_views, "mode", 'DISPLAY')
+
+        # UI : operators
+        row = layout.row()
+        row.operator("stored_views.save").index = -1
+
+        # IO Operators
+        if core.get_preferences():
+            row = layout.row(align=True)
+            row.operator("stored_views.import_from_scene", text="Import from Scene")
+            row.operator("stored_views.import_blsv", text="", icon="IMPORT")
+            row.operator("stored_views.export_blsv", text="", icon="EXPORT")
+
+        data_store = DataStore()
+        list = data_store.list
+        # UI : items list
+        if len(list) > 0:
+            row = layout.row()
+            box = row.box()
+            # items list
+            mode = stored_views.mode
+            for i in range(len(list)):
+                # associated icon
+                icon_string = "MESH_CUBE"  # default icon
+                # TODO: icons for view
+                if mode == 'POV':
+                    persp = list[i].perspective
+                    if persp == 'PERSP':
+                        icon_string = "MESH_CUBE"
+                    elif persp == 'ORTHO':
+                        icon_string = "MESH_PLANE"
+                    elif persp == 'CAMERA':
+                        if list[i].camera_type != 'CAMERA':
+                            icon_string = 'OBJECT_DATAMODE'
+                        else:
+                            icon_string = "OUTLINER_DATA_CAMERA"
+                if mode == 'LAYERS':
+                    if list[i].lock_camera_and_layers is True:
+                        icon_string = 'SCENE_DATA'
+                    else:
+                        icon_string = 'RENDERLAYERS'
+                if mode == 'DISPLAY':
+                    shade = list[i].viewport_shade
+                    if shade == 'TEXTURED':
+                        icon_string = 'TEXTURE_SHADED'
+                    if shade == 'MATERIAL':
+                        icon_string = 'MATERIAL_DATA'
+                    elif shade == 'SOLID':
+                        icon_string = 'SOLID'
+                    elif shade == 'WIREFRAME':
+                        icon_string = "WIRE"
+                    elif shade == 'BOUNDBOX':
+                        icon_string = 'BBOX'
+                    elif shade == 'RENDERED':
+                        icon_string = 'MATERIAL'
+                # stored view row
+                subrow = box.row(align=True)
+                # current view indicator
+                if data_store.current_index == i and context.scene.stored_views.view_modified is False:
+                    subrow.label(text="", icon='SMALL_TRI_RIGHT_VEC')
+                subrow.operator("stored_views.set",
+                                text="", icon=icon_string).index = i
+                subrow.prop(list[i], "name", text="")
+                subrow.operator("stored_views.save",
+                                text="", icon="REC").index = i
+                subrow.operator("stored_views.delete",
+                                text="", icon="PANEL_CLOSE").index = i
+
+        layout = self.layout
+        scene = context.scene
+        layout.label(text="Camera Selector")
+        cameras = sorted([o for o in scene.objects if o.type == 'CAMERA'],
+                         key=lambda o: o.name)
+
+        if len(cameras) > 0:
+            for camera in cameras:
+                row = layout.row(align=True)
+                row.context_pointer_set("active_object", camera)
+                row.operator("cameraselector.set_scene_camera",
+                                   text=camera.name, icon='OUTLINER_DATA_CAMERA')
+                row.operator("cameraselector.preview_scene_camera",
+                                   text='', icon='RESTRICT_VIEW_OFF')
+                row.operator("cameraselector.add_camera_marker",
+                                   text='', icon='MARKER')
+        else:
+            layout.label(text="No cameras in this scene")
+# Addon Preferences
+
+class VIEW3D_OT_stored_views_preferences(AddonPreferences):
+    bl_idname = __name__
+
+    show_exporters: BoolProperty(
+        name="Enable I/O Operators",
+        default=False,
+        description="Enable Import/Export Operations in the UI:\n"
+                    "Import Stored Views preset,\n"
+                    "Export Stored Views preset and \n"
+                    "Import stored views from scene",
+    )
+    view_3d_update_rate: IntProperty(
+        name="3D view update",
+        description="Update rate of the 3D view redraw\n"
+                    "Increse the value if the UI feels sluggish",
+        min=1, max=10,
+        default=1
+    )
+
+    def draw(self, context):
+        layout = self.layout
+
+        row = layout.row(align=True)
+        row.prop(self, "view_3d_update_rate", toggle=True)
+        row.prop(self, "show_exporters", toggle=True)
+
+
+# Register
+classes = [
+    VIEW3D_OT_stored_views_initialize,
+    VIEW3D_OT_stored_views_preferences,
+    VIEW3D_PT_properties_stored_views,
+    POVData,
+    LayersData,
+    DisplayData,
+    ViewData,
+    StoredViewsData,
+    VIEW3D_OT_stored_views_draw,
+    VIEW3D_OT_stored_views_save,
+    VIEW3D_OT_stored_views_set,
+    VIEW3D_OT_stored_views_delete,
+    VIEW3D_OT_New_Camera_to_View,
+    VIEW3D_OT_SetSceneCamera,
+    VIEW3D_OT_PreviewSceneCamera,
+    VIEW3D_OT_AddCameraMarker,
+#    IO_Utils,
+    VIEW3D_OT_stored_views_import,
+    VIEW3D_OT_stored_views_import_from_scene,
+    VIEW3D_OT_stored_views_export
+    ]
+
+
+def register():
+    from bpy.utils import register_class
+    for cls in classes:
+        register_class(cls)
+
+
+def unregister():
+    ui.VIEW3D_OT_stored_views_draw.handle_remove(bpy.context)
+    from bpy.utils import unregister_class
+    for cls in reversed(classes):
+        unregister_class(cls)
+    if hasattr(bpy.types.Scene, "stored_views"):
+        del bpy.types.Scene.stored_views
+
+
+if __name__ == "__main__":
+    register()