Allow select range in animation editor

This patch will add support to select all channels between active channel
and last-clicked channel without deselecting previous selection. `Shift` key is
now assigned for the range selection and `ctrl` key to extend the selection. This
changes will make multi-selection similar as outliner tree-elements. New function is
created `animchannel_select_range` to handle the range selection of channels.

Old Differential revision: https://archive.blender.org/developer/D17079

Pull Request: https://projects.blender.org/blender/blender/pulls/104565
This commit is contained in:
Pratik Borhade 2023-05-05 17:46:05 +02:00 committed by Pratik Borhade
parent c9b51591da
commit 80feb13665
7 changed files with 223 additions and 29 deletions

@ -3463,7 +3463,9 @@ def km_animation_channels(params):
items.extend([
# Click select.
("anim.channels_click", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None),
("anim.channels_click", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True},
("anim.channels_click", {"type": 'LEFTMOUSE', "value": 'CLICK', "shift": True},
{"properties": [("extend_range", True)]}),
("anim.channels_click", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True},
{"properties": [("extend", True)]}),
("anim.channels_click", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True, "ctrl": True},
{"properties": [("children_only", True)]}),

@ -313,7 +313,64 @@ void ANIM_set_active_channel(bAnimContext *ac,
ANIM_animdata_freelist(&anim_data);
}
static void select_pchan_for_action_group(bAnimContext *ac, bActionGroup *agrp, bAnimListElem *ale)
bool ANIM_is_active_channel(bAnimListElem *ale)
{
switch (ale->type) {
case ANIMTYPE_FILLACTD: /* Action Expander */
case ANIMTYPE_DSMAT: /* Datablock AnimData Expanders */
case ANIMTYPE_DSLAM:
case ANIMTYPE_DSCAM:
case ANIMTYPE_DSCACHEFILE:
case ANIMTYPE_DSCUR:
case ANIMTYPE_DSSKEY:
case ANIMTYPE_DSWOR:
case ANIMTYPE_DSPART:
case ANIMTYPE_DSMBALL:
case ANIMTYPE_DSARM:
case ANIMTYPE_DSMESH:
case ANIMTYPE_DSNTREE:
case ANIMTYPE_DSTEX:
case ANIMTYPE_DSLAT:
case ANIMTYPE_DSLINESTYLE:
case ANIMTYPE_DSSPK:
case ANIMTYPE_DSGPENCIL:
case ANIMTYPE_DSMCLIP:
case ANIMTYPE_DSHAIR:
case ANIMTYPE_DSPOINTCLOUD:
case ANIMTYPE_DSVOLUME:
case ANIMTYPE_NLAACTION:
case ANIMTYPE_DSSIMULATION: {
if (ale->adt) {
return ale->adt->flag & ADT_UI_ACTIVE;
}
}
case ANIMTYPE_GROUP: {
bActionGroup *argp = (bActionGroup *)ale->data;
return argp->flag & AGRP_ACTIVE;
}
case ANIMTYPE_FCURVE:
case ANIMTYPE_NLACURVE: {
FCurve *fcu = (FCurve *)ale->data;
return fcu->flag & FCURVE_ACTIVE;
}
case ANIMTYPE_GPLAYER: {
bGPDlayer *gpl = (bGPDlayer *)ale->data;
return gpl->flag & GP_LAYER_ACTIVE;
}
/* These channel types do not have active flags. */
case ANIMTYPE_MASKLAYER:
case ANIMTYPE_SHAPEKEY:
break;
}
return false;
}
/* change_active determines whether to change the active bone of the armature when selecting pose
* channels. It is false during range selection otherwise true. */
static void select_pchan_for_action_group(bAnimContext *ac,
bActionGroup *agrp,
bAnimListElem *ale,
const bool change_active)
{
/* Armatures-Specific Feature:
* See mouse_anim_channels() -> ANIMTYPE_GROUP case for more details (#38737)
@ -329,11 +386,12 @@ static void select_pchan_for_action_group(bAnimContext *ac, bActionGroup *agrp,
* TODO: check the first F-Curve or so to be sure...
*/
bPoseChannel *pchan = BKE_pose_channel_find_name(ob->pose, agrp->name);
if (agrp->flag & AGRP_SELECTED) {
ED_pose_bone_select(ob, pchan, true);
ED_pose_bone_select(ob, pchan, true, change_active);
}
else {
ED_pose_bone_select(ob, pchan, false);
ED_pose_bone_select(ob, pchan, false, change_active);
}
}
}
@ -441,10 +499,15 @@ static void anim_channels_select_set(bAnimContext *ac,
eAnimChannels_SetFlag sel)
{
bAnimListElem *ale;
/* Boolean to keep active channel status during range selection. */
const bool change_active = (sel != ACHANNEL_SETFLAG_EXTEND_RANGE);
for (ale = anim_data.first; ale; ale = ale->next) {
switch (ale->type) {
case ANIMTYPE_SCENE: {
if (change_active) {
break;
}
Scene *scene = (Scene *)ale->data;
ACHANNEL_SET_FLAG(scene, sel, SCE_DS_SELECTED);
@ -471,8 +534,10 @@ static void anim_channels_select_set(bAnimContext *ac,
case ANIMTYPE_GROUP: {
bActionGroup *agrp = (bActionGroup *)ale->data;
ACHANNEL_SET_FLAG(agrp, sel, AGRP_SELECTED);
select_pchan_for_action_group(ac, agrp, ale);
agrp->flag &= ~AGRP_ACTIVE;
select_pchan_for_action_group(ac, agrp, ale, change_active);
if (change_active) {
agrp->flag &= ~AGRP_ACTIVE;
}
break;
}
case ANIMTYPE_FCURVE:
@ -480,7 +545,7 @@ static void anim_channels_select_set(bAnimContext *ac,
FCurve *fcu = (FCurve *)ale->data;
ACHANNEL_SET_FLAG(fcu, sel, FCURVE_SELECTED);
if ((fcu->flag & FCURVE_SELECTED) == 0) {
if (!(fcu->flag & FCURVE_SELECTED) && change_active) {
/* Only erase the ACTIVE flag when deselecting. This ensures that "select all curves"
* retains the currently active curve. */
fcu->flag &= ~FCURVE_ACTIVE;
@ -527,7 +592,9 @@ static void anim_channels_select_set(bAnimContext *ac,
/* need to verify that this data is valid for now */
if (ale->adt) {
ACHANNEL_SET_FLAG(ale->adt, sel, ADT_UI_SELECTED);
ale->adt->flag &= ~ADT_UI_ACTIVE;
if (change_active) {
ale->adt->flag &= ~ADT_UI_ACTIVE;
}
}
break;
}
@ -2891,7 +2958,7 @@ static void box_select_anim_channels(bAnimContext *ac, rcti *rect, short selectm
switch (ale->type) {
case ANIMTYPE_GROUP: {
bActionGroup *agrp = (bActionGroup *)ale->data;
select_pchan_for_action_group(ac, agrp, ale);
select_pchan_for_action_group(ac, agrp, ale, true);
/* always clear active flag after doing this */
agrp->flag &= ~AGRP_ACTIVE;
break;
@ -3172,6 +3239,70 @@ static int click_select_channel_scene(bAnimListElem *ale,
return (ND_ANIMCHAN | NA_SELECTED);
}
/* Return whether active channel of given type is present. */
static bool animchannel_has_active_of_type(bAnimContext *ac, const eAnim_ChannelType type)
{
ListBase anim_data = anim_channels_for_selection(ac);
bool is_active_found = false;
LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
if (ale->type != type) {
continue;
}
is_active_found = ANIM_is_active_channel(ale);
if (is_active_found) {
break;
}
}
ANIM_animdata_freelist(&anim_data);
return is_active_found;
}
/* Select channels that lies between active channel and cursor_elem. */
static void animchannel_select_range(bAnimContext *ac, bAnimListElem *cursor_elem)
{
ListBase anim_data = anim_channels_for_selection(ac);
bool in_selection_range = false;
LISTBASE_FOREACH (bAnimListElem *, ale, &anim_data) {
/* Allow selection when active channel and `cursor_elem` are of same type. */
if (ale->type != cursor_elem->type) {
continue;
}
const bool is_cursor_elem = (ale->data == cursor_elem->data);
const bool is_active_elem = ANIM_is_active_channel(ale);
/* Restrict selection when active element is not found and group-channels are excluded from the
* selection. */
if (is_active_elem || is_cursor_elem) {
/* Select first and last element from the range. Reverse selection status on extremes. */
ANIM_channel_setting_set(ac, ale, ACHANNEL_SETTING_SELECT, ACHANNEL_SETFLAG_ADD);
in_selection_range = !in_selection_range;
if (ale->type == ANIMTYPE_GROUP) {
select_pchan_for_action_group(ac, (bActionGroup *)ale->data, ale, false);
}
}
else if (in_selection_range) {
/* Select elements between the range. */
ANIM_channel_setting_set(ac, ale, ACHANNEL_SETTING_SELECT, ACHANNEL_SETFLAG_ADD);
if (ale->type == ANIMTYPE_GROUP) {
select_pchan_for_action_group(ac, (bActionGroup *)ale->data, ale, false);
}
}
if (is_active_elem && is_cursor_elem) {
/* Selection range is only one element when active channel and clicked channel are same. So
* exit out of the loop when this condition is hit. */
break;
}
}
ANIM_animdata_freelist(&anim_data);
}
static int click_select_channel_object(bContext *C,
bAnimContext *ac,
bAnimListElem *ale,
@ -3195,8 +3326,13 @@ static int click_select_channel_object(bContext *C,
adt->flag ^= ADT_UI_SELECTED;
}
}
else if (selectmode == SELECT_EXTEND_RANGE) {
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
}
else {
/* deselect all */
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_CLEAR);
BKE_view_layer_synced_ensure(scene, view_layer);
/* TODO: should this deselect all other types of channels too? */
LISTBASE_FOREACH (Base *, b, BKE_view_layer_object_bases_get(view_layer)) {
@ -3219,7 +3355,8 @@ static int click_select_channel_object(bContext *C,
* to avoid getting stuck there, see: #48747. */
ED_object_base_activate_with_mode_exit_if_needed(C, base); /* adds notifier */
if ((adt) && (adt->flag & ADT_UI_SELECTED)) {
/* Similar to outliner, do not change active element when selecting elements in range.*/
if ((adt) && (adt->flag & ADT_UI_SELECTED) && (selectmode != SELECT_EXTEND_RANGE)) {
adt->flag |= ADT_UI_ACTIVE;
}
@ -3239,14 +3376,18 @@ static int click_select_channel_dummy(bAnimContext *ac,
/* inverse selection status of this AnimData block only */
ale->adt->flag ^= ADT_UI_SELECTED;
}
else if (selectmode == SELECT_EXTEND_RANGE) {
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
}
else {
/* select AnimData block by itself */
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_CLEAR);
ale->adt->flag |= ADT_UI_SELECTED;
}
/* set active? */
if (ale->adt->flag & ADT_UI_SELECTED) {
/* Similar to outliner, do not change active element when selecting elements in range. */
if ((ale->adt->flag & ADT_UI_SELECTED) && (selectmode != SELECT_EXTEND_RANGE)) {
ale->adt->flag |= ADT_UI_ACTIVE;
}
@ -3292,6 +3433,10 @@ static int click_select_channel_group(bAnimContext *ac,
/* inverse selection status of this group only */
agrp->flag ^= AGRP_SELECTED;
}
else if (selectmode == SELECT_EXTEND_RANGE) {
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
}
else if (selectmode == -1) {
/* select all in group (and deselect everything else) */
FCurve *fcu;
@ -3318,17 +3463,22 @@ static int click_select_channel_group(bAnimContext *ac,
agrp->flag |= AGRP_SELECTED;
}
/* if group is selected now, make group the 'active' one in the visible list */
/* if group is selected now, make group the 'active' one in the visible list.
* Similar to outliner, do not change active element when selecting elements in range. */
if (agrp->flag & AGRP_SELECTED) {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, agrp, ANIMTYPE_GROUP);
if (pchan) {
ED_pose_bone_select(ob, pchan, true);
if (selectmode != SELECT_EXTEND_RANGE) {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, agrp, ANIMTYPE_GROUP);
if (pchan) {
ED_pose_bone_select(ob, pchan, true, true);
}
}
}
else {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, NULL, ANIMTYPE_GROUP);
if (pchan) {
ED_pose_bone_select(ob, pchan, false);
if (selectmode != SELECT_EXTEND_RANGE) {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, NULL, ANIMTYPE_GROUP);
if (pchan) {
ED_pose_bone_select(ob, pchan, false, true);
}
}
}
@ -3347,14 +3497,19 @@ static int click_select_channel_fcurve(bAnimContext *ac,
/* inverse selection status of this F-Curve only */
fcu->flag ^= FCURVE_SELECTED;
}
else if (selectmode == SELECT_EXTEND_RANGE) {
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
}
else {
/* select F-Curve by itself */
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_CLEAR);
fcu->flag |= FCURVE_SELECTED;
}
/* if F-Curve is selected now, make F-Curve the 'active' one in the visible list */
if (fcu->flag & FCURVE_SELECTED) {
/* if F-Curve is selected now, make F-Curve the 'active' one in the visible list.
* Similar to outliner, do not change active element when selecting elements in range. */
if ((fcu->flag & FCURVE_SELECTED) && (selectmode != SELECT_EXTEND_RANGE)) {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, fcu, ale->type);
}
@ -3422,14 +3577,19 @@ static int click_select_channel_gplayer(bContext *C,
/* invert selection status of this layer only */
gpl->flag ^= GP_LAYER_SELECT;
}
else if (selectmode == SELECT_EXTEND_RANGE) {
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_EXTEND_RANGE);
animchannel_select_range(ac, ale);
}
else {
/* select layer by itself */
ANIM_anim_channels_select_set(ac, ACHANNEL_SETFLAG_CLEAR);
gpl->flag |= GP_LAYER_SELECT;
}
/* change active layer, if this is selected (since we must always have an active layer) */
if (gpl->flag & GP_LAYER_SELECT) {
/* change active layer, if this is selected (since we must always have an active layer).
* Similar to outliner, do not change active element when selecting elements in range. */
if ((gpl->flag & GP_LAYER_SELECT) && (selectmode != SELECT_EXTEND_RANGE)) {
ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, gpl, ANIMTYPE_GPLAYER);
/* update other layer status */
BKE_gpencil_layer_active_set(gpd, gpl);
@ -3478,7 +3638,7 @@ static int click_select_channel_masklayer(bAnimContext *ac,
static int mouse_anim_channels(bContext *C,
bAnimContext *ac,
const int channel_index,
const short /* eEditKeyframes_Select or -1 */ selectmode)
short /* eEditKeyframes_Select or -1 */ selectmode)
{
ListBase anim_data = {NULL, NULL};
bAnimListElem *ale;
@ -3516,6 +3676,11 @@ static int mouse_anim_channels(bContext *C,
return 0;
}
/* Change selection mode to single when no active element is found. */
if ((selectmode == SELECT_EXTEND_RANGE) && !animchannel_has_active_of_type(ac, ale->type)) {
selectmode = SELECT_INVERT;
}
/* action to take depends on what channel we've got */
/* WARNING: must keep this in sync with the equivalent function in nla_channels.c */
switch (ale->type) {
@ -3619,6 +3784,9 @@ static int animchannels_mouseclick_invoke(bContext *C, wmOperator *op, const wmE
if (RNA_boolean_get(op->ptr, "extend")) {
selectmode = SELECT_INVERT;
}
else if (RNA_boolean_get(op->ptr, "extend_range")) {
selectmode = SELECT_EXTEND_RANGE;
}
else if (RNA_boolean_get(op->ptr, "children_only")) {
/* this is a bit of a special case for ActionGroups only...
* should it be removed or extended to all instead? */
@ -3672,6 +3840,13 @@ static void ANIM_OT_channels_click(wmOperatorType *ot)
prop = RNA_def_boolean(ot->srna, "extend", false, "Extend Select", "");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
prop = RNA_def_boolean(ot->srna,
"extend_range",
false,
"Extend Range",
"Selection of active channel to clicked channel");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);
/* Key-map: Enable with `Ctrl-Shift`. */
prop = RNA_def_boolean(ot->srna, "children_only", false, "Select Children Only", "");
RNA_def_property_flag(prop, PROP_SKIP_SAVE);

@ -92,7 +92,7 @@ void ED_pose_bone_select_tag_update(Object *ob)
DEG_id_tag_update(&arm->id, ID_RECALC_SELECT);
}
void ED_pose_bone_select(Object *ob, bPoseChannel *pchan, bool select)
void ED_pose_bone_select(Object *ob, bPoseChannel *pchan, bool select, bool change_active)
{
bArmature *arm;
@ -109,11 +109,15 @@ void ED_pose_bone_select(Object *ob, bPoseChannel *pchan, bool select)
/* change selection state - activate too if selected */
if (select) {
pchan->bone->flag |= BONE_SELECTED;
arm->act_bone = pchan->bone;
if (change_active) {
arm->act_bone = pchan->bone;
}
}
else {
pchan->bone->flag &= ~BONE_SELECTED;
arm->act_bone = NULL;
if (change_active) {
arm->act_bone = NULL;
}
}
/* TODO: select and activate corresponding vgroup? */

@ -530,6 +530,8 @@ typedef enum eAnimChannels_SetFlag {
ACHANNEL_SETFLAG_INVERT = 2,
/** some on -> all off / all on */
ACHANNEL_SETFLAG_TOGGLE = 3,
/** turn off, keep active flag **/
ACHANNEL_SETFLAG_EXTEND_RANGE = 4,
} eAnimChannels_SetFlag;
/* types of settings for AnimChannels */
@ -698,6 +700,11 @@ void ANIM_set_active_channel(bAnimContext *ac,
void *channel_data,
eAnim_ChannelType channel_type);
/**
* Return whether channel is active.
*/
bool ANIM_is_active_channel(bAnimListElem *ale);
/**
* Delete the F-Curve from the given AnimData block (if possible),
* as appropriate according to animation context.

@ -356,8 +356,13 @@ bool ED_pose_deselect_all(struct Object *ob, int select_mode, bool ignore_visibi
void ED_pose_bone_select_tag_update(struct Object *ob);
/**
* Utility method for changing the selection status of a bone.
* change_active determines whether to change the active bone of the armature when selecting pose
* channels. It is false during range selection otherwise true.
*/
void ED_pose_bone_select(struct Object *ob, struct bPoseChannel *pchan, bool select);
void ED_pose_bone_select(struct Object *ob,
struct bPoseChannel *pchan,
bool select,
bool change_active);
/* meshlaplacian.cc */

@ -58,6 +58,7 @@ typedef enum eEditKeyframes_Select {
SELECT_SUBTRACT = (1 << 2),
/* flip ok status of keyframes based on key status */
SELECT_INVERT = (1 << 3),
SELECT_EXTEND_RANGE = (1 << 4),
} eEditKeyframes_Select;
/* "selection map" building modes */

@ -345,7 +345,7 @@ bool ED_object_jump_to_bone(bContext *C,
/* Select it. */
ED_pose_deselect_all(ob, SEL_DESELECT, true);
ED_pose_bone_select(ob, pchan, true);
ED_pose_bone_select(ob, pchan, true, true);
arm->act_bone = pchan->bone;