UI: Custom Tooltips with Optional Images #105905

Merged
Harley Acheson merged 6 commits from Harley/blender:CustomTooltips into main 2023-09-15 21:06:38 +02:00
48 changed files with 852 additions and 321 deletions
Showing only changes of commit edc96f19a6 - Show all commits

View File

@ -573,6 +573,12 @@ bool BVHMetal::build_BLAS_hair(Progress &progress,
return true;
}
# else /* MAC_OS_VERSION_14_0 */
(void)progress;
(void)device;
(void)queue;
(void)geom;
(void)(refit);
# endif /* MAC_OS_VERSION_14_0 */
return false;
}

View File

@ -1108,10 +1108,9 @@ void GHOST_SystemX11::processEvent(XEvent *xe)
}
if (ELEM(status, XLookupChars, XLookupBoth)) {
if (uchar(utf8_buf[0]) >= 32) { /* not an ascii control character */
/* do nothing for now, this is valid utf8 */
}
else {
/* Check for ASCII control characters.
* Inline `iscntrl` because the users locale must not change behavior. */
if ((utf8_buf[0] < 32 && utf8_buf[0] > 0) || (utf8_buf[0] == 127)) {
utf8_buf[0] = '\0';
}
}

View File

@ -56,7 +56,7 @@ const bTheme U_theme_default = {
.outline = RGBA(0x3d3d3dff),
.inner = RGBA(0x1d1d1dff),
.inner_sel = RGBA(0x181818ff),
.item = RGBA(0x4772b3ff),
.item = RGBA(0xffffff33),
.text = RGBA(0xe6e6e6ff),
.text_sel = RGBA(0xffffffff),
.roundness = 0.2f,
@ -191,7 +191,7 @@ const bTheme U_theme_default = {
.outline = RGBA(0x2d2d2dff),
.inner = RGBA(0xffffff00),
.inner_sel = RGBA(0x4772b3ff),
.item = RGBA(0x4772b3ff),
.item = RGBA(0xffffff33),
.text = RGBA(0xccccccff),
.text_sel = RGBA(0xffffffff),
.roundness = 0.2f,
@ -758,8 +758,8 @@ const bTheme U_theme_default = {
.back = RGBA(0x3d3d3dff),
.sub_back = RGBA(0x0000001f),
},
.shade2 = RGBA(0x2d4366e6),
.hilite = RGBA(0xff0000ff),
.shade2 = RGBA(0x4d4d4de6),
.hilite = RGBA(0x65a2ffff),
.grid = RGBA(0x1d1d1dff),
.vertex_size = 3,
.outline_width = 1,
@ -767,7 +767,7 @@ const bTheme U_theme_default = {
.syntaxl = RGBA(0xe6d573ff),
.syntaxs = RGBA(0xff734dff),
.syntaxb = RGBA(0xe62e67ff),
.syntaxn = RGBA(0x48c5e6ff),
.syntaxn = RGBA(0x48d9e6ff),
.syntaxv = RGBA(0x689d06ff),
.syntaxc = RGBA(0x939393ff),
.syntaxd = RGBA(0x9c73e6ff),

View File

@ -369,10 +369,11 @@ def enable(module_name, *, default_set=False, persistent=False, handle_error=Non
mod.__time__ = os.path.getmtime(mod.__file__)
mod.__addon_enabled__ = False
except BaseException as ex:
# if the addon doesn't exist, don't print full traceback
if type(ex) is ImportError and ex.name == module_name:
print("addon not loaded:", repr(module_name))
print("cause:", str(ex))
# If the add-on doesn't exist, don't print full trace-back because the back-trace is in this case
# is verbose without any useful details. A missing path is better communicated in a short message.
# Account for `ImportError` & `ModuleNotFoundError`.
if isinstance(ex, ImportError) and ex.name == module_name:
print("Add-on not loaded:", repr(module_name), "cause:", str(ex))
else:
handle_error(ex)

View File

@ -345,15 +345,15 @@ def load_scripts_extensions(*, reload_scripts=False):
bl_app_template_utils.reset(reload_scripts=reload_scripts)
del bl_app_template_utils
# deal with addons separately
_initialize = getattr(_addon_utils, "_initialize", None)
if _initialize is not None:
# Deal with add-ons separately.
_initialize_once = getattr(_addon_utils, "_initialize_once", None)
if _initialize_once is not None:
# first time, use fast-path
_initialize()
del _addon_utils._initialize
_initialize_once()
del _addon_utils._initialize_once
else:
_addon_utils.reset_all(reload_scripts=reload_scripts)
del _initialize
del _initialize_once
def script_path_user():

View File

@ -7,9 +7,29 @@ import bpy
language_id = "python"
# store our own __main__ module, not 100% needed
# but python expects this in some places
_BPY_MAIN_OWN = True
class _TempModuleOverride:
__slots__ = (
"module_name",
"module",
"module_override",
)
def __init__(self, module_name, module_override):
self.module_name = module_name
self.module = None
self.module_override = module_override
def __enter__(self):
self.module = sys.modules.get(self.module_name)
sys.modules[self.module_name] = self.module_override
def __exit__(self, type, value, traceback):
if self.module is None:
# Account for removal of `module_override` (albeit unlikely).
sys.modules.pop(self.module_name, None)
else:
sys.modules[self.module_name] = self.module
def add_scrollback(text, text_type):
@ -72,12 +92,9 @@ def get_console(console_id):
stdout = io.StringIO()
stderr = io.StringIO()
else:
if _BPY_MAIN_OWN:
import types
bpy_main_mod = types.ModuleType("__main__")
namespace = bpy_main_mod.__dict__
else:
namespace = {}
import types
bpy_main_mod = types.ModuleType("__main__")
namespace = bpy_main_mod.__dict__
namespace["__builtins__"] = sys.modules["builtins"]
namespace["bpy"] = bpy
@ -94,8 +111,7 @@ def get_console(console_id):
console.push("from mathutils import *")
console.push("from math import *")
if _BPY_MAIN_OWN:
console._bpy_main_mod = bpy_main_mod
console._bpy_main_mod = bpy_main_mod
import io
stdout = io.StringIO()
@ -121,25 +137,23 @@ def execute(context, is_interactive):
console, stdout, stderr = get_console(hash(context.region))
if _BPY_MAIN_OWN:
main_mod_back = sys.modules["__main__"]
sys.modules["__main__"] = console._bpy_main_mod
# redirect output
from contextlib import (
redirect_stdout,
redirect_stderr,
)
# not included with Python
# Not included with Python.
class redirect_stdin(redirect_stdout.__base__):
_stream = "stdin"
# don't allow the stdin to be used, can lock blender.
with redirect_stdout(stdout), \
redirect_stderr(stderr), \
redirect_stdin(None):
with (
redirect_stdout(stdout),
redirect_stderr(stderr),
# Don't allow the `stdin` to be used because it can lock Blender.
redirect_stdin(None),
_TempModuleOverride("__main__", console._bpy_main_mod),
):
# in case exception happens
line = "" # in case of encoding error
is_multiline = False
@ -156,9 +170,6 @@ def execute(context, is_interactive):
import traceback
stderr.write(traceback.format_exc())
if _BPY_MAIN_OWN:
sys.modules["__main__"] = main_mod_back
output = stdout.getvalue()
output_err = stderr.getvalue()
@ -220,46 +231,48 @@ def autocomplete(context):
if not console:
return {'CANCELLED'}
# don't allow the stdin to be used, can lock blender.
# note: unlikely stdin would be used for autocomplete. but its possible.
stdin_backup = sys.stdin
sys.stdin = None
scrollback = ""
scrollback_error = ""
if _BPY_MAIN_OWN:
main_mod_back = sys.modules["__main__"]
sys.modules["__main__"] = console._bpy_main_mod
# Don't allow the `stdin` to be used, can lock blender.
# note: unlikely `stdin` would be used for auto-complete - but it's possible.
try:
current_line = sc.history[-1]
line = current_line.body
from contextlib import redirect_stdout
# This function isn't aware of the text editor or being an operator
# just does the autocomplete then copy its results back
result = intellisense.expand(
line=line,
cursor=current_line.current_character,
namespace=console.locals,
private=bpy.app.debug_python)
# Not included with Python.
class redirect_stdin(redirect_stdout.__base__):
_stream = "stdin"
line_new = result[0]
current_line.body, current_line.current_character, scrollback = result
del result
with (
# Don't allow the `stdin` to be used because it can lock Blender.
redirect_stdin(None),
_TempModuleOverride("__main__", console._bpy_main_mod),
):
try:
current_line = sc.history[-1]
line = current_line.body
# update selection. setting body should really do this!
ofs = len(line_new) - len(line)
sc.select_start += ofs
sc.select_end += ofs
except:
# unlikely, but this can happen with unicode errors for example.
# or if the api attribute access itself causes an error.
import traceback
scrollback_error = traceback.format_exc()
# This function isn't aware of the text editor or being an operator
# just does the autocomplete then copy its results back
result = intellisense.expand(
line=line,
cursor=current_line.current_character,
namespace=console.locals,
private=bpy.app.debug_python)
if _BPY_MAIN_OWN:
sys.modules["__main__"] = main_mod_back
line_new = result[0]
current_line.body, current_line.current_character, scrollback = result
del result
# update selection. setting body should really do this!
ofs = len(line_new) - len(line)
sc.select_start += ofs
sc.select_end += ofs
except:
# unlikely, but this can happen with unicode errors for example.
# or if the API attribute access itself causes an error.
import traceback
scrollback_error = traceback.format_exc()
# Separate autocomplete output by command prompts
if scrollback != '':
@ -275,9 +288,6 @@ def autocomplete(context):
if scrollback_error:
add_scrollback(scrollback_error, 'ERROR')
# restore the stdin
sys.stdin = stdin_backup
context.area.tag_redraw()
return {'FINISHED'}

View File

@ -96,7 +96,7 @@
outline="#4d4d4dff"
inner="#282828ff"
inner_sel="#333333ff"
item="#8aace6ff"
item="#ffffff33"
text="#dddddd"
text_sel="#ffffff"
show_shaded="TRUE"
@ -306,7 +306,7 @@
outline="#e6e6e6ff"
inner="#1a1a1a00"
inner_sel="#668cccff"
item="#8aace6ff"
item="#ffffff33"
text="#1a1a1a"
text_sel="#ffffff"
show_shaded="FALSE"
@ -918,7 +918,7 @@
line_numbers="#d0d0d0"
line_numbers_background="#313133"
selected_text="#19191a"
cursor="#ff0000"
cursor="#71a8ff"
syntax_builtin="#ff1961"
syntax_symbols="#ff734d"
syntax_special="#95d600"
@ -926,7 +926,7 @@
syntax_reserved="#c4753b"
syntax_comment="#939393"
syntax_string="#f6e162"
syntax_numbers="#50dbff"
syntax_numbers="#50faff"
>
<space>
<ThemeSpaceGeneric

View File

@ -17,10 +17,13 @@ class ASSETSHELF_PT_display(Panel):
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
shelf = context.asset_shelf
layout.prop(shelf, "preview_size", text="Size")
layout.prop(shelf, "show_names", text="Names")
layout.prop(shelf, "preview_size")
@classmethod
def poll(cls, context):

View File

@ -89,6 +89,16 @@ class DATA_UL_bone_collections(UIList):
layout.prop(bcoll, "name", text="", emboss=False,
icon='DOT' if has_active_bone else 'BLANK1')
if armature.override_library:
icon = 'LIBRARY_DATA_OVERRIDE' if bcoll.is_local_override else 'BLANK1'
layout.prop(
bcoll,
"is_local_override",
text="",
emboss=False,
icon=icon)
layout.prop(bcoll, "is_visible", text="", emboss=False,
icon='HIDE_OFF' if bcoll.is_visible else 'HIDE_ON')
@ -245,7 +255,17 @@ class DATA_PT_custom_props_bcoll(ArmatureButtonsPanel, PropertyPanel, Panel):
@classmethod
def poll(cls, context):
return context.armature and context.armature.collections.active
arm = context.armature
if not arm:
return False
is_lib_override = arm.id_data.override_library and arm.id_data.override_library.reference
if is_lib_override:
# This is due to a limitation in scripts/modules/rna_prop_ui.py; if that
# limitation is lifted, this poll function should be adjusted.
return False
return arm.collections.active
classes = (

View File

@ -368,7 +368,8 @@ class _draw_tool_settings_context_mode:
elif not tool.has_datablock:
return False
paint = context.tool_settings.gpencil_paint
tool_settings = context.tool_settings
paint = tool_settings.gpencil_paint
brush = paint.brush
if brush is None:
return False
@ -376,7 +377,6 @@ class _draw_tool_settings_context_mode:
gp_settings = brush.gpencil_settings
row = layout.row(align=True)
tool_settings = context.scene.tool_settings
settings = tool_settings.gpencil_paint
row.template_ID_preview(settings, "brush", rows=3, cols=8, hide_buttons=True)
@ -401,7 +401,9 @@ class _draw_tool_settings_context_mode:
def SCULPT_GPENCIL(context, layout, tool):
if (tool is None) or (not tool.has_datablock):
return False
paint = context.tool_settings.gpencil_sculpt_paint
tool_settings = context.tool_settings
paint = tool_settings.gpencil_sculpt_paint
brush = paint.brush
from bl_ui.properties_paint_common import (
@ -415,7 +417,9 @@ class _draw_tool_settings_context_mode:
def WEIGHT_GPENCIL(context, layout, tool):
if (tool is None) or (not tool.has_datablock):
return False
paint = context.tool_settings.gpencil_weight_paint
tool_settings = context.tool_settings
paint = tool_settings.gpencil_weight_paint
brush = paint.brush
layout.template_ID_preview(paint, "brush", rows=3, cols=8, hide_buttons=True)
@ -432,11 +436,11 @@ class _draw_tool_settings_context_mode:
if (tool is None) or (not tool.has_datablock):
return False
paint = context.tool_settings.gpencil_vertex_paint
tool_settings = context.tool_settings
paint = tool_settings.gpencil_vertex_paint
brush = paint.brush
row = layout.row(align=True)
tool_settings = context.scene.tool_settings
settings = tool_settings.gpencil_vertex_paint
row.template_ID_preview(settings, "brush", rows=3, cols=8, hide_buttons=True)
@ -458,7 +462,8 @@ class _draw_tool_settings_context_mode:
return False
# See: 'VIEW3D_PT_tools_brush', basically a duplicate
settings = context.tool_settings.particle_edit
tool_settings = context.tool_settings
settings = tool_settings.particle_edit
brush = settings.brush
tool = settings.tool
if tool == 'NONE':
@ -696,7 +701,7 @@ class VIEW3D_HT_header(Header):
row = layout.row(align=True)
obj = context.active_object
# mode_string = context.mode
mode_string = context.mode
object_mode = 'OBJECT' if obj is None else obj.mode
has_pose_mode = (
(object_mode == 'POSE') or
@ -959,31 +964,29 @@ class VIEW3D_HT_header(Header):
sub.active = overlay.show_overlays
sub.popover(panel="VIEW3D_PT_overlay", text="")
show_bone_overlays = (
(object_mode == 'POSE') or
(object_mode == 'PAINT_WEIGHT' and context.pose_object) or
(object_mode in {'EDIT_ARMATURE', 'OBJECT'} and
VIEW3D_PT_overlay_bones.is_using_wireframe(context))
)
if mode_string == 'EDIT_MESH':
sub.popover(panel="VIEW3D_PT_overlay_edit_mesh", text="", icon='EDITMODE_HLT')
if mode_string == 'EDIT_CURVE':
sub.popover(panel="VIEW3D_PT_overlay_edit_curve", text="", icon='EDITMODE_HLT')
elif mode_string == 'SCULPT':
sub.popover(panel="VIEW3D_PT_overlay_sculpt", text="", icon='SCULPTMODE_HLT')
elif mode_string == 'SCULPT_CURVES':
sub.popover(panel="VIEW3D_PT_overlay_sculpt_curves", text="", icon='SCULPTMODE_HLT')
elif mode_string == 'PAINT_WEIGHT':
sub.popover(panel="VIEW3D_PT_overlay_weight_paint", text="", icon='WPAINT_HLT')
elif mode_string == 'PAINT_TEXTURE':
sub.popover(panel="VIEW3D_PT_overlay_texture_paint", text="", icon='TPAINT_HLT')
elif mode_string == 'PAINT_VERTEX':
sub.popover(panel="VIEW3D_PT_overlay_vertex_paint", text="", icon='VPAINT_HLT')
elif obj is not None and obj.type == 'GPENCIL':
sub.popover(panel="VIEW3D_PT_overlay_gpencil_options", text="", icon='OUTLINER_DATA_GREASEPENCIL')
if show_bone_overlays:
sub.popover(panel="VIEW3D_PT_overlay_bones", text="", icon="POSE_HLT")
elif context.mode == 'EDIT_MESH':
sub.popover(panel="VIEW3D_PT_overlay_edit_mesh", text="", icon="EDITMODE_HLT")
if context.mode == 'EDIT_CURVE':
sub.popover(panel="VIEW3D_PT_overlay_edit_curve", text="", icon="EDITMODE_HLT")
elif context.mode == 'SCULPT' and context.sculpt_object:
sub.popover(panel="VIEW3D_PT_overlay_sculpt", text="", icon="SCULPTMODE_HLT")
elif context.mode == 'SCULPT_CURVES' and context.object:
sub.popover(panel="VIEW3D_PT_overlay_sculpt_curves", text="", icon="SCULPTMODE_HLT")
elif context.mode == 'PAINT_WEIGHT':
sub.popover(panel="VIEW3D_PT_overlay_weight_paint", text="", icon="WPAINT_HLT")
elif context.mode == 'PAINT_TEXTURE':
sub.popover(panel="VIEW3D_PT_overlay_texture_paint", text="", icon="TPAINT_HLT")
elif context.mode == 'PAINT_VERTEX':
sub.popover(panel="VIEW3D_PT_overlay_vertex_paint", text="", icon="VPAINT_HLT")
elif context.object and context.object.type == 'GPENCIL':
sub.popover(panel="VIEW3D_PT_overlay_gpencil_options", text="", icon="OUTLINER_DATA_GREASEPENCIL")
# Separate from `elif` chain because it may coexist with weight-paint.
if (
has_pose_mode or
(object_mode in {'EDIT_ARMATURE', 'OBJECT'} and VIEW3D_PT_overlay_bones.is_using_wireframe(context))
):
sub.popover(panel="VIEW3D_PT_overlay_bones", text="", icon='POSE_HLT')
row = layout.row()
row.active = (object_mode == 'EDIT') or (shading.type in {'WIREFRAME', 'SOLID'})
@ -1023,7 +1026,7 @@ class VIEW3D_MT_editor_menus(Menu):
edit_object = context.edit_object
gp_edit = obj and obj.mode in {'EDIT_GPENCIL', 'PAINT_GPENCIL', 'SCULPT_GPENCIL',
'WEIGHT_GPENCIL', 'VERTEX_GPENCIL'}
tool_settings = context.scene.tool_settings
tool_settings = context.tool_settings
layout.menu("VIEW3D_MT_view")
@ -5520,7 +5523,9 @@ class VIEW3D_MT_edit_gpencil_stroke(Menu):
def draw(self, context):
layout = self.layout
settings = context.tool_settings.gpencil_sculpt
tool_settings = context.tool_settings
settings = tool_settings.gpencil_sculpt
layout.operator("gpencil.stroke_subdivide", text="Subdivide").only_selected = False
layout.menu("VIEW3D_MT_gpencil_simplify")
@ -5797,18 +5802,20 @@ class VIEW3D_MT_pivot_pie(Menu):
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
tool_settings = context.tool_settings
obj = context.active_object
mode = context.mode
pie.prop_enum(context.scene.tool_settings, "transform_pivot_point", value='BOUNDING_BOX_CENTER')
pie.prop_enum(context.scene.tool_settings, "transform_pivot_point", value='CURSOR')
pie.prop_enum(context.scene.tool_settings, "transform_pivot_point", value='INDIVIDUAL_ORIGINS')
pie.prop_enum(context.scene.tool_settings, "transform_pivot_point", value='MEDIAN_POINT')
pie.prop_enum(context.scene.tool_settings, "transform_pivot_point", value='ACTIVE_ELEMENT')
pie.prop_enum(tool_settings, "transform_pivot_point", value='BOUNDING_BOX_CENTER')
pie.prop_enum(tool_settings, "transform_pivot_point", value='CURSOR')
pie.prop_enum(tool_settings, "transform_pivot_point", value='INDIVIDUAL_ORIGINS')
pie.prop_enum(tool_settings, "transform_pivot_point", value='MEDIAN_POINT')
pie.prop_enum(tool_settings, "transform_pivot_point", value='ACTIVE_ELEMENT')
if (obj is None) or (mode in {'OBJECT', 'POSE', 'WEIGHT_PAINT'}):
pie.prop(context.scene.tool_settings, "use_transform_pivot_point_align")
pie.prop(tool_settings, "use_transform_pivot_point_align")
if mode == 'EDIT_GPENCIL':
pie.prop(context.scene.tool_settings.gpencil_sculpt, "use_scale_thickness")
pie.prop(tool_settings.gpencil_sculpt, "use_scale_thickness")
class VIEW3D_MT_orientations_pie(Menu):
@ -5853,6 +5860,7 @@ class VIEW3D_MT_proportional_editing_falloff_pie(Menu):
def draw(self, context):
layout = self.layout
pie = layout.menu_pie()
tool_settings = context.scene.tool_settings
pie.prop(tool_settings, "proportional_edit_falloff", expand=True)
@ -7104,10 +7112,7 @@ class VIEW3D_PT_overlay_sculpt(Panel):
@classmethod
def poll(cls, context):
return (
context.mode == 'SCULPT' and
context.sculpt_object
)
return context.mode == 'SCULPT'
def draw(self, context):
layout = self.layout
@ -7138,7 +7143,7 @@ class VIEW3D_PT_overlay_sculpt_curves(Panel):
@classmethod
def poll(cls, context):
return context.mode == 'SCULPT_CURVES' and (context.object)
return context.mode == 'SCULPT_CURVES'
def draw(self, context):
layout = self.layout
@ -8428,7 +8433,7 @@ class VIEW3D_AST_sculpt_brushes(bpy.types.AssetShelf):
@classmethod
def asset_poll(cls, asset):
return asset.file_data.id_type == 'BRUSH'
return asset.id_type == 'BRUSH'
classes = (

View File

@ -11,7 +11,7 @@ class MyAssetShelf(bpy.types.AssetShelf):
@classmethod
def asset_poll(cls, asset):
return asset.file_data.id_type in {'MATERIAL', 'OBJECT'}
return asset.id_type in {'MATERIAL', 'OBJECT'}
def register():

View File

@ -54,9 +54,6 @@ void ANIM_bonecoll_free(struct BoneCollection *bcoll);
/**
* Recalculate the armature & bone runtime data.
*
* NOTE: this should only be used when the runtime structs on the Armature and Bones are still
* empty. Any data allocated there will NOT be freed.
*
* TODO: move to BKE?
*/
void ANIM_armature_runtime_refresh(struct bArmature *armature);
@ -74,6 +71,17 @@ void ANIM_armature_runtime_free(struct bArmature *armature);
*/
struct BoneCollection *ANIM_armature_bonecoll_new(struct bArmature *armature, const char *name);
/**
* Add a bone collection to the Armature.
*
* NOTE: this should not typically be used. It is only used by the library overrides system to
* apply override operations.
*/
struct BoneCollection *ANIM_armature_bonecoll_insert_copy_after(
struct bArmature *armature,
struct BoneCollection *anchor,
const struct BoneCollection *bcoll_to_copy);
/**
* Remove a bone collection from the armature.
*/
@ -83,6 +91,9 @@ void ANIM_armature_bonecoll_remove(struct bArmature *armature, struct BoneCollec
* Set the given bone collection as the active one.
*
* Pass `nullptr` to clear the active bone collection.
*
* The bone collection MUST already be owned by this armature. If it is not,
* this function will simply clear the active bone collection.
*/
void ANIM_armature_bonecoll_active_set(struct bArmature *armature, struct BoneCollection *bcoll);
@ -94,6 +105,15 @@ void ANIM_armature_bonecoll_active_set(struct bArmature *armature, struct BoneCo
void ANIM_armature_bonecoll_active_index_set(struct bArmature *armature,
int bone_collection_index);
/**
* Determine whether the given bone collection is editable.
*
* Bone collections are editable when they are local, so either on a local Armature or added to a
* linked Armature via a library override in the local file.
*/
bool ANIM_armature_bonecoll_is_editable(const struct bArmature *armature,
const struct BoneCollection *bcoll);
/**
* Move the bone collection by \a step places up/down.
*

View File

@ -7,6 +7,7 @@
*/
#include "BLI_linklist.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_color.h"
#include "BLI_string.h"
@ -72,19 +73,31 @@ void ANIM_bonecoll_free(BoneCollection *bcoll)
MEM_delete(bcoll);
}
/**
* Construct the mapping from the bones to this collection.
*
* This assumes that the bones do not have such a pointer yet, i.e. calling this
* twice for the same bone collection will cause duplicate pointers. */
static void add_reverse_pointers(BoneCollection *bcoll)
{
LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) {
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&member->bone->runtime.collections, ref);
}
}
void ANIM_armature_runtime_refresh(bArmature *armature)
{
ANIM_armature_runtime_free(armature);
ANIM_armature_bonecoll_active_set(armature, armature->active_collection);
BoneCollection *active = ANIM_armature_bonecoll_get_by_name(armature,
armature->active_collection_name);
ANIM_armature_bonecoll_active_set(armature, active);
/* Construct the bone-to-collections mapping. */
LISTBASE_FOREACH (BoneCollection *, bcoll, &armature->collections) {
LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) {
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&member->bone->runtime.collections, ref);
}
add_reverse_pointers(bcoll);
}
}
@ -108,15 +121,48 @@ static void bonecoll_ensure_name_unique(bArmature *armature, BoneCollection *bco
BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature, const char *name)
{
BoneCollection *bcoll = ANIM_bonecoll_new(name);
if (!ID_IS_LINKED(&armature->id) && ID_IS_OVERRIDE_LIBRARY(&armature->id)) {
/* Mark this bone collection as local override, so that certain operations can be allowed. */
bcoll->flags |= BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL;
}
bonecoll_ensure_name_unique(armature, bcoll);
BLI_addtail(&armature->collections, bcoll);
return bcoll;
}
BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature,
BoneCollection *anchor,
const BoneCollection *bcoll_to_copy)
{
BoneCollection *bcoll = static_cast<BoneCollection *>(MEM_dupallocN(bcoll_to_copy));
/* Remap the bone pointers to the given armature, as `bcoll_to_copy` will
* likely be owned by another copy of the armature. */
BLI_duplicatelist(&bcoll->bones, &bcoll->bones);
BLI_assert_msg(armature->bonehash, "Expected armature bone hash to be there");
LISTBASE_FOREACH (BoneCollectionMember *, member, &bcoll->bones) {
member->bone = BKE_armature_find_bone_name(armature, member->bone->name);
}
if (bcoll_to_copy->prop) {
bcoll->prop = IDP_CopyProperty_ex(bcoll_to_copy->prop,
0 /*do_id_user ? 0 : LIB_ID_CREATE_NO_USER_REFCOUNT*/);
}
BLI_insertlinkafter(&armature->collections, anchor, bcoll);
bonecoll_ensure_name_unique(armature, bcoll);
add_reverse_pointers(bcoll);
return bcoll;
}
static void armature_bonecoll_active_clear(bArmature *armature)
{
armature->runtime.active_collection_index = -1;
armature->active_collection = nullptr;
armature->runtime.active_collection = nullptr;
armature->active_collection_name[0] = '\0';
}
void ANIM_armature_bonecoll_active_set(bArmature *armature, BoneCollection *bcoll)
@ -133,8 +179,9 @@ void ANIM_armature_bonecoll_active_set(bArmature *armature, BoneCollection *bcol
return;
}
STRNCPY(armature->active_collection_name, bcoll->name);
armature->runtime.active_collection_index = index;
armature->active_collection = bcoll;
armature->runtime.active_collection = bcoll;
}
void ANIM_armature_bonecoll_active_index_set(bArmature *armature, const int bone_collection_index)
@ -152,8 +199,21 @@ void ANIM_armature_bonecoll_active_index_set(bArmature *armature, const int bone
return;
}
STRNCPY(armature->active_collection_name, bcoll->name);
armature->runtime.active_collection_index = bone_collection_index;
armature->active_collection = bcoll;
armature->runtime.active_collection = bcoll;
}
bool ANIM_armature_bonecoll_is_editable(const bArmature *armature, const BoneCollection *bcoll)
{
const bool is_override = ID_IS_OVERRIDE_LIBRARY(armature);
if (ID_IS_LINKED(armature) && !is_override) {
return false;
}
if (is_override && (bcoll->flags & BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL) == 0) {
return false;
}
return true;
}
bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, const int step)
@ -166,7 +226,7 @@ bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, con
return false;
}
if (bcoll == armature->active_collection) {
if (bcoll == armature->runtime.active_collection) {
armature->runtime.active_collection_index = BLI_findindex(&armature->collections, bcoll);
}
return true;
@ -178,7 +238,15 @@ void ANIM_armature_bonecoll_name_set(bArmature *armature, BoneCollection *bcoll,
STRNCPY(old_name, bcoll->name);
STRNCPY_UTF8(bcoll->name, name);
if (name[0] == '\0') {
/* Refuse to have nameless collections. The name of the active collection is stored in DNA, and
* an empty string means 'no active collection'. */
STRNCPY(bcoll->name, bonecoll_default_name);
}
else {
STRNCPY_UTF8(bcoll->name, name);
}
bonecoll_ensure_name_unique(armature, bcoll);
BKE_animdata_fix_paths_rename_all(&armature->id, "collections", old_name, bcoll->name);
@ -226,6 +294,13 @@ static void add_membership(BoneCollection *bcoll, Bone *bone)
member->bone = bone;
BLI_addtail(&bcoll->bones, member);
}
/* Store reverse membership on the bone. */
static void add_reference(Bone *bone, BoneCollection *bcoll)
{
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&bone->runtime.collections, ref);
}
bool ANIM_armature_bonecoll_assign(BoneCollection *bcoll, Bone *bone)
{
@ -237,11 +312,7 @@ bool ANIM_armature_bonecoll_assign(BoneCollection *bcoll, Bone *bone)
}
add_membership(bcoll, bone);
/* Store reverse membership on the bone. */
BoneCollectionReference *ref = MEM_cnew<BoneCollectionReference>(__func__);
ref->bcoll = bcoll;
BLI_addtail(&bone->runtime.collections, ref);
add_reference(bone, bcoll);
return true;
}
@ -300,6 +371,8 @@ bool ANIM_armature_bonecoll_unassign(BoneCollection *bcoll, Bone *bone)
void ANIM_armature_bonecoll_unassign_all(Bone *bone)
{
LISTBASE_FOREACH_MUTABLE (BoneCollectionReference *, ref, &bone->runtime.collections) {
/* TODO: include Armature as parameter, and check that the bone collection to unassign from is
* actually editable. */
ANIM_armature_bonecoll_unassign(ref->bcoll, bone);
}
}
@ -394,7 +467,7 @@ void ANIM_bone_set_layer_ebone(EditBone *ebone, const int layer)
void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *ebone)
{
if (armature->active_collection == nullptr) {
if (armature->runtime.active_collection == nullptr) {
/* No active collection, do not assign to any. */
printf("ANIM_armature_bonecoll_assign_active(%s, %s): no active collection\n",
ebone->name,
@ -402,7 +475,7 @@ void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *e
return;
}
ANIM_armature_bonecoll_assign_editbone(armature->active_collection, ebone);
ANIM_armature_bonecoll_assign_editbone(armature->runtime.active_collection, ebone);
}
void ANIM_armature_bonecoll_show_from_bone(bArmature *armature, const Bone *bone)

View File

@ -55,6 +55,13 @@ class ANIM_armature_bone_collections : public testing::Test {
BLI_addtail(&arm.bonebase, &bone2); /* bone2 is root bone. */
BLI_addtail(&bone2.childbase, &bone3); /* bone3 has bone2 as parent. */
}
void TearDown() override
{
LISTBASE_FOREACH_BACKWARD_MUTABLE (BoneCollection *, bcoll, &arm.collections) {
ANIM_armature_bonecoll_remove(&arm, bcoll);
}
}
};
TEST_F(ANIM_armature_bone_collections, armature_owned_collections)
@ -112,4 +119,93 @@ TEST_F(ANIM_armature_bone_collections, bones_assign_remove)
"removed";
}
TEST_F(ANIM_armature_bone_collections, active_set_clear_by_pointer)
{
BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "Bones 1");
BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "Bones 2");
BoneCollection *bcoll3 = ANIM_bonecoll_new("Alien Bones");
ANIM_armature_bonecoll_active_set(&arm, bcoll1);
EXPECT_EQ(0, arm.runtime.active_collection_index);
EXPECT_EQ(bcoll1, arm.runtime.active_collection);
EXPECT_STREQ(bcoll1->name, arm.active_collection_name);
ANIM_armature_bonecoll_active_set(&arm, nullptr);
EXPECT_EQ(-1, arm.runtime.active_collection_index);
EXPECT_EQ(nullptr, arm.runtime.active_collection);
EXPECT_STREQ("", arm.active_collection_name);
ANIM_armature_bonecoll_active_set(&arm, bcoll2);
EXPECT_EQ(1, arm.runtime.active_collection_index);
EXPECT_EQ(bcoll2, arm.runtime.active_collection);
EXPECT_STREQ(bcoll2->name, arm.active_collection_name);
ANIM_armature_bonecoll_active_set(&arm, bcoll3);
EXPECT_EQ(-1, arm.runtime.active_collection_index);
EXPECT_EQ(nullptr, arm.runtime.active_collection);
EXPECT_STREQ("", arm.active_collection_name);
ANIM_bonecoll_free(bcoll3);
}
TEST_F(ANIM_armature_bone_collections, active_set_clear_by_index)
{
BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "Bones 1");
BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "Bones 2");
ANIM_armature_bonecoll_active_index_set(&arm, 0);
EXPECT_EQ(0, arm.runtime.active_collection_index);
EXPECT_EQ(bcoll1, arm.runtime.active_collection);
EXPECT_STREQ(bcoll1->name, arm.active_collection_name);
ANIM_armature_bonecoll_active_index_set(&arm, -1);
EXPECT_EQ(-1, arm.runtime.active_collection_index);
EXPECT_EQ(nullptr, arm.runtime.active_collection);
EXPECT_STREQ("", arm.active_collection_name);
ANIM_armature_bonecoll_active_index_set(&arm, 1);
EXPECT_EQ(1, arm.runtime.active_collection_index);
EXPECT_EQ(bcoll2, arm.runtime.active_collection);
EXPECT_STREQ(bcoll2->name, arm.active_collection_name);
ANIM_armature_bonecoll_active_index_set(&arm, 47);
EXPECT_EQ(-1, arm.runtime.active_collection_index);
EXPECT_EQ(nullptr, arm.runtime.active_collection);
EXPECT_STREQ("", arm.active_collection_name);
}
TEST_F(ANIM_armature_bone_collections, bcoll_is_editable)
{
BoneCollection *bcoll1 = ANIM_armature_bonecoll_new(&arm, "Bones 1");
BoneCollection *bcoll2 = ANIM_armature_bonecoll_new(&arm, "Bones 2");
EXPECT_EQ(0, bcoll1->flags & BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL);
EXPECT_EQ(0, bcoll2->flags & BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL);
EXPECT_TRUE(ANIM_armature_bonecoll_is_editable(&arm, bcoll1))
<< "Expecting local armature to be editable";
/* Fake that the armature is linked from another blend file. */
Library fake_lib;
arm.id.lib = &fake_lib;
EXPECT_FALSE(ANIM_armature_bonecoll_is_editable(&arm, bcoll1))
<< "Expecting local armature to not be editable";
/* Fake that the armature is an override, but linked from another blend file. */
IDOverrideLibrary fake_override;
bArmature fake_reference;
fake_override.reference = &fake_reference.id;
arm.id.override_library = &fake_override;
EXPECT_FALSE(ANIM_armature_bonecoll_is_editable(&arm, bcoll1))
<< "Expecting linked armature override to not be editable";
/* Fake that the armature is a local override. */
arm.id.lib = nullptr;
bcoll2->flags |= BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL;
EXPECT_FALSE(ANIM_armature_bonecoll_is_editable(&arm, bcoll1))
<< "Expecting linked bone collection on local armature override to not be editable";
EXPECT_TRUE(ANIM_armature_bonecoll_is_editable(&arm, bcoll2))
<< "Expecting local bone collection on local armature override to be editable";
}
} // namespace blender::animrig::tests

View File

@ -1053,7 +1053,10 @@ static FT_GlyphSlot blf_glyph_render(FontBLF *settings_font,
}
if ((settings_font->flags & BLF_MONOSPACED) && (settings_font != glyph_font)) {
blf_glyph_transform_monospace(glyph, BLI_wcwidth(char32_t(charcode)) * fixed_width);
const int col = BLI_wcwidth(char32_t(charcode));
if (col > 0) {
blf_glyph_transform_monospace(glyph, col * fixed_width);
}
}
/* Fallback glyph transforms, but only if required and not yet done. */

View File

@ -794,9 +794,14 @@ inline bool bNodeSocket::is_panel_collapsed() const
return (this->flag & SOCK_PANEL_COLLAPSED) != 0;
}
inline bool bNodeSocket::is_visible_or_panel_collapsed() const
{
return !this->is_hidden() && this->is_available();
}
inline bool bNodeSocket::is_visible() const
{
return !this->is_hidden() && this->is_available() && !this->is_panel_collapsed();
return this->is_visible_or_panel_collapsed() && !this->is_panel_collapsed();
}
inline bNode &bNodeSocket::owner_node()

View File

@ -13,6 +13,15 @@
#include "BKE_context.h"
#ifdef __cplusplus
namespace blender::asset_system {
class AssetRepresentation;
}
using AssetRepresentationHandle = blender::asset_system::AssetRepresentation;
#else
typedef struct AssetRepresentationHandle AssetRepresentationHandle;
#endif
#ifdef __cplusplus
extern "C" {
#endif
@ -464,12 +473,13 @@ typedef struct AssetShelfType {
/** Determine if an individual asset should be visible or not. May be a temporary design,
* visibility should first and foremost be controlled by asset traits. */
bool (*asset_poll)(const struct AssetShelfType *shelf_type, const struct AssetHandle *asset);
bool (*asset_poll)(const struct AssetShelfType *shelf_type,
const AssetRepresentationHandle *asset);
/** Asset shelves can define their own context menu via this layout definition callback. */
void (*draw_context_menu)(const struct bContext *C,
const struct AssetShelfType *shelf_type,
const struct AssetHandle *asset,
const AssetRepresentationHandle *asset,
struct uiLayout *layout);
/* RNA integration */

View File

@ -350,10 +350,18 @@ static void armature_blend_read_data(BlendDataReader *reader, ID *id)
}
BLO_read_list(reader, &arm->collections);
/* Bone collections added via an override can be edited, but ones that already exist in another
* blend file (so on the linked Armature) should not be touched. */
const bool reset_bcoll_override_flag = ID_IS_LINKED(&arm->id);
LISTBASE_FOREACH (BoneCollection *, bcoll, &arm->collections) {
direct_link_bone_collection(reader, bcoll);
if (reset_bcoll_override_flag) {
/* The linked Armature may have overrides in the library file already, and
* those should *not* be editable here. */
bcoll->flags &= ~BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL;
}
}
BLO_read_data_address(reader, &arm->active_collection);
BLO_read_data_address(reader, &arm->act_bone);
arm->act_edbone = nullptr;

View File

@ -2756,7 +2756,8 @@ void nodeRemSocketLinks(bNodeTree *ntree, bNodeSocket *sock)
bool nodeLinkIsHidden(const bNodeLink *link)
{
return !(link->fromsock->is_visible() && link->tosock->is_visible());
return !(link->fromsock->is_visible_or_panel_collapsed() &&
link->tosock->is_visible_or_panel_collapsed());
}
namespace blender::bke {

View File

@ -121,7 +121,7 @@ bool BLI_str_cursor_step_next_utf8(const char *str, size_t str_maxlen, int *pos)
const char *str_next = str_pos;
do {
str_next = BLI_str_find_next_char_utf8(str_next, str_end);
} while (str_next < str_end && str_next[0] != 0 && BLI_str_utf8_char_width(str_next) < 1);
} while (str_next < str_end && str_next[0] != 0 && BLI_str_utf8_char_width(str_next) == 0);
(*pos) += (str_next - str_pos);
if ((*pos) > (int)str_maxlen) {
(*pos) = (int)str_maxlen;

View File

@ -83,7 +83,7 @@ void SceneState::init(Object *camera_ob /*=nullptr*/)
}
xray_mode = shading.xray_alpha != 1.0f;
if (SHADING_XRAY_FLAG_ENABLED(shading)) {
if (xray_mode) {
/* Disable shading options that aren't supported in transparency mode. */
shading.flag &= ~(V3D_SHADING_SHADOW | V3D_SHADING_CAVITY | V3D_SHADING_DEPTH_OF_FIELD);
}

View File

@ -41,15 +41,32 @@ struct wmOperator;
/* ********************************************** */
/* Bone collections */
static bool bone_collection_poll(bContext *C)
static bool bone_collection_add_poll(bContext *C)
{
Object *ob = ED_object_context(C);
if (ob == nullptr) {
return false;
}
if (ID_IS_OVERRIDE_LIBRARY(ob)) {
CTX_wm_operator_poll_msg_set(C, "Cannot edit bone collections for library overrides");
if (ob->type != OB_ARMATURE) {
CTX_wm_operator_poll_msg_set(C, "Bone collections can only be added to an Armature");
return false;
}
if (ID_IS_LINKED(ob->data)) {
CTX_wm_operator_poll_msg_set(
C, "Cannot add bone collections to a linked Armature without an override");
return false;
}
return true;
}
/** Allow edits of local bone collection only (full local or local override). */
static bool active_bone_collection_poll(bContext *C)
{
Object *ob = ED_object_context(C);
if (ob == nullptr) {
return false;
}
@ -57,25 +74,20 @@ static bool bone_collection_poll(bContext *C)
CTX_wm_operator_poll_msg_set(C, "Bone collections can only be edited on an Armature");
return false;
}
return true;
}
static bool active_bone_collection_poll(bContext *C)
{
if (!bone_collection_poll(C)) {
return false;
}
Object *ob = ED_object_context(C);
if (ob == nullptr) {
return false;
}
bArmature *armature = static_cast<bArmature *>(ob->data);
if (armature->active_collection == nullptr) {
BoneCollection *bcoll = armature->runtime.active_collection;
if (bcoll == nullptr) {
CTX_wm_operator_poll_msg_set(C, "Armature has no active bone collection, select one first");
return false;
}
if (!ANIM_armature_bonecoll_is_editable(armature, bcoll)) {
CTX_wm_operator_poll_msg_set(
C, "Cannot edit bone collections that are linked from another blend file");
return false;
}
return true;
}
@ -103,7 +115,7 @@ void ARMATURE_OT_collection_add(wmOperatorType *ot)
/* api callbacks */
ot->exec = bone_collection_add_exec;
ot->poll = bone_collection_poll;
ot->poll = bone_collection_add_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@ -118,7 +130,7 @@ static int bone_collection_remove_exec(bContext *C, wmOperator * /* op */)
/* The poll function ensures armature->active_collection is not NULL. */
bArmature *armature = static_cast<bArmature *>(ob->data);
ANIM_armature_bonecoll_remove(armature, armature->active_collection);
ANIM_armature_bonecoll_remove(armature, armature->runtime.active_collection);
/* notifiers for updates */
WM_event_add_notifier(C, NC_OBJECT | ND_POSE, ob);
@ -153,7 +165,8 @@ static int bone_collection_move_exec(bContext *C, wmOperator *op)
/* Poll function makes sure this is valid. */
bArmature *armature = static_cast<bArmature *>(ob->data);
const bool ok = ANIM_armature_bonecoll_move(armature, armature->active_collection, direction);
const bool ok = ANIM_armature_bonecoll_move(
armature, armature->runtime.active_collection, direction);
if (!ok) {
return OPERATOR_CANCELLED;
}
@ -206,7 +219,7 @@ static BoneCollection *get_bonecoll_named_or_active(bContext * /*C*/,
RNA_string_get(op->ptr, "name", bcoll_name);
if (bcoll_name[0] == '\0') {
return armature->active_collection;
return armature->runtime.active_collection;
}
BoneCollection *bcoll = ANIM_armature_bonecoll_get_by_name(armature, bcoll_name);
@ -313,6 +326,29 @@ static bool bone_collection_assign_mode_specific(bContext *C,
}
}
static bool bone_collection_assign_poll(bContext *C)
{
Object *ob = ED_object_context(C);
if (ob == nullptr) {
return false;
}
if (ob->type != OB_ARMATURE) {
CTX_wm_operator_poll_msg_set(C, "Bone collections can only be edited on an Armature");
return false;
}
if (ID_IS_LINKED(ob->data)) {
CTX_wm_operator_poll_msg_set(
C, "Cannot edit bone collections on linked Armatures without override");
return false;
}
/* The target bone collection can be specified by name in an operator property, but that's not
* available here. So just allow in the poll function, and do the final check in the execute. */
return true;
}
/* Assign selected pchans to the bone collection that the user selects */
static int bone_collection_assign_exec(bContext *C, wmOperator *op)
{
@ -326,6 +362,12 @@ static int bone_collection_assign_exec(bContext *C, wmOperator *op)
return OPERATOR_CANCELLED;
}
bArmature *armature = static_cast<bArmature *>(ob->data);
if (!ANIM_armature_bonecoll_is_editable(armature, bcoll)) {
WM_reportf(RPT_ERROR, "Cannot assign to linked bone collection %s", bcoll->name);
return OPERATOR_CANCELLED;
}
bool made_any_changes = false;
bool had_bones_to_assign = false;
const bool mode_is_supported = bone_collection_assign_mode_specific(
@ -364,7 +406,7 @@ void ARMATURE_OT_collection_assign(wmOperatorType *ot)
// TODO: reinstate the menu?
// ot->invoke = bone_collections_menu_invoke;
ot->exec = bone_collection_assign_exec;
ot->poll = bone_collection_poll;
ot->poll = bone_collection_assign_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@ -426,7 +468,7 @@ void ARMATURE_OT_collection_unassign(wmOperatorType *ot)
/* api callbacks */
ot->exec = bone_collection_unassign_exec;
ot->poll = bone_collection_poll;
ot->poll = bone_collection_assign_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@ -450,6 +492,23 @@ static bool editbone_is_member(const EditBone *ebone, const BoneCollection *bcol
return false;
}
static bool armature_bone_select_poll(bContext *C)
{
Object *ob = ED_object_context(C);
if (ob == nullptr || ob->type != OB_ARMATURE) {
return false;
}
/* For bone selection, at least the pose should be editable to actually store
* the selection state. */
if (ID_IS_LINKED(ob->data) && !ID_IS_OVERRIDE_LIBRARY(ob->data)) {
CTX_wm_operator_poll_msg_set(C, "Cannot (de)select bones on linked Armature");
return false;
}
return true;
}
static void bone_collection_select(bContext *C,
Object *ob,
BoneCollection *bcoll,
@ -524,7 +583,7 @@ void ARMATURE_OT_collection_select(wmOperatorType *ot)
/* api callbacks */
ot->exec = bone_collection_select_exec;
ot->poll = bone_collection_poll;
ot->poll = armature_bone_select_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@ -564,7 +623,7 @@ void ARMATURE_OT_collection_deselect(wmOperatorType *ot)
/* api callbacks */
ot->exec = bone_collection_deselect_exec;
ot->poll = bone_collection_poll;
ot->poll = armature_bone_select_poll;
/* flags */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
@ -612,6 +671,13 @@ static int add_or_move_to_collection_exec(bContext *C,
}
}
if (!ANIM_armature_bonecoll_is_editable(arm, target_bcoll)) {
WM_reportf(RPT_ERROR,
"Bone collection %s is not editable, maybe add an override on the armature?",
target_bcoll->name);
return OPERATOR_CANCELLED;
}
FOREACH_PCHAN_SELECTED_IN_OBJECT_BEGIN (obpose, pchan) {
assign_func(target_bcoll, pchan->bone);
}
@ -636,13 +702,25 @@ static int assign_to_collection_exec(bContext *C, wmOperator *op)
static bool move_to_collection_poll(bContext *C)
{
/* TODO: add outliner support.
if (CTX_wm_space_outliner(C) != nullptr) {
return ED_outliner_collections_editor_poll(C);
Object *ob = ED_object_context(C);
if (ob == nullptr) {
return false;
}
*/
// TODO: add armature edit mode support.
return ED_operator_object_active_local_editable_posemode_exclusive(C);
if (ob->type != OB_ARMATURE) {
CTX_wm_operator_poll_msg_set(C, "Bone collections can only be edited on an Armature");
return false;
}
if (ID_IS_LINKED(ob->data) && !ID_IS_OVERRIDE_LIBRARY(ob->data)) {
CTX_wm_operator_poll_msg_set(C, "This needs a local Armature or an override");
return false;
}
/* Ideally this would also check the target bone collection to move/assign to.
* However, that requires access to the operator properties, and those are not
* available in the poll function. */
return true;
}
static const EnumPropertyItem *bone_collection_enum_itemf(bContext *C,
@ -659,6 +737,12 @@ static const EnumPropertyItem *bone_collection_enum_itemf(bContext *C,
int bcoll_index = 0;
LISTBASE_FOREACH_INDEX (BoneCollection *, bcoll, &arm->collections, bcoll_index) {
if (!ANIM_armature_bonecoll_is_editable(arm, bcoll)) {
/* Skip bone collections that cannot be assigned to because they're
* linked and thus uneditable. If there is a way to still show these, but in a disabled
* state, that would be preferred. */
continue;
}
item_tmp.identifier = bcoll->name;
item_tmp.name = bcoll->name;
item_tmp.value = bcoll_index;

View File

@ -18,6 +18,7 @@
#include "BLI_array_utils.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_string.h"
#include "BKE_armature.h"
#include "BKE_context.h"
@ -70,7 +71,7 @@ static void remap_ebone_bone_collection_references(
struct UndoArmature {
EditBone *act_edbone;
BoneCollection *active_collection;
char active_collection_name[MAX_NAME];
ListBase /* EditBone */ ebones;
ListBase /* BoneCollection */ bone_collections;
size_t undo_size;
@ -98,7 +99,12 @@ static void undoarm_to_editarm(UndoArmature *uarm, bArmature *arm)
ANIM_bonecoll_listbase_free(&arm->collections, true);
auto bcoll_map = ANIM_bonecoll_listbase_copy_no_membership(
&arm->collections, &uarm->bone_collections, true);
arm->active_collection = bcoll_map.lookup_default(uarm->active_collection, nullptr);
/* Always do a lookup-by-name and assignment. Even when the name of the active collection is
* still the same, the order may have changed and thus the index needs to be updated. */
BoneCollection *active_bcoll = ANIM_armature_bonecoll_get_by_name(arm,
uarm->active_collection_name);
ANIM_armature_bonecoll_active_set(arm, active_bcoll);
remap_ebone_bone_collection_references(arm->edbo, bcoll_map);
@ -123,7 +129,7 @@ static void *undoarm_from_editarm(UndoArmature *uarm, bArmature *arm)
/* Copy bone collections. */
auto bcoll_map = ANIM_bonecoll_listbase_copy_no_membership(
&uarm->bone_collections, &arm->collections, false);
uarm->active_collection = bcoll_map.lookup_default(arm->active_collection, nullptr);
STRNCPY(uarm->active_collection_name, arm->active_collection_name);
/* Point the new edit bones at the new collections. */
remap_ebone_bone_collection_references(&uarm->ebones, bcoll_map);

View File

@ -18,7 +18,6 @@
#include "AS_asset_catalog_tree.hh"
struct AssetFilterSettings;
struct AssetHandle;
struct AssetLibraryReference;
struct bContext;
@ -55,7 +54,7 @@ struct AssetItemTree {
asset_system::AssetCatalogTree build_filtered_catalog_tree(
const asset_system::AssetLibrary &library,
const AssetLibraryReference &library_ref,
blender::FunctionRef<bool(const AssetHandle &)> is_asset_visible_fn);
blender::FunctionRef<bool(const asset_system::AssetRepresentation &)> is_asset_visible_fn);
AssetItemTree build_filtered_all_catalog_tree(
const AssetLibraryReference &library_ref,
const bContext &C,

View File

@ -55,17 +55,17 @@ namespace blender::ed::asset {
asset_system::AssetCatalogTree build_filtered_catalog_tree(
const asset_system::AssetLibrary &library,
const AssetLibraryReference &library_ref,
const blender::FunctionRef<bool(const AssetHandle &)> is_asset_visible_fn)
const blender::FunctionRef<bool(const asset_system::AssetRepresentation &)>
is_asset_visible_fn)
{
Set<StringRef> known_paths;
/* Collect paths containing assets. */
ED_assetlist_iterate(library_ref, [&](AssetHandle asset_handle) {
if (!is_asset_visible_fn(asset_handle)) {
ED_assetlist_iterate(library_ref, [&](asset_system::AssetRepresentation &asset) {
if (!is_asset_visible_fn(asset)) {
return true;
}
asset_system::AssetRepresentation &asset = *ED_asset_handle_get_representation(&asset_handle);
const AssetMetaData &meta_data = asset.get_metadata();
if (BLI_uuid_is_nil(meta_data.catalog_id)) {
return true;

View File

@ -627,7 +627,7 @@ static uiBut *add_tab_button(uiBlock &block, StringRefNull name)
return but;
}
static void add_catalog_toggle_buttons(AssetShelfSettings &shelf_settings, uiLayout &layout)
static void add_catalog_tabs(AssetShelfSettings &shelf_settings, uiLayout &layout)
{
uiBlock *block = uiLayoutGetBlock(&layout);
@ -685,13 +685,16 @@ static void asset_shelf_header_draw(const bContext *C, Header *header)
PointerRNA shelf_ptr = shelf::active_shelf_ptr_from_context(C);
AssetShelf *shelf = static_cast<AssetShelf *>(shelf_ptr.data);
if (shelf) {
add_catalog_toggle_buttons(shelf->settings, *layout);
add_catalog_tabs(shelf->settings, *layout);
}
uiItemSpacer(layout);
uiItemR(layout, &shelf_ptr, "search_filter", UI_ITEM_NONE, "", ICON_VIEWZOOM);
uiItemPopoverPanel(layout, C, "ASSETSHELF_PT_display", "", ICON_IMGDISPLAY);
uiLayout *sub = uiLayoutRow(layout, false);
/* Same as file/asset browser header. */
uiLayoutSetUnitsX(sub, 8);
uiItemR(sub, &shelf_ptr, "search_filter", UI_ITEM_NONE, "", ICON_VIEWZOOM);
}
void ED_asset_shelf_header_regiontype_register(ARegionType *region_type, const int space_type)

View File

@ -104,12 +104,13 @@ void AssetView::build_items()
}
ED_assetlist_iterate(library_ref_, [&](AssetHandle asset_handle) {
if (shelf_.type->asset_poll && !shelf_.type->asset_poll(shelf_.type, &asset_handle)) {
const asset_system::AssetRepresentation *asset = ED_asset_handle_get_representation(
&asset_handle);
if (shelf_.type->asset_poll && !shelf_.type->asset_poll(shelf_.type, asset)) {
return true;
}
const asset_system::AssetRepresentation *asset = ED_asset_handle_get_representation(
&asset_handle);
const AssetMetaData &asset_data = asset->get_metadata();
if (catalog_filter_ && !catalog_filter_->contains(asset_data.catalog_id)) {
@ -213,7 +214,8 @@ void AssetViewItem::build_context_menu(bContext &C, uiLayout &column) const
const AssetView &asset_view = dynamic_cast<const AssetView &>(get_view());
const AssetShelfType &shelf_type = *asset_view.shelf_.type;
if (shelf_type.draw_context_menu) {
shelf_type.draw_context_menu(&C, &shelf_type, &asset_, &column);
asset_system::AssetRepresentation *asset = ED_asset_handle_get_representation(&asset_);
shelf_type.draw_context_menu(&C, &shelf_type, asset, &column);
}
}

View File

@ -45,8 +45,10 @@ class AssetCatalogSelectorTree : public ui::AbstractTreeView {
: shelf_(shelf), shelf_settings_(shelf_.settings)
{
catalog_tree_ = build_filtered_catalog_tree(
library, asset_system::all_library_reference(), [this](const AssetHandle asset_handle) {
return (!shelf_.type->asset_poll || shelf_.type->asset_poll(shelf_.type, &asset_handle));
library,
asset_system::all_library_reference(),
[this](const asset_system::AssetRepresentation &asset) {
return (!shelf_.type->asset_poll || shelf_.type->asset_poll(shelf_.type, &asset));
});
}

View File

@ -23,7 +23,6 @@
struct ARegion;
struct AssetFilterSettings;
struct AssetRepresentation;
struct AutoComplete;
struct EnumPropertyItem;
struct FileSelectParams;

View File

@ -19,6 +19,8 @@
#include "ED_undo.hh"
#include "WM_api.hh"
#include <fmt/format.h>
namespace blender::ui::greasepencil {
@ -74,7 +76,7 @@ class LayerNodeDropTarget : public TreeViewItemDropTarget {
return "";
}
bool on_drop(bContext * /*C*/, const DragInfo &drag_info) const override
bool on_drop(bContext *C, const DragInfo &drag_info) const override
{
const wmDragGreasePencilLayer *drag_grease_pencil =
static_cast<const wmDragGreasePencilLayer *>(drag_info.drag_data.poin);
@ -102,21 +104,28 @@ class LayerNodeDropTarget : public TreeViewItemDropTarget {
LayerGroup &drop_group = drop_tree_node_.as_group();
drag_parent.unlink_node(&drag_layer.as_node());
drop_group.add_layer(&drag_layer);
return true;
break;
}
case DropLocation::Before:
case DropLocation::Before: {
drag_parent.unlink_node(&drag_layer.as_node());
/* Draw order is inverted, so inserting before means inserting below. */
drop_parent_group->add_layer_after(&drag_layer, &drop_tree_node_);
return true;
case DropLocation::After:
break;
}
case DropLocation::After: {
drag_parent.unlink_node(&drag_layer.as_node());
/* Draw order is inverted, so inserting after means inserting above. */
drop_parent_group->add_layer_before(&drag_layer, &drop_tree_node_);
return true;
break;
}
default: {
BLI_assert_unreachable();
return false;
}
}
WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr);
return false;
return true;
}
};

View File

@ -49,6 +49,7 @@
#include "ED_transform_snap_object_context.hh"
#include "ED_undo.hh"
#include "ED_view3d.hh"
#include "ED_outliner.hh"
#include "WM_toolsystem.h"
@ -492,6 +493,8 @@ static bool object_transfer_mode_to_base(bContext *C, wmOperator *op, Base *base
}
WM_event_add_notifier(C, NC_SCENE | ND_OB_SELECT, scene);
ED_outliner_select_sync_from_object_tag(C);
WM_toolsystem_update_from_context_view3d(C);
mode_transferred = true;
}

View File

@ -407,8 +407,8 @@ static bool node_update_basis_socket(const bContext &C,
const int &locx,
int &locy)
{
if ((!input_socket || !input_socket->is_visible()) &&
(!output_socket || !output_socket->is_visible()))
if ((!input_socket || !input_socket->is_visible_or_panel_collapsed()) &&
(!output_socket || !output_socket->is_visible_or_panel_collapsed()))
{
return false;
}
@ -701,7 +701,7 @@ static void node_update_basis_from_declaration(
}
else {
/* Space between items. */
if (!is_first && item.input->is_visible()) {
if (!is_first && item.input->is_visible_or_panel_collapsed()) {
locy -= NODE_SOCKDY;
}
}
@ -714,7 +714,7 @@ static void node_update_basis_from_declaration(
}
else {
/* Space between items. */
if (!is_first && item.output->is_visible()) {
if (!is_first && item.output->is_visible_or_panel_collapsed()) {
locy -= NODE_SOCKDY;
}
}
@ -865,12 +865,12 @@ static void node_update_hidden(bNode &node, uiBlock &block)
/* Calculate minimal radius. */
for (const bNodeSocket *socket : node.input_sockets()) {
if (socket->is_visible()) {
if (socket->is_visible_or_panel_collapsed()) {
totin++;
}
}
for (const bNodeSocket *socket : node.output_sockets()) {
if (socket->is_visible()) {
if (socket->is_visible_or_panel_collapsed()) {
totout++;
}
}
@ -891,7 +891,7 @@ static void node_update_hidden(bNode &node, uiBlock &block)
float drad = rad;
for (bNodeSocket *socket : node.output_sockets()) {
if (socket->is_visible()) {
if (socket->is_visible_or_panel_collapsed()) {
/* Round the socket location to stop it from jiggling. */
socket->runtime->location = {
round(node.runtime->totr.xmax - hiddenrad + sinf(rad) * hiddenrad),
@ -904,7 +904,7 @@ static void node_update_hidden(bNode &node, uiBlock &block)
rad = drad = -float(M_PI) / (1.0f + float(totin));
for (bNodeSocket *socket : node.input_sockets()) {
if (socket->is_visible()) {
if (socket->is_visible_or_panel_collapsed()) {
/* Round the socket location to stop it from jiggling. */
socket->runtime->location = {
round(node.runtime->totr.xmin + hiddenrad + sinf(rad) * hiddenrad),
@ -1751,7 +1751,7 @@ static void node_draw_sockets(const View2D &v2d,
/* Socket inputs. */
int selected_input_len = 0;
for (const bNodeSocket *sock : node.input_sockets()) {
if (!sock->is_visible() || sock->is_panel_collapsed()) {
if (!sock->is_visible()) {
continue;
}
if (select_all || (sock->flag & SELECT)) {
@ -1774,7 +1774,7 @@ static void node_draw_sockets(const View2D &v2d,
int selected_output_len = 0;
if (draw_outputs) {
for (const bNodeSocket *sock : node.output_sockets()) {
if (!sock->is_visible() || sock->is_panel_collapsed()) {
if (!sock->is_visible()) {
continue;
}
if (select_all || (sock->flag & SELECT)) {

View File

@ -170,7 +170,7 @@ static void pick_input_link_by_link_intersect(const bContext &C,
static bool socket_is_available(bNodeTree * /*ntree*/, bNodeSocket *sock, const bool allow_used)
{
if (!sock->is_visible()) {
if (!sock->is_visible_or_panel_collapsed()) {
return false;
}
@ -2264,7 +2264,7 @@ bNodeSocket *get_main_socket(bNodeTree &ntree, bNode &node, eNodeSocketInOut in_
int index;
LISTBASE_FOREACH_INDEX (bNodeSocket *, socket, sockets, index) {
const nodes::SocketDeclaration &socket_decl = *socket_decls[index];
if (!socket->is_visible()) {
if (!socket->is_visible_or_panel_collapsed()) {
continue;
}
if (socket_decl.is_default_link_socket) {
@ -2285,7 +2285,7 @@ bNodeSocket *get_main_socket(bNodeTree &ntree, bNode &node, eNodeSocketInOut in_
/* Try all priorities, starting from 'highest'. */
for (int priority = maxpriority; priority >= 0; priority--) {
LISTBASE_FOREACH (bNodeSocket *, sock, sockets) {
if (!!sock->is_visible() && priority == get_main_socket_priority(sock)) {
if (!!sock->is_visible_or_panel_collapsed() && priority == get_main_socket_priority(sock)) {
return sock;
}
}

View File

@ -660,9 +660,28 @@ static void snap_transform_data(TransInfo *t, TransDataContainer *tc)
if (t->modifiers & MOD_SNAP_INVERT) {
invert_snap(snap_mode);
}
TransData *td = tc->data;
for (int i = 0; i < tc->data_len; i++, td++) {
transform_snap_anim_flush_data(t, td, snap_mode, td->loc);
float offset = 0;
float smallest_snap_delta = FLT_MAX;
/* In order to move the strip in a block and not each end individually,
* find the minimal snap offset first and then shift the whole strip by that amount. */
for (int i = 0; i < tc->data_len; i++) {
TransData td = tc->data[i];
float snap_value;
transform_snap_anim_flush_data(t, &td, snap_mode, &snap_value);
/* The snap_delta measures how far from the unsnapped position the value has moved. */
const float snap_delta = *td.loc - snap_value;
if (fabs(snap_delta) < fabs(smallest_snap_delta)) {
offset = snap_value - td.iloc[0];
smallest_snap_delta = snap_delta;
}
}
for (int i = 0; i < tc->data_len; i++) {
TransData td = tc->data[i];
*td.loc = td.iloc[0] + offset;
}
}

View File

@ -147,6 +147,7 @@ typedef struct bArmature_Runtime {
*/
int active_collection_index;
uint8_t _pad0[4];
struct BoneCollection *active_collection;
} bArmature_Runtime;
typedef struct bArmature {
@ -185,8 +186,12 @@ typedef struct bArmature {
/* BoneCollection. */
ListBase collections;
/* Do not directly assign, use `ANIM_armature_bonecoll_active_set` instead. */
struct BoneCollection *active_collection;
/** Do not directly assign, use `ANIM_armature_bonecoll_active_set` instead.
* This is stored as a string to make it possible for the library overrides system to understand
* when it actually changed (compared to a BoneCollection*, which would change on every load).
*/
char active_collection_name[64]; /* MAX_NAME. */
/** For UI, to show which layers are there. */
unsigned int layer_used DNA_DEPRECATED;
@ -419,8 +424,9 @@ typedef enum eBone_BBoneHandleFlag {
typedef enum eBoneCollection_Flag {
BONE_COLLECTION_VISIBLE = (1 << 0),
BONE_COLLECTION_SELECTABLE = (1 << 1), /* Intended to be implemented in the not-so-far future. */
BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL = (1 << 2), /* Added by a local library override. */
} eBoneCollection_Flag;
ENUM_OPERATORS(eBoneCollection_Flag, BONE_COLLECTION_SELECTABLE)
ENUM_OPERATORS(eBoneCollection_Flag, BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL)
#ifdef __cplusplus

View File

@ -189,6 +189,7 @@ typedef struct bNodeSocket {
bool is_hidden() const;
bool is_available() const;
bool is_panel_collapsed() const;
bool is_visible_or_panel_collapsed() const;
bool is_visible() const;
bool is_multi_input() const;
bool is_input() const;

View File

@ -157,6 +157,12 @@ bool RNA_property_overridable_get(PointerRNA *ptr, PropertyRNA *prop)
return true;
}
}
else if (RNA_struct_is_a(ptr->type, &RNA_BoneCollection)) {
BoneCollection *bcoll = static_cast<BoneCollection *>(ptr->data);
if (bcoll->flags & BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL) {
return true;
}
}
/* If this is a RNA-defined property (real or 'virtual' IDProp),
* we want to use RNA prop flag. */
return !(prop->flag_override & PROPOVERRIDE_NO_COMPARISON) &&

View File

@ -185,6 +185,15 @@ static void rna_Armature_edit_bone_remove(bArmature *arm,
RNA_POINTER_INVALIDATE(ebone_ptr);
}
static void rna_BoneCollections_active_set(PointerRNA *ptr,
PointerRNA value,
struct ReportList * /*reports*/)
{
bArmature *arm = (bArmature *)ptr->data;
BoneCollection *bcoll = (BoneCollection *)value.data;
ANIM_armature_bonecoll_active_set(arm, bcoll);
}
static int rna_BoneCollections_active_index_get(PointerRNA *ptr)
{
bArmature *arm = (bArmature *)ptr->data;
@ -210,6 +219,14 @@ static void rna_BoneCollections_active_index_range(
*max = max_ii(0, BLI_listbase_count(&arm->collections) - 1);
}
static void rna_BoneCollections_active_name_set(PointerRNA *ptr, const char *name)
{
bArmature *arm = (bArmature *)ptr->data;
BoneCollection *bcoll = ANIM_armature_bonecoll_get_by_name(arm, name);
ANIM_armature_bonecoll_active_set(arm, bcoll);
WM_main_add_notifier(NC_OBJECT | ND_BONE_COLLECTION, ptr->data);
}
static void rna_BoneCollections_move(bArmature *arm, ReportList *reports, int from, int to)
{
const int count = BLI_listbase_count(&arm->collections);
@ -299,6 +316,36 @@ static void rna_EditBone_collections_begin(CollectionPropertyIterator *iter, Poi
rna_iterator_listbase_begin(iter, &bone_collection_refs, nullptr);
}
/* Armature.collections library override support. */
static bool rna_Armature_collections_override_apply(Main *bmain,
RNAPropertyOverrideApplyContext &rnaapply_ctx)
{
PointerRNA *ptr_dst = &rnaapply_ctx.ptr_dst;
PropertyRNA *prop_dst = rnaapply_ctx.prop_dst;
PointerRNA *ptr_item_dst = &rnaapply_ctx.ptr_item_dst;
PointerRNA *ptr_item_src = &rnaapply_ctx.ptr_item_src;
IDOverrideLibraryPropertyOperation *opop = rnaapply_ctx.liboverride_operation;
if (opop->operation != LIBOVERRIDE_OP_INSERT_AFTER) {
printf("Unsupported RNA override operation on armature collections, ignoring\n");
return false;
}
bArmature *arm_dst = (bArmature *)ptr_dst->owner_id;
BoneCollection *bcoll_anchor = static_cast<BoneCollection *>(ptr_item_dst->data);
BoneCollection *bcoll_src = static_cast<BoneCollection *>(ptr_item_src->data);
BoneCollection *bcoll = ANIM_armature_bonecoll_insert_copy_after(
arm_dst, bcoll_anchor, bcoll_src);
if (!ID_IS_LINKED(&arm_dst->id)) {
/* Mark this bone collection as local override, so that certain operations can be allowed. */
bcoll->flags |= BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL;
}
RNA_property_update_main(bmain, nullptr, ptr_dst, prop_dst);
return true;
}
static char *rna_BoneColor_path_posebone(const PointerRNA *ptr)
{
/* Find the bPoseChan that owns this BoneColor. */
@ -1766,13 +1813,16 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop)
prop = RNA_def_property(srna, "active", PROP_POINTER, PROP_NONE);
RNA_def_property_struct_type(prop, "BoneCollection");
RNA_def_property_pointer_sdna(prop, nullptr, "active_collection");
RNA_def_property_pointer_sdna(prop, nullptr, "runtime.active_collection");
RNA_def_property_override_flag(prop, PROPOVERRIDE_IGNORE);
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_pointer_funcs(
prop, nullptr, "rna_BoneCollections_active_set", nullptr, nullptr);
RNA_def_property_ui_text(prop, "Active Collection", "Armature's active bone collection");
prop = RNA_def_property(srna, "active_index", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "runtime.active_collection_index");
RNA_def_property_flag(prop, PROP_EDITABLE);
RNA_def_property_override_flag(prop, PROPOVERRIDE_IGNORE);
RNA_def_property_ui_text(prop,
"Active Collection Index",
"The index of the Armature's active bone collection; -1 when there "
@ -1782,6 +1832,17 @@ static void rna_def_armature_collections(BlenderRNA *brna, PropertyRNA *cprop)
"rna_BoneCollections_active_index_set",
"rna_BoneCollections_active_index_range");
prop = RNA_def_property(srna, "active_name", PROP_STRING, PROP_NONE);
RNA_def_property_string_sdna(prop, nullptr, "active_collection_name");
/* TODO: For some reason the overrides system doesn't register a new operation when this property
* changes. Needs further investigation to figure out why & fix it. */
RNA_def_property_override_flag(prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY);
RNA_def_property_ui_text(prop,
"Active Collection Name",
"The name of the Armature's active bone collection; empty when there "
"is no active collection");
RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_BoneCollections_active_name_set");
/* Armature.collections.new(...) */
func = RNA_def_function(srna, "new", "ANIM_armature_bonecoll_new");
RNA_def_function_ui_description(func, "Add a new empty bone collection to the armature");
@ -1907,6 +1968,10 @@ static void rna_def_armature(BlenderRNA *brna)
RNA_def_property_collection_sdna(prop, nullptr, "collections", nullptr);
RNA_def_property_struct_type(prop, "BoneCollection");
RNA_def_property_ui_text(prop, "Bone Collections", "");
RNA_def_property_override_funcs(
prop, nullptr, nullptr, "rna_Armature_collections_override_apply");
RNA_def_property_override_flag(
prop, PROPOVERRIDE_OVERRIDABLE_LIBRARY | PROPOVERRIDE_LIBRARY_INSERTION);
rna_def_armature_collections(brna, prop);
/* Enum values */
@ -2003,7 +2068,6 @@ static void rna_def_bonecollection(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Name", "Unique within the Armature");
RNA_def_struct_name_property(srna, prop);
RNA_def_property_string_funcs(prop, nullptr, nullptr, "rna_BoneCollection_name_set");
// RNA_def_property_update(prop, 0, "rna_Bone_update_renamed");
prop = RNA_def_property(srna, "is_visible", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flags", BONE_COLLECTION_VISIBLE);
@ -2014,6 +2078,14 @@ static void rna_def_bonecollection(BlenderRNA *brna)
RNA_def_property_update(prop, NC_OBJECT | ND_POSE, nullptr);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
prop = RNA_def_property(srna, "is_local_override", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flags", BONE_COLLECTION_OVERRIDE_LIBRARY_LOCAL);
RNA_def_property_ui_text(
prop,
"Is Local Override",
"This collection was added via a library override in the current blend file");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
prop = RNA_def_property(srna, "bones", PROP_COLLECTION, PROP_NONE);
RNA_def_property_struct_type(prop, "Bone");
RNA_def_property_collection_funcs(prop,

View File

@ -8,6 +8,8 @@
#include <cstdlib>
#include "BLT_translation.h"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
@ -380,6 +382,14 @@ static PointerRNA rna_AssetHandle_local_id_get(PointerRNA *ptr)
return rna_pointer_inherit_refine(ptr, &RNA_ID, id);
}
static int rna_AssetRepresentation_id_type_get(PointerRNA *ptr)
{
using namespace blender::asset_system;
const AssetRepresentation *asset = static_cast<const AssetRepresentation *>(ptr->data);
return asset->get_id_type();
}
const EnumPropertyItem *rna_asset_library_reference_itemf(bContext * /*C*/,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
@ -585,12 +595,25 @@ static void rna_def_asset_handle(BlenderRNA *brna)
static void rna_def_asset_representation(BlenderRNA *brna)
{
StructRNA *srna;
PropertyRNA *prop;
srna = RNA_def_struct(brna, "AssetRepresentation", nullptr);
RNA_def_struct_ui_text(srna,
"Asset Representation",
"Information about an entity that makes it possible for the asset system "
"to deal with the entity as asset");
prop = RNA_def_property(srna, "id_type", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_items(prop, rna_enum_id_type_items);
RNA_def_property_enum_funcs(prop, "rna_AssetRepresentation_id_type_get", nullptr, nullptr);
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
RNA_def_property_ui_text(
prop,
"Data-block Type",
/* Won't ever actually return 'NONE' currently, this is just for information for once non-ID
* assets are supported. */
"The type of the data-block, if the asset represents one ('NONE' otherwise)");
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_ID);
}
static void rna_def_asset_catalog_path(BlenderRNA *brna)

View File

@ -279,6 +279,12 @@ static bool rna_Attribute_is_internal_get(PointerRNA *ptr)
return !BKE_attribute_allow_procedural_access(layer->name);
}
static bool rna_Attribute_is_required_get(PointerRNA *ptr)
{
const CustomDataLayer *layer = (const CustomDataLayer *)ptr->data;
return BKE_id_attribute_required(ptr->owner_id, layer->name);
}
static void rna_Attribute_data_begin(CollectionPropertyIterator *iter, PointerRNA *ptr)
{
ID *id = ptr->owner_id;
@ -1156,6 +1162,11 @@ static void rna_def_attribute(BlenderRNA *brna)
prop, "Is Internal", "The attribute is meant for internal use by Blender");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
prop = RNA_def_property(srna, "is_required", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_funcs(prop, "rna_Attribute_is_required_get", nullptr);
RNA_def_property_ui_text(prop, "Is Required", "Whether the attribute can be removed or renamed");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
/* types */
rna_def_attribute_float(brna);
rna_def_attribute_float_vector(brna);

View File

@ -1071,7 +1071,8 @@ static StructRNA *rna_Menu_refine(PointerRNA *mtr)
/* Asset Shelf */
static bool asset_shelf_asset_poll(const AssetShelfType *shelf_type, const AssetHandle *asset)
static bool asset_shelf_asset_poll(const AssetShelfType *shelf_type,
const AssetRepresentationHandle *asset)
{
extern FunctionRNA rna_AssetShelf_asset_poll_func;
@ -1080,7 +1081,7 @@ static bool asset_shelf_asset_poll(const AssetShelfType *shelf_type, const Asset
ParameterList list;
RNA_parameter_list_create(&list, &ptr, func);
RNA_parameter_set_lookup(&list, "asset_handle", &asset);
RNA_parameter_set_lookup(&list, "asset", &asset);
shelf_type->rna_ext.call(nullptr, &ptr, func, &list);
void *ret;
@ -1117,7 +1118,7 @@ static bool asset_shelf_poll(const bContext *C, const AssetShelfType *shelf_type
static void asset_shelf_draw_context_menu(const bContext *C,
const AssetShelfType *shelf_type,
const AssetHandle *asset,
const AssetRepresentationHandle *asset,
uiLayout *layout)
{
extern FunctionRNA rna_AssetShelf_draw_context_menu_func;
@ -1130,7 +1131,7 @@ static void asset_shelf_draw_context_menu(const bContext *C,
ParameterList list;
RNA_parameter_list_create(&list, &ptr, func);
RNA_parameter_set_lookup(&list, "context", &C);
RNA_parameter_set_lookup(&list, "asset_handle", &asset);
RNA_parameter_set_lookup(&list, "asset", &asset);
RNA_parameter_set_lookup(&list, "layout", &layout);
shelf_type->rna_ext.call((bContext *)C, &ptr, func, &list);
@ -2143,7 +2144,7 @@ static void rna_def_asset_shelf(BlenderRNA *brna)
"non-null output, the asset will be visible");
RNA_def_function_flag(func, FUNC_NO_SELF | FUNC_REGISTER_OPTIONAL);
RNA_def_function_return(func, RNA_def_boolean(func, "visible", true, "", ""));
parm = RNA_def_pointer(func, "asset_handle", "AssetHandle", "", "");
parm = RNA_def_pointer(func, "asset", "AssetRepresentation", "", "");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
func = RNA_def_function(srna, "draw_context_menu", nullptr);
@ -2152,7 +2153,7 @@ static void rna_def_asset_shelf(BlenderRNA *brna)
RNA_def_function_flag(func, FUNC_NO_SELF | FUNC_REGISTER_OPTIONAL);
parm = RNA_def_pointer(func, "context", "Context", "", "");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
parm = RNA_def_pointer(func, "asset_handle", "AssetHandle", "", "");
parm = RNA_def_pointer(func, "asset", "AssetRepresentation", "", "");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);
parm = RNA_def_pointer(func, "layout", "UILayout", "", "");
RNA_def_parameter_flags(parm, PropertyFlag(0), PARM_REQUIRED);

View File

@ -124,7 +124,7 @@ static void node_gather_link_searches(GatherLinkSearchOpParams &params)
/* If the source node has a geometry socket, connect it to the new viewer node as well. */
LISTBASE_FOREACH (bNodeSocket *, socket, &params.node.outputs) {
if (socket->type == SOCK_GEOMETRY && socket->is_visible()) {
if (socket->type == SOCK_GEOMETRY && socket->is_visible_or_panel_collapsed()) {
nodeAddLink(&params.node_tree,
&params.node,
socket,

View File

@ -766,35 +766,47 @@ PyObject *PyC_FrozenSetFromStrings(const char **strings)
PyObject *PyC_Err_Format_Prefix(PyObject *exception_type_prefix, const char *format, ...)
{
PyObject *error_value_prefix;
va_list args;
va_start(args, format);
error_value_prefix = PyUnicode_FromFormatV(format, args); /* can fail and be nullptr */
va_end(args);
PyObject *error_value_as_unicode = nullptr;
if (PyErr_Occurred()) {
PyObject *error_type, *error_value, *error_traceback;
PyErr_Fetch(&error_type, &error_value, &error_traceback);
if (PyUnicode_Check(error_value)) {
PyErr_Format(exception_type_prefix, "%S, %S", error_value_prefix, error_value);
error_value_as_unicode = error_value;
Py_INCREF(error_value_as_unicode);
}
else {
PyErr_Format(exception_type_prefix,
"%S, %.200s(%S)",
error_value_prefix,
Py_TYPE(error_value)->tp_name,
error_value);
error_value_as_unicode = PyUnicode_FromFormat(
"%.200s(%S)", Py_TYPE(error_value)->tp_name, error_value);
}
}
else {
PyErr_SetObject(exception_type_prefix, error_value_prefix);
PyErr_Restore(error_type, error_value, error_traceback);
}
Py_XDECREF(error_value_prefix);
va_list args;
/* dumb to always return nullptr but matches PyErr_Format */
va_start(args, format);
PyObject *error_value_format = PyUnicode_FromFormatV(format, args); /* Can fail and be null. */
va_end(args);
if (error_value_as_unicode) {
if (error_value_format) {
PyObject *error_value_format_prev = error_value_format;
error_value_format = PyUnicode_FromFormat(
"%S, %S", error_value_format, error_value_as_unicode);
Py_DECREF(error_value_format_prev);
}
else {
/* Should never happen, hints at incorrect API use or memory corruption. */
error_value_format = PyUnicode_FromFormat("(internal error), %S", error_value_as_unicode);
}
Py_DECREF(error_value_as_unicode);
}
PyErr_SetObject(exception_type_prefix, error_value_format);
Py_XDECREF(error_value_format);
/* Strange to always return null, matches #PyErr_Format. */
return nullptr;
}
@ -1162,15 +1174,25 @@ bool PyC_NameSpace_ImportArray(PyObject *py_dict, const char *imports[])
void PyC_MainModule_Backup(PyObject **r_main_mod)
{
PyObject *modules = PyImport_GetModuleDict();
*r_main_mod = PyDict_GetItemString(modules, "__main__");
Py_XINCREF(*r_main_mod); /* don't free */
PyObject *main_mod = PyDict_GetItemString(modules, "__main__");
if (main_mod) {
/* Ensure the backed up module is kept until it's ownership */
/* is transferred back to `sys.modules`. */
Py_INCREF(main_mod);
}
*r_main_mod = main_mod;
}
void PyC_MainModule_Restore(PyObject *main_mod)
{
PyObject *modules = PyImport_GetModuleDict();
PyDict_SetItemString(modules, "__main__", main_mod);
Py_XDECREF(main_mod);
if (main_mod) {
PyDict_SetItemString(modules, "__main__", main_mod);
Py_DECREF(main_mod);
}
else {
PyDict_DelItemString(modules, "__main__");
}
}
bool PyC_IsInterpreterActive()

View File

@ -1665,11 +1665,11 @@ static int pyrna_py_to_prop(
}
}
else {
PyC_Err_Format_Prefix(PyExc_TypeError,
"%.200s %.200s.%.200s doesn't support None from string types",
error_prefix,
RNA_struct_identifier(ptr->type),
RNA_property_identifier(prop));
PyErr_Format(PyExc_TypeError,
"%.200s %.200s.%.200s doesn't support None from string types",
error_prefix,
RNA_struct_identifier(ptr->type),
RNA_property_identifier(prop));
return -1;
}
}

View File

@ -20,11 +20,11 @@
#include "bpy_traceback.h"
static const char *traceback_filepath(PyTracebackObject *tb, PyObject **coerce)
static const char *traceback_filepath(PyTracebackObject *tb, PyObject **r_coerce)
{
PyCodeObject *code = PyFrame_GetCode(tb->tb_frame);
*coerce = PyUnicode_EncodeFSDefault(code->co_filename);
return PyBytes_AS_STRING(*coerce);
*r_coerce = PyUnicode_EncodeFSDefault(code->co_filename);
return PyBytes_AS_STRING(*r_coerce);
}
#define MAKE_PY_IDENTIFIER_EX(varname, value) static _Py_Identifier varname{value, -1};
@ -170,13 +170,8 @@ finally:
bool python_script_error_jump(
const char *filepath, int *r_lineno, int *r_offset, int *r_lineno_end, int *r_offset_end)
{
/* WARNING(@ideasman42): The normalized exception is restored (losing line number info).
* Ideally this would leave the exception state as it found it, but that needs to be done
* carefully with regards to reference counting, see: #97731. */
bool success = false;
PyObject *exception, *value;
PyTracebackObject *tb;
PyObject *exception, *value, *tb;
*r_lineno = -1;
*r_offset = 0;
@ -185,14 +180,19 @@ bool python_script_error_jump(
*r_offset_end = 0;
PyErr_Fetch(&exception, &value, (PyObject **)&tb);
if (exception == nullptr) {
if (exception == nullptr) { /* Equivalent of `!PyErr_Occurred()`. */
return false;
}
PyObject *base_exception_type = nullptr;
if (PyErr_GivenExceptionMatches(exception, PyExc_SyntaxError)) {
base_exception_type = PyExc_SyntaxError;
}
PyErr_NormalizeException(&exception, &value, &tb);
if (base_exception_type == PyExc_SyntaxError) {
/* No trace-back available when `SyntaxError`.
* Python has no API's to this. reference #parse_syntax_error() from `pythonrun.c`. */
PyErr_NormalizeException(&exception, &value, (PyObject **)&tb);
* Python has no API for this. reference #parse_syntax_error() from `pythonrun.c`. */
if (value) { /* Should always be true. */
PyObject *message;
@ -208,7 +208,7 @@ bool python_script_error_jump(
&text_py))
{
const char *filepath_exc = PyUnicode_AsUTF8(filepath_exc_py);
/* python adds a '/', prefix, so check for both */
/* Python adds a '/', prefix, so check for both. */
if ((BLI_path_cmp(filepath_exc, filepath) == 0) ||
(ELEM(filepath_exc[0], '\\', '/') && BLI_path_cmp(filepath_exc + 1, filepath) == 0))
{
@ -218,28 +218,28 @@ bool python_script_error_jump(
}
}
else {
PyErr_NormalizeException(&exception, &value, (PyObject **)&tb);
for (tb = (PyTracebackObject *)PySys_GetObject("last_traceback");
tb && (PyObject *)tb != Py_None;
tb = tb->tb_next)
BLI_assert((tb == PySys_GetObject("last_traceback")) ||
(tb == nullptr && PySys_GetObject("last_traceback") == Py_None));
for (PyTracebackObject *tb_iter = (PyTracebackObject *)tb;
tb_iter && (PyObject *)tb_iter != Py_None;
tb_iter = tb_iter->tb_next)
{
PyObject *coerce;
const char *tb_filepath = traceback_filepath(tb, &coerce);
const char *tb_filepath = traceback_filepath(tb_iter, &coerce);
const int match = ((BLI_path_cmp(tb_filepath, filepath) == 0) ||
(ELEM(tb_filepath[0], '\\', '/') &&
BLI_path_cmp(tb_filepath + 1, filepath) == 0));
Py_DECREF(coerce);
if (match) {
/* Even though a match has been found, keep searching to find the inner most line. */
success = true;
*r_lineno = *r_lineno_end = tb->tb_lineno;
/* used to break here, but better find the inner most line */
*r_lineno = *r_lineno_end = tb_iter->tb_lineno;
}
}
}
PyErr_Restore(exception, value, (PyObject *)tb); /* takes away reference! */
PyErr_Restore(exception, value, tb);
return success;
}

View File

@ -1934,6 +1934,7 @@ static ImBuf *seq_render_strip_stack(const SeqRenderData *context,
ImBuf *ibuf2 = seq_render_strip(context, state, seq, timeline_frame);
out = seq_render_strip_stack_apply_effect(context, seq, timeline_frame, ibuf1, ibuf2);
IMB_metadata_copy(out, ibuf2);
seq_cache_put(context, seq_arr[i], timeline_frame, SEQ_CACHE_STORE_COMPOSITE, out);

View File

@ -5670,7 +5670,9 @@ void wm_event_add_ghostevent(wmWindowManager *wm, wmWindow *win, const int type,
event.utf8_buf[0] = '\0';
}
else {
if (event.utf8_buf[0] < 32 && event.utf8_buf[0] > 0) {
/* Check for ASCII control characters.
* Inline `iscntrl` because the users locale must not change behavior. */
if ((event.utf8_buf[0] < 32 && event.utf8_buf[0] > 0) || (event.utf8_buf[0] == 127)) {
event.utf8_buf[0] = '\0';
}
}

View File

@ -152,7 +152,7 @@ class EulerFilterTest(AbstractAnimationTest, unittest.TestCase):
ob = bpy.context.view_layer.objects.active
action = ob.animation_data.action
return [action.fcurves.find('rotation_euler', index=idx) for idx in range(3)]
def get_view3d_context():
ctx = bpy.context.copy()
@ -173,7 +173,6 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
super().setUp()
bpy.ops.wm.read_homefile()
def test_keyframe_insertion_basic(self):
bpy.ops.mesh.primitive_monkey_add()
key_count = 100
@ -186,7 +185,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
self.assertEqual(key.co.x, key_index)
bpy.ops.object.delete(use_global=False)
def test_keyframe_insertion_high_frame_number(self):
@ -201,8 +200,8 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
self.assertEqual(key.co.x, key_index+frame_offset)
self.assertEqual(key.co.x, key_index + frame_offset)
bpy.ops.object.delete(use_global=False)
def test_keyframe_insertion_subframes_basic(self):
@ -210,14 +209,14 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
key_count = 50
with bpy.context.temp_override(**get_view3d_context()):
for i in range(key_count):
bpy.context.scene.frame_set(0, subframe=i/key_count)
bpy.context.scene.frame_set(0, subframe=i / key_count)
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
for key_index in range(key_count):
key = key_object.animation_data.action.fcurves[0].keyframe_points[key_index]
self.assertAlmostEqual(key.co.x, key_index/key_count)
self.assertAlmostEqual(key.co.x, key_index / key_count)
bpy.ops.object.delete(use_global=False)
def test_keyframe_insertion_subframes_high_frame_number(self):
@ -226,7 +225,7 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
frame_offset = 1000000
with bpy.context.temp_override(**get_view3d_context()):
for i in range(key_count):
bpy.context.scene.frame_set(frame_offset, subframe=i/key_count)
bpy.context.scene.frame_set(frame_offset, subframe=i / key_count)
bpy.ops.anim.keyframe_insert_by_name(type="Location")
key_object = bpy.context.active_object
@ -248,7 +247,8 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
1000000.8125,
1000000.875,
1000000.9375,
1000001.0 # Even though range() is exclusive, the floating point limitations mean keys end up on that position.
# Even though range() is exclusive, the floating point limitations mean keys end up on that position.
1000001.0
]
keyframe_points = key_object.animation_data.action.fcurves[0].keyframe_points
for i, value in enumerate(floating_point_steps):
@ -257,9 +257,9 @@ class KeyframeInsertTest(AbstractAnimationTest, unittest.TestCase):
# This checks that there is a key on every possible floating point value and not more than that.
self.assertEqual(len(floating_point_steps), len(keyframe_points))
bpy.ops.object.delete(use_global=False)
class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
def setUp(self):
@ -285,7 +285,7 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
# Only the key on frame -1 should be left
self.assertEqual(len(fcu.keyframe_points), 1)
bpy.ops.object.delete(use_global=False)
def test_keyframe_deletion_high_frame_number(self):
@ -299,16 +299,16 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
for i in range(key_count):
fcu.keyframe_points.insert(frame=i+frame_offset, value=0)
fcu.keyframe_points.insert(frame=i + frame_offset, value=0)
with bpy.context.temp_override(**get_view3d_context()):
for frame in range(key_count):
bpy.context.scene.frame_set(frame+frame_offset)
bpy.context.scene.frame_set(frame + frame_offset)
bpy.ops.anim.keyframe_delete_by_name(type="Location")
# Only the key on frame -1 should be left
self.assertEqual(len(fcu.keyframe_points), 1)
bpy.ops.object.delete(use_global=False)
def test_keyframe_deletion_subframe_basic(self):
@ -321,16 +321,16 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
for i in range(key_count):
fcu.keyframe_points.insert(frame=i/key_count, value=0)
fcu.keyframe_points.insert(frame=i / key_count, value=0)
with bpy.context.temp_override(**get_view3d_context()):
for frame in range(key_count):
bpy.context.scene.frame_set(0, subframe=frame/key_count)
bpy.context.scene.frame_set(0, subframe=frame / key_count)
bpy.ops.anim.keyframe_delete_by_name(type="Location")
# Only the key on frame -1 should be left
self.assertEqual(len(fcu.keyframe_points), 1)
bpy.ops.object.delete(use_global=False)
def test_keyframe_deletion_subframe_high_frame_number(self):
@ -344,20 +344,20 @@ class KeyframeDeleteTest(AbstractAnimationTest, unittest.TestCase):
key_object = bpy.context.active_object
fcu = key_object.animation_data.action.fcurves[0]
for i in range(key_count):
fcu.keyframe_points.insert(frame=i/key_count+frame_offset, value=0)
fcu.keyframe_points.insert(frame=i / key_count + frame_offset, value=0)
with bpy.context.temp_override(**get_view3d_context()):
for frame in range(key_count):
bpy.context.scene.frame_set(frame_offset, subframe=frame/key_count)
bpy.context.scene.frame_set(frame_offset, subframe=frame / key_count)
bpy.ops.anim.keyframe_delete_by_name(type="Location")
# Only the key on frame -1 should be left
# This works even though there are floating point precision issues,
# This works even though there are floating point precision issues,
# because the deletion has the exact same precision as the insertion.
# Due to that, the code calls keyframe_delete_by_name for
# Due to that, the code calls keyframe_delete_by_name for
# every floating point step multiple times.
self.assertEqual(len(fcu.keyframe_points), 1)
bpy.ops.object.delete(use_global=False)