Files
test/scripts/startup/bl_operators/wm.py
Damien Picard 58b8ae1717 UI: Fix a few typos
- "Acion Slot" -> "Action": typo.
- "Specifies the input node used the created zone" -> "by the created
  zone": typo.
- "No reference available {!r}, Update..." -> "update": lower case.
- "uninitialized file-list" -> "Uninitialized": sentence case.

Some issues reported by Alexandr Fatih.

Pull Request: https://projects.blender.org/blender/blender/pulls/148265
2025-10-17 16:13:30 +02:00

3723 lines
130 KiB
Python

# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import annotations
import bpy
from bpy.types import (
Menu,
Operator,
)
from bpy.props import (
BoolProperty,
CollectionProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
BoolVectorProperty,
IntVectorProperty,
FloatVectorProperty,
)
from bpy.app.translations import (
pgettext_iface as iface_,
pgettext_n as n_,
pgettext_tip as tip_,
pgettext_rpt as rpt_,
contexts as i18n_contexts,
)
def _rna_path_prop_search_for_context_impl(context, edit_text, unique_attrs):
# Use the same logic as auto-completing in the Python console to expand the data-path.
from _bl_console_utils.autocomplete import intellisense
context_prefix = "context."
line = context_prefix + edit_text
cursor = len(line)
namespace = {"context": context}
comp_prefix, _, comp_options = intellisense.expand(line=line, cursor=cursor, namespace=namespace, private=False)
prefix = comp_prefix[len(context_prefix):] # Strip "context."
for attr in comp_options.split("\n"):
if attr.endswith((
# Exclude function calls because they are generally not part of data-paths.
"(", ")",
# RNA properties for introspection, not useful to expand.
".bl_rna", ".rna_type",
)):
continue
# If we type/paste in complete attributes, intellisense expands with a ".", remove that again (see #134092)
attr_full = (prefix + attr.lstrip()).removesuffix(".")
if attr_full in unique_attrs:
continue
unique_attrs.add(attr_full)
yield attr_full
def rna_path_prop_search_for_context(self, context, edit_text):
# NOTE(@campbellbarton): Limiting data-path expansion is rather arbitrary.
# It's possible for example that someone would want to set a shortcut in the preferences or
# in other region types than those currently expanded. Unless there is a reasonable likelihood
# users might expand these space-type/region-type combinations - exclude them from this search.
# After all, this list is mainly intended as a hint, users are not prevented from constructing
# the data-paths themselves.
unique_attrs = set()
for window in context.window_manager.windows:
for area in window.screen.areas:
# Users are very unlikely to be setting shortcuts in the preferences, skip this.
if area.type == 'PREFERENCES':
continue
# Ignore the same region type multiple times in an area.
# Prevents the 3D-viewport quad-view from attempting to expand 3 extra times for example
region_type_unique = set()
for region in area.regions:
if region.type not in {'WINDOW', 'PREVIEW'}:
continue
if region.type in region_type_unique:
continue
region_type_unique.add(region.type)
with context.temp_override(window=window, area=area, region=region):
yield from _rna_path_prop_search_for_context_impl(context, edit_text, unique_attrs)
if not unique_attrs:
# Users *might* only have a preferences area shown, in that case just expand the current context.
yield from _rna_path_prop_search_for_context_impl(context, edit_text, unique_attrs)
rna_path_prop = StringProperty(
name="Context Attributes",
description="Context data-path (expanded using visible windows in the current .blend file)",
maxlen=1024,
search=rna_path_prop_search_for_context,
)
rna_reverse_prop = BoolProperty(
name="Reverse",
description="Cycle backwards",
default=False,
options={'SKIP_SAVE'},
)
rna_wrap_prop = BoolProperty(
name="Wrap",
description="Wrap back to the first/last values",
default=False,
options={'SKIP_SAVE'},
)
rna_relative_prop = BoolProperty(
name="Relative",
description="Apply relative to the current value (delta)",
default=False,
options={'SKIP_SAVE'},
)
rna_space_type_prop = EnumProperty(
name="Type",
items=tuple(
(e.identifier, e.name, "", e. value)
for e in bpy.types.Space.bl_rna.properties["type"].enum_items
),
default='EMPTY',
)
# Note, this can be used for more operators,
# currently not used for all "WM_OT_context_" operators.
rna_module_prop = StringProperty(
name="Module",
description="Optionally override the context with a module",
maxlen=1024,
)
def context_path_validate(context, data_path):
try:
value = eval("context.{:s}".format(data_path)) if data_path else Ellipsis
except AttributeError as ex:
if str(ex).startswith("'NoneType'"):
# One of the items in the rna path is None, just ignore this
value = Ellipsis
else:
# Print invalid path, but don't show error to the users and fully
# break the UI if the operator is bound to an event like left click.
print("context_path_validate error: context.{:s} not found (invalid keymap entry?)".format(data_path))
value = Ellipsis
return value
def context_path_to_rna_property(context, data_path):
from _bl_rna_utils.data_path import property_definition_from_data_path
rna_prop = property_definition_from_data_path(context, "." + data_path)
if rna_prop is not None:
return rna_prop
return None
def context_path_decompose(data_path):
# Decompose a data_path into 3 components:
# base_path, prop_attr, prop_item, where:
# `"foo.bar["baz"].fiz().bob.buz[10][2]"`, returns...
# `("foo.bar["baz"].fiz().bob", "buz", "[10][2]")`
#
# This is useful as we often want the base and the property, ignoring any item access.
# Note that item access includes function calls since these aren't properties.
#
# Note that the `.` is removed from the start of the first and second values,
# this is done because `.attr` isn't convenient to use as an argument,
# also the convention is not to include this within the data paths or the operator logic for `bpy.ops.wm.*`.
from _bl_rna_utils.data_path import decompose_data_path
path_split = decompose_data_path("." + data_path)
# Find the last property that isn't a function call.
value_prev = ""
i = len(path_split)
while (i := i - 1) >= 0:
value = path_split[i]
if value.startswith("."):
if not value_prev.startswith("("):
break
value_prev = value
if i != -1:
base_path = "".join(path_split[:i])
prop_attr = path_split[i]
prop_item = "".join(path_split[i + 1:])
if base_path:
assert base_path.startswith(".")
base_path = base_path[1:]
if prop_attr:
assert prop_attr.startswith(".")
prop_attr = prop_attr[1:]
else:
# If there are no properties, everything is an item.
# Note that should not happen in practice with values which are added onto `context`,
# include since it's correct to account for this case and not doing so will create a confusing exception.
base_path = ""
prop_attr = ""
prop_item = "".join(path_split)
return (base_path, prop_attr, prop_item)
def description_from_data_path(base, data_path, *, prefix, value=Ellipsis):
if context_path_validate(base, data_path) is Ellipsis:
return None
if (
(rna_prop := context_path_to_rna_property(base, data_path)) and
(description := tip_(rna_prop.description))
):
description = tip_("{:s}: {:s}").format(prefix, description)
if value != Ellipsis:
description = "{:s}\n{:s}: {:s}".format(description, tip_("Value"), str(value))
return description
return None
def operator_value_is_undo(value):
if value in {None, Ellipsis}:
return False
# typical properties or objects
id_data = getattr(value, "id_data", Ellipsis)
if id_data is None:
return False
elif id_data is Ellipsis:
# handle mathutils types
id_data = getattr(getattr(value, "owner", None), "id_data", None)
if id_data is None:
return False
# return True if its a non window ID type
return (
isinstance(id_data, bpy.types.ID) and
(not isinstance(id_data, (
bpy.types.WindowManager,
bpy.types.Screen,
bpy.types.Brush,
)))
)
def operator_path_is_undo(context, data_path):
data_path_head, _, _ = context_path_decompose(data_path)
# When we can't find the data owner assume no undo is needed.
if not data_path_head:
return False
value = context_path_validate(context, data_path_head)
return operator_value_is_undo(value)
def operator_path_undo_return(context, data_path):
return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
def operator_value_undo_return(value):
return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
def execute_context_assign(self, context):
data_path = self.data_path
if context_path_validate(context, data_path) is Ellipsis:
return {'PASS_THROUGH'}
if getattr(self, "relative", False):
exec("context.{:s} += self.value".format(data_path))
else:
exec("context.{:s} = self.value".format(data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_set_boolean(Operator):
"""Set a context value"""
bl_idname = "wm.context_set_boolean"
bl_label = "Context Set Boolean"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: BoolProperty(
name="Value",
description="Assignment value",
default=True,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
execute = execute_context_assign
class WM_OT_context_set_int(Operator): # same as enum
"""Set a context value"""
bl_idname = "wm.context_set_int"
bl_label = "Context Set"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: IntProperty(
name="Value",
description="Assign value",
default=0,
)
relative: rna_relative_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
execute = execute_context_assign
class WM_OT_context_scale_float(Operator):
"""Scale a float context value"""
bl_idname = "wm.context_scale_float"
bl_label = "Context Scale Float"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: FloatProperty(
name="Value",
description="Assign value",
default=1.0,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Scale"), value=props.value)
def execute(self, context):
data_path = self.data_path
if context_path_validate(context, data_path) is Ellipsis:
return {'PASS_THROUGH'}
value = self.value
if value == 1.0: # nothing to do
return {'CANCELLED'}
exec("context.{:s} *= value".format(data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_scale_int(Operator):
"""Scale an int context value"""
bl_idname = "wm.context_scale_int"
bl_label = "Context Scale Int"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: FloatProperty(
name="Value",
description="Assign value",
default=1.0,
)
always_step: BoolProperty(
name="Always Step",
description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
default=True,
options={'SKIP_SAVE'},
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Scale"), value=props.value)
def execute(self, context):
data_path = self.data_path
if context_path_validate(context, data_path) is Ellipsis:
return {'PASS_THROUGH'}
value = self.value
if value == 1.0: # nothing to do
return {'CANCELLED'}
if getattr(self, "always_step", False):
if value > 1.0:
add = "1"
func = "max"
else:
add = "-1"
func = "min"
exec("context.{:s} = {:s}(round(context.{:s} * value), context.{:s} + {:s})".format(
data_path, func, data_path, data_path, add,
))
else:
exec("context.{:s} *= value".format(data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_set_float(Operator): # same as enum
"""Set a context value"""
bl_idname = "wm.context_set_float"
bl_label = "Context Set Float"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: FloatProperty(
name="Value",
description="Assignment value",
default=0.0,
)
relative: rna_relative_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
execute = execute_context_assign
class WM_OT_context_set_string(Operator): # same as enum
"""Set a context value"""
bl_idname = "wm.context_set_string"
bl_label = "Context Set String"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: StringProperty(
name="Value",
description="Assign value",
maxlen=1024,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
execute = execute_context_assign
class WM_OT_context_set_enum(Operator):
"""Set a context value"""
bl_idname = "wm.context_set_enum"
bl_label = "Context Set Enum"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: StringProperty(
name="Value",
description="Assignment value (as a string)",
maxlen=1024,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
execute = execute_context_assign
class WM_OT_context_set_value(Operator):
"""Set a context value"""
bl_idname = "wm.context_set_value"
bl_label = "Context Set Value"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: StringProperty(
name="Value",
description="Assignment value (as a string)",
maxlen=1024,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Assign"), value=props.value)
def execute(self, context):
data_path = self.data_path
if context_path_validate(context, data_path) is Ellipsis:
return {'PASS_THROUGH'}
exec("context.{:s} = {:s}".format(data_path, self.value))
return operator_path_undo_return(context, data_path)
class WM_OT_context_toggle(Operator):
"""Toggle a context value"""
bl_idname = "wm.context_toggle"
bl_label = "Context Toggle"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
module: rna_module_prop
@classmethod
def description(cls, context, props):
# Currently unsupported, it might be possible to extract this.
if props.module:
return None
return description_from_data_path(context, props.data_path, prefix=tip_("Toggle"))
def execute(self, context):
data_path = self.data_path
module = self.module
if not module:
base = context
else:
from importlib import import_module
base = import_module(self.module)
if context_path_validate(base, data_path) is Ellipsis:
return {'PASS_THROUGH'}
exec("base.{:s} = not (base.{:s})".format(data_path, data_path))
return operator_path_undo_return(base, data_path)
class WM_OT_context_toggle_enum(Operator):
"""Toggle a context value"""
bl_idname = "wm.context_toggle_enum"
bl_label = "Context Toggle Values"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value_1: StringProperty(
name="Value",
description="Toggle enum",
maxlen=1024,
)
value_2: StringProperty(
name="Value",
description="Toggle enum",
maxlen=1024,
)
@classmethod
def description(cls, context, props):
value = "({!r}, {!r})".format(props.value_1, props.value_2)
return description_from_data_path(context, props.data_path, prefix=tip_("Toggle"), value=value)
def execute(self, context):
data_path = self.data_path
if context_path_validate(context, data_path) is Ellipsis:
return {'PASS_THROUGH'}
# failing silently is not ideal, but we don't want errors for shortcut
# keys that some values that are only available in a particular context
try:
exec(
"context.{:s} = {!r} if (context.{:s} != {!r}) else {!r}".format(
data_path,
self.value_2,
data_path,
self.value_2,
self.value_1,
)
)
except Exception:
return {'PASS_THROUGH'}
return operator_path_undo_return(context, data_path)
class WM_OT_context_cycle_int(Operator):
"""Set a context value (useful for cycling active material, """ \
"""shape keys, groups, etc.)"""
bl_idname = "wm.context_cycle_int"
bl_label = "Context Int Cycle"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
reverse: rna_reverse_prop
wrap: rna_wrap_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Cycle"))
def execute(self, context):
data_path = self.data_path
value = context_path_validate(context, data_path)
if value is Ellipsis:
return {'PASS_THROUGH'}
if self.reverse:
value -= 1
else:
value += 1
exec("context.{:s} = value".format(data_path))
if self.wrap:
if value != eval("context.{:s}".format(data_path)):
# relies on rna clamping integers out of the range
if self.reverse:
value = (1 << 31) - 1
else:
value = -1 << 31
exec("context.{:s} = value".format(data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_cycle_enum(Operator):
"""Toggle a context value"""
bl_idname = "wm.context_cycle_enum"
bl_label = "Context Enum Cycle"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
reverse: rna_reverse_prop
wrap: rna_wrap_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Cycle"))
def execute(self, context):
data_path = self.data_path
value = context_path_validate(context, data_path)
if value is Ellipsis:
return {'PASS_THROUGH'}
orig_value = value
rna_prop = context_path_to_rna_property(context, data_path)
if type(rna_prop) != bpy.types.EnumProperty:
raise Exception("expected an enum property")
enums = rna_prop.enum_items.keys()
orig_index = enums.index(orig_value)
# Have the info we need, advance to the next item.
#
# When wrap's disabled we may set the value to itself,
# this is done to ensure update callbacks run.
if self.reverse:
if orig_index == 0:
advance_enum = enums[-1] if self.wrap else enums[0]
else:
advance_enum = enums[orig_index - 1]
else:
if orig_index == len(enums) - 1:
advance_enum = enums[0] if self.wrap else enums[-1]
else:
advance_enum = enums[orig_index + 1]
# set the new value
exec("context.{:s} = advance_enum".format(data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_cycle_array(Operator):
"""Set a context array value """ \
"""(useful for cycling the active mesh edit mode)"""
bl_idname = "wm.context_cycle_array"
bl_label = "Context Array Cycle"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
reverse: rna_reverse_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Cycle"))
def execute(self, context):
data_path = self.data_path
value = context_path_validate(context, data_path)
if value is Ellipsis:
return {'PASS_THROUGH'}
def cycle(array):
if self.reverse:
array.insert(0, array.pop())
else:
array.append(array.pop(0))
return array
exec("context.{:s} = cycle(context.{:s}[:])".format(data_path, data_path))
return operator_path_undo_return(context, data_path)
class WM_OT_context_menu_enum(Operator):
bl_idname = "wm.context_menu_enum"
bl_label = "Context Enum Menu"
# The menu items & UI logic handles undo.
bl_options = {'INTERNAL'}
data_path: rna_path_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Menu"))
def execute(self, context):
data_path = self.data_path
value = context_path_validate(context, data_path)
if value is Ellipsis:
return {'PASS_THROUGH'}
base_path, prop_attr, _ = context_path_decompose(data_path)
value_base = context_path_validate(context, base_path)
rna_prop = context_path_to_rna_property(context, data_path)
def draw_cb(self, context):
layout = self.layout
layout.prop(value_base, prop_attr, expand=True)
context.window_manager.popup_menu(draw_func=draw_cb, title=rna_prop.name, icon=rna_prop.icon)
return {'FINISHED'}
class WM_OT_context_pie_enum(Operator):
bl_idname = "wm.context_pie_enum"
bl_label = "Context Enum Pie"
# The menu items & UI logic handles undo.
bl_options = {'INTERNAL'}
data_path: rna_path_prop
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Pie Menu"))
def invoke(self, context, event):
wm = context.window_manager
data_path = self.data_path
value = context_path_validate(context, data_path)
if value is Ellipsis:
return {'PASS_THROUGH'}
base_path, prop_attr, _ = context_path_decompose(data_path)
value_base = context_path_validate(context, base_path)
rna_prop = context_path_to_rna_property(context, data_path)
def draw_cb(self, context):
layout = self.layout
layout.prop(value_base, prop_attr, expand=True)
wm.popup_menu_pie(draw_func=draw_cb, title=rna_prop.name, icon=rna_prop.icon, event=event)
return {'FINISHED'}
class WM_OT_operator_pie_enum(Operator):
bl_idname = "wm.operator_pie_enum"
bl_label = "Operator Enum Pie"
# The menu items & UI logic handles undo.
bl_options = {'INTERNAL'}
data_path: StringProperty(
name="Operator",
description="Operator name (in Python as string)",
maxlen=1024,
)
prop_string: StringProperty(
name="Property",
description="Property name (as a string)",
maxlen=1024,
)
@classmethod
def description(cls, context, props):
return description_from_data_path(context, props.data_path, prefix=tip_("Pie Menu"))
def invoke(self, context, event):
wm = context.window_manager
data_path = self.data_path
prop_attr = self.prop_string
# same as eval("bpy.ops." + data_path)
op_mod_str, ob_id_str = data_path.split(".", 1)
op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
del op_mod_str, ob_id_str
try:
op_rna = op.get_rna_type()
except KeyError:
self.report({'ERROR'}, rpt_("Operator not found: bpy.ops.{:s}").format(data_path))
return {'CANCELLED'}
def draw_cb(self, context):
layout = self.layout
pie = layout.menu_pie()
pie.operator_enum(data_path, prop_attr)
wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
return {'FINISHED'}
class WM_OT_context_set_id(Operator):
"""Set a context value to an ID data-block"""
bl_idname = "wm.context_set_id"
bl_label = "Set Library ID"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path_prop
value: StringProperty(
name="Value",
description="Assign value",
maxlen=1024,
)
def execute(self, context):
value = self.value
data_path = self.data_path
# Match the pointer type from the target property to `bpy.data.*`
# so we lookup the correct list.
rna_prop = context_path_to_rna_property(context, data_path)
rna_prop_fixed_type = rna_prop.fixed_type
id_iter = None
for prop in bpy.data.rna_type.properties:
if prop.rna_type.identifier == "CollectionProperty":
if prop.fixed_type == rna_prop_fixed_type:
id_iter = prop.identifier
break
if id_iter:
value_id = getattr(bpy.data, id_iter).get(value)
exec("context.{:s} = value_id".format(data_path))
return operator_path_undo_return(context, data_path)
doc_id = StringProperty(
name="Doc ID",
maxlen=1024,
options={'HIDDEN'},
)
data_path_iter = StringProperty(
description="The data path relative to the context, must point to an iterable",
)
data_path_item = StringProperty(
description="The data path from each iterable to the value (int or float)",
)
class WM_OT_context_collection_boolean_set(Operator):
"""Set boolean values for a collection of items"""
bl_idname = "wm.context_collection_boolean_set"
bl_label = "Context Collection Boolean Set"
bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
data_path_iter: data_path_iter
data_path_item: data_path_item
type: EnumProperty(
name="Type",
items=(
('TOGGLE', "Toggle", ""),
('ENABLE', "Enable", ""),
('DISABLE', "Disable", ""),
),
)
def execute(self, context):
data_path_iter = self.data_path_iter
data_path_item = self.data_path_item
items = list(getattr(context, data_path_iter))
items_ok = []
is_set = False
for item in items:
try:
value_orig = eval("item." + data_path_item)
except Exception:
continue
if value_orig is True:
is_set = True
elif value_orig is False:
pass
else:
self.report(
{'WARNING'},
rpt_("Non boolean value found: {:s}[ ].{:s}").format(data_path_iter, data_path_item),
)
return {'CANCELLED'}
items_ok.append(item)
# avoid undo push when nothing to do
if not items_ok:
return {'CANCELLED'}
if self.type == 'ENABLE':
is_set = True
elif self.type == 'DISABLE':
is_set = False
else:
is_set = not is_set
exec_str = "item.{:s} = {:s}".format(data_path_item, str(is_set))
for item in items_ok:
exec(exec_str)
return operator_value_undo_return(item)
class WM_OT_context_modal_mouse(Operator):
"""Adjust arbitrary values with mouse input"""
bl_idname = "wm.context_modal_mouse"
bl_label = "Context Modal Mouse"
bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
data_path_iter: data_path_iter
data_path_item: data_path_item
header_text: StringProperty(
name="Header Text",
description="Text to display in header during scale",
)
input_scale: FloatProperty(
description="Scale the mouse movement by this value before applying the delta",
default=0.01,
options={'SKIP_SAVE'},
)
invert: BoolProperty(
description="Invert the mouse input",
default=False,
options={'SKIP_SAVE'},
)
initial_x: IntProperty(options={'HIDDEN'})
def _values_store(self, context):
data_path_iter = self.data_path_iter
data_path_item = self.data_path_item
self._values = values = {}
for item in getattr(context, data_path_iter):
try:
value_orig = eval("item." + data_path_item)
except Exception:
continue
# check this can be set, maybe this is library data.
try:
exec("item.{:s} = {:s}".format(data_path_item, str(value_orig)))
except Exception:
continue
values[item] = value_orig
def _values_delta(self, delta):
delta *= self.input_scale
if self.invert:
delta = - delta
data_path_item = self.data_path_item
for item, value_orig in self._values.items():
if type(value_orig) == int:
exec("item.{:s} = int({:d})".format(data_path_item, round(value_orig + delta)))
else:
exec("item.{:s} = {:f}".format(data_path_item, value_orig + delta))
def _values_restore(self):
data_path_item = self.data_path_item
for item, value_orig in self._values.items():
exec("item.{:s} = {:s}".format(data_path_item, str(value_orig)))
self._values.clear()
def _values_clear(self):
self._values.clear()
def modal(self, context, event):
event_type = event.type
if event_type == 'MOUSEMOVE':
delta = event.mouse_x - self.initial_x
self._values_delta(delta)
header_text = self.header_text
if header_text:
if len(self._values) == 1:
(item, ) = self._values.keys()
header_text = header_text % eval("item.{:s}".format(self.data_path_item))
else:
header_text = (self.header_text % delta) + rpt_(" (delta)")
context.area.header_text_set(header_text)
elif 'LEFTMOUSE' == event_type:
item = next(iter(self._values.keys()))
self._values_clear()
context.area.header_text_set(None)
return operator_value_undo_return(item)
elif event_type in {'RIGHTMOUSE', 'ESC'}:
self._values_restore()
context.area.header_text_set(None)
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self._values_store(context)
if not self._values:
self.report(
{'WARNING'},
rpt_("Nothing to operate on: {:s}[ ].{:s}").format(
self.data_path_iter, self.data_path_item,
),
)
return {'CANCELLED'}
else:
self.initial_x = event.mouse_x
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
class WM_OT_url_open(Operator):
"""Open a website in the web browser"""
bl_idname = "wm.url_open"
bl_label = ""
bl_options = {'INTERNAL'}
url: StringProperty(
name="URL",
description="URL to open",
)
@staticmethod
def _add_utm_param_to_url(url, utm_source):
import urllib.parse
# Parse the URL to get its domain and query parameters.
if not urllib.parse.urlparse(url).scheme:
url = "https://" + url
parsed_url = urllib.parse.urlparse(url)
# Only add a utm source if it points to a blender.org domain.
domain = parsed_url.netloc
if not (domain.endswith(".blender.org") or domain == "blender.org"):
return url
# Parse the query parameters and add or update the utm_source parameter.
query_params = urllib.parse.parse_qs(parsed_url.query)
query_params["utm_source"] = utm_source
new_query = urllib.parse.urlencode(query_params, doseq=True)
# Create a new URL with the updated query parameters.
new_url_parts = list(parsed_url)
new_url_parts[4] = new_query
new_url = urllib.parse.urlunparse(new_url_parts)
return new_url
@staticmethod
def _get_utm_source():
version = bpy.app.version_string
return "blender-" + version.replace(" ", "-").lower()
def execute(self, _context):
import webbrowser
complete_url = self._add_utm_param_to_url(self.url, self._get_utm_source())
webbrowser.open(complete_url)
return {'FINISHED'}
class WM_OT_url_open_preset(Operator):
"""Open a preset website in the web browser"""
bl_idname = "wm.url_open_preset"
bl_label = "Open Preset Website"
bl_options = {'INTERNAL'}
bl_property = "type"
@staticmethod
def _wm_url_open_preset_type_items(_self, _context):
return [item for (item, _) in WM_OT_url_open_preset.preset_items]
type: EnumProperty(
name="Site",
items=WM_OT_url_open_preset._wm_url_open_preset_type_items,
)
def _url_from_bug(self, _context):
from _bpy_internal.system_info.url_prefill_runtime import url_from_blender
return url_from_blender()
def _url_from_release_notes(self, _context):
return "https://www.blender.org/download/releases/{:d}-{:d}/".format(*bpy.app.version[:2])
def _url_from_manual(self, _context):
return "https://docs.blender.org/manual/{:s}/{:d}.{:d}/".format(
bpy.utils.manual_language_code(), *bpy.app.version[:2],
)
def _url_from_api(self, _context):
return "https://docs.blender.org/api/{:d}.{:d}/".format(*bpy.app.version[:2])
# This list is: (enum_item, url) pairs.
# Allow dynamically extending.
preset_items = [
# Dynamic URL's.
(('BUG', iface_("Bug"),
tip_("Report a bug with pre-filled version information")),
_url_from_bug),
(('RELEASE_NOTES', iface_("Release Notes"),
tip_("Read about what's new in this version of Blender")),
_url_from_release_notes),
(('MANUAL', iface_("User Manual"),
tip_("The reference manual for this version of Blender")),
_url_from_manual),
(('API', iface_("Python API Reference"),
tip_("The API reference manual for this version of Blender")),
_url_from_api),
# Static URL's.
(('FUND', iface_("Development Fund"),
tip_("The donation program to support maintenance and improvements")),
"https://fund.blender.org"),
(('BLENDER', "blender.org",
tip_("Blender's official web-site")),
"https://www.blender.org"),
(('CREDITS', iface_("Credits"),
tip_("Lists committers to Blender's source code")),
"https://www.blender.org/about/credits/"),
(('EXTENSIONS', iface_("Extensions Platform"),
tip_("Online directory of free and open source extensions")),
"https://extensions.blender.org/"),
]
def execute(self, context):
url = None
type = self.type
for (item_id, _, _), url in self.preset_items:
if item_id == type:
if callable(url):
url = url(self, context)
break
return bpy.ops.wm.url_open(url=url)
class WM_OT_path_open(Operator):
"""Open a path in a file browser"""
bl_idname = "wm.path_open"
bl_label = ""
bl_options = {'INTERNAL'}
filepath: StringProperty(
subtype='FILE_PATH',
options={'SKIP_SAVE'},
)
def execute(self, _context):
import sys
import os
import subprocess
filepath = self.filepath
if not filepath:
self.report({'ERROR'}, "File path was not set")
return {'CANCELLED'}
filepath = bpy.path.abspath(filepath)
filepath = os.path.normpath(filepath)
if not os.path.exists(filepath):
self.report({'ERROR'}, rpt_("File '{:s}' not found").format(filepath))
return {'CANCELLED'}
if sys.platform[:3] == "win":
os.startfile(filepath)
elif sys.platform == "darwin":
subprocess.check_call(["open", filepath])
else:
try:
subprocess.check_call(["xdg-open", filepath])
except Exception:
# `xdg-open` *should* be supported by recent Gnome, KDE, XFCE.
import traceback
traceback.print_exc()
return {'FINISHED'}
def _wm_doc_get_id(doc_id, *, do_url=True, url_prefix="", report=None):
def operator_exists_pair(a, b):
# Not fast, this is only for docs.
return b in dir(getattr(bpy.ops, a))
def operator_exists_single(a):
a, b = a.partition("_OT_")[::2]
return operator_exists_pair(a.lower(), b)
id_split = doc_id.split(".")
url = rna = None
if len(id_split) == 1: # rna, class
if do_url:
url = "{:s}/bpy.types.{:s}.html".format(url_prefix, id_split[0])
else:
rna = "bpy.types.{:s}".format(id_split[0])
elif len(id_split) == 2: # rna, class.prop
class_name, class_prop = id_split
# an operator (common case - just button referencing an op)
if operator_exists_pair(class_name, class_prop):
if do_url:
url = "{:s}/bpy.ops.{:s}.html#bpy.ops.{:s}.{:s}".format(url_prefix, class_name, class_name, class_prop)
else:
rna = "bpy.ops.{:s}.{:s}".format(class_name, class_prop)
elif operator_exists_single(class_name):
# note: ignore the prop name since we don't have a way to link into it
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}".format(url_prefix, class_name, class_name, class_prop)
else:
rna = "bpy.ops.{:s}.{:s}".format(class_name, class_prop)
else:
# An RNA setting, common case.
# Check the built-in RNA types.
rna_class = getattr(bpy.types, class_name, None)
if rna_class is None:
# Check class for dynamically registered types.
rna_class = bpy.types.PropertyGroup.bl_rna_get_subclass_py(class_name)
if rna_class is None:
rna_class = bpy.types.AddonPreferences.bl_rna_get_subclass_py(class_name)
if rna_class is None:
if report is not None:
report({'ERROR'}, rpt_("Type \"{:s}\" cannot be found").format(class_name))
return None
# Detect if this is a inherited member and use that name instead.
rna_parent = rna_class.bl_rna
rna_prop = rna_parent.properties.get(class_prop)
if rna_prop:
rna_parent = rna_parent.base
while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
class_name = rna_parent.identifier
rna_parent = rna_parent.base
if do_url:
url = "{:s}/bpy.types.{:s}.html#bpy.types.{:s}.{:s}".format(
url_prefix, class_name, class_name, class_prop,
)
else:
rna = "bpy.types.{:s}.{:s}".format(class_name, class_prop)
else:
# We assume this is custom property, only try to generate generic url/rna_id...
if do_url:
url = ("{:s}/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items".format(url_prefix))
else:
rna = "bpy.types.bpy_struct"
return url if do_url else rna
class WM_OT_doc_view_manual(Operator):
"""Load online manual"""
bl_idname = "wm.doc_view_manual"
bl_label = "View Manual"
doc_id: doc_id
@staticmethod
def _find_reference(rna_id, url_mapping, *, verbose=True):
if verbose:
print("online manual check for: '{:s}'... ".format(rna_id))
from fnmatch import fnmatchcase
# XXX, for some reason all RNA ID's are stored lowercase
# Adding case into all ID's isn't worth the hassle so force lowercase.
rna_id = rna_id.lower()
# NOTE: `fnmatch` in Python is slow as it translates the string to a regular-expression
# which needs to be compiled (as of Python 3.11), this is slow enough to cause a noticeable
# delay when opening manual links (approaching half a second).
#
# Resolve by matching characters that have a special meaning to `fnmatch`.
# The characters that can occur as the first special character are `*?[`.
# If any of these are used we must let `fnmatch` run its own matching logic.
# However, in most cases a literal prefix is used making it considerably faster
# to do a simple `startswith` check before performing a full match.
# An alternative solution could be to use `fnmatch` from C which is significantly
# faster than Python's, see !104581 for details.
import re
re_match_non_special = re.compile(r"^[^?\*\[]+").match
for pattern, url_suffix in url_mapping:
# Simple optimization, makes a big difference (over 50x speedup).
# Even when `non_special.end()` is zero (resulting in an empty-string),
# the `startswith` check succeeds so there is no need to check for an empty match.
non_special = re_match_non_special(pattern)
if non_special is None or not rna_id.startswith(pattern[:non_special.end()]):
continue
# End simple optimization.
if fnmatchcase(rna_id, pattern):
if verbose:
print(" match found: '{:s}' --> '{:s}'".format(pattern, url_suffix))
return url_suffix
if verbose:
print("match not found")
return None
@staticmethod
def _lookup_rna_url(rna_id, verbose=True):
for prefix, url_manual_mapping in bpy.utils.manual_map():
rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
if rna_ref is not None:
url = prefix + rna_ref
return url
def execute(self, _context):
rna_id = _wm_doc_get_id(self.doc_id, do_url=False, report=self.report)
if rna_id is None:
return {'CANCELLED'}
url = self._lookup_rna_url(rna_id)
if url is None:
self.report(
{'WARNING'},
rpt_("No reference available {!r}, "
"update info in '_rna_manual_reference.py' "
"or callback to bpy.utils.manual_map()").format(self.doc_id)
)
return {'CANCELLED'}
else:
return bpy.ops.wm.url_open(url=url)
class WM_OT_doc_view(Operator):
"""Open online reference docs in a web browser"""
bl_idname = "wm.doc_view"
bl_label = "View Documentation"
doc_id: doc_id
_prefix = "https://docs.blender.org/api/{:d}.{:d}".format(*bpy.app.version[:2])
def execute(self, _context):
url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix, report=self.report)
if url is None:
return {'CANCELLED'}
return bpy.ops.wm.url_open(url=url)
rna_path = StringProperty(
name="Property Edit",
description="Property data_path edit",
maxlen=1024,
options={'HIDDEN'},
)
rna_custom_property_name = StringProperty(
name="Property Name",
description="Property name edit",
# Match `MAX_IDPROP_NAME - 1` in Blender's source.
maxlen=63,
)
# Most useful entries of rna_enum_property_subtype_items:
rna_custom_property_type_items = (
('FLOAT', "Float", "A single floating-point value"),
('FLOAT_ARRAY', "Float Array", "An array of floating-point values"),
('INT', "Integer", "A single integer"),
('INT_ARRAY', "Integer Array", "An array of integers"),
('BOOL', "Boolean", "A true or false value"),
('BOOL_ARRAY', "Boolean Array", "An array of true or false values"),
('STRING', "String", "A string value"),
('DATA_BLOCK', "Data-Block", "A data-block value"),
('PYTHON', "Python", "Edit a Python value directly, for unsupported property types"),
)
rna_custom_property_subtype_none_item = (
'NONE', n_("Plain Data", i18n_contexts.unit), n_("Data values without special behavior")
)
rna_custom_property_subtype_number_items = (
rna_custom_property_subtype_none_item,
('PIXEL', n_("Pixel", i18n_contexts.unit), n_("A distance on screen")),
('PERCENTAGE', n_("Percentage", i18n_contexts.unit), n_("A percentage between 0 and 100")),
('FACTOR', n_("Factor", i18n_contexts.unit), n_("A factor between 0.0 and 1.0")),
('ANGLE', n_("Angle", i18n_contexts.unit), n_("A rotational value specified in radians")),
('TIME_ABSOLUTE', n_("Time", i18n_contexts.unit), n_("Time specified in seconds")),
('DISTANCE', n_("Distance", i18n_contexts.unit), n_("A distance between two points")),
('POWER', n_("Power", i18n_contexts.unit), ""),
('TEMPERATURE', n_("Temperature", i18n_contexts.unit), ""),
)
rna_custom_property_subtype_vector_items = (
rna_custom_property_subtype_none_item,
('COLOR', n_("Linear Color", i18n_contexts.unit), n_("Color in the linear space")),
('COLOR_GAMMA', n_("Gamma-Corrected Color", i18n_contexts.unit), n_("Color in the gamma corrected space")),
('TRANSLATION', n_("Translation", i18n_contexts.unit), ""),
('DIRECTION', n_("Direction", i18n_contexts.unit), ""),
('VELOCITY', n_("Velocity", i18n_contexts.unit), ""),
('ACCELERATION', n_("Acceleration", i18n_contexts.unit), ""),
('EULER', n_("Euler Angles", i18n_contexts.unit), n_("Euler rotation angles in radians")),
('QUATERNION', n_("Quaternion Rotation", i18n_contexts.unit), n_("Quaternion rotation (affects NLA blending)")),
('AXISANGLE', n_("Axis-Angle", i18n_contexts.unit), n_("Angle and axis to rotate around")),
('XYZ', n_("XYZ", i18n_contexts.unit), ""),
)
rna_id_type_items = tuple(
(item.identifier, item.name, item.description, item.icon, item.value)
for item in bpy.types.ID.bl_rna.properties["id_type"].enum_items
)
class WM_OT_properties_edit(Operator):
"""Change a custom property's type, or adjust how it is displayed in the interface"""
bl_idname = "wm.properties_edit"
bl_label = "Edit Property"
# register only because invoke_props_popup requires.
bl_options = {'REGISTER', 'INTERNAL'}
def subtype_items_cb(self, context):
match self.property_type:
case 'FLOAT':
return rna_custom_property_subtype_number_items
case 'FLOAT_ARRAY':
return rna_custom_property_subtype_vector_items
case _:
# Needed so 'NONE' can always be assigned.
return (
rna_custom_property_subtype_none_item,
)
def property_type_update_cb(self, context):
self.subtype = 'NONE'
# Common settings used for all property types. Generally, separate properties are used for each
# type to improve the experience when choosing UI data values.
data_path: rna_path
property_name: rna_custom_property_name
property_type: EnumProperty(
name="Type",
items=rna_custom_property_type_items,
update=property_type_update_cb,
)
is_overridable_library: BoolProperty(
name="Library Overridable",
description="Allow the property to be overridden when the data-block is linked",
default=False,
)
description: StringProperty(
name="Description",
)
# Shared for integer and string properties.
use_soft_limits: BoolProperty(
name="Soft Limits",
description=(
"Limits the Property Value slider to a range, "
"values outside the range must be inputted numerically"
),
)
array_length: IntProperty(
name="Array Length",
default=3,
min=1,
# 32 is the maximum size for RNA array properties.
max=32,
)
# Integer properties.
# This property stores values for both array and non-array properties.
default_int: IntVectorProperty(
name="Default Value",
size=32,
)
min_int: IntProperty(
name="Min",
default=-10000,
)
max_int: IntProperty(
name="Max",
default=10000,
)
soft_min_int: IntProperty(
name="Soft Min",
default=-10000,
)
soft_max_int: IntProperty(
name="Soft Max",
default=10000,
)
step_int: IntProperty(
name="Step",
min=1,
default=1,
)
# Boolean properties.
# This property stores values for both array and non-array properties.
default_bool: BoolVectorProperty(
name="Default Value",
size=32,
)
# Float properties.
# This property stores values for both array and non-array properties.
default_float: FloatVectorProperty(
name="Default Value",
size=32,
)
min_float: FloatProperty(
name="Min",
default=-10000.0,
)
max_float: FloatProperty(
name="Max",
default=-10000.0,
)
soft_min_float: FloatProperty(
name="Soft Min",
default=-10000.0,
)
soft_max_float: FloatProperty(
name="Soft Max",
default=-10000.0,
)
precision: IntProperty(
name="Precision",
default=3,
min=0,
max=8,
)
step_float: FloatProperty(
name="Step",
default=0.1,
min=0.001,
)
subtype: EnumProperty(
name="Subtype",
items=subtype_items_cb,
translation_context=i18n_contexts.unit,
)
# String properties.
default_string: StringProperty(
name="Default Value",
maxlen=1024,
)
# Data-block properties.
id_type: EnumProperty(
name="ID Type",
items=rna_id_type_items,
translation_context=i18n_contexts.id_id,
default='OBJECT',
)
# Store the value converted to a string as a fallback for otherwise unsupported types.
eval_string: StringProperty(
name="Value",
description="Python value for unsupported custom property types",
)
# Helper method to avoid repetitive code to retrieve a single value from sequences and non-sequences.
@staticmethod
def _convert_new_value_single(old_value, new_type):
if hasattr(old_value, "__len__") and len(old_value) > 0:
return new_type(old_value[0])
return new_type(old_value)
# Helper method to create a list of a given value and type, using a sequence or non-sequence old value.
@staticmethod
def _convert_new_value_array(old_value, new_type, new_len):
if hasattr(old_value, "__len__"):
new_array = [new_type()] * new_len
for i in range(min(len(old_value), new_len)):
new_array[i] = new_type(old_value[i])
return new_array
return [new_type(old_value)] * new_len
# Convert an old property for a string, avoiding unhelpful string representations for custom list types.
@staticmethod
def convert_custom_property_to_string(item, name):
# The IDProperty group view API currently doesn't have a "lookup" method.
for key, value in item.items():
if key == name:
old_value = value
break
# In order to get a better string conversion, convert the property to a builtin sequence type first.
to_dict = getattr(old_value, "to_dict", None)
to_list = getattr(old_value, "to_list", None)
if to_dict:
old_value = to_dict()
elif to_list:
old_value = to_list()
return str(old_value)
# Retrieve the current type of the custom property on the RNA struct. Some properties like group properties
# can be created in the UI, but editing their meta-data isn't supported. In that case, return 'PYTHON'.
@staticmethod
def get_property_type(item, property_name):
from rna_prop_ui import (
rna_idprop_value_item_type,
)
prop_value = item[property_name]
prop_type, is_array = rna_idprop_value_item_type(prop_value)
if prop_type == int:
if is_array:
return 'INT_ARRAY'
return 'INT'
elif prop_type == float:
if is_array:
return 'FLOAT_ARRAY'
return 'FLOAT'
elif prop_type == bool:
if is_array:
return 'BOOL_ARRAY'
return 'BOOL'
elif prop_type == str:
if is_array:
return 'PYTHON'
return 'STRING'
elif prop_type == type(None) or issubclass(prop_type, bpy.types.ID):
if is_array:
return 'PYTHON'
return 'DATA_BLOCK'
return 'PYTHON'
# For `DATA_BLOCK` types, return the `id_type` or an empty string for non data-block types.
@staticmethod
def get_property_id_type(item, property_name):
ui_data = item.id_properties_ui(property_name)
rna_data = ui_data.as_dict()
# For non `DATA_BLOCK` types, the `id_type` wont exist.
return rna_data.get("id_type", "")
def _init_subtype(self, subtype):
self.subtype = subtype or 'NONE'
# Fill the operator's properties with the UI data properties from the existing custom property.
# Note that if the UI data doesn't exist yet, the access will create it and use those default values.
def _fill_old_ui_data(self, item, name):
ui_data = item.id_properties_ui(name)
rna_data = ui_data.as_dict()
if self.property_type in {'FLOAT', 'FLOAT_ARRAY'}:
self.min_float = rna_data["min"]
self.max_float = rna_data["max"]
self.soft_min_float = rna_data["soft_min"]
self.soft_max_float = rna_data["soft_max"]
self.precision = rna_data["precision"]
self.step_float = rna_data["step"]
if rna_data["subtype"] in [item[0] for item in self.subtype_items_cb(None)]:
self.subtype = rna_data["subtype"]
self.use_soft_limits = (
self.min_float != self.soft_min_float or
self.max_float != self.soft_max_float
)
default = self._convert_new_value_array(rna_data["default"], float, 32)
self.default_float = default if isinstance(default, list) else [default] * 32
elif self.property_type in {'INT', 'INT_ARRAY'}:
self.min_int = rna_data["min"]
self.max_int = rna_data["max"]
self.soft_min_int = rna_data["soft_min"]
self.soft_max_int = rna_data["soft_max"]
self.step_int = rna_data["step"]
self.use_soft_limits = (
self.min_int != self.soft_min_int or
self.max_int != self.soft_max_int
)
self.default_int = self._convert_new_value_array(rna_data["default"], int, 32)
elif self.property_type == 'STRING':
self.default_string = rna_data["default"]
elif self.property_type in {'BOOL', 'BOOL_ARRAY'}:
self.default_bool = self._convert_new_value_array(rna_data["default"], bool, 32)
elif self.property_type == 'DATA_BLOCK':
self.id_type = rna_data["id_type"]
if self.property_type in {'FLOAT_ARRAY', 'INT_ARRAY', 'BOOL_ARRAY'}:
self.array_length = len(item[name])
# The dictionary does not contain the description if it was empty.
self.description = rna_data.get("description", "")
self._init_subtype(self.subtype)
escaped_name = bpy.utils.escape_identifier(name)
self.is_overridable_library = bool(item.is_property_overridable_library('["{:s}"]'.format(escaped_name)))
# When the operator chooses a different type than the original property,
# attempt to convert the old value to the new type for continuity and speed.
def _get_converted_value(self, item, name_old, prop_type_new, id_type_old, id_type_new):
if prop_type_new == 'INT':
return self._convert_new_value_single(item[name_old], int)
elif prop_type_new == 'FLOAT':
return self._convert_new_value_single(item[name_old], float)
elif prop_type_new == 'BOOL':
return self._convert_new_value_single(item[name_old], bool)
elif prop_type_new == 'INT_ARRAY':
prop_type_old = self.get_property_type(item, name_old)
if prop_type_old in {'INT', 'FLOAT', 'INT_ARRAY', 'FLOAT_ARRAY', 'BOOL_ARRAY'}:
return self._convert_new_value_array(item[name_old], int, self.array_length)
elif prop_type_new == 'FLOAT_ARRAY':
prop_type_old = self.get_property_type(item, name_old)
if prop_type_old in {'INT', 'FLOAT', 'FLOAT_ARRAY', 'INT_ARRAY', 'BOOL_ARRAY'}:
return self._convert_new_value_array(item[name_old], float, self.array_length)
elif prop_type_new == 'BOOL_ARRAY':
prop_type_old = self.get_property_type(item, name_old)
if prop_type_old in {'INT', 'FLOAT', 'FLOAT_ARRAY', 'INT_ARRAY', 'BOOL_ARRAY'}:
return self._convert_new_value_array(item[name_old], bool, self.array_length)
else:
return [False] * self.array_length
elif prop_type_new == 'STRING':
return self.convert_custom_property_to_string(item, name_old)
elif prop_type_new == 'DATA_BLOCK':
if id_type_old != id_type_new:
return None
old_value = item[name_old]
if not isinstance(old_value, bpy.types.ID):
return None
return old_value
# If all else fails, create an empty string property. That should avoid errors later on anyway.
return ""
# Any time the target type is changed in the dialog, it's helpful to convert the UI data values
# to the new type as well, when possible, currently this only applies for floats and ints.
def _convert_old_ui_data_to_new_type(self, prop_type_old, prop_type_new):
if prop_type_new in {'INT', 'INT_ARRAY'} and prop_type_old in {'FLOAT', 'FLOAT_ARRAY'}:
self.min_int = int(self.min_float)
self.max_int = int(self.max_float)
self.soft_min_int = int(self.soft_min_float)
self.soft_max_int = int(self.soft_max_float)
self.default_int = self._convert_new_value_array(self.default_float, int, 32)
elif prop_type_new in {'FLOAT', 'FLOAT_ARRAY'} and prop_type_old in {'INT', 'INT_ARRAY'}:
self.min_float = float(self.min_int)
self.max_float = float(self.max_int)
self.soft_min_float = float(self.soft_min_int)
self.soft_max_float = float(self.soft_max_int)
self.default_float = self._convert_new_value_array(self.default_int, float, 32)
elif prop_type_new in {'BOOL', 'BOOL_ARRAY'} and prop_type_old in {'INT', 'INT_ARRAY'}:
self.default_bool = self._convert_new_value_array(self.default_int, bool, 32)
# Don't convert between string and float/int defaults here, it's not expected like the other conversions.
# Fill the property's UI data with the values chosen in the operator.
def _create_ui_data_for_new_prop(self, item, name, prop_type_new):
if prop_type_new in {'INT', 'INT_ARRAY'}:
ui_data = item.id_properties_ui(name)
ui_data.update(
min=self.min_int,
max=self.max_int,
soft_min=self.soft_min_int if self.use_soft_limits else self.min_int,
soft_max=self.soft_max_int if self.use_soft_limits else self.max_int,
step=self.step_int,
default=self.default_int[0] if prop_type_new == 'INT' else self.default_int[:self.array_length],
description=self.description,
)
elif prop_type_new in {'BOOL', 'BOOL_ARRAY'}:
ui_data = item.id_properties_ui(name)
ui_data.update(
default=self.default_bool[0] if prop_type_new == 'BOOL' else self.default_bool[:self.array_length],
description=self.description,
)
elif prop_type_new in {'FLOAT', 'FLOAT_ARRAY'}:
ui_data = item.id_properties_ui(name)
ui_data.update(
min=self.min_float,
max=self.max_float,
soft_min=self.soft_min_float if self.use_soft_limits else self.min_float,
soft_max=self.soft_max_float if self.use_soft_limits else self.max_float,
step=self.step_float,
precision=self.precision,
default=self.default_float[0] if prop_type_new == 'FLOAT' else self.default_float[:self.array_length],
description=self.description,
subtype=self.subtype,
)
elif prop_type_new == 'STRING':
ui_data = item.id_properties_ui(name)
ui_data.update(
default=self.default_string,
description=self.description,
)
elif prop_type_new == 'DATA_BLOCK':
ui_data = item.id_properties_ui(name)
ui_data.update(
description=self.description,
id_type=self.id_type,
)
escaped_name = bpy.utils.escape_identifier(name)
item.property_overridable_library_set('["{:s}"]'.format(escaped_name), self.is_overridable_library)
def _update_blender_for_prop_change(self, context, item, name, prop_type_old, prop_type_new):
from bpy_extras import anim_utils
from rna_prop_ui import (
rna_idprop_ui_prop_update,
)
rna_idprop_ui_prop_update(item, name)
# If we have changed the type of the property, update its potential anim curves!
if prop_type_old != prop_type_new:
escaped_name = bpy.utils.escape_identifier(name)
data_path = '["{:s}"]'.format(escaped_name)
done = set()
def _update(fcurves):
for fcu in fcurves:
if fcu not in done and fcu.data_path == data_path:
fcu.update_autoflags(item)
done.add(fcu)
def _update_strips(strips):
for st in strips:
if st.type == 'CLIP':
channelbag = anim_utils.action_get_channelbag_for_slot(st.action, st.action_slot)
if not channelbag:
continue
_update(channelbag.fcurves)
elif st.type == 'META':
_update_strips(st.strips)
adt = getattr(item, "animation_data", None)
if adt is not None:
channelbag = anim_utils.action_get_channelbag_for_slot(adt.action, adt.action_slot)
if channelbag:
_update(channelbag.fcurves)
if adt.drivers:
_update(adt.drivers)
if adt.nla_tracks:
for nt in adt.nla_tracks:
_update_strips(nt.strips)
# Otherwise existing buttons which reference freed memory may crash Blender (#26510).
for win in context.window_manager.windows:
for area in win.screen.areas:
area.tag_redraw()
def execute(self, context):
name_old = getattr(self, "_old_prop_name", [None])[0]
if name_old is None:
self.report({'ERROR'}, "Direct execution not supported")
return {'CANCELLED'}
data_path = self.data_path
name = self.property_name
item = eval("context.{:s}".format(data_path))
if (item.id_data and item.id_data.override_library and item.id_data.override_library.reference):
self.report({'ERROR'}, "Cannot edit properties from override data")
return {'CANCELLED'}
prop_type_old = self.get_property_type(item, name_old)
prop_type_new = self.property_type
self._old_prop_name[:] = [name]
id_type_old = self.get_property_id_type(item, name_old)
id_type_new = self.id_type
if prop_type_new == 'PYTHON':
try:
new_value = eval(self.eval_string)
except Exception as ex:
self.report({'WARNING'}, "Python evaluation failed: " + str(ex))
return {'CANCELLED'}
try:
item[name] = new_value
except Exception as ex:
self.report({'ERROR'}, "Failed to assign value: " + str(ex))
return {'CANCELLED'}
if name_old != name:
del item[name_old]
else:
new_value = self._get_converted_value(item, name_old, prop_type_new, id_type_old, id_type_new)
del item[name_old]
item[name] = new_value
self._create_ui_data_for_new_prop(item, name, prop_type_new)
self._update_blender_for_prop_change(context, item, name, prop_type_old, prop_type_new)
if name_old != name:
adt = getattr(item, "animation_data", None)
if adt is not None:
adt.fix_paths_rename_all(prefix="", old_name=name_old, new_name=name)
return {'FINISHED'}
def invoke(self, context, _event):
data_path = self.data_path
if not data_path:
self.report({'ERROR'}, "Data path not set")
return {'CANCELLED'}
name = self.property_name
self._old_prop_name = [name]
item = eval("context.{:s}".format(data_path))
if (item.id_data and item.id_data.override_library and item.id_data.override_library.reference):
self.report({'ERROR'}, "Properties from override data cannot be edited")
return {'CANCELLED'}
# Set operator's property type with the type of the existing property, to display the right settings.
old_type = self.get_property_type(item, name)
self.property_type = old_type
self.last_property_type = old_type
# So that the operator can do something for unsupported properties, change the property into
# a string, just for editing in the dialog. When the operator executes, it will be converted back
# into a python value. Always do this conversion, in case the Python property edit type is selected.
self.eval_string = self.convert_custom_property_to_string(item, name)
if old_type != 'PYTHON':
self._fill_old_ui_data(item, name)
wm = context.window_manager
return wm.invoke_props_dialog(self)
def check(self, context):
changed = False
# In order to convert UI data between types for type changes before the operator has actually executed,
# compare against the type the last time the check method was called (the last time a value was edited).
if self.property_type != self.last_property_type:
self._convert_old_ui_data_to_new_type(self.last_property_type, self.property_type)
changed = True
# Make sure that min is less than max, soft range is inside hard range, etc.
if self.property_type in {'FLOAT', 'FLOAT_ARRAY'}:
if self.min_float > self.max_float:
self.min_float, self.max_float = self.max_float, self.min_float
changed = True
if self.use_soft_limits:
if self.soft_min_float > self.soft_max_float:
self.soft_min_float, self.soft_max_float = self.soft_max_float, self.soft_min_float
changed = True
if self.soft_max_float > self.max_float:
self.soft_max_float = self.max_float
changed = True
if self.soft_min_float < self.min_float:
self.soft_min_float = self.min_float
changed = True
elif self.property_type in {'INT', 'INT_ARRAY'}:
if self.min_int > self.max_int:
self.min_int, self.max_int = self.max_int, self.min_int
changed = True
if self.use_soft_limits:
if self.soft_min_int > self.soft_max_int:
self.soft_min_int, self.soft_max_int = self.soft_max_int, self.soft_min_int
changed = True
if self.soft_max_int > self.max_int:
self.soft_max_int = self.max_int
changed = True
if self.soft_min_int < self.min_int:
self.soft_min_int = self.min_int
changed = True
self.last_property_type = self.property_type
return changed
def draw(self, _context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, "property_type")
layout.prop(self, "property_name")
if self.property_type in {'FLOAT', 'FLOAT_ARRAY'}:
if self.property_type == 'FLOAT_ARRAY':
layout.prop(self, "array_length")
col = layout.column(align=True)
col.prop(self, "default_float", index=0, text="Default")
for i in range(1, self.array_length):
col.prop(self, "default_float", index=i, text=" ")
else:
layout.prop(self, "default_float", index=0)
col = layout.column(align=True)
col.prop(self, "min_float")
col.prop(self, "max_float")
col = layout.column()
col.prop(self, "use_soft_limits")
col = layout.column(align=True)
col.enabled = self.use_soft_limits
col.prop(self, "soft_min_float", text="Soft Min")
col.prop(self, "soft_max_float", text="Max")
layout.prop(self, "step_float")
layout.prop(self, "precision")
layout.prop(self, "subtype")
elif self.property_type in {'INT', 'INT_ARRAY'}:
if self.property_type == 'INT_ARRAY':
layout.prop(self, "array_length")
col = layout.column(align=True)
col.prop(self, "default_int", index=0, text="Default")
for i in range(1, self.array_length):
col.prop(self, "default_int", index=i, text=" ")
else:
layout.prop(self, "default_int", index=0)
col = layout.column(align=True)
col.prop(self, "min_int")
col.prop(self, "max_int")
col = layout.column()
col.prop(self, "use_soft_limits")
col = layout.column(align=True)
col.enabled = self.use_soft_limits
col.prop(self, "soft_min_int", text="Soft Min")
col.prop(self, "soft_max_int", text="Max")
layout.prop(self, "step_int")
elif self.property_type in {'BOOL', 'BOOL_ARRAY'}:
if self.property_type == 'BOOL_ARRAY':
layout.prop(self, "array_length")
col = layout.column(align=True)
col.prop(self, "default_bool", index=0, text="Default")
for i in range(1, self.array_length):
col.prop(self, "default_bool", index=i, text=" ")
else:
layout.prop(self, "default_bool", index=0)
elif self.property_type == 'STRING':
layout.prop(self, "default_string")
elif self.property_type == 'DATA_BLOCK':
layout.prop(self, "id_type")
if self.property_type == 'PYTHON':
layout.prop(self, "eval_string")
else:
layout.prop(self, "description")
layout.prop(self, "is_overridable_library")
# Edit the value of a custom property with the given name on the RNA struct at the given data path.
# For supported types, this simply acts as a convenient way to create a popup for a specific property
# and draws the custom property value directly in the popup. For types like groups which can't be edited
# directly with buttons, instead convert the value to a string, evaluate the changed string when executing.
class WM_OT_properties_edit_value(Operator):
"""Edit the value of a custom property"""
bl_idname = "wm.properties_edit_value"
bl_label = "Edit Property Value"
# register only because invoke_props_popup requires.
bl_options = {'REGISTER', 'INTERNAL'}
data_path: rna_path
property_name: rna_custom_property_name
# Store the value converted to a string as a fallback for otherwise unsupported types.
eval_string: StringProperty(
name="Value",
description="Value for custom property types that can only be edited as a Python expression",
)
def execute(self, context):
if self.eval_string:
rna_item = eval("context.{:s}".format(self.data_path))
try:
new_value = eval(self.eval_string)
except Exception as ex:
self.report({'WARNING'}, "Python evaluation failed: " + str(ex))
return {'CANCELLED'}
rna_item[self.property_name] = new_value
return {'FINISHED'}
def invoke(self, context, _event):
rna_item = eval("context.{:s}".format(self.data_path))
if WM_OT_properties_edit.get_property_type(rna_item, self.property_name) == 'PYTHON':
self.eval_string = WM_OT_properties_edit.convert_custom_property_to_string(rna_item, self.property_name)
else:
self.eval_string = ""
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
from bpy.utils import escape_identifier
rna_item = eval("context.{:s}".format(self.data_path))
layout = self.layout
if WM_OT_properties_edit.get_property_type(rna_item, self.property_name) == 'PYTHON':
layout.prop(self, "eval_string")
else:
col = layout.column(align=True)
col.prop(rna_item, '["{:s}"]'.format(escape_identifier(self.property_name)), text="")
class WM_OT_properties_add(Operator):
"""Add your own property to the data-block"""
bl_idname = "wm.properties_add"
bl_label = "Add Property"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path
def execute(self, context):
from rna_prop_ui import (
rna_idprop_ui_create,
)
data_path = self.data_path
item = eval("context.{:s}".format(data_path))
if (item.id_data and item.id_data.override_library and item.id_data.override_library.reference):
self.report({'ERROR'}, "Cannot add properties to override data")
return {'CANCELLED'}
def unique_name(names):
prop = "prop"
prop_new = prop
i = 1
while prop_new in names:
prop_new = prop + str(i)
i += 1
return prop_new
prop = unique_name({
*item.keys(),
*type(item).bl_rna.properties.keys(),
})
rna_idprop_ui_create(item, prop, default=1.0)
return {'FINISHED'}
class WM_OT_properties_context_change(Operator):
"""Jump to a different tab inside the properties editor"""
bl_idname = "wm.properties_context_change"
bl_label = ""
bl_options = {'INTERNAL'}
context: StringProperty(
name="Context",
maxlen=64,
)
def execute(self, context):
context.space_data.context = self.context
return {'FINISHED'}
class WM_OT_properties_remove(Operator):
"""Internal use (edit a property data_path)"""
bl_idname = "wm.properties_remove"
bl_label = "Remove Property"
bl_options = {'UNDO', 'INTERNAL'}
data_path: rna_path
property_name: rna_custom_property_name
def execute(self, context):
from rna_prop_ui import (
rna_idprop_ui_prop_update,
)
data_path = self.data_path
item = eval("context.{:s}".format(data_path))
if (item.id_data and item.id_data.override_library and item.id_data.override_library.reference):
self.report({'ERROR'}, "Cannot remove properties from override data")
return {'CANCELLED'}
name = self.property_name
rna_idprop_ui_prop_update(item, name)
del item[name]
return {'FINISHED'}
class WM_OT_sysinfo(Operator):
"""Generate system information, saved into a text file"""
bl_idname = "wm.sysinfo"
bl_label = "Save System Info"
filepath: StringProperty(
subtype='FILE_PATH',
options={'SKIP_SAVE'},
)
def execute(self, _context):
from _bpy_internal.system_info.text_generate_runtime import write
with open(self.filepath, "w", encoding="utf-8") as output:
try:
write(output)
except Exception as ex:
# Not expected to occur, simply forward the exception.
self.report({'ERROR'}, str(ex))
# Also write into the file (to avoid confusion).
output.write("ERROR: {:s}\n".format(str(ex)))
return {'CANCELLED'}
return {'FINISHED'}
def invoke(self, context, _event):
import os
if not self.filepath:
self.filepath = os.path.join(
os.path.expanduser("~"), "system-info.txt")
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
class WM_OT_operator_cheat_sheet(Operator):
"""List all the operators in a text-block, useful for scripting"""
bl_idname = "wm.operator_cheat_sheet"
bl_label = "Operator Cheat Sheet"
def execute(self, _context):
op_strings = []
tot = 0
for op_module_name in dir(bpy.ops):
op_module = getattr(bpy.ops, op_module_name)
for op_submodule_name in dir(op_module):
op = getattr(op_module, op_submodule_name)
text = repr(op)
if text.split("\n")[-1].startswith("bpy.ops."):
op_strings.append(text)
tot += 1
op_strings.append('')
textblock = bpy.data.texts.new("OperatorList.txt")
textblock.write("# {:d} Operators\n\n".format(tot))
textblock.write("\n".join(op_strings))
self.report({'INFO'}, "See OperatorList.txt text block")
return {'FINISHED'}
# -----------------------------------------------------------------------------
# Add-on Operators
class WM_OT_owner_enable(Operator):
"""Enable add-on for workspace"""
bl_idname = "wm.owner_enable"
bl_label = "Enable Add-on"
owner_id: StringProperty(
name="UI Tag",
)
def execute(self, context):
workspace = context.workspace
workspace.owner_ids.new(self.owner_id)
return {'FINISHED'}
class WM_OT_owner_disable(Operator):
"""Disable add-on for workspace"""
bl_idname = "wm.owner_disable"
bl_label = "Disable Add-on"
owner_id: StringProperty(
name="UI Tag",
)
def execute(self, context):
workspace = context.workspace
owner_id = workspace.owner_ids[self.owner_id]
workspace.owner_ids.remove(owner_id)
return {'FINISHED'}
class WM_OT_tool_set_by_id(Operator):
"""Set the tool by name (for key-maps)"""
bl_idname = "wm.tool_set_by_id"
bl_label = "Set Tool by Name"
name: StringProperty(
name="Identifier",
description="Identifier of the tool",
)
cycle: BoolProperty(
name="Cycle",
description="Cycle through tools in this group",
default=False,
options={'SKIP_SAVE'},
)
as_fallback: BoolProperty(
name="Set Fallback",
description="Set the fallback tool instead of the primary tool",
default=False,
options={'SKIP_SAVE', 'HIDDEN'},
)
space_type: rna_space_type_prop
@staticmethod
def space_type_from_operator(op, context):
if op.properties.is_property_set("space_type"):
space_type = op.space_type
else:
space = context.space_data
if space is None:
op.report({'WARNING'}, rpt_("Tool cannot be set with an empty space"))
return None
space_type = space.type
return space_type
def execute(self, context):
from bl_ui.space_toolsystem_common import (
activate_by_id,
activate_by_id_or_cycle,
)
if (space_type := WM_OT_tool_set_by_id.space_type_from_operator(self, context)) is None:
return {'CANCELLED'}
fn = activate_by_id_or_cycle if self.cycle else activate_by_id
if fn(context, space_type, self.name, as_fallback=self.as_fallback):
if self.as_fallback:
tool_settings = context.tool_settings
tool_settings.workspace_tool_type = 'FALLBACK'
return {'FINISHED'}
else:
self.report({'WARNING'}, rpt_("Tool {!r} not found for space {!r}").format(self.name, space_type))
return {'CANCELLED'}
class WM_OT_tool_set_by_index(Operator):
"""Set the tool by index (for key-maps)"""
bl_idname = "wm.tool_set_by_index"
bl_label = "Set Tool by Index"
index: IntProperty(
name="Index in Toolbar",
default=0,
)
cycle: BoolProperty(
name="Cycle",
description="Cycle through tools in this group",
default=False,
options={'SKIP_SAVE'},
)
expand: BoolProperty(
description="Include tool subgroups",
default=True,
options={'SKIP_SAVE'},
)
as_fallback: BoolProperty(
name="Set Fallback",
description="Set the fallback tool instead of the primary",
default=False,
options={'SKIP_SAVE', 'HIDDEN'},
)
space_type: rna_space_type_prop
def execute(self, context):
from bl_ui.space_toolsystem_common import (
activate_by_id,
activate_by_id_or_cycle,
item_from_index_active,
item_from_flat_index,
)
if (space_type := WM_OT_tool_set_by_id.space_type_from_operator(self, context)) is None:
return {'CANCELLED'}
fn = item_from_flat_index if self.expand else item_from_index_active
item = fn(context, space_type, self.index)
if item is None:
# Don't report, since the number of tools may change.
return {'CANCELLED'}
# Same as: WM_OT_tool_set_by_id
fn = activate_by_id_or_cycle if self.cycle else activate_by_id
if fn(context, space_type, item.idname, as_fallback=self.as_fallback):
if self.as_fallback:
tool_settings = context.tool_settings
tool_settings.workspace_tool_type = 'FALLBACK'
return {'FINISHED'}
else:
# Since we already have the tool, this can't happen.
raise Exception("Internal error setting tool")
class WM_OT_tool_set_by_brush_type(Operator):
"""Look up the most appropriate tool for the given brush type and activate that"""
bl_idname = "wm.tool_set_by_brush_type"
bl_label = "Set Tool by Brush Type"
brush_type: StringProperty(
name="Brush Type",
description="Brush type identifier for which the most appropriate tool will be looked up",
)
space_type: rna_space_type_prop
def execute(self, context):
from bl_ui.space_toolsystem_common import (
ToolSelectPanelHelper,
activate_by_id
)
if (space_type := WM_OT_tool_set_by_id.space_type_from_operator(self, context)) is None:
return {'CANCELLED'}
tool_helper_cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
# Lookup a tool with a matching brush type (ignoring some specific ones).
tool_id = "builtin.brush"
for item in ToolSelectPanelHelper._tools_flatten(
tool_helper_cls.tools_from_context(context, mode=context.mode),
):
if item is None:
continue
# Never automatically activate these tools, they use a brush type that we want to use
# the main brush for (e.g. grease pencil primitive tools use 'DRAW' brush type, which
# is the most general one).
if item.idname in {
"builtin.arc",
"builtin.curve",
"builtin.line",
"builtin.box",
"builtin.circle",
"builtin.polyline",
}:
continue
if item.options is not None and ('USE_BRUSHES' in item.options) and item.brush_type is not None:
if item.brush_type == self.brush_type:
tool_id = item.idname
break
if activate_by_id(context, space_type, tool_id):
return {'FINISHED'}
else:
self.report({'WARNING'}, rpt_("Tool {!r} not found for space {!r}").format(tool_id, space_type))
return {'CANCELLED'}
class WM_OT_toolbar(Operator):
bl_idname = "wm.toolbar"
bl_label = "Toolbar"
@classmethod
def poll(cls, context):
return context.space_data is not None
@staticmethod
def keymap_from_toolbar(context, space_type, *, use_fallback_keys=True, use_reset=True):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
from bl_keymap_utils import keymap_from_toolbar
cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
if cls is None:
return None, None
return cls, keymap_from_toolbar.generate(
context,
space_type,
use_fallback_keys=use_fallback_keys,
use_reset=use_reset,
)
def execute(self, context):
space_type = context.space_data.type
cls, keymap = self.keymap_from_toolbar(context, space_type)
if keymap is None:
return {'CANCELLED'}
def draw_menu(popover, context):
layout = popover.layout
layout.operator_context = 'INVOKE_REGION_WIN'
cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
wm = context.window_manager
wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
return {'FINISHED'}
class WM_OT_toolbar_fallback_pie(Operator):
bl_idname = "wm.toolbar_fallback_pie"
bl_label = "Fallback Tool Pie Menu"
@classmethod
def poll(cls, context):
return context.space_data is not None
def invoke(self, context, event):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
space_type = context.space_data.type
cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
if cls is None:
return {'PASS_THROUGH'}
# It's possible we don't have the fallback tool available.
# This can happen in the image editor for example when there is no selection
# in painting modes.
item, _ = cls._tool_get_by_id(context, cls.tool_fallback_id)
if item is None:
print("Tool", cls.tool_fallback_id, "not active in", cls)
return {'PASS_THROUGH'}
def draw_cb(self, context):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
ToolSelectPanelHelper.draw_fallback_tool_items_for_pie_menu(self.layout, context)
wm = context.window_manager
wm.popup_menu_pie(draw_func=draw_cb, title=iface_("Fallback Tool"), event=event)
return {'FINISHED'}
class WM_OT_toolbar_prompt(Operator):
"""Leader key like functionality for accessing tools"""
bl_idname = "wm.toolbar_prompt"
bl_label = "Toolbar Prompt"
@staticmethod
def _status_items_generate(cls, keymap, context):
from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
# The keymap doesn't have the same order the tools are declared in,
# while we could support this, it's simpler to apply order here.
tool_map_id_to_order = {}
# Map the
tool_map_id_to_label = {}
for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context)):
if item is not None:
tool_map_id_to_label[item.idname] = item.label
tool_map_id_to_order[item.idname] = len(tool_map_id_to_order)
status_items = []
for item in keymap.keymap_items:
name = item.name
key_str = item.to_string()
# These are duplicated from regular numbers.
if key_str.startswith("Numpad "):
continue
properties = item.properties
idname = item.idname
if idname == "wm.tool_set_by_id":
tool_idname = properties["name"]
name = tool_map_id_to_label[tool_idname]
name = name.replace("Annotate ", "")
else:
continue
status_items.append((tool_idname, name, item))
status_items.sort(
key=lambda a: tool_map_id_to_order[a[0]]
)
return status_items
def modal(self, context, event):
event_type = event.type
event_value = event.value
if event_type in {
'LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE',
'WHEELDOWNMOUSE', 'WHEELUPMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE',
'ESC',
}:
context.workspace.status_text_set(None)
return {'CANCELLED', 'PASS_THROUGH'}
keymap = self._keymap
item = keymap.keymap_items.match_event(event)
if item is not None:
idname = item.idname
properties = item.properties
if idname == "wm.tool_set_by_id":
tool_idname = properties["name"]
bpy.ops.wm.tool_set_by_id(name=tool_idname)
context.workspace.status_text_set(None)
return {'FINISHED'}
# Pressing entry even again exists, as long as it's not mapped to a key (for convenience).
if event_type == self._init_event_type:
if event_value == 'RELEASE':
if not (event.ctrl or event.alt or event.shift or event.oskey or event.hyper):
context.workspace.status_text_set(None)
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
space_data = context.space_data
if space_data is None:
return {'CANCELLED'}
space_type = space_data.type
cls, keymap = WM_OT_toolbar.keymap_from_toolbar(
context,
space_type,
use_fallback_keys=False,
use_reset=False,
)
if (keymap is None) or (not keymap.keymap_items):
return {'CANCELLED'}
self._init_event_type = event.type
# Strip Left/Right, since "Left Alt" isn't especially useful.
init_event_type_as_text = self._init_event_type.title().split("_")
if init_event_type_as_text[0] in {"Left", "Right"}:
del init_event_type_as_text[0]
init_event_type_as_text = " ".join(init_event_type_as_text)
status_items = self._status_items_generate(cls, keymap, context)
def status_text_fn(self, context):
layout = self.layout
if True:
box = layout.row(align=True).box()
box.scale_x = 0.8
box.label(text=init_event_type_as_text)
flow = layout.grid_flow(columns=len(status_items), align=True, row_major=True)
for _, name, item in status_items:
row = flow.row(align=True)
row.template_event_from_keymap_item(
item, text=name, text_ctxt=i18n_contexts.operator_default
)
self._keymap = keymap
context.workspace.status_text_set(status_text_fn)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
class BatchRenameAction(bpy.types.PropertyGroup):
__slots__ = ()
# category: StringProperty()
type: EnumProperty(
name="Operation",
items=(
('REPLACE', "Find/Replace", "Replace text in the name"),
('SET', "Set Name", "Set a new name or prefix/suffix the existing one"),
('STRIP', "Strip Characters", "Strip leading/trailing text from the name"),
('CASE', "Change Case", "Change case of each name"),
),
)
# We could split these into sub-properties, however it's not so important.
# Used when `type == 'SET'`.
set_name: StringProperty(name="Name")
set_method: EnumProperty(
name="Method",
items=(
('NEW', "New", ""),
('PREFIX', "Prefix", ""),
('SUFFIX', "Suffix", ""),
),
default='SUFFIX',
)
# Used when `type == 'STRIP'`.
strip_chars: EnumProperty(
name="Strip Characters",
translation_context=i18n_contexts.id_text,
options={'ENUM_FLAG'},
items=(
('SPACE', "Spaces", ""),
('DIGIT', "Digits", ""),
('PUNCT', "Punctuation", ""),
),
)
# Used when `type == 'STRIP'`.
strip_part: EnumProperty(
name="Strip Part",
options={'ENUM_FLAG'},
items=(
('START', "Start", ""),
('END', "End", ""),
),
)
# Used when `type == 'REPLACE'`.
replace_src: StringProperty(name="Find")
replace_dst: StringProperty(name="Replace")
replace_match_case: BoolProperty(name="Case Sensitive")
use_replace_regex_src: BoolProperty(
name="Regular Expression Find",
description="Use regular expressions to match text in the 'Find' field",
)
use_replace_regex_dst: BoolProperty(
name="Regular Expression Replace",
description="Use regular expression for the replacement text (supporting groups)",
)
# Used when `type == 'CASE'`.
case_method: EnumProperty(
name="Case",
items=(
('UPPER', "Upper Case", ""),
('LOWER', "Lower Case", ""),
('TITLE', "Title Case", ""),
),
)
# Weak, add/remove as properties.
op_add: BoolProperty(name="Add", translation_context=i18n_contexts.operator_default)
op_remove: BoolProperty(name="Remove", translation_context=i18n_contexts.operator_default)
class WM_OT_batch_rename(Operator):
"""Rename multiple items at once"""
bl_idname = "wm.batch_rename"
bl_label = "Batch Rename"
bl_options = {'UNDO'}
data_type: EnumProperty(
name="Type",
items=(
('OBJECT', "Objects", "", 'OBJECT_DATA', 0),
('COLLECTION', "Collections", "", 'OUTLINER_COLLECTION', 1),
('MATERIAL', "Materials", "", 'MATERIAL_DATA', 2),
None,
# Enum identifiers are compared with `object.type`.
# Follow order in "Add" menu.
('MESH', "Meshes", "", 'MESH_DATA', 3),
('CURVE', "Curves", "", 'CURVE_DATA', 4),
('META', "Metaballs", "", 'META_DATA', 5),
('VOLUME', "Volumes", "", 'VOLUME_DATA', 6),
('GREASEPENCIL', "Grease Pencils", "", 'OUTLINER_DATA_GREASEPENCIL', 7),
('ARMATURE', "Armatures", "", 'ARMATURE_DATA', 8),
('LATTICE', "Lattices", "", 'LATTICE_DATA', 9),
('LIGHT', "Lights", "", 'LIGHT_DATA', 10),
('LIGHT_PROBE', "Light Probes", "", 'OUTLINER_DATA_LIGHTPROBE', 11),
('CAMERA', "Cameras", "", 'CAMERA_DATA', 12),
('SPEAKER', "Speakers", "", 'OUTLINER_DATA_SPEAKER', 13),
None,
('BONE', "Bones", "", 'BONE_DATA', 14),
('NODE', "Nodes", "", 'NODETREE', 15),
('SEQUENCE_STRIP', "Sequence Strips", "", 'SEQ_SEQUENCER', 16),
('ACTION_CLIP', "Action Clips", "", 'ACTION', 17),
None,
('SCENE', "Scenes", "", 'SCENE_DATA', 18),
('BRUSH', "Brushes", "", 'BRUSH_DATA', 19),
),
translation_context=i18n_contexts.id_id,
description="Type of data to rename",
)
data_source: EnumProperty(
name="Source",
items=(
('SELECT', "Selected", ""),
('ALL', "All", ""),
),
)
actions: CollectionProperty(type=BatchRenameAction)
@staticmethod
def _selected_ids_from_outliner_by_type(context, ty):
return [
id for id in context.selected_ids
if isinstance(id, ty)
if id.is_editable
]
@staticmethod
def _selected_ids_from_outliner_by_type_for_object_data(context, ty):
# Include selected object-data as well as the selected ID's.
from bpy.types import Object
# De-duplicate the result as object-data may cause duplicates.
return tuple(set([
id for id_base in context.selected_ids
if isinstance(id := id_base.data if isinstance(id_base, Object) else id_base, ty)
if id.is_editable
]))
@staticmethod
def _selected_actions_from_outliner(context):
# Actions are a special case because they can be accessed directly or via animation-data.
from bpy.types import Action
def action_from_any_id(id_data):
if isinstance(id_data, Action):
return id_data
# Not all ID's have animation data.
if (animation_data := getattr(id_data, "animation_data", None)) is not None:
return animation_data.action
return None
return tuple(set(
action for id in context.selected_ids
if (action := action_from_any_id(id)) is not None
if action.is_editable
))
@classmethod
def _data_from_context(cls, context, data_type, only_selected, *, check_context=False):
def _is_editable(data):
return data.id_data.is_editable and not data.id_data.override_library
mode = context.mode
scene = context.scene
space = context.space_data
space_type = None if (space is None) else space.type
data = None
if space_type == 'SEQUENCE_EDITOR':
data_type_test = 'SEQUENCE_STRIP'
if check_context:
return data_type_test
if data_type == data_type_test:
data = (
context.selected_strips
if only_selected else
scene.sequence_editor.strips_all,
"name",
iface_("Strip(s)"),
)
elif space_type == 'NODE_EDITOR':
data_type_test = 'NODE'
if check_context:
return data_type_test
if data_type == data_type_test:
data = (
context.selected_nodes
if only_selected else
list(space.node_tree.nodes),
"name",
iface_("Node(s)"),
)
elif space_type == 'OUTLINER':
data_type_test = 'COLLECTION'
if check_context:
return data_type_test
if data_type == data_type_test:
data = (
cls._selected_ids_from_outliner_by_type(context, bpy.types.Collection)
if only_selected else
scene.collection.children_recursive,
"name",
iface_("Collection(s)"),
)
else:
if mode == 'POSE' or (mode == 'WEIGHT_PAINT' and context.pose_object):
data_type_test = 'BONE'
if check_context:
return data_type_test
if data_type == data_type_test:
data = (
[pchan.bone for pchan in context.selected_pose_bones]
if only_selected else
[pbone.bone for ob in context.objects_in_mode_unique_data for pbone in ob.pose.bones],
"name",
iface_("Bone(s)"),
)
elif mode == 'EDIT_ARMATURE':
data_type_test = 'BONE'
if check_context:
return data_type_test
if data_type == data_type_test:
data = (
context.selected_editable_bones
if only_selected else
[ebone for ob in context.objects_in_mode_unique_data for ebone in ob.data.edit_bones],
"name",
iface_("Edit Bone(s)"),
)
if check_context:
return 'OBJECT'
object_data_type_attrs_map = {
'MESH': ("meshes", iface_("Mesh(es)"), bpy.types.Mesh),
'CURVE': ("curves", iface_("Curve(s)"), bpy.types.Curve),
'META': ("metaballs", iface_("Metaball(s)"), bpy.types.MetaBall),
'VOLUME': ("volumes", iface_("Volume(s)"), bpy.types.Volume),
'GREASEPENCIL': ("grease_pencils", iface_("Grease Pencil(s)"), bpy.types.GreasePencil),
'ARMATURE': ("armatures", iface_("Armature(s)"), bpy.types.Armature),
'LATTICE': ("lattices", iface_("Lattice(s)"), bpy.types.Lattice),
'LIGHT': ("lights", iface_("Light(s)"), bpy.types.Light),
'LIGHT_PROBE': ("lightprobes", iface_("Light Probe(s)"), bpy.types.LightProbe),
'CAMERA': ("cameras", iface_("Camera(s)"), bpy.types.Camera),
'SPEAKER': ("speakers", iface_("Speaker(s)"), bpy.types.Speaker),
}
# Finish with space types.
if data is None:
if data_type == 'OBJECT':
data = (
(
# Outliner.
cls._selected_ids_from_outliner_by_type(context, bpy.types.Object)
if space_type == 'OUTLINER' else
# 3D View (default).
context.selected_editable_objects
)
if only_selected else
[id for id in bpy.data.objects if id.is_editable],
"name",
iface_("Object(s)"),
)
elif data_type == 'COLLECTION':
data = (
# Outliner case is handled already.
tuple(set(
ob.instance_collection
for ob in context.selected_objects
if ((ob.instance_type == 'COLLECTION') and
(collection := ob.instance_collection) is not None and
(collection.is_editable))
))
if only_selected else
[id for id in bpy.data.collections if id.is_editable],
"name",
iface_("Collection(s)"),
)
elif data_type == 'MATERIAL':
data = (
(
# Outliner.
cls._selected_ids_from_outliner_by_type(context, bpy.types.Material)
if space_type == 'OUTLINER' else
# 3D View (default).
tuple(set(
id
for ob in context.selected_objects
for slot in ob.material_slots
if (id := slot.material) is not None and id.is_editable
))
)
if only_selected else
[id for id in bpy.data.materials if id.is_editable],
"name",
iface_("Material(s)"),
)
elif data_type == 'ACTION_CLIP':
data = (
(
# Outliner.
cls._selected_actions_from_outliner(context)
if space_type == 'OUTLINER' else
# 3D View (default).
tuple(set(
action for ob in context.selected_objects
if (((animation_data := ob.animation_data) is not None) and
((action := animation_data.action) is not None) and
(action.is_editable))
))
)
if only_selected else
[id for id in bpy.data.actions if id.is_editable],
"name",
iface_("Action(s)"),
)
elif data_type == 'SCENE':
data = (
(
# Outliner.
cls._selected_ids_from_outliner_by_type(context, bpy.types.Scene)
if ((space_type == 'OUTLINER') and only_selected) else
[id for id in bpy.data.scenes if id.is_editable]
),
"name",
iface_("Scene(s)"),
)
elif data_type == 'BRUSH':
data = (
(
# Outliner.
cls._selected_ids_from_outliner_by_type(context, bpy.types.Brush)
if ((space_type == 'OUTLINER') and only_selected) else
[id for id in bpy.data.brushes if id.is_editable]
),
"name",
iface_("Brush(es)"),
)
elif data_type in object_data_type_attrs_map.keys():
attr, descr, ty = object_data_type_attrs_map[data_type]
data = (
(
# Outliner.
cls._selected_ids_from_outliner_by_type_for_object_data(context, ty)
if space_type == 'OUTLINER' else
# 3D View (default).
tuple(set(
id
for ob in context.selected_objects
if ob.type == data_type
if (id := ob.data) is not None and id.is_editable
))
)
if only_selected else
[id for id in getattr(bpy.data, attr) if id.is_editable],
"name",
descr,
)
if data is None:
return None
data = ([it for it in data[0] if _is_editable(it)], data[1], data[2])
return data
@staticmethod
def _apply_actions(actions, name):
import string
import re
for action in actions:
ty = action.type
if ty == 'SET':
text = action.set_name
method = action.set_method
if method == 'NEW':
name = text
elif method == 'PREFIX':
name = text + name
elif method == 'SUFFIX':
name = name + text
else:
assert False, "unreachable"
elif ty == 'STRIP':
chars = action.strip_chars
chars_strip = (
"{:s}{:s}{:s}"
).format(
string.punctuation if 'PUNCT' in chars else "",
string.digits if 'DIGIT' in chars else "",
" " if 'SPACE' in chars else "",
)
part = action.strip_part
if 'START' in part:
name = name.lstrip(chars_strip)
if 'END' in part:
name = name.rstrip(chars_strip)
elif ty == 'REPLACE':
if action.use_replace_regex_src:
replace_src = action.replace_src
if action.use_replace_regex_dst:
replace_dst = action.replace_dst
else:
replace_dst = action.replace_dst.replace("\\", "\\\\")
else:
replace_src = re.escape(action.replace_src)
replace_dst = action.replace_dst.replace("\\", "\\\\")
name = re.sub(
replace_src,
replace_dst,
name,
flags=(
0 if action.replace_match_case else
re.IGNORECASE
),
)
elif ty == 'CASE':
method = action.case_method
if method == 'UPPER':
name = name.upper()
elif method == 'LOWER':
name = name.lower()
elif method == 'TITLE':
name = name.title()
else:
assert False, "unreachable"
else:
assert False, "unreachable"
return name
def _data_update(self, context):
only_selected = self.data_source == 'SELECT'
self._data = self._data_from_context(context, self.data_type, only_selected)
if self._data is None:
self.data_type = self._data_from_context(context, None, False, check_context=True)
self._data = self._data_from_context(context, self.data_type, only_selected)
self._data_source_prev = self.data_source
self._data_type_prev = self.data_type
def draw(self, context):
import re
layout = self.layout
split = layout.split(align=True)
split.row(align=True).prop(self, "data_source", expand=True)
split.prop(self, "data_type", text="")
for action in self.actions:
box = layout.box()
split = box.split(factor=0.87)
# Column 1: main content.
col = split.column()
# Label's width.
fac = 0.25
# Row 1: type.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Type")
row.prop(action, "type", text="")
ty = action.type
if ty == 'SET':
# Row 2: method.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Method")
row.row().prop(action, "set_method", expand=True)
# Row 3: name.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Name")
row.prop(action, "set_name", text="")
elif ty == 'STRIP':
# Row 2: chars.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Characters")
row.row().prop(action, "strip_chars")
# Row 3: part.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Strip From")
row.row().prop(action, "strip_part")
elif ty == 'REPLACE':
# Row 2: find.
row = col.split(factor=fac)
re_error_src = None
if action.use_replace_regex_src:
try:
re.compile(action.replace_src)
except Exception as ex:
re_error_src = str(ex)
row.alert = True
row.alignment = 'RIGHT'
row.label(text="Find")
sub = row.row(align=True)
sub.prop(action, "replace_src", text="")
sub.prop(action, "use_replace_regex_src", text="", icon='SORTBYEXT')
# Row.
if re_error_src is not None:
row = col.split(factor=fac)
row.label(text="")
row.alert = True
row.label(text=re_error_src)
# Row 3: replace.
row = col.split(factor=fac)
re_error_dst = None
if action.use_replace_regex_src:
if action.use_replace_regex_dst:
if re_error_src is None:
try:
re.sub(action.replace_src, action.replace_dst, "")
except Exception as ex:
re_error_dst = str(ex)
row.alert = True
row.alignment = 'RIGHT'
row.label(text="Replace")
sub = row.row(align=True)
sub.prop(action, "replace_dst", text="")
subsub = sub.row(align=True)
subsub.active = action.use_replace_regex_src
subsub.prop(action, "use_replace_regex_dst", text="", icon='SORTBYEXT')
# Row.
if re_error_dst is not None:
row = col.split(factor=fac)
row.label(text="")
row.alert = True
row.label(text=re_error_dst)
# Row 4: case.
row = col.split(factor=fac)
row.label(text="")
row.prop(action, "replace_match_case")
elif ty == 'CASE':
# Row 2: method.
row = col.split(factor=fac)
row.alignment = 'RIGHT'
row.label(text="Convert To")
row.row().prop(action, "case_method", expand=True)
# Column 2: add-remove.
row = split.split(align=True)
row.prop(action, "op_remove", text="", icon='REMOVE')
row.prop(action, "op_add", text="", icon='ADD')
layout.label(text=iface_("Rename {:d} {:s}").format(len(self._data[0]), self._data[2]), translate=False)
def check(self, context):
changed = False
for i, action in enumerate(self.actions):
if action.op_add:
action.op_add = False
self.actions.add()
if i + 2 != len(self.actions):
self.actions.move(len(self.actions) - 1, i + 1)
changed = True
break
if action.op_remove:
action.op_remove = False
if len(self.actions) > 1:
self.actions.remove(i)
changed = True
break
if (
(self._data_source_prev != self.data_source) or
(self._data_type_prev != self.data_type)
):
self._data_update(context)
changed = True
return changed
def execute(self, context):
import re
seq, attr, descr = self._data
actions = self.actions
# Sanitize actions.
for action in actions:
if action.use_replace_regex_src:
try:
re.compile(action.replace_src)
except Exception as ex:
self.report({'ERROR'}, "Invalid regular expression (find): " + str(ex))
return {'CANCELLED'}
if action.use_replace_regex_dst:
try:
re.sub(action.replace_src, action.replace_dst, "")
except Exception as ex:
self.report({'ERROR'}, "Invalid regular expression (replace): " + str(ex))
return {'CANCELLED'}
total_len = 0
change_len = 0
for item in seq:
name_src = getattr(item, attr)
name_dst = self._apply_actions(actions, name_src)
if name_src != name_dst:
setattr(item, attr, name_dst)
change_len += 1
total_len += 1
self.report({'INFO'}, rpt_("Renamed {:d} of {:d} {:s}").format(change_len, total_len, descr))
return {'FINISHED'}
def invoke(self, context, event):
self._data_update(context)
if not self.actions:
self.actions.add()
wm = context.window_manager
return wm.invoke_props_dialog(self, width=400)
class WM_MT_splash_quick_setup(Menu):
bl_label = "Quick Setup"
def draw(self, context):
layout = self.layout
wm = context.window_manager
prefs = context.preferences
layout.operator_context = 'EXEC_DEFAULT'
old_version = bpy.types.PREFERENCES_OT_copy_prev.previous_version()
can_import = bpy.types.PREFERENCES_OT_copy_prev.poll(context) and old_version
if can_import:
layout.label(text="Import Preferences From Previous Version")
split = layout.split(factor=0.20) # Left margin.
split.label()
split = split.split(factor=0.73) # Content width.
col = split.column()
col.operator(
"preferences.copy_prev",
text=iface_("Import Blender {:d}.{:d} Preferences", "Operator").format(*old_version),
icon='NONE',
translate=False,
)
layout.separator()
layout.separator(type='LINE')
if can_import:
layout.label(text="Create New Preferences")
else:
layout.label(text="Quick Setup")
split = layout.split(factor=0.20) # Left margin.
split.label()
split = split.split(factor=0.73) # Content width.
col = split.column()
col.use_property_split = True
col.use_property_decorate = False
# Languages.
if bpy.app.build_options.international:
col.prop(prefs.view, "language")
# Themes.
sub = col.column(heading="Theme")
label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
if label == "Presets":
label = "Blender Dark"
sub.menu("USERPREF_MT_interface_theme_presets", text=label)
col.separator()
# Shortcuts.
kc = wm.keyconfigs.active
kc_prefs = kc.preferences
sub = col.column(heading="Keymap")
text = bpy.path.display_name(kc.name)
if not text:
text = "Blender"
sub.menu("USERPREF_MT_keyconfigs", text=text)
if hasattr(kc_prefs, "select_mouse"):
col.row().prop(kc_prefs, "select_mouse", text="Mouse Select", expand=True)
if hasattr(kc_prefs, "spacebar_action"):
col.row().prop(kc_prefs, "spacebar_action", text="Spacebar Action")
# Save Preferences.
sub = col.column()
sub.separator(factor=2)
if can_import:
sub.operator("wm.save_userpref", text="Save New Preferences", icon='NONE')
else:
sub.operator("wm.save_userpref", text="Continue")
layout.separator(factor=2.0)
class WM_MT_splash(Menu):
bl_label = "Splash"
def draw(self, context):
layout = self.layout
layout.operator_context = 'EXEC_DEFAULT'
layout.emboss = 'PULLDOWN_MENU'
split = layout.split()
# Templates
col1 = split.column()
col1.label(text="New File")
bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
# Recent
col2 = split.column()
col2_title = col2.row()
found_recent = col2.template_recent_files(rows=5)
if found_recent:
col2_title.label(text="Recent Files")
col_more = col2.column()
col_more.operator_context = 'INVOKE_DEFAULT'
more_props = col_more.operator("wm.search_single_menu", text="More...", icon='VIEWZOOM')
more_props.menu_idname = "TOPBAR_MT_file_open_recent"
else:
# Links if no recent files.
col2_title.label(text="Getting Started")
col2.operator("wm.url_open_preset", text="Manual", icon='URL').type = 'MANUAL'
col2.operator("wm.url_open", text="Support", icon='URL').url = "https://www.blender.org/support/"
col2.operator("wm.url_open", text="User Communities", icon='URL').url = "https://www.blender.org/community/"
col2.operator("wm.url_open", text="Get Involved", icon='URL').url = "https://www.blender.org/get-involved/"
col2.operator("wm.url_open_preset", text="Blender Website", icon='URL').type = 'BLENDER'
col_sep = layout.column()
col_sep.separator()
col_sep.separator(type='LINE')
col_sep.separator()
split = layout.split()
col1 = split.column()
sub = col1.row()
sub.operator_context = 'INVOKE_DEFAULT'
sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
col2 = split.column()
col2.operator("wm.url_open_preset", text="What's New", icon='URL').type = 'RELEASE_NOTES'
col2.operator("wm.url_open_preset", text="Donate to Blender", icon='FUND').type = 'FUND'
layout.separator()
if (not bpy.app.online_access) and bpy.app.online_access_override:
self.layout.label(text="Running in Offline Mode", icon='INTERNET_OFFLINE')
layout.separator()
class WM_MT_splash_about(Menu):
bl_label = "About"
def draw(self, context):
layout = self.layout
layout.operator_context = 'EXEC_DEFAULT'
split = layout.split(factor=0.65)
col = split.column(align=True)
col.scale_y = 0.8
col.label(text=iface_("Version: {:s}").format(bpy.app.version_string), translate=False)
col.separator(factor=2.5)
col.label(text=iface_("Date: {:s} {:s}").format(
bpy.app.build_commit_date.decode("utf-8", "replace"),
bpy.app.build_commit_time.decode("utf-8", "replace")),
translate=False,
)
col.label(text=iface_("Hash: {:s}").format(bpy.app.build_hash.decode("ascii")), translate=False)
col.label(text=iface_("Branch: {:s}").format(bpy.app.build_branch.decode("utf-8", "replace")), translate=False)
# This isn't useful information on MS-Windows or Apple systems as dynamically switching
# between windowing systems is only supported between X11/WAYLAND.
from _bpy import _ghost_backend
ghost_backend = _ghost_backend()
if ghost_backend not in {'NONE', 'DEFAULT'}:
col.label(text=iface_("Windowing Environment: {:s}").format(_ghost_backend()), translate=False)
del _ghost_backend, ghost_backend
col.separator(factor=2.0)
col.label(text="Blender is free software")
col.label(text="Licensed under the GNU General Public License")
col = split.column(align=True)
col.emboss = 'PULLDOWN_MENU'
col.operator("wm.url_open_preset", text="Donate", icon='FUND').type = 'FUND'
col.operator("wm.url_open_preset", text="What's New", icon='URL').type = 'RELEASE_NOTES'
col.separator(factor=2.0)
col.operator("wm.url_open_preset", text="Credits", icon='URL').type = 'CREDITS'
col.operator("wm.url_open", text="License", icon='URL').url = "https://www.blender.org/about/license/"
col.operator("wm.url_open", text="Blender Store", icon='URL').url = "https://store.blender.org"
col.operator("wm.url_open_preset", text="Blender Website", icon='URL').type = 'BLENDER'
class WM_MT_region_toggle_pie(Menu):
bl_label = "Region Toggle"
# Map the `region.type` to the `space_data` attribute & text label.
# The order of items defines priority, so for example in the sequencer
# when there is both a toolbar and channels, the toolbar gets the
# axis-aligned pie, and the channels don't.
_region_info = {
'TOOLS': "show_region_toolbar",
'UI': "show_region_ui",
# Note that the tool header is enabled/disabled along with the header,
# no need to include both in this list.
'HEADER': "show_region_header",
'FOOTER': "show_region_footer",
'ASSET_SHELF': "show_region_asset_shelf",
'CHANNELS': "show_region_channels",
}
# Map the `region.alignment` to the axis-aligned pie position.
_region_align_pie = {
'LEFT': 0,
'RIGHT': 1,
'BOTTOM': 2,
'TOP': 3,
}
# Map the axis-aligned pie to alternative directions, see `ui_radial_dir_order` in C++ source.
# The value is the preferred direction in order of priority, two diagonals, then the flipped direction.
_region_dir_pie_alternatives = {
0: (4, 6, 1),
1: (5, 7, 0),
2: (6, 7, 3),
3: (4, 5, 2),
}
@classmethod
def poll(cls, context):
return context.space_data is not None
@classmethod
def _draw_pie_regions_from_alignment(cls, context, pie):
space_data = context.space_data
# Store each region by it's type.
region_by_type = {}
for region in context.area.regions:
region_type = region.type
# If the attribute doesn't exist, the RNA definition is outdated.
# See: #134339 and its fix for reference.
attr = cls._region_info.get(region_type, None)
if attr is None:
continue
# In some cases channels exists but can't be toggled.
assert hasattr(space_data, attr)
if space_data.is_property_readonly(attr):
continue
# Technically possible these double-up, in practice this should never happen.
if region_type in region_by_type:
print("{:s}: Unexpected double-up of region types {!r}".format(cls.__name__, region_type))
region_by_type[region_type] = region
# Axis aligned pie menu items to populate.
items = [[], [], [], [], [], [], [], []]
# Use predictable ordering.
for region_type in cls._region_info.keys():
region = region_by_type.get(region_type)
if region is None:
continue
index = cls._region_align_pie[region.alignment]
items[index].append(region_type)
# Handle any overflow (two or more regions with the same alignment).
# This happens in the sequencer (channels + toolbar),
# otherwise it should not be that common.
items_overflow = []
for index in range(4):
if len(items[index]) <= 1:
continue
for index_other in cls._region_dir_pie_alternatives[index]:
if not items[index_other]:
items[index_other].append(items[index].pop(1))
if len(items[index]) <= 1:
break
del index_other
for index in range(4):
if len(items[index]) <= 1:
continue
for index_other in range(4, 8):
if not items[index_other]:
items[index_other].append(items[index].pop(1))
if len(items[index]) <= 1:
break
# Only happens when there are more than 8 regions - practically never!
for index in range(4):
while len(items[index]) > 1:
items_overflow.append([items[index].pop(1)])
# Use to access the labels.
enum_items = bpy.types.Region.bl_rna.properties["type"].enum_items_static_ui
for region_type_list in (items + items_overflow):
if not region_type_list:
pie.separator()
continue
assert len(region_type_list) == 1
region_type = region_type_list[0]
text = enum_items[region_type].name
attr = cls._region_info[region_type]
value = getattr(space_data, attr)
props = pie.operator(
"wm.context_toggle",
text=text,
text_ctxt=i18n_contexts.default,
icon='CHECKBOX_HLT' if value else 'CHECKBOX_DEHLT',
)
props.data_path = "space_data." + attr
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
self._draw_pie_regions_from_alignment(context, pie)
class WM_OT_drop_blend_file(Operator):
bl_idname = "wm.drop_blend_file"
bl_label = "Handle dropped .blend file"
bl_options = {'INTERNAL'}
filepath: StringProperty(
subtype='FILE_PATH',
options={'SKIP_SAVE'},
)
def invoke(self, context, _event):
context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
return {'FINISHED'}
def draw_menu(self, menu, _context):
layout = menu.layout
col = layout.column()
col.operator_context = 'INVOKE_DEFAULT'
props = col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER')
props.filepath = self.filepath
props.display_file_selector = False
layout.separator()
col = layout.column()
col.operator_context = 'INVOKE_DEFAULT'
col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
classes = (
WM_OT_context_collection_boolean_set,
WM_OT_context_cycle_array,
WM_OT_context_cycle_enum,
WM_OT_context_cycle_int,
WM_OT_context_menu_enum,
WM_OT_context_modal_mouse,
WM_OT_context_pie_enum,
WM_OT_context_scale_float,
WM_OT_context_scale_int,
WM_OT_context_set_boolean,
WM_OT_context_set_enum,
WM_OT_context_set_float,
WM_OT_context_set_id,
WM_OT_context_set_int,
WM_OT_context_set_string,
WM_OT_context_set_value,
WM_OT_context_toggle,
WM_OT_context_toggle_enum,
WM_OT_doc_view,
WM_OT_doc_view_manual,
WM_OT_drop_blend_file,
WM_OT_operator_cheat_sheet,
WM_OT_operator_pie_enum,
WM_OT_path_open,
WM_OT_properties_add,
WM_OT_properties_context_change,
WM_OT_properties_edit,
WM_OT_properties_edit_value,
WM_OT_properties_remove,
WM_OT_sysinfo,
WM_OT_owner_disable,
WM_OT_owner_enable,
WM_OT_url_open,
WM_OT_url_open_preset,
WM_OT_tool_set_by_id,
WM_OT_tool_set_by_index,
WM_OT_tool_set_by_brush_type,
WM_OT_toolbar,
WM_OT_toolbar_fallback_pie,
WM_OT_toolbar_prompt,
BatchRenameAction,
WM_OT_batch_rename,
WM_MT_splash_quick_setup,
WM_MT_splash,
WM_MT_splash_about,
WM_MT_region_toggle_pie,
)