Bastien Montagne b8febd87e1 Fix #121712: outdated mapping between UI translations and Doc languages.
Both lists got heavily out of sync over the years.

Some day would be nice to have this managed in a single place.
2024-05-14 16:53:00 +02:00

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