diff options
author | Campbell Barton <ideasman42@gmail.com> | 2017-03-25 05:49:13 +0300 |
---|---|---|
committer | Campbell Barton <ideasman42@gmail.com> | 2017-03-25 05:49:13 +0300 |
commit | f730e386eb0a23e544c02efa5159c47fad88c00c (patch) | |
tree | 2511de212da8cb3b414cf9c310b3e4930e748be7 /release | |
parent | edd2c556cda28becf38de6191d793c6f496495d9 (diff) | |
parent | 7cb2974182173f4a48b89d4bdbc6a6aa9c9c50fd (diff) |
Merge branch 'master' into blender2.8
Diffstat (limited to 'release')
17 files changed, 953 insertions, 104 deletions
diff --git a/release/scripts/modules/bl_app_override/__init__.py b/release/scripts/modules/bl_app_override/__init__.py new file mode 100644 index 00000000000..efd7c525e62 --- /dev/null +++ b/release/scripts/modules/bl_app_override/__init__.py @@ -0,0 +1,200 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +""" +Module to manage overriding various parts of Blender. + +Intended for use with 'app_templates', though it can be used from anywhere. +""" + + +# TODO, how to check these aren't from add-ons. +# templates might need to un-register while filtering. +def class_filter(cls_parent, **kw): + whitelist = kw.pop("whitelist", None) + blacklist = kw.pop("blacklist", None) + kw_items = tuple(kw.items()) + for cls in cls_parent.__subclasses__(): + # same as is_registered() + if "bl_rna" in cls.__dict__: + if blacklist is not None and cls.__name__ in blacklist: + continue + if ((whitelist is not None and cls.__name__ is whitelist) or + all((getattr(cls, attr) in expect) for attr, expect in kw_items)): + yield cls + + +def ui_draw_filter_register( + *, + ui_ignore_classes=None, + ui_ignore_operator=None, + ui_ignore_property=None, + ui_ignore_menu=None, + ui_ignore_label=None, +): + import bpy + + UILayout = bpy.types.UILayout + + if ui_ignore_classes is None: + ui_ignore_classes = ( + bpy.types.Panel, + bpy.types.Menu, + bpy.types.Header, + ) + + class OperatorProperties_Fake: + pass + + class UILayout_Fake(bpy.types.UILayout): + __slots__ = () + + def __getattribute__(self, attr): + # ensure we always pass down UILayout_Fake instances + if attr in {"row", "split", "column", "box", "column_flow"}: + real_func = UILayout.__getattribute__(self, attr) + + def dummy_func(*args, **kw): + # print("wrapped", attr) + ret = real_func(*args, **kw) + return UILayout_Fake(ret) + return dummy_func + + elif attr in {"operator", "operator_menu_enum", "operator_enum"}: + if ui_ignore_operator is None: + return UILayout.__getattribute__(self, attr) + + real_func = UILayout.__getattribute__(self, attr) + + def dummy_func(*args, **kw): + # print("wrapped", attr) + if not ui_ignore_operator(args[0]): + ret = real_func(*args, **kw) + else: + # UILayout.__getattribute__(self, "label")() + # may need to be set + ret = OperatorProperties_Fake() + return ret + return dummy_func + + elif attr in {"prop", "prop_enum"}: + if ui_ignore_property is None: + return UILayout.__getattribute__(self, attr) + + real_func = UILayout.__getattribute__(self, attr) + + def dummy_func(*args, **kw): + # print("wrapped", attr) + if not ui_ignore_property(args[0].__class__.__name__, args[1]): + ret = real_func(*args, **kw) + else: + ret = None + return ret + return dummy_func + + elif attr == "menu": + if ui_ignore_menu is None: + return UILayout.__getattribute__(self, attr) + + real_func = UILayout.__getattribute__(self, attr) + + def dummy_func(*args, **kw): + # print("wrapped", attr) + if not ui_ignore_menu(args[0]): + ret = real_func(*args, **kw) + else: + ret = None + return ret + return dummy_func + + elif attr == "label": + if ui_ignore_label is None: + return UILayout.__getattribute__(self, attr) + + real_func = UILayout.__getattribute__(self, attr) + + def dummy_func(*args, **kw): + # print("wrapped", attr) + if not ui_ignore_label(args[0] if args else kw.get("text", "")): + ret = real_func(*args, **kw) + else: + # ret = real_func() + ret = None + return ret + return dummy_func + else: + return UILayout.__getattribute__(self, attr) + # print(self, attr) + + def operator(*args, **kw): + return super().operator(*args, **kw) + + def draw_override(func_orig, self_real, context): + # simple, no wrapping + # return func_orig(self_wrap, context) + + class Wrapper(self_real.__class__): + __slots__ = () + def __getattribute__(self, attr): + if attr == "layout": + return UILayout_Fake(self_real.layout) + else: + cls = super() + try: + return cls.__getattr__(self, attr) + except AttributeError: + # class variable + try: + return getattr(cls, attr) + except AttributeError: + # for preset bl_idname access + return getattr(UILayout(self), attr) + + @property + def layout(self): + # print("wrapped") + return self_real.layout + + return func_orig(Wrapper(self_real), context) + + ui_ignore_store = [] + + for cls in ui_ignore_classes: + for subcls in list(cls.__subclasses__()): + if "draw" in subcls.__dict__: # don't want to get parents draw() + + def replace_draw(): + # function also serves to hold draw_old in a local name-space + draw_orig = subcls.draw + + def draw(self, context): + return draw_override(draw_orig, self, context) + subcls.draw = draw + + ui_ignore_store.append((subcls, "draw", subcls.draw)) + + replace_draw() + + return ui_ignore_store + + +def ui_draw_filter_unregister(ui_ignore_store): + for (obj, attr, value) in ui_ignore_store: + setattr(obj, attr, value) diff --git a/release/scripts/modules/bl_app_override/helpers.py b/release/scripts/modules/bl_app_override/helpers.py new file mode 100644 index 00000000000..981039e8ddc --- /dev/null +++ b/release/scripts/modules/bl_app_override/helpers.py @@ -0,0 +1,167 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +# ----------------------------------------------------------------------------- +# AppOverrideState + + +class AppOverrideState: + """ + Utility class to encapsulate overriding the application state + so that settings can be restored afterwards. + """ + __slots__ = ( + # setup_classes + "_class_store", + # setup_ui_ignore + "_ui_ignore_store", + # setup_addons + "_addon_store", + ) + + # --------- + # Callbacks + # + # Set as None, to make it simple to check if they're being overridden. + + # setup/teardown classes + class_ignore = None + + # setup/teardown ui_ignore + ui_ignore_classes = None + ui_ignore_operator = None + ui_ignore_property = None + ui_ignore_menu = None + ui_ignore_label = None + + addon_paths = None + addons = None + + # End callbacks + + def __init__(self): + self._class_store = None + self._addon_store = None + self._ui_ignore_store = None + + def _setup_classes(self): + import bpy + assert(self._class_store is None) + self._class_store = self.class_ignore() + from bpy.utils import unregister_class + for cls in self._class_store: + unregister_class(cls) + + def _teardown_classes(self): + assert(self._class_store is not None) + + from bpy.utils import register_class + for cls in self._class_store: + register_class(cls) + self._class_store = None + + def _setup_ui_ignore(self): + import bl_app_override + + self._ui_ignore_store = bl_app_override.ui_draw_filter_register( + ui_ignore_classes=( + None if self.ui_ignore_classes is None + else self.ui_ignore_classes() + ), + ui_ignore_operator=self.ui_ignore_operator, + ui_ignore_property=self.ui_ignore_property, + ui_ignore_menu=self.ui_ignore_menu, + ui_ignore_label=self.ui_ignore_label, + ) + + def _teardown_ui_ignore(self): + import bl_app_override + bl_app_override.ui_draw_filter_unregister( + self._ui_ignore_store + ) + self._ui_ignore_store = None + + def _setup_addons(self): + import sys + import os + + sys_path = [] + if self.addon_paths is not None: + for path in self.addon_paths(): + if path not in sys.path: + sys.path.append(path) + + import addon_utils + addons = [] + if self.addons is not None: + addons.extend(self.addons()) + for addon in addons: + addon_utils.enable(addon) + + self._addon_store = { + "sys_path": sys_path, + "addons": addons, + } + + def _teardown_addons(self): + import sys + + sys_path = self._addon_store["sys_path"] + for path in sys_path: + # should always succeed, but if not it doesn't matter + # (someone else was changing the sys.path), ignore! + try: + sys.path.remove(path) + except: + pass + + addons = self._addon_store["addons"] + import addon_utils + for addon in addons: + addon_utils.disable(addon) + + self._addon_store.clear() + self._addon_store = None + + def setup(self): + if self.class_ignore is not None: + self._setup_classes() + + if any((self.addon_paths, + self.addons, + )): + self._setup_addons() + + if any((self.ui_ignore_operator, + self.ui_ignore_property, + self.ui_ignore_menu, + self.ui_ignore_label, + )): + self._setup_ui_ignore() + + def teardown(self): + if self._class_store is not None: + self._teardown_classes() + + if self._addon_store is not None: + self._teardown_addons() + + if self._ui_ignore_store is not None: + self._teardown_ui_ignore() diff --git a/release/scripts/modules/bl_app_template_utils.py b/release/scripts/modules/bl_app_template_utils.py new file mode 100644 index 00000000000..b3a4824aa7b --- /dev/null +++ b/release/scripts/modules/bl_app_template_utils.py @@ -0,0 +1,198 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +""" +Similar to ``addon_utils``, except we can only have one active at a time. + +In most cases users of this module will simply call 'activate'. +""" + +__all__ = ( + "activate", + "import_from_path", + "import_from_id", + "reset", +) + +import bpy as _bpy + +# Normally matches 'user_preferences.app_template_id', +# but loading new preferences will get us out of sync. +_app_template = { + "id": "", +} + +# instead of sys.modules +# note that we only ever have one template enabled at a time +# so it may not seem necessary to use this. +# +# However, templates may want to share between each-other, +# so any loaded modules are stored here? +# +# Note that the ID here is the app_template_id , not the modules __name__. +_modules = {} + + +def _enable(template_id, *, handle_error=None, ignore_not_found=False): + import os + import sys + from bpy_restrict_state import RestrictBlend + + if handle_error is None: + def handle_error(ex): + import traceback + traceback.print_exc() + + # Split registering up into 2 steps so we can undo + # if it fails par way through. + + # disable the context, using the context at all is + # really bad while loading an template, don't do it! + with RestrictBlend(): + + # 1) try import + try: + mod = import_from_id(template_id, ignore_not_found=ignore_not_found) + if mod is None: + return None + mod.__template_enabled__ = False + _modules[template_id] = mod + except Exception as ex: + handle_error(ex) + return None + + # 2) try run the modules register function + try: + mod.register() + except Exception as ex: + print("Exception in module register(): %r" % + getattr(mod, "__file__", template_id)) + handle_error(ex) + del _modules[template_id] + return None + + # * OK loaded successfully! * + mod.__template_enabled__ = True + + if _bpy.app.debug_python: + print("\tapp_template_utils.enable", mod.__name__) + + return mod + + +def _disable(template_id, *, handle_error=None): + """ + Disables a template by name. + + :arg template_id: The name of the template and module. + :type template_id: string + :arg handle_error: Called in the case of an error, + taking an exception argument. + :type handle_error: function + """ + import sys + + if handle_error is None: + def handle_error(ex): + import traceback + traceback.print_exc() + + mod = _modules.get(template_id) + + if mod and getattr(mod, "__template_enabled__", False) is not False: + mod.__template_enabled__ = False + + try: + mod.unregister() + except Exception as ex: + print("Exception in module unregister(): %r" % + getattr(mod, "__file__", template_id)) + handle_error(ex) + else: + print("\tapp_template_utils.disable: %s not %s." % + (template_id, "disabled" if mod is None else "loaded")) + + if _bpy.app.debug_python: + print("\tapp_template_utils.disable", template_id) + + +def import_from_path(path, ignore_not_found=False): + import os + from importlib import import_module + base_module, template_id = path.rsplit(os.sep, 2)[-2:] + module_name = base_module + "." + template_id + + try: + return import_module(module_name) + except ModuleNotFoundError as ex: + if ignore_not_found and ex.name == module_name: + return None + raise ex + + +def import_from_id(template_id, ignore_not_found=False): + import os + path = next(iter(_bpy.utils.app_template_paths(template_id)), None) + if path is None: + if ignore_not_found: + return None + else: + raise Exception("%r template not found!" % template_id) + else: + if ignore_not_found: + if not os.path.exists(os.path.join(path, "__init__.py")): + return None + return import_from_path(path, ignore_not_found=ignore_not_found) + + +def activate(template_id=None): + template_id_prev = _app_template["id"] + + # not needed but may as well avoid activating same template + # ... in fact keep this, it will show errors early on! + """ + if template_id_prev == template_id: + return + """ + + if template_id_prev: + _disable(template_id_prev) + + # Disable all addons, afterwards caller must reset. + import addon_utils + addon_utils.disable_all() + + # ignore_not_found so modules that don't contain scripts don't raise errors + mod = _enable(template_id, ignore_not_found=True) if template_id else None + + _app_template["id"] = template_id + + +def reset(*, reload_scripts=False): + """ + Sets default state. + """ + template_id = _bpy.context.user_preferences.app_template + if _bpy.app.debug_python: + print("bl_app_template_utils.reset('%s')" % template_id) + + # TODO reload_scripts + + activate(template_id) diff --git a/release/scripts/modules/bpy/utils/__init__.py b/release/scripts/modules/bpy/utils/__init__.py index 31dd836e034..65a2f278465 100644 --- a/release/scripts/modules/bpy/utils/__init__.py +++ b/release/scripts/modules/bpy/utils/__init__.py @@ -32,6 +32,7 @@ __all__ = ( "preset_find", "preset_paths", "refresh_script_paths", + "app_template_paths", "register_class", "register_module", "register_manual_map", @@ -245,6 +246,12 @@ def load_scripts(reload_scripts=False, refresh_scripts=False): for mod in modules_from_path(path, loaded_modules): test_register(mod) + # load template (if set) + if any(_bpy.utils.app_template_paths()): + import bl_app_template_utils + bl_app_template_utils.reset(reload_scripts=reload_scripts) + del bl_app_template_utils + # deal with addons separately _initialize = getattr(_addon_utils, "_initialize", None) if _initialize is not None: @@ -356,6 +363,38 @@ def refresh_script_paths(): _sys_path_ensure(path) +def app_template_paths(subdir=None): + """ + Returns valid application template paths. + + :arg subdir: Optional subdir. + :type subdir: string + :return: app template paths. + :rtype: generator + """ + + # note: LOCAL, USER, SYSTEM order matches script resolution order. + subdir_tuple = (subdir,) if subdir is not None else () + + path = _os.path.join(*( + resource_path('LOCAL'), "scripts", "startup", + "bl_app_templates_user", *subdir_tuple)) + if _os.path.isdir(path): + yield path + else: + path = _os.path.join(*( + resource_path('USER'), "scripts", "startup", + "bl_app_templates_user", *subdir_tuple)) + if _os.path.isdir(path): + yield path + + path = _os.path.join(*( + resource_path('SYSTEM'), "scripts", "startup", + "bl_app_templates_system", *subdir_tuple)) + if _os.path.isdir(path): + yield path + + def preset_paths(subdir): """ Returns a list of paths for a specific preset. diff --git a/release/scripts/startup/bl_operators/clip.py b/release/scripts/startup/bl_operators/clip.py index e3a7bd384c6..6747bfec1fb 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 ae2287ee7bb..4cee1abf15f 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 f5190f0653d..b730e866323 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): @@ -1054,4 +1054,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 c2b057827f1..e10a2a2915e 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/wm.py b/release/scripts/startup/bl_operators/wm.py index 42f1e723d1a..7827e7ab5e2 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" @@ -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) @@ -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) @@ -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'} @@ -1890,7 +1920,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() @@ -1917,10 +1950,12 @@ class WM_OT_addon_refresh(Operator): return {'FINISHED'} +# Note: shares some logic with WM_OT_app_template_install +# but not enough to de-duplicate. Fixed here may apply to both. class WM_OT_addon_install(Operator): "Install an add-on" bl_idname = "wm.addon_install" - bl_label = "Install from File..." + bl_label = "Install Add-on from File..." overwrite = BoolProperty( name="Overwrite", @@ -1951,20 +1986,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 +2038,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 +2056,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 +2091,10 @@ class WM_OT_addon_install(Operator): bpy.utils.refresh_script_paths() # print message - msg = tip_("Modules Installed from %r into %r (%s)") % (pyfile, path_addons, ", ".join(sorted(addons_new))) + msg = ( + tip_("Modules Installed (%s) from %r into %r (%s)") % + (", ".join(sorted(addons_new)), pyfile, path_addons) + ) print(msg) self.report({'INFO'}, msg) @@ -2164,6 +2188,7 @@ class WM_OT_addon_expand(Operator): return {'FINISHED'} + class WM_OT_addon_userpref_show(Operator): "Show add-on user preferences" bl_idname = "wm.addon_userpref_show" @@ -2194,6 +2219,123 @@ class WM_OT_addon_userpref_show(Operator): return {'FINISHED'} +# Note: shares some logic with WM_OT_addon_install +# but not enough to de-duplicate. Fixes here may apply to both. +class WM_OT_app_template_install(Operator): + "Install an application-template" + bl_idname = "wm.app_template_install" + bl_label = "Install Template from File..." + + overwrite = BoolProperty( + name="Overwrite", + description="Remove existing template with the same ID", + default=True, + ) + + filepath = StringProperty( + subtype='FILE_PATH', + ) + filter_folder = BoolProperty( + name="Filter folders", + default=True, + options={'HIDDEN'}, + ) + filter_python = BoolProperty( + name="Filter python", + default=True, + options={'HIDDEN'}, + ) + filter_glob = StringProperty( + default="*.py;*.zip", + options={'HIDDEN'}, + ) + + def execute(self, context): + import traceback + import zipfile + import shutil + import os + + pyfile = self.filepath + + path_app_templates = bpy.utils.user_resource( + 'SCRIPTS', os.path.join("startup", "bl_app_templates_user"), + create=True, + ) + + if not path_app_templates: + self.report({'ERROR'}, "Failed to get add-ons path") + return {'CANCELLED'} + + if not os.path.isdir(path_app_templates): + try: + os.makedirs(path_app_templates, exist_ok=True) + except: + traceback.print_exc() + + app_templates_old = set(os.listdir(path_app_templates)) + + # check to see if the file is in compressed format (.zip) + if zipfile.is_zipfile(pyfile): + try: + file_to_extract = zipfile.ZipFile(pyfile, 'r') + except: + traceback.print_exc() + return {'CANCELLED'} + + if self.overwrite: + for f in file_to_extract.namelist(): + module_filesystem_remove(path_app_templates, f) + else: + for f in file_to_extract.namelist(): + path_dest = os.path.join(path_app_templates, os.path.basename(f)) + if os.path.exists(path_dest): + self.report({'WARNING'}, "File already installed to %r\n" % path_dest) + return {'CANCELLED'} + + try: # extract the file to "bl_app_templates_user" + file_to_extract.extractall(path_app_templates) + except: + traceback.print_exc() + return {'CANCELLED'} + + else: + path_dest = os.path.join(path_app_templates, os.path.basename(pyfile)) + + if self.overwrite: + module_filesystem_remove(path_app_templates, os.path.basename(pyfile)) + elif os.path.exists(path_dest): + self.report({'WARNING'}, "File already installed to %r\n" % path_dest) + return {'CANCELLED'} + + # if not compressed file just copy into the addon path + try: + shutil.copyfile(pyfile, path_dest) + except: + traceback.print_exc() + return {'CANCELLED'} + + app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old + + # in case a new module path was created to install this addon. + bpy.utils.refresh_script_paths() + + # print message + msg = ( + tip_("Template Installed (%s) from %r into %r") % + (", ".join(sorted(app_templates_new)), pyfile, path_app_templates) + ) + print(msg) + self.report({'INFO'}, msg) + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + wm.fileselect_add(self) + return {'RUNNING_MODAL'} + + classes = ( BRUSH_OT_active_index_set, WM_OT_addon_disable, @@ -2203,6 +2345,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 +2389,4 @@ classes = ( WM_OT_sysinfo, WM_OT_theme_install, WM_OT_url_open, -)
\ No newline at end of file +) diff --git a/release/scripts/startup/bl_ui/space_info.py b/release/scripts/startup/bl_ui/space_info.py index 189160c2fde..f986235cc90 100644 --- a/release/scripts/startup/bl_ui/space_info.py +++ b/release/scripts/startup/bl_ui/space_info.py @@ -127,6 +127,18 @@ class INFO_MT_file(Menu): layout.operator("wm.save_homefile", icon='SAVE_PREFS') layout.operator("wm.read_factory_settings", icon='LOAD_FACTORY') + if any(bpy.utils.app_template_paths()): + app_template = context.user_preferences.app_template + if app_template: + layout.operator( + "wm.read_factory_settings", + text="Load Factory Template Settings", + icon='LOAD_FACTORY', + ).app_template = app_template + del app_template + + layout.menu("USERPREF_MT_app_templates", icon='FILE_BLEND') + layout.separator() layout.operator_context = 'INVOKE_AREA' diff --git a/release/scripts/startup/bl_ui/space_userpref.py b/release/scripts/startup/bl_ui/space_userpref.py index fe126f6522c..f4e2cf006b2 100644 --- a/release/scripts/startup/bl_ui/space_userpref.py +++ b/release/scripts/startup/bl_ui/space_userpref.py @@ -90,6 +90,63 @@ class USERPREF_MT_interaction_presets(Menu): draw = Menu.draw_preset +class USERPREF_MT_app_templates(Menu): + bl_label = "Application Templates" + preset_subdir = "app_templates" + + def draw_ex(self, context, *, use_splash=False, use_default=False, use_install=False): + import os + + layout = self.layout + + # now draw the presets + layout.operator_context = 'EXEC_DEFAULT' + + if use_default: + props = layout.operator("wm.read_homefile", text="Default") + props.use_splash = True + props.app_template = "" + layout.separator() + + template_paths = bpy.utils.app_template_paths() + + # expand template paths + app_templates = [] + for path in template_paths: + for d in os.listdir(path): + if d.startswith(("__", ".")): + continue + template = os.path.join(path, d) + if os.path.isdir(template): + # template_paths_expand.append(template) + app_templates.append(d) + + for d in sorted(app_templates): + props = layout.operator( + "wm.read_homefile", + text=bpy.path.display_name(d), + ) + props.use_splash = True + props.app_template = d; + + if use_install: + layout.separator() + layout.operator_context = 'INVOKE_DEFAULT' + props = layout.operator("wm.app_template_install") + + + def draw(self, context): + self.draw_ex(context, use_splash=False, use_default=True, use_install=True) + + +class USERPREF_MT_templates_splash(Menu): + bl_label = "Startup Templates" + preset_subdir = "templates" + + def draw(self, context): + USERPREF_MT_app_templates.draw_ex(self, context, use_splash=True, use_default=True) + + class USERPREF_MT_appconfigs(Menu): bl_label = "AppPresets" preset_subdir = "keyconfig" @@ -110,7 +167,17 @@ class USERPREF_MT_splash(Menu): split = layout.split() row = split.row() - row.label("") + + if any(bpy.utils.app_template_paths()): + row.label("Template:") + template = context.user_preferences.app_template + row.menu( + "USERPREF_MT_templates_splash", + text=bpy.path.display_name(template) if template else "Default", + ) + else: + row.label("") + row = split.row() row.label("Interaction:") @@ -1485,6 +1552,8 @@ classes = ( USERPREF_HT_header, USERPREF_PT_tabs, USERPREF_MT_interaction_presets, + USERPREF_MT_templates_splash, + USERPREF_MT_app_templates, USERPREF_MT_appconfigs, USERPREF_MT_splash, USERPREF_MT_splash_footer, diff --git a/release/scripts/startup/nodeitems_builtins.py b/release/scripts/startup/nodeitems_builtins.py index e915aa5bb72..afc6e5455c4 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 |