Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'release/scripts/modules')
-rw-r--r--release/scripts/modules/addon_utils.py16
-rw-r--r--release/scripts/modules/bl_app_override/__init__.py202
-rw-r--r--release/scripts/modules/bl_app_override/helpers.py167
-rw-r--r--release/scripts/modules/bl_app_template_utils.py198
-rw-r--r--release/scripts/modules/bpy/__init__.py10
-rw-r--r--release/scripts/modules/bpy/path.py4
-rw-r--r--release/scripts/modules/bpy/utils/__init__.py82
-rw-r--r--release/scripts/modules/bpy_extras/object_utils.py9
-rw-r--r--release/scripts/modules/bpy_types.py56
-rw-r--r--release/scripts/modules/rna_keymap_ui.py7
10 files changed, 706 insertions, 45 deletions
diff --git a/release/scripts/modules/addon_utils.py b/release/scripts/modules/addon_utils.py
index 0f096f5812c..51e3e65b78c 100644
--- a/release/scripts/modules/addon_utils.py
+++ b/release/scripts/modules/addon_utils.py
@@ -24,6 +24,7 @@ __all__ = (
"check",
"enable",
"disable",
+ "disable_all",
"reset_all",
"module_bl_info",
)
@@ -31,8 +32,9 @@ __all__ = (
import bpy as _bpy
_user_preferences = _bpy.context.user_preferences
-error_duplicates = False
error_encoding = False
+# (name, file, path)
+error_duplicates = []
addons_fake_modules = {}
@@ -57,12 +59,11 @@ def paths():
def modules_refresh(module_cache=addons_fake_modules):
- global error_duplicates
global error_encoding
import os
- error_duplicates = False
error_encoding = False
+ error_duplicates.clear()
path_list = paths()
@@ -168,7 +169,7 @@ def modules_refresh(module_cache=addons_fake_modules):
if mod.__file__ != mod_path:
print("multiple addons with the same name:\n %r\n %r" %
(mod.__file__, mod_path))
- error_duplicates = True
+ error_duplicates.append((mod.bl_info["name"], mod.__file__, mod_path))
elif mod.__time__ != os.path.getmtime(mod_path):
print("reloading addon:",
@@ -444,6 +445,13 @@ def reset_all(*, reload_scripts=False):
disable(mod_name)
+def disable_all():
+ import sys
+ for mod_name, mod in sys.modules.items():
+ if getattr(mod, "__addon_enabled__", False):
+ disable(mod_name)
+
+
def module_bl_info(mod, info_basis=None):
if info_basis is None:
info_basis = {
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..89cc8a0eb28
--- /dev/null
+++ b/release/scripts/modules/bl_app_override/__init__.py
@@ -0,0 +1,202 @@
+# ##### 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):
+ cls_real = self_real.__class__
+ if cls_real is super:
+ # simple, no wrapping
+ return func_orig(self_real, context)
+
+ class Wrapper(cls_real):
+ __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/__init__.py b/release/scripts/modules/bpy/__init__.py
index 26fdbc8cc56..545b891505f 100644
--- a/release/scripts/modules/bpy/__init__.py
+++ b/release/scripts/modules/bpy/__init__.py
@@ -48,11 +48,11 @@ def main():
import sys
# Possibly temp. addons path
- from os.path import join, dirname, normpath
- sys.path.append(normpath(join(dirname(__file__),
- "..", "..", "addons", "modules")))
- sys.path.append(join(utils.user_resource('SCRIPTS'),
- "addons", "modules"))
+ from os.path import join, dirname
+ sys.path.extend([
+ join(dirname(dirname(dirname(__file__))), "addons", "modules"),
+ join(utils.user_resource('SCRIPTS'), "addons", "modules"),
+ ])
# fake module to allow:
# from bpy.types import Panel
diff --git a/release/scripts/modules/bpy/path.py b/release/scripts/modules/bpy/path.py
index a864a86eba7..e17d710068c 100644
--- a/release/scripts/modules/bpy/path.py
+++ b/release/scripts/modules/bpy/path.py
@@ -204,7 +204,9 @@ def display_name(name):
name = name.replace("_colon_", ":")
name = name.replace("_plus_", "+")
- name = name.replace("_", " ")
+ # strip to allow underscore prefix
+ # (when paths can't start with numbers for eg).
+ name = name.replace("_", " ").lstrip(" ")
if name.islower():
name = name.lower().title()
diff --git a/release/scripts/modules/bpy/utils/__init__.py b/release/scripts/modules/bpy/utils/__init__.py
index 31dd836e034..1d555ae7123 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",
@@ -49,18 +50,18 @@ __all__ = (
"unregister_class",
"unregister_module",
"user_resource",
- )
+)
from _bpy import (
- _utils_units as units,
- blend_paths,
- escape_identifier,
- register_class,
- resource_path,
- script_paths as _bpy_script_paths,
- unregister_class,
- user_resource as _user_resource,
- )
+ _utils_units as units,
+ blend_paths,
+ escape_identifier,
+ register_class,
+ resource_path,
+ script_paths as _bpy_script_paths,
+ unregister_class,
+ user_resource as _user_resource,
+)
import bpy as _bpy
import os as _os
@@ -142,7 +143,7 @@ def load_scripts(reload_scripts=False, refresh_scripts=False):
as modules.
:type refresh_scripts: bool
"""
- use_time = _bpy.app.debug_python
+ use_time = use_class_register_check = _bpy.app.debug_python
if use_time:
import time
@@ -161,7 +162,8 @@ def load_scripts(reload_scripts=False, refresh_scripts=False):
for module_name in [ext.module for ext in _user_preferences.addons]:
_addon_utils.disable(module_name)
- # *AFTER* unregistering all add-ons, otherwise all calls to unregister_module() will silently fail (do nothing).
+ # *AFTER* unregistering all add-ons, otherwise all calls to
+ # unregister_module() will silently fail (do nothing).
_bpy_types.TypeMap.clear()
def register_module_call(mod):
@@ -245,6 +247,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:
@@ -269,13 +277,21 @@ def load_scripts(reload_scripts=False, refresh_scripts=False):
if use_time:
print("Python Script Load Time %.4f" % (time.time() - t_main))
+ if use_class_register_check:
+ for cls in _bpy.types.bpy_struct.__subclasses__():
+ if getattr(cls, "is_registered", False):
+ for subcls in cls.__subclasses__():
+ if not subcls.is_registered:
+ print(
+ "Warning, unregistered class: %s(%s)" %
+ (subcls.__name__, cls.__name__)
+ )
+
# base scripts
-_scripts = _os.path.join(_os.path.dirname(__file__),
- _os.path.pardir,
- _os.path.pardir,
- )
-_scripts = (_os.path.normpath(_scripts), )
+_scripts = (
+ _os.path.dirname(_os.path.dirname(_os.path.dirname(__file__))),
+)
def script_path_user():
@@ -356,6 +372,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/modules/bpy_extras/object_utils.py b/release/scripts/modules/bpy_extras/object_utils.py
index d740137b0c3..83d3b2066b4 100644
--- a/release/scripts/modules/bpy_extras/object_utils.py
+++ b/release/scripts/modules/bpy_extras/object_utils.py
@@ -121,7 +121,14 @@ def object_data_add(context, obdata, operator=None, name=None):
"""
scene = context.scene
layer = context.render_layer
- scene_collection = context.scene_collection
+ layer_collection = context.layer_collection
+
+ if not layer_collection:
+ # when there is no collection linked to this render_layer create one
+ scene_collection = scene.master_collection.collections.new("")
+ layer_collection = layer.collections.link(scene_collection)
+ else:
+ scene_collection = layer_collection.collection
bpy.ops.object.select_all(action='DESELECT')
diff --git a/release/scripts/modules/bpy_types.py b/release/scripts/modules/bpy_types.py
index d64acd2ce3b..600b29a6b2b 100644
--- a/release/scripts/modules/bpy_types.py
+++ b/release/scripts/modules/bpy_types.py
@@ -683,6 +683,10 @@ class _GenericUI:
return draw_funcs
@classmethod
+ def is_extended(cls):
+ return bool(getattr(cls.draw, "_draw_funcs", None))
+
+ @classmethod
def append(cls, draw_func):
"""
Append a draw function to this menu,
@@ -725,11 +729,30 @@ class Header(StructRNA, _GenericUI, metaclass=RNAMeta):
class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
__slots__ = ()
- def path_menu(self, searchpaths, operator,
- props_default=None, filter_ext=None):
+ def path_menu(self, searchpaths, operator, *,
+ props_default=None, prop_filepath="filepath",
+ filter_ext=None, filter_path=None, display_name=None):
+ """
+ Populate a menu from a list of paths.
+
+ :arg searchpaths: Paths to scan.
+ :type searchpaths: sequence of strings.
+ :arg operator: The operator id to use with each file.
+ :type operator: string
+ :arg prop_filepath: Optional operator filepath property (defaults to "filepath").
+ :type prop_filepath: string
+ :arg props_default: Properties to assign to each operator.
+ :type props_default: dict
+ :arg filter_ext: Optional callback that takes the file extensions.
+
+ Returning false excludes the file from the list.
+
+ :type filter_ext: Callable that takes a string and returns a bool.
+ :arg display_name: Optional callback that takes the full path, returns the name to display.
+ :type display_name: Callable that takes a string and returns a string.
+ """
layout = self.layout
- # hard coded to set the operators 'filepath' to the filename.
import os
import bpy.utils
@@ -742,25 +765,32 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta):
# collect paths
files = []
for directory in searchpaths:
- files.extend([(f, os.path.join(directory, f))
- for f in os.listdir(directory)
- if (not f.startswith("."))
- if ((filter_ext is None) or
- (filter_ext(os.path.splitext(f)[1])))
- ])
+ files.extend(
+ [(f, os.path.join(directory, f))
+ for f in os.listdir(directory)
+ if (not f.startswith("."))
+ if ((filter_ext is None) or
+ (filter_ext(os.path.splitext(f)[1])))
+ if ((filter_path is None) or
+ (filter_path(f)))
+ ])
files.sort()
for f, filepath in files:
- props = layout.operator(operator,
- text=bpy.path.display_name(f),
- translate=False)
+ # Intentionally pass the full path to 'display_name' callback,
+ # since the callback may want to use part a directory in the name.
+ props = layout.operator(
+ operator,
+ text=display_name(filepath) if display_name else bpy.path.display_name(f),
+ translate=False,
+ )
if props_default is not None:
for attr, value in props_default.items():
setattr(props, attr, value)
- props.filepath = filepath
+ setattr(props, prop_filepath, filepath)
if operator == "script.execute_preset":
props.menu_idname = self.bl_idname
diff --git a/release/scripts/modules/rna_keymap_ui.py b/release/scripts/modules/rna_keymap_ui.py
index 2ca7a7997a5..a1a4e5b8763 100644
--- a/release/scripts/modules/rna_keymap_ui.py
+++ b/release/scripts/modules/rna_keymap_ui.py
@@ -128,16 +128,15 @@ def draw_kmi(display_keymaps, kc, km, kmi, layout, level):
else:
box = col.column()
- split = box.split(percentage=0.01)
+ split = box.split()
# header bar
- row = split.row()
+ row = split.row(align=True)
row.prop(kmi, "show_expanded", text="", emboss=False)
-
- row = split.row()
row.prop(kmi, "active", text="", emboss=False)
if km.is_modal:
+ row.separator()
row.prop(kmi, "propvalue", text="")
else:
row.label(text=kmi.name)