Both lists got heavily out of sync over the years. Some day would be nice to have this managed in a single place.
1173 lines
36 KiB
Python
1173 lines
36 KiB
Python
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
"""
|
|
This module contains utility functions specific to blender but
|
|
not associated with blenders internal data.
|
|
"""
|
|
|
|
__all__ = (
|
|
"blend_paths",
|
|
"escape_identifier",
|
|
"flip_name",
|
|
"unescape_identifier",
|
|
"keyconfig_init",
|
|
"keyconfig_set",
|
|
"load_scripts",
|
|
"modules_from_path",
|
|
"preset_find",
|
|
"preset_paths",
|
|
"refresh_script_paths",
|
|
"app_template_paths",
|
|
"register_class",
|
|
"register_cli_command",
|
|
"unregister_cli_command",
|
|
"register_manual_map",
|
|
"unregister_manual_map",
|
|
"register_classes_factory",
|
|
"register_submodule_factory",
|
|
"register_tool",
|
|
"make_rna_paths",
|
|
"manual_map",
|
|
"manual_language_code",
|
|
"previews",
|
|
"resource_path",
|
|
"script_path_user",
|
|
"script_paths",
|
|
"smpte_from_frame",
|
|
"smpte_from_seconds",
|
|
"units",
|
|
"unregister_class",
|
|
"unregister_tool",
|
|
"user_resource",
|
|
"execfile",
|
|
)
|
|
|
|
from _bpy import (
|
|
_utils_units as units,
|
|
blend_paths,
|
|
escape_identifier,
|
|
flip_name,
|
|
unescape_identifier,
|
|
register_class,
|
|
register_cli_command,
|
|
resource_path,
|
|
script_paths as _bpy_script_paths,
|
|
unregister_class,
|
|
unregister_cli_command,
|
|
user_resource as _user_resource,
|
|
system_resource,
|
|
)
|
|
|
|
import bpy as _bpy
|
|
import os as _os
|
|
import sys as _sys
|
|
|
|
import addon_utils as _addon_utils
|
|
|
|
_preferences = _bpy.context.preferences
|
|
_is_factory_startup = _bpy.app.factory_startup
|
|
|
|
# Directories added to the start of `sys.path` for all of Blender's "scripts" directories.
|
|
_script_module_dirs = "startup", "modules"
|
|
|
|
# Base scripts, this points to the directory containing: "modules" & "startup" (see `_script_module_dirs`).
|
|
# In Blender's code-base this is `./scripts`.
|
|
#
|
|
# NOTE: in virtually all cases this should match `BLENDER_SYSTEM_SCRIPTS` as this script is itself a system script,
|
|
# it must be in the `BLENDER_SYSTEM_SCRIPTS` by definition and there is no need for a look-up from `_bpy_script_paths`.
|
|
_script_base_dir = _os.path.dirname(_os.path.dirname(_os.path.dirname(_os.path.dirname(__file__))))
|
|
|
|
|
|
def execfile(filepath, *, mod=None):
|
|
"""
|
|
Execute a file path as a Python script.
|
|
|
|
:arg filepath: Path of the script to execute.
|
|
:type filepath: string
|
|
:arg mod: Optional cached module, the result of a previous execution.
|
|
:type mod: Module or None
|
|
:return: The module which can be passed back in as ``mod``.
|
|
:rtype: ModuleType
|
|
"""
|
|
|
|
import importlib.util
|
|
mod_name = "__main__"
|
|
mod_spec = importlib.util.spec_from_file_location(mod_name, filepath)
|
|
if mod is None:
|
|
mod = importlib.util.module_from_spec(mod_spec)
|
|
|
|
# While the module name is not added to `sys.modules`, it's important to temporarily
|
|
# include this so statements such as `sys.modules[cls.__module__].__dict__` behave as expected.
|
|
# See: https://bugs.python.org/issue9499 for details.
|
|
modules = _sys.modules
|
|
mod_orig = modules.get(mod_name, None)
|
|
modules[mod_name] = mod
|
|
|
|
# No error suppression, just ensure `sys.modules[mod_name]` is properly restored in the case of an error.
|
|
try:
|
|
mod_spec.loader.exec_module(mod)
|
|
finally:
|
|
if mod_orig is None:
|
|
modules.pop(mod_name, None)
|
|
else:
|
|
modules[mod_name] = mod_orig
|
|
|
|
return mod
|
|
|
|
|
|
def _test_import(module_name, loaded_modules):
|
|
use_time = _bpy.app.debug_python
|
|
|
|
if module_name in loaded_modules:
|
|
return None
|
|
if "." in module_name:
|
|
print("Ignoring '{:s}', can't import files containing multiple periods".format(module_name))
|
|
return None
|
|
|
|
if use_time:
|
|
import time
|
|
t = time.time()
|
|
|
|
try:
|
|
mod = __import__(module_name)
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return None
|
|
|
|
if use_time:
|
|
print("time {:s} {:.4f}".format(module_name, time.time() - t))
|
|
|
|
loaded_modules.add(mod.__name__) # should match mod.__name__ too
|
|
return mod
|
|
|
|
|
|
# Check before adding paths as reloading would add twice.
|
|
|
|
# Storing and restoring the full `sys.path` is risky as this may be intentionally modified
|
|
# by technical users/developers.
|
|
#
|
|
# Instead, track which paths have been added, clearing them before refreshing.
|
|
# This supports the case of loading a new preferences file which may reset scripts path.
|
|
_sys_path_ensure_paths = set()
|
|
|
|
|
|
def _sys_path_ensure_prepend(path):
|
|
if path not in _sys.path:
|
|
_sys.path.insert(0, path)
|
|
_sys_path_ensure_paths.add(path)
|
|
|
|
|
|
def _sys_path_ensure_append(path):
|
|
if path not in _sys.path:
|
|
_sys.path.append(path)
|
|
_sys_path_ensure_paths.add(path)
|
|
|
|
|
|
def modules_from_path(path, loaded_modules):
|
|
"""
|
|
Load all modules in a path and return them as a list.
|
|
|
|
:arg path: this path is scanned for scripts and packages.
|
|
:type path: string
|
|
:arg loaded_modules: already loaded module names, files matching these
|
|
names will be ignored.
|
|
:type loaded_modules: set
|
|
:return: all loaded modules.
|
|
:rtype: list
|
|
"""
|
|
modules = []
|
|
|
|
for mod_name, _mod_path in _bpy.path.module_names(path):
|
|
mod = _test_import(mod_name, loaded_modules)
|
|
if mod:
|
|
modules.append(mod)
|
|
|
|
return modules
|
|
|
|
|
|
_global_loaded_modules = [] # store loaded module names for reloading.
|
|
import bpy_types as _bpy_types # keep for comparisons, never ever reload this.
|
|
|
|
|
|
def load_scripts(*, reload_scripts=False, refresh_scripts=False, extensions=True):
|
|
"""
|
|
Load scripts and run each modules register function.
|
|
|
|
:arg reload_scripts: Causes all scripts to have their unregister method
|
|
called before loading.
|
|
:type reload_scripts: bool
|
|
:arg refresh_scripts: only load scripts which are not already loaded
|
|
as modules.
|
|
:type refresh_scripts: bool
|
|
:arg extensions: Loads additional scripts (add-ons & app-templates).
|
|
:type extensions: bool
|
|
"""
|
|
use_time = use_class_register_check = _bpy.app.debug_python
|
|
use_user = not _is_factory_startup
|
|
|
|
if use_time:
|
|
import time
|
|
t_main = time.time()
|
|
|
|
loaded_modules = set()
|
|
|
|
if refresh_scripts:
|
|
original_modules = _sys.modules.values()
|
|
|
|
if reload_scripts:
|
|
# just unload, don't change user defaults, this means we can sync
|
|
# to reload. note that they will only actually reload of the
|
|
# modification time changes. This `won't` work for packages so...
|
|
# its not perfect.
|
|
for addon_module_name in [ext.module for ext in _preferences.addons]:
|
|
_addon_utils.disable(addon_module_name)
|
|
|
|
def register_module_call(mod):
|
|
register = getattr(mod, "register", None)
|
|
if register:
|
|
try:
|
|
register()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
else:
|
|
print(
|
|
"\nWarning! {!r} has no register function, "
|
|
"this is now a requirement for registerable scripts".format(mod.__file__)
|
|
)
|
|
|
|
def unregister_module_call(mod):
|
|
unregister = getattr(mod, "unregister", None)
|
|
if unregister:
|
|
try:
|
|
unregister()
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
def test_reload(mod):
|
|
import importlib
|
|
# reloading this causes internal errors
|
|
# because the classes from this module are stored internally
|
|
# possibly to refresh internal references too but for now, best not to.
|
|
if mod == _bpy_types:
|
|
return mod
|
|
|
|
try:
|
|
return importlib.reload(mod)
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
def test_register(mod):
|
|
|
|
if refresh_scripts and mod in original_modules:
|
|
return
|
|
|
|
if reload_scripts and mod:
|
|
print("Reloading:", mod)
|
|
mod = test_reload(mod)
|
|
|
|
if mod:
|
|
register_module_call(mod)
|
|
_global_loaded_modules.append(mod.__name__)
|
|
|
|
if reload_scripts:
|
|
|
|
# module names -> modules
|
|
_global_loaded_modules[:] = [_sys.modules[mod_name]
|
|
for mod_name in _global_loaded_modules]
|
|
|
|
# loop over and unload all scripts
|
|
_global_loaded_modules.reverse()
|
|
for mod in _global_loaded_modules:
|
|
unregister_module_call(mod)
|
|
|
|
for mod in _global_loaded_modules:
|
|
test_reload(mod)
|
|
|
|
del _global_loaded_modules[:]
|
|
|
|
# Update key-maps to account for operators no longer existing.
|
|
# Typically unloading operators would refresh the event system (such as disabling an add-on)
|
|
# however reloading scripts re-enable all add-ons immediately (which may inspect key-maps).
|
|
# For this reason it's important to update key-maps which will have been tagged to update.
|
|
# Without this, add-on register functions accessing key-map properties can crash, see: #111702.
|
|
_bpy.context.window_manager.keyconfigs.update(keep_properties=True)
|
|
|
|
from bpy_restrict_state import RestrictBlend
|
|
|
|
with RestrictBlend():
|
|
for base_path in script_paths(use_user=use_user):
|
|
for path_subdir in _script_module_dirs:
|
|
path = _os.path.join(base_path, path_subdir)
|
|
if _os.path.isdir(path):
|
|
_sys_path_ensure_prepend(path)
|
|
|
|
# Only add to `sys.modules` unless this is 'startup'.
|
|
if path_subdir == "startup":
|
|
for mod in modules_from_path(path, loaded_modules):
|
|
test_register(mod)
|
|
|
|
if reload_scripts:
|
|
# Update key-maps for key-map items referencing operators defined in "startup".
|
|
# Without this, key-map items wont be set properly, see: #113309.
|
|
_bpy.context.window_manager.keyconfigs.update()
|
|
|
|
if extensions:
|
|
load_scripts_extensions(reload_scripts=reload_scripts)
|
|
|
|
if reload_scripts:
|
|
_bpy.context.window_manager.tag_script_reload()
|
|
|
|
import gc
|
|
print("gc.collect() -> {:d}".format(gc.collect()))
|
|
|
|
if use_time:
|
|
print("Python Script Load Time {:.4f}".format(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})".format(subcls.__name__, cls.__name__))
|
|
|
|
|
|
def load_scripts_extensions(*, reload_scripts=False):
|
|
"""
|
|
Load extensions scripts (add-ons and app-templates)
|
|
|
|
:arg reload_scripts: Causes all scripts to have their unregister method
|
|
called before loading.
|
|
:type reload_scripts: bool
|
|
"""
|
|
# 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 add-ons separately.
|
|
_initialize_once = getattr(_addon_utils, "_initialize_once", None)
|
|
if _initialize_once is not None:
|
|
# first time, use fast-path
|
|
_initialize_once()
|
|
del _addon_utils._initialize_once
|
|
else:
|
|
_addon_utils.reset_all(reload_scripts=reload_scripts)
|
|
del _initialize_once
|
|
|
|
|
|
def script_path_user():
|
|
"""returns the env var and falls back to home dir or None"""
|
|
path = _user_resource('SCRIPTS')
|
|
return _os.path.normpath(path) if path else None
|
|
|
|
|
|
def script_paths_pref():
|
|
"""Returns a list of user preference script directories."""
|
|
paths = []
|
|
for script_directory in _preferences.filepaths.script_directories:
|
|
directory = script_directory.directory
|
|
if directory:
|
|
paths.append(_os.path.normpath(directory))
|
|
return paths
|
|
|
|
|
|
def script_paths(*, subdir=None, user_pref=True, check_all=False, use_user=True):
|
|
"""
|
|
Returns a list of valid script paths.
|
|
|
|
:arg subdir: Optional subdir.
|
|
:type subdir: string
|
|
:arg user_pref: Include the user preference script paths.
|
|
:type user_pref: bool
|
|
:arg check_all: Include local, user and system paths rather just the paths Blender uses.
|
|
:type check_all: bool
|
|
:return: script paths.
|
|
:rtype: list
|
|
"""
|
|
|
|
if check_all or use_user:
|
|
path_system, path_user = _bpy_script_paths()
|
|
|
|
base_paths = []
|
|
|
|
if check_all:
|
|
# Order: 'LOCAL', 'USER', 'SYSTEM' (where user is optional).
|
|
if path_local := resource_path('LOCAL'):
|
|
base_paths.append(_os.path.join(path_local, "scripts"))
|
|
if use_user:
|
|
base_paths.append(path_user)
|
|
base_paths.append(path_system) # Same as: `system_resource('SCRIPTS')`.
|
|
|
|
# Note that `_script_base_dir` may be either:
|
|
# - `os.path.join(bpy.utils.resource_path('LOCAL'), "scripts")`
|
|
# - `bpy.utils.system_resource('SCRIPTS')`.
|
|
# When `check_all` is enabled duplicate paths will be added however
|
|
# paths are de-duplicated so it wont cause problems.
|
|
base_paths.append(_script_base_dir)
|
|
|
|
if not check_all:
|
|
if use_user:
|
|
base_paths.append(path_user)
|
|
|
|
if user_pref:
|
|
base_paths.extend(script_paths_pref())
|
|
|
|
scripts = []
|
|
for path in base_paths:
|
|
if not path:
|
|
continue
|
|
|
|
path = _os.path.normpath(path)
|
|
if subdir is not None:
|
|
path = _os.path.join(path, subdir)
|
|
|
|
if path in scripts:
|
|
continue
|
|
if not _os.path.isdir(path):
|
|
continue
|
|
scripts.append(path)
|
|
|
|
return scripts
|
|
|
|
|
|
def refresh_script_paths():
|
|
"""
|
|
Run this after creating new script paths to update sys.path
|
|
"""
|
|
|
|
for path in _sys_path_ensure_paths:
|
|
try:
|
|
_sys.path.remove(path)
|
|
except ValueError:
|
|
pass
|
|
_sys_path_ensure_paths.clear()
|
|
|
|
for base_path in script_paths():
|
|
for path_subdir in _script_module_dirs:
|
|
path = _os.path.join(base_path, path_subdir)
|
|
if _os.path.isdir(path):
|
|
_sys_path_ensure_prepend(path)
|
|
|
|
for path in _addon_utils.paths():
|
|
_sys_path_ensure_append(path)
|
|
path = _os.path.join(path, "modules")
|
|
if _os.path.isdir(path):
|
|
_sys_path_ensure_append(path)
|
|
|
|
|
|
def app_template_paths(*, path=None):
|
|
"""
|
|
Returns valid application template paths.
|
|
|
|
:arg path: Optional subdir.
|
|
:type path: string
|
|
:return: app template paths.
|
|
:rtype: generator
|
|
"""
|
|
subdir_args = (path,) if path is not None else ()
|
|
# Note: keep in sync with: Blender's 'BKE_appdir_app_template_any'.
|
|
# Uses 'BLENDER_USER_SCRIPTS', 'BLENDER_SYSTEM_SCRIPTS'
|
|
# ... in this case 'system' accounts for 'local' too.
|
|
for resource_fn, module_name in (
|
|
(_user_resource, "bl_app_templates_user"),
|
|
(system_resource, "bl_app_templates_system"),
|
|
):
|
|
path_test = resource_fn('SCRIPTS', path=_os.path.join("startup", module_name, *subdir_args))
|
|
if path_test and _os.path.isdir(path_test):
|
|
yield path_test
|
|
|
|
|
|
def preset_paths(subdir):
|
|
"""
|
|
Returns a list of paths for a specific preset.
|
|
|
|
:arg subdir: preset subdirectory (must not be an absolute path).
|
|
:type subdir: string
|
|
:return: script paths.
|
|
:rtype: list
|
|
"""
|
|
dirs = []
|
|
for path in script_paths(subdir="presets", check_all=True):
|
|
directory = _os.path.join(path, subdir)
|
|
if not directory.startswith(path):
|
|
raise Exception("invalid subdir given {!r}".format(subdir))
|
|
elif _os.path.isdir(directory):
|
|
dirs.append(directory)
|
|
|
|
# Find addons preset paths
|
|
for path in _addon_utils.paths():
|
|
directory = _os.path.join(path, "presets", subdir)
|
|
if _os.path.isdir(directory):
|
|
dirs.append(directory)
|
|
|
|
return dirs
|
|
|
|
|
|
def is_path_builtin(path):
|
|
"""
|
|
Returns True if the path is one of the built-in paths used by Blender.
|
|
|
|
:arg path: Path you want to check if it is in the built-in settings directory
|
|
:type path: str
|
|
:rtype: bool
|
|
"""
|
|
# Note that this function isn't optimized for speed,
|
|
# it's intended to be used to check if it's OK to remove presets.
|
|
#
|
|
# If this is used in a draw-loop for example, we could cache some of the values.
|
|
user_path = resource_path('USER')
|
|
|
|
for res in ('SYSTEM', 'LOCAL'):
|
|
parent_path = resource_path(res)
|
|
if not parent_path or parent_path == user_path:
|
|
# Make sure that the current path is not empty string and that it is
|
|
# not the same as the user config path. IE "~/.config/blender" on Linux
|
|
# This can happen on portable installs.
|
|
continue
|
|
|
|
try:
|
|
if _os.path.samefile(
|
|
_os.path.commonpath([parent_path]),
|
|
_os.path.commonpath([parent_path, path])
|
|
):
|
|
return True
|
|
except FileNotFoundError:
|
|
# The path we tried to look up doesn't exist.
|
|
pass
|
|
except ValueError:
|
|
# Happens on Windows when paths don't have the same drive.
|
|
pass
|
|
|
|
return False
|
|
|
|
|
|
def smpte_from_seconds(time, *, fps=None, fps_base=None):
|
|
"""
|
|
Returns an SMPTE formatted string from the *time*:
|
|
``HH:MM:SS:FF``.
|
|
|
|
If *fps* and *fps_base* are not given the current scene is used.
|
|
|
|
:arg time: time in seconds.
|
|
:type time: int, float or ``datetime.timedelta``.
|
|
:return: the frame string.
|
|
:rtype: string
|
|
"""
|
|
|
|
return smpte_from_frame(
|
|
time_to_frame(time, fps=fps, fps_base=fps_base),
|
|
fps=fps,
|
|
fps_base=fps_base
|
|
)
|
|
|
|
|
|
def smpte_from_frame(frame, *, fps=None, fps_base=None):
|
|
"""
|
|
Returns an SMPTE formatted string from the *frame*:
|
|
``HH:MM:SS:FF``.
|
|
|
|
If *fps* and *fps_base* are not given the current scene is used.
|
|
|
|
:arg frame: frame number.
|
|
:type frame: int or float.
|
|
:return: the frame string.
|
|
:rtype: string
|
|
"""
|
|
|
|
if fps is None:
|
|
fps = _bpy.context.scene.render.fps
|
|
|
|
if fps_base is None:
|
|
fps_base = _bpy.context.scene.render.fps_base
|
|
|
|
fps = fps / fps_base
|
|
sign = "-" if frame < 0 else ""
|
|
frame = abs(frame)
|
|
|
|
return (
|
|
"{:s}{:02d}:{:02d}:{:02d}:{:02d}".format(
|
|
sign,
|
|
int(frame / (3600 * fps)), # HH
|
|
int((frame / (60 * fps)) % 60), # MM
|
|
int((frame / fps) % 60), # SS
|
|
int(frame % fps), # FF
|
|
)
|
|
)
|
|
|
|
|
|
def time_from_frame(frame, *, fps=None, fps_base=None):
|
|
"""
|
|
Returns the time from a frame number .
|
|
|
|
If *fps* and *fps_base* are not given the current scene is used.
|
|
|
|
:arg frame: number.
|
|
:type frame: int or float.
|
|
:return: the time in seconds.
|
|
:rtype: datetime.timedelta
|
|
"""
|
|
|
|
if fps is None:
|
|
fps = _bpy.context.scene.render.fps
|
|
|
|
if fps_base is None:
|
|
fps_base = _bpy.context.scene.render.fps_base
|
|
|
|
fps = fps / fps_base
|
|
|
|
from datetime import timedelta
|
|
|
|
return timedelta(0, frame / fps)
|
|
|
|
|
|
def time_to_frame(time, *, fps=None, fps_base=None):
|
|
"""
|
|
Returns a float frame number from a time given in seconds or
|
|
as a datetime.timedelta object.
|
|
|
|
If *fps* and *fps_base* are not given the current scene is used.
|
|
|
|
:arg time: time in seconds.
|
|
:type time: number or a ``datetime.timedelta`` object
|
|
:return: the frame.
|
|
:rtype: float
|
|
"""
|
|
|
|
if fps is None:
|
|
fps = _bpy.context.scene.render.fps
|
|
|
|
if fps_base is None:
|
|
fps_base = _bpy.context.scene.render.fps_base
|
|
|
|
fps = fps / fps_base
|
|
|
|
from datetime import timedelta
|
|
|
|
if isinstance(time, timedelta):
|
|
time = time.total_seconds()
|
|
|
|
return time * fps
|
|
|
|
|
|
def preset_find(name, preset_path, *, display_name=False, ext=".py"):
|
|
if not name:
|
|
return None
|
|
|
|
for directory in preset_paths(preset_path):
|
|
|
|
if display_name:
|
|
filename = ""
|
|
for fn in _os.listdir(directory):
|
|
if fn.endswith(ext) and name == _bpy.path.display_name(fn, title_case=False):
|
|
filename = fn
|
|
break
|
|
else:
|
|
filename = name + ext
|
|
|
|
if filename:
|
|
filepath = _os.path.join(directory, filename)
|
|
if _os.path.exists(filepath):
|
|
return filepath
|
|
|
|
|
|
def keyconfig_init():
|
|
# Key configuration initialization and refresh, called from the Blender
|
|
# window manager on startup and refresh.
|
|
default_config = "Blender"
|
|
active_config = _preferences.keymap.active_keyconfig
|
|
|
|
# Load the default key configuration.
|
|
filepath = preset_find(default_config, "keyconfig")
|
|
keyconfig_set(filepath)
|
|
|
|
# Set the active key configuration if different.
|
|
if default_config != active_config:
|
|
filepath = preset_find(active_config, "keyconfig")
|
|
if filepath:
|
|
keyconfig_set(filepath)
|
|
|
|
|
|
def keyconfig_set(filepath, *, report=None):
|
|
from os.path import basename, splitext
|
|
|
|
if _bpy.app.debug_python:
|
|
print("loading preset:", filepath)
|
|
|
|
keyconfigs = _bpy.context.window_manager.keyconfigs
|
|
name = splitext(basename(filepath))[0]
|
|
|
|
# Store the old key-configuration case of error, to know if it should be removed or not on failure.
|
|
kc_old = keyconfigs.get(name)
|
|
|
|
try:
|
|
error_msg = ""
|
|
execfile(filepath)
|
|
except:
|
|
import traceback
|
|
error_msg = traceback.format_exc()
|
|
|
|
kc_new = keyconfigs.get(name)
|
|
|
|
if error_msg:
|
|
if report is not None:
|
|
report({'ERROR'}, error_msg)
|
|
print(error_msg)
|
|
if (kc_new is not None) and (kc_new != kc_old):
|
|
keyconfigs.remove(kc_new)
|
|
return False
|
|
|
|
# Get name, exception for default keymap to keep backwards compatibility.
|
|
if kc_new is None:
|
|
if report is not None:
|
|
report({'ERROR'}, "Failed to load keymap {!r}".format(filepath))
|
|
return False
|
|
else:
|
|
keyconfigs.active = kc_new
|
|
return True
|
|
|
|
|
|
def user_resource(resource_type, *, path="", create=False):
|
|
"""
|
|
Return a user resource path (normally from the users home directory).
|
|
|
|
:arg type: Resource type in ['DATAFILES', 'CONFIG', 'SCRIPTS', 'EXTENSIONS'].
|
|
:type type: string
|
|
:arg path: Optional subdirectory.
|
|
:type path: string
|
|
:arg create: Treat the path as a directory and create
|
|
it if its not existing.
|
|
:type create: boolean
|
|
:return: a path.
|
|
:rtype: string
|
|
"""
|
|
|
|
target_path = _user_resource(resource_type, path=path)
|
|
|
|
if create:
|
|
# should always be true.
|
|
if target_path:
|
|
# create path if not existing.
|
|
if not _os.path.exists(target_path):
|
|
try:
|
|
_os.makedirs(target_path)
|
|
except:
|
|
import traceback
|
|
traceback.print_exc()
|
|
target_path = ""
|
|
elif not _os.path.isdir(target_path):
|
|
print("Path {!r} found but isn't a directory!".format(target_path))
|
|
target_path = ""
|
|
|
|
return target_path
|
|
|
|
|
|
def register_classes_factory(classes):
|
|
"""
|
|
Utility function to create register and unregister functions
|
|
which simply registers and unregisters a sequence of classes.
|
|
"""
|
|
def register():
|
|
for cls in classes:
|
|
register_class(cls)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
unregister_class(cls)
|
|
|
|
return register, unregister
|
|
|
|
|
|
def register_submodule_factory(module_name, submodule_names):
|
|
"""
|
|
Utility function to create register and unregister functions
|
|
which simply load submodules,
|
|
calling their register & unregister functions.
|
|
|
|
.. note::
|
|
|
|
Modules are registered in the order given,
|
|
unregistered in reverse order.
|
|
|
|
:arg module_name: The module name, typically ``__name__``.
|
|
:type module_name: string
|
|
:arg submodule_names: List of submodule names to load and unload.
|
|
:type submodule_names: list of strings
|
|
:return: register and unregister functions.
|
|
:rtype: tuple pair of functions
|
|
"""
|
|
|
|
module = None
|
|
submodules = []
|
|
|
|
def register():
|
|
nonlocal module
|
|
module = __import__(name=module_name, fromlist=submodule_names)
|
|
submodules[:] = [getattr(module, name) for name in submodule_names]
|
|
for mod in submodules:
|
|
mod.register()
|
|
|
|
def unregister():
|
|
from sys import modules
|
|
for mod in reversed(submodules):
|
|
mod.unregister()
|
|
name = mod.__name__
|
|
delattr(module, name.partition(".")[2])
|
|
del modules[name]
|
|
submodules.clear()
|
|
|
|
return register, unregister
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Tool Registration
|
|
|
|
|
|
def register_tool(tool_cls, *, after=None, separator=False, group=False):
|
|
"""
|
|
Register a tool in the toolbar.
|
|
|
|
:arg tool: A tool subclass.
|
|
:type tool: :class:`bpy.types.WorkSpaceTool` subclass.
|
|
:arg space_type: Space type identifier.
|
|
:type space_type: string
|
|
:arg after: Optional identifiers this tool will be added after.
|
|
:type after: collection of strings or None.
|
|
:arg separator: When true, add a separator before this tool.
|
|
:type separator: bool
|
|
:arg group: When true, add a new nested group of tools.
|
|
:type group: bool
|
|
"""
|
|
space_type = tool_cls.bl_space_type
|
|
context_mode = tool_cls.bl_context_mode
|
|
|
|
from bl_ui.space_toolsystem_common import (
|
|
ToolSelectPanelHelper,
|
|
ToolDef,
|
|
)
|
|
|
|
cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
|
|
if cls is None:
|
|
raise Exception("Space type {!r} has no toolbar".format(space_type))
|
|
tools = cls._tools[context_mode]
|
|
|
|
# First sanity check
|
|
from bpy.types import WorkSpaceTool
|
|
tools_id = {
|
|
item.idname for item in ToolSelectPanelHelper._tools_flatten(tools)
|
|
if item is not None
|
|
}
|
|
if not issubclass(tool_cls, WorkSpaceTool):
|
|
raise Exception("Expected WorkSpaceTool subclass, not {!r}".format(type(tool_cls)))
|
|
if tool_cls.bl_idname in tools_id:
|
|
raise Exception("Tool {!r} already exists!".format(tool_cls.bl_idname))
|
|
del tools_id, WorkSpaceTool
|
|
|
|
# Convert the class into a ToolDef.
|
|
def tool_from_class(tool_cls):
|
|
# Convert class to tuple, store in the class for removal.
|
|
tool_def = ToolDef.from_dict({
|
|
"idname": tool_cls.bl_idname,
|
|
"label": tool_cls.bl_label,
|
|
"description": getattr(tool_cls, "bl_description", tool_cls.__doc__),
|
|
"icon": getattr(tool_cls, "bl_icon", None),
|
|
"cursor": getattr(tool_cls, "bl_cursor", None),
|
|
"options": getattr(tool_cls, "bl_options", None),
|
|
"widget": getattr(tool_cls, "bl_widget", None),
|
|
"widget_properties": getattr(tool_cls, "bl_widget_properties", None),
|
|
"keymap": getattr(tool_cls, "bl_keymap", None),
|
|
"data_block": getattr(tool_cls, "bl_data_block", None),
|
|
"operator": getattr(tool_cls, "bl_operator", None),
|
|
"draw_settings": getattr(tool_cls, "draw_settings", None),
|
|
"draw_cursor": getattr(tool_cls, "draw_cursor", None),
|
|
})
|
|
tool_cls._bl_tool = tool_def
|
|
|
|
keymap_data = tool_def.keymap
|
|
if keymap_data is not None and callable(keymap_data[0]):
|
|
from bpy import context
|
|
wm = context.window_manager
|
|
keyconfigs = wm.keyconfigs
|
|
kc_default = keyconfigs.default
|
|
# Note that Blender's default tools use the default key-config for both.
|
|
# We need to use the add-ons for 3rd party tools so reloading the key-map doesn't clear them.
|
|
kc = keyconfigs.addon
|
|
if kc is not None:
|
|
if context_mode is None:
|
|
context_descr = "All"
|
|
else:
|
|
context_descr = context_mode.replace("_", " ").title()
|
|
cls._km_action_simple(kc_default, kc, context_descr, tool_def.label, keymap_data)
|
|
return tool_def
|
|
|
|
tool_converted = tool_from_class(tool_cls)
|
|
|
|
if group:
|
|
# Create a new group
|
|
tool_converted = (tool_converted,)
|
|
|
|
tool_def_insert = (
|
|
(None, tool_converted) if separator else
|
|
(tool_converted,)
|
|
)
|
|
|
|
def skip_to_end_of_group(seq, i):
|
|
i_prev = i
|
|
while i < len(seq) and seq[i] is not None:
|
|
i_prev = i
|
|
i += 1
|
|
return i_prev
|
|
|
|
changed = False
|
|
if after is not None:
|
|
for i, item in enumerate(tools):
|
|
if item is None:
|
|
pass
|
|
elif isinstance(item, ToolDef):
|
|
if item.idname in after:
|
|
i = skip_to_end_of_group(item, i)
|
|
tools[i + 1:i + 1] = tool_def_insert
|
|
changed = True
|
|
break
|
|
elif isinstance(item, tuple):
|
|
for j, sub_item in enumerate(item, 1):
|
|
if isinstance(sub_item, ToolDef):
|
|
if sub_item.idname in after:
|
|
if group:
|
|
# Can't add a group within a group,
|
|
# add a new group after this group.
|
|
i = skip_to_end_of_group(tools, i)
|
|
tools[i + 1:i + 1] = tool_def_insert
|
|
else:
|
|
j = skip_to_end_of_group(item, j)
|
|
item = item[:j + 1] + tool_def_insert + item[j + 1:]
|
|
tools[i] = item
|
|
changed = True
|
|
break
|
|
if changed:
|
|
break
|
|
|
|
if not changed:
|
|
print("bpy.utils.register_tool: could not find 'after'", after)
|
|
if not changed:
|
|
tools.extend(tool_def_insert)
|
|
|
|
|
|
def unregister_tool(tool_cls):
|
|
space_type = tool_cls.bl_space_type
|
|
context_mode = tool_cls.bl_context_mode
|
|
|
|
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
|
|
cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
|
|
if cls is None:
|
|
raise Exception("Space type {!r} has no toolbar".format(space_type))
|
|
tools = cls._tools[context_mode]
|
|
|
|
tool_def = tool_cls._bl_tool
|
|
try:
|
|
i = tools.index(tool_def)
|
|
except ValueError:
|
|
i = -1
|
|
|
|
def tool_list_clean(tool_list):
|
|
# Trim separators.
|
|
while tool_list and tool_list[-1] is None:
|
|
del tool_list[-1]
|
|
while tool_list and tool_list[0] is None:
|
|
del tool_list[0]
|
|
# Remove duplicate separators.
|
|
for i in range(len(tool_list) - 1, -1, -1):
|
|
is_none = tool_list[i] is None
|
|
if is_none and prev_is_none:
|
|
del tool_list[i]
|
|
prev_is_none = is_none
|
|
|
|
changed = False
|
|
if i != -1:
|
|
del tools[i]
|
|
tool_list_clean(tools)
|
|
changed = True
|
|
|
|
if not changed:
|
|
for i, item in enumerate(tools):
|
|
if isinstance(item, tuple):
|
|
try:
|
|
j = item.index(tool_def)
|
|
except ValueError:
|
|
j = -1
|
|
|
|
if j != -1:
|
|
item_clean = list(item)
|
|
del item_clean[j]
|
|
tool_list_clean(item_clean)
|
|
if item_clean:
|
|
tools[i] = tuple(item_clean)
|
|
else:
|
|
del tools[i]
|
|
tool_list_clean(tools)
|
|
del item_clean
|
|
|
|
# tuple(sub_item for sub_item in items if sub_item is not tool_def)
|
|
changed = True
|
|
break
|
|
|
|
if not changed:
|
|
raise Exception("Unable to remove {!r}".format(tool_cls))
|
|
del tool_cls._bl_tool
|
|
|
|
keymap_data = tool_def.keymap
|
|
if keymap_data is not None:
|
|
from bpy import context
|
|
wm = context.window_manager
|
|
keyconfigs = wm.keyconfigs
|
|
for kc in (keyconfigs.default, keyconfigs.addon):
|
|
if kc is None:
|
|
continue
|
|
km = kc.keymaps.get(keymap_data[0])
|
|
if km is None:
|
|
print("Warning keymap {!r} not found in {!r}!".format(keymap_data[0], kc.name))
|
|
else:
|
|
kc.keymaps.remove(km)
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Manual lookups, each function has to return a basepath and a sequence
|
|
# of...
|
|
|
|
# we start with the built-in default mapping
|
|
def _blender_default_map():
|
|
# NOTE(@ideasman42): Avoid importing this as there is no need to keep the lookup table in memory.
|
|
# As this runs when the user accesses the "Online Manual", the overhead loading the file is acceptable.
|
|
# In my tests it's under 1/100th of a second loading from a `pyc`.
|
|
ref_mod = execfile(_os.path.join(_script_base_dir, "modules", "rna_manual_reference.py"))
|
|
return (ref_mod.url_manual_prefix, ref_mod.url_manual_mapping)
|
|
|
|
|
|
# hooks for doc lookups
|
|
_manual_map = [_blender_default_map]
|
|
|
|
|
|
def register_manual_map(manual_hook):
|
|
_manual_map.append(manual_hook)
|
|
|
|
|
|
def unregister_manual_map(manual_hook):
|
|
_manual_map.remove(manual_hook)
|
|
|
|
|
|
def manual_map():
|
|
# reverse so default is called last
|
|
for cb in reversed(_manual_map):
|
|
try:
|
|
prefix, url_manual_mapping = cb()
|
|
except:
|
|
print("Error calling {!r}".format(cb))
|
|
import traceback
|
|
traceback.print_exc()
|
|
continue
|
|
|
|
yield prefix, url_manual_mapping
|
|
|
|
|
|
# Languages which are supported by the user manual (commented when there is no translation).
|
|
_manual_language_codes = {
|
|
# "ab": "ab", #Abkhaz
|
|
# "am_ET": "am", # Amharic
|
|
"ar_EG": "ar", # Arabic
|
|
# "be": "be", # Belarusian
|
|
# "bg_BG": "bg", # Bulgarian
|
|
"ca_AD": "ca", # Catalan
|
|
# "cs_CZ": "cz", # Czech
|
|
# "da": "da", # Danish
|
|
"de_DE": "de", # German
|
|
"el_GR": "el", # Greek
|
|
# "eo": "eo", # Esperanto
|
|
"es": "es", # Spanish
|
|
# "et_EE": "et", # Estonian
|
|
# "eu_EU": "eu", # Basque
|
|
# "fa_IR": "fa", # Persian
|
|
"fi_FI": "fi", # Finnish
|
|
"fr_FR": "fr", # French
|
|
# "ha": "ha", # Hausa
|
|
# "he_IL": "he", # Hebrew
|
|
# "hi_IN": "hi", # Hindi
|
|
# "hr_HR": "hr", # Croatian
|
|
# "hu_HU": "hu", # Hungarian
|
|
"id_ID": "id", # Indonesian
|
|
"it_IT": "it", # Italian
|
|
"ja_JP": "ja", # Japanese
|
|
# "ka": "ka", # Georgian
|
|
# "kk_KZ": "kk", # kazakh
|
|
# "km": "km", # Khmer
|
|
"ko_KR": "ko", # Korean
|
|
# "ky_KG": "ky", # Kyrgyz
|
|
# "nb": "nb", # Norwegian
|
|
# "ne_NP": "ne", # Nepali
|
|
"nl_NL": "nl", # Dutch
|
|
# "pl_PL": "pl", # Polish
|
|
"pt_PT": "pt", # Portuguese
|
|
# Portuguese - Brazil, for until we have a pt_BR version.
|
|
"pt_BR": "pt",
|
|
# "ro_RO": "ro", # Romanian
|
|
"ru_RU": "ru", # Russian
|
|
"sk_SK": "sk", # Slovak
|
|
# "sl": "sl", # Slovenian
|
|
"sr_RS": "sr", # Serbian
|
|
# "sv_SE": "sv", # Swedish
|
|
# "sw": "sw", # Swahili
|
|
# "ta": "ta", # Tamil
|
|
"th_TH": "th", # Thai
|
|
# "tr_TR": "tr", # Turkish
|
|
"uk_UA": "uk", # Ukrainian
|
|
# "uz_UZ": "uz", # Uzbek
|
|
"vi_VN": "vi", # Vietnamese
|
|
"zh_HANS": "zh-hans", # Simplified Chinese
|
|
"zh_HANT": "zh-hant", # Traditional Chinese
|
|
}
|
|
|
|
|
|
def manual_language_code(default="en"):
|
|
"""
|
|
:return:
|
|
The language code used for user manual URL component based on the current language user-preference,
|
|
falling back to the ``default`` when unavailable.
|
|
:rtype: str
|
|
"""
|
|
language = _bpy.context.preferences.view.language
|
|
if language == 'DEFAULT':
|
|
language = _os.getenv("LANG", "").split(".")[0]
|
|
return _manual_language_codes.get(language, default)
|
|
|
|
|
|
# Build an RNA path from struct/property/enum names.
|
|
def make_rna_paths(struct_name, prop_name, enum_name):
|
|
"""
|
|
Create RNA "paths" from given names.
|
|
|
|
:arg struct_name: Name of a RNA struct (like e.g. "Scene").
|
|
:type struct_name: string
|
|
:arg prop_name: Name of a RNA struct's property.
|
|
:type prop_name: string
|
|
:arg enum_name: Name of a RNA enum identifier.
|
|
:type enum_name: string
|
|
:return: A triple of three "RNA paths"
|
|
(most_complete_path, "struct.prop", "struct.prop:'enum'").
|
|
If no enum_name is given, the third element will always be void.
|
|
:rtype: tuple of strings
|
|
"""
|
|
src = src_rna = src_enum = ""
|
|
if struct_name:
|
|
if prop_name:
|
|
src = src_rna = ".".join((struct_name, prop_name))
|
|
if enum_name:
|
|
src = src_enum = "{:s}:'{:s}'".format(src_rna, enum_name)
|
|
else:
|
|
src = src_rna = struct_name
|
|
return src, src_rna, src_enum
|