diff options
Diffstat (limited to 'release/scripts/modules')
-rw-r--r-- | release/scripts/modules/addon_utils.py | 16 | ||||
-rw-r--r-- | release/scripts/modules/bl_app_override/__init__.py | 202 | ||||
-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/__init__.py | 10 | ||||
-rw-r--r-- | release/scripts/modules/bpy/path.py | 4 | ||||
-rw-r--r-- | release/scripts/modules/bpy/utils/__init__.py | 82 | ||||
-rw-r--r-- | release/scripts/modules/bpy_extras/object_utils.py | 9 | ||||
-rw-r--r-- | release/scripts/modules/bpy_types.py | 56 | ||||
-rw-r--r-- | release/scripts/modules/rna_keymap_ui.py | 7 |
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) |