diff options
-rw-r--r-- | release/scripts/modules/bl_app_override/__init__.py | 200 | ||||
-rw-r--r-- | release/scripts/modules/bl_app_override/helpers.py | 167 | ||||
-rw-r--r-- | release/scripts/modules/bl_app_template_utils.py | 198 | ||||
-rw-r--r-- | release/scripts/modules/bpy/utils/__init__.py | 39 | ||||
-rw-r--r-- | release/scripts/startup/bl_operators/wm.py | 163 | ||||
-rw-r--r-- | release/scripts/startup/bl_ui/space_info.py | 12 | ||||
-rw-r--r-- | release/scripts/startup/bl_ui/space_userpref.py | 71 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_appdir.h | 3 | ||||
-rw-r--r-- | source/blender/blenkernel/BKE_blender.h | 2 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/appdir.c | 42 | ||||
-rw-r--r-- | source/blender/blenkernel/intern/blender.c | 38 | ||||
-rw-r--r-- | source/blender/makesdna/DNA_userdef_types.h | 5 | ||||
-rw-r--r-- | source/blender/makesrna/intern/rna_userdef.c | 5 | ||||
-rw-r--r-- | source/blender/windowmanager/intern/wm_files.c | 182 | ||||
-rw-r--r-- | source/blender/windowmanager/intern/wm_init_exit.c | 2 | ||||
-rw-r--r-- | source/blender/windowmanager/intern/wm_operators.c | 30 | ||||
-rw-r--r-- | source/blender/windowmanager/wm_files.h | 4 |
17 files changed, 1118 insertions, 45 deletions
diff --git a/release/scripts/modules/bl_app_override/__init__.py b/release/scripts/modules/bl_app_override/__init__.py new file mode 100644 index 00000000000..efd7c525e62 --- /dev/null +++ b/release/scripts/modules/bl_app_override/__init__.py @@ -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 index 00000000000..981039e8ddc --- /dev/null +++ b/release/scripts/modules/bl_app_override/helpers.py @@ -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 index 00000000000..b3a4824aa7b --- /dev/null +++ b/release/scripts/modules/bl_app_template_utils.py @@ -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) diff --git a/release/scripts/modules/bpy/utils/__init__.py b/release/scripts/modules/bpy/utils/__init__.py index 31dd836e034..65a2f278465 100644 --- a/release/scripts/modules/bpy/utils/__init__.py +++ b/release/scripts/modules/bpy/utils/__init__.py @@ -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. diff --git a/release/scripts/startup/bl_operators/wm.py b/release/scripts/startup/bl_operators/wm.py index 42f1e723d1a..5393015229e 100644 --- a/release/scripts/startup/bl_operators/wm.py +++ b/release/scripts/startup/bl_operators/wm.py @@ -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 +) diff --git a/release/scripts/startup/bl_ui/space_info.py b/release/scripts/startup/bl_ui/space_info.py index 16ac6339504..a7b518dfd2e 100644 --- a/release/scripts/startup/bl_ui/space_info.py +++ b/release/scripts/startup/bl_ui/space_info.py @@ -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' diff --git a/release/scripts/startup/bl_ui/space_userpref.py b/release/scripts/startup/bl_ui/space_userpref.py index fe126f6522c..f4e2cf006b2 100644 --- a/release/scripts/startup/bl_ui/space_userpref.py +++ b/release/scripts/startup/bl_ui/space_userpref.py @@ -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, diff --git a/source/blender/blenkernel/BKE_appdir.h b/source/blender/blenkernel/BKE_appdir.h index c6587b94666..ac8f861fa56 100644 --- a/source/blender/blenkernel/BKE_appdir.h +++ b/source/blender/blenkernel/BKE_appdir.h @@ -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); diff --git a/source/blender/blenkernel/BKE_blender.h b/source/blender/blenkernel/BKE_blender.h index 62a15bae153..d55926ffb1e 100644 --- a/source/blender/blenkernel/BKE_blender.h +++ b/source/blender/blenkernel/BKE_blender.h @@ -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); diff --git a/source/blender/blenkernel/intern/appdir.c b/source/blender/blenkernel/intern/appdir.c index 3fb8a147960..43fd47981b1 100644 --- a/source/blender/blenkernel/intern/appdir.c +++ b/source/blender/blenkernel/intern/appdir.c @@ -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 diff --git a/source/blender/blenkernel/intern/blender.c b/source/blender/blenkernel/intern/blender.c index f661f18fbc0..ceb641073e0 100644 --- a/source/blender/blenkernel/intern/blender.c +++ b/source/blender/blenkernel/intern/blender.c @@ -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; diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 73c341e35ba..d76452edb83 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -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; diff --git a/source/blender/makesrna/intern/rna_userdef.c b/source/blender/makesrna/intern/rna_userdef.c index 74888bf4f00..7b6eb5fef47 100644 --- a/source/blender/makesrna/intern/rna_userdef.c +++ b/source/blender/makesrna/intern/rna_userdef.c @@ -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"); diff --git a/source/blender/windowmanager/intern/wm_files.c b/source/blender/windowmanager/intern/wm_files.c index cff27dfc9e8..826cf490f53 100644 --- a/source/blender/windowmanager/intern/wm_files.c +++ b/source/blender/windowmanager/intern/wm_files.c @@ -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 */ } diff --git a/source/blender/windowmanager/intern/wm_init_exit.c b/source/blender/windowmanager/intern/wm_init_exit.c index be74a0c7362..5483cf25e40 100644 --- a/source/blender/windowmanager/intern/wm_init_exit.c +++ b/source/blender/windowmanager/intern/wm_init_exit.c @@ -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); diff --git a/source/blender/windowmanager/intern/wm_operators.c b/source/blender/windowmanager/intern/wm_operators.c index 841b63d4ab8..39e06ccc3c8 100644 --- a/source/blender/windowmanager/intern/wm_operators.c +++ b/source/blender/windowmanager/intern/wm_operators.c @@ -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); diff --git a/source/blender/windowmanager/wm_files.h b/source/blender/windowmanager/wm_files.h index 048b5a997bb..15a94d2da70 100644 --- a/source/blender/windowmanager/wm_files.h +++ b/source/blender/windowmanager/wm_files.h @@ -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); |