WM: Application Templates

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
This commit is contained in:
Campbell Barton 2017-03-25 09:29:51 +11:00
parent a7f16c17c2
commit f68145011f
17 changed files with 1118 additions and 45 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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)

View 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.

View 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,
)
)

View 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'

View 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,

View 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);

View 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);

View 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

View 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;

View 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;

View 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");

View 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 */
}

View 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);

View 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);

View 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);