diff options
Diffstat (limited to 'release/scripts')
30 files changed, 1230 insertions, 190 deletions
diff --git a/release/scripts/freestyle/modules/freestyle/utils.py b/release/scripts/freestyle/modules/freestyle/utils.py index d3eba187f70..2d47995d474 100644 --- a/release/scripts/freestyle/modules/freestyle/utils.py +++ b/release/scripts/freestyle/modules/freestyle/utils.py @@ -318,7 +318,7 @@ class BoundingBox: class StrokeCollector(StrokeShader): - "Collects and Stores stroke objects" + """Collects and Stores stroke objects""" def __init__(self): StrokeShader.__init__(self) self.strokes = [] diff --git a/release/scripts/modules/addon_utils.py b/release/scripts/modules/addon_utils.py index 886f078f046..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", ) @@ -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/keyconfig_utils.py b/release/scripts/modules/bpy_extras/keyconfig_utils.py index 6ecdd0c5e13..3203bc41b76 100644 --- a/release/scripts/modules/bpy_extras/keyconfig_utils.py +++ b/release/scripts/modules/bpy_extras/keyconfig_utils.py @@ -141,6 +141,78 @@ KM_HIERARCHY = [ # ----------------------------------------------------------------------------- +# Add-on helpers to properly (un)register their own keymaps. + +# Example of keymaps_description: +keymaps_description_doc = """ +keymaps_description is a tuple (((keymap_description), (tuple of keymap_item_descriptions))). +keymap_description is a tuple (name, space_type, region_type, is_modal). +keymap_item_description is a tuple ({kw_args_for_keymap_new}, (tuple of properties)). +kw_args_for_keymap_new is a mapping which keywords match parameters of keymap.new() function. +tuple of properties is a tuple of pairs (prop_name, prop_value) (properties being those of called operator). + +Example: + +KEYMAPS = ( + # First, keymap identifiers (last bool is True for modal km). + (('Sequencer', 'SEQUENCE_EDITOR', 'WINDOW', False), ( + # Then a tuple of keymap items, defined by a dict of kwargs for the km new func, and a tuple of tuples (name, val) + # for ops properties, if needing non-default values. + ({"idname": export_strips.SEQExportStrip.bl_idname, "type": 'P', "value": 'PRESS', "shift": True, "ctrl": True}, + ()), + )), +) +""" + + +def addon_keymap_register(wm, keymaps_description): + """ + Register a set of keymaps for addons. + + """ + keymaps_description_doc + kconf = wm.keyconfigs.addon + if not kconf: + return # happens in background mode... + for km_info, km_items in keymaps_description: + km_name, km_sptype, km_regtype, km_ismodal = km_info + kmap = [k for k in kconf.keymaps + if k.name == km_name and k.region_type == km_regtype and + k.space_type == km_sptype and k.is_modal == km_ismodal] + if kmap: + kmap = kmap[0] + else: + kmap = kconf.keymaps.new(km_name, region_type=km_regtype, space_type=km_sptype, modal=km_ismodal) + for kmi_kwargs, props in km_items: + kmi = kmap.keymap_items.new(**kmi_kwargs) + kmi.active = True + for prop, val in props: + setattr(kmi.properties, prop, val) + + +def addon_keymap_unregister(wm, keymaps_description): + """ + Unregister a set of keymaps for addons. + + """ + keymaps_description_doc + # NOTE: We must also clean up user keyconfig, else, if user has customized one of add-on's shortcut, this + # customization remains in memory, and comes back when re-enabling the addon, causing a segfault... :/ + kconfs = wm.keyconfigs + for kconf in (kconfs.user, kconfs.addon): + for km_info, km_items in keymaps_description: + km_name, km_sptype, km_regtype, km_ismodal = km_info + kmaps = (k for k in kconf.keymaps + if k.name == km_name and k.region_type == km_regtype and + k.space_type == km_sptype and k.is_modal == km_ismodal) + for kmap in kmaps: + for kmi_kwargs, props in km_items: + idname = kmi_kwargs["idname"] + for kmi in kmap.keymap_items: + if kmi.idname == idname: + kmap.keymap_items.remove(kmi) + # NOTE: We won't remove addons keymaps themselves, other addons might also use them! + + +# ----------------------------------------------------------------------------- # Utility functions def km_exists_in(km, export_keymaps): diff --git a/release/scripts/modules/bpy_types.py b/release/scripts/modules/bpy_types.py index 5eb8b946568..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, diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py index c28c1461003..1e0dbe6925e 100644 --- a/release/scripts/startup/bl_operators/__init__.py +++ b/release/scripts/startup/bl_operators/__init__.py @@ -21,8 +21,7 @@ # support reloading sub-modules if "bpy" in locals(): from importlib import reload - for val in _modules_loaded: - reload(val) + _modules_loaded[:] = [reload(val) for val in _modules_loaded] del reload _modules = [ @@ -73,4 +72,5 @@ def unregister(): from bpy.utils import unregister_class for mod in reversed(_modules_loaded): for cls in reversed(mod.classes): - unregister_class(cls) + if cls.is_registered: + unregister_class(cls) diff --git a/release/scripts/startup/bl_operators/anim.py b/release/scripts/startup/bl_operators/anim.py index 78fcf0dd124..02fb05e29eb 100644 --- a/release/scripts/startup/bl_operators/anim.py +++ b/release/scripts/startup/bl_operators/anim.py @@ -36,7 +36,7 @@ from bpy.props import ( class ANIM_OT_keying_set_export(Operator): - "Export Keying Set to a python script" + """Export Keying Set to a python script""" bl_idname = "anim.keying_set_export" bl_label = "Export Keying Set..." @@ -102,15 +102,13 @@ class ANIM_OT_keying_set_export(Operator): if ksp.id in id_to_paths_cache: continue - """ - - idtype_list is used to get the list of id-datablocks from - bpy.data.* since this info isn't available elsewhere - - id.bl_rna.name gives a name suitable for UI, - with a capitalised first letter, but we need - the plural form that's all lower case - - special handling is needed for "nested" ID-blocks - (e.g. nodetree in Material) - """ + # - idtype_list is used to get the list of id-datablocks from + # bpy.data.* since this info isn't available elsewhere + # - id.bl_rna.name gives a name suitable for UI, + # with a capitalised first letter, but we need + # the plural form that's all lower case + # - special handling is needed for "nested" ID-blocks + # (e.g. nodetree in Material) if ksp.id.bl_rna.identifier.startswith("ShaderNodeTree"): # Find material or lamp using this node tree... id_bpy_path = "bpy.data.nodes[\"%s\"]" diff --git a/release/scripts/startup/bl_operators/clip.py b/release/scripts/startup/bl_operators/clip.py index e82bc0bf2de..e52d577b900 100644 --- a/release/scripts/startup/bl_operators/clip.py +++ b/release/scripts/startup/bl_operators/clip.py @@ -21,7 +21,10 @@ import bpy import os from bpy.types import Operator from bpy.props import FloatProperty -from mathutils import Vector, Matrix +from mathutils import ( + Vector, + Matrix, +) def CLIP_spaces_walk(context, all_screens, tarea, tspace, callback, *args): @@ -1084,4 +1087,4 @@ classes = ( CLIP_OT_track_settings_as_default, CLIP_OT_track_settings_to_track, CLIP_OT_track_to_empty, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/freestyle.py b/release/scripts/startup/bl_operators/freestyle.py index d8cffb7f0ff..2e46160aeeb 100644 --- a/release/scripts/startup/bl_operators/freestyle.py +++ b/release/scripts/startup/bl_operators/freestyle.py @@ -16,13 +16,15 @@ # # ##### END GPL LICENSE BLOCK ##### +# <pep8 compliant> + import bpy from bpy.props import ( - BoolProperty, - EnumProperty, - StringProperty, - ) + BoolProperty, + EnumProperty, + StringProperty, +) class SCENE_OT_freestyle_fill_range_by_selection(bpy.types.Operator): diff --git a/release/scripts/startup/bl_operators/mesh.py b/release/scripts/startup/bl_operators/mesh.py index bce38a6bf3a..4edefd7bf9b 100644 --- a/release/scripts/startup/bl_operators/mesh.py +++ b/release/scripts/startup/bl_operators/mesh.py @@ -21,7 +21,10 @@ import bpy from bpy.types import Operator -from bpy.props import EnumProperty, IntProperty +from bpy.props import ( + EnumProperty, + IntProperty, +) class MeshMirrorUV(Operator): @@ -254,4 +257,4 @@ classes = ( MeshMirrorUV, MeshSelectNext, MeshSelectPrev, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/node.py b/release/scripts/startup/bl_operators/node.py index 7b280507bbb..40876e2b069 100644 --- a/release/scripts/startup/bl_operators/node.py +++ b/release/scripts/startup/bl_operators/node.py @@ -21,16 +21,16 @@ import bpy import nodeitems_utils from bpy.types import ( - Operator, - PropertyGroup, - ) + Operator, + PropertyGroup, +) from bpy.props import ( - BoolProperty, - CollectionProperty, - EnumProperty, - IntProperty, - StringProperty, - ) + BoolProperty, + CollectionProperty, + EnumProperty, + IntProperty, + StringProperty, +) class NodeSetting(PropertyGroup): diff --git a/release/scripts/startup/bl_operators/object.py b/release/scripts/startup/bl_operators/object.py index 7e9be607281..3a42d8d2e78 100644 --- a/release/scripts/startup/bl_operators/object.py +++ b/release/scripts/startup/bl_operators/object.py @@ -21,12 +21,12 @@ import bpy from bpy.types import Operator from bpy.props import ( - StringProperty, - BoolProperty, - EnumProperty, - IntProperty, - FloatProperty, - ) + BoolProperty, + EnumProperty, + FloatProperty, + IntProperty, + StringProperty, +) class SelectPattern(Operator): @@ -1052,4 +1052,4 @@ classes = ( SubdivisionSet, TransformsToDeltas, TransformsToDeltasAnim, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/object_align.py b/release/scripts/startup/bl_operators/object_align.py index a088898b14d..1539ffb3545 100644 --- a/release/scripts/startup/bl_operators/object_align.py +++ b/release/scripts/startup/bl_operators/object_align.py @@ -26,13 +26,14 @@ from mathutils import Vector def GlobalBB_LQ(bb_world): # Initialize the variables with the 8th vertex - left, right, front, back, down, up = (bb_world[7][0], - bb_world[7][0], - bb_world[7][1], - bb_world[7][1], - bb_world[7][2], - bb_world[7][2], - ) + left, right, front, back, down, up = ( + bb_world[7][0], + bb_world[7][0], + bb_world[7][1], + bb_world[7][1], + bb_world[7][2], + bb_world[7][2], + ) # Test against the other 7 verts for i in range(7): @@ -398,13 +399,15 @@ class AlignObjects(Operator): def execute(self, context): align_axis = self.align_axis - ret = align_objects(context, - 'X' in align_axis, - 'Y' in align_axis, - 'Z' in align_axis, - self.align_mode, - self.relative_to, - self.bb_quality) + ret = align_objects( + context, + 'X' in align_axis, + 'Y' in align_axis, + 'Z' in align_axis, + self.align_mode, + self.relative_to, + self.bb_quality, + ) if not ret: self.report({'WARNING'}, "No objects with bound-box selected") @@ -415,4 +418,4 @@ class AlignObjects(Operator): classes = ( AlignObjects, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/object_quick_effects.py b/release/scripts/startup/bl_operators/object_quick_effects.py index 57d7f03fcd4..16f29c77bb9 100644 --- a/release/scripts/startup/bl_operators/object_quick_effects.py +++ b/release/scripts/startup/bl_operators/object_quick_effects.py @@ -22,12 +22,12 @@ from mathutils import Vector import bpy from bpy.types import Operator from bpy.props import ( - BoolProperty, - EnumProperty, - IntProperty, - FloatProperty, - FloatVectorProperty, - ) + BoolProperty, + EnumProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, +) def object_ensure_material(obj, mat_name): @@ -652,4 +652,4 @@ classes = ( QuickFluid, QuickFur, QuickSmoke, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/rigidbody.py b/release/scripts/startup/bl_operators/rigidbody.py index 639a558fbab..6792d525683 100644 --- a/release/scripts/startup/bl_operators/rigidbody.py +++ b/release/scripts/startup/bl_operators/rigidbody.py @@ -20,8 +20,10 @@ import bpy from bpy.types import Operator -from bpy.props import IntProperty -from bpy.props import EnumProperty +from bpy.props import ( + EnumProperty, + IntProperty, +) class CopyRigidbodySettings(Operator): @@ -315,4 +317,4 @@ classes = ( BakeToKeyframes, ConnectRigidBodies, CopyRigidbodySettings, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/uvcalc_smart_project.py b/release/scripts/startup/bl_operators/uvcalc_smart_project.py index 1dd5b78d599..5581415c083 100644 --- a/release/scripts/startup/bl_operators/uvcalc_smart_project.py +++ b/release/scripts/startup/bl_operators/uvcalc_smart_project.py @@ -18,7 +18,11 @@ # TODO <pep8 compliant> -from mathutils import Matrix, Vector, geometry +from mathutils import ( + Matrix, + Vector, + geometry, +) import bpy from bpy.types import Operator @@ -1104,4 +1108,4 @@ class SmartProject(Operator): classes = ( SmartProject, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/view3d.py b/release/scripts/startup/bl_operators/view3d.py index acec2d8fe91..18f91110053 100644 --- a/release/scripts/startup/bl_operators/view3d.py +++ b/release/scripts/startup/bl_operators/view3d.py @@ -24,7 +24,7 @@ from bpy.props import BoolProperty class VIEW3D_OT_edit_mesh_extrude_individual_move(Operator): - "Extrude individual elements and move" + """Extrude individual elements and move""" bl_label = "Extrude Individual and Move" bl_idname = "view3d.edit_mesh_extrude_individual_move" @@ -62,7 +62,7 @@ class VIEW3D_OT_edit_mesh_extrude_individual_move(Operator): class VIEW3D_OT_edit_mesh_extrude_move(Operator): - "Extrude and move along normals" + """Extrude and move along normals""" bl_label = "Extrude and Move on Normals" bl_idname = "view3d.edit_mesh_extrude_move_normal" @@ -111,7 +111,7 @@ class VIEW3D_OT_edit_mesh_extrude_move(Operator): class VIEW3D_OT_edit_mesh_extrude_shrink_fatten(Operator): - "Extrude and move along individual normals" + """Extrude and move along individual normals""" bl_label = "Extrude and Move on Individual Normals" bl_idname = "view3d.edit_mesh_extrude_move_shrink_fatten" @@ -128,7 +128,7 @@ class VIEW3D_OT_edit_mesh_extrude_shrink_fatten(Operator): class VIEW3D_OT_select_or_deselect_all(Operator): - "Select element under the mouse, deselect everything is there's nothing under the mouse" + """Select element under the mouse, deselect everything is there's nothing under the mouse""" bl_label = "Select or Deselect All" bl_idname = "view3d.select_or_deselect_all" bl_options = {'UNDO'} @@ -220,4 +220,4 @@ classes = ( VIEW3D_OT_edit_mesh_extrude_move, VIEW3D_OT_edit_mesh_extrude_shrink_fatten, VIEW3D_OT_select_or_deselect_all, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_operators/wm.py b/release/scripts/startup/bl_operators/wm.py index 42f1e723d1a..20586b727d5 100644 --- a/release/scripts/startup/bl_operators/wm.py +++ b/release/scripts/startup/bl_operators/wm.py @@ -21,12 +21,12 @@ import bpy from bpy.types import Operator from bpy.props import ( - StringProperty, - BoolProperty, - IntProperty, - FloatProperty, - EnumProperty, - ) + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + EnumProperty, +) from bpy.app.translations import pgettext_tip as tip_ @@ -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" @@ -831,7 +845,7 @@ class WM_OT_context_modal_mouse(Operator): class WM_OT_url_open(Operator): - "Open a website in the web-browser" + """Open a website in the web-browser""" bl_idname = "wm.url_open" bl_label = "" bl_options = {'INTERNAL'} @@ -848,7 +862,7 @@ class WM_OT_url_open(Operator): class WM_OT_path_open(Operator): - "Open a path in a file browser" + """Open a path in a file browser""" bl_idname = "wm.path_open" bl_label = "" bl_options = {'INTERNAL'} @@ -907,7 +921,10 @@ def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""): # an operator (common case - just button referencing an op) if hasattr(bpy.types, class_name.upper() + "_OT_" + class_prop): if do_url: - url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop)) + url = ( + "%s/bpy.ops.%s.html#bpy.ops.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) else: rna = "bpy.ops.%s.%s" % (class_name, class_prop) else: @@ -922,7 +939,10 @@ def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""): class_name, class_prop = class_name.split("_OT_", 1) class_name = class_name.lower() if do_url: - url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop)) + url = ( + "%s/bpy.ops.%s.html#bpy.ops.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) else: rna = "bpy.ops.%s.%s" % (class_name, class_prop) else: @@ -938,9 +958,12 @@ def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""): rna_parent = rna_parent.base if do_url: - url = ("%s/bpy.types.%s.html#bpy.types.%s.%s" % (url_prefix, class_name, class_name, class_prop)) + url = ( + "%s/bpy.types.%s.html#bpy.types.%s.%s" % + (url_prefix, class_name, class_name, class_prop) + ) else: - rna = ("bpy.types.%s.%s" % (class_name, class_prop)) + rna = "bpy.types.%s.%s" % (class_name, class_prop) else: # We assume this is custom property, only try to generate generic url/rna_id... if do_url: @@ -1087,10 +1110,10 @@ class WM_OT_properties_edit(Operator): def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_get, - rna_idprop_ui_prop_clear, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_get, + rna_idprop_ui_prop_clear, + rna_idprop_ui_prop_update, + ) data_path = self.data_path value = self.value @@ -1267,9 +1290,9 @@ class WM_OT_properties_add(Operator): def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_get, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_get, + rna_idprop_ui_prop_update, + ) data_path = self.data_path item = eval("context.%s" % data_path) @@ -1284,10 +1307,10 @@ class WM_OT_properties_add(Operator): return prop_new - prop = unique_name( - {*item.keys(), - *type(item).bl_rna.properties.keys(), - }) + prop = unique_name({ + *item.keys(), + *type(item).bl_rna.properties.keys(), + }) item[prop] = 1.0 rna_idprop_ui_prop_update(item, prop) @@ -1301,7 +1324,7 @@ class WM_OT_properties_add(Operator): class WM_OT_properties_context_change(Operator): - "Jump to a different tab inside the properties editor" + """Jump to a different tab inside the properties editor""" bl_idname = "wm.properties_context_change" bl_label = "" bl_options = {'INTERNAL'} @@ -1327,9 +1350,9 @@ class WM_OT_properties_remove(Operator): def execute(self, context): from rna_prop_ui import ( - rna_idprop_ui_prop_clear, - rna_idprop_ui_prop_update, - ) + rna_idprop_ui_prop_clear, + rna_idprop_ui_prop_update, + ) data_path = self.data_path item = eval("context.%s" % data_path) prop = self.property @@ -1367,7 +1390,10 @@ class WM_OT_appconfig_default(Operator): filepath = os.path.join(bpy.utils.preset_paths("interaction")[0], "blender.py") if os.path.exists(filepath): - bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets") + bpy.ops.script.execute_preset( + filepath=filepath, + menu_idname="USERPREF_MT_interaction_presets", + ) return {'FINISHED'} @@ -1387,7 +1413,10 @@ class WM_OT_appconfig_activate(Operator): filepath = self.filepath.replace("keyconfig", "interaction") if os.path.exists(filepath): - bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets") + bpy.ops.script.execute_preset( + filepath=filepath, + menu_idname="USERPREF_MT_interaction_presets", + ) return {'FINISHED'} @@ -1492,7 +1521,7 @@ class WM_OT_blenderplayer_start(Operator): "-g", "show_profile", "=", "%d" % gs.show_framerate_profile, "-g", "show_properties", "=", "%d" % gs.show_debug_properties, "-g", "ignore_deprecation_warnings", "=", "%d" % (not gs.use_deprecation_warnings), - ]) + ]) # finish the call with the path to the blend file args.append(filepath) @@ -1503,7 +1532,7 @@ class WM_OT_blenderplayer_start(Operator): class WM_OT_keyconfig_test(Operator): - "Test key-config for conflicts" + """Test key-config for conflicts""" bl_idname = "wm.keyconfig_test" bl_label = "Test Key Configuration for Conflicts" @@ -1520,7 +1549,7 @@ class WM_OT_keyconfig_test(Operator): class WM_OT_keyconfig_import(Operator): - "Import key configuration from a python script" + """Import key configuration from a python script""" bl_idname = "wm.keyconfig_import" bl_label = "Import Key Configuration..." @@ -1587,7 +1616,7 @@ class WM_OT_keyconfig_import(Operator): class WM_OT_keyconfig_export(Operator): - "Export key configuration to a python script" + """Export key configuration to a python script""" bl_idname = "wm.keyconfig_export" bl_label = "Export Key Configuration..." @@ -1622,10 +1651,11 @@ class WM_OT_keyconfig_export(Operator): wm = context.window_manager - keyconfig_utils.keyconfig_export(wm, - wm.keyconfigs.active, - self.filepath, - ) + keyconfig_utils.keyconfig_export( + wm, + wm.keyconfigs.active, + self.filepath, + ) return {'FINISHED'} @@ -1636,7 +1666,7 @@ class WM_OT_keyconfig_export(Operator): class WM_OT_keymap_restore(Operator): - "Restore key map(s)" + """Restore key map(s)""" bl_idname = "wm.keymap_restore" bl_label = "Restore Key Map(s)" @@ -1659,7 +1689,7 @@ class WM_OT_keymap_restore(Operator): class WM_OT_keyitem_restore(Operator): - "Restore key map item" + """Restore key map item""" bl_idname = "wm.keyitem_restore" bl_label = "Restore Key Map Item" @@ -1684,7 +1714,7 @@ class WM_OT_keyitem_restore(Operator): class WM_OT_keyitem_add(Operator): - "Add key map item" + """Add key map item""" bl_idname = "wm.keyitem_add" bl_label = "Add Key Map Item" @@ -1706,7 +1736,7 @@ class WM_OT_keyitem_add(Operator): class WM_OT_keyitem_remove(Operator): - "Remove key map item" + """Remove key map item""" bl_idname = "wm.keyitem_remove" bl_label = "Remove Key Map Item" @@ -1727,7 +1757,7 @@ class WM_OT_keyitem_remove(Operator): class WM_OT_keyconfig_remove(Operator): - "Remove key config" + """Remove key config""" bl_idname = "wm.keyconfig_remove" bl_label = "Remove Key Config" @@ -1745,6 +1775,7 @@ class WM_OT_keyconfig_remove(Operator): class WM_OT_operator_cheat_sheet(Operator): + """List all the Operators in a text-block, useful for scripting""" bl_idname = "wm.operator_cheat_sheet" bl_label = "Operator Cheat Sheet" @@ -1773,7 +1804,7 @@ class WM_OT_operator_cheat_sheet(Operator): # Add-on Operators class WM_OT_addon_enable(Operator): - "Enable an add-on" + """Enable an add-on""" bl_idname = "wm.addon_enable" bl_label = "Enable Add-on" @@ -1817,7 +1848,7 @@ class WM_OT_addon_enable(Operator): class WM_OT_addon_disable(Operator): - "Disable an add-on" + """Disable an add-on""" bl_idname = "wm.addon_disable" bl_label = "Disable Add-on" @@ -1846,7 +1877,7 @@ class WM_OT_addon_disable(Operator): class WM_OT_theme_install(Operator): - "Load and apply a Blender XML theme file" + """Load and apply a Blender XML theme file""" bl_idname = "wm.theme_install" bl_label = "Install Theme..." @@ -1890,7 +1921,10 @@ class WM_OT_theme_install(Operator): try: shutil.copyfile(xmlfile, path_dest) - bpy.ops.script.execute_preset(filepath=path_dest, menu_idname="USERPREF_MT_interface_theme_presets") + bpy.ops.script.execute_preset( + filepath=path_dest, + menu_idname="USERPREF_MT_interface_theme_presets", + ) except: traceback.print_exc() @@ -1905,7 +1939,7 @@ class WM_OT_theme_install(Operator): class WM_OT_addon_refresh(Operator): - "Scan add-on directories for new modules" + """Scan add-on directories for new modules""" bl_idname = "wm.addon_refresh" bl_label = "Refresh" @@ -1917,10 +1951,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" + """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 +1987,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 +2039,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 +2057,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 +2092,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") % + (", ".join(sorted(addons_new)), pyfile, path_addons) + ) print(msg) self.report({'INFO'}, msg) @@ -2083,7 +2108,7 @@ class WM_OT_addon_install(Operator): class WM_OT_addon_remove(Operator): - "Delete the add-on from the file system" + """Delete the add-on from the file system""" bl_idname = "wm.addon_remove" bl_label = "Remove Add-on" @@ -2142,7 +2167,7 @@ class WM_OT_addon_remove(Operator): class WM_OT_addon_expand(Operator): - "Display information and preferences for this add-on" + """Display information and preferences for this add-on""" bl_idname = "wm.addon_expand" bl_label = "" bl_options = {'INTERNAL'} @@ -2164,8 +2189,9 @@ class WM_OT_addon_expand(Operator): return {'FINISHED'} + class WM_OT_addon_userpref_show(Operator): - "Show add-on user preferences" + """Show add-on user preferences""" bl_idname = "wm.addon_userpref_show" bl_label = "" bl_options = {'INTERNAL'} @@ -2194,6 +2220,107 @@ 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_glob = StringProperty( + default="*.zip", + options={'HIDDEN'}, + ) + + def execute(self, context): + import traceback + import zipfile + import shutil + import os + + filepath = 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(filepath): + try: + file_to_extract = zipfile.ZipFile(filepath, '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: + # Only support installing zipfiles + self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath) + 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)), filepath, 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 +2330,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 +2374,4 @@ classes = ( WM_OT_sysinfo, WM_OT_theme_install, WM_OT_url_open, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_ui/__init__.py b/release/scripts/startup/bl_ui/__init__.py index 3e81724c1f9..5b609605cee 100644 --- a/release/scripts/startup/bl_ui/__init__.py +++ b/release/scripts/startup/bl_ui/__init__.py @@ -23,8 +23,7 @@ # support reloading sub-modules if "bpy" in locals(): from importlib import reload - for val in _modules_loaded: - reload(val) + _modules_loaded[:] = [reload(val) for val in _modules_loaded] del reload _modules = [ @@ -149,7 +148,8 @@ def unregister(): from bpy.utils import unregister_class for mod in reversed(_modules_loaded): for cls in reversed(mod.classes): - unregister_class(cls) + if cls.is_registered: + unregister_class(cls) # Define a default UIList, when a list does not need any custom drawing... # Keep in sync with its #defined name in UI_interface.h diff --git a/release/scripts/startup/bl_ui/properties_data_modifier.py b/release/scripts/startup/bl_ui/properties_data_modifier.py index a098953699e..a37d61dd0af 100644 --- a/release/scripts/startup/bl_ui/properties_data_modifier.py +++ b/release/scripts/startup/bl_ui/properties_data_modifier.py @@ -569,6 +569,14 @@ class DATA_PT_modifiers(ModifierButtonsPanel, Panel): col.prop(md, "use_mirror_u", text="U") col.prop(md, "use_mirror_v", text="V") + col = layout.column(align=True) + + if md.use_mirror_u: + col.prop(md, "mirror_offset_u") + + if md.use_mirror_v: + col.prop(md, "mirror_offset_v") + col = layout.column() if md.use_mirror_merge is True: diff --git a/release/scripts/startup/bl_ui/space_image.py b/release/scripts/startup/bl_ui/space_image.py index b274657b4f4..c748e71a0a2 100644 --- a/release/scripts/startup/bl_ui/space_image.py +++ b/release/scripts/startup/bl_ui/space_image.py @@ -18,6 +18,7 @@ # <pep8 compliant> import bpy +import math from bpy.types import Header, Menu, Panel from bl_ui.properties_paint_common import ( UnifiedPaintPanel, @@ -727,11 +728,73 @@ class IMAGE_PT_tools_transform_uvs(Panel, UVToolsPanel): col.operator("transform.translate") col.operator("transform.rotate") col.operator("transform.resize", text="Scale") - col.separator() - col.operator("transform.shear") +class IMAGE_PT_tools_align_uvs(Panel, UVToolsPanel): + bl_label = "UV Align" + + @classmethod + def poll(cls, context): + sima = context.space_data + return sima.show_uvedit and not context.tool_settings.use_uv_sculpt + + def draw(self, context): + layout = self.layout + layout.operator_context = 'EXEC_REGION_WIN' + + split = layout.split() + col = split.column(align=True) + col.operator("transform.mirror", text="Mirror X").constraint_axis[0] = True + col.operator("transform.mirror", text="Mirror Y").constraint_axis[1] = True + col = split.column(align=True) + col.operator("transform.rotate", text="Rotate +90°").value = math.pi / 2 + col.operator("transform.rotate", text="Rotate - 90°").value = math.pi / -2 + + split = layout.split() + col = split.column(align=True) + col.operator("uv.align", text="Straighten").axis = 'ALIGN_S' + col.operator("uv.align", text="Straighten X").axis = 'ALIGN_T' + col.operator("uv.align", text="Straighten Y").axis = 'ALIGN_U' + col = split.column(align=True) + col.operator("uv.align", text="Align Auto").axis = 'ALIGN_AUTO' + col.operator("uv.align", text="Align X").axis = 'ALIGN_X' + col.operator("uv.align", text="Align Y").axis = 'ALIGN_Y' + + +class IMAGE_PT_tools_uvs(Panel, UVToolsPanel): + bl_label = "UV Tools" + + @classmethod + def poll(cls, context): + sima = context.space_data + return sima.show_uvedit and not context.tool_settings.use_uv_sculpt + + def draw(self, context): + layout = self.layout + + col = layout.column(align=True) + row = col.row(align=True) + row.operator("uv.weld") + row.operator("uv.stitch") + col.operator("uv.remove_doubles") + col.operator("uv.average_islands_scale") + col.operator("uv.pack_islands") + col.operator("mesh.faces_mirror_uv") + col.operator("uv.minimize_stretch") + + layout.label(text="UV Unwrap:") + row = layout.row(align=True) + row.operator("uv.pin").clear = False + row.operator("uv.pin", text="Unpin").clear = True + col = layout.column(align=True) + row = col.row(align=True) + row.operator("uv.mark_seam", text="Mark Seam").clear = False + row.operator("uv.mark_seam", text="Clear Seam").clear = True + col.operator("uv.seams_from_islands", text="Mark Seams from Islands") + col.operator("uv.unwrap") + + class IMAGE_PT_paint(Panel, ImagePaintPanel): bl_label = "Paint" bl_category = "Tools" @@ -1100,6 +1163,29 @@ class IMAGE_PT_tools_mask(MASK_PT_tools, Panel): # --- end mask --- +class IMAGE_PT_options_uvs(Panel, UVToolsPanel): + bl_label = "UV Options" + bl_category = "Options" + + @classmethod + def poll(cls, context): + sima = context.space_data + return sima.show_uvedit + + def draw(self, context): + layout = self.layout + + sima = context.space_data + uv = sima.uv_editor + toolsettings = context.tool_settings + + col = layout.column(align=True) + col.prop(toolsettings, "use_uv_sculpt") + col.prop(uv, "use_live_unwrap") + col.prop(uv, "use_snap_to_pixels") + col.prop(uv, "lock_bounds") + + class ImageScopesPanel: @classmethod def poll(cls, context): @@ -1267,6 +1353,9 @@ classes = ( IMAGE_PT_game_properties, IMAGE_PT_view_properties, IMAGE_PT_tools_transform_uvs, + IMAGE_PT_tools_align_uvs, + IMAGE_PT_tools_uvs, + IMAGE_PT_options_uvs, IMAGE_PT_paint, IMAGE_PT_tools_brush_overlay, IMAGE_PT_tools_brush_texture, diff --git a/release/scripts/startup/bl_ui/space_info.py b/release/scripts/startup/bl_ui/space_info.py index 16ac6339504..a7b518dfd2e 100644 --- a/release/scripts/startup/bl_ui/space_info.py +++ b/release/scripts/startup/bl_ui/space_info.py @@ -127,6 +127,18 @@ class INFO_MT_file(Menu): layout.operator("wm.save_homefile", icon='SAVE_PREFS') layout.operator("wm.read_factory_settings", icon='LOAD_FACTORY') + if any(bpy.utils.app_template_paths()): + app_template = context.user_preferences.app_template + if app_template: + layout.operator( + "wm.read_factory_settings", + text="Load Factory Template Settings", + icon='LOAD_FACTORY', + ).app_template = app_template + del app_template + + layout.menu("USERPREF_MT_app_templates", icon='FILE_BLEND') + layout.separator() layout.operator_context = 'INVOKE_AREA' diff --git a/release/scripts/startup/bl_ui/space_userpref.py b/release/scripts/startup/bl_ui/space_userpref.py index fe126f6522c..5ed481a215a 100644 --- a/release/scripts/startup/bl_ui/space_userpref.py +++ b/release/scripts/startup/bl_ui/space_userpref.py @@ -90,6 +90,63 @@ class USERPREF_MT_interaction_presets(Menu): draw = Menu.draw_preset +class USERPREF_MT_app_templates(Menu): + bl_label = "Application Templates" + preset_subdir = "app_templates" + + def draw_ex(self, context, *, use_splash=False, use_default=False, use_install=False): + import os + + layout = self.layout + + # now draw the presets + layout.operator_context = 'EXEC_DEFAULT' + + if use_default: + props = layout.operator("wm.read_homefile", text="Default") + props.use_splash = True + props.app_template = "" + layout.separator() + + template_paths = bpy.utils.app_template_paths() + + # expand template paths + app_templates = [] + for path in template_paths: + for d in os.listdir(path): + if d.startswith(("__", ".")): + continue + template = os.path.join(path, d) + if os.path.isdir(template): + # template_paths_expand.append(template) + app_templates.append(d) + + for d in sorted(app_templates): + props = layout.operator( + "wm.read_homefile", + text=bpy.path.display_name(d), + ) + props.use_splash = True + props.app_template = d; + + if use_install: + layout.separator() + layout.operator_context = 'INVOKE_DEFAULT' + props = layout.operator("wm.app_template_install") + + + def draw(self, context): + self.draw_ex(context, use_splash=False, use_default=True, use_install=True) + + +class USERPREF_MT_templates_splash(Menu): + bl_label = "Startup Templates" + preset_subdir = "templates" + + def draw(self, context): + USERPREF_MT_app_templates.draw_ex(self, context, use_splash=True, use_default=True) + + class USERPREF_MT_appconfigs(Menu): bl_label = "AppPresets" preset_subdir = "keyconfig" @@ -110,7 +167,17 @@ class USERPREF_MT_splash(Menu): split = layout.split() row = split.row() - row.label("") + + if any(bpy.utils.app_template_paths()): + row.label("Template:") + template = context.user_preferences.app_template + row.menu( + "USERPREF_MT_templates_splash", + text=bpy.path.display_name(template) if template else "Default", + ) + else: + row.label("") + row = split.row() row.label("Interaction:") @@ -150,6 +217,7 @@ class USERPREF_PT_interface(Panel): col = row.column() col.label(text="Display:") + col.prop(view, "ui_scale", text="Scale") col.prop(view, "show_tooltips") col.prop(view, "show_tooltips_python") col.prop(view, "show_object_info", text="Object Info") @@ -400,11 +468,6 @@ class USERPREF_PT_system(Panel): col = colsplit.column() col.label(text="General:") - col.prop(system, "dpi") - col.label("Virtual Pixel Mode:") - col.prop(system, "virtual_pixel_mode", text="") - - col.separator() col.prop(system, "frame_server_port") col.prop(system, "scrollback", text="Console Scrollback") @@ -1159,23 +1222,25 @@ class USERPREF_PT_input(Panel): sub = col.column() sub.label(text="View Navigation:") sub.row().prop(inputs, "navigation_mode", expand=True) - if inputs.navigation_mode == 'WALK': - walk = inputs.walk_navigation - sub.prop(walk, "use_mouse_reverse") - sub.prop(walk, "mouse_speed") - sub.prop(walk, "teleport_time") + sub.label(text="Walk Navigation:") - sub = col.column(align=True) - sub.prop(walk, "walk_speed") - sub.prop(walk, "walk_speed_factor") + walk = inputs.walk_navigation - sub.separator() - sub.prop(walk, "use_gravity") - sub = col.column(align=True) - sub.active = walk.use_gravity - sub.prop(walk, "view_height") - sub.prop(walk, "jump_height") + sub.prop(walk, "use_mouse_reverse") + sub.prop(walk, "mouse_speed") + sub.prop(walk, "teleport_time") + + sub = col.column(align=True) + sub.prop(walk, "walk_speed") + sub.prop(walk, "walk_speed_factor") + + sub.separator() + sub.prop(walk, "use_gravity") + sub = col.column(align=True) + sub.active = walk.use_gravity + sub.prop(walk, "view_height") + sub.prop(walk, "jump_height") if inputs.use_ndof: col.separator() @@ -1485,6 +1550,8 @@ classes = ( USERPREF_HT_header, USERPREF_PT_tabs, USERPREF_MT_interaction_presets, + USERPREF_MT_templates_splash, + USERPREF_MT_app_templates, USERPREF_MT_appconfigs, USERPREF_MT_splash, USERPREF_MT_splash_footer, diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py index b718228e7b2..f13c7095f67 100644 --- a/release/scripts/startup/bl_ui/space_view3d.py +++ b/release/scripts/startup/bl_ui/space_view3d.py @@ -1206,6 +1206,16 @@ class INFO_MT_lamp_add(Menu): layout.operator_enum("object.lamp_add", "type") +class INFO_MT_camera_add(Menu): + bl_idname = "INFO_MT_camera_add" + bl_label = "Camera" + + def draw(self, context): + layout = self.layout + layout.operator_context = 'EXEC_REGION_WIN' + layout.operator("object.camera_add", text="Camera", icon='OUTLINER_OB_CAMERA') + + class INFO_MT_add(Menu): bl_label = "Add" @@ -1237,7 +1247,11 @@ class INFO_MT_add(Menu): layout.operator("object.speaker_add", text="Speaker", icon='OUTLINER_OB_SPEAKER') layout.separator() - layout.operator("object.camera_add", text="Camera", icon='OUTLINER_OB_CAMERA') + if INFO_MT_camera_add.is_extended(): + layout.menu("INFO_MT_camera_add", icon='OUTLINER_OB_CAMERA') + else: + INFO_MT_camera_add.draw(self, context) + layout.menu("INFO_MT_lamp_add", icon='OUTLINER_OB_LAMP') layout.separator() @@ -3832,6 +3846,7 @@ classes = ( INFO_MT_edit_armature_add, INFO_MT_armature_add, INFO_MT_lamp_add, + INFO_MT_camera_add, INFO_MT_add, VIEW3D_MT_object, VIEW3D_MT_object_animation, diff --git a/release/scripts/startup/nodeitems_builtins.py b/release/scripts/startup/nodeitems_builtins.py index e915aa5bb72..517a0738b44 100644 --- a/release/scripts/startup/nodeitems_builtins.py +++ b/release/scripts/startup/nodeitems_builtins.py @@ -19,7 +19,11 @@ # <pep8 compliant> import bpy import nodeitems_utils -from nodeitems_utils import NodeCategory, NodeItem, NodeItemCustom +from nodeitems_utils import ( + NodeCategory, + NodeItem, + NodeItemCustom, +) # Subclasses for standard node types @@ -153,6 +157,7 @@ shader_node_categories = [ NodeItem("ShaderNodeGeometry"), NodeItem("ShaderNodeExtendedMaterial"), NodeItem("ShaderNodeParticleInfo"), + NodeItem("ShaderNodeObjectInfo"), NodeItem("NodeGroupInput", poll=group_input_output_item_poll), ]), ShaderOldNodeCategory("SH_OUTPUT", "Output", items=[ |