WM: Application Templates
authorCampbell Barton <ideasman42@gmail.com>
Fri, 24 Mar 2017 22:29:51 +0000 (09:29 +1100)
committerCampbell Barton <ideasman42@gmail.com>
Fri, 24 Mar 2017 23:04:04 +0000 (10:04 +1100)
This adds the ability to switch between different application-configurations
without interfering with Blender's normal operation.

This commit doesn't include any templates,
so its mostly to allow collaboration for the Blender 101 project
and other custom configurations.

Application templates can be installed & selected from the file menu.

Other details:

- The `bl_app_template_utils` module handles template activation
  (similar to `addon_utils`).
- The `bl_app_override` module is a general module
  to assist scripts overriding parts of Blender in reversible way.

See docs:
https://docs.blender.org/manual/en/dev/advanced/app_templates.html

See patch: D2565

17 files changed:
release/scripts/modules/bl_app_override/__init__.py [new file with mode: 0644]
release/scripts/modules/bl_app_override/helpers.py [new file with mode: 0644]
release/scripts/modules/bl_app_template_utils.py [new file with mode: 0644]
release/scripts/modules/bpy/utils/__init__.py
release/scripts/startup/bl_operators/wm.py
release/scripts/startup/bl_ui/space_info.py
release/scripts/startup/bl_ui/space_userpref.py
source/blender/blenkernel/BKE_appdir.h
source/blender/blenkernel/BKE_blender.h
source/blender/blenkernel/intern/appdir.c
source/blender/blenkernel/intern/blender.c
source/blender/makesdna/DNA_userdef_types.h
source/blender/makesrna/intern/rna_userdef.c
source/blender/windowmanager/intern/wm_files.c
source/blender/windowmanager/intern/wm_init_exit.c
source/blender/windowmanager/intern/wm_operators.c
source/blender/windowmanager/wm_files.h

diff --git a/release/scripts/modules/bl_app_override/__init__.py b/release/scripts/modules/bl_app_override/__init__.py
new file mode 100644 (file)
index 0000000..efd7c52
--- /dev/null
@@ -0,0 +1,200 @@
+# ##### 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-80 compliant>
+
+"""
+Module to manage overriding various parts of Blender.
+
+Intended for use with 'app_templates', though it can be used from anywhere.
+"""
+
+
+# TODO, how to check these aren't from add-ons.
+# templates might need to un-register while filtering.
+def class_filter(cls_parent, **kw):
+    whitelist = kw.pop("whitelist", None)
+    blacklist = kw.pop("blacklist", None)
+    kw_items = tuple(kw.items())
+    for cls in cls_parent.__subclasses__():
+        # same as is_registered()
+        if "bl_rna" in cls.__dict__:
+            if blacklist is not None and cls.__name__ in blacklist:
+                continue
+            if ((whitelist is not None and cls.__name__ is whitelist) or
+                    all((getattr(cls, attr) in expect) for attr, expect in kw_items)):
+                yield cls
+
+
+def ui_draw_filter_register(
+    *,
+    ui_ignore_classes=None,
+    ui_ignore_operator=None,
+    ui_ignore_property=None,
+    ui_ignore_menu=None,
+    ui_ignore_label=None,
+):
+    import bpy
+
+    UILayout = bpy.types.UILayout
+
+    if ui_ignore_classes is None:
+        ui_ignore_classes = (
+            bpy.types.Panel,
+            bpy.types.Menu,
+            bpy.types.Header,
+        )
+
+    class OperatorProperties_Fake:
+        pass
+
+    class UILayout_Fake(bpy.types.UILayout):
+        __slots__ = ()
+
+        def __getattribute__(self, attr):
+            # ensure we always pass down UILayout_Fake instances
+            if attr in {"row", "split", "column", "box", "column_flow"}:
+                real_func = UILayout.__getattribute__(self, attr)
+
+                def dummy_func(*args, **kw):
+                    # print("wrapped", attr)
+                    ret = real_func(*args, **kw)
+                    return UILayout_Fake(ret)
+                return dummy_func
+
+            elif attr in {"operator", "operator_menu_enum", "operator_enum"}:
+                if ui_ignore_operator is None:
+                    return UILayout.__getattribute__(self, attr)
+
+                real_func = UILayout.__getattribute__(self, attr)
+
+                def dummy_func(*args, **kw):
+                    # print("wrapped", attr)
+                    if not ui_ignore_operator(args[0]):
+                        ret = real_func(*args, **kw)
+                    else:
+                        # UILayout.__getattribute__(self, "label")()
+                        # may need to be set
+                        ret = OperatorProperties_Fake()
+                    return ret
+                return dummy_func
+
+            elif attr in {"prop", "prop_enum"}:
+                if ui_ignore_property is None:
+                    return UILayout.__getattribute__(self, attr)
+
+                real_func = UILayout.__getattribute__(self, attr)
+
+                def dummy_func(*args, **kw):
+                    # print("wrapped", attr)
+                    if not ui_ignore_property(args[0].__class__.__name__, args[1]):
+                        ret = real_func(*args, **kw)
+                    else:
+                        ret = None
+                    return ret
+                return dummy_func
+
+            elif attr == "menu":
+                if ui_ignore_menu is None:
+                    return UILayout.__getattribute__(self, attr)
+
+                real_func = UILayout.__getattribute__(self, attr)
+
+                def dummy_func(*args, **kw):
+                    # print("wrapped", attr)
+                    if not ui_ignore_menu(args[0]):
+                        ret = real_func(*args, **kw)
+                    else:
+                        ret = None
+                    return ret
+                return dummy_func
+
+            elif attr == "label":
+                if ui_ignore_label is None:
+                    return UILayout.__getattribute__(self, attr)
+
+                real_func = UILayout.__getattribute__(self, attr)
+
+                def dummy_func(*args, **kw):
+                    # print("wrapped", attr)
+                    if not ui_ignore_label(args[0] if args else kw.get("text", "")):
+                        ret = real_func(*args, **kw)
+                    else:
+                        # ret = real_func()
+                        ret = None
+                    return ret
+                return dummy_func
+            else:
+                return UILayout.__getattribute__(self, attr)
+            # print(self, attr)
+
+        def operator(*args, **kw):
+            return super().operator(*args, **kw)
+
+    def draw_override(func_orig, self_real, context):
+        # simple, no wrapping
+        # return func_orig(self_wrap, context)
+
+        class Wrapper(self_real.__class__):
+            __slots__ = ()
+            def __getattribute__(self, attr):
+                if attr == "layout":
+                    return UILayout_Fake(self_real.layout)
+                else:
+                    cls = super()
+                    try:
+                        return cls.__getattr__(self, attr)
+                    except AttributeError:
+                        # class variable
+                        try:
+                            return getattr(cls, attr)
+                        except AttributeError:
+                            # for preset bl_idname access
+                            return getattr(UILayout(self), attr)
+
+            @property
+            def layout(self):
+                # print("wrapped")
+                return self_real.layout
+
+        return func_orig(Wrapper(self_real), context)
+
+    ui_ignore_store = []
+
+    for cls in ui_ignore_classes:
+        for subcls in list(cls.__subclasses__()):
+            if "draw" in subcls.__dict__:  # don't want to get parents draw()
+
+                def replace_draw():
+                    # function also serves to hold draw_old in a local name-space
+                    draw_orig = subcls.draw
+
+                    def draw(self, context):
+                        return draw_override(draw_orig, self, context)
+                    subcls.draw = draw
+
+                ui_ignore_store.append((subcls, "draw", subcls.draw))
+
+                replace_draw()
+
+    return ui_ignore_store
+
+
+def ui_draw_filter_unregister(ui_ignore_store):
+    for (obj, attr, value) in ui_ignore_store:
+        setattr(obj, attr, value)
diff --git a/release/scripts/modules/bl_app_override/helpers.py b/release/scripts/modules/bl_app_override/helpers.py
new file mode 100644 (file)
index 0000000..981039e
--- /dev/null
@@ -0,0 +1,167 @@
+# ##### 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-80 compliant>
+
+# -----------------------------------------------------------------------------
+# AppOverrideState
+
+
+class AppOverrideState:
+    """
+    Utility class to encapsulate overriding the application state
+    so that settings can be restored afterwards.
+    """
+    __slots__ = (
+        # setup_classes
+        "_class_store",
+        # setup_ui_ignore
+        "_ui_ignore_store",
+        # setup_addons
+        "_addon_store",
+    )
+
+    # ---------
+    # Callbacks
+    #
+    # Set as None, to make it simple to check if they're being overridden.
+
+    # setup/teardown classes
+    class_ignore = None
+
+    # setup/teardown ui_ignore
+    ui_ignore_classes = None
+    ui_ignore_operator = None
+    ui_ignore_property = None
+    ui_ignore_menu = None
+    ui_ignore_label = None
+
+    addon_paths = None
+    addons = None
+
+    # End callbacks
+
+    def __init__(self):
+        self._class_store = None
+        self._addon_store = None
+        self._ui_ignore_store = None
+
+    def _setup_classes(self):
+        import bpy
+        assert(self._class_store is None)
+        self._class_store = self.class_ignore()
+        from bpy.utils import unregister_class
+        for cls in self._class_store:
+            unregister_class(cls)
+
+    def _teardown_classes(self):
+        assert(self._class_store is not None)
+
+        from bpy.utils import register_class
+        for cls in self._class_store:
+            register_class(cls)
+        self._class_store = None
+
+    def _setup_ui_ignore(self):
+        import bl_app_override
+
+        self._ui_ignore_store = bl_app_override.ui_draw_filter_register(
+            ui_ignore_classes=(
+                None if self.ui_ignore_classes is None
+                else self.ui_ignore_classes()
+            ),
+            ui_ignore_operator=self.ui_ignore_operator,
+            ui_ignore_property=self.ui_ignore_property,
+            ui_ignore_menu=self.ui_ignore_menu,
+            ui_ignore_label=self.ui_ignore_label,
+        )
+
+    def _teardown_ui_ignore(self):
+        import bl_app_override
+        bl_app_override.ui_draw_filter_unregister(
+            self._ui_ignore_store
+        )
+        self._ui_ignore_store = None
+
+    def _setup_addons(self):
+        import sys
+        import os
+
+        sys_path = []
+        if self.addon_paths is not None:
+            for path in self.addon_paths():
+                if path not in sys.path:
+                    sys.path.append(path)
+
+        import addon_utils
+        addons = []
+        if self.addons is not None:
+            addons.extend(self.addons())
+            for addon in addons:
+                addon_utils.enable(addon)
+
+        self._addon_store = {
+            "sys_path": sys_path,
+            "addons": addons,
+        }
+
+    def _teardown_addons(self):
+        import sys
+
+        sys_path = self._addon_store["sys_path"]
+        for path in sys_path:
+            # should always succeed, but if not it doesn't matter
+            # (someone else was changing the sys.path), ignore!
+            try:
+                sys.path.remove(path)
+            except:
+                pass
+
+        addons = self._addon_store["addons"]
+        import addon_utils
+        for addon in addons:
+            addon_utils.disable(addon)
+
+        self._addon_store.clear()
+        self._addon_store = None
+
+    def setup(self):
+        if self.class_ignore is not None:
+            self._setup_classes()
+
+        if any((self.addon_paths,
+                self.addons,
+                )):
+            self._setup_addons()
+
+        if any((self.ui_ignore_operator,
+                self.ui_ignore_property,
+                self.ui_ignore_menu,
+                self.ui_ignore_label,
+                )):
+            self._setup_ui_ignore()
+
+    def teardown(self):
+        if self._class_store is not None:
+            self._teardown_classes()
+
+        if self._addon_store is not None:
+            self._teardown_addons()
+
+        if self._ui_ignore_store is not None:
+            self._teardown_ui_ignore()
diff --git a/release/scripts/modules/bl_app_template_utils.py b/release/scripts/modules/bl_app_template_utils.py
new file mode 100644 (file)
index 0000000..b3a4824
--- /dev/null
@@ -0,0 +1,198 @@
+# ##### 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-80 compliant>
+
+"""
+Similar to ``addon_utils``, except we can only have one active at a time.
+
+In most cases users of this module will simply call 'activate'.
+"""
+
+__all__ = (
+    "activate",
+    "import_from_path",
+    "import_from_id",
+    "reset",
+)
+
+import bpy as _bpy
+
+# Normally matches 'user_preferences.app_template_id',
+# but loading new preferences will get us out of sync.
+_app_template = {
+    "id": "",
+}
+
+# instead of sys.modules
+# note that we only ever have one template enabled at a time
+# so it may not seem necessary to use this.
+#
+# However, templates may want to share between each-other,
+# so any loaded modules are stored here?
+#
+# Note that the ID here is the app_template_id , not the modules __name__.
+_modules = {}
+
+
+def _enable(template_id, *, handle_error=None, ignore_not_found=False):
+    import os
+    import sys
+    from bpy_restrict_state import RestrictBlend
+
+    if handle_error is None:
+        def handle_error(ex):
+            import traceback
+            traceback.print_exc()
+
+    # Split registering up into 2 steps so we can undo
+    # if it fails par way through.
+
+    # disable the context, using the context at all is
+    # really bad while loading an template, don't do it!
+    with RestrictBlend():
+
+        # 1) try import
+        try:
+            mod = import_from_id(template_id, ignore_not_found=ignore_not_found)
+            if mod is None:
+                return None
+            mod.__template_enabled__ = False
+            _modules[template_id] = mod
+        except Exception as ex:
+            handle_error(ex)
+            return None
+
+        # 2) try run the modules register function
+        try:
+            mod.register()
+        except Exception as ex:
+            print("Exception in module register(): %r" %
+                  getattr(mod, "__file__", template_id))
+            handle_error(ex)
+            del _modules[template_id]
+            return None
+
+    # * OK loaded successfully! *
+    mod.__template_enabled__ = True
+
+    if _bpy.app.debug_python:
+        print("\tapp_template_utils.enable", mod.__name__)
+
+    return mod
+
+
+def _disable(template_id, *, handle_error=None):
+    """
+    Disables a template by name.
+
+    :arg template_id: The name of the template and module.
+    :type template_id: string
+    :arg handle_error: Called in the case of an error,
+       taking an exception argument.
+    :type handle_error: function
+    """
+    import sys
+
+    if handle_error is None:
+        def handle_error(ex):
+            import traceback
+            traceback.print_exc()
+
+    mod = _modules.get(template_id)
+
+    if mod and getattr(mod, "__template_enabled__", False) is not False:
+        mod.__template_enabled__ = False
+
+        try:
+            mod.unregister()
+        except Exception as ex:
+            print("Exception in module unregister(): %r" %
+                  getattr(mod, "__file__", template_id))
+            handle_error(ex)
+    else:
+        print("\tapp_template_utils.disable: %s not %s." %
+              (template_id, "disabled" if mod is None else "loaded"))
+
+    if _bpy.app.debug_python:
+        print("\tapp_template_utils.disable", template_id)
+
+
+def import_from_path(path, ignore_not_found=False):
+    import os
+    from importlib import import_module
+    base_module, template_id = path.rsplit(os.sep, 2)[-2:]
+    module_name = base_module + "." + template_id
+
+    try:
+        return import_module(module_name)
+    except ModuleNotFoundError as ex:
+        if ignore_not_found and ex.name == module_name:
+            return None
+        raise ex
+
+
+def import_from_id(template_id, ignore_not_found=False):
+    import os
+    path = next(iter(_bpy.utils.app_template_paths(template_id)), None)
+    if path is None:
+        if ignore_not_found:
+            return None
+        else:
+            raise Exception("%r template not found!" % template_id)
+    else:
+        if ignore_not_found:
+            if not os.path.exists(os.path.join(path, "__init__.py")):
+                return None
+        return import_from_path(path, ignore_not_found=ignore_not_found)
+
+
+def activate(template_id=None):
+    template_id_prev = _app_template["id"]
+
+    # not needed but may as well avoid activating same template
+    # ... in fact keep this, it will show errors early on!
+    """
+    if template_id_prev == template_id:
+        return
+    """
+
+    if template_id_prev:
+        _disable(template_id_prev)
+
+    # Disable all addons, afterwards caller must reset.
+    import addon_utils
+    addon_utils.disable_all()
+
+    # ignore_not_found so modules that don't contain scripts don't raise errors
+    mod = _enable(template_id, ignore_not_found=True) if template_id else None
+
+    _app_template["id"] = template_id
+
+
+def reset(*, reload_scripts=False):
+    """
+    Sets default state.
+    """
+    template_id = _bpy.context.user_preferences.app_template
+    if _bpy.app.debug_python:
+        print("bl_app_template_utils.reset('%s')" % template_id)
+
+    # TODO reload_scripts
+
+    activate(template_id)
index 31dd836..65a2f27 100644 (file)
@@ -32,6 +32,7 @@ __all__ = (
     "preset_find",
     "preset_paths",
     "refresh_script_paths",
+    "app_template_paths",
     "register_class",
     "register_module",
     "register_manual_map",
@@ -245,6 +246,12 @@ def load_scripts(reload_scripts=False, refresh_scripts=False):
                     for mod in modules_from_path(path, loaded_modules):
                         test_register(mod)
 
+    # load template (if set)
+    if any(_bpy.utils.app_template_paths()):
+        import bl_app_template_utils
+        bl_app_template_utils.reset(reload_scripts=reload_scripts)
+        del bl_app_template_utils
+
     # deal with addons separately
     _initialize = getattr(_addon_utils, "_initialize", None)
     if _initialize is not None:
@@ -356,6 +363,38 @@ def refresh_script_paths():
             _sys_path_ensure(path)
 
 
+def app_template_paths(subdir=None):
+    """
+    Returns valid application template paths.
+
+    :arg subdir: Optional subdir.
+    :type subdir: string
+    :return: app template paths.
+    :rtype: generator
+    """
+
+    # note: LOCAL, USER, SYSTEM order matches script resolution order.
+    subdir_tuple = (subdir,) if subdir is not None else ()
+
+    path = _os.path.join(*(
+        resource_path('LOCAL'), "scripts", "startup",
+        "bl_app_templates_user", *subdir_tuple))
+    if _os.path.isdir(path):
+        yield path
+    else:
+        path = _os.path.join(*(
+            resource_path('USER'), "scripts", "startup",
+            "bl_app_templates_user", *subdir_tuple))
+        if _os.path.isdir(path):
+            yield path
+
+    path = _os.path.join(*(
+        resource_path('SYSTEM'), "scripts", "startup",
+        "bl_app_templates_system", *subdir_tuple))
+    if _os.path.isdir(path):
+        yield path
+
+
 def preset_paths(subdir):
     """
     Returns a list of paths for a specific preset.
index 42f1e72..5393015 100644 (file)
@@ -130,6 +130,20 @@ def execute_context_assign(self, context):
     return operator_path_undo_return(context, data_path)
 
 
+def module_filesystem_remove(path_base, module_name):
+    import os
+    module_name = os.path.splitext(module_name)[0]
+    for f in os.listdir(path_base):
+        f_base = os.path.splitext(f)[0]
+        if f_base == module_name:
+            f_full = os.path.join(path_base, f)
+
+            if os.path.isdir(f_full):
+                os.rmdir(f_full)
+            else:
+                os.remove(f_full)
+
+
 class BRUSH_OT_active_index_set(Operator):
     """Set active sculpt/paint brush from it's number"""
     bl_idname = "brush.active_index_set"
@@ -1917,10 +1931,12 @@ class WM_OT_addon_refresh(Operator):
         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 from File..."
+    bl_label = "Install Add-on from File..."
 
     overwrite = BoolProperty(
             name="Overwrite",
@@ -1951,20 +1967,6 @@ class WM_OT_addon_install(Operator):
             options={'HIDDEN'},
             )
 
-    @staticmethod
-    def _module_remove(path_addons, module):
-        import os
-        module = os.path.splitext(module)[0]
-        for f in os.listdir(path_addons):
-            f_base = os.path.splitext(f)[0]
-            if f_base == module:
-                f_full = os.path.join(path_addons, f)
-
-                if os.path.isdir(f_full):
-                    os.rmdir(f_full)
-                else:
-                    os.remove(f_full)
-
     def execute(self, context):
         import addon_utils
         import traceback
@@ -2017,7 +2019,7 @@ class WM_OT_addon_install(Operator):
 
             if self.overwrite:
                 for f in file_to_extract.namelist():
-                    WM_OT_addon_install._module_remove(path_addons, f)
+                    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))
@@ -2035,7 +2037,7 @@ class WM_OT_addon_install(Operator):
             path_dest = os.path.join(path_addons, os.path.basename(pyfile))
 
             if self.overwrite:
-                WM_OT_addon_install._module_remove(path_addons, os.path.basename(pyfile))
+                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'}
@@ -2070,7 +2072,10 @@ class WM_OT_addon_install(Operator):
         bpy.utils.refresh_script_paths()
 
         # print message
-        msg = tip_("Modules Installed from %r into %r (%s)") % (pyfile, path_addons, ", ".join(sorted(addons_new)))
+        msg = (
+            tip_("Modules Installed (%s) from %r into %r (%s)") %
+            (", ".join(sorted(addons_new)), pyfile, path_addons)
+        )
         print(msg)
         self.report({'INFO'}, msg)
 
@@ -2164,6 +2169,7 @@ class WM_OT_addon_expand(Operator):
 
         return {'FINISHED'}
 
+
 class WM_OT_addon_userpref_show(Operator):
     "Show add-on user preferences"
     bl_idname = "wm.addon_userpref_show"
@@ -2194,6 +2200,124 @@ class WM_OT_addon_userpref_show(Operator):
         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_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
+
+        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(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_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:
+            path_dest = os.path.join(path_app_templates, os.path.basename(pyfile))
+
+            if self.overwrite:
+                module_filesystem_remove(path_app_templates, 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'}
+
+        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)), pyfile, 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'}
+
+
 classes = (
     BRUSH_OT_active_index_set,
     WM_OT_addon_disable,
@@ -2203,6 +2327,7 @@ classes = (
     WM_OT_addon_refresh,
     WM_OT_addon_remove,
     WM_OT_addon_userpref_show,
+    WM_OT_app_template_install,
     WM_OT_appconfig_activate,
     WM_OT_appconfig_default,
     WM_OT_blenderplayer_start,
@@ -2246,4 +2371,4 @@ classes = (
     WM_OT_sysinfo,
     WM_OT_theme_install,
     WM_OT_url_open,
-)
\ No newline at end of file
+)
index 16ac633..a7b518d 100644 (file)
@@ -127,6 +127,18 @@ class INFO_MT_file(Menu):
         layout.operator("wm.save_homefile", icon='SAVE_PREFS')
         layout.operator("wm.read_factory_settings", icon='LOAD_FACTORY')
 
+        if any(bpy.utils.app_template_paths()):
+            app_template = context.user_preferences.app_template
+            if app_template:
+                layout.operator(
+                    "wm.read_factory_settings",
+                    text="Load Factory Template Settings",
+                    icon='LOAD_FACTORY',
+                ).app_template = app_template
+            del app_template
+
+        layout.menu("USERPREF_MT_app_templates", icon='FILE_BLEND')
+
         layout.separator()
 
         layout.operator_context = 'INVOKE_AREA'
index fe126f6..f4e2cf0 100644 (file)
@@ -90,6 +90,63 @@ class USERPREF_MT_interaction_presets(Menu):
     draw = Menu.draw_preset
 
 
+class USERPREF_MT_app_templates(Menu):
+    bl_label = "Application Templates"
+    preset_subdir = "app_templates"
+
+    def draw_ex(self, context, *, use_splash=False, use_default=False, use_install=False):
+        import os
+
+        layout = self.layout
+
+        # now draw the presets
+        layout.operator_context = 'EXEC_DEFAULT'
+
+        if use_default:
+            props = layout.operator("wm.read_homefile", text="Default")
+            props.use_splash = True
+            props.app_template = ""
+            layout.separator()
+
+        template_paths = bpy.utils.app_template_paths()
+
+        # expand template paths
+        app_templates = []
+        for path in template_paths:
+            for d in os.listdir(path):
+                if d.startswith(("__", ".")):
+                    continue
+                template = os.path.join(path, d)
+                if os.path.isdir(template):
+                    # template_paths_expand.append(template)
+                    app_templates.append(d)
+
+        for d in sorted(app_templates):
+            props = layout.operator(
+                "wm.read_homefile",
+                text=bpy.path.display_name(d),
+            )
+            props.use_splash = True
+            props.app_template = d;
+
+        if use_install:
+            layout.separator()
+            layout.operator_context = 'INVOKE_DEFAULT'
+            props = layout.operator("wm.app_template_install")
+
+
+    def draw(self, context):
+        self.draw_ex(context, use_splash=False, use_default=True, use_install=True)
+
+
+class USERPREF_MT_templates_splash(Menu):
+    bl_label = "Startup Templates"
+    preset_subdir = "templates"
+
+    def draw(self, context):
+        USERPREF_MT_app_templates.draw_ex(self, context, use_splash=True, use_default=True)
+
+
 class USERPREF_MT_appconfigs(Menu):
     bl_label = "AppPresets"
     preset_subdir = "keyconfig"
@@ -110,7 +167,17 @@ class USERPREF_MT_splash(Menu):
 
         split = layout.split()
         row = split.row()
-        row.label("")
+
+        if any(bpy.utils.app_template_paths()):
+            row.label("Template:")
+            template = context.user_preferences.app_template
+            row.menu(
+                "USERPREF_MT_templates_splash",
+                text=bpy.path.display_name(template) if template else "Default",
+            )
+        else:
+            row.label("")
+
         row = split.row()
         row.label("Interaction:")
 
@@ -1485,6 +1552,8 @@ classes = (
     USERPREF_HT_header,
     USERPREF_PT_tabs,
     USERPREF_MT_interaction_presets,
+    USERPREF_MT_templates_splash,
+    USERPREF_MT_app_templates,
     USERPREF_MT_appconfigs,
     USERPREF_MT_splash,
     USERPREF_MT_splash_footer,
index c6587b9..ac8f861 100644 (file)
@@ -33,6 +33,9 @@ const char *BKE_appdir_folder_id_create(const int folder_id, const char *subfold
 const char *BKE_appdir_folder_id_user_notest(const int folder_id, const char *subfolder);
 const char *BKE_appdir_folder_id_version(const int folder_id, const int ver, const bool do_check);
 
+bool BKE_appdir_app_template_any(void);
+bool BKE_appdir_app_template_id_search(const char *app_template, char *path, size_t path_len);
+
 /* Initialize path to program executable */
 void        BKE_appdir_program_path_init(const char *argv0);
 
index 62a15ba..d55926f 100644 (file)
@@ -52,6 +52,8 @@ void BKE_blender_userdef_set_data(struct UserDef *userdef);
 void BKE_blender_userdef_free_data(struct UserDef *userdef);
 void BKE_blender_userdef_refresh(void);
 
+void BKE_blender_userdef_set_app_template(struct UserDef *userdef);
+
 /* set this callback when a UI is running */
 void BKE_blender_callback_test_break_set(void (*func)(void));
 int  BKE_blender_test_break(void);
index 3fb8a14..43fd479 100644 (file)
@@ -683,6 +683,48 @@ bool BKE_appdir_program_python_search(
        return is_found;
 }
 
+static const char *app_template_directory_search[2] = {
+       "startup" SEP_STR "bl_app_templates_user",
+       "startup" SEP_STR "bl_app_templates_system",
+};
+
+static const int app_template_directory_id[2] = {
+       BLENDER_USER_SCRIPTS,
+       BLENDER_SYSTEM_SCRIPTS,
+};
+
+/**
+ * Return true if templates exist
+ */
+bool BKE_appdir_app_template_any(void)
+{
+       char temp_dir[FILE_MAX];
+       for (int i = 0; i < 2; i++) {
+               if (BKE_appdir_folder_id_ex(
+                       app_template_directory_id[i], app_template_directory_search[i],
+                       temp_dir, sizeof(temp_dir)))
+               {
+                       return true;
+               }
+       }
+       return false;
+}
+
+bool BKE_appdir_app_template_id_search(const char *app_template, char *path, size_t path_len)
+{
+       for (int i = 0; i < 2; i++) {
+               char subdir[FILE_MAX];
+               BLI_join_dirfile(subdir, sizeof(subdir), app_template_directory_search[i], app_template);
+               if (BKE_appdir_folder_id_ex(
+                       app_template_directory_id[i], subdir,
+                       path, path_len))
+               {
+                       return true;
+               }
+       }
+       return false;
+}
+
 /**
  * Gets the temp directory when blender first runs.
  * If the default path is not found, use try $TEMP
index f661f18..ceb6410 100644 (file)
@@ -238,6 +238,44 @@ void BKE_blender_userdef_refresh(void)
 
 }
 
+/**
+ * Write U from userdef.
+ * This function defines which settings a template will override for the user preferences.
+ */
+void BKE_blender_userdef_set_app_template(UserDef *userdef)
+{
+       /* TODO:
+        * - keymaps
+        * - various minor settings (add as needed).
+        */
+
+#define LIST_OVERRIDE(id) { \
+       BLI_freelistN(&U.id); \
+       BLI_movelisttolist(&U.id, &userdef->id); \
+} ((void)0)
+
+#define MEMCPY_OVERRIDE(id) \
+       memcpy(U.id, userdef->id, sizeof(U.id));
+
+       /* for some types we need custom free functions */
+       userdef_free_addons(&U);
+       userdef_free_keymaps(&U);
+
+       LIST_OVERRIDE(uistyles);
+       LIST_OVERRIDE(uifonts);
+       LIST_OVERRIDE(themes);
+       LIST_OVERRIDE(addons);
+       LIST_OVERRIDE(user_keymaps);
+
+       MEMCPY_OVERRIDE(light);
+
+       MEMCPY_OVERRIDE(font_path_ui);
+       MEMCPY_OVERRIDE(font_path_ui_mono);
+
+#undef LIST_OVERRIDE
+#undef MEMCPY_OVERRIDE
+}
+
 /* *****************  testing for break ************* */
 
 static void (*blender_test_break_cb)(void) = NULL;
index 73c341e..d76452e 100644 (file)
@@ -473,7 +473,10 @@ typedef struct UserDef {
        char pad2;
        short transopts;
        short menuthreshold1, menuthreshold2;
-       
+
+       /* startup template */
+       char app_template[64];
+
        struct ListBase themes;
        struct ListBase uifonts;
        struct ListBase uistyles;
index 74888bf..7b6eb5f 100644 (file)
@@ -4678,6 +4678,11 @@ void RNA_def_userdef(BlenderRNA *brna)
                                 "Active section of the user preferences shown in the user interface");
        RNA_def_property_update(prop, 0, "rna_userdef_update");
 
+       /* don't expose this directly via the UI, modify via an operator */
+       prop = RNA_def_property(srna, "app_template", PROP_STRING, PROP_NONE);
+       RNA_def_property_string_sdna(prop, NULL, "app_template");
+       RNA_def_property_ui_text(prop, "Application Template", "");
+
        prop = RNA_def_property(srna, "themes", PROP_COLLECTION, PROP_NONE);
        RNA_def_property_collection_sdna(prop, NULL, "themes", NULL);
        RNA_def_property_struct_type(prop, "Theme");
index cff27df..826cf49 100644 (file)
@@ -470,6 +470,10 @@ static void wm_file_read_post(bContext *C, bool is_startup_file)
        if (is_startup_file) {
                /* possible python hasn't been initialized */
                if (CTX_py_init_get(C)) {
+                       /* Only run when we have a template path found. */
+                       if (BKE_appdir_app_template_any()) {
+                               BPY_execute_string(C, "__import__('bl_app_template_utils').reset()");
+                       }
                        /* sync addons, these may have changed from the defaults */
                        BPY_execute_string(C, "__import__('addon_utils').reset_all()");
 
@@ -635,15 +639,23 @@ bool WM_file_read(bContext *C, const char *filepath, ReportList *reports)
  * \param use_factory_settings: Ignore on-disk startup file, use bundled ``datatoc_startup_blend`` instead.
  * Used for "Restore Factory Settings".
  * \param filepath_startup_override: Optional path pointing to an alternative blend file (may be NULL).
+ * \param app_template_override: Template to use instead of the template defined in user-preferences.
+ * When not-null, this is written into the user preferences.
  */
 int wm_homefile_read(
-        bContext *C, ReportList *reports,
-        bool use_factory_settings, const char *filepath_startup_override)
+        bContext *C, ReportList *reports, bool use_factory_settings,
+        const char *filepath_startup_override, const char *app_template_override)
 {
        ListBase wmbase;
+       bool success = false;
+
        char filepath_startup[FILE_MAX];
        char filepath_userdef[FILE_MAX];
-       bool success = false;
+
+       /* When 'app_template' is set: '{BLENDER_USER_CONFIG}/{app_template}' */
+       char app_template_system[FILE_MAX];
+       /* When 'app_template' is set: '{BLENDER_SYSTEM_SCRIPTS}/startup/bl_app_templates_system/{app_template}' */
+       char app_template_config[FILE_MAX];
 
        /* Indicates whether user preferences were really load from memory.
         *
@@ -675,12 +687,14 @@ int wm_homefile_read(
 
        filepath_startup[0] = '\0';
        filepath_userdef[0] = '\0';
+       app_template_system[0] = '\0';
+       app_template_config[0] = '\0';
 
+       const char * const cfgdir = BKE_appdir_folder_id(BLENDER_USER_CONFIG, NULL);
        if (!use_factory_settings) {
-               const char * const cfgdir = BKE_appdir_folder_id(BLENDER_USER_CONFIG, NULL);
                if (cfgdir) {
-                       BLI_make_file_string("/", filepath_startup, cfgdir, BLENDER_STARTUP_FILE);
-                       BLI_make_file_string("/", filepath_userdef, cfgdir, BLENDER_USERPREF_FILE);
+                       BLI_path_join(filepath_startup, sizeof(filepath_startup), cfgdir, BLENDER_STARTUP_FILE, NULL);
+                       BLI_path_join(filepath_userdef, sizeof(filepath_startup), cfgdir, BLENDER_USERPREF_FILE, NULL);
                }
                else {
                        use_factory_settings = true;
@@ -704,7 +718,43 @@ int wm_homefile_read(
                }
        }
 
-       if (!use_factory_settings) {
+       const char *app_template = NULL;
+
+       if (filepath_startup_override != NULL) {
+               /* pass */
+       }
+       else if (app_template_override) {
+               app_template = app_template_override;
+       }
+       else if (!use_factory_settings && U.app_template[0]) {
+               app_template = U.app_template;
+       }
+
+       if (app_template != NULL) {
+               BKE_appdir_app_template_id_search(app_template, app_template_system, sizeof(app_template_system));
+               BLI_path_join(app_template_config, sizeof(app_template_config), cfgdir, app_template, NULL);
+       }
+
+       /* insert template name into startup file */
+       if (app_template != NULL) {
+               /* note that the path is being set even when 'use_factory_settings == true'
+                * this is done so we can load a templates factory-settings */
+               if (!use_factory_settings) {
+                       BLI_path_join(filepath_startup, sizeof(filepath_startup), app_template_config, BLENDER_STARTUP_FILE, NULL);
+                       if (BLI_access(filepath_startup, R_OK) != 0) {
+                               filepath_startup[0] = '\0';
+                       }
+               }
+               else {
+                       filepath_startup[0] = '\0';
+               }
+
+               if (filepath_startup[0] == '\0') {
+                       BLI_path_join(filepath_startup, sizeof(filepath_startup), app_template_system, BLENDER_STARTUP_FILE, NULL);
+               }
+       }
+
+       if (!use_factory_settings || (filepath_startup[0] != '\0')) {
                if (BLI_access(filepath_startup, R_OK) == 0) {
                        success = (BKE_blendfile_read(C, filepath_startup, NULL, skip_flags) != BKE_BLENDFILE_READ_FAIL);
                }
@@ -716,8 +766,8 @@ int wm_homefile_read(
        }
 
        if (success == false && filepath_startup_override && reports) {
+               /* We can not return from here because wm is already reset */
                BKE_reportf(reports, RPT_ERROR, "Could not read '%s'", filepath_startup_override);
-               /*We can not return from here because wm is already reset*/
        }
 
        if (success == false) {
@@ -733,7 +783,45 @@ int wm_homefile_read(
                U.flag |= USER_SCRIPT_AUTOEXEC_DISABLE;
 #endif
        }
-       
+
+       /* Load template preferences,
+        * unlike regular preferences we only use some of the settings,
+        * see: BKE_blender_userdef_set_app_template */
+       if (app_template_system[0] != '\0') {
+               char temp_path[FILE_MAX];
+               temp_path[0] = '\0';
+               if (!use_factory_settings) {
+                       BLI_path_join(temp_path, sizeof(temp_path), app_template_config, BLENDER_USERPREF_FILE, NULL);
+                       if (BLI_access(temp_path, R_OK) != 0) {
+                               temp_path[0] = '\0';
+                       }
+               }
+
+               if (temp_path[0] == '\0') {
+                       BLI_path_join(temp_path, sizeof(temp_path), app_template_system, BLENDER_USERPREF_FILE, NULL);
+               }
+
+               UserDef *userdef_template = NULL;
+               /* just avoids missing file warning */
+               if (BLI_exists(temp_path)) {
+                       userdef_template = BKE_blendfile_userdef_read(temp_path, NULL);
+               }
+               if (userdef_template == NULL) {
+                       /* we need to have preferences load to overwrite preferences from previous template */
+                       userdef_template = BKE_blendfile_userdef_read_from_memory(
+                                       datatoc_startup_blend, datatoc_startup_blend_size, NULL);
+               }
+               if (userdef_template) {
+                       BKE_blender_userdef_set_app_template(userdef_template);
+                       BKE_blender_userdef_free_data(userdef_template);
+                       MEM_freeN(userdef_template);
+               }
+       }
+
+       if (app_template_override) {
+               BLI_strncpy(U.app_template, app_template_override, sizeof(U.app_template));
+       }
+
        /* prevent buggy files that had G_FILE_RELATIVE_REMAP written out by mistake. Screws up autosaves otherwise
         * can remove this eventually, only in a 2.53 and older, now its not written */
        G.fileflags &= ~G_FILE_RELATIVE_REMAP;
@@ -1271,6 +1359,13 @@ static int wm_homefile_write_exec(bContext *C, wmOperator *op)
        char filepath[FILE_MAX];
        int fileflags;
 
+       const char *app_template = U.app_template[0] ? U.app_template : NULL;
+       const char * const cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, app_template);
+       if (cfgdir == NULL) {
+               BKE_report(op->reports, RPT_ERROR, "Unable to create user config path");
+               return OPERATOR_CANCELLED;
+       }
+
        BLI_callback_exec(G.main, NULL, BLI_CB_EVT_SAVE_PRE);
 
        /* check current window and close it if temp */
@@ -1280,7 +1375,8 @@ static int wm_homefile_write_exec(bContext *C, wmOperator *op)
        /* update keymaps in user preferences */
        WM_keyconfig_update(wm);
 
-       BLI_make_file_string("/", filepath, BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, NULL), BLENDER_STARTUP_FILE);
+       BLI_path_join(filepath, sizeof(filepath), cfgdir, BLENDER_STARTUP_FILE, NULL);
+
        printf("trying to save homefile at %s ", filepath);
 
        ED_editors_flush_edits(C, false);
@@ -1358,21 +1454,44 @@ static int wm_userpref_write_exec(bContext *C, wmOperator *op)
 {
        wmWindowManager *wm = CTX_wm_manager(C);
        char filepath[FILE_MAX];
+       const char *cfgdir;
+       bool ok = false;
 
        /* update keymaps in user preferences */
        WM_keyconfig_update(wm);
 
-       BLI_make_file_string("/", filepath, BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, NULL), BLENDER_USERPREF_FILE);
-       printf("trying to save userpref at %s ", filepath);
-
-       if (BKE_blendfile_userdef_write(filepath, op->reports) == 0) {
-               printf("fail\n");
-               return OPERATOR_CANCELLED;
+       if ((cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, NULL))) {
+               BLI_path_join(filepath, sizeof(filepath), cfgdir, BLENDER_USERPREF_FILE, NULL);
+               printf("trying to save userpref at %s ", filepath);
+               if (BKE_blendfile_userdef_write(filepath, op->reports) != 0) {
+                       printf("ok\n");
+                       ok = true;
+               }
+               else {
+                       printf("fail\n");
+               }
+       }
+       else {
+               BKE_report(op->reports, RPT_ERROR, "Unable to create userpref path");
        }
 
-       printf("ok\n");
+       if (U.app_template[0] && (cfgdir = BKE_appdir_folder_id_create(BLENDER_USER_CONFIG, U.app_template))) {
+               /* Also save app-template prefs */
+               BLI_path_join(filepath, sizeof(filepath), cfgdir, BLENDER_USERPREF_FILE, NULL);
+               printf("trying to save app-template userpref at %s ", filepath);
+               if (BKE_blendfile_userdef_write(filepath, op->reports) == 0) {
+                       printf("fail\n");
+                       ok = true;
+               }
+               else {
+                       printf("ok\n");
+               }
+       }
+       else if (U.app_template[0]) {
+               BKE_report(op->reports, RPT_ERROR, "Unable to create app-template userpref path");
+       }
 
-       return OPERATOR_FINISHED;
+       return ok ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
 }
 
 void WM_OT_save_userpref(wmOperatorType *ot)
@@ -1433,9 +1552,21 @@ static int wm_homefile_read_exec(bContext *C, wmOperator *op)
                G.fileflags &= ~G_FILE_NO_UI;
        }
 
-       if (wm_homefile_read(C, op->reports, use_factory_settings, filepath)) {
-               /* Load a file but keep the splash open */
-               if (!use_factory_settings && RNA_boolean_get(op->ptr, "use_splash")) {
+       char app_template_buf[sizeof(U.app_template)];
+       const char *app_template;
+       PropertyRNA *prop_app_template = RNA_struct_find_property(op->ptr, "app_template");
+       const bool use_splash = !use_factory_settings && RNA_boolean_get(op->ptr, "use_splash");
+
+       if (prop_app_template && RNA_property_is_set(op->ptr, prop_app_template)) {
+               RNA_property_string_get(op->ptr, prop_app_template, app_template_buf);
+               app_template = app_template_buf;
+       }
+       else {
+               app_template = NULL;
+       }
+
+       if (wm_homefile_read(C, op->reports, use_factory_settings, filepath, app_template)) {
+               if (use_splash) {
                        WM_init_splash(C);
                }
                return OPERATOR_FINISHED;
@@ -1469,17 +1600,26 @@ void WM_OT_read_homefile(wmOperatorType *ot)
        prop = RNA_def_boolean(ot->srna, "use_splash", false, "Splash", "");
        RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
 
+       prop = RNA_def_string(ot->srna, "app_template", "Template", sizeof(U.app_template), "", "");
+       RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
+
        /* omit poll to run in background mode */
 }
 
 void WM_OT_read_factory_settings(wmOperatorType *ot)
 {
+       PropertyRNA *prop;
+
        ot->name = "Load Factory Settings";
        ot->idname = "WM_OT_read_factory_settings";
        ot->description = "Load default file and user preferences";
 
        ot->invoke = WM_operator_confirm;
        ot->exec = wm_homefile_read_exec;
+
+       prop = RNA_def_string(ot->srna, "app_template", "Template", sizeof(U.app_template), "", "");
+       RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
+
        /* omit poll to run in background mode */
 }
 
index be74a0c..5483cf2 100644 (file)
@@ -192,7 +192,7 @@ void WM_init(bContext *C, int argc, const char **argv)
        wm_init_reports(C);
 
        /* get the default database, plus a wm */
-       wm_homefile_read(C, NULL, G.factory_startup, NULL);
+       wm_homefile_read(C, NULL, G.factory_startup, NULL, NULL);
        
 
        BLT_lang_set(NULL);
index 841b63d..39e06cc 100644 (file)
@@ -1762,6 +1762,36 @@ static uiBlock *wm_block_create_splash(bContext *C, ARegion *ar, void *UNUSED(ar
                ibuf = IMB_ibImageFromMemory((unsigned char *)datatoc_splash_png,
                                             datatoc_splash_png_size, IB_rect, NULL, "<splash screen>");
        }
+
+       /* overwrite splash with template image */
+       if (U.app_template[0] != '\0') {
+               ImBuf *ibuf_template = NULL;
+               char splash_filepath[FILE_MAX];
+               char template_directory[FILE_MAX];
+
+               if (BKE_appdir_app_template_id_search(
+                       U.app_template,
+                       template_directory, sizeof(template_directory)))
+               {
+                       BLI_join_dirfile(
+                               splash_filepath, sizeof(splash_filepath), template_directory,
+                               (U.pixelsize == 2) ? "splash_2x.png" : "splash.png");
+                       ibuf_template = IMB_loadiffname(splash_filepath, IB_rect, NULL);
+                       if (ibuf_template) {
+                               const int x_expect = ibuf_template->x;
+                               const int y_expect = 230 * (int)U.pixelsize;
+                               /* don't cover the header text */
+                               if (ibuf_template->x == x_expect && ibuf_template->y == y_expect) {
+                                       memcpy(ibuf->rect, ibuf_template->rect, ibuf_template->x * ibuf_template->y * sizeof(char[4]));
+                               }
+                               else {
+                                       printf("Splash expected %dx%d found %dx%d, ignoring: %s\n",
+                                              x_expect, y_expect, ibuf_template->x, ibuf_template->y, splash_filepath);
+                               }
+                               IMB_freeImBuf(ibuf_template);
+                       }
+               }
+       }
 #endif
 
        block = UI_block_begin(C, ar, "_popup", UI_EMBOSS);
index 048b5a9..15a94d2 100644 (file)
@@ -36,8 +36,8 @@ struct wmOperatorType;
 /* wm_files.c */
 void           wm_history_file_read(void);
 int                    wm_homefile_read(
-        struct bContext *C, struct ReportList *reports,
-        bool use_factory_settings, const char *filepath_startup_override);
+        struct bContext *C, struct ReportList *reports, bool use_factory_settings,
+        const char *filepath_startup_override, const char *app_template_override);
 void           wm_file_read_report(bContext *C);
 
 void        WM_OT_save_homefile(struct wmOperatorType *ot);