text: /*
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software Foundation,
   * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
   *
   * The Original Code is Copyright (C) 2009 Blender Foundation, Joshua Leung
   * All rights reserved.
   */
  
  /** \file
   * \ingroup edanimation
   */
  
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  
  #include "MEM_guardedalloc.h"
  
  #include "BLI_blenlib.h"
  #include "BLI_utildefines.h"
  #include "BLI_listbase.h"
  
  #include "DNA_anim_types.h"
  #include "DNA_object_types.h"
  #include "DNA_scene_types.h"
  #include "DNA_key_types.h"
  #include "DNA_gpencil_types.h"
  #include "DNA_mask_types.h"
  
  #include "RNA_access.h"
  #include "RNA_define.h"
  
  #include "BKE_animsys.h"
  #include "BKE_action.h"
  #include "BKE_fcurve.h"
  #include "BKE_gpencil.h"
  #include "BKE_context.h"
  #include "BKE_library.h"
  #include "BKE_mask.h"
  #include "BKE_global.h"
  #include "BKE_scene.h"
  
  #include "DEG_depsgraph.h"
  #include "DEG_depsgraph_build.h"
  
  #include "UI_view2d.h"
  #include "UI_interface.h"
  
  #include "ED_anim_api.h"
  #include "ED_armature.h"
  #include "ED_keyframes_edit.h"  // XXX move the select modes out of there!
  #include "ED_object.h"
  #include "ED_screen.h"
  #include "ED_select_utils.h"
  
  #include "WM_api.h"
  #include "WM_types.h"
  
  /* ************************************************************************** */
  /* CHANNELS API - Exposed API */
  
  /* -------------------------- Selection ------------------------------------- */
  
  /* Set the given animation-channel as the active one for the active context */
  // TODO: extend for animdata types...
  void ANIM_set_active_channel(bAnimContext *ac,
                               void *data,
                               eAnimCont_Types datatype,
                               eAnimFilter_Flags filter,
                               void *channel_data,
                               eAnim_ChannelType channel_type)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
  
    /* try to build list of filtered items */
    ANIM_animdata_filter(ac, &anim_data, filter, data, datatype);
    if (BLI_listbase_is_empty(&anim_data)) {
      return;
    }
  
    /* only clear the 'active' flag for the channels of the same type */
    for (ale = anim_data.first; ale; ale = ale->next) {
      /* skip if types don't match */
      if (channel_type != ale->type) {
        continue;
      }
  
      /* flag to set depends on type */
      switch (ale->type) {
        case ANIMTYPE_GROUP: {
          bActionGroup *agrp = (bActionGroup *)ale->data;
  
          ACHANNEL_SET_FLAG(agrp, ACHANNEL_SETFLAG_CLEAR, AGRP_ACTIVE);
          break;
        }
        case ANIMTYPE_FCURVE:
        case ANIMTYPE_NLACURVE: {
          FCurve *fcu = (FCurve *)ale->data;
  
          ACHANNEL_SET_FLAG(fcu, ACHANNEL_SETFLAG_CLEAR, FCURVE_ACTIVE);
          break;
        }
        case ANIMTYPE_NLATRACK: {
          NlaTrack *nlt = (NlaTrack *)ale->data;
  
          ACHANNEL_SET_FLAG(nlt, ACHANNEL_SETFLAG_CLEAR, NLATRACK_ACTIVE);
          break;
        }
        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_DSTEX:
        case ANIMTYPE_DSLAT:
        case ANIMTYPE_DSLINESTYLE:
        case ANIMTYPE_DSSPK:
        case ANIMTYPE_DSGPENCIL:
        case ANIMTYPE_DSMCLIP: {
          /* need to verify that this data is valid for now */
          if (ale->adt) {
            ACHANNEL_SET_FLAG(ale->adt, ACHANNEL_SETFLAG_CLEAR, ADT_UI_ACTIVE);
          }
          break;
        }
        case ANIMTYPE_GPLAYER: {
          bGPDlayer *gpl = (bGPDlayer *)ale->data;
  
          ACHANNEL_SET_FLAG(gpl, ACHANNEL_SETFLAG_CLEAR, GP_LAYER_ACTIVE);
          break;
        }
      }
    }
  
    /* set active flag */
    if (channel_data) {
      switch (channel_type) {
        case ANIMTYPE_GROUP: {
          bActionGroup *agrp = (bActionGroup *)channel_data;
          agrp->flag |= AGRP_ACTIVE;
          break;
        }
        case ANIMTYPE_FCURVE:
        case ANIMTYPE_NLACURVE: {
          FCurve *fcu = (FCurve *)channel_data;
          fcu->flag |= FCURVE_ACTIVE;
          break;
        }
        case ANIMTYPE_NLATRACK: {
          NlaTrack *nlt = (NlaTrack *)channel_data;
          nlt->flag |= NLATRACK_ACTIVE;
          break;
        }
        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_DSLAT:
        case ANIMTYPE_DSLINESTYLE:
        case ANIMTYPE_DSSPK:
        case ANIMTYPE_DSNTREE:
        case ANIMTYPE_DSTEX:
        case ANIMTYPE_DSGPENCIL:
        case ANIMTYPE_DSMCLIP: {
          /* need to verify that this data is valid for now */
          if (ale && ale->adt) {
            ale->adt->flag |= ADT_UI_ACTIVE;
          }
          break;
        }
  
        case ANIMTYPE_GPLAYER: {
          bGPDlayer *gpl = (bGPDlayer *)channel_data;
          gpl->flag |= GP_LAYER_ACTIVE;
          break;
        }
  
        /* unhandled currently, but may be interesting */
        case ANIMTYPE_MASKLAYER:
        case ANIMTYPE_SHAPEKEY:
        case ANIMTYPE_NLAACTION:
          break;
  
        /* other types */
        default:
          break;
      }
    }
  
    /* clean up */
    ANIM_animdata_freelist(&anim_data);
  }
  
  static void select_pchan_for_action_group(bAnimContext *ac, bActionGroup *agrp, bAnimListElem *ale)
  {
    /* Armatures-Specific Feature:
     * See mouse_anim_channels() -> ANIMTYPE_GROUP case for more details (T38737)
     */
    if ((ac->ads->filterflag & ADS_FILTER_ONLYSEL) == 0) {
      if ((ale->id) && (GS(ale->id->name) == ID_OB)) {
        Object *ob = (Object *)ale->id;
        if (ob->type == OB_ARMATURE) {
          /* Assume for now that any group with corresponding name is what we want
           * (i.e. for an armature whose location is animated, things would break
           * if the user were to add a bone named "Location").
           *
           * 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);
          }
          else {
            ED_pose_bone_select(ob, pchan, false);
          }
        }
      }
    }
  }
  
  /* Deselect all animation channels
   * - data: pointer to datatype, as contained in bAnimContext
   * - datatype: the type of data that 'data' represents (eAnimCont_Types)
   * - test: check if deselecting instead of selecting
   * - sel: eAnimChannels_SetFlag;
   */
  void ANIM_deselect_anim_channels(
      bAnimContext *ac, void *data, eAnimCont_Types datatype, bool test, eAnimChannels_SetFlag sel)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* filter data */
    /* NOTE: no list visible, otherwise, we get dangling */
    filter = ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_CHANNELS;
    ANIM_animdata_filter(ac, &anim_data, filter, data, datatype);
  
    /* See if we should be selecting or deselecting */
    if (test) {
      for (ale = anim_data.first; ale; ale = ale->next) {
        if (sel == 0) {
          break;
        }
  
        switch (ale->type) {
          case ANIMTYPE_SCENE:
            if (ale->flag & SCE_DS_SELECTED) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          case ANIMTYPE_OBJECT:
  #if 0 /* for now, do not take object selection into account, since it gets too annoying */
            if (ale->flag & SELECT) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
  #endif
            break;
          case ANIMTYPE_GROUP:
            if (ale->flag & AGRP_SELECTED) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          case ANIMTYPE_FCURVE:
          case ANIMTYPE_NLACURVE:
            if (ale->flag & FCURVE_SELECTED) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          case ANIMTYPE_SHAPEKEY:
            if (ale->flag & KEYBLOCK_SEL) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          case ANIMTYPE_NLATRACK:
            if (ale->flag & NLATRACK_SELECTED) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
  
          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: {
            if ((ale->adt) && (ale->adt->flag & ADT_UI_SELECTED)) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          }
          case ANIMTYPE_GPLAYER:
            if (ale->flag & GP_LAYER_SELECT) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
          case ANIMTYPE_MASKLAYER:
            if (ale->flag & MASK_LAYERFLAG_SELECT) {
              sel = ACHANNEL_SETFLAG_CLEAR;
            }
            break;
        }
      }
    }
  
    /* Now set the flags */
    for (ale = anim_data.first; ale; ale = ale->next) {
      switch (ale->type) {
        case ANIMTYPE_SCENE: {
          Scene *scene = (Scene *)ale->data;
  
          ACHANNEL_SET_FLAG(scene, sel, SCE_DS_SELECTED);
  
          if (scene->adt) {
            ACHANNEL_SET_FLAG(scene, sel, ADT_UI_SELECTED);
          }
          break;
        }
        case ANIMTYPE_OBJECT: {
  #if 0 /* for now, do not take object selection into account, since it gets too annoying */
          Base *base = (Base *)ale->data;
          Object *ob = base->object;
  
          ACHANNEL_SET_FLAG(base, sel, SELECT);
          ACHANNEL_SET_FLAG(ob, sel, SELECT);
  
          if (ob->adt) {
            ACHANNEL_SET_FLAG(ob, sel, ADT_UI_SELECTED);
          }
  #endif
          break;
        }
        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;
          break;
        }
        case ANIMTYPE_FCURVE:
        case ANIMTYPE_NLACURVE: {
          FCurve *fcu = (FCurve *)ale->data;
  
          ACHANNEL_SET_FLAG(fcu, sel, FCURVE_SELECTED);
          fcu->flag &= ~FCURVE_ACTIVE;
          break;
        }
        case ANIMTYPE_SHAPEKEY: {
          KeyBlock *kb = (KeyBlock *)ale->data;
  
          ACHANNEL_SET_FLAG(kb, sel, KEYBLOCK_SEL);
          break;
        }
        case ANIMTYPE_NLATRACK: {
          NlaTrack *nlt = (NlaTrack *)ale->data;
  
          ACHANNEL_SET_FLAG(nlt, sel, NLATRACK_SELECTED);
          nlt->flag &= ~NLATRACK_ACTIVE;
          break;
        }
        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: {
          /* 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;
          }
          break;
        }
        case ANIMTYPE_GPLAYER: {
          bGPDlayer *gpl = (bGPDlayer *)ale->data;
  
          ACHANNEL_SET_FLAG(gpl, sel, GP_LAYER_SELECT);
          break;
        }
        case ANIMTYPE_MASKLAYER: {
          MaskLayer *masklay = (MaskLayer *)ale->data;
  
          ACHANNEL_SET_FLAG(masklay, sel, MASK_LAYERFLAG_SELECT);
          break;
        }
      }
    }
  
    /* Cleanup */
    ANIM_animdata_freelist(&anim_data);
  }
  
  /* ---------------------------- Graph Editor ------------------------------------- */
  
  /* Flush visibility (for Graph Editor) changes up/down hierarchy for changes in the given setting
   * - anim_data: list of the all the anim channels that can be chosen
   *   -> filtered using ANIMFILTER_CHANNELS only, since if we took VISIBLE too,
   *      then the channels under closed expanders get ignored...
   * - ale_setting: the anim channel (not in the anim_data list directly, though occurring there)
   *   with the new state of the setting that we want flushed up/down the hierarchy
   * - setting: type of setting to set
   * - on: whether the visibility setting has been enabled or disabled
   */
  void ANIM_flush_setting_anim_channels(bAnimContext *ac,
                                        ListBase *anim_data,
                                        bAnimListElem *ale_setting,
                                        eAnimChannel_Settings setting,
                                        eAnimChannels_SetFlag mode)
  {
    bAnimListElem *ale, *match = NULL;
    int prevLevel = 0, matchLevel = 0;
  
    /* sanity check */
    if (ELEM(NULL, anim_data, anim_data->first)) {
      return;
    }
  
    if (setting == ACHANNEL_SETTING_ALWAYS_VISIBLE) {
      return;
    }
  
    /* find the channel that got changed */
    for (ale = anim_data->first; ale; ale = ale->next) {
      /* compare data, and type as main way of identifying the channel */
      if ((ale->data == ale_setting->data) && (ale->type == ale_setting->type)) {
        /* We also have to check the ID, this is assigned to,
         * since a block may have multiple users. */
        /* TODO: is the owner-data more revealing? */
        if (ale->id == ale_setting->id) {
          match = ale;
          break;
        }
      }
    }
    if (match == NULL) {
      printf("ERROR: no channel matching the one changed was found\n");
      return;
    }
    else {
      const bAnimChannelType *acf = ANIM_channel_get_typeinfo(ale_setting);
  
      if (acf == NULL) {
        printf("ERROR: no channel info for the changed channel\n");
        return;
      }
  
      /* get the level of the channel that was affected
       *   - we define the level as simply being the offset for the start of the channel
       */
      matchLevel = (acf->get_offset) ? acf->get_offset(ac, ale_setting) : 0;
      prevLevel = matchLevel;
    }
  
    /* flush up?
     *
     * For Visibility:
     * - only flush up if the current state is now enabled (positive 'on' state is default)
     *   (otherwise, it's too much work to force the parents to be inactive too)
     *
     * For everything else:
     * - only flush up if the current state is now disabled (negative 'off' state is default)
     *   (otherwise, it's too much work to force the parents to be active too)
     */
    if (((setting == ACHANNEL_SETTING_VISIBLE) && (mode != ACHANNEL_SETFLAG_CLEAR)) ||
        ((setting != ACHANNEL_SETTING_VISIBLE) && (mode == ACHANNEL_SETFLAG_CLEAR))) {
      /* Go backwards in the list, until the highest-ranking element
       * (by indention has been covered). */
      for (ale = match->prev; ale; ale = ale->prev) {
        const bAnimChannelType *acf = ANIM_channel_get_typeinfo(ale);
        int level;
  
        /* if no channel info was found, skip, since this type might not have any useful info */
        if (acf == NULL) {
          continue;
        }
  
        /* get the level of the current channel traversed
         *   - we define the level as simply being the offset for the start of the channel
         */
        level = (acf->get_offset) ? acf->get_offset(ac, ale) : 0;
  
        /* if the level is 'less than' (i.e. more important) the level we're matching
         * but also 'less than' the level just tried (i.e. only the 1st group above grouped F-Curves,
         * when toggling visibility of F-Curves, gets flushed, which should happen if we don't let
         * prevLevel get updated below once the first 1st group is found).
         */
        if (level < prevLevel) {
          /* flush the new status... */
          ANIM_channel_setting_set(ac, ale, setting, mode);
  
          /* store this level as the 'old' level now */
          prevLevel = level;
        }
        /* if the level is 'greater than' (i.e. less important) than the previous level... */
        else if (level > prevLevel) {
          /* if previous level was a base-level (i.e. 0 offset / root of one hierarchy),
           * stop here
           */
          if (prevLevel == 0) {
            break;
            /* otherwise, this level weaves into another sibling hierarchy to the previous one just
             * finished, so skip until we get to the parent of this level
             */
          }
          else {
            continue;
          }
        }
      }
    }
  
    /* flush down (always) */
    {
      /* go forwards in the list, until the lowest-ranking element (by indention has been covered) */
      for (ale = match->next; ale; ale = ale->next) {
        const bAnimChannelType *acf = ANIM_channel_get_typeinfo(ale);
        int level;
  
        /* if no channel info was found, skip, since this type might not have any useful info */
        if (acf == NULL) {
          continue;
        }
  
        /* get the level of the current channel traversed
         *   - we define the level as simply being the offset for the start of the channel
         */
        level = (acf->get_offset) ? acf->get_offset(ac, ale) : 0;
  
        /* if the level is 'greater than' (i.e. less important) the channel that was changed,
         * flush the new status...
         */
        if (level > matchLevel) {
          ANIM_channel_setting_set(ac, ale, setting, mode);
          /* however, if the level is 'less than or equal to' the channel that was changed,
           * (i.e. the current channel is as important if not more important than the changed
           * channel) then we should stop, since we've found the last one of the children we should
           * flush
           */
        }
        else {
          break;
        }
  
        /* store this level as the 'old' level now */
        // prevLevel = level; // XXX: prevLevel is unused
      }
    }
  }
  
  /* -------------------------- F-Curves ------------------------------------- */
  
  /* Delete the given F-Curve from its AnimData block */
  void ANIM_fcurve_delete_from_animdata(bAnimContext *ac, AnimData *adt, FCurve *fcu)
  {
    /* - if no AnimData, we've got nowhere to remove the F-Curve from
     *   (this doesn't guarantee that the F-Curve is in there, but at least we tried
     * - if no F-Curve, there is nothing to remove
     */
    if (ELEM(NULL, adt, fcu)) {
      return;
    }
  
    /* remove from whatever list it came from
     * - Action Group
     * - Action
     * - Drivers
     * - TODO... some others?
     */
    if ((ac) && (ac->datatype == ANIMCONT_DRIVERS)) {
      /* driver F-Curve */
      BLI_remlink(&adt->drivers, fcu);
    }
    else if (adt->action) {
      bAction *act = adt->action;
  
      /* remove from group or action, whichever one "owns" the F-Curve */
      if (fcu->grp) {
        bActionGroup *agrp = fcu->grp;
  
        /* remove F-Curve from group+action */
        action_groups_remove_channel(act, fcu);
  
        /* if group has no more channels, remove it too,
         * otherwise can have many dangling groups [#33541]
         */
        if (BLI_listbase_is_empty(&agrp->channels)) {
          BLI_freelinkN(&act->groups, agrp);
        }
      }
      else {
        BLI_remlink(&act->curves, fcu);
      }
  
      /* if action has no more F-Curves as a result of this, unlink it from
       * AnimData if it did not come from a NLA Strip being tweaked.
       *
       * This is done so that we don't have dangling Object+Action entries in
       * channel list that are empty, and linger around long after the data they
       * are for has disappeared (and probably won't come back).
       */
      ANIM_remove_empty_action_from_animdata(adt);
    }
  
    /* free the F-Curve itself */
    free_fcurve(fcu);
  }
  
  /* If the action has no F-Curves, unlink it from AnimData if it did not
   * come from a NLA Strip being tweaked. */
  bool ANIM_remove_empty_action_from_animdata(struct AnimData *adt)
  {
    if (adt->action != NULL) {
      bAction *act = adt->action;
  
      if (BLI_listbase_is_empty(&act->curves) && (adt->flag & ADT_NLA_EDIT_ON) == 0) {
        id_us_min(&act->id);
        adt->action = NULL;
        return true;
      }
    }
  
    return false;
  }
  
  /* ************************************************************************** */
  /* OPERATORS */
  
  /* ****************** Operator Utilities ********************************** */
  
  /* poll callback for being in an Animation Editor channels list region */
  static bool animedit_poll_channels_active(bContext *C)
  {
    ScrArea *sa = CTX_wm_area(C);
  
    /* channels region test */
    /* TODO: could enhance with actually testing if channels region? */
    if (ELEM(NULL, sa, CTX_wm_region(C))) {
      return 0;
    }
    /* animation editor test */
    if (ELEM(sa->spacetype, SPACE_ACTION, SPACE_GRAPH, SPACE_NLA) == 0) {
      return 0;
    }
  
    return 1;
  }
  
  /* poll callback for Animation Editor channels list region + not in NLA-tweakmode for NLA */
  static bool animedit_poll_channels_nla_tweakmode_off(bContext *C)
  {
    ScrArea *sa = CTX_wm_area(C);
    Scene *scene = CTX_data_scene(C);
  
    /* channels region test */
    /* TODO: could enhance with actually testing if channels region? */
    if (ELEM(NULL, sa, CTX_wm_region(C))) {
      return 0;
    }
    /* animation editor test */
    if (ELEM(sa->spacetype, SPACE_ACTION, SPACE_GRAPH, SPACE_NLA) == 0) {
      return 0;
    }
  
    /* NLA TweakMode test */
    if (sa->spacetype == SPACE_NLA) {
      if ((scene == NULL) || (scene->flag & SCE_NLA_EDIT_ON)) {
        return 0;
      }
    }
  
    return 1;
  }
  
  /* ****************** Rearrange Channels Operator ******************* */
  
  /* constants for channel rearranging */
  /* WARNING: don't change existing ones without modifying rearrange func accordingly */
  typedef enum eRearrangeAnimChan_Mode {
    REARRANGE_ANIMCHAN_TOP = -2,
    REARRANGE_ANIMCHAN_UP = -1,
    REARRANGE_ANIMCHAN_DOWN = 1,
    REARRANGE_ANIMCHAN_BOTTOM = 2,
  } eRearrangeAnimChan_Mode;
  
  /* defines for rearranging channels */
  static const EnumPropertyItem prop_animchannel_rearrange_types[] = {
      {REARRANGE_ANIMCHAN_TOP, "TOP", 0, "To Top", ""},
      {REARRANGE_ANIMCHAN_UP, "UP", 0, "Up", ""},
      {REARRANGE_ANIMCHAN_DOWN, "DOWN", 0, "Down", ""},
      {REARRANGE_ANIMCHAN_BOTTOM, "BOTTOM", 0, "To Bottom", ""},
      {0, NULL, 0, NULL, NULL},
  };
  
  /* Reordering "Islands" Defines ----------------------------------- */
  
  /* Island definition - just a listbase container */
  typedef struct tReorderChannelIsland {
    struct tReorderChannelIsland *next, *prev;
  
    ListBase channels; /* channels within this region with the same state */
    int flag;          /* eReorderIslandFlag */
  } tReorderChannelIsland;
  
  /* flags for channel reordering islands */
  typedef enum eReorderIslandFlag {
    REORDER_ISLAND_SELECTED = (1 << 0),    /* island is selected */
    REORDER_ISLAND_UNTOUCHABLE = (1 << 1), /* island should be ignored */
    REORDER_ISLAND_MOVED = (1 << 2),       /* island has already been moved */
    REORDER_ISLAND_HIDDEN = (1 << 3),      /* island is not visible */
  } eReorderIslandFlag;
  
  /* Rearrange Methods --------------------------------------------- */
  
  static bool rearrange_island_ok(tReorderChannelIsland *island)
  {
    /* island must not be untouchable */
    if (island->flag & REORDER_ISLAND_UNTOUCHABLE) {
      return 0;
    }
  
    /* island should be selected to be moved */
    return (island->flag & REORDER_ISLAND_SELECTED) && !(island->flag & REORDER_ISLAND_MOVED);
  }
  
  /* ............................. */
  
  static bool rearrange_island_top(ListBase *list, tReorderChannelIsland *island)
  {
    if (rearrange_island_ok(island)) {
      /* remove from current position */
      BLI_remlink(list, island);
  
      /* make it first element */
      BLI_insertlinkbefore(list, list->first, island);
  
      return 1;
    }
  
    return 0;
  }
  
  static bool rearrange_island_up(ListBase *list, tReorderChannelIsland *island)
  {
    if (rearrange_island_ok(island)) {
      /* moving up = moving before the previous island, otherwise we're in the same place */
      tReorderChannelIsland *prev = island->prev;
  
      /* Skip hidden islands! */
      while (prev && prev->flag & REORDER_ISLAND_HIDDEN) {
        prev = prev->prev;
      }
  
      if (prev) {
        /* remove from current position */
        BLI_remlink(list, island);
  
        /* push it up */
        BLI_insertlinkbefore(list, prev, island);
  
        return 1;
      }
    }
  
    return 0;
  }
  
  static bool rearrange_island_down(ListBase *list, tReorderChannelIsland *island)
  {
    if (rearrange_island_ok(island)) {
      /* moving down = moving after the next island, otherwise we're in the same place */
      tReorderChannelIsland *next = island->next;
  
      /* Skip hidden islands! */
      while (next && next->flag & REORDER_ISLAND_HIDDEN) {
        next = next->next;
      }
  
      if (next) {
        /* can only move past if next is not untouchable (i.e. nothing can go after it) */
        if ((next->flag & REORDER_ISLAND_UNTOUCHABLE) == 0) {
          /* remove from current position */
          BLI_remlink(list, island);
  
          /* push it down */
          BLI_insertlinkafter(list, next, island);
  
          return true;
        }
      }
      /* else: no next channel, so we're at the bottom already, so can't move */
    }
  
    return false;
  }
  
  static bool rearrange_island_bottom(ListBase *list, tReorderChannelIsland *island)
  {
    if (rearrange_island_ok(island)) {
      tReorderChannelIsland *last = list->last;
  
      /* remove island from current position */
      BLI_remlink(list, island);
  
      /* add before or after the last channel? */
      if ((last->flag & REORDER_ISLAND_UNTOUCHABLE) == 0) {
        /* can add after it */
        BLI_addtail(list, island);
      }
      else {
        /* can at most go just before it, since last cannot be moved */
        BLI_insertlinkbefore(list, last, island);
      }
  
      return true;
    }
  
    return false;
  }
  
  /* ............................. */
  
  /**
   * typedef for channel rearranging function
   *
   * \param list: List of tReorderChannelIsland's that channels belong to
   * \param island: Island to be moved
   * \return Whether operation was a success
   */
  typedef bool (*AnimChanRearrangeFp)(ListBase *list, tReorderChannelIsland *island);
  
  /* get rearranging function, given 'rearrange' mode */
  static AnimChanRearrangeFp rearrange_get_mode_func(eRearrangeAnimChan_Mode mode)
  {
    switch (mode) {
      case REARRANGE_ANIMCHAN_TOP:
        return rearrange_island_top;
      case REARRANGE_ANIMCHAN_UP:
        return rearrange_island_up;
      case REARRANGE_ANIMCHAN_DOWN:
        return rearrange_island_down;
      case REARRANGE_ANIMCHAN_BOTTOM:
        return rearrange_island_bottom;
      default:
        return NULL;
    }
  }
  
  /* Rearrange Islands Generics ------------------------------------- */
  
  /* add channel into list of islands */
  static void rearrange_animchannel_add_to_islands(ListBase *islands,
                                                   ListBase *srcList,
                                                   Link *channel,
                                                   eAnim_ChannelType type,
                                                   const bool is_hidden)
  {
    /* always try to add to last island if possible */
    tReorderChannelIsland *island = islands->last;
    bool is_sel = false, is_untouchable = false;
  
    /* get flags - selected and untouchable from the channel */
    switch (type) {
      case ANIMTYPE_GROUP: {
        bActionGroup *agrp = (bActionGroup *)channel;
  
        is_sel = SEL_AGRP(agrp);
        is_untouchable = (agrp->flag & AGRP_TEMP) != 0;
        break;
      }
      case ANIMTYPE_FCURVE:
      case ANIMTYPE_NLACURVE: {
        FCurve *fcu = (FCurve *)channel;
  
        is_sel = SEL_FCU(fcu);
        break;
      }
      case ANIMTYPE_NLATRACK: {
        NlaTrack *nlt = (NlaTrack *)channel;
  
        is_sel = SEL_NLT(nlt);
        break;
      }
      case ANIMTYPE_GPLAYER: {
        bGPDlayer *gpl = (bGPDlayer *)channel;
  
        is_sel = SEL_GPL(gpl);
        break;
      }
      default:
        printf(
            "rearrange_animchannel_add_to_islands(): don't know how to handle channels of type %u\n",
            type);
        return;
    }
  
    /* do we need to add to a new island? */
    if (/* 1) no islands yet */
        (island == NULL) ||
        /* 2) unselected islands have single channels only - to allow up/down movement */
        ((island->flag & REORDER_ISLAND_SELECTED) == 0) ||
        /* 3) if channel is unselected, stop existing island
         * (it was either wrong sel status, or full already) */
        (is_sel == 0) ||
        /* 4) hidden status changes */
        ((island->flag & REORDER_ISLAND_HIDDEN) != is_hidden)) {
      /* create a new island now */
      island = MEM_callocN(sizeof(tReorderChannelIsland), "tReorderChannelIsland");
      BLI_addtail(islands, island);
  
      if (is_sel) {
        island->flag |= REORDER_ISLAND_SELECTED;
      }
      if (is_untouchable) {
        island->flag |= REORDER_ISLAND_UNTOUCHABLE;
      }
      if (is_hidden) {
        island->flag |= REORDER_ISLAND_HIDDEN;
      }
    }
  
    /* add channel to island - need to remove it from its existing list first though */
    BLI_remlink(srcList, channel);
    BLI_addtail(&island->channels, channel);
  }
  
  /* flatten islands out into a single list again */
  static void rearrange_animchannel_flatten_islands(ListBase *islands, ListBase *srcList)
  {
    tReorderChannelIsland *island, *isn = NULL;
  
    /* make sure srcList is empty now */
    BLI_assert(BLI_listbase_is_empty(srcList));
  
    /* go through merging islands */
    for (island = islands->first; island; island = isn) {
      isn = island->next;
  
      /* merge island channels back to main list, then delete the island */
      BLI_movelisttolist(srcList, &island->channels);
      BLI_freelinkN(islands, island);
    }
  }
  
  /* ............................. */
  
  /* get a list of all bAnimListElem's of a certain type which are currently visible */
  static void rearrange_animchannels_filter_visible(ListBase *anim_data_visible,
                                                    bAnimContext *ac,
                                                    eAnim_ChannelType type)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale, *ale_next;
    int filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS);
  
    /* get all visible channels */
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    /* now, only keep the ones that are of the types we are interested in */
    for (ale = anim_data.first; ale; ale = ale_next) {
      ale_next = ale->next;
  
      if (ale->type != type) {
        BLI_freelinkN(&anim_data, ale);
      }
    }
  
    /* return cleaned up list */
    *anim_data_visible = anim_data;
  }
  
  /* performing rearranging of channels using islands */
  static bool rearrange_animchannel_islands(ListBase *list,
                                            AnimChanRearrangeFp rearrange_func,
                                            eRearrangeAnimChan_Mode mode,
                                            eAnim_ChannelType type,
                                            ListBase *anim_data_visible)
  {
    ListBase islands = {NULL, NULL};
    Link *channel, *chanNext = NULL;
    bool done = false;
  
    /* don't waste effort on an empty list */
    if (BLI_listbase_is_empty(list)) {
      return 0;
    }
  
    /* group channels into islands */
    for (channel = list->first; channel; channel = chanNext) {
      /* find out whether this channel is present in anim_data_visible or not! */
      const bool is_hidden =
          (BLI_findptr(anim_data_visible, channel, offsetof(bAnimListElem, data)) == NULL);
      chanNext = channel->next;
      rearrange_animchannel_add_to_islands(&islands, list, channel, type, is_hidden);
    }
  
    /* Perform moving of selected islands now, but only if there is more than one of them
     * so that something will happen:
     *
     * - Scanning of the list is performed in the opposite direction
     *   to the direction we're moving things,
     *   so that we shouldn't need to encounter items we've moved already.
     */
    if (islands.first != islands.last) {
      tReorderChannelIsland *first = (mode > 0) ? islands.last : islands.first;
      tReorderChannelIsland *island, *isn = NULL;
  
      for (island = first; island; island = isn) {
        isn = (mode > 0) ? island->prev : island->next;
  
        /* perform rearranging */
        if (rearrange_func(&islands, island)) {
          island->flag |= REORDER_ISLAND_MOVED;
          done = true;
        }
      }
    }
  
    /* ungroup islands */
    rearrange_animchannel_flatten_islands(&islands, list);
  
    /* did we do anything? */
    return done;
  }
  
  /* NLA Specific Stuff ----------------------------------------------------- */
  
  /* Change the order NLA Tracks within NLA Stack
   * ! NLA tracks are displayed in opposite order, so directions need care
   * mode: REARRANGE_ANIMCHAN_*
   */
  static void rearrange_nla_channels(bAnimContext *ac, AnimData *adt, eRearrangeAnimChan_Mode mode)
  {
    AnimChanRearrangeFp rearrange_func;
    ListBase anim_data_visible = {NULL, NULL};
  
    /* hack: invert mode so that functions will work in right order */
    mode *= -1;
  
    /* get rearranging function */
    rearrange_func = rearrange_get_mode_func(mode);
    if (rearrange_func == NULL) {
      return;
    }
  
    /* Filter visible data. */
    rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_NLATRACK);
  
    /* perform rearranging on tracks list */
    rearrange_animchannel_islands(
        &adt->nla_tracks, rearrange_func, mode, ANIMTYPE_NLATRACK, &anim_data_visible);
  
    /* free temp data */
    BLI_freelistN(&anim_data_visible);
  }
  
  /* Drivers Specific Stuff ------------------------------------------------- */
  
  /* Change the order drivers within AnimData block
   * mode: REARRANGE_ANIMCHAN_*
   */
  static void rearrange_driver_channels(bAnimContext *ac,
                                        AnimData *adt,
                                        eRearrangeAnimChan_Mode mode)
  {
    /* get rearranging function */
    AnimChanRearrangeFp rearrange_func = rearrange_get_mode_func(mode);
    ListBase anim_data_visible = {NULL, NULL};
  
    if (rearrange_func == NULL) {
      return;
    }
  
    /* only consider drivers if they're accessible */
    if (EXPANDED_DRVD(adt) == 0) {
      return;
    }
  
    /* Filter visible data. */
    rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_FCURVE);
  
    /* perform rearranging on drivers list (drivers are really just F-Curves) */
    rearrange_animchannel_islands(
        &adt->drivers, rearrange_func, mode, ANIMTYPE_FCURVE, &anim_data_visible);
  
    /* free temp data */
    BLI_freelistN(&anim_data_visible);
  }
  
  /* Action Specific Stuff ------------------------------------------------- */
  
  /* make sure all action-channels belong to a group (and clear action's list) */
  static void split_groups_action_temp(bAction *act, bActionGroup *tgrp)
  {
    bActionGroup *agrp;
    FCurve *fcu;
  
    if (act == NULL) {
      return;
    }
  
    /* Separate F-Curves into lists per group */
    for (agrp = act->groups.first; agrp; agrp = agrp->next) {
      if (agrp->channels.first) {
        fcu = agrp->channels.last;
        act->curves.first = fcu->next;
  
        fcu = agrp->channels.first;
        fcu->prev = NULL;
  
        fcu = agrp->channels.last;
        fcu->next = NULL;
      }
    }
  
    /* Initialize memory for temp-group */
    memset(tgrp, 0, sizeof(bActionGroup));
    tgrp->flag |= (AGRP_EXPANDED | AGRP_TEMP);
    BLI_strncpy(tgrp->name, "#TempGroup", sizeof(tgrp->name));
  
    /* Move any action-channels not already moved, to the temp group */
    if (act->curves.first) {
      /* start of list */
      fcu = act->curves.first;
      fcu->prev = NULL;
      tgrp->channels.first = fcu;
      act->curves.first = NULL;
  
      /* end of list */
      fcu = act->curves.last;
      fcu->next = NULL;
      tgrp->channels.last = fcu;
      act->curves.last = NULL;
  
      /* ensure that all of these get their group set to this temp group
       * (so that visibility filtering works)
       */
      for (fcu = tgrp->channels.first; fcu; fcu = fcu->next) {
        fcu->grp = tgrp;
      }
    }
  
    /* Add temp-group to list */
    BLI_addtail(&act->groups, tgrp);
  }
  
  /* link lists of channels that groups have */
  static void join_groups_action_temp(bAction *act)
  {
    bActionGroup *agrp;
  
    for (agrp = act->groups.first; agrp; agrp = agrp->next) {
      ListBase tempGroup;
  
      /* add list of channels to action's channels */
      tempGroup = agrp->channels;
      BLI_movelisttolist(&act->curves, &agrp->channels);
      agrp->channels = tempGroup;
  
      /* clear moved flag */
      agrp->flag &= ~AGRP_MOVED;
  
      /* if group was temporary one:
       * - unassign all FCurves which were temporarily added to it
       * - remove from list (but don't free as it's on the stack!)
       */
      if (agrp->flag & AGRP_TEMP) {
        FCurve *fcu;
  
        for (fcu = agrp->channels.first; fcu; fcu = fcu->next) {
          fcu->grp = NULL;
        }
  
        BLI_remlink(&act->groups, agrp);
        break;
      }
    }
  }
  
  /* Change the order of anim-channels within action
   * mode: REARRANGE_ANIMCHAN_*
   */
  static void rearrange_action_channels(bAnimContext *ac, bAction *act, eRearrangeAnimChan_Mode mode)
  {
    bActionGroup tgrp;
    ListBase anim_data_visible = {NULL, NULL};
    bool do_channels;
  
    /* get rearranging function */
    AnimChanRearrangeFp rearrange_func = rearrange_get_mode_func(mode);
  
    if (rearrange_func == NULL) {
      return;
    }
  
    /* make sure we're only operating with groups (vs a mixture of groups+curves) */
    split_groups_action_temp(act, &tgrp);
  
    /* Filter visible data. */
    rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_GROUP);
  
    /* Rearrange groups first:
     * - The group's channels will only get considered
     *   if nothing happened when rearranging the groups
     *   i.e. the rearrange function returned 0.
     */
    do_channels = (rearrange_animchannel_islands(
                       &act->groups, rearrange_func, mode, ANIMTYPE_GROUP, &anim_data_visible) == 0);
  
    /* free temp data */
    BLI_freelistN(&anim_data_visible);
  
    if (do_channels) {
      bActionGroup *agrp;
  
      /* Filter visible data. */
      rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_FCURVE);
  
      for (agrp = act->groups.first; agrp; agrp = agrp->next) {
        /* only consider F-Curves if they're visible (group expanded) */
        if (EXPANDED_AGRP(ac, agrp)) {
          rearrange_animchannel_islands(
              &agrp->channels, rearrange_func, mode, ANIMTYPE_FCURVE, &anim_data_visible);
        }
      }
  
      /* free temp data */
      BLI_freelistN(&anim_data_visible);
    }
  
    /* assemble lists into one list (and clear moved tags) */
    join_groups_action_temp(act);
  }
  
  /* ------------------- */
  
  static void rearrange_nla_control_channels(bAnimContext *ac,
                                             AnimData *adt,
                                             eRearrangeAnimChan_Mode mode)
  {
    ListBase anim_data_visible = {NULL, NULL};
  
    NlaTrack *nlt;
    NlaStrip *strip;
  
    /* get rearranging function */
    AnimChanRearrangeFp rearrange_func = rearrange_get_mode_func(mode);
  
    if (rearrange_func == NULL) {
      return;
    }
  
    /* skip if these curves aren't being shown */
    if (adt->flag & ADT_NLA_SKEYS_COLLAPSED) {
      return;
    }
  
    /* Filter visible data. */
    rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_NLACURVE);
  
    /* we cannot rearrange between strips, but within each strip, we can rearrange those curves */
    for (nlt = adt->nla_tracks.first; nlt; nlt = nlt->next) {
      for (strip = nlt->strips.first; strip; strip = strip->next) {
        rearrange_animchannel_islands(
            &strip->fcurves, rearrange_func, mode, ANIMTYPE_NLACURVE, &anim_data_visible);
      }
    }
  
    /* free temp data */
    BLI_freelistN(&anim_data_visible);
  }
  
  /* ------------------- */
  
  static void rearrange_gpencil_channels(bAnimContext *ac, eRearrangeAnimChan_Mode mode)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* get rearranging function */
    AnimChanRearrangeFp rearrange_func = rearrange_get_mode_func(mode);
  
    if (rearrange_func == NULL) {
      return;
    }
  
    /* get Grease Pencil datablocks */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_ANIMDATA);
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    for (ale = anim_data.first; ale; ale = ale->next) {
      ListBase anim_data_visible = {NULL, NULL};
      bGPdata *gpd = ale->data;
  
      /* only consider layers if this datablock is open */
      BLI_assert(ale->type == ANIMTYPE_GPDATABLOCK);
      if ((gpd->flag & GP_DATA_EXPAND) == 0) {
        continue;
      }
  
      /* Filter visible data. */
      rearrange_animchannels_filter_visible(&anim_data_visible, ac, ANIMTYPE_GPLAYER);
  
      /* rearrange datablock's layers */
      rearrange_animchannel_islands(
          &gpd->layers, rearrange_func, mode, ANIMTYPE_GPLAYER, &anim_data_visible);
  
      /* free visible layers data */
      BLI_freelistN(&anim_data_visible);
    }
  
    /* free GPD channel data */
    ANIM_animdata_freelist(&anim_data);
  }
  
  /* ------------------- */
  
  static int animchannels_rearrange_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    eRearrangeAnimChan_Mode mode;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* get mode */
    mode = RNA_enum_get(op->ptr, "direction");
  
    /* method to move channels depends on the editor */
    if (ac.datatype == ANIMCONT_GPENCIL) {
      /* Grease Pencil channels */
      rearrange_gpencil_channels(&ac, mode);
    }
    else if (ac.datatype == ANIMCONT_MASK) {
      /* Grease Pencil channels */
      printf("Mask does not supported for moving yet\n");
    }
    else if (ac.datatype == ANIMCONT_ACTION) {
      /* Directly rearrange action's channels */
      rearrange_action_channels(&ac, ac.data, mode);
    }
    else {
      ListBase anim_data = {NULL, NULL};
      bAnimListElem *ale;
      int filter;
  
      /* get animdata blocks */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_ANIMDATA);
      ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
      for (ale = anim_data.first; ale; ale = ale->next) {
        AnimData *adt = ale->data;
  
        switch (ac.datatype) {
          case ANIMCONT_NLA: /* NLA-tracks only */
            rearrange_nla_channels(&ac, adt, mode);
            DEG_id_tag_update(ale->id, ID_RECALC_ANIMATION);
            break;
  
          case ANIMCONT_DRIVERS: /* Drivers list only */
            rearrange_driver_channels(&ac, adt, mode);
            break;
  
          case ANIMCONT_ACTION:    /* Single Action only... */
          case ANIMCONT_SHAPEKEY:  // DOUBLE CHECK ME...
          {
            if (adt->action) {
              rearrange_action_channels(&ac, adt->action, mode);
            }
            else if (G.debug & G_DEBUG) {
              printf("Animdata has no action\n");
            }
            break;
          }
  
          default: /* DopeSheet/Graph Editor - Some Actions + NLA Control Curves */
          {
            /* NLA Control Curves */
            if (adt->nla_tracks.first) {
              rearrange_nla_control_channels(&ac, adt, mode);
            }
  
            /* Action */
            if (adt->action) {
              rearrange_action_channels(&ac, adt->action, mode);
            }
            else if (G.debug & G_DEBUG) {
              printf("Animdata has no action\n");
            }
            break;
          }
        }
      }
  
      /* free temp data */
      ANIM_animdata_freelist(&anim_data);
    }
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_move(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Move Channels";
    ot->idname = "ANIM_OT_channels_move";
    ot->description = "Rearrange selected animation channels";
  
    /* api callbacks */
    ot->exec = animchannels_rearrange_exec;
    ot->poll = animedit_poll_channels_nla_tweakmode_off;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    ot->prop = RNA_def_enum(ot->srna,
                            "direction",
                            prop_animchannel_rearrange_types,
                            REARRANGE_ANIMCHAN_DOWN,
                            "Direction",
                            "");
  }
  
  /* ******************** Group Channel Operator ************************ */
  
  static bool animchannels_grouping_poll(bContext *C)
  {
    ScrArea *sa = CTX_wm_area(C);
    SpaceLink *sl;
  
    /* channels region test */
    /* TODO: could enhance with actually testing if channels region? */
    if (ELEM(NULL, sa, CTX_wm_region(C))) {
      return 0;
    }
  
    /* animation editor test - must be suitable modes only */
    sl = CTX_wm_space_data(C);
  
    switch (sa->spacetype) {
      /* supported... */
      case SPACE_ACTION: {
        SpaceAction *saction = (SpaceAction *)sl;
  
        /* dopesheet and action only - all others are for other datatypes or have no groups */
        if (ELEM(saction->mode, SACTCONT_ACTION, SACTCONT_DOPESHEET) == 0) {
          return 0;
        }
  
        break;
      }
      case SPACE_GRAPH: {
        SpaceGraph *sipo = (SpaceGraph *)sl;
  
        /* drivers can't have groups... */
        if (sipo->mode != SIPO_MODE_ANIMATION) {
          return 0;
        }
  
        break;
      }
      /* unsupported... */
      default:
        return 0;
    }
  
    return 1;
  }
  
  /* ----------------------------------------------------------- */
  
  static void animchannels_group_channels(bAnimContext *ac,
                                          bAnimListElem *adt_ref,
                                          const char name[])
  {
    AnimData *adt = adt_ref->adt;
    bAction *act = adt->action;
  
    if (act) {
      ListBase anim_data = {NULL, NULL};
      int filter;
  
      /* find selected F-Curves to re-group */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_SEL);
      ANIM_animdata_filter(ac, &anim_data, filter, adt_ref, ANIMCONT_CHANNEL);
  
      if (anim_data.first) {
        bActionGroup *agrp;
        bAnimListElem *ale;
  
        /* create new group, which should now be part of the action */
        agrp = action_groups_add_new(act, name);
        BLI_assert(agrp != NULL);
  
        /* transfer selected F-Curves across to new group  */
        for (ale = anim_data.first; ale; ale = ale->next) {
          FCurve *fcu = (FCurve *)ale->data;
          bActionGroup *grp = fcu->grp;
  
          /* remove F-Curve from group, then group too if it is now empty */
          action_groups_remove_channel(act, fcu);
  
          if ((grp) && BLI_listbase_is_empty(&grp->channels)) {
            BLI_freelinkN(&act->groups, grp);
          }
  
          /* add F-Curve to group */
          action_groups_add_channel(act, agrp, fcu);
        }
      }
  
      /* cleanup */
      ANIM_animdata_freelist(&anim_data);
    }
  }
  
  static int animchannels_group_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    char name[MAX_NAME];
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* get name for new group */
    RNA_string_get(op->ptr, "name", name);
  
    /* XXX: name for group should never be empty... */
    if (name[0]) {
      ListBase anim_data = {NULL, NULL};
      bAnimListElem *ale;
      int filter;
  
      /* handle each animdata block separately, so that the regrouping doesn't flow into blocks  */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_ANIMDATA |
                ANIMFILTER_NODUPLIS);
      ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
      for (ale = anim_data.first; ale; ale = ale->next) {
        animchannels_group_channels(&ac, ale, name);
      }
  
      /* free temp data */
      ANIM_animdata_freelist(&anim_data);
  
      /* updatss */
      WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
    }
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_group(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Group Channels";
    ot->idname = "ANIM_OT_channels_group";
    ot->description = "Add selected F-Curves to a new group";
  
    /* callbacks */
    ot->invoke = WM_operator_props_popup;
    ot->exec = animchannels_group_exec;
    ot->poll = animchannels_grouping_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    ot->prop = RNA_def_string(ot->srna,
                              "name",
                              "New Group",
                              sizeof(((bActionGroup *)NULL)->name),
                              "Name",
                              "Name of newly created group");
    /* XXX: still not too sure about this - keeping same text is confusing... */
    // RNA_def_property_flag(ot->prop, PROP_SKIP_SAVE);
  }
  
  /* ----------------------------------------------------------- */
  
  static int animchannels_ungroup_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bAnimContext ac;
  
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* just selected F-Curves... */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_SEL |
              ANIMFILTER_NODUPLIS);
    ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
    for (ale = anim_data.first; ale; ale = ale->next) {
      /* find action for this F-Curve... */
      if (ale->adt && ale->adt->action) {
        FCurve *fcu = (FCurve *)ale->data;
        bAction *act = ale->adt->action;
  
        /* only proceed to remove if F-Curve is in a group... */
        if (fcu->grp) {
          bActionGroup *agrp = fcu->grp;
  
          /* remove F-Curve from group and add at tail (ungrouped) */
          action_groups_remove_channel(act, fcu);
          BLI_addtail(&act->curves, fcu);
  
          /* delete group if it is now empty */
          if (BLI_listbase_is_empty(&agrp->channels)) {
            BLI_freelinkN(&act->groups, agrp);
          }
        }
      }
    }
  
    /* cleanup */
    ANIM_animdata_freelist(&anim_data);
  
    /* updates */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_ungroup(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Ungroup Channels";
    ot->idname = "ANIM_OT_channels_ungroup";
    ot->description = "Remove selected F-Curves from their current groups";
  
    /* callbacks */
    ot->exec = animchannels_ungroup_exec;
    ot->poll = animchannels_grouping_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /* ******************** Delete Channel Operator *********************** */
  
  static void tag_update_animation_element(bAnimListElem *ale)
  {
    ID *id = ale->id;
    AnimData *adt = BKE_animdata_from_id(id);
    /* TODO(sergey): Technically, if the animation element is being deleted
     * from a driver we don't have to tag action. This is something we can check
     * for in the future. For now just do most reliable tag which was always happening. */
    if (adt != NULL) {
      DEG_id_tag_update(id, ID_RECALC_ANIMATION);
      if (adt->action != NULL) {
        DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION);
      }
    }
    /* Deals with NLA and drivers.
     * Doesn't cause overhead for action updates, since object will receive
     * animation update after dependency graph flushes update from action to
     * all its users. */
    DEG_id_tag_update(id, ID_RECALC_ANIMATION);
  }
  
  static int animchannels_delete_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bAnimContext ac;
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* cannot delete in shapekey */
    if (ac.datatype == ANIMCONT_SHAPEKEY) {
      return OPERATOR_CANCELLED;
    }
  
    /* do groups only first (unless in Drivers mode, where there are none) */
    if (ac.datatype != ANIMCONT_DRIVERS) {
      /* filter data */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_SEL |
                ANIMFILTER_LIST_CHANNELS | ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS);
      ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
      /* delete selected groups and their associated channels */
      for (ale = anim_data.first; ale; ale = ale->next) {
        /* only groups - don't check other types yet, since they may no-longer exist */
        if (ale->type == ANIMTYPE_GROUP) {
          bActionGroup *agrp = (bActionGroup *)ale->data;
          AnimData *adt = ale->adt;
          FCurve *fcu, *fcn;
  
          /* skip this group if no AnimData available, as we can't safely remove the F-Curves */
          if (adt == NULL) {
            continue;
          }
  
          /* delete all of the Group's F-Curves, but no others */
          for (fcu = agrp->channels.first; fcu && fcu->grp == agrp; fcu = fcn) {
            fcn = fcu->next;
  
            /* remove from group and action, then free */
            action_groups_remove_channel(adt->action, fcu);
            free_fcurve(fcu);
          }
  
          /* free the group itself */
          if (adt->action) {
            BLI_freelinkN(&adt->action->groups, agrp);
            DEG_id_tag_update_ex(CTX_data_main(C), &adt->action->id, ID_RECALC_ANIMATION);
          }
          else {
            MEM_freeN(agrp);
          }
        }
      }
  
      /* cleanup */
      ANIM_animdata_freelist(&anim_data);
    }
  
    /* filter data */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_SEL |
              ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS);
    ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
    /* delete selected data channels */
    for (ale = anim_data.first; ale; ale = ale->next) {
      switch (ale->type) {
        case ANIMTYPE_FCURVE: {
          /* F-Curves if we can identify its parent */
          AnimData *adt = ale->adt;
          FCurve *fcu = (FCurve *)ale->data;
  
          /* try to free F-Curve */
          ANIM_fcurve_delete_from_animdata(&ac, adt, fcu);
          tag_update_animation_element(ale);
          break;
        }
        case ANIMTYPE_NLACURVE: {
          /* NLA Control Curve - Deleting it should disable the corresponding setting... */
          NlaStrip *strip = (NlaStrip *)ale->owner;
          FCurve *fcu = (FCurve *)ale->data;
  
          if (STREQ(fcu->rna_path, "strip_time")) {
            strip->flag &= ~NLASTRIP_FLAG_USR_TIME;
          }
          else if (STREQ(fcu->rna_path, "influence")) {
            strip->flag &= ~NLASTRIP_FLAG_USR_INFLUENCE;
          }
          else {
            printf("ERROR: Trying to delete NLA Control Curve for unknown property '%s'\n",
                   fcu->rna_path);
          }
  
          /* unlink and free the F-Curve */
          BLI_remlink(&strip->fcurves, fcu);
          free_fcurve(fcu);
          tag_update_animation_element(ale);
          break;
        }
        case ANIMTYPE_GPLAYER: {
          /* Grease Pencil layer */
          bGPdata *gpd = (bGPdata *)ale->id;
          bGPDlayer *gpl = (bGPDlayer *)ale->data;
  
          /* try to delete the layer's data and the layer itself */
          BKE_gpencil_layer_delete(gpd, gpl);
          ale->update = ANIM_UPDATE_DEPS;
          break;
        }
        case ANIMTYPE_MASKLAYER: {
          /* Mask layer */
          Mask *mask = (Mask *)ale->id;
          MaskLayer *masklay = (MaskLayer *)ale->data;
  
          /* try to delete the layer's data and the layer itself */
          BKE_mask_layer_remove(mask, masklay);
          break;
        }
      }
    }
  
    /* cleanup */
    ANIM_animdata_freelist(&anim_data);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
    DEG_relations_tag_update(CTX_data_main(C));
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_delete(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Delete Channels";
    ot->idname = "ANIM_OT_channels_delete";
    ot->description = "Delete all selected animation channels";
  
    /* api callbacks */
    ot->exec = animchannels_delete_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /* ********************** Set Flags Operator *********************** */
  
  /* defines for setting animation-channel flags */
  static const EnumPropertyItem prop_animchannel_setflag_types[] = {
      {ACHANNEL_SETFLAG_TOGGLE, "TOGGLE", 0, "Toggle", ""},
      {ACHANNEL_SETFLAG_CLEAR, "DISABLE", 0, "Disable", ""},
      {ACHANNEL_SETFLAG_ADD, "ENABLE", 0, "Enable", ""},
      {ACHANNEL_SETFLAG_INVERT, "INVERT", 0, "Invert", ""},
      {0, NULL, 0, NULL, NULL},
  };
  
  /* defines for set animation-channel settings */
  // TODO: could add some more types, but those are really quite dependent on the mode...
  static const EnumPropertyItem prop_animchannel_settings_types[] = {
      {ACHANNEL_SETTING_PROTECT, "PROTECT", 0, "Protect", ""},
      {ACHANNEL_SETTING_MUTE, "MUTE", 0, "Mute", ""},
      {0, NULL, 0, NULL, NULL},
  };
  
  /* ------------------- */
  
  /* Set/clear a particular flag (setting) for all selected + visible channels
   * setting: the setting to modify
   * mode: eAnimChannels_SetFlag
   * onlysel: only selected channels get the flag set
   */
  // TODO: enable a setting which turns flushing on/off?
  static void setflag_anim_channels(bAnimContext *ac,
                                    eAnimChannel_Settings setting,
                                    eAnimChannels_SetFlag mode,
                                    bool onlysel,
                                    bool flush)
  {
    ListBase anim_data = {NULL, NULL};
    ListBase all_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* filter data that we need if flush is on */
    if (flush) {
      /* get list of all channels that selection may need to be flushed to
       * - hierarchy visibility needs to be ignored so that settings can get flushed
       *   "down" inside closed containers
       */
      filter = ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_CHANNELS;
      ANIM_animdata_filter(ac, &all_data, filter, ac->data, ac->datatype);
    }
  
    /* filter data that we're working on
     * - hierarchy matters if we're doing this from the channels region
     *   since we only want to apply this to channels we can "see",
     *   and have these affect their relatives
     * - but for Graph Editor, this gets used also from main region
     *   where hierarchy doesn't apply [#21276]
     */
    if ((ac->spacetype == SPACE_GRAPH) && (ac->regiontype != RGN_TYPE_CHANNELS)) {
      /* graph editor (case 2) */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_CHANNELS | ANIMFILTER_CURVE_VISIBLE |
                ANIMFILTER_NODUPLIS);
    }
    else {
      /* standard case */
      filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS |
                ANIMFILTER_NODUPLIS);
    }
    if (onlysel) {
      filter |= ANIMFILTER_SEL;
    }
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    /* if toggling, check if disable or enable */
    if (mode == ACHANNEL_SETFLAG_TOGGLE) {
      /* default to turn all on, unless we encounter one that's on... */
      mode = ACHANNEL_SETFLAG_ADD;
  
      /* see if we should turn off instead... */
      for (ale = anim_data.first; ale; ale = ale->next) {
        /* set the setting in the appropriate way (if available) */
        if (ANIM_channel_setting_get(ac, ale, setting) > 0) {
          mode = ACHANNEL_SETFLAG_CLEAR;
          break;
        }
      }
    }
  
    /* apply the setting */
    for (ale = anim_data.first; ale; ale = ale->next) {
      /* skip channel if setting is not available */
      if (ANIM_channel_setting_get(ac, ale, setting) == -1) {
        continue;
      }
  
      /* set the setting in the appropriate way */
      ANIM_channel_setting_set(ac, ale, setting, mode);
      tag_update_animation_element(ale);
  
      /* if flush status... */
      if (flush) {
        ANIM_flush_setting_anim_channels(ac, &all_data, ale, setting, mode);
      }
    }
  
    ANIM_animdata_freelist(&anim_data);
    BLI_freelistN(&all_data);
  }
  
  /* ------------------- */
  
  static int animchannels_setflag_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    eAnimChannel_Settings setting;
    eAnimChannels_SetFlag mode;
    bool flush = true;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* mode (eAnimChannels_SetFlag), setting (eAnimChannel_Settings) */
    mode = RNA_enum_get(op->ptr, "mode");
    setting = RNA_enum_get(op->ptr, "type");
  
    /* check if setting is flushable */
    if (setting == ACHANNEL_SETTING_EXPAND) {
      flush = false;
    }
  
    /* modify setting
     * - only selected channels are affected
     */
    setflag_anim_channels(&ac, setting, mode, true, flush);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  /* duplicate of 'ANIM_OT_channels_setting_toggle' for menu title only, weak! */
  static void ANIM_OT_channels_setting_enable(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Enable Channel Setting";
    ot->idname = "ANIM_OT_channels_setting_enable";
    ot->description = "Enable specified setting on all selected animation channels";
  
    /* api callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = animchannels_setflag_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    /* flag-setting mode */
    prop = RNA_def_enum(
        ot->srna, "mode", prop_animchannel_setflag_types, ACHANNEL_SETFLAG_ADD, "Mode", "");
    RNA_def_property_flag(prop, PROP_HIDDEN);
    /* setting to set */
    ot->prop = RNA_def_enum(ot->srna, "type", prop_animchannel_settings_types, 0, "Type", "");
  }
  /* duplicate of 'ANIM_OT_channels_setting_toggle' for menu title only, weak! */
  static void ANIM_OT_channels_setting_disable(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Disable Channel Setting";
    ot->idname = "ANIM_OT_channels_setting_disable";
    ot->description = "Disable specified setting on all selected animation channels";
  
    /* api callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = animchannels_setflag_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    /* flag-setting mode */
    prop = RNA_def_enum(
        ot->srna, "mode", prop_animchannel_setflag_types, ACHANNEL_SETFLAG_CLEAR, "Mode", "");
    RNA_def_property_flag(prop, PROP_HIDDEN); /* internal hack - don't expose */
    /* setting to set */
    ot->prop = RNA_def_enum(ot->srna, "type", prop_animchannel_settings_types, 0, "Type", "");
  }
  
  static void ANIM_OT_channels_setting_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Toggle Channel Setting";
    ot->idname = "ANIM_OT_channels_setting_toggle";
    ot->description = "Toggle specified setting on all selected animation channels";
  
    /* api callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = animchannels_setflag_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    /* flag-setting mode */
    prop = RNA_def_enum(
        ot->srna, "mode", prop_animchannel_setflag_types, ACHANNEL_SETFLAG_TOGGLE, "Mode", "");
    RNA_def_property_flag(prop, PROP_HIDDEN); /* internal hack - don't expose */
    /* setting to set */
    ot->prop = RNA_def_enum(ot->srna, "type", prop_animchannel_settings_types, 0, "Type", "");
  }
  
  static void ANIM_OT_channels_editable_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Toggle Channel Editability";
    ot->idname = "ANIM_OT_channels_editable_toggle";
    ot->description = "Toggle editability of selected channels";
  
    /* api callbacks */
    ot->exec = animchannels_setflag_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    /* flag-setting mode */
    RNA_def_enum(
        ot->srna, "mode", prop_animchannel_setflag_types, ACHANNEL_SETFLAG_TOGGLE, "Mode", "");
    /* setting to set */
    prop = RNA_def_enum(
        ot->srna, "type", prop_animchannel_settings_types, ACHANNEL_SETTING_PROTECT, "Type", "");
    RNA_def_property_flag(prop, PROP_HIDDEN); /* internal hack - don't expose */
  }
  
  /* ********************** Expand Channels Operator *********************** */
  
  static int animchannels_expand_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    bool onlysel = true;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* only affect selected channels? */
    if (RNA_boolean_get(op->ptr, "all")) {
      onlysel = false;
    }
  
    /* modify setting */
    setflag_anim_channels(&ac, ACHANNEL_SETTING_EXPAND, ACHANNEL_SETFLAG_ADD, onlysel, false);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_expand(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Expand Channels";
    ot->idname = "ANIM_OT_channels_expand";
    ot->description = "Expand (i.e. open) all selected expandable animation channels";
  
    /* api callbacks */
    ot->exec = animchannels_expand_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    ot->prop = RNA_def_boolean(
        ot->srna, "all", 1, "All", "Expand all channels (not just selected ones)");
  }
  
  /* ********************** Collapse Channels Operator *********************** */
  
  static int animchannels_collapse_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    bool onlysel = true;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* only affect selected channels? */
    if (RNA_boolean_get(op->ptr, "all")) {
      onlysel = false;
    }
  
    /* modify setting */
    setflag_anim_channels(&ac, ACHANNEL_SETTING_EXPAND, ACHANNEL_SETFLAG_CLEAR, onlysel, false);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_collapse(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Collapse Channels";
    ot->idname = "ANIM_OT_channels_collapse";
    ot->description = "Collapse (i.e. close) all selected expandable animation channels";
  
    /* api callbacks */
    ot->exec = animchannels_collapse_exec;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* props */
    ot->prop = RNA_def_boolean(
        ot->srna, "all", true, "All", "Collapse all channels (not just selected ones)");
  }
  
  /* ************ Remove All "Empty" AnimData Blocks Operator ********* */
  /* We define "empty" AnimData blocks here as those which have all 3 of criteria:
   *  1) No active action OR that active actions are empty
   *     Assuming that all legitimate entries will have an action,
   *     and that empty actions
   *  2) No NLA Tracks + NLA Strips
   *     Assuming that users haven't set up any of these as "placeholders"
   *     for convenience sake, and that most that exist were either unintentional
   *     or are no longer wanted
   *  3) No drivers
   */
  
  static int animchannels_clean_empty_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bAnimContext ac;
  
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* get animdata blocks */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_ANIMDATA);
    ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
    for (ale = anim_data.first; ale; ale = ale->next) {
      ID *id = ale->id;
      AnimData *adt = ale->data;
  
      bool action_empty = false;
      bool nla_empty = false;
      bool drivers_empty = false;
  
      /* sanity checks */
      BLI_assert((id != NULL) && (adt != NULL));
  
      /* check if this is "empty" and can be deleted */
      /* (For now, there are only these 3 criteria) */
  
      /* 1) Active Action is missing or empty */
      if (ELEM(NULL, adt->action, adt->action->curves.first)) {
        action_empty = true;
      }
      else {
        /* TODO: check for keyframe + fmodifier data on these too */
      }
  
      /* 2) No NLA Tracks and/or NLA Strips */
      if (adt->nla_tracks.first == NULL) {
        nla_empty = true;
      }
      else {
        NlaTrack *nlt;
  
        /* empty tracks? */
        for (nlt = adt->nla_tracks.first; nlt; nlt = nlt->next) {
          if (nlt->strips.first) {
            /* stop searching, as we found one that actually had stuff we don't want lost
             * NOTE: nla_empty gets reset to false, as a previous track may have been empty
             */
            nla_empty = false;
            break;
          }
          else if (nlt->strips.first == NULL) {
            /* this track is empty, but another one may still have stuff in it, so can't break yet */
            nla_empty = true;
          }
        }
      }
  
      /* 3) Drivers */
      drivers_empty = (adt->drivers.first == NULL);
  
      /* remove AnimData? */
      if (action_empty && nla_empty && drivers_empty) {
        BKE_animdata_free(id, true);
      }
    }
  
    /* free temp data */
    ANIM_animdata_freelist(&anim_data);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_clean_empty(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Remove Empty Animation Data";
    ot->idname = "ANIM_OT_channels_clean_empty";
    ot->description = "Delete all empty animation data containers from visible data-blocks";
  
    /* api callbacks */
    ot->exec = animchannels_clean_empty_exec;
    ot->poll = animedit_poll_channels_nla_tweakmode_off;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /* ******************* Reenable Disabled Operator ******************* */
  
  static bool animchannels_enable_poll(bContext *C)
  {
    ScrArea *sa = CTX_wm_area(C);
  
    /* channels region test */
    /* TODO: could enhance with actually testing if channels region? */
    if (ELEM(NULL, sa, CTX_wm_region(C))) {
      return 0;
    }
  
    /* animation editor test - Action/Dopesheet/etc. and Graph only */
    if (ELEM(sa->spacetype, SPACE_ACTION, SPACE_GRAPH) == 0) {
      return 0;
    }
  
    return 1;
  }
  
  static int animchannels_enable_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bAnimContext ac;
  
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* filter data */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_NODUPLIS);
    ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, ac.datatype);
  
    /* loop through filtered data and clean curves */
    for (ale = anim_data.first; ale; ale = ale->next) {
      FCurve *fcu = (FCurve *)ale->data;
  
      /* remove disabled flags from F-Curves */
      fcu->flag &= ~FCURVE_DISABLED;
  
      /* for drivers, let's do the same too */
      if (fcu->driver) {
        fcu->driver->flag &= ~DRIVER_FLAG_INVALID;
      }
  
      /* tag everything for updates - in particular, this is needed to get drivers working again */
      ale->update |= ANIM_UPDATE_DEPS;
    }
  
    ANIM_animdata_update(&ac, &anim_data);
    ANIM_animdata_freelist(&anim_data);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_fcurves_enable(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Revive Disabled F-Curves";
    ot->idname = "ANIM_OT_channels_fcurves_enable";
    ot->description = "Clears 'disabled' tag from all F-Curves to get broken F-Curves working again";
  
    /* api callbacks */
    ot->exec = animchannels_enable_exec;
    ot->poll = animchannels_enable_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /* ****************** Find / Set Filter Operator ******************** */
  
  /* XXX: make this generic? */
  static bool animchannels_find_poll(bContext *C)
  {
    ScrArea *sa = CTX_wm_area(C);
  
    if (sa == NULL) {
      return 0;
    }
  
    /* animation editor with dopesheet */
    return ELEM(sa->spacetype, SPACE_ACTION, SPACE_GRAPH, SPACE_NLA);
  }
  
  /* find_invoke() - Get initial channels */
  static int animchannels_find_invoke(bContext *C, wmOperator *op, const wmEvent *evt)
  {
    bAnimContext ac;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* set initial filter text, and enable filter */
    RNA_string_set(op->ptr, "query", ac.ads->searchstr);
  
    /* defer to popup */
    return WM_operator_props_popup(C, op, evt);
  }
  
  /* find_exec() -  Called to set the value */
  static int animchannels_find_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* update filter text */
    RNA_string_get(op->ptr, "query", ac.ads->searchstr);
  
    /* redraw */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_find(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Find Channels";
    ot->idname = "ANIM_OT_channels_find";
    ot->description = "Filter the set of channels shown to only include those with matching names";
  
    /* callbacks */
    ot->invoke = animchannels_find_invoke;
    ot->exec = animchannels_find_exec;
    ot->poll = animchannels_find_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_string(ot->srna,
                              "query",
                              "Query",
                              sizeof(((bDopeSheet *)NULL)->searchstr),
                              "",
                              "Text to search for in channel names");
  }
  
  /* ********************** Select All Operator *********************** */
  
  static int animchannels_deselectall_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* 'standard' behavior - check if selected, then apply relevant selection */
    const int action = RNA_enum_get(op->ptr, "action");
    switch (action) {
      case SEL_TOGGLE:
        ANIM_deselect_anim_channels(&ac, ac.data, ac.datatype, true, ACHANNEL_SETFLAG_ADD);
        break;
      case SEL_SELECT:
        ANIM_deselect_anim_channels(&ac, ac.data, ac.datatype, false, ACHANNEL_SETFLAG_ADD);
        break;
      case SEL_DESELECT:
        ANIM_deselect_anim_channels(&ac, ac.data, ac.datatype, false, ACHANNEL_SETFLAG_CLEAR);
        break;
      case SEL_INVERT:
        ANIM_deselect_anim_channels(&ac, ac.data, ac.datatype, false, ACHANNEL_SETFLAG_INVERT);
        break;
      default:
        BLI_assert(0);
        break;
    }
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_SELECTED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_select_all(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Select All";
    ot->idname = "ANIM_OT_channels_select_all";
    ot->description = "Toggle selection of all animation channels";
  
    /* api callbacks */
    ot->exec = animchannels_deselectall_exec;
    ot->poll = animedit_poll_channels_nla_tweakmode_off;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    WM_operator_properties_select_all(ot);
  }
  
  /* ******************** Box Select Operator *********************** */
  
  static void box_select_anim_channels(bAnimContext *ac, rcti *rect, short selectmode)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
  
    SpaceNla *snla = (SpaceNla *)ac->sl;
    View2D *v2d = &ac->ar->v2d;
    rctf rectf;
  
    /* convert border-region to view coordinates */
    UI_view2d_region_to_view(v2d, rect->xmin, rect->ymin + 2, &rectf.xmin, &rectf.ymin);
    UI_view2d_region_to_view(v2d, rect->xmax, rect->ymax - 2, &rectf.xmax, &rectf.ymax);
  
    /* filter data */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS);
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    float ymax;
    if (ac->datatype == ANIMCONT_NLA) {
      ymax = NLACHANNEL_FIRST_TOP(ac);
    }
    else {
      ymax = ACHANNEL_FIRST_TOP(ac);
    }
  
    /* loop over data, doing box select */
    for (ale = anim_data.first; ale; ale = ale->next) {
      float ymin;
      if (ac->datatype == ANIMCONT_NLA) {
        ymin = ymax - NLACHANNEL_STEP(snla);
      }
      else {
        ymin = ymax - ACHANNEL_STEP(ac);
      }
  
      /* if channel is within border-select region, alter it */
      if (!((ymax < rectf.ymin) || (ymin > rectf.ymax))) {
        /* set selection flags only */
        ANIM_channel_setting_set(ac, ale, ACHANNEL_SETTING_SELECT, selectmode);
  
        /* type specific actions */
        switch (ale->type) {
          case ANIMTYPE_GROUP: {
            bActionGroup *agrp = (bActionGroup *)ale->data;
            select_pchan_for_action_group(ac, agrp, ale);
            /* always clear active flag after doing this */
            agrp->flag &= ~AGRP_ACTIVE;
            break;
          }
          case ANIMTYPE_NLATRACK: {
            NlaTrack *nlt = (NlaTrack *)ale->data;
  
            /* for now, it's easier just to do this here manually, as defining a new type
             * currently adds complications when doing other stuff
             */
            ACHANNEL_SET_FLAG(nlt, selectmode, NLATRACK_SELECTED);
            break;
          }
        }
      }
  
      /* set minimum extent to be the maximum of the next channel */
      ymax = ymin;
    }
  
    /* cleanup */
    ANIM_animdata_freelist(&anim_data);
  }
  
  /* ------------------- */
  
  static int animchannels_box_select_exec(bContext *C, wmOperator *op)
  {
    bAnimContext ac;
    rcti rect;
    short selectmode = 0;
    const bool select = !RNA_boolean_get(op->ptr, "deselect");
    const bool extend = RNA_boolean_get(op->ptr, "extend");
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* get settings from operator */
    WM_operator_properties_border_to_rcti(op, &rect);
  
    if (!extend) {
      ANIM_deselect_anim_channels(&ac, ac.data, ac.datatype, true, ACHANNEL_SETFLAG_CLEAR);
    }
  
    if (select) {
      selectmode = ACHANNEL_SETFLAG_ADD;
    }
    else {
      selectmode = ACHANNEL_SETFLAG_CLEAR;
    }
  
    /* apply box_select animation channels */
    box_select_anim_channels(&ac, &rect, selectmode);
  
    /* send notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | ND_ANIMCHAN | NA_SELECTED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_select_box(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Box Select";
    ot->idname = "ANIM_OT_channels_select_box";
    ot->description = "Select all animation channels within the specified region";
  
    /* api callbacks */
    ot->invoke = WM_gesture_box_invoke;
    ot->exec = animchannels_box_select_exec;
    ot->modal = WM_gesture_box_modal;
    ot->cancel = WM_gesture_box_cancel;
  
    ot->poll = animedit_poll_channels_nla_tweakmode_off;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* rna */
    WM_operator_properties_gesture_box_select(ot);
  }
  
  /* ******************* Rename Operator ***************************** */
  /* Allow renaming some channels by clicking on them */
  
  static bool rename_anim_channels(bAnimContext *ac, int channel_index)
  {
    ListBase anim_data = {NULL, NULL};
    const bAnimChannelType *acf;
    bAnimListElem *ale;
    int filter;
    bool success = false;
  
    /* get the channel that was clicked on */
    /* filter channels */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS);
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    /* get channel from index */
    ale = BLI_findlink(&anim_data, channel_index);
    if (ale == NULL) {
      /* channel not found */
      if (G.debug & G_DEBUG) {
        printf("Error: animation channel (index = %d) not found in rename_anim_channels()\n",
               channel_index);
      }
  
      ANIM_animdata_freelist(&anim_data);
      return false;
    }
  
    /* don't allow renaming linked channels */
    if ((ale->fcurve_owner_id != NULL && ID_IS_LINKED(ale->fcurve_owner_id)) ||
        (ale->id != NULL && ID_IS_LINKED(ale->id))) {
      ANIM_animdata_freelist(&anim_data);
      return false;
    }
  
    /* check that channel can be renamed */
    acf = ANIM_channel_get_typeinfo(ale);
    if (acf && acf->name_prop) {
      PointerRNA ptr;
      PropertyRNA *prop;
  
      /* ok if we can get name property to edit from this channel */
      if (acf->name_prop(ale, &ptr, &prop)) {
        /* actually showing the rename textfield is done on redraw,
         * so here we just store the index of this channel in the
         * dopesheet data, which will get utilized when drawing the
         * channel...
         *
         * +1 factor is for backwards compat issues
         */
        if (ac->ads) {
          ac->ads->renameIndex = channel_index + 1;
          success = true;
        }
      }
    }
  
    /* free temp data and tag for refresh */
    ANIM_animdata_freelist(&anim_data);
    ED_region_tag_redraw(ac->ar);
    return success;
  }
  
  static int animchannels_channel_get(bAnimContext *ac, const int mval[2])
  {
    ARegion *ar;
    View2D *v2d;
    int channel_index;
    float x, y;
  
    /* get useful pointers from animation context data */
    ar = ac->ar;
    v2d = &ar->v2d;
  
    /* Figure out which channel user clicked in. */
    UI_view2d_region_to_view(v2d, mval[0], mval[1], &x, &y);
  
    if (ac->datatype == ANIMCONT_NLA) {
      SpaceNla *snla = (SpaceNla *)ac->sl;
      UI_view2d_listview_view_to_cell(NLACHANNEL_NAMEWIDTH,
                                      NLACHANNEL_STEP(snla),
                                      0,
                                      NLACHANNEL_FIRST_TOP(ac),
                                      x,
                                      y,
                                      NULL,
                                      &channel_index);
    }
    else {
      UI_view2d_listview_view_to_cell(ACHANNEL_NAMEWIDTH,
                                      ACHANNEL_STEP(ac),
                                      0,
                                      ACHANNEL_FIRST_TOP(ac),
                                      x,
                                      y,
                                      NULL,
                                      &channel_index);
    }
  
    return channel_index;
  }
  
  static int animchannels_rename_invoke(bContext *C, wmOperator *UNUSED(op), const wmEvent *event)
  {
    bAnimContext ac;
    int channel_index;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    channel_index = animchannels_channel_get(&ac, event->mval);
  
    /* handle click */
    if (rename_anim_channels(&ac, channel_index)) {
      return OPERATOR_FINISHED;
    }
    else {
      /* allow event to be handled by selectall operator */
      return OPERATOR_PASS_THROUGH;
    }
  }
  
  static void ANIM_OT_channels_rename(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Rename Channels";
    ot->idname = "ANIM_OT_channels_rename";
    ot->description = "Rename animation channel under mouse";
  
    /* api callbacks */
    ot->invoke = animchannels_rename_invoke;
    ot->poll = animedit_poll_channels_active;
  }
  
  /* ******************** Mouse-Click Operator *********************** */
  /* Handle selection changes due to clicking on channels. Settings will get caught by UI code... */
  
  static int mouse_anim_channels(bContext *C, bAnimContext *ac, int channel_index, short selectmode)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
    int notifierFlags = 0;
  
    /* get the channel that was clicked on */
    /* filter channels */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS);
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    /* get channel from index */
    ale = BLI_findlink(&anim_data, channel_index);
    if (ale == NULL) {
      /* channel not found */
      if (G.debug & G_DEBUG) {
        printf("Error: animation channel (index = %d) not found in mouse_anim_channels()\n",
               channel_index);
      }
  
      ANIM_animdata_freelist(&anim_data);
      return 0;
    }
  
    /* selectmode -1 is a special case for ActionGroups only,
     * which selects all of the channels underneath it only. */
    /* TODO: should this feature be extended to work with other channel types too? */
    if ((selectmode == -1) && (ale->type != ANIMTYPE_GROUP)) {
      /* normal channels should not behave normally in this case */
      ANIM_animdata_freelist(&anim_data);
      return 0;
    }
  
    /* 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) {
      case ANIMTYPE_SCENE: {
        Scene *sce = (Scene *)ale->data;
        AnimData *adt = sce->adt;
  
        /* set selection status */
        if (selectmode == SELECT_INVERT) {
          /* swap select */
          sce->flag ^= SCE_DS_SELECTED;
          if (adt) {
            adt->flag ^= ADT_UI_SELECTED;
          }
        }
        else {
          sce->flag |= SCE_DS_SELECTED;
          if (adt) {
            adt->flag |= ADT_UI_SELECTED;
          }
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        break;
      }
      case ANIMTYPE_OBJECT: {
  #if 0
        bDopeSheet *ads = (bDopeSheet *)ac->data;
        Scene *sce = (Scene *)ads->source;
  #endif
        ViewLayer *view_layer = ac->view_layer;
        Base *base = (Base *)ale->data;
        Object *ob = base->object;
        AnimData *adt = ob->adt;
  
        /* set selection status */
        if (base->flag & BASE_SELECTABLE) {
          if (selectmode == SELECT_INVERT) {
            /* swap select */
            ED_object_base_select(base, BA_INVERT);
  
            if (adt) {
              adt->flag ^= ADT_UI_SELECTED;
            }
          }
          else {
            Base *b;
  
            /* deselect all */
            /* TODO: should this deselect all other types of channels too? */
            for (b = view_layer->object_bases.first; b; b = b->next) {
              ED_object_base_select(b, BA_DESELECT);
              if (b->object->adt) {
                b->object->adt->flag &= ~(ADT_UI_SELECTED | ADT_UI_ACTIVE);
              }
            }
  
            /* select object now */
            ED_object_base_select(base, BA_SELECT);
            if (adt) {
              adt->flag |= ADT_UI_SELECTED;
            }
          }
  
          /* change active object - regardless of whether it is now selected [T37883] */
          ED_object_base_activate(C, base); /* adds notifier */
  
          if ((adt) && (adt->flag & ADT_UI_SELECTED)) {
            adt->flag |= ADT_UI_ACTIVE;
          }
  
          /* Ensure we exit editmode on whatever object was active before
           * to avoid getting stuck there - T48747. */
          if (ob != CTX_data_edit_object(C)) {
            ED_object_editmode_exit(C, EM_FREEDATA);
          }
  
          notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        }
        break;
      }
      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: {
        /* sanity checking... */
        if (ale->adt) {
          /* select/deselect */
          if (selectmode == SELECT_INVERT) {
            /* inverse selection status of this AnimData block only */
            ale->adt->flag ^= ADT_UI_SELECTED;
          }
          else {
            /* select AnimData block by itself */
            ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, ACHANNEL_SETFLAG_CLEAR);
            ale->adt->flag |= ADT_UI_SELECTED;
          }
  
          /* set active? */
          if ((ale->adt) && (ale->adt->flag & ADT_UI_SELECTED)) {
            ale->adt->flag |= ADT_UI_ACTIVE;
          }
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        break;
      }
      case ANIMTYPE_GROUP: {
        bActionGroup *agrp = (bActionGroup *)ale->data;
  
        Object *ob = NULL;
        bPoseChannel *pchan = NULL;
  
        /* Armatures-Specific Feature:
         * Since groups are used to collect F-Curves of the same Bone by default
         * (via Keying Sets) so that they can be managed better, we try to make
         * things here easier for animators by mapping group selection to bone
         * selection.
         *
         * Only do this if "Only Selected" dopesheet filter is not active, or else it
         * becomes too unpredictable/tricky to manage
         */
        if ((ac->ads->filterflag & ADS_FILTER_ONLYSEL) == 0) {
          if ((ale->id) && (GS(ale->id->name) == ID_OB)) {
            ob = (Object *)ale->id;
  
            if (ob->type == OB_ARMATURE) {
              /* Assume for now that any group with corresponding name is what we want
               * (i.e. for an armature whose location is animated, things would break
               * if the user were to add a bone named "Location").
               *
               * TODO: check the first F-Curve or so to be sure...
               */
              pchan = BKE_pose_channel_find_name(ob->pose, agrp->name);
            }
          }
        }
  
        /* select/deselect group */
        if (selectmode == SELECT_INVERT) {
          /* inverse selection status of this group only */
          agrp->flag ^= AGRP_SELECTED;
        }
        else if (selectmode == -1) {
          /* select all in group (and deselect everything else) */
          FCurve *fcu;
  
          /* deselect all other channels */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, ACHANNEL_SETFLAG_CLEAR);
          if (pchan) {
            ED_pose_deselect_all(ob, SEL_DESELECT, false);
          }
  
          /* only select channels in group and group itself */
          for (fcu = agrp->channels.first; fcu && fcu->grp == agrp; fcu = fcu->next) {
            fcu->flag |= FCURVE_SELECTED;
          }
          agrp->flag |= AGRP_SELECTED;
        }
        else {
          /* select group by itself */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, ACHANNEL_SETFLAG_CLEAR);
          if (pchan) {
            ED_pose_deselect_all(ob, SEL_DESELECT, false);
          }
  
          agrp->flag |= AGRP_SELECTED;
        }
  
        /* if group is selected now, make group the 'active' one in the visible list */
        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);
          }
        }
        else {
          ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, NULL, ANIMTYPE_GROUP);
          if (pchan) {
            ED_pose_bone_select(ob, pchan, false);
          }
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        break;
      }
      case ANIMTYPE_FCURVE:
      case ANIMTYPE_NLACURVE: {
        FCurve *fcu = (FCurve *)ale->data;
  
        /* select/deselect */
        if (selectmode == SELECT_INVERT) {
          /* inverse selection status of this F-Curve only */
          fcu->flag ^= FCURVE_SELECTED;
        }
        else {
          /* select F-Curve by itself */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, 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) {
          ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, fcu, ale->type);
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        break;
      }
      case ANIMTYPE_SHAPEKEY: {
        KeyBlock *kb = (KeyBlock *)ale->data;
  
        /* select/deselect */
        if (selectmode == SELECT_INVERT) {
          /* inverse selection status of this ShapeKey only */
          kb->flag ^= KEYBLOCK_SEL;
        }
        else {
          /* select ShapeKey by itself */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, ACHANNEL_SETFLAG_CLEAR);
          kb->flag |= KEYBLOCK_SEL;
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_SELECTED);
        break;
      }
      case ANIMTYPE_NLACONTROLS: {
        AnimData *adt = (AnimData *)ale->data;
  
        /* Toggle expand:
         * - Although the triangle widget already allows this,
         *   since there's nothing else that can be done here now,
         *   let's just use it for easier expand/collapse for now.
         */
        adt->flag ^= ADT_NLA_SKEYS_COLLAPSED;
  
        notifierFlags |= (ND_ANIMCHAN | NA_EDITED);
        break;
      }
      case ANIMTYPE_GPDATABLOCK: {
        bGPdata *gpd = (bGPdata *)ale->data;
  
        /* Toggle expand:
         * - Although the triangle widget already allows this,
         *   the whole channel can also be used for this purpose.
         */
        gpd->flag ^= GP_DATA_EXPAND;
  
        notifierFlags |= (ND_ANIMCHAN | NA_EDITED);
        break;
      }
      case ANIMTYPE_GPLAYER: {
        bGPdata *gpd = (bGPdata *)ale->id;
        bGPDlayer *gpl = (bGPDlayer *)ale->data;
  
        /* select/deselect */
        if (selectmode == SELECT_INVERT) {
          /* invert selection status of this layer only */
          gpl->flag ^= GP_LAYER_SELECT;
        }
        else {
          /* select layer by itself */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, 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) {
          ANIM_set_active_channel(ac, ac->data, ac->datatype, filter, gpl, ANIMTYPE_GPLAYER);
          /* update other layer status */
          BKE_gpencil_layer_setactive(gpd, gpl);
          BKE_gpencil_layer_autolock_set(gpd, false);
          DEG_id_tag_update(&gpd->id, ID_RECALC_GEOMETRY);
        }
  
        /* Grease Pencil updates */
        WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED | ND_SPACE_PROPERTIES, NULL);
        notifierFlags |= (ND_ANIMCHAN | NA_EDITED); /* Animation Editors updates */
        break;
      }
      case ANIMTYPE_MASKDATABLOCK: {
        Mask *mask = (Mask *)ale->data;
  
        /* Toggle expand
         * - Although the triangle widget already allows this,
         *   the whole channel can also be used for this purpose.
         */
        mask->flag ^= MASK_ANIMF_EXPAND;
  
        notifierFlags |= (ND_ANIMCHAN | NA_EDITED);
        break;
      }
      case ANIMTYPE_MASKLAYER: {
        MaskLayer *masklay = (MaskLayer *)ale->data;
  
        /* select/deselect */
        if (selectmode == SELECT_INVERT) {
          /* invert selection status of this layer only */
          masklay->flag ^= MASK_LAYERFLAG_SELECT;
        }
        else {
          /* select layer by itself */
          ANIM_deselect_anim_channels(ac, ac->data, ac->datatype, false, ACHANNEL_SETFLAG_CLEAR);
          masklay->flag |= MASK_LAYERFLAG_SELECT;
        }
  
        notifierFlags |= (ND_ANIMCHAN | NA_EDITED);
        break;
      }
      default:
        if (G.debug & G_DEBUG) {
          printf("Error: Invalid channel type in mouse_anim_channels()\n");
        }
        break;
    }
  
    /* free channels */
    ANIM_animdata_freelist(&anim_data);
  
    /* return notifier flags */
    return notifierFlags;
  }
  
  /* ------------------- */
  
  /* handle clicking */
  static int animchannels_mouseclick_invoke(bContext *C, wmOperator *op, const wmEvent *event)
  {
    bAnimContext ac;
    ARegion *ar;
    View2D *v2d;
    int channel_index;
    int notifierFlags = 0;
    short selectmode;
    float x, y;
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    /* get useful pointers from animation context data */
    ar = ac.ar;
    v2d = &ar->v2d;
  
    /* select mode is either replace (deselect all, then add) or add/extend */
    if (RNA_boolean_get(op->ptr, "extend")) {
      selectmode = SELECT_INVERT;
    }
    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? */
      selectmode = -1;
    }
    else {
      selectmode = SELECT_REPLACE;
    }
  
    /* figure out which channel user clicked in */
    UI_view2d_region_to_view(v2d, event->mval[0], event->mval[1], &x, &y);
    UI_view2d_listview_view_to_cell(ACHANNEL_NAMEWIDTH,
                                    ACHANNEL_STEP(&ac),
                                    0,
                                    ACHANNEL_FIRST_TOP(&ac),
                                    x,
                                    y,
                                    NULL,
                                    &channel_index);
  
    /* handle mouse-click in the relevant channel then */
    notifierFlags = mouse_anim_channels(C, &ac, channel_index, selectmode);
  
    /* set notifier that things have changed */
    WM_event_add_notifier(C, NC_ANIMATION | notifierFlags, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  static void ANIM_OT_channels_click(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Mouse Click on Channels";
    ot->idname = "ANIM_OT_channels_click";
    ot->description = "Handle mouse-clicks over animation channels";
  
    /* api callbacks */
    ot->invoke = animchannels_mouseclick_invoke;
    ot->poll = animedit_poll_channels_active;
  
    /* flags */
    ot->flag = OPTYPE_UNDO;
  
    /* properties */
    /* NOTE: don't save settings, otherwise, can end up with some weird behavior (sticky extend) */
    prop = RNA_def_boolean(ot->srna, "extend", false, "Extend Select", "");  // SHIFTKEY
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  
    prop = RNA_def_boolean(
        ot->srna, "children_only", false, "Select Children Only", "");  // CTRLKEY|SHIFTKEY
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  static bool select_anim_channel_keys(bAnimContext *ac, int channel_index, bool extend)
  {
    ListBase anim_data = {NULL, NULL};
    bAnimListElem *ale;
    int filter;
    bool success = false;
    FCurve *fcu;
    int i;
  
    /* get the channel that was clicked on */
    /* filter channels */
    filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_LIST_CHANNELS);
    ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
  
    /* get channel from index */
    ale = BLI_findlink(&anim_data, channel_index);
    if (ale == NULL) {
      /* channel not found */
      if (G.debug & G_DEBUG) {
        printf("Error: animation channel (index = %d) not found in rename_anim_channels()\n",
               channel_index);
      }
  
      ANIM_animdata_freelist(&anim_data);
      return false;
    }
  
    fcu = (FCurve *)ale->key_data;
    success = (fcu != NULL);
  
    ANIM_animdata_freelist(&anim_data);
  
    /* F-Curve may not have any keyframes */
    if (fcu && fcu->bezt) {
      BezTriple *bezt;
  
      if (!extend) {
        filter = (ANIMFILTER_DATA_VISIBLE);
        ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
        for (ale = anim_data.first; ale; ale = ale->next) {
          FCurve *fcu_inner = (FCurve *)ale->key_data;
  
          if (fcu_inner != NULL && fcu_inner->bezt != NULL) {
            for (i = 0, bezt = fcu_inner->bezt; i < fcu_inner->totvert; i++, bezt++) {
              bezt->f2 = bezt->f1 = bezt->f3 = 0;
            }
          }
        }
  
        ANIM_animdata_freelist(&anim_data);
      }
  
      for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
        bezt->f2 = bezt->f1 = bezt->f3 = SELECT;
      }
    }
  
    /* free temp data and tag for refresh */
    ED_region_tag_redraw(ac->ar);
    return success;
  }
  
  static int animchannels_channel_select_keys_invoke(bContext *C,
                                                     wmOperator *op,
                                                     const wmEvent *event)
  {
    bAnimContext ac;
    int channel_index;
    bool extend = RNA_boolean_get(op->ptr, "extend");
  
    /* get editor data */
    if (ANIM_animdata_get_context(C, &ac) == 0) {
      return OPERATOR_CANCELLED;
    }
  
    channel_index = animchannels_channel_get(&ac, event->mval);
  
    /* handle click */
    if (select_anim_channel_keys(&ac, channel_index, extend)) {
      WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_SELECTED, NULL);
      return OPERATOR_FINISHED;
    }
    else {
      /* allow event to be handled by selectall operator */
      return OPERATOR_PASS_THROUGH;
    }
  }
  
  static void ANIM_OT_channel_select_keys(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Select Channel Keyframes";
    ot->idname = "ANIM_OT_channel_select_keys";
    ot->description = "Select all keyframes of channel under mouse";
  
    /* api callbacks */
    ot->invoke = animchannels_channel_select_keys_invoke;
    ot->poll = animedit_poll_channels_active;
  
    prop = RNA_def_boolean(ot->srna, "extend", false, "Extend", "Extend selection");
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /* ************************************************************************** */
  /* Operator Registration */
  
  void ED_operatortypes_animchannels(void)
  {
    WM_operatortype_append(ANIM_OT_channels_select_all);
    WM_operatortype_append(ANIM_OT_channels_select_box);
  
    WM_operatortype_append(ANIM_OT_channels_click);
    WM_operatortype_append(ANIM_OT_channel_select_keys);
    WM_operatortype_append(ANIM_OT_channels_rename);
  
    WM_operatortype_append(ANIM_OT_channels_find);
  
    WM_operatortype_append(ANIM_OT_channels_setting_enable);
    WM_operatortype_append(ANIM_OT_channels_setting_disable);
    WM_operatortype_append(ANIM_OT_channels_setting_toggle);
  
    WM_operatortype_append(ANIM_OT_channels_delete);
  
    /* XXX does this need to be a separate operator? */
    WM_operatortype_append(ANIM_OT_channels_editable_toggle);
  
    WM_operatortype_append(ANIM_OT_channels_move);
  
    WM_operatortype_append(ANIM_OT_channels_expand);
    WM_operatortype_append(ANIM_OT_channels_collapse);
  
    WM_operatortype_append(ANIM_OT_channels_fcurves_enable);
  
    WM_operatortype_append(ANIM_OT_channels_clean_empty);
  
    WM_operatortype_append(ANIM_OT_channels_group);
    WM_operatortype_append(ANIM_OT_channels_ungroup);
  }
  
  // TODO: check on a poll callback for this, to get hotkeys into menus
  void ED_keymap_animchannels(wmKeyConfig *keyconf)
  {
    WM_keymap_ensure(keyconf, "Animation Channels", 0, 0);
  }
  
  /* ************************************************************************** */
  