Cleanup: split user preferences out of wm.py
authorCampbell Barton <ideasman42@gmail.com>
Mon, 11 Feb 2019 12:17:05 +0000 (23:17 +1100)
committerCampbell Barton <ideasman42@gmail.com>
Mon, 11 Feb 2019 12:24:09 +0000 (23:24 +1100)
These operations are for handling preference related tasks so move into
into a preferences file.

Operators still need to be renamed.

release/scripts/startup/bl_operators/__init__.py
release/scripts/startup/bl_operators/userpref.py [new file with mode: 0644]
release/scripts/startup/bl_operators/wm.py

index 4d90386..bb92e07 100644 (file)
@@ -35,14 +35,15 @@ _modules = [
     "mask",
     "mesh",
     "node",
-    "object_align",
     "object",
-    "object_randomize_transform",
+    "object_align",
     "object_quick_effects",
+    "object_randomize_transform",
     "presets",
     "rigidbody",
     "screen_play_rendered_anim",
     "sequencer",
+    "userpref",
     "uvcalc_follow_active",
     "uvcalc_lightmap",
     "uvcalc_smart_project",
diff --git a/release/scripts/startup/bl_operators/userpref.py b/release/scripts/startup/bl_operators/userpref.py
new file mode 100644 (file)
index 0000000..0728993
--- /dev/null
@@ -0,0 +1,1090 @@
+# ##### 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 #####
+
+# <pep8 compliant>
+
+# TODO, use PREFERENCES_OT_* prefix for operators.
+
+import bpy
+from bpy.types import (
+    Operator,
+    OperatorFileListElement
+)
+from bpy.props import (
+    BoolProperty,
+    EnumProperty,
+    IntProperty,
+    StringProperty,
+    CollectionProperty,
+)
+
+from bpy.app.translations import pgettext_tip as tip_
+
+
+class WM_OT_keyconfig_activate(Operator):
+    bl_idname = "wm.keyconfig_activate"
+    bl_label = "Activate Keyconfig"
+
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+    )
+
+    def execute(self, context):
+        if bpy.utils.keyconfig_set(self.filepath, report=self.report):
+            return {'FINISHED'}
+        else:
+            return {'CANCELLED'}
+
+
+class WM_OT_copy_prev_settings(Operator):
+    """Copy settings from previous version"""
+    bl_idname = "wm.copy_prev_settings"
+    bl_label = "Copy Previous Settings"
+
+    @staticmethod
+    def previous_version():
+        ver = bpy.app.version
+        ver_old = ((ver[0] * 100) + ver[1]) - 1
+        return ver_old // 100, ver_old % 100
+
+    @staticmethod
+    def _old_path():
+        ver = bpy.app.version
+        ver_old = ((ver[0] * 100) + ver[1]) - 1
+        return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
+
+    @staticmethod
+    def _new_path():
+        return bpy.utils.resource_path('USER')
+
+    @classmethod
+    def poll(cls, context):
+        import os
+
+        old = cls._old_path()
+        new = cls._new_path()
+
+        # Disable operator in case config path is overriden with environment
+        # variable. That case has no automatic per-version configuration.
+        userconfig_path = os.path.normpath(bpy.utils.user_resource('CONFIG'))
+        new_userconfig_path = os.path.normpath(os.path.join(new, "config"))
+        if userconfig_path != new_userconfig_path:
+            return False
+
+        # Enable operator if new config path does not exist yet.
+        if os.path.isdir(old) and not os.path.isdir(new):
+            return True
+
+        # Enable operator also if there are no new user preference yet.
+        old_userpref = os.path.join(old, "config", "userpref.blend")
+        new_userpref = os.path.join(new, "config", "userpref.blend")
+        return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
+
+    def execute(self, context):
+        import shutil
+
+        shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
+
+        # reload recent-files.txt
+        bpy.ops.wm.read_history()
+
+        # don't loose users work if they open the splash later.
+        if bpy.data.is_saved is bpy.data.is_dirty is False:
+            bpy.ops.wm.read_homefile()
+        else:
+            self.report({'INFO'}, "Reload Start-Up file to restore settings")
+
+        return {'FINISHED'}
+
+
+class WM_OT_keyconfig_test(Operator):
+    """Test key-config for conflicts"""
+    bl_idname = "wm.keyconfig_test"
+    bl_label = "Test Key Configuration for Conflicts"
+
+    def execute(self, context):
+        from bpy_extras import keyconfig_utils
+
+        wm = context.window_manager
+        kc = wm.keyconfigs.default
+
+        if keyconfig_utils.keyconfig_test(kc):
+            print("CONFLICT")
+
+        return {'FINISHED'}
+
+
+class WM_OT_keyconfig_import(Operator):
+    """Import key configuration from a python script"""
+    bl_idname = "wm.keyconfig_import"
+    bl_label = "Import Key Configuration..."
+
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+        default="keymap.py",
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_text: BoolProperty(
+        name="Filter text",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_python: BoolProperty(
+        name="Filter python",
+        default=True,
+        options={'HIDDEN'},
+    )
+    keep_original: BoolProperty(
+        name="Keep original",
+        description="Keep original file after copying to configuration folder",
+        default=True,
+    )
+
+    def execute(self, context):
+        import os
+        from os.path import basename
+        import shutil
+
+        if not self.filepath:
+            self.report({'ERROR'}, "Filepath not set")
+            return {'CANCELLED'}
+
+        config_name = basename(self.filepath)
+
+        path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
+        path = os.path.join(path, config_name)
+
+        try:
+            if self.keep_original:
+                shutil.copy(self.filepath, path)
+            else:
+                shutil.move(self.filepath, path)
+        except Exception as ex:
+            self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
+            return {'CANCELLED'}
+
+        # sneaky way to check we're actually running the code.
+        if bpy.utils.keyconfig_set(path, report=self.report):
+            return {'FINISHED'}
+        else:
+            return {'CANCELLED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+# This operator is also used by interaction presets saving - AddPresetBase
+
+
+class WM_OT_keyconfig_export(Operator):
+    """Export key configuration to a python script"""
+    bl_idname = "wm.keyconfig_export"
+    bl_label = "Export Key Configuration..."
+
+    all: BoolProperty(
+        name="All Keymaps",
+        default=False,
+        description="Write all keymaps (not just user modified)",
+    )
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+        default="keymap.py",
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_text: BoolProperty(
+        name="Filter text",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_python: BoolProperty(
+        name="Filter python",
+        default=True,
+        options={'HIDDEN'},
+    )
+
+    def execute(self, context):
+        from bl_keymap_utils.io import keyconfig_export_as_data
+
+        if not self.filepath:
+            raise Exception("Filepath not set")
+
+        if not self.filepath.endswith(".py"):
+            self.filepath += ".py"
+
+        wm = context.window_manager
+
+        keyconfig_export_as_data(
+            wm,
+            wm.keyconfigs.active,
+            self.filepath,
+            all_keymaps=self.all,
+        )
+
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+
+class WM_OT_keymap_restore(Operator):
+    """Restore key map(s)"""
+    bl_idname = "wm.keymap_restore"
+    bl_label = "Restore Key Map(s)"
+
+    all: BoolProperty(
+        name="All Keymaps",
+        description="Restore all keymaps to default",
+    )
+
+    def execute(self, context):
+        wm = context.window_manager
+
+        if self.all:
+            for km in wm.keyconfigs.user.keymaps:
+                km.restore_to_default()
+        else:
+            km = context.keymap
+            km.restore_to_default()
+
+        return {'FINISHED'}
+
+
+class WM_OT_keyitem_restore(Operator):
+    """Restore key map item"""
+    bl_idname = "wm.keyitem_restore"
+    bl_label = "Restore Key Map Item"
+
+    item_id: IntProperty(
+        name="Item Identifier",
+        description="Identifier of the item to remove",
+    )
+
+    @classmethod
+    def poll(cls, context):
+        keymap = getattr(context, "keymap", None)
+        return keymap
+
+    def execute(self, context):
+        km = context.keymap
+        kmi = km.keymap_items.from_id(self.item_id)
+
+        if (not kmi.is_user_defined) and kmi.is_user_modified:
+            km.restore_item_to_default(kmi)
+
+        return {'FINISHED'}
+
+
+class WM_OT_keyitem_add(Operator):
+    """Add key map item"""
+    bl_idname = "wm.keyitem_add"
+    bl_label = "Add Key Map Item"
+
+    def execute(self, context):
+        km = context.keymap
+
+        if km.is_modal:
+            km.keymap_items.new_modal("", 'A', 'PRESS')
+        else:
+            km.keymap_items.new("none", 'A', 'PRESS')
+
+        # clear filter and expand keymap so we can see the newly added item
+        if context.space_data.filter_text != "":
+            context.space_data.filter_text = ""
+            km.show_expanded_items = True
+            km.show_expanded_children = True
+
+        return {'FINISHED'}
+
+
+class WM_OT_keyitem_remove(Operator):
+    """Remove key map item"""
+    bl_idname = "wm.keyitem_remove"
+    bl_label = "Remove Key Map Item"
+
+    item_id: IntProperty(
+        name="Item Identifier",
+        description="Identifier of the item to remove",
+    )
+
+    @classmethod
+    def poll(cls, context):
+        return hasattr(context, "keymap")
+
+    def execute(self, context):
+        km = context.keymap
+        kmi = km.keymap_items.from_id(self.item_id)
+        km.keymap_items.remove(kmi)
+        return {'FINISHED'}
+
+
+class WM_OT_keyconfig_remove(Operator):
+    """Remove key config"""
+    bl_idname = "wm.keyconfig_remove"
+    bl_label = "Remove Key Config"
+
+    @classmethod
+    def poll(cls, context):
+        wm = context.window_manager
+        keyconf = wm.keyconfigs.active
+        return keyconf and keyconf.is_user_defined
+
+    def execute(self, context):
+        wm = context.window_manager
+        keyconfig = wm.keyconfigs.active
+        wm.keyconfigs.remove(keyconfig)
+        return {'FINISHED'}
+
+
+# -----------------------------------------------------------------------------
+# Add-on Operators
+
+class WM_OT_addon_enable(Operator):
+    """Enable an add-on"""
+    bl_idname = "wm.addon_enable"
+    bl_label = "Enable Add-on"
+
+    module: StringProperty(
+        name="Module",
+        description="Module name of the add-on to enable",
+    )
+
+    def execute(self, context):
+        import addon_utils
+
+        err_str = ""
+
+        def err_cb(ex):
+            import traceback
+            nonlocal err_str
+            err_str = traceback.format_exc()
+            print(err_str)
+
+        mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
+
+        if mod:
+            info = addon_utils.module_bl_info(mod)
+
+            info_ver = info.get("blender", (0, 0, 0))
+
+            if info_ver > bpy.app.version:
+                self.report(
+                    {'WARNING'},
+                    "This script was written Blender "
+                    "version %d.%d.%d and might not "
+                    "function (correctly), "
+                    "though it is enabled" %
+                    info_ver
+                )
+            return {'FINISHED'}
+        else:
+
+            if err_str:
+                self.report({'ERROR'}, err_str)
+
+            return {'CANCELLED'}
+
+
+class WM_OT_addon_disable(Operator):
+    """Disable an add-on"""
+    bl_idname = "wm.addon_disable"
+    bl_label = "Disable Add-on"
+
+    module: StringProperty(
+        name="Module",
+        description="Module name of the add-on to disable",
+    )
+
+    def execute(self, context):
+        import addon_utils
+
+        err_str = ""
+
+        def err_cb(ex):
+            import traceback
+            nonlocal err_str
+            err_str = traceback.format_exc()
+            print(err_str)
+
+        addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
+
+        if err_str:
+            self.report({'ERROR'}, err_str)
+
+        return {'FINISHED'}
+
+
+class WM_OT_theme_install(Operator):
+    """Load and apply a Blender XML theme file"""
+    bl_idname = "wm.theme_install"
+    bl_label = "Install Theme..."
+
+    overwrite: BoolProperty(
+        name="Overwrite",
+        description="Remove existing theme file if exists",
+        default=True,
+    )
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_glob: StringProperty(
+        default="*.xml",
+        options={'HIDDEN'},
+    )
+
+    def execute(self, context):
+        import os
+        import shutil
+        import traceback
+
+        xmlfile = self.filepath
+
+        path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
+
+        if not path_themes:
+            self.report({'ERROR'}, "Failed to get themes path")
+            return {'CANCELLED'}
+
+        path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
+
+        if not self.overwrite:
+            if os.path.exists(path_dest):
+                self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
+                return {'CANCELLED'}
+
+        try:
+            shutil.copyfile(xmlfile, path_dest)
+            bpy.ops.script.execute_preset(
+                filepath=path_dest,
+                menu_idname="USERPREF_MT_interface_theme_presets",
+            )
+
+        except:
+            traceback.print_exc()
+            return {'CANCELLED'}
+
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+
+class WM_OT_addon_refresh(Operator):
+    """Scan add-on directories for new modules"""
+    bl_idname = "wm.addon_refresh"
+    bl_label = "Refresh"
+
+    def execute(self, context):
+        import addon_utils
+
+        addon_utils.modules_refresh()
+
+        return {'FINISHED'}
+
+
+# Note: shares some logic with WM_OT_app_template_install
+# but not enough to de-duplicate. Fixed here may apply to both.
+class WM_OT_addon_install(Operator):
+    """Install an add-on"""
+    bl_idname = "wm.addon_install"
+    bl_label = "Install Add-on from File..."
+
+    overwrite: BoolProperty(
+        name="Overwrite",
+        description="Remove existing add-ons with the same ID",
+        default=True,
+    )
+    target: EnumProperty(
+        name="Target Path",
+        items=(('DEFAULT', "Default", ""),
+               ('PREFS', "User Prefs", "")),
+    )
+
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_python: BoolProperty(
+        name="Filter python",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_glob: StringProperty(
+        default="*.py;*.zip",
+        options={'HIDDEN'},
+    )
+
+    def execute(self, context):
+        import addon_utils
+        import traceback
+        import zipfile
+        import shutil
+        import os
+
+        pyfile = self.filepath
+
+        if self.target == 'DEFAULT':
+            # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
+            path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
+        else:
+            path_addons = context.preferences.filepaths.script_directory
+            if path_addons:
+                path_addons = os.path.join(path_addons, "addons")
+
+        if not path_addons:
+            self.report({'ERROR'}, "Failed to get add-ons path")
+            return {'CANCELLED'}
+
+        if not os.path.isdir(path_addons):
+            try:
+                os.makedirs(path_addons, exist_ok=True)
+            except:
+                traceback.print_exc()
+
+        # Check if we are installing from a target path,
+        # doing so causes 2+ addons of same name or when the same from/to
+        # location is used, removal of the file!
+        addon_path = ""
+        pyfile_dir = os.path.dirname(pyfile)
+        for addon_path in addon_utils.paths():
+            if os.path.samefile(pyfile_dir, addon_path):
+                self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
+                return {'CANCELLED'}
+        del addon_path
+        del pyfile_dir
+        # done checking for exceptional case
+
+        addons_old = {mod.__name__ for mod in addon_utils.modules()}
+
+        # check to see if the file is in compressed format (.zip)
+        if zipfile.is_zipfile(pyfile):
+            try:
+                file_to_extract = zipfile.ZipFile(pyfile, 'r')
+            except:
+                traceback.print_exc()
+                return {'CANCELLED'}
+
+            if self.overwrite:
+                for f in file_to_extract.namelist():
+                    module_filesystem_remove(path_addons, f)
+            else:
+                for f in file_to_extract.namelist():
+                    path_dest = os.path.join(path_addons, os.path.basename(f))
+                    if os.path.exists(path_dest):
+                        self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
+                        return {'CANCELLED'}
+
+            try:  # extract the file to "addons"
+                file_to_extract.extractall(path_addons)
+            except:
+                traceback.print_exc()
+                return {'CANCELLED'}
+
+        else:
+            path_dest = os.path.join(path_addons, os.path.basename(pyfile))
+
+            if self.overwrite:
+                module_filesystem_remove(path_addons, os.path.basename(pyfile))
+            elif os.path.exists(path_dest):
+                self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
+                return {'CANCELLED'}
+
+            # if not compressed file just copy into the addon path
+            try:
+                shutil.copyfile(pyfile, path_dest)
+            except:
+                traceback.print_exc()
+                return {'CANCELLED'}
+
+        addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
+        addons_new.discard("modules")
+
+        # disable any addons we may have enabled previously and removed.
+        # this is unlikely but do just in case. bug [#23978]
+        for new_addon in addons_new:
+            addon_utils.disable(new_addon, default_set=True)
+
+        # possible the zip contains multiple addons, we could disallow this
+        # but for now just use the first
+        for mod in addon_utils.modules(refresh=False):
+            if mod.__name__ in addons_new:
+                info = addon_utils.module_bl_info(mod)
+
+                # show the newly installed addon.
+                context.window_manager.addon_filter = 'All'
+                context.window_manager.addon_search = info["name"]
+                break
+
+        # in case a new module path was created to install this addon.
+        bpy.utils.refresh_script_paths()
+
+        # print message
+        msg = (
+            tip_("Modules Installed (%s) from %r into %r") %
+            (", ".join(sorted(addons_new)), pyfile, path_addons)
+        )
+        print(msg)
+        self.report({'INFO'}, msg)
+
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+
+class WM_OT_addon_remove(Operator):
+    """Delete the add-on from the file system"""
+    bl_idname = "wm.addon_remove"
+    bl_label = "Remove Add-on"
+
+    module: StringProperty(
+        name="Module",
+        description="Module name of the add-on to remove",
+    )
+
+    @staticmethod
+    def path_from_addon(module):
+        import os
+        import addon_utils
+
+        for mod in addon_utils.modules():
+            if mod.__name__ == module:
+                filepath = mod.__file__
+                if os.path.exists(filepath):
+                    if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
+                        return os.path.dirname(filepath), True
+                    else:
+                        return filepath, False
+        return None, False
+
+    def execute(self, context):
+        import addon_utils
+        import os
+
+        path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
+        if path is None:
+            self.report({'WARNING'}, "Add-on path %r could not be found" % path)
+            return {'CANCELLED'}
+
+        # in case its enabled
+        addon_utils.disable(self.module, default_set=True)
+
+        import shutil
+        if isdir and (not os.path.islink(path)):
+            shutil.rmtree(path)
+        else:
+            os.remove(path)
+
+        addon_utils.modules_refresh()
+
+        context.area.tag_redraw()
+        return {'FINISHED'}
+
+    # lame confirmation check
+    def draw(self, context):
+        self.layout.label(text="Remove Add-on: %r?" % self.module)
+        path, _isdir = WM_OT_addon_remove.path_from_addon(self.module)
+        self.layout.label(text="Path: %r" % path)
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        return wm.invoke_props_dialog(self, width=600)
+
+
+class WM_OT_addon_expand(Operator):
+    """Display information and preferences for this add-on"""
+    bl_idname = "wm.addon_expand"
+    bl_label = ""
+    bl_options = {'INTERNAL'}
+
+    module: StringProperty(
+        name="Module",
+        description="Module name of the add-on to expand",
+    )
+
+    def execute(self, context):
+        import addon_utils
+
+        module_name = self.module
+
+        mod = addon_utils.addons_fake_modules.get(module_name)
+        if mod is not None:
+            info = addon_utils.module_bl_info(mod)
+            info["show_expanded"] = not info["show_expanded"]
+
+        return {'FINISHED'}
+
+
+class WM_OT_addon_userpref_show(Operator):
+    """Show add-on preferences"""
+    bl_idname = "wm.addon_userpref_show"
+    bl_label = ""
+    bl_options = {'INTERNAL'}
+
+    module: StringProperty(
+        name="Module",
+        description="Module name of the add-on to expand",
+    )
+
+    def execute(self, context):
+        import addon_utils
+
+        module_name = self.module
+
+        _modules = addon_utils.modules(refresh=False)
+        mod = addon_utils.addons_fake_modules.get(module_name)
+        if mod is not None:
+            info = addon_utils.module_bl_info(mod)
+            info["show_expanded"] = True
+
+            context.preferences.active_section = 'ADDONS'
+            context.window_manager.addon_filter = 'All'
+            context.window_manager.addon_search = info["name"]
+            bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
+
+        return {'FINISHED'}
+
+
+# Note: shares some logic with WM_OT_addon_install
+# but not enough to de-duplicate. Fixes here may apply to both.
+class WM_OT_app_template_install(Operator):
+    """Install an application-template"""
+    bl_idname = "wm.app_template_install"
+    bl_label = "Install Template from File..."
+
+    overwrite: BoolProperty(
+        name="Overwrite",
+        description="Remove existing template with the same ID",
+        default=True,
+    )
+
+    filepath: StringProperty(
+        subtype='FILE_PATH',
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_glob: StringProperty(
+        default="*.zip",
+        options={'HIDDEN'},
+    )
+
+    def execute(self, context):
+        import traceback
+        import zipfile
+        import os
+
+        filepath = self.filepath
+
+        path_app_templates = bpy.utils.user_resource(
+            'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
+            create=True,
+        )
+
+        if not path_app_templates:
+            self.report({'ERROR'}, "Failed to get add-ons path")
+            return {'CANCELLED'}
+
+        if not os.path.isdir(path_app_templates):
+            try:
+                os.makedirs(path_app_templates, exist_ok=True)
+            except:
+                traceback.print_exc()
+
+        app_templates_old = set(os.listdir(path_app_templates))
+
+        # check to see if the file is in compressed format (.zip)
+        if zipfile.is_zipfile(filepath):
+            try:
+                file_to_extract = zipfile.ZipFile(filepath, 'r')
+            except:
+                traceback.print_exc()
+                return {'CANCELLED'}
+
+            if self.overwrite:
+                for f in file_to_extract.namelist():
+                    module_filesystem_remove(path_app_templates, f)
+            else:
+                for f in file_to_extract.namelist():
+                    path_dest = os.path.join(path_app_templates, os.path.basename(f))
+                    if os.path.exists(path_dest):
+                        self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
+                        return {'CANCELLED'}
+
+            try:  # extract the file to "bl_app_templates_user"
+                file_to_extract.extractall(path_app_templates)
+            except:
+                traceback.print_exc()
+                return {'CANCELLED'}
+
+        else:
+            # Only support installing zipfiles
+            self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
+            return {'CANCELLED'}
+
+        app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
+
+        # in case a new module path was created to install this addon.
+        bpy.utils.refresh_script_paths()
+
+        # print message
+        msg = (
+            tip_("Template Installed (%s) from %r into %r") %
+            (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
+        )
+        print(msg)
+        self.report({'INFO'}, msg)
+
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+
+# -----------------------------------------------------------------------------
+# Studio Light Operations
+
+class WM_OT_studiolight_install(Operator):
+    """Install a user defined studio light"""
+    bl_idname = "wm.studiolight_install"
+    bl_label = "Install Custom Studio Light"
+
+    files: CollectionProperty(
+        name="File Path",
+        type=OperatorFileListElement,
+    )
+    directory: StringProperty(
+        subtype='DIR_PATH',
+    )
+    filter_folder: BoolProperty(
+        name="Filter folders",
+        default=True,
+        options={'HIDDEN'},
+    )
+    filter_glob: StringProperty(
+        default="*.png;*.jpg;*.hdr;*.exr",
+        options={'HIDDEN'},
+    )
+    type: EnumProperty(
+        items=(
+            ('MATCAP', "MatCap", ""),
+            ('WORLD', "World", ""),
+            ('STUDIO', "Studio", ""),
+        )
+    )
+
+    def execute(self, context):
+        import os
+        import shutil
+        prefs = context.preferences
+
+        path_studiolights = os.path.join("studiolights", self.type.lower())
+        path_studiolights = bpy.utils.user_resource('DATAFILES', path_studiolights, create=True)
+        if not path_studiolights:
+            self.report({'ERROR'}, "Failed to create Studio Light path")
+            return {'CANCELLED'}
+
+        for e in self.files:
+            shutil.copy(os.path.join(self.directory, e.name), path_studiolights)
+            prefs.studio_lights.load(os.path.join(path_studiolights, e.name), self.type)
+
+        # print message
+        msg = (
+            tip_("StudioLight Installed %r into %r") %
+            (", ".join(e.name for e in self.files), path_studiolights)
+        )
+        print(msg)
+        self.report({'INFO'}, msg)
+        return {'FINISHED'}
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+
+        if self.type == 'STUDIO':
+            self.filter_glob = "*.sl"
+
+        wm.fileselect_add(self)
+        return {'RUNNING_MODAL'}
+
+
+class WM_OT_studiolight_new(Operator):
+    """Save custom studio light from the studio light editor settings"""
+    bl_idname = 'wm.studiolight_new'
+    bl_label = "Save custom Studio light"
+
+    filename: StringProperty(
+        name="Name",
+        default="StudioLight",
+    )
+
+    ask_overide = False
+
+    def execute(self, context):
+        import os
+        prefs = context.preferences
+        wm = context.window_manager
+        filename = bpy.path.ensure_ext(self.filename, ".sl")
+
+        path_studiolights = bpy.utils.user_resource('DATAFILES', os.path.join("studiolights", "studio"), create=True)
+        if not path_studiolights:
+            self.report({'ERROR'}, "Failed to get Studio Light path")
+            return {'CANCELLED'}
+
+        filepath_final = os.path.join(path_studiolights, filename)
+        if os.path.isfile(filepath_final):
+            if not self.ask_overide:
+                self.ask_overide = True
+                return wm.invoke_props_dialog(self, width=600)
+            else:
+                for studio_light in prefs.studio_lights:
+                    if studio_light.name == filename:
+                        bpy.ops.wm.studiolight_uninstall(index=studio_light.index)
+
+        prefs.studio_lights.new(path=filepath_final)
+
+        # print message
+        msg = (
+            tip_("StudioLight Installed %r into %r") %
+            (self.filename, str(path_studiolights))
+        )
+        print(msg)
+        self.report({'INFO'}, msg)
+        return {'FINISHED'}
+
+    def draw(self, context):
+        layout = self.layout
+        if self.ask_overide:
+            layout.label(text="Warning, file already exists. Overwrite existing file?")
+        else:
+            layout.prop(self, "filename")
+
+    def invoke(self, context, event):
+        wm = context.window_manager
+        return wm.invoke_props_dialog(self, width=600)
+
+
+class WM_OT_studiolight_uninstall(Operator):
+    """Delete Studio Light"""
+    bl_idname = 'wm.studiolight_uninstall'
+    bl_label = "Uninstall Studio Light"
+    index: bpy.props.IntProperty()
+
+    def execute(self, context):
+        import os
+        prefs = context.preferences
+        for studio_light in prefs.studio_lights:
+            if studio_light.index == self.index:
+                for filepath in (
+                        studio_light.path,
+                        studio_light.path_irr_cache,
+                        studio_light.path_sh_cache,
+                ):
+                    if filepath and os.path.exists(filepath):
+                        os.unlink(filepath)
+                prefs.studio_lights.remove(studio_light)
+                return {'FINISHED'}
+        return {'CANCELLED'}
+
+
+class WM_OT_studiolight_copy_settings(Operator):
+    """Copy Studio Light settings to the Studio light editor"""
+    bl_idname = 'wm.studiolight_copy_settings'
+    bl_label = "Copy Studio Light settings"
+    index: bpy.props.IntProperty()
+
+    def execute(self, context):
+        prefs = context.preferences
+        system = prefs.system
+        for studio_light in prefs.studio_lights:
+            if studio_light.index == self.index:
+                system.light_ambient = studio_light.light_ambient
+                for sys_light, light in zip(system.solid_lights, studio_light.solid_lights):
+                    sys_light.use = light.use
+                    sys_light.diffuse_color = light.diffuse_color
+                    sys_light.specular_color = light.specular_color
+                    sys_light.smooth = light.smooth
+                    sys_light.direction = light.direction
+                return {'FINISHED'}
+        return {'CANCELLED'}
+
+
+class WM_OT_studiolight_userpref_show(Operator):
+    """Show light preferences"""
+    bl_idname = "wm.studiolight_userpref_show"
+    bl_label = ""
+    bl_options = {'INTERNAL'}
+
+    def execute(self, context):
+        context.preferences.active_section = 'LIGHTS'
+        bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
+        return {'FINISHED'}
+
+
+classes = (
+    WM_OT_addon_disable,
+    WM_OT_addon_enable,
+    WM_OT_addon_expand,
+    WM_OT_addon_install,
+    WM_OT_addon_refresh,
+    WM_OT_addon_remove,
+    WM_OT_addon_userpref_show,
+    WM_OT_app_template_install,
+    WM_OT_copy_prev_settings,
+    WM_OT_keyconfig_activate,
+    WM_OT_keyconfig_export,
+    WM_OT_keyconfig_import,
+    WM_OT_keyconfig_remove,
+    WM_OT_keyconfig_test,
+    WM_OT_keyitem_add,
+    WM_OT_keyitem_remove,
+    WM_OT_keyitem_restore,
+    WM_OT_keymap_restore,
+    WM_OT_theme_install,
+    WM_OT_studiolight_install,
+    WM_OT_studiolight_new,
+    WM_OT_studiolight_uninstall,
+    WM_OT_studiolight_copy_settings,
+    WM_OT_studiolight_userpref_show,
+)
index 8d663c5..3c6e819 100644 (file)
@@ -22,7 +22,6 @@ import bpy
 from bpy.types import (
     Menu,
     Operator,
-    OperatorFileListElement
 )
 from bpy.props import (
     BoolProperty,
@@ -30,11 +29,8 @@ from bpy.props import (
     FloatProperty,
     IntProperty,
     StringProperty,
-    CollectionProperty,
 )
 
-from bpy.app.translations import pgettext_tip as tip_
-
 # FIXME, we need a way to detect key repeat events.
 # unfortunately checking event previous values isn't reliable.
 use_toolbar_release_hack = True
@@ -1413,21 +1409,6 @@ class WM_OT_properties_remove(Operator):
         return {'FINISHED'}
 
 
-class WM_OT_keyconfig_activate(Operator):
-    bl_idname = "wm.keyconfig_activate"
-    bl_label = "Activate Keyconfig"
-
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-    )
-
-    def execute(self, context):
-        if bpy.utils.keyconfig_set(self.filepath, report=self.report):
-            return {'FINISHED'}
-        else:
-            return {'CANCELLED'}
-
-
 class WM_OT_sysinfo(Operator):
     """Generate system information, saved into a text file"""
 
@@ -1456,315 +1437,6 @@ class WM_OT_sysinfo(Operator):
         return {'RUNNING_MODAL'}
 
 
-class WM_OT_copy_prev_settings(Operator):
-    """Copy settings from previous version"""
-    bl_idname = "wm.copy_prev_settings"
-    bl_label = "Copy Previous Settings"
-
-    @staticmethod
-    def previous_version():
-        ver = bpy.app.version
-        ver_old = ((ver[0] * 100) + ver[1]) - 1
-        return ver_old // 100, ver_old % 100
-
-    @staticmethod
-    def _old_path():
-        ver = bpy.app.version
-        ver_old = ((ver[0] * 100) + ver[1]) - 1
-        return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
-
-    @staticmethod
-    def _new_path():
-        return bpy.utils.resource_path('USER')
-
-    @classmethod
-    def poll(cls, context):
-        import os
-
-        old = cls._old_path()
-        new = cls._new_path()
-
-        # Disable operator in case config path is overriden with environment
-        # variable. That case has no automatic per-version configuration.
-        userconfig_path = os.path.normpath(bpy.utils.user_resource('CONFIG'))
-        new_userconfig_path = os.path.normpath(os.path.join(new, "config"))
-        if userconfig_path != new_userconfig_path:
-            return False
-
-        # Enable operator if new config path does not exist yet.
-        if os.path.isdir(old) and not os.path.isdir(new):
-            return True
-
-        # Enable operator also if there are no new user preference yet.
-        old_userpref = os.path.join(old, "config", "userpref.blend")
-        new_userpref = os.path.join(new, "config", "userpref.blend")
-        return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
-
-    def execute(self, context):
-        import shutil
-
-        shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
-
-        # reload recent-files.txt
-        bpy.ops.wm.read_history()
-
-        # don't loose users work if they open the splash later.
-        if bpy.data.is_saved is bpy.data.is_dirty is False:
-            bpy.ops.wm.read_homefile()
-        else:
-            self.report({'INFO'}, "Reload Start-Up file to restore settings")
-
-        return {'FINISHED'}
-
-
-class WM_OT_keyconfig_test(Operator):
-    """Test key-config for conflicts"""
-    bl_idname = "wm.keyconfig_test"
-    bl_label = "Test Key Configuration for Conflicts"
-
-    def execute(self, context):
-        from bpy_extras import keyconfig_utils
-
-        wm = context.window_manager
-        kc = wm.keyconfigs.default
-
-        if keyconfig_utils.keyconfig_test(kc):
-            print("CONFLICT")
-
-        return {'FINISHED'}
-
-
-class WM_OT_keyconfig_import(Operator):
-    """Import key configuration from a python script"""
-    bl_idname = "wm.keyconfig_import"
-    bl_label = "Import Key Configuration..."
-
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-        default="keymap.py",
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_text: BoolProperty(
-        name="Filter text",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_python: BoolProperty(
-        name="Filter python",
-        default=True,
-        options={'HIDDEN'},
-    )
-    keep_original: BoolProperty(
-        name="Keep original",
-        description="Keep original file after copying to configuration folder",
-        default=True,
-    )
-
-    def execute(self, context):
-        import os
-        from os.path import basename
-        import shutil
-
-        if not self.filepath:
-            self.report({'ERROR'}, "Filepath not set")
-            return {'CANCELLED'}
-
-        config_name = basename(self.filepath)
-
-        path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
-        path = os.path.join(path, config_name)
-
-        try:
-            if self.keep_original:
-                shutil.copy(self.filepath, path)
-            else:
-                shutil.move(self.filepath, path)
-        except Exception as ex:
-            self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
-            return {'CANCELLED'}
-
-        # sneaky way to check we're actually running the code.
-        if bpy.utils.keyconfig_set(path, report=self.report):
-            return {'FINISHED'}
-        else:
-            return {'CANCELLED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
-# This operator is also used by interaction presets saving - AddPresetBase
-
-
-class WM_OT_keyconfig_export(Operator):
-    """Export key configuration to a python script"""
-    bl_idname = "wm.keyconfig_export"
-    bl_label = "Export Key Configuration..."
-
-    all: BoolProperty(
-        name="All Keymaps",
-        default=False,
-        description="Write all keymaps (not just user modified)",
-    )
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-        default="keymap.py",
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_text: BoolProperty(
-        name="Filter text",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_python: BoolProperty(
-        name="Filter python",
-        default=True,
-        options={'HIDDEN'},
-    )
-
-    def execute(self, context):
-        from bl_keymap_utils.io import keyconfig_export_as_data
-
-        if not self.filepath:
-            raise Exception("Filepath not set")
-
-        if not self.filepath.endswith(".py"):
-            self.filepath += ".py"
-
-        wm = context.window_manager
-
-        keyconfig_export_as_data(
-            wm,
-            wm.keyconfigs.active,
-            self.filepath,
-            all_keymaps=self.all,
-        )
-
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
-
-class WM_OT_keymap_restore(Operator):
-    """Restore key map(s)"""
-    bl_idname = "wm.keymap_restore"
-    bl_label = "Restore Key Map(s)"
-
-    all: BoolProperty(
-        name="All Keymaps",
-        description="Restore all keymaps to default",
-    )
-
-    def execute(self, context):
-        wm = context.window_manager
-
-        if self.all:
-            for km in wm.keyconfigs.user.keymaps:
-                km.restore_to_default()
-        else:
-            km = context.keymap
-            km.restore_to_default()
-
-        return {'FINISHED'}
-
-
-class WM_OT_keyitem_restore(Operator):
-    """Restore key map item"""
-    bl_idname = "wm.keyitem_restore"
-    bl_label = "Restore Key Map Item"
-
-    item_id: IntProperty(
-        name="Item Identifier",
-        description="Identifier of the item to remove",
-    )
-
-    @classmethod
-    def poll(cls, context):
-        keymap = getattr(context, "keymap", None)
-        return keymap
-
-    def execute(self, context):
-        km = context.keymap
-        kmi = km.keymap_items.from_id(self.item_id)
-
-        if (not kmi.is_user_defined) and kmi.is_user_modified:
-            km.restore_item_to_default(kmi)
-
-        return {'FINISHED'}
-
-
-class WM_OT_keyitem_add(Operator):
-    """Add key map item"""
-    bl_idname = "wm.keyitem_add"
-    bl_label = "Add Key Map Item"
-
-    def execute(self, context):
-        km = context.keymap
-
-        if km.is_modal:
-            km.keymap_items.new_modal("", 'A', 'PRESS')
-        else:
-            km.keymap_items.new("none", 'A', 'PRESS')
-
-        # clear filter and expand keymap so we can see the newly added item
-        if context.space_data.filter_text != "":
-            context.space_data.filter_text = ""
-            km.show_expanded_items = True
-            km.show_expanded_children = True
-
-        return {'FINISHED'}
-
-
-class WM_OT_keyitem_remove(Operator):
-    """Remove key map item"""
-    bl_idname = "wm.keyitem_remove"
-    bl_label = "Remove Key Map Item"
-
-    item_id: IntProperty(
-        name="Item Identifier",
-        description="Identifier of the item to remove",
-    )
-
-    @classmethod
-    def poll(cls, context):
-        return hasattr(context, "keymap")
-
-    def execute(self, context):
-        km = context.keymap
-        kmi = km.keymap_items.from_id(self.item_id)
-        km.keymap_items.remove(kmi)
-        return {'FINISHED'}
-
-
-class WM_OT_keyconfig_remove(Operator):
-    """Remove key config"""
-    bl_idname = "wm.keyconfig_remove"
-    bl_label = "Remove Key Config"
-
-    @classmethod
-    def poll(cls, context):
-        wm = context.window_manager
-        keyconf = wm.keyconfigs.active
-        return keyconf and keyconf.is_user_defined
-
-    def execute(self, context):
-        wm = context.window_manager
-        keyconfig = wm.keyconfigs.active
-        wm.keyconfigs.remove(keyconfig)
-        return {'FINISHED'}
-
 
 class WM_OT_operator_cheat_sheet(Operator):
     """List all the Operators in a text-block, useful for scripting"""
@@ -1795,81 +1467,6 @@ class WM_OT_operator_cheat_sheet(Operator):
 # -----------------------------------------------------------------------------
 # Add-on Operators
 
-class WM_OT_addon_enable(Operator):
-    """Enable an add-on"""
-    bl_idname = "wm.addon_enable"
-    bl_label = "Enable Add-on"
-
-    module: StringProperty(
-        name="Module",
-        description="Module name of the add-on to enable",
-    )
-
-    def execute(self, context):
-        import addon_utils
-
-        err_str = ""
-
-        def err_cb(ex):
-            import traceback
-            nonlocal err_str
-            err_str = traceback.format_exc()
-            print(err_str)
-
-        mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
-
-        if mod:
-            info = addon_utils.module_bl_info(mod)
-
-            info_ver = info.get("blender", (0, 0, 0))
-
-            if info_ver > bpy.app.version:
-                self.report(
-                    {'WARNING'},
-                    "This script was written Blender "
-                    "version %d.%d.%d and might not "
-                    "function (correctly), "
-                    "though it is enabled" %
-                    info_ver
-                )
-            return {'FINISHED'}
-        else:
-
-            if err_str:
-                self.report({'ERROR'}, err_str)
-
-            return {'CANCELLED'}
-
-
-class WM_OT_addon_disable(Operator):
-    """Disable an add-on"""
-    bl_idname = "wm.addon_disable"
-    bl_label = "Disable Add-on"
-
-    module: StringProperty(
-        name="Module",
-        description="Module name of the add-on to disable",
-    )
-
-    def execute(self, context):
-        import addon_utils
-
-        err_str = ""
-
-        def err_cb(ex):
-            import traceback
-            nonlocal err_str
-            err_str = traceback.format_exc()
-            print(err_str)
-
-        addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
-
-        if err_str:
-            self.report({'ERROR'}, err_str)
-
-        return {'FINISHED'}
-
-
 class WM_OT_owner_enable(Operator):
     """Enable workspace owner ID"""
     bl_idname = "wm.owner_enable"
@@ -1901,449 +1498,6 @@ class WM_OT_owner_disable(Operator):
         return {'FINISHED'}
 
 
-class WM_OT_theme_install(Operator):
-    """Load and apply a Blender XML theme file"""
-    bl_idname = "wm.theme_install"
-    bl_label = "Install Theme..."
-
-    overwrite: BoolProperty(
-        name="Overwrite",
-        description="Remove existing theme file if exists",
-        default=True,
-    )
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_glob: StringProperty(
-        default="*.xml",
-        options={'HIDDEN'},
-    )
-
-    def execute(self, context):
-        import os
-        import shutil
-        import traceback
-
-        xmlfile = self.filepath
-
-        path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
-
-        if not path_themes:
-            self.report({'ERROR'}, "Failed to get themes path")
-            return {'CANCELLED'}
-
-        path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
-
-        if not self.overwrite:
-            if os.path.exists(path_dest):
-                self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
-                return {'CANCELLED'}
-
-        try:
-            shutil.copyfile(xmlfile, path_dest)
-            bpy.ops.script.execute_preset(
-                filepath=path_dest,
-                menu_idname="USERPREF_MT_interface_theme_presets",
-            )
-
-        except:
-            traceback.print_exc()
-            return {'CANCELLED'}
-
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
-
-class WM_OT_addon_refresh(Operator):
-    """Scan add-on directories for new modules"""
-    bl_idname = "wm.addon_refresh"
-    bl_label = "Refresh"
-
-    def execute(self, context):
-        import addon_utils
-
-        addon_utils.modules_refresh()
-
-        return {'FINISHED'}
-
-
-# Note: shares some logic with WM_OT_app_template_install
-# but not enough to de-duplicate. Fixed here may apply to both.
-class WM_OT_addon_install(Operator):
-    """Install an add-on"""
-    bl_idname = "wm.addon_install"
-    bl_label = "Install Add-on from File..."
-
-    overwrite: BoolProperty(
-        name="Overwrite",
-        description="Remove existing add-ons with the same ID",
-        default=True,
-    )
-    target: EnumProperty(
-        name="Target Path",
-        items=(('DEFAULT', "Default", ""),
-               ('PREFS', "User Prefs", "")),
-    )
-
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_python: BoolProperty(
-        name="Filter python",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_glob: StringProperty(
-        default="*.py;*.zip",
-        options={'HIDDEN'},
-    )
-
-    def execute(self, context):
-        import addon_utils
-        import traceback
-        import zipfile
-        import shutil
-        import os
-
-        pyfile = self.filepath
-
-        if self.target == 'DEFAULT':
-            # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
-            path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
-        else:
-            path_addons = context.preferences.filepaths.script_directory
-            if path_addons:
-                path_addons = os.path.join(path_addons, "addons")
-
-        if not path_addons:
-            self.report({'ERROR'}, "Failed to get add-ons path")
-            return {'CANCELLED'}
-
-        if not os.path.isdir(path_addons):
-            try:
-                os.makedirs(path_addons, exist_ok=True)
-            except:
-                traceback.print_exc()
-
-        # Check if we are installing from a target path,
-        # doing so causes 2+ addons of same name or when the same from/to
-        # location is used, removal of the file!
-        addon_path = ""
-        pyfile_dir = os.path.dirname(pyfile)
-        for addon_path in addon_utils.paths():
-            if os.path.samefile(pyfile_dir, addon_path):
-                self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
-                return {'CANCELLED'}
-        del addon_path
-        del pyfile_dir
-        # done checking for exceptional case
-
-        addons_old = {mod.__name__ for mod in addon_utils.modules()}
-
-        # check to see if the file is in compressed format (.zip)
-        if zipfile.is_zipfile(pyfile):
-            try:
-                file_to_extract = zipfile.ZipFile(pyfile, 'r')
-            except:
-                traceback.print_exc()
-                return {'CANCELLED'}
-
-            if self.overwrite:
-                for f in file_to_extract.namelist():
-                    module_filesystem_remove(path_addons, f)
-            else:
-                for f in file_to_extract.namelist():
-                    path_dest = os.path.join(path_addons, os.path.basename(f))
-                    if os.path.exists(path_dest):
-                        self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
-                        return {'CANCELLED'}
-
-            try:  # extract the file to "addons"
-                file_to_extract.extractall(path_addons)
-            except:
-                traceback.print_exc()
-                return {'CANCELLED'}
-
-        else:
-            path_dest = os.path.join(path_addons, os.path.basename(pyfile))
-
-            if self.overwrite:
-                module_filesystem_remove(path_addons, os.path.basename(pyfile))
-            elif os.path.exists(path_dest):
-                self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
-                return {'CANCELLED'}
-
-            # if not compressed file just copy into the addon path
-            try:
-                shutil.copyfile(pyfile, path_dest)
-            except:
-                traceback.print_exc()
-                return {'CANCELLED'}
-
-        addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
-        addons_new.discard("modules")
-
-        # disable any addons we may have enabled previously and removed.
-        # this is unlikely but do just in case. bug [#23978]
-        for new_addon in addons_new:
-            addon_utils.disable(new_addon, default_set=True)
-
-        # possible the zip contains multiple addons, we could disallow this
-        # but for now just use the first
-        for mod in addon_utils.modules(refresh=False):
-            if mod.__name__ in addons_new:
-                info = addon_utils.module_bl_info(mod)
-
-                # show the newly installed addon.
-                context.window_manager.addon_filter = 'All'
-                context.window_manager.addon_search = info["name"]
-                break
-
-        # in case a new module path was created to install this addon.
-        bpy.utils.refresh_script_paths()
-
-        # print message
-        msg = (
-            tip_("Modules Installed (%s) from %r into %r") %
-            (", ".join(sorted(addons_new)), pyfile, path_addons)
-        )
-        print(msg)
-        self.report({'INFO'}, msg)
-
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
-
-class WM_OT_addon_remove(Operator):
-    """Delete the add-on from the file system"""
-    bl_idname = "wm.addon_remove"
-    bl_label = "Remove Add-on"
-
-    module: StringProperty(
-        name="Module",
-        description="Module name of the add-on to remove",
-    )
-
-    @staticmethod
-    def path_from_addon(module):
-        import os
-        import addon_utils
-
-        for mod in addon_utils.modules():
-            if mod.__name__ == module:
-                filepath = mod.__file__
-                if os.path.exists(filepath):
-                    if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
-                        return os.path.dirname(filepath), True
-                    else:
-                        return filepath, False
-        return None, False
-
-    def execute(self, context):
-        import addon_utils
-        import os
-
-        path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
-        if path is None:
-            self.report({'WARNING'}, "Add-on path %r could not be found" % path)
-            return {'CANCELLED'}
-
-        # in case its enabled
-        addon_utils.disable(self.module, default_set=True)
-
-        import shutil
-        if isdir and (not os.path.islink(path)):
-            shutil.rmtree(path)
-        else:
-            os.remove(path)
-
-        addon_utils.modules_refresh()
-
-        context.area.tag_redraw()
-        return {'FINISHED'}
-
-    # lame confirmation check
-    def draw(self, context):
-        self.layout.label(text="Remove Add-on: %r?" % self.module)
-        path, _isdir = WM_OT_addon_remove.path_from_addon(self.module)
-        self.layout.label(text="Path: %r" % path)
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        return wm.invoke_props_dialog(self, width=600)
-
-
-class WM_OT_addon_expand(Operator):
-    """Display information and preferences for this add-on"""
-    bl_idname = "wm.addon_expand"
-    bl_label = ""
-    bl_options = {'INTERNAL'}
-
-    module: StringProperty(
-        name="Module",
-        description="Module name of the add-on to expand",
-    )
-
-    def execute(self, context):
-        import addon_utils
-
-        module_name = self.module
-
-        mod = addon_utils.addons_fake_modules.get(module_name)
-        if mod is not None:
-            info = addon_utils.module_bl_info(mod)
-            info["show_expanded"] = not info["show_expanded"]
-
-        return {'FINISHED'}
-
-
-class WM_OT_addon_userpref_show(Operator):
-    """Show add-on preferences"""
-    bl_idname = "wm.addon_userpref_show"
-    bl_label = ""
-    bl_options = {'INTERNAL'}
-
-    module: StringProperty(
-        name="Module",
-        description="Module name of the add-on to expand",
-    )
-
-    def execute(self, context):
-        import addon_utils
-
-        module_name = self.module
-
-        _modules = addon_utils.modules(refresh=False)
-        mod = addon_utils.addons_fake_modules.get(module_name)
-        if mod is not None:
-            info = addon_utils.module_bl_info(mod)
-            info["show_expanded"] = True
-
-            context.preferences.active_section = 'ADDONS'
-            context.window_manager.addon_filter = 'All'
-            context.window_manager.addon_search = info["name"]
-            bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
-
-        return {'FINISHED'}
-
-
-# Note: shares some logic with WM_OT_addon_install
-# but not enough to de-duplicate. Fixes here may apply to both.
-class WM_OT_app_template_install(Operator):
-    """Install an application-template"""
-    bl_idname = "wm.app_template_install"
-    bl_label = "Install Template from File..."
-
-    overwrite: BoolProperty(
-        name="Overwrite",
-        description="Remove existing template with the same ID",
-        default=True,
-    )
-
-    filepath: StringProperty(
-        subtype='FILE_PATH',
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_glob: StringProperty(
-        default="*.zip",
-        options={'HIDDEN'},
-    )
-
-    def execute(self, context):
-        import traceback
-        import zipfile
-        import os
-
-        filepath = self.filepath
-
-        path_app_templates = bpy.utils.user_resource(
-            'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
-            create=True,
-        )
-
-        if not path_app_templates:
-            self.report({'ERROR'}, "Failed to get add-ons path")
-            return {'CANCELLED'}
-
-        if not os.path.isdir(path_app_templates):
-            try:
-                os.makedirs(path_app_templates, exist_ok=True)
-            except:
-                traceback.print_exc()
-
-        app_templates_old = set(os.listdir(path_app_templates))
-
-        # check to see if the file is in compressed format (.zip)
-        if zipfile.is_zipfile(filepath):
-            try:
-                file_to_extract = zipfile.ZipFile(filepath, 'r')
-            except:
-                traceback.print_exc()
-                return {'CANCELLED'}
-
-            if self.overwrite:
-                for f in file_to_extract.namelist():
-                    module_filesystem_remove(path_app_templates, f)
-            else:
-                for f in file_to_extract.namelist():
-                    path_dest = os.path.join(path_app_templates, os.path.basename(f))
-                    if os.path.exists(path_dest):
-                        self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
-                        return {'CANCELLED'}
-
-            try:  # extract the file to "bl_app_templates_user"
-                file_to_extract.extractall(path_app_templates)
-            except:
-                traceback.print_exc()
-                return {'CANCELLED'}
-
-        else:
-            # Only support installing zipfiles
-            self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
-            return {'CANCELLED'}
-
-        app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
-
-        # in case a new module path was created to install this addon.
-        bpy.utils.refresh_script_paths()
-
-        # print message
-        msg = (
-            tip_("Template Installed (%s) from %r into %r") %
-            (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
-        )
-        print(msg)
-        self.report({'INFO'}, msg)
-
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
 
 class WM_OT_tool_set_by_name(Operator):
     """Set the tool by name (for keymaps)"""
@@ -2430,183 +1584,6 @@ class WM_OT_toolbar(Operator):
         return {'FINISHED'}
 
 
-# Studio Light operations
-class WM_OT_studiolight_install(Operator):
-    """Install a user defined studio light"""
-    bl_idname = "wm.studiolight_install"
-    bl_label = "Install Custom Studio Light"
-
-    files: CollectionProperty(
-        name="File Path",
-        type=OperatorFileListElement,
-    )
-    directory: StringProperty(
-        subtype='DIR_PATH',
-    )
-    filter_folder: BoolProperty(
-        name="Filter folders",
-        default=True,
-        options={'HIDDEN'},
-    )
-    filter_glob: StringProperty(
-        default="*.png;*.jpg;*.hdr;*.exr",
-        options={'HIDDEN'},
-    )
-    type: EnumProperty(
-        items=(
-            ('MATCAP', "MatCap", ""),
-            ('WORLD', "World", ""),
-            ('STUDIO', "Studio", ""),
-        )
-    )
-
-    def execute(self, context):
-        import os
-        import shutil
-        prefs = context.preferences
-
-        path_studiolights = os.path.join("studiolights", self.type.lower())
-        path_studiolights = bpy.utils.user_resource('DATAFILES', path_studiolights, create=True)
-        if not path_studiolights:
-            self.report({'ERROR'}, "Failed to create Studio Light path")
-            return {'CANCELLED'}
-
-        for e in self.files:
-            shutil.copy(os.path.join(self.directory, e.name), path_studiolights)
-            prefs.studio_lights.load(os.path.join(path_studiolights, e.name), self.type)
-
-        # print message
-        msg = (
-            tip_("StudioLight Installed %r into %r") %
-            (", ".join(e.name for e in self.files), path_studiolights)
-        )
-        print(msg)
-        self.report({'INFO'}, msg)
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-
-        if self.type == 'STUDIO':
-            self.filter_glob = "*.sl"
-
-        wm.fileselect_add(self)
-        return {'RUNNING_MODAL'}
-
-
-class WM_OT_studiolight_new(Operator):
-    """Save custom studio light from the studio light editor settings"""
-    bl_idname = 'wm.studiolight_new'
-    bl_label = "Save custom Studio light"
-
-    filename: StringProperty(
-        name="Name",
-        default="StudioLight",
-    )
-
-    ask_overide = False
-
-    def execute(self, context):
-        import os
-        prefs = context.preferences
-        wm = context.window_manager
-        filename = bpy.path.ensure_ext(self.filename, ".sl")
-
-        path_studiolights = bpy.utils.user_resource('DATAFILES', os.path.join("studiolights", "studio"), create=True)
-        if not path_studiolights:
-            self.report({'ERROR'}, "Failed to get Studio Light path")
-            return {'CANCELLED'}
-
-        filepath_final = os.path.join(path_studiolights, filename)
-        if os.path.isfile(filepath_final):
-            if not self.ask_overide:
-                self.ask_overide = True
-                return wm.invoke_props_dialog(self, width=600)
-            else:
-                for studio_light in prefs.studio_lights:
-                    if studio_light.name == filename:
-                        bpy.ops.wm.studiolight_uninstall(index=studio_light.index)
-
-        prefs.studio_lights.new(path=filepath_final)
-
-        # print message
-        msg = (
-            tip_("StudioLight Installed %r into %r") %
-            (self.filename, str(path_studiolights))
-        )
-        print(msg)
-        self.report({'INFO'}, msg)
-        return {'FINISHED'}
-
-    def draw(self, context):
-        layout = self.layout
-        if self.ask_overide:
-            layout.label(text="Warning, file already exists. Overwrite existing file?")
-        else:
-            layout.prop(self, "filename")
-
-    def invoke(self, context, event):
-        wm = context.window_manager
-        return wm.invoke_props_dialog(self, width=600)
-
-
-class WM_OT_studiolight_uninstall(Operator):
-    """Delete Studio Light"""
-    bl_idname = 'wm.studiolight_uninstall'
-    bl_label = "Uninstall Studio Light"
-    index: bpy.props.IntProperty()
-
-    def execute(self, context):
-        import os
-        prefs = context.preferences
-        for studio_light in prefs.studio_lights:
-            if studio_light.index == self.index:
-                for filepath in (
-                        studio_light.path,
-                        studio_light.path_irr_cache,
-                        studio_light.path_sh_cache,
-                ):
-                    if filepath and os.path.exists(filepath):
-                        os.unlink(filepath)
-                prefs.studio_lights.remove(studio_light)
-                return {'FINISHED'}
-        return {'CANCELLED'}
-
-
-class WM_OT_studiolight_copy_settings(Operator):
-    """Copy Studio Light settings to the Studio light editor"""
-    bl_idname = 'wm.studiolight_copy_settings'
-    bl_label = "Copy Studio Light settings"
-    index: bpy.props.IntProperty()
-
-    def execute(self, context):
-        prefs = context.preferences
-        system = prefs.system
-        for studio_light in prefs.studio_lights:
-            if studio_light.index == self.index:
-                system.light_ambient = studio_light.light_ambient
-                for sys_light, light in zip(system.solid_lights, studio_light.solid_lights):
-                    sys_light.use = light.use
-                    sys_light.diffuse_color = light.diffuse_color
-                    sys_light.specular_color = light.specular_color
-                    sys_light.smooth = light.smooth
-                    sys_light.direction = light.direction
-                return {'FINISHED'}
-        return {'CANCELLED'}
-
-
-class WM_OT_studiolight_userpref_show(Operator):
-    """Show light preferences"""
-    bl_idname = "wm.studiolight_userpref_show"
-    bl_label = ""
-    bl_options = {'INTERNAL'}
-
-    def execute(self, context):
-        context.preferences.active_section = 'LIGHTS'
-        bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
-        return {'FINISHED'}
-
-
 class WM_MT_splash(Menu):
     bl_label = "Splash"
 
@@ -2804,14 +1781,6 @@ class WM_OT_drop_blend_file(Operator):
 
 
 classes = (
-    WM_OT_addon_disable,
-    WM_OT_addon_enable,
-    WM_OT_addon_expand,
-    WM_OT_addon_install,
-    WM_OT_addon_refresh,
-    WM_OT_addon_remove,
-    WM_OT_addon_userpref_show,
-    WM_OT_app_template_install,
     WM_OT_context_collection_boolean_set,
     WM_OT_context_cycle_array,
     WM_OT_context_cycle_enum,
@@ -2830,19 +1799,9 @@ classes = (
     WM_OT_context_set_value,
     WM_OT_context_toggle,
     WM_OT_context_toggle_enum,
-    WM_OT_copy_prev_settings,
     WM_OT_doc_view,
     WM_OT_doc_view_manual,
     WM_OT_drop_blend_file,
-    WM_OT_keyconfig_activate,
-    WM_OT_keyconfig_export,
-    WM_OT_keyconfig_import,
-    WM_OT_keyconfig_remove,
-    WM_OT_keyconfig_test,
-    WM_OT_keyitem_add,
-    WM_OT_keyitem_remove,
-    WM_OT_keyitem_restore,
-    WM_OT_keymap_restore,
     WM_OT_operator_cheat_sheet,
     WM_OT_operator_pie_enum,
     WM_OT_path_open,
@@ -2851,15 +1810,9 @@ classes = (
     WM_OT_properties_edit,
     WM_OT_properties_remove,
     WM_OT_sysinfo,
-    WM_OT_theme_install,
     WM_OT_owner_disable,
     WM_OT_owner_enable,
     WM_OT_url_open,
-    WM_OT_studiolight_install,
-    WM_OT_studiolight_new,
-    WM_OT_studiolight_uninstall,
-    WM_OT_studiolight_copy_settings,
-    WM_OT_studiolight_userpref_show,
     WM_OT_tool_set_by_name,
     WM_OT_toolbar,
     WM_MT_splash,