/*
    This file is part of darktable,
    Copyright (C) 2011-2025 darktable developers.

    darktable 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 3 of the License, or
    (at your option) any later version.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "gui/accelerators.h"
#include "common/action.h"
#include "common/darktable.h"
#include "common/file_location.h"
#include "common/utility.h"
#include "control/control.h"
#include "gui/presets.h"
#include "dtgtk/expander.h"
#include "bauhaus/bauhaus.h"

#include <assert.h>
#include <gtk/gtk.h>
#include <math.h>

typedef struct dt_shortcut_t
{
  dt_view_type_flags_t views;
  gboolean is_default;

  dt_input_device_t key_device;
  guint key;
  guint mods;
  guint press     : 3;
  guint button    : 3;
  guint click     : 3;
  guint direction : 2;
  dt_input_device_t move_device;
  dt_shortcut_move_t move;

  dt_action_t *action;

  dt_action_element_t element;
  dt_action_effect_t effect;
  float speed;
  int instance; // 0 is from prefs, >0 counting from first, <0 counting from last
} dt_shortcut_t;

typedef enum dt_shortcut_category_t
{
  DT_SHORTCUT_CATEGORY_ACTIVE,
  DT_SHORTCUT_CATEGORY_OTHER,
  DT_SHORTCUT_CATEGORY_FALLBACKS,
  DT_SHORTCUT_CATEGORY_SPEED,
  DT_SHORTCUT_CATEGORY_DISABLED,
  DT_SHORTCUT_CATEGORY_LAST,
} dt_shortcut_category_t;

const gchar *_shortcut_category_label[]
  = { [DT_SHORTCUT_CATEGORY_ACTIVE   ] = N_("in active view"),
      [DT_SHORTCUT_CATEGORY_OTHER    ] = N_("in other views"),
      [DT_SHORTCUT_CATEGORY_FALLBACKS] = N_("fallbacks"),
      [DT_SHORTCUT_CATEGORY_SPEED    ] = N_("speed adjustments"),
      [DT_SHORTCUT_CATEGORY_DISABLED ] = N_("disabled defaults") };

static gboolean _is_shortcut_category(GSequenceIter *shortcut_iter)
{
  return GPOINTER_TO_UINT(shortcut_iter) < DT_SHORTCUT_CATEGORY_LAST;
}

typedef struct dt_device_key_t
{
  dt_input_device_t key_device;
  guint key;
  const dt_action_def_t *hold_def;
  dt_action_element_t hold_element;
} dt_device_key_t;

#define DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE 0
#define DT_SHORTCUT_DEVICE_TABLET 1

const char *move_string[]
  = { "",
      N_("scroll"),
      N_("pan"),
      N_("horizontal"),
      N_("vertical"),
      N_("diagonal"),
      N_("skew"),
      N_("leftright"),
      N_("updown"),
      N_("pgupdown"),
      NULL };

const struct _modifier_name
{
  GdkModifierType modifier;
  char           *name;
} modifier_string[]
  = { { GDK_SHIFT_MASK  , N_("shift") },
      { GDK_CONTROL_MASK, N_("ctrl" ) },
#ifdef __APPLE__
      { GDK_MOD1_MASK   , N_("option") },
#else
      { GDK_MOD1_MASK   , N_("alt"  ) },
#endif
      { GDK_MOD2_MASK   , N_("cmd"  ) },
      { GDK_MOD5_MASK   , N_("altgr") },
      { 0, NULL } };

static dt_shortcut_t _sc = { 0 };  //  shortcut under construction
static guint _previous_move = DT_SHORTCUT_MOVE_NONE;
static dt_action_t *_selected_action = NULL;
static dt_shortcut_t *_selected_shortcut = NULL;
static GQuark _action_quark = 0;

#define ELEMENT_IS(type, shortcut, elements) ((elements) && (elements)[(shortcut)->element].effects == dt_action_effect_##type)

const gchar *dt_action_effect_value[]
  = { N_("edit"),
      N_("up"),
      N_("down"),
      N_("reset"),
      N_("top"),
      N_("bottom"),
      N_("set"),
      NULL };

const gchar *dt_action_effect_selection[]
  = { N_("popup"),
      N_("next"),
      N_("previous"),
      N_("reset"),
      N_("last"),
      N_("first"),
      NULL };

const gchar *dt_action_effect_toggle[]
  = { N_("toggle"),
      N_("on"),
      N_("off"),
      N_("ctrl-toggle"),
      N_("ctrl-on"),
      N_("right-toggle"),
      N_("right-on"),
      NULL };

const gchar *dt_action_effect_hold[]
  = { N_("hold"),
      N_("on"),
      N_("off"),
      N_("toggle"),
      NULL };

const gchar *dt_action_effect_activate[]
  = { N_("activate"),
      N_("ctrl-activate"),
      N_("right-activate"),
      NULL };

const gchar *dt_action_effect_presets[]
  = { N_("show"),
      N_("previous"),
      N_("next"),
      N_("store"),
      N_("delete"),
      N_("edit"),
      N_("update"),
      N_("preferences"),
      NULL };

const gchar *dt_action_effect_preset_iop[]
  = { N_("apply"),
      N_("apply on new instance"),
      NULL };

const gchar *dt_action_effect_entry[]
  = { N_("focus"),
      N_("start"),
      N_("end"),
      N_("clear"),
      NULL };

const dt_action_element_def_t dt_action_elements_hold[]
  = { { NULL, dt_action_effect_hold } };

const dt_action_element_def_t _action_elements_toggle[]
  = { { NULL, dt_action_effect_toggle } };

const dt_action_element_def_t _action_elements_button[]
  = { { NULL, dt_action_effect_activate } };

const dt_action_element_def_t _action_elements_entry[]
  = { { NULL, dt_action_effect_entry } };

const dt_action_element_def_t _action_elements_value_fallback[]
  = { { NULL, dt_action_effect_value } };

static float _action_process_toggle(gpointer target,
                                    dt_action_element_t element,
                                    dt_action_effect_t effect,
                                    float move_size)
{
  float value = gtk_toggle_button_get_active(target);

  if(DT_ACTION_TOGGLE_NEEDED(effect, move_size, value)
     && gtk_widget_get_ancestor(target, GTK_TYPE_WINDOW))
  {
    GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS);
    event->button.state = (effect == DT_ACTION_EFFECT_TOGGLE_CTRL
                           || effect == DT_ACTION_EFFECT_ON_CTRL)
                        ? GDK_CONTROL_MASK : 0;
    event->button.button = (effect == DT_ACTION_EFFECT_TOGGLE_RIGHT
                            || effect == DT_ACTION_EFFECT_ON_RIGHT)
                         ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY;

    if(!gtk_widget_get_realized(target)) gtk_widget_realize(target);
    event->button.window = gtk_widget_get_window(target);
    g_object_ref(event->button.window);

    // some togglebuttons connect to the clicked signal, others to toggled or button-press-event
    // gtk_widget_event does not work when widgets are hidden in event boxes or some other conditions
    gboolean handled;
    g_signal_emit_by_name(G_OBJECT(target), "button-press-event", event, &handled);
    if(!handled) gtk_button_clicked(GTK_BUTTON(target));
    event->type = GDK_BUTTON_RELEASE;
    g_signal_emit_by_name(G_OBJECT(target), "button-release-event", event, &handled);

    gdk_event_free(event);

    value = gtk_toggle_button_get_active(target);

    if(!gtk_widget_is_visible(target))
      dt_action_widget_toast(NULL, target, value ? _("on") : _("off"));
  }

  return value;
}

static float _action_process_button(gpointer target,
                                    dt_action_element_t element,
                                    dt_action_effect_t effect,
                                    float move_size)
{
  dt_lib_gui_update(g_object_get_data(G_OBJECT(target), "module"));

  if(DT_PERFORM_ACTION(move_size)
     && gtk_widget_is_sensitive(target)
     && gtk_widget_get_ancestor(target, GTK_TYPE_WINDOW))
  {
    if(!gtk_widget_get_realized(target)) gtk_widget_realize(target);

    if(effect != DT_ACTION_EFFECT_ACTIVATE
      || !g_signal_handler_find(target, G_SIGNAL_MATCH_ID,
                                g_signal_lookup("clicked", gtk_button_get_type()),
                                0, NULL, NULL, NULL)
      || !gtk_widget_activate(GTK_WIDGET(target)))
    {
      GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS);
      event->button.state = effect == DT_ACTION_EFFECT_ACTIVATE_CTRL
                          ? GDK_CONTROL_MASK : 0;
      event->button.button = effect == DT_ACTION_EFFECT_ACTIVATE_RIGHT
                          ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY;

      event->button.window = gtk_widget_get_window(target);
      g_object_ref(event->button.window);

      gtk_widget_event(target, event);
      event->type = GDK_BUTTON_RELEASE;
      gtk_widget_event(target, event);

      gdk_event_free(event);
    }
  }

  return DT_ACTION_NOT_VALID;
}

static const gchar *_entry_set_element = NULL;

static float _action_process_entry(gpointer target,
                                   dt_action_element_t element,
                                   dt_action_effect_t effect,
                                   float move_size)
{
  GtkTextBuffer *buffer = GTK_IS_TEXT_VIEW(target)
                        ? gtk_text_view_get_buffer(target)
                        : NULL;
  if(DT_PERFORM_ACTION(move_size))
  {
    gint start_or_end = -1;
    switch(effect)
    {
    case DT_ACTION_EFFECT_FOCUS:
      gtk_widget_grab_focus(target);
      break;
    case DT_ACTION_EFFECT_START:
      start_or_end = 0;
    case DT_ACTION_EFFECT_END:
      gtk_widget_grab_focus(target);
      if(buffer)
      {
        GtkTextIter iter;
        gtk_text_buffer_get_iter_at_offset(buffer, &iter, start_or_end);
        gtk_text_buffer_place_cursor(buffer, &iter);
      }
      else
        gtk_editable_set_position(target, start_or_end);
      break;
    case DT_ACTION_EFFECT_CLEAR:
      if(buffer)
        gtk_text_buffer_set_text(buffer, "", 0);
      else
        gtk_editable_delete_text(target, 0, -1);
      break;
    case DT_ACTION_EFFECT_SET:;
      gint position = move_size;
      if(buffer)
      {
        GtkTextIter iter;
        gtk_text_buffer_get_iter_at_offset(buffer, &iter, move_size);
        gtk_text_buffer_insert(buffer, &iter, _entry_set_element, -1);
      }
      else
        gtk_editable_insert_text(target, _entry_set_element, -1, &position);
      break;
    }
  }
  else if(effect == DT_ACTION_EFFECT_SET)
  {
    if(buffer)
      gtk_text_buffer_set_text(buffer, _entry_set_element, -1);
    else
      gtk_entry_set_text(target, _entry_set_element);
  }

  return DT_ACTION_NOT_VALID;
}

static const dt_shortcut_fallback_t _action_fallbacks_toggle[]
  = { { .mods = GDK_CONTROL_MASK    , .effect = DT_ACTION_EFFECT_TOGGLE_CTRL  },
      { .button = DT_SHORTCUT_RIGHT , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT },
      { .press = DT_SHORTCUT_LONG   , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT },
      { } };

const dt_action_def_t dt_action_def_toggle
  = { N_("toggle"),
      _action_process_toggle,
      _action_elements_toggle,
      _action_fallbacks_toggle };

static const dt_shortcut_fallback_t _action_fallbacks_button[]
  = { { .mods = GDK_CONTROL_MASK    , .effect = DT_ACTION_EFFECT_ACTIVATE_CTRL  },
      { .button = DT_SHORTCUT_RIGHT , .effect = DT_ACTION_EFFECT_ACTIVATE_RIGHT },
      { .press = DT_SHORTCUT_LONG   , .effect = DT_ACTION_EFFECT_ACTIVATE_RIGHT },
      { } };

const dt_action_def_t dt_action_def_button
  = { N_("button"),
      _action_process_button,
      _action_elements_button,
      _action_fallbacks_button };

const dt_action_def_t dt_action_def_entry
  = { N_("entry"),
      _action_process_entry,
      _action_elements_entry };

static const dt_shortcut_fallback_t _action_fallbacks_value[]
  = { { .mods = GDK_CONTROL_MASK                  , .effect = -1, .speed = 0.1 },
      { .mods = GDK_SHIFT_MASK                    , .effect = -1, .speed = 10. },
      { .mods = GDK_CONTROL_MASK | GDK_SHIFT_MASK , .effect = -1, .speed = 10. },
      { .move = DT_SHORTCUT_MOVE_HORIZONTAL       , .effect = -1, .speed = 0.1 },
      { .move = DT_SHORTCUT_MOVE_VERTICAL         , .effect = -1, .speed = 10. },
      { .effect = DT_ACTION_EFFECT_RESET  , .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE },
      { .effect = DT_ACTION_EFFECT_TOP    , .button = DT_SHORTCUT_LEFT, .click = DT_SHORTCUT_DOUBLE, .move = DT_SHORTCUT_MOVE_VERTICAL },
      { } };

const dt_action_def_t dt_action_def_value
  = { N_("value"),
      NULL,
      _action_elements_value_fallback,
      _action_fallbacks_value };

const dt_action_def_t _action_def_dummy
  = { };

static dt_action_t *_action_find(const gchar *action_name)
{
  gchar **path = g_strsplit(action_name, "/", 0);
  for(gchar **cur = path; *cur; cur++)
  {
    gchar *d = *cur;
    for(gchar *c = *cur; *c; c++)
    {
      if(*c == '@')
        *d++ = *++c == ':' ? ';' : *c == '<' ? '/' : *c == '>' ? '\\' : '@';
      else
        *d++ = *c;
    }
    *d = 0;
  }
  dt_action_t *action = dt_action_locate(NULL, path, FALSE);
  g_strfreev(path);

  return action;
}

static const dt_action_def_t *_action_find_definition(dt_action_t *action)
{
  if(!action) return NULL;

  dt_action_type_t type = action->type != DT_ACTION_TYPE_FALLBACK
                        ? action->type : GPOINTER_TO_INT(action->target);
  const int index = type - DT_ACTION_TYPE_WIDGET - 1;

  if(index >= 0 && index < darktable.control->widget_definitions->len)
    return darktable.control->widget_definitions->pdata[index];
  else if(type == DT_ACTION_TYPE_IOP)
    return &dt_action_def_iop;
  else if(type == DT_ACTION_TYPE_LIB)
    return &dt_action_def_lib;
  else if(type == DT_ACTION_TYPE_VALUE_FALLBACK)
    return &dt_action_def_value;
  else
    return NULL;
}

static const dt_action_element_def_t *_action_find_elements(dt_action_t *action)
{
  const dt_action_def_t *definition = _action_find_definition(action);

  if(!definition)
    return NULL;
  else
    return definition->elements;
}

static const gchar *_action_find_effect_combo(dt_action_t *ac,
                                              const dt_action_element_def_t *el,
                                              dt_action_effect_t ef)
{
  if(el->effects == dt_action_effect_selection && ef > DT_ACTION_EFFECT_COMBO_SEPARATOR)
  {
    dt_introspection_type_enum_tuple_t *values
      = g_hash_table_lookup(darktable.bauhaus->combo_introspection, ac);
    if(values)
    {
      values += ef - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1;
      if(values->description)
        return values->description;
      else
        return values->name; // if not set up by introspection but for example in blend_gui
    }
    else
    {
      gchar **strings
        = g_hash_table_lookup(darktable.bauhaus->combo_list, ac);
      if(strings)
        return strings[ef - DT_ACTION_EFFECT_COMBO_SEPARATOR - 1];
      else
        return _("combo effect not found");
    }
  }

  return NULL;
}

dt_action_t *dt_action_widget(GtkWidget *widget)
{
  return widget ? g_object_get_qdata(G_OBJECT(widget), _action_quark) : NULL;
}

static gboolean _is_kp_key(guint keycode)
{
  return keycode >= GDK_KEY_KP_Space && keycode <= GDK_KEY_KP_Equal;
}

static gboolean _shortcut_is_speed(const dt_shortcut_t *s)
{
  return (!s->key_device && !s->key && !s->press && !s->move_device && !s->move
          && !s->button && !s->click && !s->mods);
}

static int _shortcut_category(const dt_shortcut_t *s,
                              dt_view_type_flags_t view)
{
  return !s || !s->views             ? DT_SHORTCUT_CATEGORY_DISABLED :
         _shortcut_is_speed(s)       ? DT_SHORTCUT_CATEGORY_SPEED :
         s->views & DT_VIEW_FALLBACK ? DT_SHORTCUT_CATEGORY_FALLBACKS :
         s->views & view             ? DT_SHORTCUT_CATEGORY_ACTIVE :
                                       DT_SHORTCUT_CATEGORY_OTHER;
}

static gint _shortcut_compare_func(gconstpointer shortcut_a,
                                   gconstpointer shortcut_b,
                                   gpointer user_data)
{
  const dt_shortcut_t *a = (const dt_shortcut_t *)shortcut_a;
  const dt_shortcut_t *b = (const dt_shortcut_t *)shortcut_b;

  dt_view_type_flags_t active_view = GPOINTER_TO_INT(user_data);
  const int a_category = _shortcut_category(a, active_view);
  const int b_category = _shortcut_category(b, active_view);

  if(a_category != b_category)
    return b_category - a_category;

  if(a_category == DT_SHORTCUT_CATEGORY_FALLBACKS
     && a->action && b->action
     && a->action->target != b->action->target)
    // order fallbacks by referred type
    return GPOINTER_TO_INT(a->action->target) - GPOINTER_TO_INT(b->action->target);

#define RETURN_DIFFERENCE(field) if(a->field != b->field) return a->field - b->field
  RETURN_DIFFERENCE(key_device);
  RETURN_DIFFERENCE(key);
  RETURN_DIFFERENCE(press);
  RETURN_DIFFERENCE(button);
  RETURN_DIFFERENCE(click);
  RETURN_DIFFERENCE(move_device);
  RETURN_DIFFERENCE(move);
  RETURN_DIFFERENCE(mods);

  if((a->direction | b->direction) == (DT_SHORTCUT_UP | DT_SHORTCUT_DOWN)
     && a->views == b->views)
    return a->direction - b->direction;

  if(a_category == DT_SHORTCUT_CATEGORY_DISABLED
     || a_category == DT_SHORTCUT_CATEGORY_SPEED)
    // Order disabled default shortcuts by action to immediately find correct one
    return GPOINTER_TO_INT(a->action) - GPOINTER_TO_INT(b->action);

  return 0;
};

static gchar *_action_full_id(dt_action_t *action)
{
  gchar *full_label = NULL;
  size_t owner_len = 0;
  size_t max_len = 2 * strlen(action->id) + 2;
  if(action->owner)
  {
    full_label = _action_full_id(action->owner);
    owner_len = strlen(full_label);
  }
  full_label = g_realloc(full_label, owner_len + max_len);
  gchar *d = full_label + owner_len;
  if(owner_len) *d++ = '/';
  for(const gchar *c = action->id; *c; c++)
  {
    if(strchr("@;/\\", *c))
    {
      *d++ = '@';
      *d++ = *c == ';' ? ':' : *c == '/' ? '<' : *c == '\\' ? '>' : '@';
    }
    else
      *d++ = *c;
  }
  *d = 0;
  return full_label;
}

static gchar *_action_full_label(dt_action_t *action)
{
  if(action->owner)
  {
    gchar *owner_label = _action_full_label(action->owner);
    gchar *full_label = g_strdup_printf("%s/%s", owner_label, action->label);
    g_free(owner_label);
    return full_label;
  }
  else
    return g_strdup(action->label);
}

static void _action_distinct_label(gchar **label, dt_action_t *action, gchar *instance)
{
  if(!action || action->type <= DT_ACTION_TYPE_GLOBAL)
    return;

  gchar *instance_label = action->type == DT_ACTION_TYPE_IOP && *instance
                        ? g_strdup_printf("%s • %s", action->label, instance)
                        : g_strdup(action->label);

  if(*label)
  {
    if(!strstr(action->label, *label) || *instance)
    {
      gchar *distinct_label = g_strdup_printf("%s / %s", instance_label, *label);
      g_free(*label);
      *label = distinct_label;
    }

    g_free(instance_label);
  }
  else
    *label = instance_label;

  _action_distinct_label(label, action->owner, instance);
}

static void _dump_actions(FILE *f, dt_action_t *action)
{
  while(action)
  {
    gchar *label = _action_full_id(action);
    fprintf(f, "%s %s %d\n", label, !action->target ? "*" : "", action->type);
    g_free(label);
    if(action->type <= DT_ACTION_TYPE_SECTION)
      _dump_actions(f, action->target);
    action = action->next;
  }
}

dt_input_device_t dt_register_input_driver(dt_lib_module_t *module,
                                           const dt_input_driver_definition_t *callbacks)
{
  dt_input_device_t id = 10;

  for(GSList *d = darktable.control->input_drivers; d; d = d->next, id += 10)
    if(((dt_input_driver_definition_t *)d->data)->module == module) return id;

  dt_input_driver_definition_t *new_driver = calloc(1, sizeof(dt_input_driver_definition_t));
  *new_driver = *callbacks;
  new_driver->module = module;
  darktable.control->input_drivers
    = g_slist_append(darktable.control->input_drivers, (gpointer)new_driver);

  return id;
}

#define DT_MOVE_NAME -1
static gchar *_shortcut_key_move_name(dt_input_device_t id,
                                      guint key_or_move,
                                      guint mods,
                                      gboolean display)
{
  gchar *name = NULL, *post_name = NULL;
  if(id == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE)
  {
    if(mods == DT_MOVE_NAME)
      return g_strdup(display && key_or_move != 0
                      ? _(move_string[key_or_move])
                      : move_string[key_or_move]);
    else
    {
      if(display)
      {
        gchar *key_name = gtk_accelerator_get_label(key_or_move, 0);
        post_name = g_utf8_strdown(key_name, -1);
        if(strlen(post_name) == 1 && _is_kp_key(key_or_move))
          dt_util_str_cat(&post_name, " %s", _("(keypad)"));
        g_free(key_name);
      }
      else
        name = key_or_move ? gtk_accelerator_name(key_or_move, 0) : g_strdup("None");
    }
  }
  else if(id == DT_SHORTCUT_DEVICE_TABLET)
  {
    return g_strdup_printf("%s %u", display ? _("tablet button") : "tablet button", key_or_move);
  }
  else
  {
    GSList *driver = darktable.control->input_drivers;
    while(driver && (id -= 10) >= 10)
      driver = driver->next;

    if(!driver)
      name = g_strdup(_("unknown driver"));
    else
    {
      dt_input_driver_definition_t *callbacks = driver->data;
      gchar *without_device
        = mods == DT_MOVE_NAME
        ? callbacks->move_to_string(key_or_move, display)
        : callbacks->key_to_string(key_or_move, display);

      if(display && id == 0)
        post_name = without_device;
      else
      {
        char id_str[2] = "\0\0";
        if(id) id_str[0] = '0' + id;

        name = g_strdup_printf("%s%s:%s", display ? "" : callbacks->name, id_str, without_device);
        g_free(without_device);
      }
    }
  }
  if(mods != DT_MOVE_NAME)
  {
    for(const struct _modifier_name *mod_str = modifier_string;
        mod_str->modifier;
        mod_str++)
    {
      if(mods & mod_str->modifier)
      {
        gchar *save_name = name;
        name = display
             ? g_strdup_printf("%s%s+", name ? name : "", _(mod_str->name))
             : g_strdup_printf("%s;%s", name ? name : "",   mod_str->name);
        g_free(save_name);
      }
    }
  }

  if(post_name)
  {
    gchar *save_name = name;
    name = g_strdup_printf("%s%s", name ? name : "", post_name);
    g_free(save_name);
    g_free(post_name);
  }

  return name;
}

static gboolean _shortcut_is_move(const dt_shortcut_t *s)
{
  return (s->move_device != DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE ||
          s->move != DT_SHORTCUT_MOVE_NONE) && !s->direction;
}

static dt_action_effect_t _shortcut_default_effect(const dt_shortcut_t *s)
{
  return _shortcut_is_move(s) ? DT_ACTION_EFFECT_DEFAULT_MOVE
                              : DT_ACTION_EFFECT_DEFAULT_KEY;
}

static gchar *_shortcut_description(dt_shortcut_t *s)
{
  static gchar hint[1024];
  int length = 0;

#define add_hint(format, ...) length += length >= sizeof(hint) ? 0 : snprintf(hint + length, sizeof(hint) - length, format, ##__VA_ARGS__)

  gchar *key_name = _shortcut_key_move_name(s->key_device, s->key, s->mods, TRUE);
  gchar *move_name = _shortcut_key_move_name(s->move_device, s->move, DT_MOVE_NAME, TRUE);

  add_hint("%s%s", key_name, s->key_device || s->key ? "" : move_name);

  if(s->press & DT_SHORTCUT_LONG  ) add_hint(" %s", _("long"));
  if(s->press & DT_SHORTCUT_DOUBLE) add_hint(" %s", _("double-press")); else
  if(s->press & DT_SHORTCUT_TRIPLE) add_hint(" %s", _("triple-press")); else
  if(s->press) add_hint(" %s", _("press"));
  if(s->button)
  {
    if(*key_name || *move_name) add_hint(",");
    if(s->button & DT_SHORTCUT_LEFT  ) add_hint(" %s", C_("accel", "left"));
    if(s->button & DT_SHORTCUT_RIGHT ) add_hint(" %s", C_("accel", "right"));
    if(s->button & DT_SHORTCUT_MIDDLE) add_hint(" %s", C_("accel", "middle"));
    if(s->click  & DT_SHORTCUT_LONG  ) add_hint(" %s", C_("accel", "long"));
    if(s->click  & DT_SHORTCUT_DOUBLE) add_hint(" %s", C_("accel", "double-click")); else
    if(s->click  & DT_SHORTCUT_TRIPLE) add_hint(" %s", C_("accel", "triple-click")); else
      add_hint(" %s", _("click"));
  }

  if(*move_name && (s->key_device || s->key))
    add_hint(", %s", move_name);
  if(s->direction)
    add_hint(", %s", s->direction == DT_SHORTCUT_UP ? _("up") : _("down"));

  g_free(key_name);
  g_free(move_name);

  return hint + (hint[0] == ' ' ? 1 : 0);
}

static gchar *_action_description(dt_shortcut_t *s, int components)
{
  static gchar hint[1024];
  int length = 0;
  hint[0] = 0;

  if(components == 2)
  {
    gchar *action_label = _action_full_label(s->action);
    add_hint("%s", action_label);
    g_free(action_label);
  }

  if(s->instance == 1)
    add_hint(", %s", _("first instance"));
  else if(s->instance == -1)
    add_hint(", %s", _("last instance"));
  else if(s->instance != 0)
    add_hint(", %s %+d", _("relative instance"), s->instance);

  const dt_action_def_t *def = _action_find_definition(s->action);

  if(def && def->elements)
  {
    if(components && (s->element || (!def->fallbacks && def->elements->name)))
      add_hint(", %s", _(def->elements[s->element].name));
    const gchar *cef = _action_find_effect_combo(s->action, &def->elements[s->element], s->effect);
    if(cef || s->effect > _shortcut_default_effect(s))
      add_hint(", %s", Q_(cef ? cef : def->elements[s->element].effects[s->effect]));
  }

  if(def && ELEMENT_IS(value, s, def->elements) && s->effect == DT_ACTION_EFFECT_SET)
    add_hint(" = %g", s->speed);
  else if(s->speed != 1.0)
    add_hint("%s%s *%g", !_shortcut_is_speed(s) || components == 2 ? ", " : "",
                         _("speed"), s->speed);

#undef add_hint

  return hint;
}

static void _insert_shortcut_in_list(GHashTable *ht,
                                     char *shortcut,
                                     dt_action_t *ac,
                                     char *label)
{
  if(ac->owner && ac->owner->owner)
    _insert_shortcut_in_list(ht, shortcut, ac->owner,
                             g_strdup_printf("%s/%s", ac->owner->label, label));
  GtkListStore *list_store = g_hash_table_lookup(ht, ac->owner);
  if(!list_store)
  {
    list_store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_STRING);
    g_hash_table_insert(ht, ac->owner, list_store);
  }

  gtk_list_store_insert_with_values(list_store, NULL, -1, 0, shortcut, 1, label, -1);

  g_free(label);
}

GHashTable *dt_shortcut_category_lists(dt_view_type_flags_t v)
{
  GHashTable *ht = g_hash_table_new(NULL, NULL);

  for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
      !g_sequence_iter_is_end(iter);
      iter = g_sequence_iter_next(iter))
  {
    dt_shortcut_t *s = g_sequence_get(iter);
    if(s && s->views & v && !_shortcut_is_speed(s))
      _insert_shortcut_in_list(ht, _shortcut_description(s), s->action, g_strdup_printf("%s%s", s->action->label, _action_description(s, 1)));
  }

  return ht;
}

static gboolean _find_relative_instance(dt_action_t *action,
                                        GtkWidget *widget,
                                        int *instance)
{
  dt_action_t *owner = action;
  while(owner && owner->type != DT_ACTION_TYPE_IOP) owner = owner->owner;

  dt_iop_module_so_t *module = (dt_iop_module_so_t *)owner;
  if(!owner || owner == &darktable.control->actions_focus
     || (module->flags() & IOP_FLAGS_ONE_INSTANCE))
    return FALSE;

  if(!widget || action->target == widget) return TRUE;

  GtkWidget *expander = gtk_widget_get_ancestor(widget, DTGTK_TYPE_EXPANDER);

  dt_iop_module_t *preferred = dt_iop_get_module_preferred_instance(module);

  if(preferred && expander != preferred->expander)
  {
    int current_instance = 0;
    for(GList *iop_mods = darktable.develop->iop;
        iop_mods;
        iop_mods = g_list_next(iop_mods))
    {
      const dt_iop_module_t *mod = iop_mods->data;

      if(mod->so == module && mod->iop_order != INT_MAX)
      {
        current_instance++;

        if(mod->expander == expander)
          *instance = current_instance; // and continue counting
      }
    }

    if(current_instance + 1 - *instance < *instance) *instance -= current_instance + 1;
  }

  return TRUE;
}

static gchar *_shortcut_lua_command(GtkWidget *widget,
                                    dt_shortcut_t *s)
{
  const dt_action_element_def_t *elements = _action_find_elements(s->action);

  if(!s->action || s->action->owner == &darktable.control->actions_fallbacks
     || !(elements || s->action->type == DT_ACTION_TYPE_COMMAND
          || s->action->type == DT_ACTION_TYPE_PRESET))
    return NULL;

  gchar instance_string[5] = ""; // longest is ", -9"
  if(_find_relative_instance(s->action, widget, &s->instance))
    g_snprintf(instance_string, sizeof(instance_string), ", %d", s->instance);

  int elem = 0;
  while(elements && elements[0].name && elem < s->element && elements[elem + 1].name) elem++;

  if(DT_IS_BAUHAUS_WIDGET(widget) && s->element == DT_ACTION_ELEMENT_DEFAULT)
  {
    if(dt_bauhaus_widget_get_type(widget) == DT_BAUHAUS_COMBOBOX)
    {
      int value = GPOINTER_TO_INT(dt_bauhaus_combobox_get_data(widget));
      dt_introspection_type_enum_tuple_t *values
        = g_hash_table_lookup(darktable.bauhaus->combo_introspection, s->action);
      for(int i = 0; values && values->name; values++, i++)
      {
        if(values->value == value)
        {
          value = i;
          break;
        }
      }
      s->effect = DT_ACTION_EFFECT_COMBO_SEPARATOR + 1 + value;
    }
    else
    {
      s->effect = DT_ACTION_EFFECT_SET;
      s->speed = dt_bauhaus_slider_get(widget);
    }
  }

  const gchar *cef = elements ? _action_find_effect_combo(s->action, &elements[elem], s->effect) : NULL;
  const gchar *el = elements ? elements[elem].name : NULL;
  const gchar **ef = elements && s->effect >= 0 ? elements[elem].effects : NULL;
  const gchar *quo = elements ? "\", \"" : "";

  return g_strdup_printf("dt.gui.action(\"%s%s%s%s%s%s\", %.3f%s)\n",
                         _action_full_id(s->action), quo, el ? el : "", quo,
                         cef ? "item:" : "", cef ? NQ_(cef) : ef ? NQ_(ef[s->effect]) : "",
                         s->speed, instance_string);
}

void _shortcut_copy_lua(GtkWidget *widget, dt_shortcut_t *shortcut, gchar *preset_name)
{
  gchar *lua_command = _shortcut_lua_command(widget, shortcut);
  if(!lua_command) return;
  gtk_clipboard_set_text(gtk_clipboard_get_default(gdk_display_get_default()), lua_command, -1);
  dt_control_log(_("Lua script command copied to clipboard:\n\n<tt>%s</tt>"), lua_command);
  g_free(lua_command);
}

void dt_shortcut_copy_lua(dt_action_t *action, gchar *preset_name)
{
  GtkWidget *widget = NULL;
  dt_shortcut_t shortcut = { .speed = 1.0 };

  if(!action)
  {
    if(preset_name)
      shortcut.action = dt_action_locate(&darktable.control->actions_global,
                                         (gchar *[]){"styles", (gchar *)preset_name, NULL}, FALSE);
    else
    {
      widget = darktable.control->mapping_widget;
      shortcut.action = dt_action_widget(widget);
      shortcut.element = darktable.control->element;
    }
  }
  else
  {
    if(action->type == DT_ACTION_TYPE_IOP_INSTANCE)
      action = &((dt_iop_module_t*)action)->so->actions;
    shortcut.action = dt_action_locate(action, (gchar *[]){"preset", preset_name, NULL}, FALSE);
  }

  _shortcut_copy_lua(widget, &shortcut, preset_name);
}

static void _tooltip_reposition(GtkWidget *widget,
                                GdkRectangle *allocation,
                                gpointer user_data)
{
  GdkWindow *window = gtk_widget_get_window(gtk_widget_get_toplevel(widget));
  if(!window) return;

  gint wx, wy, width = gdk_window_get_width(window);
  gdk_window_get_origin(window, &wx, &wy);

  GdkRectangle workarea;
  gdk_monitor_get_workarea(gdk_display_get_monitor_at_window(gdk_window_get_display(window),
                                                             window),
                           &workarea);

  const gint nx = CLAMP(wx, workarea.x, workarea.x + workarea.width - width);

  if(nx != wx)
    gdk_window_move(window, nx, wy);
}

gboolean dt_shortcut_tooltip_callback(GtkWidget *widget,
                                      gint x,
                                      gint y,
                                      gboolean keyboard_mode,
                                      GtkTooltip *tooltip,
                                      GtkWidget *vbox)
{
  GtkWindow *top = GTK_WINDOW(gtk_widget_get_toplevel(widget));
  if(!gtk_window_is_active(top)
     && gtk_window_get_window_type(top) != GTK_WINDOW_POPUP)
    return FALSE;

  if(dt_key_modifier_state() & (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK|GDK_BUTTON3_MASK
                               |GDK_SHIFT_MASK|GDK_CONTROL_MASK|GDK_MOD1_MASK)
     || darktable.bauhaus->current
     || darktable.gui->hide_tooltips)
    return FALSE;

  gchar *markup_text = NULL;
  gchar *description = NULL;
  dt_action_t *action = NULL;
  dt_action_def_t const *def = NULL;
  int show_element = 0;
  dt_shortcut_t lua_shortcut = { .speed = 1.0 };

  gchar *original_markup = dt_bauhaus_widget_get_tooltip_markup(widget, x, y);
  const gchar *widget_name = gtk_widget_get_name(widget);

  if(!strcmp(widget_name, "actions_view") || !strcmp(widget_name, "shortcuts_view"))
  {
    if(!gtk_widget_is_sensitive(widget)) return FALSE;

    show_element = 1;

    GtkTreePath *path = NULL;
    GtkTreeModel *model;
    GtkTreeIter iter;
    if(!gtk_tree_view_get_tooltip_context(GTK_TREE_VIEW(widget), &x, &y,
                                          keyboard_mode, &model, &path, &iter))
      return FALSE;

    gtk_tree_view_set_tooltip_row(GTK_TREE_VIEW(widget), tooltip, path);
    gtk_tree_path_free(path);


    if(!strcmp(widget_name, "shortcuts_view"))
    {
      GSequenceIter  *shortcut_iter = NULL;
      gtk_tree_model_get(model, &iter, 0, &shortcut_iter, -1);
      markup_text = g_markup_printf_escaped
        ("%s%s%s",
         _("start typing for incremental search"),
         !_selected_shortcut ? "" :
         !_selected_shortcut->is_default
         ? _("\npress Delete to delete selected shortcut")
         : _selected_shortcut->views
         ? _("\npress Delete to disable selected default shortcut")
         : _("\npress Delete to restore selected default shortcut"),
         _is_shortcut_category(shortcut_iter) ? "" :
         _("\ndouble-click to add new shortcut"));

      if(!_is_shortcut_category(shortcut_iter))
        lua_shortcut = *(dt_shortcut_t*)g_sequence_get(shortcut_iter);
    }
    else
    {
      gtk_tree_model_get(model, &iter, 0, &action, -1);
      def = _action_find_definition(action);
      markup_text = g_markup_printf_escaped
        ("%s\n%s%s%s%s%s",
         _("start typing for incremental search"),
         _("click to filter shortcuts list"),
         _selected_shortcut ?
         _("\nright-click to show action of selected shortcut")
         : "",
         def || action->type > DT_ACTION_TYPE_SECTION ?
         _("\ndouble-click to define new shortcut")
         : "",
         def ?
         "\n\nmultiple shortcuts can be defined for the same action;"
         "\na different element, effect, speed or instance can be set for each in the shortcuts list."
         : "",
         def && def->fallbacks && action->type != DT_ACTION_TYPE_FALLBACK ?
         "\n\nwith fallbacks enabled, the same shortcut can be used with additional modifiers"
         "\nor mouse scroll/clicks/moves to affect a different element or change the effect or speed."
         : "");
    }
  }
  else
  {
    if(g_object_get_data(G_OBJECT(widget), "scroll-resize-tooltip"))
      dt_util_str_cat(&original_markup, "%s%s",
                                        original_markup ? "\n" : "", _("shift+alt+scroll to change height"));
    action = dt_action_widget(widget);
    if(!action)
    {
      widget = gtk_widget_get_parent(widget);
      action = dt_action_widget(widget);
      show_element = -1; // for notebook tabs
    }

    if(darktable.control->element > 0)
      lua_shortcut.element = darktable.control->element;

    if(darktable.control->mapping_widget == widget)
    {

      const int add_remove_qap = darktable.develop
        ? dt_dev_modulegroups_basics_module_toggle(darktable.develop, widget, FALSE)
        : 0;

      markup_text = g_markup_printf_escaped("%s\n%s\n%s%s\n%s",
                                            _("press keys with mouse click and scroll or move combinations to create a shortcut"),
                                            _("click to open shortcut configuration"),
                                            add_remove_qap == CPF_DIRECTION_UP   ? _("ctrl+click to add to quick access panel\n") :
                                            add_remove_qap == CPF_DIRECTION_DOWN ? _("ctrl+click to remove from quick access panel\n")  : "",
                                            _("scroll to change default speed"),
                                            _("right-click to exit mapping mode"));
    }
  }

  if(!def) def = _action_find_definition(action);
  const gboolean has_fallbacks = def && def->fallbacks;

  const gchar *element_name = NULL;
  if(def)
  {
    for(int i = 0; i <= lua_shortcut.element; i++)
    {
      element_name = def->elements[i].name;
      if(!element_name) break;
    }
    if(element_name
       && (lua_shortcut.element || !has_fallbacks)
       && show_element == 0
       && darktable.control->element != -1)
      description = g_markup_escape_text(_(element_name), -1);
  }

  const dt_view_type_flags_t current_view = dt_view_get_current();
  int num_shortcuts = 0;
  for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
      !g_sequence_iter_is_end(iter);
      iter = g_sequence_iter_next(iter))
  {
    dt_shortcut_t *s = g_sequence_get(iter);
    if(!(s->views & current_view)) continue;

    if(s->action == action &&
       (!def || darktable.control->element == -1 ||
        s->element == darktable.control->element ||
        (s->element == DT_ACTION_ELEMENT_DEFAULT && has_fallbacks)))
    {
      num_shortcuts++;
      gchar *sc_escaped = g_markup_escape_text(_shortcut_description(s), -1);
      const int components = (show_element > 0 || s->element != darktable.control->element) ? 1 : 0;
      gchar *ac_escaped = g_markup_escape_text(_action_description(s, components), -1);
      dt_util_str_cat(&description, "%s<b><big>%s</big></b><i>%s</i>",
                                    description ? "\n" : "",
                                    sc_escaped, ac_escaped);
      g_free(sc_escaped);
      g_free(ac_escaped);
    }
  }

  if(!num_shortcuts && original_markup && darktable.control->mapping_widget != widget)
    g_clear_pointer(&description, g_free);

#ifdef USE_LUA
  if(markup_text)
  {
    if(action) lua_shortcut.action = action;
    gchar *lua_command = _shortcut_lua_command(widget, &lua_shortcut);
    if(lua_command)
    {
      gchar *lua_escaped = g_markup_printf_escaped("\n\nLua: <tt>%s</tt>%s %s", lua_command,
                                    show_element == 1 ? _("ctrl+v") : _("right long click") , _("to copy Lua command"));
      dt_util_str_cat(&markup_text, "%s", lua_escaped);
      g_free(lua_escaped);
      g_free(lua_command);
    }
  }
#endif

  if(description || original_markup || markup_text)
  {
    if(original_markup) dt_util_str_cat(&markup_text, markup_text ? "\n\n%s" : "%s", original_markup);
    if(description    ) dt_util_str_cat(&markup_text, markup_text ? "\n\n%s" : "%s", description);

    GtkWidget *label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), markup_text);
    gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
    if(original_markup && !strchr(original_markup,'\n'))
      gtk_label_set_max_width_chars(GTK_LABEL(label), 70);
    gtk_widget_set_halign(label, GTK_ALIGN_START);

    g_free(markup_text);
    g_free(original_markup);
    g_free(description);

    if(vbox)
      dt_gui_box_add(vbox, label);
    else
      vbox = label;
  }

  if(!vbox) return FALSE;

  gtk_widget_show_all(vbox);
  gtk_tooltip_set_custom(tooltip, vbox);
  g_signal_connect(G_OBJECT(vbox), "size-allocate", G_CALLBACK(_tooltip_reposition), widget);

  return TRUE;
}

static dt_view_type_flags_t _find_views(dt_action_t *action)
{
  dt_view_type_flags_t vws = 0;

  dt_action_t *owner = action;
  while(owner && owner->type >= DT_ACTION_TYPE_SECTION) owner = owner->owner;

  if(owner)

  switch(owner->type)
  {
  case DT_ACTION_TYPE_IOP:
    vws = DT_VIEW_DARKROOM;
    break;
  case DT_ACTION_TYPE_VIEW:;
    dt_view_t *view = (dt_view_t *)owner;
    vws = view->view(view);
    break;
  case DT_ACTION_TYPE_LIB:;
    dt_lib_module_t *lib = (dt_lib_module_t *)owner;
    vws = lib->views(lib);
    break;
  case DT_ACTION_TYPE_BLEND:
    vws = DT_VIEW_DARKROOM;
    break;
  case DT_ACTION_TYPE_CATEGORY:
    if(owner == &darktable.control->actions_fallbacks)
      vws = DT_VIEW_FALLBACK;
    else if(owner == &darktable.control->actions_lua)
      vws = DT_VIEW_ALL;
    else if(owner == &darktable.control->actions_thumb)
    {
      vws = DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING | DT_VIEW_PRINT;
      if(!g_ascii_strcasecmp(action->id,"rating")
         || !g_ascii_strcasecmp(action->id,"color label"))
        vws |= DT_VIEW_LIGHTTABLE; // lighttable has copy/paste history shortcuts in separate lib
    }
    else
      dt_print(DT_DEBUG_ALWAYS,
               "[find_views] views for category '%s' unknown", owner->id);
    break;
  case DT_ACTION_TYPE_GLOBAL:
    vws = DT_VIEW_ALL;
    break;
  default:
    break;
  }

  return vws;
}

static GtkTreeStore *_shortcuts_store = NULL;
static GtkTreeStore *_actions_store = NULL;
static GtkWidget *_grab_widget = NULL, *_grab_window = NULL;

static void _shortcuts_store_category(GtkTreeIter *category,
                                      dt_shortcut_t *s,
                                      dt_view_type_flags_t view)
{
  gtk_tree_model_iter_nth_child(GTK_TREE_MODEL(_shortcuts_store), category, NULL,
                                _shortcut_category(s, view));
}

static gboolean _remove_shortcut_from_store(GtkTreeModel *model,
                                            GtkTreePath *path,
                                            GtkTreeIter *iter,
                                            gpointer data)
{
  gpointer iter_data;
  gtk_tree_model_get(model, iter, 0, &iter_data, -1);
  if(iter_data == data)
  {
    gtk_tree_store_remove(GTK_TREE_STORE(model), iter);
    return TRUE;
  }

  return FALSE;
}

static gboolean _insert_shortcut(dt_shortcut_t *shortcut,
                                 const gboolean confirm,
                                 const gboolean disable);

static void _remove_shortcut(GSequenceIter *shortcut)
{
  dt_shortcut_t *s = g_sequence_get(shortcut);
  if(!s) return;

  _selected_shortcut = NULL;

  gboolean disabled = s->views == DT_VIEW_NONE;
  if(s->is_default)
  {
    _insert_shortcut(s, disabled, !disabled);
    return;
  }

  if(_shortcuts_store)
    gtk_tree_model_foreach(GTK_TREE_MODEL(_shortcuts_store),
                           _remove_shortcut_from_store, shortcut);

  if(s->direction) // was this a split move?
  {
    // unsplit the other half of the move
    s->direction = 0;
    dt_shortcut_t *o = g_sequence_get(g_sequence_iter_prev(shortcut));
    if(g_sequence_iter_is_begin(shortcut)
       || _shortcut_compare_func(s, o, GINT_TO_POINTER(s->views)))
      o = g_sequence_get(g_sequence_iter_next(shortcut));
    o->direction = 0;
  }

  g_sequence_remove(shortcut);
}

static void _add_shortcut(dt_shortcut_t *shortcut,
                          const dt_view_type_flags_t view)
{
  GSequenceIter *new_shortcut =
    g_sequence_insert_sorted(darktable.control->shortcuts, shortcut,
                             _shortcut_compare_func, GINT_TO_POINTER(view));

  GtkTreeModel *model = GTK_TREE_MODEL(_shortcuts_store);
  if(model)
  {
    GSequenceIter *prev_shortcut = g_sequence_iter_prev(new_shortcut);
    GSequenceIter *seq_iter = NULL;
    GtkTreeIter category, child;
    _shortcuts_store_category(&category, shortcut, view);

    gint position = 1, found = 0;
    if(gtk_tree_model_iter_children(model, &child, &category))
    do
    {
      gtk_tree_model_get(model, &child, 0, &seq_iter, -1);
      if(seq_iter == prev_shortcut)
      {
        found = position;
        break;
      }
      position++;
    } while(gtk_tree_model_iter_next(model, &child));

    gtk_tree_store_insert_with_values(_shortcuts_store, NULL, &category,
                                      found, 0, new_shortcut, -1);
  }

  if(shortcut->views != DT_VIEW_NONE)
  {
    dt_shortcut_t find_disabled = *shortcut;
    find_disabled.is_default = TRUE;
    find_disabled.views = DT_VIEW_NONE;
    GSequenceIter *found_disabled =
      g_sequence_lookup(darktable.control->shortcuts,
                        &find_disabled, _shortcut_compare_func, NULL);
    if(found_disabled)
    {
      shortcut->is_default = TRUE;
      dt_shortcut_t *found_shortcut = g_sequence_get(found_disabled);
      found_shortcut->is_default = FALSE;
      _remove_shortcut(found_disabled);
    }
  }
}

static void _shortcut_row_inserted(GtkTreeModel *tree_model,
                                   GtkTreePath *path,
                                   GtkTreeIter *iter,
                                   gpointer view)
{
  // connect to original store, not filtered one, because otherwise
  // view not sufficiently updated to expand

  GtkTreePath *filter_path = gtk_tree_model_filter_convert_child_path_to_path
    (GTK_TREE_MODEL_FILTER(gtk_tree_view_get_model(view)),
     path);
  if(!filter_path)
    return;

  gtk_tree_view_expand_to_path(view, filter_path);
  gtk_tree_view_scroll_to_cell(view, filter_path, NULL, TRUE, 0.5, 0);
  gtk_tree_view_set_cursor(view, filter_path, NULL, FALSE);
  gtk_tree_path_free(filter_path);
}

static gboolean _insert_shortcut(dt_shortcut_t *shortcut,
                                 const gboolean confirm,
                                 const gboolean disable)
{
  if((!shortcut->speed && shortcut->effect != DT_ACTION_EFFECT_SET)
         || (shortcut->action->type == DT_ACTION_TYPE_SECTION // styles
         && shortcut->action->owner == &darktable.control->actions_global))
    return FALSE;

  dt_shortcut_t *s = calloc(sizeof(dt_shortcut_t), 1);
  *s = *shortcut;
  const dt_view_type_flags_t real_views = s->views = _find_views(s->action);

  const dt_view_type_flags_t view = dt_view_get_current();

  guint replaced_direction = 0;

  // check (and remove if confirmed) clashes in current and other views
  gboolean remove_existing = !confirm;
  do
  {
    gchar *existing_labels = NULL;
    do
    {
      GSequenceIter *existing =
        g_sequence_lookup(darktable.control->shortcuts, s,
                          _shortcut_compare_func, GINT_TO_POINTER(view));
      if(existing) // at least one found
      {
        // go to first one that has same shortcut
        while(!g_sequence_iter_is_begin(existing)
              && !_shortcut_compare_func(s, g_sequence_get(g_sequence_iter_prev(existing)),
                                         GINT_TO_POINTER(view)))
          existing = g_sequence_iter_prev(existing);

        do
        {
          GSequenceIter *saved_next = g_sequence_iter_next(existing);

          dt_shortcut_t *e = g_sequence_get(existing);

          if(e->views == s->views
             && _shortcut_is_move(e)
             && e->effect != DT_ACTION_EFFECT_DEFAULT_MOVE
             && s->direction
             && !(confirm && remove_existing)
             && (!confirm ||
                 dt_gui_show_yes_no_dialog
                   (_("shortcut for move exists with single effect"),
                    "",
                    _("%s\n\n(assign %s to %s)"),
                    _("create separate shortcuts for up and down move?"),
                    _action_description(e, 2),
                    s->direction == DT_SHORTCUT_UP ? _("down") : _("up"))))
          {
            e->direction = (DT_SHORTCUT_UP | DT_SHORTCUT_DOWN) ^ s->direction;
            if(s->effect == DT_ACTION_EFFECT_DEFAULT_MOVE)
              s->effect = DT_ACTION_EFFECT_DEFAULT_KEY;
            _add_shortcut(s, view);
            return TRUE;
          }
          else if(e->action == s->action)
          {
            if(_shortcut_is_speed(e))
            {
              // adjust if ui action, overwrite on import
              if(confirm)
                shortcut->speed = s->speed = roundf(s->speed * e->speed * 1000.) / 1000.;
              if(fabsf(s->speed) >= .001 && fabsf(s->speed) <= 1000.)
              {
                _remove_shortcut(existing);
                if(s->speed != 1.0)
                {
                  _add_shortcut(s, view);
                  return TRUE;
                }
                else
                  dt_control_log(_("%s, speed reset"), _action_description(s, 2));
              }
            }
            else if(disable && e->is_default)
            {
              *s = *e;
              e->is_default = FALSE;
              _remove_shortcut(existing);
              s->views = DT_VIEW_NONE;
              _add_shortcut(s, view);
              return TRUE;
            }
            else if(e->element  != s->element ||
                    e->effect   != s->effect  ||
                    e->speed    != s->speed   ||
                    e->instance != s->instance )
            {
              if(!confirm ||
                 dt_gui_show_yes_no_dialog
                 (_("shortcut exists with different settings"), "",
                  _("reset the settings of the shortcut?")))
              {
                *e = *s;
              }
            }
            else
            {
              // there should be no other clashes because same mapping already existed
              if(confirm &&
                 dt_gui_show_yes_no_dialog(_("shortcut already exists"), "",
                                           e->is_default ?
                                           _("disable this default shortcut?") :
                                           _("remove the shortcut?")))
              {
                _remove_shortcut(existing);
              }
            }
            g_free(s);
            return FALSE;
          }

          if(e->views & real_views || real_views == DT_VIEW_NONE) // overlap
          {
            if(remove_existing)
            {
              replaced_direction |= e->direction;
              e->direction = 0;
              _remove_shortcut(existing);
            }
            else
            {
              gchar *old_labels = existing_labels;
              existing_labels = g_strdup_printf("%s\n%s",
                                                existing_labels ? existing_labels : "",
                                                _action_description(e, 2));
              g_free(old_labels);
            }
          }

          existing = saved_next;
        } while(!g_sequence_iter_is_end(existing)
                && !_shortcut_compare_func(s, g_sequence_get(existing),
                                           GINT_TO_POINTER(view)));
      }

      if(!real_views || real_views == view || real_views == DT_VIEW_FALLBACK) break;
      s->views ^= view; // look in the opposite selection (and flip
                        // back on second go-around)
    } while(s->views != real_views);

    if(existing_labels)
    {
      remove_existing = dt_gui_show_yes_no_dialog(_("clashing shortcuts exist"), "", "%s\n%s",
                                                  _("remove these existing shortcuts?"),
         existing_labels);
      g_free(existing_labels);

      if(!remove_existing)
      {
        g_free(s);
        return FALSE;
      }
    }
    else
    {
      remove_existing = FALSE;
    }

  } while(remove_existing);

  if(disable)
  {
    g_free(s);
    return FALSE;
  }

  //keep direction if replacing one half of split move
  if(replaced_direction != s->direction)
    s->direction = shortcut->direction = 0;

  _add_shortcut(s, view);

  return TRUE;
}

typedef enum
{
  SHORTCUT_VIEW_DESCRIPTION,
  SHORTCUT_VIEW_ACTION,
  SHORTCUT_VIEW_ELEMENT,
  SHORTCUT_VIEW_EFFECT,
  SHORTCUT_VIEW_SPEED,
  SHORTCUT_VIEW_INSTANCE,
  SHORTCUT_VIEW_COLUMNS
} field_id;

#define NUM_INSTANCES 5 // or 3, but change char relative[] = "-2" to "-1"
const gchar *instance_label[/*NUM_INSTANCES*/]
  = { N_("preferred"),
      N_("first"),
      N_("last"),
      N_("second"),
      N_("last but one") };

static void _fill_shortcut_fields(GtkTreeViewColumn *column,
                                  GtkCellRenderer *cell,
                                  GtkTreeModel *model,
                                  GtkTreeIter *iter,
                                  gpointer data)
{
  void *data_ptr = NULL;
  gtk_tree_model_get(model, iter, 0, &data_ptr, -1);
  const field_id field = GPOINTER_TO_INT(data);
  gchar *field_text = NULL;
  gboolean editable = FALSE;
  int weight = PANGO_WEIGHT_NORMAL;

  if(_is_shortcut_category(data_ptr))
  {
    if(field == SHORTCUT_VIEW_DESCRIPTION)
      field_text = g_strdup(_(_shortcut_category_label[GPOINTER_TO_INT(data_ptr)]));
  }
  else
  {
    const dt_action_element_def_t *elements = NULL;
    dt_shortcut_t *s = g_sequence_get(data_ptr);

    dt_action_t *owner = s->action;
    while(owner && owner->type >= DT_ACTION_TYPE_SECTION) owner = owner->owner;

    switch(field)
    {
    case SHORTCUT_VIEW_DESCRIPTION:
      field_text = g_strdup(_shortcut_description(s));
      break;
    case SHORTCUT_VIEW_ACTION:
      if(s->action)
        field_text = _action_full_label(s->action);
      break;
    case SHORTCUT_VIEW_ELEMENT:
      if(owner == &darktable.control->actions_lua || _shortcut_is_speed(s)) break;
      elements = _action_find_elements(s->action);
      if(elements && elements->name)
      {
        if(s->element || s->action->type != DT_ACTION_TYPE_FALLBACK)
          field_text = g_strdup(_(elements[s->element].name));
        if(s->element == 0) weight = PANGO_WEIGHT_LIGHT;
        editable = TRUE;
      }
      break;
    case SHORTCUT_VIEW_EFFECT:
      if(owner == &darktable.control->actions_lua || _shortcut_is_speed(s)) break;
      elements = _action_find_elements(s->action);
      if(elements)
      {
        const gchar *cef =
          _action_find_effect_combo(s->action, &elements[s->element], s->effect);

        if(cef
           || s->effect > 0
           || (s->effect == 0
               && s->action->type != DT_ACTION_TYPE_FALLBACK))
          field_text = g_strdup(Q_(cef ? cef : elements[s->element].effects[s->effect]));

        if(s->effect == 0)
          weight = PANGO_WEIGHT_LIGHT;
        editable = TRUE;
      }
      break;
    case SHORTCUT_VIEW_SPEED:
      elements = _action_find_elements(s->action);
      if(s->speed != 1.0
         || (ELEMENT_IS(value, s, elements)
             && (s->effect == DT_ACTION_EFFECT_DEFAULT_MOVE
                 || s->effect == DT_ACTION_EFFECT_DEFAULT_KEY
                 || s->effect == DT_ACTION_EFFECT_DEFAULT_UP
                 || s->effect == DT_ACTION_EFFECT_DEFAULT_DOWN
                 || s->effect == DT_ACTION_EFFECT_SET
                 || (!s->effect && s->action->type == DT_ACTION_TYPE_FALLBACK))))
      {
        field_text = g_strdup_printf("%.3f", s->speed);
        if(s->speed == 1.0) weight = PANGO_WEIGHT_LIGHT;
      }
      if(s->action->type != DT_ACTION_TYPE_COMMAND) editable = TRUE;
      break;
    case SHORTCUT_VIEW_INSTANCE:
      if(_shortcut_is_speed(s)) break;
      for(; owner; owner = owner->owner)
      {
        if(owner->type == DT_ACTION_TYPE_IOP)
        {
          dt_iop_module_so_t *iop = (dt_iop_module_so_t *)owner;

          if(owner != &darktable.control->actions_focus
             && !(iop->flags() & IOP_FLAGS_ONE_INSTANCE))
          {
            field_text = abs(s->instance) <= (NUM_INSTANCES - 1) /2
                       ? g_strdup(_(instance_label[abs(s->instance)*2 - (s->instance > 0)]))
                       : g_strdup_printf("%+d", s->instance);
            if(s->instance == 0) weight = PANGO_WEIGHT_LIGHT;
            editable = TRUE;
          }
          break;
        }
      }
      break;
    default:
      break;
    }
    if(!s->views) editable = FALSE; //disabled default shortcuts
  }
  g_object_set(cell, "text", field_text,
                     "editable", editable,
                     "underline", PANGO_UNDERLINE_NONE,
                     "weight", editable && weight != PANGO_WEIGHT_LIGHT ? PANGO_WEIGHT_BOLD : weight,
                     NULL);
  g_free(field_text);
}

static void _add_prefs_column(GtkTreeView *tree,
                              GtkCellRenderer *renderer,
                              char *name,
                              const int position)
{
  GtkTreeViewColumn *column =
    gtk_tree_view_column_new_with_attributes(name, renderer, NULL);
  gtk_tree_view_column_set_cell_data_func(column, renderer,
                                          _fill_shortcut_fields,
                                          GINT_TO_POINTER(position), NULL);
  gtk_tree_view_column_set_resizable(column, TRUE);
  gtk_tree_view_append_column(tree, column);
}

static dt_shortcut_t *_find_edited_shortcut(GtkTreeModel *model,
                                            const gchar *path_string)
{
  GtkTreePath *path = gtk_tree_path_new_from_string(path_string);
  GtkTreeIter iter;
  gtk_tree_model_get_iter(model, &iter, path);
  gtk_tree_path_free(path);

  void *data_ptr = NULL;
  gtk_tree_model_get(model, &iter, 0, &data_ptr, -1);

  return g_sequence_get(data_ptr);
}

static void _element_editing_started(GtkCellRenderer *renderer,
                                     GtkCellEditable *editable,
                                     char *path,
                                     gpointer data)
{
  dt_shortcut_t *s = _find_edited_shortcut(data, path);

  GtkComboBox *combo_box = GTK_COMBO_BOX(editable);
  GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo_box));
  gtk_list_store_clear(store);

  int show_all = s->action->type != DT_ACTION_TYPE_FALLBACK;
  for(const dt_action_element_def_t *element = _action_find_elements(s->action);
      element && element->name ;
      element++)
    gtk_list_store_insert_with_values(store, NULL, -1, 0,
                                      show_all++
                                      ? _(element->name)
                                      : _("(unchanged)"), -1);

  gtk_combo_box_set_active(combo_box, s->element);
}

static void _element_changed(GtkCellRendererCombo *combo,
                             char *path_string,
                             GtkTreeIter *new_iter,
                             gpointer data)
{
  dt_shortcut_t *s = _find_edited_shortcut(data, path_string);

  GtkTreeModel *model = NULL;
  g_object_get(combo, "model", &model, NULL);
  GtkTreePath *path = gtk_tree_model_get_path(model, new_iter);
  const gint new_index = gtk_tree_path_get_indices(path)[0];
  gtk_tree_path_free(path);

  const dt_action_element_def_t *elements = _action_find_elements(s->action);
  if(elements[s->element].effects != elements[new_index].effects)
  {
    s->effect = _shortcut_default_effect(s);
  }
  s->element = new_index;

  dt_shortcuts_save(NULL, FALSE);
}

enum
{
  DT_ACTION_EFFECT_COLUMN_NAME,
  DT_ACTION_EFFECT_COLUMN_SEPARATOR,
  DT_ACTION_EFFECT_COLUMN_WEIGHT,
};

static gboolean _effects_separator_func(GtkTreeModel *model,
                                        GtkTreeIter *iter,
                                        gpointer data)
{
  gboolean is_separator;
  gtk_tree_model_get(model, iter, DT_ACTION_EFFECT_COLUMN_SEPARATOR, &is_separator, -1);
  return is_separator;
}

static void _effect_editing_started(GtkCellRenderer *renderer,
                                    GtkCellEditable *editable,
                                    char *path,
                                    gpointer data)
{
  dt_shortcut_t *s = _find_edited_shortcut(data, path);

  GtkComboBox *combo_box = GTK_COMBO_BOX(editable);
  GtkListStore *store = GTK_LIST_STORE(gtk_combo_box_get_model(combo_box));
  gtk_list_store_clear(store);

  const dt_action_element_def_t *elements = _action_find_elements(s->action);
  int show_all = s->action->type != DT_ACTION_TYPE_FALLBACK;
  int bold_move = _shortcut_is_move(s) ? DT_ACTION_EFFECT_DEFAULT_KEY : DT_ACTION_EFFECT_DEFAULT_DOWN + 1;
  if(elements)
    for(const gchar **effect = elements[s->element].effects;
        *effect;
        effect++, bold_move++)
    {
      gtk_list_store_insert_with_values
        (store, NULL, -1,
         DT_ACTION_EFFECT_COLUMN_NAME, show_all++ ? Q_(*effect) : _("(unchanged)"),
         DT_ACTION_EFFECT_COLUMN_WEIGHT,
         bold_move >  DT_ACTION_EFFECT_DEFAULT_KEY
           && bold_move <= DT_ACTION_EFFECT_DEFAULT_DOWN
           ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
           -1);
    }

  GList *cell = gtk_cell_layout_get_cells(GTK_CELL_LAYOUT(combo_box));
  gtk_cell_layout_add_attribute(GTK_CELL_LAYOUT(combo_box), cell->data,
                                "weight", DT_ACTION_EFFECT_COLUMN_WEIGHT);
  g_list_free(cell);

  if(ELEMENT_IS(selection, s, elements))
  {
    gtk_combo_box_set_row_separator_func(combo_box, _effects_separator_func, NULL, NULL);

    dt_introspection_type_enum_tuple_t *values
      = g_hash_table_lookup(darktable.bauhaus->combo_introspection, s->action);
    if(values)
    {
      // insert empty/separator row
      gtk_list_store_insert_with_values(store, NULL, -1,
                                        DT_ACTION_EFFECT_COLUMN_SEPARATOR, TRUE, -1);

      for(; values->name; values++)
      {
        const char *text = values->description ? values->description : values->name;
        if(*text)
          gtk_list_store_insert_with_values
            (store, NULL, -1,
            DT_ACTION_EFFECT_COLUMN_NAME,
            Q_(text),
            DT_ACTION_EFFECT_COLUMN_WEIGHT,
            PANGO_WEIGHT_NORMAL,
            -1);
      }
    }
    else
    {
      gchar **strings
        = g_hash_table_lookup(darktable.bauhaus->combo_list, s->action);
      if(strings)
      {
        // insert empty/separator row
        gtk_list_store_insert_with_values(store, NULL, -1,
                                          DT_ACTION_EFFECT_COLUMN_SEPARATOR, TRUE, -1);

        while(*strings)
        {
          gtk_list_store_insert_with_values(store, NULL, -1,
                                            DT_ACTION_EFFECT_COLUMN_NAME, Q_(*(strings++)),
                                            DT_ACTION_EFFECT_COLUMN_WEIGHT,
                                            PANGO_WEIGHT_NORMAL,
                                            -1);
        }
      }
    }
  }

  gtk_combo_box_set_active(combo_box, s->effect == -1 ? 1 : s->effect);
}

static void _effect_changed(GtkCellRendererCombo *combo,
                            char *path_string,
                            GtkTreeIter *new_iter,
                            gpointer data)
{
  dt_shortcut_t *s = _find_edited_shortcut(data, path_string);

  GtkTreeModel *model = NULL;
  g_object_get(combo, "model", &model, NULL);
  GtkTreePath *path = gtk_tree_model_get_path(model, new_iter);
  const gint new_index = s->effect = gtk_tree_path_get_indices(path)[0];
  gtk_tree_path_free(path);

  if(_shortcut_is_move(s) &&
     (new_index == DT_ACTION_EFFECT_DEFAULT_UP
      || new_index == DT_ACTION_EFFECT_DEFAULT_DOWN))
    s->effect = DT_ACTION_EFFECT_DEFAULT_MOVE;
  else
    s->effect = new_index;

  dt_shortcuts_save(NULL, FALSE);
}

static void _speed_edited(GtkCellRendererText *cell,
                          const gchar *path_string,
                          const gchar *new_text,
                          gpointer data)
{
  _find_edited_shortcut(data, path_string)->speed = atof(new_text);

  dt_shortcuts_save(NULL, FALSE);
}

static void _instance_edited(GtkCellRendererText *cell,
                             const gchar *path_string,
                             const gchar *new_text,
                             gpointer data)
{
  dt_shortcut_t *s = _find_edited_shortcut(data, path_string);

  if(!(s->instance = atoi(new_text)))
    for(int i = 0; i < NUM_INSTANCES; i++)
      if(!strcmp(instance_label[i], new_text))
        s->instance = (i + 1) / 2 * (i % 2 ? 1 : -1);

  dt_shortcuts_save(NULL, FALSE);
}

static void _grab_in_tree_view(GtkTreeView *tree_view)
{
  g_set_weak_pointer(&_grab_widget,
                     gtk_widget_get_parent(gtk_widget_get_parent(GTK_WIDGET(tree_view)))); // static
  gtk_widget_set_sensitive(_grab_widget, FALSE);
  gtk_widget_set_tooltip_text(_grab_widget,
                              _("define a shortcut by pressing a key, optionally combined with modifier keys (ctrl/shift/alt)\n"
                                "a key can be double or triple pressed, with a long last press\n"
                                "while the key is held, a combination of mouse buttons can be (double/triple/long) clicked\n"
                                "still holding the key (and modifiers and/or buttons) a scroll or mouse move can be added\n"
                                "connected devices can send keys or moves using their physical controllers\n\n"
                                "right-click to cancel"));
  g_set_weak_pointer(&_grab_window, gtk_widget_get_toplevel(_grab_widget));
  if(_sc.action && _sc.action->type == DT_ACTION_TYPE_FALLBACK)
    dt_shortcut_key_press(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, 0);
  g_signal_connect(_grab_window, "event", G_CALLBACK(dt_shortcut_dispatcher), NULL);
}

static void _shortcut_row_activated(GtkTreeView *tree_view,
                                    GtkTreePath *path,
                                    GtkTreeViewColumn *column,
                                    gpointer user_data)
{
  GtkTreeIter iter;
  gtk_tree_model_get_iter(GTK_TREE_MODEL(user_data), &iter, path);

  GSequenceIter  *shortcut_iter = NULL;
  gtk_tree_model_get(GTK_TREE_MODEL(user_data), &iter, 0, &shortcut_iter, -1);

  if(_is_shortcut_category(shortcut_iter)) return;

  dt_shortcut_t *s = g_sequence_get(shortcut_iter);
  _sc.action = s->action;
  _sc.element = s->element;
  _sc.instance = s->instance;

  _grab_in_tree_view(tree_view);
}

static gboolean _view_key_pressed(GtkWidget *widget,
                                  GdkEventKey *event,
                                  gpointer user_data)
{
  GtkTreeView *view = GTK_TREE_VIEW(widget);
  GtkTreeSelection *selection = gtk_tree_view_get_selection(view);

  GtkTreeIter iter;
  GtkTreeModel *model = NULL;
  if(gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    if(!strcmp(gtk_widget_get_name(widget), "actions_view"))
    {
      // if control key pressed, copy lua command to clipboard (CTRL+C will work)
      if(dt_modifier_is(event->state, GDK_CONTROL_MASK))
      {
        dt_shortcut_t shortcut = { .speed = 1.0 };
        gtk_tree_model_get(model, &iter, 0, &shortcut.action, -1);

        _shortcut_copy_lua(NULL, &shortcut, NULL);
      }
    }
    else
    {
      GSequenceIter  *shortcut_iter = NULL;
      gtk_tree_model_get(model, &iter, 0, &shortcut_iter, -1);

      if(!_is_shortcut_category(shortcut_iter))
      {
        dt_shortcut_t *s = g_sequence_get(shortcut_iter);

        // if control key pressed, copy lua command to clipboard (CTRL+C will work)
        if(dt_modifier_is(event->state, GDK_CONTROL_MASK) && s->views)
        {
          _shortcut_copy_lua(NULL, s, NULL);
        }

        // GDK_KEY_BackSpace moves to parent in tree
        if(event->keyval == GDK_KEY_Delete || event->keyval == GDK_KEY_KP_Delete)
        {
          if(dt_gui_show_yes_no_dialog(_("removing shortcut"), "",
                                       s->is_default ? s->views ?
                                       _("disable the selected default shortcut?") :
                                       _("restore the selected default shortcut?") :
                                       _("remove the selected shortcut?")))
          {
            _remove_shortcut(shortcut_iter);

            dt_shortcuts_save(NULL, FALSE);
          }

          return TRUE;
        }
      }
    }
  }

  return dt_gui_search_start(widget, event, user_data);
}

static void _add_shortcuts_to_tree()
{
  const dt_view_type_flags_t view = dt_view_get_current();

  for(dt_shortcut_category_t i = 0; i < DT_SHORTCUT_CATEGORY_LAST; i++)
    gtk_tree_store_insert_with_values(_shortcuts_store, NULL, NULL, -1, 0, GINT_TO_POINTER(i), -1);

  for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
      !g_sequence_iter_is_end(iter);
      iter = g_sequence_iter_next(iter))
  {
    dt_shortcut_t *s = g_sequence_get(iter);
    GtkTreeIter category;
    _shortcuts_store_category(&category, s, view);

    gtk_tree_store_insert_with_values(_shortcuts_store, NULL, &category, -1, 0, iter, -1);
  }
}

static gboolean _add_actions_to_tree(GtkTreeIter *parent,
                                     dt_action_t *action,
                                     dt_action_t *find,
                                     GtkTreeIter *found)
{
  gboolean any_leaves = FALSE;
  gchar **prev_split = NULL;
  GtkTreeIter node = parent ? *parent : (GtkTreeIter){0};

  GtkTreeIter iter;
  for(; action; action = action->next)
  {
    if(action->type == DT_ACTION_TYPE_IOP)
    {
      const dt_iop_module_so_t *module = (dt_iop_module_so_t *)action;
      if(action != &darktable.control->actions_focus
         && module->flags() & (IOP_FLAGS_HIDDEN | IOP_FLAGS_DEPRECATED))
        continue;
    }

    gboolean module_is_needed = FALSE;
    if(action->type == DT_ACTION_TYPE_LIB)
    {
      dt_lib_module_t *module = (dt_lib_module_t *)action;
      module_is_needed = module->gui_reset || module->get_params || module->expandable(module);
    }

    gchar **split = g_strsplit(action->label, "|", -1);
    gchar **s = split, **p = prev_split;
    for(gchar **spl = split; *spl; spl++)
      g_strstrip(*spl); // remove leading/trailing whitespace (in-place)
    for(; p && *(p+1) && *(s+1) && !g_strcmp0(*s, *p); p++, s++)
      ;
    for(; p && *(p+1); p++, node = iter)
      gtk_tree_model_iter_parent(GTK_TREE_MODEL(_actions_store), &iter, &node);
    for(; *(s+1); s++, node = iter)
      gtk_tree_store_insert_with_values(_actions_store, &iter, &node, -1, 0, action->owner, 1, *s, -1);
    g_strfreev(prev_split);
    prev_split = split;

    gtk_tree_store_insert_with_values(_actions_store, &iter, parent ? &node : NULL, -1, 0, action, -1);

    if(action->type <= DT_ACTION_TYPE_SECTION &&
       !_add_actions_to_tree(&iter, action->target, find, found) &&
       !module_is_needed)
      gtk_tree_store_remove(_actions_store, &iter);
    else
    {
      any_leaves = TRUE;
      if(action == find) *found = iter;
    }
  }

  g_strfreev(prev_split);
  return any_leaves;
}

static void _fill_action_fields(GtkTreeViewColumn *column,
                                GtkCellRenderer *cell,
                                GtkTreeModel *model,
                                GtkTreeIter *iter,
                                gpointer data)
{
  dt_action_t *action = NULL;
  gchar *node_label = NULL;
  gtk_tree_model_get(model, iter, 0, &action, 1, &node_label, -1);

  if(node_label)
  {
    g_object_set(cell, "text", node_label, NULL);
    g_free(node_label);
    return;
  }

  gchar const *last_sep = strrchr(action->label, '|');
  gchar const *text = last_sep ? last_sep + 1 : action->label;
  if(!data)
  {
    const dt_action_def_t *def = _action_find_definition(action);
    text = def ? _(def->name) :
           action->type == DT_ACTION_TYPE_COMMAND ? _("command") :
           action->type == DT_ACTION_TYPE_PRESET ? _("preset") : "";
  }

  int weight = PANGO_WEIGHT_NORMAL;

  if(_selected_shortcut)
  {
    for(dt_action_t *ac = _selected_shortcut->action; ac; ac = ac->owner)
    {
      if(ac == action)
      {
        weight = PANGO_WEIGHT_BOLD;
        break;
      }
    }
  }

  g_object_set(cell, "text", text, "weight", weight, NULL);
}

static void _action_row_activated(GtkTreeView *tree_view,
                                  GtkTreePath *path,
                                  GtkTreeViewColumn *column,
                                  gpointer user_data)
{
  GtkTreeIter iter;
  gtk_tree_model_get_iter(GTK_TREE_MODEL(user_data), &iter, path);

  gtk_tree_model_get(GTK_TREE_MODEL(user_data), &iter, 0, &_sc.action, -1);
  _sc.element = DT_ACTION_ELEMENT_DEFAULT;
  _sc.instance = 0;

  if(_sc.action->type > DT_ACTION_TYPE_SECTION || _action_find_definition(_sc.action))
    _grab_in_tree_view(tree_view);
  else
    _sc.action = NULL;
}

static gboolean _shortcut_selection_function(GtkTreeSelection *selection,
                                             GtkTreeModel *model,
                                             GtkTreePath *path,
                                             gboolean path_currently_selected,
                                             gpointer data)
{
  GtkTreeIter iter;
  gtk_tree_model_get_iter(model, &iter, path);

  void *data_ptr = NULL;
  gtk_tree_model_get(model, &iter, 0, &data_ptr, -1);

  if(_is_shortcut_category(data_ptr))
  {
    GtkTreeView *view = gtk_tree_selection_get_tree_view(selection);
    if(gtk_tree_view_row_expanded(view, path))
      gtk_tree_view_collapse_row(view, path);
    else
      gtk_tree_view_expand_row(view, path, FALSE);

    return FALSE;
  }
  else
    return TRUE;
}

static void _shortcut_selection_changed(GtkTreeSelection *selection,
                                        gpointer data)
{
  GtkTreeModel *model = NULL;
  GtkTreeIter iter;

  if(gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    void *data_ptr = NULL;
    gtk_tree_model_get(model, &iter, 0, &data_ptr, -1);
    _selected_shortcut = g_sequence_get(data_ptr);
  }
  else
    _selected_shortcut = NULL;

  gtk_widget_queue_draw(GTK_WIDGET(data));
}

static gboolean _action_find_and_expand(GtkTreeModel *model,
                                        GtkTreeIter *iter,
                                        GtkTreeView *view)
{
  do
  {
    dt_action_t *current_action = NULL;
    gtk_tree_model_get(model, iter, 0, &current_action, -1);

    if(_selected_shortcut && current_action == _selected_shortcut->action)
    {
      GtkTreePath *path = gtk_tree_model_get_path(model, iter);
      gtk_tree_view_expand_to_path(view, path);
      gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.5, 0);
      gtk_tree_path_free(path);

      return TRUE;
    }

    GtkTreeIter child;
    if(gtk_tree_model_iter_children(model, &child, iter)
       && _action_find_and_expand(model, &child, view))
    {
      return TRUE;
    }
  } while(gtk_tree_model_iter_next(model, iter));

  return FALSE;
}

static gboolean _action_view_click(GtkWidget *widget,
                                   GdkEventButton *event,
                                   gpointer data)
{
  GtkTreeView *view = GTK_TREE_VIEW(widget);
  GtkTreeModel *model = gtk_tree_view_get_model(view);

  if(event->button == GDK_BUTTON_PRIMARY)
  {
    GtkTreeSelection *selection = gtk_tree_view_get_selection(view);

    GtkTreePath *path = NULL;
    if(gtk_tree_view_get_path_at_pos(view, (gint)event->x, (gint)event->y,
                                     &path, NULL, NULL, NULL))
    {
      if(event->type == GDK_DOUBLE_BUTTON_PRESS)
      {
        gtk_tree_selection_select_path(selection, path);
        _action_row_activated(view, path, NULL, model);
      }
      else if(gtk_tree_selection_path_is_selected(selection, path))
      {
        gtk_tree_selection_unselect_path(selection, path);
        gtk_tree_view_collapse_row(view, path);
      }
      else
      {
        gtk_tree_selection_select_path(selection, path);
        gtk_tree_view_set_cursor(view, path, NULL, FALSE);
      }

      gtk_widget_grab_focus(widget);
    }
    else
      gtk_tree_selection_unselect_all(selection);
  }
  else if(event->button == GDK_BUTTON_SECONDARY)
  {
    GtkTreeIter iter;
    gtk_tree_model_get_iter_first(model, &iter);

    _action_find_and_expand(model, &iter, view);
  }

  return TRUE;
}

static gboolean _action_view_show(GtkTreeView *view,
                                  gpointer found_iter)
{
  GtkTreePath *path = gtk_tree_model_get_path(gtk_tree_view_get_model(view), found_iter);
  gtk_tree_view_expand_to_path(view, path);
  gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.5, 0);
  gtk_tree_view_set_cursor(view, path, NULL, FALSE);
  gtk_tree_path_free(path);

  gtk_tree_selection_select_iter(gtk_tree_view_get_selection(view), found_iter);

  return FALSE;
}

static void _action_selection_changed(GtkTreeSelection *selection,
                                      gpointer data)
{
  GtkTreeIter iter;
  GtkTreeModel *model = NULL;

  if(!gtk_tree_selection_get_selected(selection, &model, &iter))
    _selected_action = NULL;
  else
  {
    gtk_tree_model_get(model, &iter, 0, &_selected_action, -1);

    GtkTreeView *view = gtk_tree_selection_get_tree_view(selection);
    GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
    gtk_tree_view_expand_row(view, path, FALSE);
    gtk_tree_path_free(path);
  }

  GtkTreeView *shortcuts_view = GTK_TREE_VIEW(data);
  gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER
                                 (gtk_tree_view_get_model(shortcuts_view)));
  gtk_tree_view_expand_all(shortcuts_view);
}

static gboolean _search_func(GtkTreeModel *model,
                             gint column,
                             const gchar *key,
                             GtkTreeIter *iter,
                             gpointer search_data)
{
  gchar *key_case = g_utf8_casefold(key, -1), *label_case = NULL;
  if(column == 1)
  {
    dt_action_t *action = NULL;
    gtk_tree_model_get(model, iter, 0, &action, -1);
    label_case = g_utf8_casefold(action->label, -1);
  }
  else
  {
    GSequenceIter *seq_iter = NULL;
    gtk_tree_model_get(model, iter, 0, &seq_iter, -1);
    if(!_is_shortcut_category(seq_iter))
    {
      dt_shortcut_t *s = g_sequence_get(seq_iter);
      if(s->action)
      {
        gchar *label = _action_full_label(s->action);
        label_case = g_utf8_casefold(label, -1);
        g_free(label);
      }
    }
  }
  const gboolean different = label_case ? !strstr(label_case, key_case) : TRUE;
  g_free(key_case);
  g_free(label_case);
  if(!different)
  {
    GtkTreePath *path = gtk_tree_model_get_path(model, iter);
    gtk_tree_view_expand_to_path(GTK_TREE_VIEW(search_data), path);
    gtk_tree_path_free(path);

    return FALSE;
  }

  GtkTreeIter child;
  if(gtk_tree_model_iter_children(model, &child, iter))
  {
    do
    {
      _search_func(model, column, key, &child, search_data);
    }
    while(gtk_tree_model_iter_next(model, &child));
  }

  return TRUE;
}

static gboolean _fallback_type_is_relevant(dt_action_t *ac,
                                           dt_action_type_t type)
{
  if(!ac) return FALSE;

  if(ac->type == type) return TRUE;

  if(ac->type >= DT_ACTION_TYPE_WIDGET)
  {
    if(type == DT_ACTION_TYPE_VALUE_FALLBACK)
    {
      const dt_action_def_t *def = _action_find_definition(ac);
      if(def && def->elements)
      {
        const dt_action_element_def_t *el = def->elements;
        do
        {
          if(el->effects == dt_action_effect_value) return TRUE;
          el++;
        } while(el->name);
      }
    }
  }
  else if(ac->type <= DT_ACTION_TYPE_SECTION)
    for(ac = ac->target; ac; ac = ac->next)
      if(_fallback_type_is_relevant(ac, type)) return TRUE;

  return FALSE;
}

static gint _sort_speed_shortcuts(GtkTreeModel *model,
                                  GtkTreeIter *a,
                                  GtkTreeIter *b,
                                  gpointer user_data)
{
  gpointer data_a, data_b;

  gtk_tree_model_get(model, a, 0, &data_a, -1);
  gtk_tree_model_get(model, b, 0, &data_b, -1);
  if(_is_shortcut_category(data_a) || _is_shortcut_category(data_b))
    return 0;

  dt_shortcut_t *shortcut_a = g_sequence_get(data_a);
  dt_shortcut_t *shortcut_b = g_sequence_get(data_b);
  if(!_shortcut_is_speed(shortcut_a) || !_shortcut_is_speed(shortcut_b))
    return 0;

  gchar *label_a = _action_full_label(shortcut_a->action);
  gchar *label_b = _action_full_label(shortcut_b->action);
  gint return_value = g_utf8_collate(label_a, label_b);
  g_free(label_a);
  g_free(label_b);

  return return_value;
}

static gboolean _visible_shortcuts(GtkTreeModel *model,
                                   GtkTreeIter *iter,
                                   gpointer data)
{
  void *data_ptr = NULL;
  gtk_tree_model_get(model, iter, 0, &data_ptr, -1);

  if(_is_shortcut_category(data_ptr))
  {
    GtkTreeIter child;
    for(gboolean valid_child = gtk_tree_model_iter_children(model, &child, iter);
        valid_child;
        valid_child = gtk_tree_model_iter_next(model, &child))
    {
      if(_visible_shortcuts(model, &child, data)) return TRUE;
    }

    return FALSE;
  }

  dt_shortcut_t *s = g_sequence_get(data_ptr);

  if(!darktable.control->enable_fallbacks && s->action->type == DT_ACTION_TYPE_FALLBACK
     && (GPOINTER_TO_INT(s->action->target) != DT_ACTION_TYPE_VALUE_FALLBACK
         || s->key_device || s->key || s->press || s->move_device || s->move || s->button))
    return FALSE;

  if(!_selected_action) return TRUE;

  if(_selected_action->type == DT_ACTION_TYPE_FALLBACK &&
     s->action->type == GPOINTER_TO_INT(_selected_action->target))
    return TRUE;

  for(dt_action_t *ac = s->action; ac; ac = ac->owner)
    if(ac == _selected_action)
      return TRUE;

  if(s->action->type == DT_ACTION_TYPE_FALLBACK)
    return _fallback_type_is_relevant(_selected_action, GPOINTER_TO_INT(s->action->target));

  return FALSE;
}

static void _resize_shortcuts_view(GtkWidget *view, GParamSpec *pspec, gpointer user_data)
{
  dt_conf_set_int("shortcuts/window_split", gtk_paned_get_position(GTK_PANED(view)));
}

const dt_input_device_t DT_ALL_DEVICES = UINT8_MAX;
static void _shortcuts_save(const gchar *shortcuts_file, const dt_input_device_t device);
static void _shortcuts_load(const gchar *shortcuts_file,
                            const dt_input_device_t file_dev,
                            const dt_input_device_t load_dev,
                            const gboolean clear);

static void _fallbacks_toggled(GtkToggleButton *button, gpointer data)
{
  dt_conf_set_bool("accel/enable_fallbacks",
                   (darktable.control->enable_fallbacks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))));

  GtkTreeView *shortcuts_view = GTK_TREE_VIEW(data);
  gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER
                                 (gtk_tree_view_get_model(shortcuts_view)));
}

static void _restore_clicked(GtkButton *button, gpointer user_data)
{
  enum
  {
    _DEFAULTS = 1,
    _STARTUP,
    _EDITS,
  };

  GtkWidget *dialog = gtk_dialog_new_with_buttons
    (_("restore shortcuts"),
     GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button))),
     GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
     _("_defaults"), _DEFAULTS,
     _("_startup"), _STARTUP,
     _("_edits"), _EDITS,
     _("_cancel"), GTK_RESPONSE_REJECT,
     NULL);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);

  GtkWidget *label = gtk_label_new(_("restore shortcuts from one of these states:\n"
                                     "  - default\n"
                                     "  - as at startup\n"
                                     "  - as when opening this dialog\n"));
  gtk_widget_set_halign(label, GTK_ALIGN_START);
  GtkWidget *clear = gtk_check_button_new_with_label
    (_("clear all newer shortcuts\n"
       "(instead of just restoring changed ones)"));
  dt_gui_dialog_add(GTK_DIALOG(dialog), label, clear);

  gtk_widget_show_all(dialog);

  const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
  const gboolean wipe = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(clear));

  gtk_widget_destroy(dialog);

  switch(resp)
  {
  case _DEFAULTS:
    dt_shortcuts_load(".defaults", wipe);
    break;
  case _STARTUP:
    dt_shortcuts_load(".backup", wipe);
    break;
  case _EDITS:
    dt_shortcuts_load(".edit", wipe);
    break;
  }

  dt_shortcuts_save(NULL, FALSE);
}

static void _import_export_dev_changed(GtkComboBox *widget,
                                       gpointer user_data)
{
  gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(widget));
  g_object_set_data(G_OBJECT(user_data), "device", GINT_TO_POINTER(dev));
  gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), 1); // make sure changed triggered
  gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), dev > 1 ? 0 : -1);
  gtk_widget_set_visible(gtk_widget_get_parent(GTK_WIDGET(user_data)), dev > 1);
}

static void _export_id_changed(GtkComboBox *widget,
                               gpointer user_data)
{
  gint dev = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "device"));
  gint id = dev <= 1 ? 0 :
            gtk_combo_box_get_active(GTK_COMBO_BOX(widget)) + (dev-1) * 10;

  gint count = 0;

  for(GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
      !g_sequence_iter_is_end(iter);
      iter = g_sequence_iter_next(iter))
  {
    const dt_shortcut_t *s = g_sequence_get(iter);
    if(dev == 0 ||
       (id == 0 &&  s->key_device == id && s->move_device == id) ||
       (id != 0 && (s->key_device == id || s->move_device == id)))
      count++;
  }

  gchar *text = g_strdup_printf("%d %s", count, _("shortcuts"));
  gtk_label_set_text(GTK_LABEL(user_data), text);
  g_free(text);
}

static void _export_clicked(GtkButton *button, gpointer user_data)
{
  GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));

  GtkWidget *dialog = gtk_dialog_new_with_buttons
    (_("export shortcuts"),
     win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
     _("_cancel"), GTK_RESPONSE_REJECT,
     _("_ok"), GTK_RESPONSE_OK,
     NULL);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);

  GtkWidget *label = gtk_label_new(_("export all shortcuts to a file\n"
                                     "or just for one selected device\n"));
  gtk_widget_set_halign(label, GTK_ALIGN_START);

  GtkWidget *combo_dev = gtk_combo_box_text_new();
  gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("all"));
  gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("keyboard"));
  for(GSList *driver = darktable.control->input_drivers; driver; driver = driver->next)
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev),
                                   ((dt_input_driver_definition_t *)driver->data)->name);

  GtkWidget *combo_id = gtk_combo_box_text_new();
  for(gchar num[] = "0"; *num <= '9'; (*num)++)
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_id), num);
  GtkWidget *count = gtk_label_new("");
  g_signal_connect(combo_dev, "changed", G_CALLBACK(_import_export_dev_changed), combo_id);
  g_signal_connect(combo_id, "changed", G_CALLBACK(_export_id_changed), count);

  dt_gui_dialog_add(GTK_DIALOG(dialog), label, combo_dev,
                                        dt_gui_hbox(combo_id, dt_ui_label_new(_("device id"))),
                                        count);
  gtk_widget_show_all(dialog);

  gtk_combo_box_set_active(GTK_COMBO_BOX(combo_dev), 0);

  const int resp = gtk_dialog_run(GTK_DIALOG(dialog));

  const gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(combo_dev));
  const gint id = dev == 0 ? DT_ALL_DEVICES :
                  dev == 1 ? 0 :
                  gtk_combo_box_get_active(GTK_COMBO_BOX(combo_id)) + (dev-1) * 10;

  gtk_widget_destroy(dialog);

  if(resp != GTK_RESPONSE_OK) return;

  GtkFileChooserNative *chooser = gtk_file_chooser_native_new(
        _("select file to export"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_SAVE,
        _("_export"), _("_cancel"));

  gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(chooser), TRUE);
  dt_conf_get_folder_to_file_chooser("ui_last/export_path", GTK_FILE_CHOOSER(chooser));
  gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(chooser), "shortcutsrc");
  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser));

    _shortcuts_save(filename, id);
    g_free(filename);
    dt_conf_set_folder_from_file_chooser("ui_last/export_path", GTK_FILE_CHOOSER(chooser));
  }
  g_object_unref(chooser);
}

static void _import_id_changed(GtkComboBox *widget, gpointer user_data)
{
  gint id = gtk_combo_box_get_active(widget);
  gtk_combo_box_set_active(GTK_COMBO_BOX(user_data), id);
}

static void _import_clicked(GtkButton *button, gpointer user_data)
{
  GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));

  GtkWidget *dialog = gtk_dialog_new_with_buttons
    (_("import shortcuts"),
     win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
     _("_cancel"), GTK_RESPONSE_REJECT,
     _("_ok"), GTK_RESPONSE_OK,
     NULL);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);

  GtkWidget *label = gtk_label_new(_("import all shortcuts from a file\n"
                                     "or just for one selected device\n"));
  gtk_widget_set_halign(label, GTK_ALIGN_START);

  GtkWidget *combo_dev = gtk_combo_box_text_new();
  gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("all"));
  gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev), _("keyboard"));
  for(GSList *driver = darktable.control->input_drivers; driver; driver = driver->next)
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_dev),
                                   ((dt_input_driver_definition_t *)driver->data)->name);

  GtkWidget *device_grid = gtk_grid_new();

  GtkWidget *combo_from_id = gtk_combo_box_text_new();
  for(gchar num[] = "0"; *num <= '9'; (*num)++)
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_from_id), num);
  gtk_grid_attach(GTK_GRID(device_grid), combo_from_id, 0, 0, 1, 1);
  gtk_grid_attach(GTK_GRID(device_grid), dt_ui_label_new(_("id in file")), 1, 0, 1, 1);

  GtkWidget *combo_to_id = gtk_combo_box_text_new();
  for(gchar num[] = "0"; *num <= '9'; (*num)++)
    gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(combo_to_id), num);
  gtk_grid_attach(GTK_GRID(device_grid), combo_to_id, 0, 1, 1, 1);
  gtk_grid_attach(GTK_GRID(device_grid), dt_ui_label_new(_("id when loaded")), 1, 1, 1, 1);


  GtkWidget *clear = gtk_check_button_new_with_label(_("clear device first"));

  g_signal_connect(combo_dev, "changed", G_CALLBACK(_import_export_dev_changed),
                   combo_from_id);
  g_signal_connect(combo_from_id, "changed", G_CALLBACK(_import_id_changed), combo_to_id);

  dt_gui_dialog_add(GTK_DIALOG(dialog), label, combo_dev, device_grid, clear);
  gtk_widget_show_all(dialog);

  gtk_combo_box_set_active(GTK_COMBO_BOX(combo_dev), 0);

  const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
  const gint dev = gtk_combo_box_get_active(GTK_COMBO_BOX(combo_dev));
  const gint from_id = dev == 0 ? DT_ALL_DEVICES :
                       dev == 1 ? 0 :
                       gtk_combo_box_get_active(GTK_COMBO_BOX(combo_from_id)) + (dev-1) * 10;
  const gint to_id = dev == 1 ? 0 :
                     gtk_combo_box_get_active(GTK_COMBO_BOX(combo_to_id)) + (dev-1) * 10;
  const gboolean wipe = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(clear));

  gtk_widget_destroy(dialog);

  if(resp != GTK_RESPONSE_OK) return;

  GtkFileChooserNative *chooser = gtk_file_chooser_native_new(
        _("select file to import"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
        _("_import"), _("_cancel"));
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(chooser), FALSE);

  dt_conf_get_folder_to_file_chooser("ui_last/import_path", GTK_FILE_CHOOSER(chooser));
  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser));

    if(wipe && from_id != DT_ALL_DEVICES)
    {
      GtkTreeModel *model = GTK_TREE_MODEL(_shortcuts_store);
      GtkTreeIter category;
      gboolean valid_category = gtk_tree_model_get_iter_first(model, &category);
      while(valid_category)
      {
        GtkTreeIter child;
        gboolean valid_child = gtk_tree_model_iter_children(model, &child, &category);
        while(valid_child)
        {
          gpointer child_data;
          gtk_tree_model_get(model, &child, 0, &child_data, -1);

          dt_shortcut_t *s = g_sequence_get(child_data);
          if((to_id == 0 &&  s->key_device == to_id && s->move_device == to_id) ||
             (to_id != 0 && (s->key_device == to_id || s->move_device == to_id)))
          {
            g_sequence_remove(child_data);
            valid_child = gtk_tree_store_remove(GTK_TREE_STORE(model), &child);
          }
          else
            valid_child = gtk_tree_model_iter_next(model, &child);
        }
        valid_category = gtk_tree_model_iter_next(model, &category);
      };
    }

    _shortcuts_load(filename, from_id, to_id, wipe && from_id == DT_ALL_DEVICES);

    g_free(filename);
    dt_conf_set_folder_from_file_chooser("ui_last/import_path", GTK_FILE_CHOOSER(chooser));
  }
  g_object_unref(chooser);

  dt_shortcuts_save(NULL, FALSE);
}

static void _import_extended_clicked(GtkButton *button, gpointer user_data)
{
  GtkWindow *win = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(button)));
  GtkWidget *dialog = gtk_dialog_new_with_buttons
    (_("import sample shortcuts"),
     win, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
     _("_cancel"), GTK_RESPONSE_REJECT,
     _("_ok"), GTK_RESPONSE_OK,
     NULL);
  gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_REJECT);
  GtkWidget *label = gtk_label_new(_("add darktable sample shortcuts?\n"
                                     "\n"
                                     "this will overwrite any existing shortcuts\n"
                                     "using the same key+mouse combinations!\n"
                                     "you will be able to undo this action during\n"
                                     "the current session by clicking 'restore...'\n"
                                     "and opting to clear all newer shortcuts.\n"
                                     "\n"
                                     "it is recommended to export your current\n"
                                     "shortcuts before adding these.\n"));
  gtk_widget_set_halign(label, GTK_ALIGN_START);
  dt_gui_dialog_add(GTK_DIALOG(dialog), label);
  gtk_widget_show_all(dialog);

  const int resp = gtk_dialog_run(GTK_DIALOG(dialog));
  gtk_widget_destroy(dialog);

  if(resp == GTK_RESPONSE_OK)
  {
    // user asked to import the darktable system shortcuts file (localized, if available)
    char sharedir[PATH_MAX] = { 0 };
    dt_loc_get_sharedir(sharedir, sizeof(sharedir));

    const char *langcode = g_getenv("LANGUAGE");
    gboolean loaded = FALSE;
    if(langcode && *langcode)
    {
      const char *underscore = strchr(langcode,'_');
      const int lang_len = underscore ? underscore - langcode : strlen(langcode);
      gchar *localized_file = g_strdup_printf("darktable/shortcutsrc.%.*s", lang_len, langcode);
      gchar *shortcuts_file = g_build_filename(sharedir, localized_file, NULL);
      if(g_file_test(shortcuts_file, G_FILE_TEST_EXISTS))
      {
        dt_print(DT_DEBUG_PARAMS,"load localized shortcuts from %s", shortcuts_file);
        dt_shortcuts_load(shortcuts_file, FALSE);
        loaded = TRUE;
      }
      g_free(shortcuts_file);
    }
    if(!loaded)
    {
      gchar *shortcuts_file = g_build_filename(sharedir, "darktable/shortcutsrc", NULL);
      dt_print(DT_DEBUG_PARAMS, "load default shortcuts from %s", shortcuts_file);
      dt_shortcuts_load(shortcuts_file, FALSE);
      g_free(shortcuts_file);
    }
    dt_shortcuts_save(NULL, FALSE);
  }
}

static void _notice_clicked(GtkWidget *button,
                            gpointer user_data)
{
  static int times = 0;
  if(++times < 3) return;

  gtk_widget_hide(button);
  dt_conf_set_bool("accel/hide_notice", TRUE);
}

GtkWidget *dt_shortcuts_prefs(GtkWidget *widget)
{
  // Save the shortcuts before editing
  dt_shortcuts_save(".edit", FALSE);

  GtkWidget *widget_or_parent = widget;
  while(!(_selected_action = dt_action_widget(widget_or_parent)) && widget_or_parent)
    widget_or_parent = gtk_widget_get_parent(widget_or_parent);
  darktable.control->element = -1;

  GtkWidget *container = gtk_paned_new(GTK_ORIENTATION_VERTICAL);

  // Building the shortcut treeview
  g_set_weak_pointer(&_shortcuts_store, gtk_tree_store_new(1, G_TYPE_POINTER)); // static

  _add_shortcuts_to_tree();

  gtk_tree_sortable_set_default_sort_func(GTK_TREE_SORTABLE(_shortcuts_store),
                                          _sort_speed_shortcuts, NULL, NULL);
  gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(_shortcuts_store),
                                       GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID,
                                       GTK_SORT_ASCENDING);
  GtkTreeModel *filtered_shortcuts =
    gtk_tree_model_filter_new(GTK_TREE_MODEL(_shortcuts_store), NULL);
  g_object_unref(G_OBJECT(_shortcuts_store));

  gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filtered_shortcuts),
                                         _visible_shortcuts, NULL, NULL);

  GtkTreeView *shortcuts_view =
    GTK_TREE_VIEW(gtk_tree_view_new_with_model(filtered_shortcuts));
  g_object_unref(G_OBJECT(filtered_shortcuts));
  gtk_tree_view_set_search_column(shortcuts_view, 0); // fake column for _search_func
  gtk_tree_view_set_search_equal_func(shortcuts_view, _search_func, shortcuts_view, NULL);
  GtkWidget *search_shortcuts = gtk_search_entry_new();
  gtk_entry_set_placeholder_text(GTK_ENTRY(search_shortcuts),
                                 _("search shortcuts list"));
  gtk_widget_set_tooltip_text(GTK_WIDGET(search_shortcuts),
                              _("incrementally search the list of shortcuts\npress up or down keys to cycle through matches"));
  g_signal_connect(G_OBJECT(search_shortcuts), "activate",
                   G_CALLBACK(dt_gui_search_stop), shortcuts_view);
  g_signal_connect(G_OBJECT(search_shortcuts), "stop-search",
                   G_CALLBACK(dt_gui_search_stop), shortcuts_view);
  gtk_tree_view_set_search_entry(shortcuts_view, GTK_ENTRY(search_shortcuts));

  gtk_tree_selection_set_select_function(gtk_tree_view_get_selection(shortcuts_view),
                                         _shortcut_selection_function, NULL, NULL);
  g_object_set(shortcuts_view, "has-tooltip", TRUE, NULL);
  gtk_widget_set_name(GTK_WIDGET(shortcuts_view), "shortcuts_view");
  g_signal_connect(G_OBJECT(shortcuts_view), "row-activated",
                   G_CALLBACK(_shortcut_row_activated), filtered_shortcuts);
  g_signal_connect(G_OBJECT(shortcuts_view), "key-press-event",
                   G_CALLBACK(_view_key_pressed), search_shortcuts);
  g_signal_connect(G_OBJECT(_shortcuts_store), "row-inserted",
                   G_CALLBACK(_shortcut_row_inserted), shortcuts_view);

  // Setting up the cell renderers
  _add_prefs_column(shortcuts_view, gtk_cell_renderer_text_new(),
                    _("shortcut"), SHORTCUT_VIEW_DESCRIPTION);

  _add_prefs_column(shortcuts_view, gtk_cell_renderer_text_new(),
                    _("action"), SHORTCUT_VIEW_ACTION);

  GtkCellRenderer *renderer = NULL;

  renderer = gtk_cell_renderer_combo_new();
  GtkListStore *elements = gtk_list_store_new(1, G_TYPE_STRING);
  g_object_set(renderer, "model", elements, "text-column", 0, "has-entry", FALSE, NULL);
  g_signal_connect(renderer, "editing-started" ,
                   G_CALLBACK(_element_editing_started), filtered_shortcuts);
  g_signal_connect(renderer, "changed", G_CALLBACK(_element_changed), filtered_shortcuts);
  _add_prefs_column(shortcuts_view, renderer, _("element"), SHORTCUT_VIEW_ELEMENT);

  renderer = gtk_cell_renderer_combo_new();
  GtkListStore *effects = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT);
  g_object_set(renderer, "model", effects, "text-column", 0, "has-entry", FALSE, NULL);
  g_signal_connect(renderer, "editing-started" ,
                   G_CALLBACK(_effect_editing_started), filtered_shortcuts);
  g_signal_connect(renderer, "changed", G_CALLBACK(_effect_changed), filtered_shortcuts);
  _add_prefs_column(shortcuts_view, renderer, _("effect"), SHORTCUT_VIEW_EFFECT);

  renderer = gtk_cell_renderer_spin_new();
  g_object_set(renderer, "adjustment", gtk_adjustment_new(1, -1000, 1000, .01, 1, 10),
                         "digits", 3, "xalign", 1.0, NULL);
  g_signal_connect(renderer, "edited", G_CALLBACK(_speed_edited), filtered_shortcuts);
  dt_gui_commit_on_focus_loss(renderer, NULL);
  _add_prefs_column(shortcuts_view, renderer, _("speed"), SHORTCUT_VIEW_SPEED);

  renderer = gtk_cell_renderer_combo_new();
  GtkListStore *instances = gtk_list_store_new(1, G_TYPE_STRING);
  for(int i = 0; i < NUM_INSTANCES; i++)
    gtk_list_store_insert_with_values(instances, NULL, -1, 0, _(instance_label[i]), -1);
  for(char relative[] = "-2"; (relative[0] ^= '+' ^ '-') == '-' || ++relative[1] <= '9'; )
    gtk_list_store_insert_with_values(instances, NULL, -1, 0, relative, -1);
  g_object_set(renderer, "model", instances, "text-column", 0, "has-entry", FALSE, NULL);
  g_signal_connect(renderer, "edited", G_CALLBACK(_instance_edited), filtered_shortcuts);
  _add_prefs_column(shortcuts_view, renderer, _("instance"), SHORTCUT_VIEW_INSTANCE);

  // Adding the shortcuts treeview to its containers
  GtkWidget *scroll = dt_gui_scroll_wrap(GTK_WIDGET(shortcuts_view));
  gtk_widget_set_size_request(scroll, -1, 100);
  gtk_paned_pack2(GTK_PANED(container), scroll, TRUE, FALSE);

  // Creating the action selection treeview
  g_set_weak_pointer(&_actions_store, gtk_tree_store_new(2, G_TYPE_POINTER, G_TYPE_STRING)); // static
  GtkTreeIter found_iter = {};
  if(widget && !_selected_action)
  {
    const dt_view_t *active_view = dt_view_manager_get_current_view(darktable.view_manager);
    if(gtk_widget_is_ancestor(widget, dt_ui_center_base(darktable.gui->ui)) ||
       dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_CENTER_TOP, widget) ||
       dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_CENTER_BOTTOM, widget) ||
       gtk_widget_is_ancestor(widget, GTK_WIDGET(dt_ui_get_container(darktable.gui->ui,
                                               DT_UI_CONTAINER_PANEL_LEFT_TOP))) ||
       gtk_widget_is_ancestor(widget, GTK_WIDGET(dt_ui_get_container(darktable.gui->ui,
                                               DT_UI_CONTAINER_PANEL_RIGHT_TOP))))
      _selected_action = (dt_action_t*)active_view;
    else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_BOTTOM, widget))
      _selected_action = &darktable.control->actions_thumb;
    else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_RIGHT, widget))
      _selected_action = active_view->view(active_view) == DT_VIEW_DARKROOM
                       ? &darktable.control->actions_iops
                       : &darktable.control->actions_libs;
    else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_LEFT, widget))
      _selected_action = &darktable.control->actions_libs;
    else
      _selected_action = &darktable.control->actions_global;
  }
  _add_actions_to_tree(NULL, darktable.control->actions, _selected_action, &found_iter);

  GtkTreeView *actions_view =
    GTK_TREE_VIEW(gtk_tree_view_new_with_model(GTK_TREE_MODEL(_actions_store)));
  g_object_unref(_actions_store);
  gtk_tree_view_set_search_column(actions_view, 1); // fake column for _search_func
  gtk_tree_view_set_search_equal_func(actions_view, _search_func, actions_view, NULL);
  GtkWidget *search_actions = gtk_search_entry_new();
  gtk_entry_set_placeholder_text(GTK_ENTRY(search_actions),
                                 _("search actions list"));
  gtk_widget_set_tooltip_text(GTK_WIDGET(search_actions),
                              _("incrementally search the list of actions\npress up or down keys to cycle through matches"));
  g_signal_connect(G_OBJECT(search_actions), "activate",
                   G_CALLBACK(dt_gui_search_stop), actions_view);
  g_signal_connect(G_OBJECT(search_actions), "stop-search",
                   G_CALLBACK(dt_gui_search_stop), actions_view);
  gtk_tree_view_set_search_entry(actions_view, GTK_ENTRY(search_actions));

  g_object_set(actions_view, "has-tooltip", TRUE, NULL);
  gtk_widget_set_name(GTK_WIDGET(actions_view), "actions_view");
  g_signal_connect(G_OBJECT(actions_view), "row-activated",
                   G_CALLBACK(_action_row_activated), _actions_store);
  g_signal_connect(G_OBJECT(actions_view), "button-press-event",
                   G_CALLBACK(_action_view_click), _actions_store);
  g_signal_connect(G_OBJECT(actions_view), "key-press-event",
                   G_CALLBACK(_view_key_pressed), search_actions);

  g_signal_connect(G_OBJECT(gtk_tree_view_get_selection(actions_view)), "changed",
                   G_CALLBACK(_action_selection_changed), shortcuts_view);
  g_signal_connect(G_OBJECT(gtk_tree_view_get_selection(shortcuts_view)), "changed",
                   G_CALLBACK(_shortcut_selection_changed), actions_view);

  renderer = gtk_cell_renderer_text_new();
  GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_("action"),
                                                                       renderer, NULL);
  gtk_tree_view_column_set_expand(column, TRUE);
  gtk_tree_view_column_set_cell_data_func(column, renderer,
                                          _fill_action_fields, GINT_TO_POINTER(TRUE), NULL);
  gtk_tree_view_append_column(GTK_TREE_VIEW(actions_view), column);

  renderer = gtk_cell_renderer_text_new();
  column = gtk_tree_view_column_new_with_attributes(_("type"), renderer, NULL);
  gtk_tree_view_column_set_alignment(column, 1.0);
  gtk_cell_renderer_set_alignment(renderer, 1.0, 0.0);
  gtk_tree_view_column_set_cell_data_func(column, renderer,
                                          _fill_action_fields, NULL, NULL);
  gtk_tree_view_append_column(GTK_TREE_VIEW(actions_view), column);

  // Adding the action treeview to its containers
  scroll = dt_gui_scroll_wrap(GTK_WIDGET(actions_view));
  gtk_widget_set_size_request(scroll, -1, 100);
  gtk_paned_pack1(GTK_PANED(container), scroll, TRUE, FALSE);

  if(found_iter.user_data)
  {
    GtkTreeIter *send_iter = calloc(1, sizeof(GtkTreeIter));
    *send_iter = found_iter;
    g_signal_connect_data(G_OBJECT(actions_view), "show",
                          G_CALLBACK(_action_view_show),
                          send_iter, (GClosureNotify)g_free, G_CONNECT_AFTER);
  }

  GtkTreePath *path = gtk_tree_path_new_first();
  gtk_tree_view_set_cursor(shortcuts_view, path, NULL, FALSE);
  gtk_tree_path_free(path);

  const int split_position = dt_conf_get_int("shortcuts/window_split");
  if(split_position) gtk_paned_set_position(GTK_PANED(container), split_position);
  g_signal_connect(G_OBJECT(container), "notify::position",
                   G_CALLBACK(_resize_shortcuts_view), container);

  GtkWidget *btn_fallbacks = gtk_check_button_new_with_label(_("enable fallbacks"));
  gtk_widget_set_tooltip_text(btn_fallbacks, _("enables default meanings for additional buttons, modifiers or moves\n"
                                               "when used in combination with a base shortcut"));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn_fallbacks),
                               darktable.control->enable_fallbacks);
  g_signal_connect(btn_fallbacks, "toggled", G_CALLBACK(_fallbacks_toggled), shortcuts_view);

  GtkWidget *btn_help = gtk_button_new_with_label(_("?"));
  gtk_widget_set_tooltip_text(btn_help, _("open help page for shortcuts"));
  dt_gui_add_help_link(btn_help, "shortcuts");
  g_signal_connect(btn_help, "clicked", G_CALLBACK(dt_gui_show_help), NULL);

  GtkWidget *btn_restore = gtk_button_new_with_label(_("restore..."));
  gtk_widget_set_tooltip_text(btn_restore, _("restore default shortcuts or previous state"));
  g_signal_connect(btn_restore, "clicked", G_CALLBACK(_restore_clicked), NULL);

  GtkWidget *btn_importx = gtk_button_new_with_label(_("import extras"));
  gtk_widget_set_tooltip_text(btn_importx, _("import extended default shortcuts"));
  g_signal_connect(btn_importx, "clicked", G_CALLBACK(_import_extended_clicked), NULL);

  GtkWidget *btn_import = gtk_button_new_with_label(_("import..."));
  gtk_widget_set_tooltip_text(btn_import, _("fully or partially import shortcuts from file"));
  g_signal_connect(btn_import, "clicked", G_CALLBACK(_import_clicked), NULL);

  GtkWidget *btn_export = gtk_button_new_with_label(_("export..."));
  gtk_widget_set_tooltip_text(btn_export, _("fully or partially export shortcuts to file"));
  g_signal_connect(btn_export, "clicked", G_CALLBACK(_export_clicked), NULL);

  GtkWidget *button_bar = dt_gui_hbox(search_actions, search_shortcuts, btn_fallbacks,
                                      dt_gui_align_right(btn_help), btn_export, btn_importx,
                                      btn_import, btn_restore);
  gtk_widget_set_name(button_bar, "shortcut-controls");

  GtkWidget *top_level = dt_gui_vbox();
  if(!dt_conf_get_bool("accel/hide_notice"))
  {
    GtkWidget *button = gtk_button_new_with_label(
      _("the recommended way to assign shortcuts to visual elements is the <b>visual shortcut mapping</b> mode.\n"
        "this is switched on by toggling the <i>\"keyboard\"</i> button next to preferences in the top panel. "
        "in this mode, clicking on a widget or area will open this dialog with the appropriate selection for advanced configuration.\n\n"
        "multiple shortcuts can be assigned to the same action. "
        "this is especially useful if it has multiple <i>elements</i>, like the module buttons or the colorpickers attached to sliders. "
        "however, with <i>fallbacks</i> enabled one can use the same simple shortcuts and "
        "change their <i>element</i> or <i>effect</i> by adding mouse clicks.\n\n"
        "<i>click <b> three times </b> to dismiss this notice permanently</i>"));
    gtk_widget_set_hexpand(button, TRUE);
    GtkLabel *label = GTK_LABEL(gtk_bin_get_child(GTK_BIN(button)));
    gtk_label_set_use_markup(label, TRUE);
    gtk_label_set_line_wrap(label, TRUE);
    gtk_label_set_xalign(label, 0);
    g_signal_connect(button, "clicked", G_CALLBACK(_notice_clicked), NULL);
    dt_gui_box_add(top_level, button);
  }
  dt_gui_box_add(top_level, container, button_bar);

  return top_level;
}

static void _shortcuts_save(const gchar *shortcuts_file, const dt_input_device_t device)
{
  FILE *f = g_fopen(shortcuts_file, "wb");
  if(f)
  {
    for(GSequenceIter *i = g_sequence_get_begin_iter(darktable.control->shortcuts);
        !g_sequence_iter_is_end(i);
        i = g_sequence_iter_next(i))
    {
      dt_shortcut_t *s = g_sequence_get(i);

      if(device != DT_ALL_DEVICES &&
         (device != 0 ||  s->key_device != device || s->move_device != device) &&
         (device == 0 || (s->key_device != device && s->move_device != device)))
        continue;

      gchar *key_name = _shortcut_key_move_name(s->key_device, s->key, s->mods, FALSE);
      fprintf(f, "%s", key_name);
      g_free(key_name);

      if(s->move_device || s->move)
      {
        gchar *move_name = _shortcut_key_move_name(s->move_device, s->move, DT_MOVE_NAME, FALSE);
        fprintf(f, ";%s", move_name);
        g_free(move_name);
        if(s->direction)
          fprintf(f, ";%s", s->direction & DT_SHORTCUT_UP ? "up" : "down");
      }

      if(s->press  & DT_SHORTCUT_DOUBLE ) fprintf(f, ";%s", "double");
      if(s->press  & DT_SHORTCUT_TRIPLE ) fprintf(f, ";%s", "triple");
      if(s->press  & DT_SHORTCUT_LONG   ) fprintf(f, ";%s", "long");
      if(s->button & DT_SHORTCUT_LEFT   ) fprintf(f, ";%s", "left");
      if(s->button & DT_SHORTCUT_MIDDLE ) fprintf(f, ";%s", "middle");
      if(s->button & DT_SHORTCUT_RIGHT  ) fprintf(f, ";%s", "right");
      if(s->click  & DT_SHORTCUT_DOUBLE ) fprintf(f, ";%s", "double");
      if(s->click  & DT_SHORTCUT_TRIPLE ) fprintf(f, ";%s", "triple");
      if(s->click  & DT_SHORTCUT_LONG   ) fprintf(f, ";%s", "long");

      fprintf(f, "=");

      if(!s->views)
        fprintf(f, "disabled;");

      gchar *action_label = _action_full_id(s->action);
      fprintf(f, "%s", action_label);
      g_free(action_label);

      const dt_action_element_def_t *elements = _action_find_elements(s->action);
      if(s->element)
        fprintf(f, ";%s", NQ_(elements[s->element].name));
      if(elements && s->effect > _shortcut_default_effect(s))
      {
        const gchar *cef =
          _action_find_effect_combo(s->action, &elements[s->element], s->effect);
        if(cef)
          fprintf(f, ";item:%s", NQ_(cef));
        else
          fprintf(f, ";%s", NQ_(elements[s->element].effects[s->effect]));
     }

      if(s->instance == -1) fprintf(f, ";last");
      if(s->instance == +1) fprintf(f, ";first");
      if(abs(s->instance) > 1) fprintf(f, ";%+d", s->instance);
      if(s->speed != 1.0) fprintf(f, ";*%g", s->speed);

      fprintf(f, "\n");
    }

    fclose(f);
  }
}

void dt_shortcuts_save(const gchar *ext,
                       const gboolean backup)
{
  char shortcuts_file[PATH_MAX] = { 0 };
  dt_loc_get_user_config_dir(shortcuts_file, sizeof(shortcuts_file));
  g_strlcat(shortcuts_file, "/shortcutsrc", PATH_MAX);
  if(ext) g_strlcat(shortcuts_file, ext, PATH_MAX);
  if(backup)
  {
    gchar *backup_file = g_strdup_printf("%s.backup", shortcuts_file);
    g_rename(shortcuts_file, backup_file);
    g_free(backup_file);
  }

  _shortcuts_save(shortcuts_file, DT_ALL_DEVICES);
}

static gboolean _find_combo_effect(const gchar **effects,
                                   const gchar *token,
                                   dt_action_t *ac,
                                   gint *ef)
{
  if(effects == dt_action_effect_selection && g_strstr_len(token, 5, "item:"))
  {
    int effect = -1;
    const char *entry = NULL;

    dt_introspection_type_enum_tuple_t *values
      = g_hash_table_lookup(darktable.bauhaus->combo_introspection, ac);
    if(values)
    {
      while((entry = (values[++effect].description
                      ? values[effect].description : values[effect].name)))
        if(!g_ascii_strcasecmp(token + 5, NQ_(entry))) break;
    }
    else
    {
      gchar **strings
        = g_hash_table_lookup(darktable.bauhaus->combo_list, ac);
      if(strings)
      {
        while((entry = strings[++effect]))
          if(!g_ascii_strcasecmp(token + 5, NQ_(entry))) break;
      }
    }
    if(entry)
    {
      *ef = effect + DT_ACTION_EFFECT_COMBO_SEPARATOR + 1;
      return TRUE;
    }
  }

  return FALSE;
}

static void _shortcuts_load(const gchar *shortcuts_file,
                            dt_input_device_t file_dev,
                            const dt_input_device_t load_dev,
                            const gboolean clear)
{
  // start with an empty shortcuts collection
  if(clear && darktable.control->shortcuts)
  {
    if(_shortcuts_store) gtk_tree_store_clear(_shortcuts_store);

    g_sequence_free(darktable.control->shortcuts);
    darktable.control->shortcuts = g_sequence_new(g_free);

    if(_shortcuts_store) _add_shortcuts_to_tree();
  }

  FILE *f = g_fopen(shortcuts_file, "rb");
  if(f)
  {
    while(!feof(f))
    {
      char line[1024];
      char *read = fgets(line, sizeof(line), f);
      if(read > 0)
      {
        line[strcspn(line, "\r\n")] = '\0';

        char *act_start = strchr(line, '=');
        if(!act_start)
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "[dt_shortcuts_load] line '%s' is not an assignment",
                   line);
          continue;
        }

        dt_shortcut_t s = { .speed = 1 };

        char *token = strtok(line, "=;");
        if(g_ascii_strcasecmp(token, "None"))
        {
          char *colon = strchr(token, ':');
          if(!colon)
          {
            gtk_accelerator_parse(token, &s.key, &s.mods);
            if(s.mods)
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] unexpected modifiers found in %s",
                       token);
            if(!s.key && sscanf(token, "tablet button %u", &s.key))
              s.key_device = DT_SHORTCUT_DEVICE_TABLET;
            if(!s.key)
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] no key name found in %s",
                       token);
          }
          else
          {
            char *key_start = colon + 1;
            *colon-- = 0;
            if(colon == token)
            {
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] missing driver name in %s", token);
              continue;
            }
            dt_input_device_t id = *colon - '0';
            if(id > 9 )
              id = 0;
            else
              *colon-- = 0;

            GSList *driver = darktable.control->input_drivers;
            while(driver)
            {
              id += 10;
              dt_input_driver_definition_t *callbacks = driver->data;
              if(!g_ascii_strcasecmp(token, callbacks->name))
              {
                if(!callbacks->string_to_key(key_start, &s.key))
                  dt_print(DT_DEBUG_ALWAYS,
                           "[dt_shortcuts_load] key not recognised in %s", key_start);

                s.key_device = id;
                break;
              }
              driver = driver->next;
            }
            if(!driver)
            {
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] '%s' is not a valid driver", token);
              continue;
            }
          }
        }

        while((token = strtok(NULL, "=;")) && token < act_start)
        {
          char *colon = strchr(token, ':');
          if(!colon)
          {
            int mod = -1;
            while(modifier_string[++mod].modifier)
              if(!g_ascii_strcasecmp(token, modifier_string[mod].name)) break;
            if(modifier_string[mod].modifier)
            {
              s.mods |= modifier_string[mod].modifier;
              continue;
            }

            if(!g_ascii_strcasecmp(token, "left"  )) { s.button |= DT_SHORTCUT_LEFT  ; continue; }
            if(!g_ascii_strcasecmp(token, "middle")) { s.button |= DT_SHORTCUT_MIDDLE; continue; }
            if(!g_ascii_strcasecmp(token, "right" )) { s.button |= DT_SHORTCUT_RIGHT ; continue; }

            if(s.button)
            {
              if(!g_ascii_strcasecmp(token, "double")) { s.click |= DT_SHORTCUT_DOUBLE; continue; }
              if(!g_ascii_strcasecmp(token, "triple")) { s.click |= DT_SHORTCUT_TRIPLE; continue; }
              if(!g_ascii_strcasecmp(token, "long"  )) { s.click |= DT_SHORTCUT_LONG  ; continue; }
            }
            else
            {
              if(!g_ascii_strcasecmp(token, "double")) { s.press |= DT_SHORTCUT_DOUBLE; continue; }
              if(!g_ascii_strcasecmp(token, "triple")) { s.press |= DT_SHORTCUT_TRIPLE; continue; }
              if(!g_ascii_strcasecmp(token, "long"  )) { s.press |= DT_SHORTCUT_LONG  ; continue; }
            }

            int move = 0;
            while(move_string[++move])
              if(!g_ascii_strcasecmp(token, move_string[move])) break;
            if(move_string[move])
            {
              s.move = move;
              continue;
            }

            if(!g_ascii_strcasecmp(token, "up"  )) { s.direction = DT_SHORTCUT_UP  ; continue; }
            if(!g_ascii_strcasecmp(token, "down")) { s.direction= DT_SHORTCUT_DOWN; continue; }

            dt_print(DT_DEBUG_ALWAYS,
                     "[dt_shortcuts_load] token '%s' not recognised", token);
          }
          else
          {
            char *move_start = colon + 1;
            *colon-- = 0;
            if(colon == token)
            {
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] missing driver name in %s", token);
              continue;
            }
            dt_input_device_t id = *colon - '0';
            if(id > 9 )
              id = 0;
            else
              *colon-- = 0;

            GSList *driver = darktable.control->input_drivers;
            while(driver)
            {
              id += 10;
              const dt_input_driver_definition_t *callbacks = driver->data;
              if(!g_ascii_strcasecmp(token, callbacks->name))
              {
                if(!callbacks->string_to_move(move_start, &s.move))
                  dt_print(DT_DEBUG_ALWAYS,
                           "[dt_shortcuts_load] move not recognised in %s", move_start);

                s.move_device = id;
                break;
              }
              driver = driver->next;
            }
            if(!driver)
            {
              dt_print(DT_DEBUG_ALWAYS,
                       "[dt_shortcuts_load] '%s' is not a valid driver", token);
              continue;
            }
          }
        }

        gboolean disable = !g_ascii_strcasecmp(token, "disabled");
        if(disable) token = strtok(NULL, ";");

        s.action = _action_find(token);

        if(!s.action)
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "[dt_shortcuts_load] action path '%s' not found", token);
          continue;
        }

        const dt_action_element_def_t *elements = _action_find_elements(s.action);
        const gchar **effects = NULL;
        const gint default_effect = s.effect = _shortcut_default_effect(&s);

        while((token = strtok(NULL, ";")))
        {
          if(elements)
          {
            int element = -1;
            while(elements[++element].name)
              if(!g_ascii_strcasecmp(token, NQ_(elements[element].name)))
                break;
            if(elements[element].name)
            {
              s.element = element;
              s.effect = default_effect; // reset if an effect for a
                                         // different element was
                                         // found first
              continue;
            }

            effects = elements[s.element].effects;

            if(_find_combo_effect(effects, token, s.action, &s.effect))
              continue;

            int effect = -1;
            while(effects[++effect])
              if(!g_ascii_strcasecmp(token, NQ_(effects[effect]))) break;
            if(effects[effect])
            {
              s.effect = effect;
              continue;
            }
          }

          if(!g_ascii_strcasecmp(token, "first"))
            s.instance =  1;
          else if(!g_ascii_strcasecmp(token, "last" ))
            s.instance = -1;
          else if(*token == '+' || *token == '-')
            sscanf(token, "%d", &s.instance);
          else if(*token == '*')
            sscanf(token, "*%g", &s.speed);
          else
            dt_print(DT_DEBUG_ALWAYS,
                     "[dt_shortcuts_load] token '%s' not recognised", token);
        }

        if(file_dev == DT_ALL_DEVICES ||
           (file_dev == 0 &&  s.key_device == file_dev && s.move_device == file_dev) ||
           (file_dev != 0 && (s.key_device == file_dev || s.move_device == file_dev)))
        {
          if(file_dev != 0)
          {
            if(s.key_device  == file_dev) s.key_device  = load_dev;
            if(s.move_device == file_dev) s.move_device = load_dev;
          }

          _insert_shortcut(&s, FALSE, disable);
        }
      }
    }
    fclose(f);
  }
}

void dt_shortcuts_load(const gchar *ext,
                       const gboolean clear)
{
  char shortcuts_file[PATH_MAX] = { 0 };
  if(!ext || (strchr(ext,'/') == NULL && strchr(ext, G_SEARCHPATH_SEPARATOR) == NULL))
  {
    dt_loc_get_user_config_dir(shortcuts_file, sizeof(shortcuts_file));
    g_strlcat(shortcuts_file, "/shortcutsrc", PATH_MAX);
  }
  if(ext) g_strlcat(shortcuts_file, ext, PATH_MAX);
  if(!g_file_test(shortcuts_file, G_FILE_TEST_EXISTS))
    return;

  _shortcuts_load(shortcuts_file, DT_ALL_DEVICES, DT_ALL_DEVICES, clear);
}

void dt_shortcuts_reinitialise(dt_action_t *action)
{
  dt_control_log(_("reinitialising input devices"));

  for(GSList *d = darktable.control->input_drivers; d; d = d->next)
  {
    const dt_input_driver_definition_t *driver = d->data;
    driver->module->gui_cleanup(driver->module);
    driver->module->gui_init(driver->module);
  }

  // reload shortcuts
  dt_shortcuts_load(NULL, TRUE);

  char actions_file[PATH_MAX] = { 0 };
  dt_loc_get_user_config_dir(actions_file, sizeof(actions_file));
  g_strlcat(actions_file, "/all_actions", PATH_MAX);
  FILE *f = g_fopen(actions_file, "wb");
  _dump_actions(f, darktable.control->actions);
  fclose(f);
}

void dt_shortcuts_select_view(dt_view_type_flags_t view)
{
  g_sequence_sort(darktable.control->shortcuts,
                  _shortcut_compare_func, GINT_TO_POINTER(view));
}

static GSList *_pressed_keys = NULL, *_hold_keys = NULL; // lists of currently pressed and held keys
static guint _pressed_button = 0;
static guint _last_time = 0; // time of key or button press
                             // used to determine if release should trigger action
                             // set to 0 by any intermediate move (so no action on release)
static guint  _last_mapping_time = 0;
static guint _timeout_source = 0;
static guint _focus_loss_key = 0;
static guint _focus_loss_press = 0;

static dt_action_t _value_action =
  { .type = DT_ACTION_TYPE_FALLBACK,
    .target = GINT_TO_POINTER(DT_ACTION_TYPE_VALUE_FALLBACK) };

static void _lookup_mapping_widget()
{
  if(_sc.action) return;
  _sc.action = dt_action_widget(darktable.control->mapping_widget);
  if(!_sc.action) return;

  _sc.instance = 0;
  if(dt_conf_get_bool("accel/assign_instance"))
    _find_relative_instance(_sc.action, darktable.control->mapping_widget, &_sc.instance);

  _sc.element = 0;
  const dt_action_def_t *def = _action_find_definition(_sc.action);
  if(def && def->elements && def->elements[0].name && darktable.control->element > 0)
    _sc.element = darktable.control->element;
}

gboolean dt_action_widget_invisible(GtkWidget *w)
{
  GtkWidget *p = gtk_widget_get_parent(w);
  return (!GTK_IS_WIDGET(w) || !gtk_widget_get_visible(w) || (!gtk_widget_get_visible(p)
          && strcmp(gtk_widget_get_name(p), "collapsible")
          && !gtk_style_context_has_class(gtk_widget_get_style_context(p),
                                          "dt_plugin_ui_main")));
}

#define ADD_EXPLANATION(cause, effect, extra, ...) if(*fb_log) \
      dt_util_str_cat(&*fb_log, "\n%s \u2192 %s" extra, cause, effect, ##__VA_ARGS__)

static gboolean _unmatched_move(dt_shortcut_t *c,
                                dt_shortcut_t *s,
                                const dt_action_element_def_t **elements,
                                char **fb_log)
{
  if(c->move == s->move) return FALSE;
  if(!darktable.control->enable_fallbacks ||
     !c->move_device || c->move > s->move) return TRUE; // don't try to match mouse moves
  *elements = _action_find_elements(c->action);
  if(!*elements) return TRUE;
  int skipped = s->move - c->move;
  if((*elements)[c->element].effects == (*elements)[c->element+1].effects)
    s->element = c->element + skipped;
  else
  {
    s->action = c->action;
    dt_action_type_t tp = s->action->type;
    while(skipped && (s->action = s->action->next))
      if(s->action->type == tp) --skipped;
    if(skipped) return TRUE;
  }
  ADD_EXPLANATION(_("move not assigned"), _("fallback to earlier move"), " %d -> %d", s->move, c->move);

  s->move = c->move;
  return FALSE;
}

static gboolean _shortcut_closest_match(GSequenceIter **current,
                                        dt_shortcut_t *s,
                                        gboolean *fully_matched,
                                        const dt_action_element_def_t **elements,
                                        char **fb_log)
{
  dt_shortcut_t *c = NULL;

  do
  {
    *current = g_sequence_iter_prev(*current);
    c = g_sequence_get(*current);

    if(c->key_device != s->key_device ||
       c->key != s->key ||
       c->press < (s->press & ~DT_SHORTCUT_LONG) ||
       ((c->move_device || c->move) &&
        (c->move_device != s->move_device ||
         _unmatched_move(c, s, elements, fb_log))) ||
       (s->action &&
        s->action->type == DT_ACTION_TYPE_FALLBACK &&
        s->action->target != c->action->target))
    {
      *fully_matched = FALSE;
      return FALSE;
    }
  }
  while(!g_sequence_iter_is_begin(*current) &&
        (((c->button || c->click) &&
          (c->button != s->button || c->click != s->click)) ||
         (c->mods       && c->mods != s->mods) ||
         (c->direction  & ~s->direction      ) ||
         (c->effect > 0 && s->effect > 0     ) ||
         (c->instance   && s->instance       ) ||
         (c->element && s->effect > 0 && *elements &&
          (*elements)[c->element].effects != (*elements)[s->element].effects)));

  s->key_device   =  0;
  s->key          =  0;
  s->mods        &= ~c->mods;
  s->press       -=  c->press;
  s->button      &= ~c->button;
  s->click       -=  c->click;
  s->direction   &= ~c->direction;
  s->move_device -=  c->move_device;
  s->move        -=  c->move;

  if(ELEMENT_IS(value, s, *elements) && s->effect == DT_ACTION_EFFECT_SET)
  {
    s->effect = DT_ACTION_EFFECT_DEFAULT_KEY;
    s->speed = 1.0f;
  }
  if(!s->action || c->effect != DT_ACTION_EFFECT_DEFAULT_KEY)
    s->effect = c->effect;
  if(!s->element) s->element = c->element;
  if(c->instance) s->instance = c->instance;

  if(!s->action) s->action = c->action;
  if(!*elements) *elements = _action_find_elements(s->action);

  if(ELEMENT_IS(value, s, *elements) && c->effect == DT_ACTION_EFFECT_SET)
    s->speed = c->speed;
  else
    s->speed *= c->speed;

  *fully_matched = !(s->mods || s->press || s->button || s->click || s->move_device || s->move);

  ADD_EXPLANATION(_shortcut_description(c), _action_description(c, 2),);

  if(s->direction == DT_SHORTCUT_DOWN
      && (s->effect == DT_ACTION_EFFECT_LAST || s->effect == DT_ACTION_EFFECT_FIRST)
      && (ELEMENT_IS(value, s, *elements) || ELEMENT_IS(selection, s, *elements)))
  {
    s->effect = s->effect == DT_ACTION_EFFECT_LAST ? DT_ACTION_EFFECT_FIRST : DT_ACTION_EFFECT_LAST;
    ADD_EXPLANATION(_("down move"), _("flip top/bottom first/last"),);
  }

  return TRUE;
}

static gboolean _shortcut_match(dt_shortcut_t *f,
                                gchar **fb_log)
{
  f->views = dt_view_get_current();
  gpointer v = GINT_TO_POINTER(f->views);

  GSequenceIter *existing =
    g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);

  gboolean matched = FALSE;

  const dt_action_element_def_t *elements = NULL;

  if(!_shortcut_closest_match(&existing, f, &matched, &elements, fb_log))
  {
    // see if there is a fallback from midi knob press to knob turn
    if(!f->key_device || f->move_device || f->move)
      return FALSE;

    dt_input_device_t id = f->key_device;
    GSList *driver = darktable.control->input_drivers;
    while(driver && (id -= 10) >= 10)
      driver = driver->next;

    if(!driver)
      return FALSE;
    else
    {
      dt_input_driver_definition_t *callbacks = driver->data;

      if(callbacks->key_to_move &&
         callbacks->key_to_move(callbacks->module, f->key_device, f->key, &f->move))
      {
        f->move_device = f->key_device;
        f->key_device = 0;
        f->key = 0;

        existing =
          g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
        if(!_shortcut_closest_match(&existing, f, &matched, &elements, fb_log)
           && !f->action)
          return FALSE;
        else
        {
          if(ELEMENT_IS(value, f, elements) || ELEMENT_IS(selection, f, elements))
            f->effect = DT_ACTION_EFFECT_RESET;
        }
      }
    }
  }

  if(!matched && f->action && darktable.control->enable_fallbacks)
  {
    // try to add fallbacks
    f->views = DT_VIEW_FALLBACK;

    dt_action_t *matched_action = f->action;
    dt_action_t fallback_action = { .type = DT_ACTION_TYPE_FALLBACK,
                                    .target = GINT_TO_POINTER(matched_action->type) };
    f->action = &fallback_action;

    existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
    while(_shortcut_closest_match(&existing, f, &matched, &elements, fb_log)
          && !matched) {};

    if(!matched && ELEMENT_IS(value, f, elements))
    {
      f->action = &_value_action;
      existing = g_sequence_search(darktable.control->shortcuts, f, _shortcut_compare_func, v);
      while(_shortcut_closest_match(&existing, f, &matched, &elements, fb_log)
            && !matched) {};
    }

    if(f->move && !f->move_device && !(f->mods || f->press || f->button || f->click))
    {
      ADD_EXPLANATION(_shortcut_description(f), _("fallback to move"),);

      if(ELEMENT_IS(value, f, elements) && f->effect == DT_ACTION_EFFECT_SET)
        f->speed = 1.0f;
      f->effect = DT_ACTION_EFFECT_DEFAULT_MOVE;
      f->move = 0;
    }

    f->action = matched_action;
  }

  return f->action != NULL && !f->move;
}


static float _process_action(dt_action_t *action,
                             int instance,
                             dt_action_element_t element,
                             dt_action_effect_t effect,
                             float move_size,
                             gchar **fb_log)
{
  float return_value = DT_ACTION_NOT_VALID;

  dt_action_t *owner = action;
  while(owner && owner->type >= DT_ACTION_TYPE_SECTION)
    owner = owner->owner;

  gpointer action_target = action->type == DT_ACTION_TYPE_LIB ? action : action->target;

  if(owner && owner->type == DT_ACTION_TYPE_IOP)
  {
    // find module instance
    dt_iop_module_so_t *module = (dt_iop_module_so_t *)owner;
    dt_iop_module_t *mod = NULL;

    if(owner == &darktable.control->actions_focus)
    {
      action_target = mod = dt_dev_gui_module();
      if(action->type != DT_ACTION_TYPE_IOP)
      {
        action_target = dt_dev_modulegroups_test_activated(darktable.develop)
                      ? mod ? (gpointer)mod->widget : NULL
                      : dt_ui_get_container(darktable.gui->ui, DT_UI_CONTAINER_PANEL_RIGHT_CENTER);
      }
      if(!action_target)
        return return_value;
    }
    else if(instance)
    {
      int current_instance = abs(instance);

      for(GList *iop_mods = instance >= 0
                          ? darktable.develop->iop
                          : g_list_last(darktable.develop->iop);
          iop_mods;
          iop_mods = instance >= 0
                  ? g_list_next(iop_mods)
                  : g_list_previous(iop_mods))
      {
        mod = (dt_iop_module_t *)iop_mods->data;

        if(mod->so == module
            && mod->iop_order != INT_MAX
            && !--current_instance)
          break;
      }

      // find module instance widget
      if(mod && action->type >= DT_ACTION_TYPE_PER_INSTANCE)
      {
        for(GSList *w = mod->widget_list; w; w = w->next)
        {
          const dt_action_target_t *referral = w->data;
          if(referral->action == action)
          {
            action_target = referral->target;
            break;
          }
        }
      }
      else
        action_target = mod;
    }
    else if(action->type == DT_ACTION_TYPE_IOP
            || action->type == DT_ACTION_TYPE_PRESET)
    {
      action_target = dt_iop_get_module_preferred_instance((dt_iop_module_so_t *)owner);
    }
  }

  if(action->type == DT_ACTION_TYPE_COMMAND
     && action->target
     && DT_PERFORM_ACTION(move_size))
  {
    ((dt_action_callback_t*)action->target)(action);
  }
  else if(action->type == DT_ACTION_TYPE_PRESET && owner && DT_PERFORM_ACTION(move_size))
  {
    if(owner->type == DT_ACTION_TYPE_LIB)
    {
      const dt_lib_module_t *lib = (dt_lib_module_t *)owner;
      dt_lib_presets_apply(action->id, lib->plugin_name, lib->version());
    }
    else if(owner->type == DT_ACTION_TYPE_IOP)
    {
      dt_action_widget_toast(action_target, NULL, _("\napplying preset '%s'"), action->label);

      dt_gui_presets_apply_preset(action->id, action_target);
    }
    else
      dt_print(DT_DEBUG_ALWAYS,
               "[process_action] preset '%s' has unsupported type", action->label);
  }
  else
  {
    const dt_action_def_t *definition = _action_find_definition(action);

    if(definition && definition->process
        && (action->type < DT_ACTION_TYPE_WIDGET
            || definition->no_widget
            || (action_target && !dt_action_widget_invisible(action_target))))
    {
      if(DT_PERFORM_ACTION(move_size)
         && (definition->elements[element].effects != dt_action_effect_value
             || effect != DT_ACTION_EFFECT_SET))
      {
        dt_shortcut_t s = { .action = action, .views = DT_VIEW_ALL };
        GSequenceIter *speed_adjustment
          = g_sequence_lookup(darktable.control->shortcuts, &s,
                              _shortcut_compare_func, NULL);
        if(speed_adjustment)
        {
          dt_shortcut_t *f = g_sequence_get(speed_adjustment);

          move_size *= f->speed;

          ADD_EXPLANATION(_action_description(f, 2), _("speed"), " = %g", move_size);
        }
      }
      return_value = definition->process(action_target, element, effect, move_size);
    }
#ifdef USE_LUA
    else if(owner == &darktable.control->actions_lua && definition)
    {
      dt_lua_lock();

      lua_State* L= darktable.lua_state.state;

      lua_getfield(L, LUA_REGISTRYINDEX, "dt_lua_mimic_list");
      int stacknum = 1;
      if(lua_isnil(L, -1)) goto lua_end;

      lua_getfield(L, -1, action->id);
      ++stacknum;
      if(lua_isnil(L, -1)) goto lua_end;

      if(!DT_PERFORM_ACTION(move_size))
        move_size = NAN;

      lua_pushstring(L, action->label);
      lua_pushstring(L, definition->elements[element].name);
      lua_pushstring(L, definition->elements[element].effects[effect]);
      lua_pushnumber(L, move_size);

      lua_pcall(L, 4, 1, 0);

      return_value = lua_tonumber(L, -1);

      if(dt_isnan(return_value))
        return_value = DT_ACTION_NOT_VALID;

lua_end:
      lua_pop(L, stacknum);
      dt_lua_unlock();
    }
#endif
    else if(DT_PERFORM_ACTION(move_size))
      dt_action_widget_toast(action, action_target, "not active");
  }

  return return_value;
}

static void _ungrab_grab_widget()
{
  gdk_seat_ungrab(gdk_display_get_default_seat(gdk_display_get_default()));

  g_slist_free_full(_pressed_keys, g_free);
  _pressed_keys = NULL;

  if(_grab_widget)
  {
    gtk_widget_set_sensitive(_grab_widget, TRUE);
    gtk_widget_set_tooltip_text(_grab_widget, NULL);
    g_signal_handlers_disconnect_by_func(gtk_widget_get_toplevel(_grab_widget),
                                         G_CALLBACK(dt_shortcut_dispatcher), NULL);
    _grab_widget = NULL;
  }
}

static void _ungrab_at_focus_loss()
{
  _grab_window = NULL;
  _focus_loss_key = _sc.key;
  _focus_loss_press = _sc.press;
  _ungrab_grab_widget();
  _sc = (dt_shortcut_t) { 0 };
}

static float _process_shortcut(float move_size)
{
  float return_value = DT_ACTION_NOT_VALID;

  dt_print(DT_DEBUG_INPUT | DT_DEBUG_VERBOSE,
            "  [_process_shortcut] processing shortcut: %s",
            _shortcut_description(&_sc));

  if(DT_PERFORM_ACTION(move_size) &&
     gtk_widget_has_grab(darktable.control->progress_system.proxy.module->widget))
  {
    if(_sc.key_device == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE && _sc.key == GDK_KEY_Escape)
      dt_print(DT_DEBUG_ALWAYS, "this should cancel the running blocking job"); // TODO

    return return_value;
  }

  dt_shortcut_t fsc = _sc;
  fsc.action = NULL;
  fsc.element  = 0;

  gchar *fb_log = darktable.control->mapping_widget && DT_PERFORM_ACTION(move_size)
                ? g_strdup_printf("[ %s ]", _shortcut_description(&fsc))
                : NULL;

  if(_shortcut_match(&fsc, &fb_log))
  {
    if(DT_PERFORM_ACTION(move_size))
      move_size *= fsc.speed;

    if(fsc.effect == DT_ACTION_EFFECT_DEFAULT_MOVE)
    {
      if(DT_PERFORM_ACTION(move_size) && move_size < .0f)
      {
        fsc.effect = DT_ACTION_EFFECT_DEFAULT_DOWN;
        move_size *= -1;
      }
      else
        fsc.effect = DT_ACTION_EFFECT_DEFAULT_UP;
    }

    return_value =  _process_action(fsc.action, fsc.instance, fsc.element,
                                    fsc.effect, move_size, &fb_log);
  }
  else if(DT_PERFORM_ACTION(move_size) && !fsc.action)
  {
    dt_toast_log(_("%s not assigned"), _shortcut_description(&_sc));
  }

  if(fb_log)
  {
    dt_control_log("%s", fb_log);
    g_free(fb_log);
  }

  return return_value;
}

float dt_action_process(const gchar *action,
                        int instance,
                        const gchar *element,
                        const gchar *effect,
                        float move_size)
{
  dt_action_t *ac = _action_find(action);

  if(!ac)
  {
    dt_print(DT_DEBUG_ALWAYS, "[dt_action_process] action path '%s' not found", action);
    return DT_ACTION_NOT_VALID;
  }

  if(ac->owner == &darktable.control->actions_lua)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[dt_action_process] lua action '%s' triggered from lua", action);
    return DT_ACTION_NOT_VALID;
  }

  const dt_view_type_flags_t vws = _find_views(ac);
  if(!(vws & dt_view_get_current()))
  {
    if(DT_PERFORM_ACTION(move_size))
      dt_print(DT_DEBUG_ALWAYS,
              "[dt_action_process] action '%s' not valid for current view", action);
    return DT_ACTION_NOT_VALID;
  }

  dt_action_element_t el = DT_ACTION_ELEMENT_DEFAULT;
  dt_action_effect_t ef = DT_ACTION_EFFECT_DEFAULT_KEY;
  if((element && *element) || (effect && *effect))
  {
    const dt_action_element_def_t *elements = _action_find_elements(ac);
    if(elements)
    {
      if(elements == _action_elements_entry
         && (_entry_set_element = element)
         && !g_ascii_strcasecmp("set", effect))
        return _process_action(ac, instance, 0, DT_ACTION_EFFECT_SET, move_size, NULL);

      if(element && *element)
      {
        while(elements[el].name
              && g_ascii_strcasecmp(elements[el].name, element))
          el++;

        if(!elements[el].name)
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "[dt_action_process] element '%s' not valid for action '%s'",
                   element, action);
          return DT_ACTION_NOT_VALID;
        }
      }

      const gchar **effects = elements[el].effects;
      if(effect
         && *effect
         && !_find_combo_effect(effects, effect, ac, &ef))
      {
        while(effects[ef]
              && g_ascii_strcasecmp(effects[ef], effect))
          ef++;

        if(!effects[ef])
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "[dt_action_process] effect '%s' not valid for action '%s'",
                   effect, action);
          return DT_ACTION_NOT_VALID;
        }
      }
    }
  }

  return _process_action(ac, instance, el, ef, move_size, NULL);
}

static gint _cmp_key(const gconstpointer a, const gconstpointer b)
{
  const dt_device_key_t *key_a = a;
  const dt_device_key_t *key_b = b;
  return key_a->key_device != key_b->key_device || key_a->key != key_b->key;
}

static inline void _interrupt_delayed_release(gboolean trigger)
{
  if(_timeout_source)
  {
    g_source_remove(_timeout_source);
    _timeout_source = 0;

    if(trigger)
      dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);

    _sc.button = _pressed_button;
    _sc.click = 0;
  }
}

static guint _key_modifiers_clean(guint mods)
{
  GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
  mods &= GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_MOD5_MASK |
          gdk_keymap_get_modifier_mask(keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
  return mods | dt_modifier_shortcuts;
}

float dt_shortcut_move(dt_input_device_t id, guint time, guint move, float move_size)
{
  if(DT_PERFORM_ACTION(move_size))
    _interrupt_delayed_release(TRUE); // reenters dt_shortcut_move

  _sc.move_device = id;
  _sc.move = move;
  _sc.speed = 1.0;
  _sc.direction = 0;

  if(_shortcut_is_move(&_sc))
  {
    _sc.effect =  DT_ACTION_EFFECT_DEFAULT_MOVE;
    _sc.direction = move_size > 0 ? DT_SHORTCUT_UP : DT_SHORTCUT_DOWN;
  }
  else
    _sc.effect = DT_ACTION_EFFECT_DEFAULT_KEY;

  if(id != DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE)
    _sc.mods = _key_modifiers_clean(dt_key_modifier_state());

  float return_value = 0;
  if(!DT_PERFORM_ACTION(move_size))
    return_value = _process_shortcut(move_size);
  else
  {
    gboolean key_or_button_released = (id == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE
                                      && move == DT_SHORTCUT_MOVE_NONE);
    _previous_move = move;

    if(!key_or_button_released)
      _last_time = 0;

    if(_grab_widget) // in mapping mode end grab immediately after first shortcut
      _ungrab_grab_widget();

    dt_print(DT_DEBUG_INPUT,
             "  [dt_shortcut_move] shortcut received: %s",
             _shortcut_description(&_sc));

    _lookup_mapping_widget();

    if(_sc.action)
    {
      if(!time
         || time > _last_mapping_time + 1000
         || time < _last_mapping_time)
      {
        _last_mapping_time = time;

        // mapping_widget gets cleared by confirmation dialog focus loss
        GtkWidget *mapped_widget = darktable.control->mapping_widget;

        dt_shortcut_t s = _sc;
        if(!_action_find_elements(s.action)) s.effect = DT_ACTION_EFFECT_DEFAULT_KEY;
        if(_insert_shortcut(&s, darktable.control->confirm_mapping, FALSE))
        {
          dt_control_log(_("%s assigned to %s"),
                         _shortcut_description(&s), _action_description(&s, 2));

          if(mapped_widget)
            gtk_widget_trigger_tooltip_query(mapped_widget);
        }

        dt_shortcuts_save(NULL, FALSE);
      }

      _sc.action = NULL;
      _sc.instance = 0;
    }
    else
    {
      if(!_pressed_keys || (key_or_button_released && !_sc.button))
        return_value = _process_shortcut(move_size);
      else
      {
        // pressed_keys can be emptied if losing grab during processing
        for(GSList *k = _pressed_keys; k; k = _pressed_keys ? k->next : NULL)
        {
          const dt_device_key_t *device_key = k->data;
          _sc.key_device = device_key->key_device;
          _sc.key = device_key->key;

          return_value = _process_shortcut(move_size);
        }
      }
    }
  }

  _sc.move_device = 0;
  _sc.move = DT_SHORTCUT_MOVE_NONE;
  _sc.direction = 0;

  return return_value;
}

static gboolean _key_release_delayed(gpointer timed_out)
{
  _timeout_source = 0;

  if(!_pressed_keys)
    _ungrab_grab_widget();

  if(!timed_out)
    dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);

  if(!_pressed_keys)
    _sc = (dt_shortcut_t) { 0 };

  return G_SOURCE_REMOVE;
}

static gboolean _button_release_delayed(gpointer timed_out)
{
  _timeout_source = 0;

  if(!timed_out)
    dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);

  _sc.button = _pressed_button;
  _sc.click = 0;

  return G_SOURCE_REMOVE;
}

gboolean break_stuck = FALSE;

void dt_shortcut_key_press(const dt_input_device_t id,
                           const guint time,
                           const guint key)
{
  dt_device_key_t this_key = { id, key };

  if(g_slist_find_custom(_pressed_keys, &this_key, _cmp_key))
  {
    // if key is still repeating (after return from popup menu) then
    // restore double/triple press state
    if(id == DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE
       && key == _focus_loss_key
       && time < _last_time + 50)
      _sc.press = _focus_loss_press;
    _focus_loss_key = 0;
  }
  else if(g_slist_find_custom(_hold_keys, &this_key, _cmp_key))
  {} // ignore repeating hold key
  else
  {
    if(id) _sc.mods = _key_modifiers_clean(dt_key_modifier_state());

    dt_shortcut_t just_key
      = { .key_device = id,
          .key = key,
          .mods = _sc.mods,
          .views = dt_view_get_current() };

    dt_shortcut_t *s = NULL;
    GSequenceIter *existing
      = g_sequence_lookup(darktable.control->shortcuts, &just_key,
                          _shortcut_compare_func, GINT_TO_POINTER(just_key.views));
    if(existing)
      s = g_sequence_get(existing);
    else
    {
      just_key.mods = 0; // fall back to key without modifiers (for multiple emulated modifiers)
      existing = g_sequence_lookup(darktable.control->shortcuts, &just_key,
                                   _shortcut_compare_func, GINT_TO_POINTER(just_key.views));
      if(existing && (s = g_sequence_get(existing)) &&
         (s->action != darktable.control->actions_modifiers || s->effect != DT_ACTION_EFFECT_HOLD))
        s = NULL;
    }
    if(s
       && !_sc.action
       && s->effect == DT_ACTION_EFFECT_HOLD
       && s->action
       && s->action->type >= DT_ACTION_TYPE_WIDGET
       && !dt_action_widget(darktable.control->mapping_widget))
    {
      const dt_action_def_t *definition = _action_find_definition(s->action);
      if(definition && ELEMENT_IS(hold, s, definition->elements) && definition->process)
      {
        if(darktable.control->mapping_widget)
        {
          dt_control_log("[ %s ]\n%s \u2192 %s", _shortcut_description(&_sc),
                         _shortcut_description(s), _action_description(s, 2));
        }

        definition->process(NULL, s->element, DT_ACTION_EFFECT_ON, 1);

        this_key.hold_def = definition;
        this_key.hold_element = s->element;

        dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t));
        *new_key = this_key;
        _hold_keys = g_slist_prepend(_hold_keys, new_key);

        return;
      }
    }

    gboolean double_press = !dt_gui_long_click(time, _last_time);

    if((id || key)
        && id == _sc.key_device
        && key == _sc.key
        && double_press
        && !(_sc.press & DT_SHORTCUT_TRIPLE))
    {
      _interrupt_delayed_release(FALSE);
      _sc.press += DT_SHORTCUT_DOUBLE;
    }
    else
      _interrupt_delayed_release(TRUE);

    if(!_pressed_keys)
    {
      _lookup_mapping_widget();

      gdk_seat_grab(gdk_display_get_default_seat(gdk_display_get_default()),
                    gtk_widget_get_window(_grab_window ? _grab_window
                                                       : dt_ui_main_window(darktable.gui->ui)),
                    GDK_SEAT_CAPABILITY_ALL, FALSE, NULL, NULL, NULL, NULL);
    }
    else
    {
      if(_sc.action)
      {
        // only one key press allowed while defining shortcut
        _ungrab_grab_widget();
        _sc = (dt_shortcut_t) { 0 };
        return;
      }
    }

    // short press after 2 seconds will clear all keys
    break_stuck = _pressed_keys && time > _last_time + 2000;

    // allow extra time when pressing multiple keys "at same time"
    if(!_pressed_keys || double_press || break_stuck)
      _last_time = time;

    _sc.key_device = id;
    _sc.key = key;
    _sc.button = _pressed_button = 0;
    _sc.click = 0;
    _sc.direction = 0;

    dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t));
    *new_key = this_key;
    _pressed_keys = g_slist_prepend(_pressed_keys, new_key);

    // FIXME: make arrow keys repeat; eventually treat up/down and left/right as move
    if(key == GDK_KEY_Left
       || key == GDK_KEY_Right
       || key == GDK_KEY_Up
       || key == GDK_KEY_Down)
    {
      dt_shortcut_key_release(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, time, key);
    }
  }
}

static void _delay_for_double_triple(guint time, guint is_key)
{
  int delay = 0;
  g_object_get(gtk_settings_get_default(), "gtk-double-click-time", &delay, NULL);

  guint passed_time = time - _last_time;

  if(passed_time > delay)
  {
    _sc.press |= DT_SHORTCUT_LONG & is_key;
    _sc.click |= DT_SHORTCUT_LONG & ~is_key;
  }
  else if(break_stuck && !_sc.button)
  {
    _ungrab_grab_widget();
    dt_control_log(_("short key press resets stuck keys"));
    return;
  }
  else if((is_key ? _sc.press : _sc.click) & DT_SHORTCUT_TRIPLE)
    passed_time += delay;
  else if(!_sc.action) // in mapping mode always wait for double/triple
  {
    // detect if any double or triple press shortcuts exist for this
    // key; otherwise skip delay
    _sc.press += DT_SHORTCUT_DOUBLE & is_key;
    _sc.click += DT_SHORTCUT_DOUBLE & ~is_key;

    _sc.views = dt_view_get_current();
    GSequenceIter *multi =
      g_sequence_search(darktable.control->shortcuts, &_sc, _shortcut_compare_func,
                        GINT_TO_POINTER(_sc.views));
    for(int checks = 2; checks--; multi = g_sequence_iter_prev(multi))
    {
      if(g_sequence_iter_is_end(multi)) continue;

      dt_shortcut_t *m = g_sequence_get(multi);
      if(m && m->key_device == _sc.key_device && m->key == _sc.key &&
         (is_key ? m->press >= _sc.press
                 : m->press == _sc.press
                   && m->button == _sc.button && m->click >= _sc.click))
        break;

      if(_sc.click
         && darktable.control->enable_fallbacks)
      {
        const dt_action_def_t *def = _action_find_definition(m->action);
        if(def && def->fallbacks)
          break;
      }

      if(!checks) passed_time += delay;
    }

    _sc.press -= DT_SHORTCUT_DOUBLE & is_key;
    _sc.click -= DT_SHORTCUT_DOUBLE & ~is_key;
  }

  GSourceFunc delay_func = is_key ? _key_release_delayed
                                  : _button_release_delayed;

  if(passed_time < delay)
    _timeout_source = g_timeout_add(delay - passed_time, delay_func, NULL);
  else
    delay_func(GINT_TO_POINTER(passed_time > 2 * delay)); // call immediately
}

void dt_shortcut_key_release(const dt_input_device_t id,
                             const guint time,
                             const guint key)
{
  dt_device_key_t this_key = { id, key };

  GSList *held_key = g_slist_find_custom(_hold_keys, &this_key, _cmp_key);
  if(held_key)
  {
    dt_device_key_t *held_data = held_key->data;
    held_data->hold_def->process(NULL, held_data->hold_element, DT_ACTION_EFFECT_OFF, 1);
    g_free(held_data);
    _hold_keys = g_slist_delete_link(_hold_keys, held_key);
    return;
  }

  GSList *stored_key = g_slist_find_custom(_pressed_keys, &this_key, _cmp_key);
  if(stored_key)
  {
    _interrupt_delayed_release(TRUE);

    g_free(stored_key->data);
    _pressed_keys = g_slist_delete_link(_pressed_keys, stored_key);

    if(_sc.key_device != id || _sc.key != key)
      break_stuck = FALSE;

    _sc.key_device = id;
    _sc.key = key;

    _delay_for_double_triple(time, -1);
  }
}

gboolean dt_shortcut_key_active(dt_input_device_t id, guint key)
{
  dt_shortcut_t saved_sc = _sc;
  _sc = (dt_shortcut_t) {.key_device = id, .key = key};

  const float value = dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0,
                                       DT_SHORTCUT_MOVE_NONE, DT_READ_ACTION_ONLY);

  _sc = saved_sc;

  return fmodf(value, 1) <= DT_VALUE_PATTERN_ACTIVE || fmodf(value, 2) > .5;
}

static guint _fix_keyval(GdkEvent *event)
{
  guint keyval = 0;
  GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
  gdk_keymap_translate_keyboard_state(keymap, event->key.hardware_keycode, 0, 0,
                                      &keyval, NULL, NULL, NULL);
  return keyval;
}

gboolean dt_shortcut_dispatcher(GtkWidget *w,
                                GdkEvent *event,
                                gpointer user_data)
{
  if((event->type ==  GDK_BUTTON_PRESS || event->type ==  GDK_BUTTON_RELEASE ||
      event->type ==  GDK_DOUBLE_BUTTON_PRESS || event->type ==  GDK_TRIPLE_BUTTON_PRESS)
     && event->button.button > 7)
  {
    if(event->type == GDK_BUTTON_RELEASE)
      dt_shortcut_key_release(DT_SHORTCUT_DEVICE_TABLET,
                              event->button.time, event->button.button - 7);
    else
      dt_shortcut_key_press  (DT_SHORTCUT_DEVICE_TABLET,
                              event->button.time, event->button.button - 7);

    return TRUE;
  }

  if(_pressed_keys == NULL)
  {
    dt_shortcut_t s = { .action = _sc.action };
    gboolean middle_click = event->type == GDK_BUTTON_PRESS
      && event->button.button == GDK_BUTTON_MIDDLE;

    if((middle_click || event->type == GDK_SCROLL)
       && (s.action || (s.action = dt_action_widget(darktable.control->mapping_widget))))
    {
      int delta;
      if(middle_click || dt_gui_get_scroll_unit_delta(&event->scroll, &delta))
      {
        s.speed = middle_click ? -1 : powf(10.0f, delta);

        if(_insert_shortcut(&s, TRUE, FALSE))
          dt_control_log("%s", _action_description(&s, 2));

        dt_shortcuts_save(NULL, FALSE);
      }

      return TRUE;
    }

    if(_grab_widget && event->type == GDK_BUTTON_PRESS)
    {
      _ungrab_grab_widget();
      _sc = (dt_shortcut_t) { 0 };
      return TRUE;
    }

    if(event->type != GDK_KEY_PRESS && event->type != GDK_KEY_RELEASE &&
       event->type != GDK_FOCUS_CHANGE)
      return FALSE;

    if(GTK_IS_WINDOW(w) &&
       (event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE))
    {
      GtkWidget *focused_widget = gtk_window_get_focus(GTK_WINDOW(w));
      if(focused_widget)
      {
        if(gtk_widget_event(focused_widget, event))
          return TRUE;

        if((GTK_IS_ENTRY(focused_widget) || GTK_IS_TREE_VIEW(focused_widget)) &&
           (event->key.keyval == GDK_KEY_Tab ||
            event->key.keyval == GDK_KEY_KP_Tab ||
            event->key.keyval == GDK_KEY_ISO_Left_Tab))
          return FALSE;
      }
    }
  }

  switch(event->type)
  {
  case GDK_KEY_PRESS:
    if(event->key.is_modifier
       || event->key.keyval == GDK_KEY_VoidSymbol
       || event->key.keyval == GDK_KEY_Meta_L
       || event->key.keyval == GDK_KEY_Meta_R
       || event->key.keyval == GDK_KEY_ISO_Level3_Shift)
      return FALSE;

    _sc.mods = _key_modifiers_clean(event->key.state);

    dt_shortcut_key_press(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->key.time, _fix_keyval(event));
    break;
  case GDK_KEY_RELEASE:
    if(event->key.is_modifier || event->key.keyval == GDK_KEY_ISO_Level3_Shift)
    {
      // are we defining shortcuts for fallbacks? just modifiers can be used.
      if(_sc.action && _sc.action->type == DT_ACTION_TYPE_FALLBACK)
      {
        _sc.mods = _key_modifiers_clean(event->key.state);
        dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1);
      }
      return FALSE;
    }

    dt_shortcut_key_release(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->key.time, _fix_keyval(event));
    break;
  case GDK_GRAB_BROKEN:
    if(!event->grab_broken.implicit)
      _ungrab_at_focus_loss();
    return FALSE;
  case GDK_WINDOW_STATE:
    if(!(event->window_state.new_window_state & GDK_WINDOW_STATE_FOCUSED))
      _ungrab_at_focus_loss();
    return FALSE;
  case GDK_FOCUS_CHANGE: // dialog boxes and switch to other app release grab
    if(event->focus_change.in)
      g_set_weak_pointer(&_grab_window, w);
    else
      _ungrab_at_focus_loss();
    return FALSE;
  case GDK_SCROLL:
    _sc.mods = _key_modifiers_clean(event->scroll.state);

    int delta_x, delta_y;
    if(dt_gui_get_scroll_unit_deltas(&event->scroll, &delta_x, &delta_y))
    {
      if(delta_x)
        dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->scroll.time,
                         DT_SHORTCUT_MOVE_PAN, -delta_x);
      if(delta_y)
        dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, event->scroll.time,
                         DT_SHORTCUT_MOVE_SCROLL, -delta_y);
    }
    break;
  case GDK_MOTION_NOTIFY:
    ;
    static gdouble move_start_x = 0, move_start_y = 0, last_distance = 0;

    const gdouble x_move = event->motion.x - move_start_x;
    const gdouble y_move = event->motion.y - move_start_y;
    const gdouble new_distance = x_move * x_move + y_move * y_move;

    static int move_last_time = 0;
    if(move_last_time != _last_time || new_distance < last_distance)
    {
      move_start_x = event->motion.x;
      move_start_y = event->motion.y;
      move_last_time = _last_time;
      last_distance = 0;
      break;
    }

    // might just be an accidental move during a key press or button click
    // possibly different time sources from midi or other devices
    if(event->motion.time > _last_time
       && !dt_gui_long_click(event->motion.time, _last_time))
      break;

    _sc.mods = _key_modifiers_clean(event->motion.state);

    const gdouble step_size = 10;

    const gdouble angle = x_move / (0.001 + y_move);
    gdouble size = trunc(x_move / step_size);
    gdouble y_size = - trunc(y_move / step_size);

    if(size != 0 || y_size != 0)
    {
      guint move = DT_SHORTCUT_MOVE_HORIZONTAL;
      if(fabs(angle) >= 2)
      {
        move_start_x += size * step_size;
        move_start_y = event->motion.y;
      }
      else
      {
        size = y_size;
        move_start_y -= size * step_size;
        if(fabs(angle) < .5)
        {
          move_start_x = event->motion.x;
          move = DT_SHORTCUT_MOVE_VERTICAL;
        }
        else
        {
          move_start_x -= size * step_size * angle;
          move = angle < 0 ? DT_SHORTCUT_MOVE_SKEW : DT_SHORTCUT_MOVE_DIAGONAL;
        }
      }

      if(_previous_move == move || _previous_move == DT_SHORTCUT_MOVE_NONE)
        dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE,
                         event->motion.time, move, size);
      else
        _previous_move = move;
    }
    break;
  case GDK_BUTTON_PRESS:
    _sc.mods = _key_modifiers_clean(event->button.state);

    _pressed_button |= 1 << (event->button.button - 1);
    _interrupt_delayed_release(_sc.button != _pressed_button);
    _sc.button = _pressed_button;
    _sc.click = 0;
    _last_time = event->button.time;
    break;
  case GDK_DOUBLE_BUTTON_PRESS:
    _sc.click |= DT_SHORTCUT_DOUBLE;
    break;
  case GDK_TRIPLE_BUTTON_PRESS:
    _sc.click |= DT_SHORTCUT_TRIPLE;
    break;
  case GDK_BUTTON_RELEASE:
    _pressed_button &= ~(1 << (event->button.button - 1));

    _interrupt_delayed_release(FALSE);

    _delay_for_double_triple(event->button.time, 0);

    _last_time = 0; // important; otherwise releasing two buttons will trigger two actions
                    // also, we seem to be receiving presses and releases twice!?! FIXME
    break;
  default:
    return FALSE;
  }

  return TRUE;
}

void dt_action_insert_sorted(dt_action_t *owner, dt_action_t *new_action)
{
  new_action->owner = owner;

  dt_action_t **insertion_point = (dt_action_t **)&owner->target;

  while(*insertion_point
        && g_ascii_strcasecmp(new_action->id, "preset")
        && (!g_ascii_strcasecmp((*insertion_point)->id, "preset")
            || g_utf8_collate((*insertion_point)->label, new_action->label) <
                 ((*((*insertion_point)->label) == '<' ? 1000 : 0) -
                  (*(        new_action->label) == '<' ? 1000 : 0))))
  {
    insertion_point = &(*insertion_point)->next;
  }
  new_action->next = *insertion_point;
  *insertion_point = new_action;
}

dt_action_t *dt_action_locate(dt_action_t *owner,
                              gchar **path,
                              gboolean create)
{
  gchar *clean_path = NULL;

  dt_action_t *action = owner ? owner->target : darktable.control->actions;
  while(*path)
  {
    if(owner == &darktable.control->actions_lua)
      create = TRUE;

    const gboolean style_or_preset =
      owner
      && owner->type == DT_ACTION_TYPE_SECTION
      && (!strcmp(owner->id, "styles")
       || !strcmp(owner->id, "preset"));

    if(!clean_path)
      clean_path = g_strdup(style_or_preset ? *path : NQ_(*path));

    if(!action)
    {
      if(!owner || !create)
      {
        dt_print(DT_DEBUG_ALWAYS, "[dt_action_locate] action '%s' %s", *path,
                !owner ? "not valid base node" : "doesn't exist");
        g_free(clean_path);
        return NULL;
      }

      dt_action_t *new_action = calloc(1, sizeof(dt_action_t));
      new_action->id = clean_path;
      new_action->label = style_or_preset
                        ? dt_util_localize_segmented_name(*path, TRUE)
                        : g_strdup(Q_(*path));
      new_action->type = DT_ACTION_TYPE_SECTION;

      dt_action_insert_sorted(owner, new_action);

      owner = new_action;
      action = NULL;
    }
    else if(!g_ascii_strcasecmp(action->id, clean_path))
    {
      g_free(clean_path);
      owner = action;
      action = action->target;
    }
    else
    {
      action = action->next;
      continue;
    }
    clean_path = NULL; // now owned by action or freed
    path++;
  }
  g_free(clean_path);

  if(owner)
  {
    if(owner->type <= DT_ACTION_TYPE_VIEW)
    {
      dt_print(DT_DEBUG_ALWAYS,
               "[dt_action_locate] found action '%s' internal node", owner->id);
      return NULL;
    }
  }

  return owner;
}

static gboolean _reset_element_on_leave(GtkWidget *widget,
                                        GdkEvent *event,
                                        gpointer user_data)
{
  darktable.control->element = -1;
  return FALSE;
}

dt_action_t *dt_action_define(dt_action_t *owner,
                              const gchar *section,
                              const gchar *label,
                              GtkWidget *widget,
                              const dt_action_def_t *action_def)
{
  if(owner->type == DT_ACTION_TYPE_IOP_INSTANCE)
  {
    return dt_action_define_iop((dt_iop_module_t *)owner, section,
                                label, widget, action_def);
  }

  dt_action_t *ac = owner;

  if(label)
  {
    const gchar *path[] = { section, label, NULL };
    ac = dt_action_locate(owner, (gchar**)&path[section ? 0 : 1], TRUE);
  }

  if(ac)
  {
    if(label)
    {
      guint index = 0;
      if(g_ptr_array_find(darktable.control->widget_definitions, action_def, &index))
        ac->type = DT_ACTION_TYPE_WIDGET + index + 1;
      else if(action_def == &_action_def_dummy)
        ac->type = DT_ACTION_TYPE_WIDGET;
      else if(action_def)
      {
        ac->type = DT_ACTION_TYPE_WIDGET + darktable.control->widget_definitions->len + 1;
        g_ptr_array_add(darktable.control->widget_definitions, (gpointer)action_def);

        dt_action_define_fallback(ac->type, action_def);
      }
    }

    if(action_def && action_def->no_widget)
    {
      ac->target = widget;
    }
    else if(widget)
    {
      if(!_action_quark)
        _action_quark = g_quark_from_static_string("dt_action");

      if(!g_object_get_qdata(G_OBJECT(widget), _action_quark))
      {
        g_object_set_qdata(G_OBJECT(widget), _action_quark, ac);

        if(label && action_def && !ac->target)
          g_set_weak_pointer(&ac->target, widget);
      }

      gtk_widget_set_has_tooltip(widget, TRUE);
      g_signal_connect(G_OBJECT(widget), "leave-notify-event",
                       G_CALLBACK(_reset_element_on_leave), NULL);
    }
  }

  return ac;
}

dt_action_t *dt_action_define_iop(dt_iop_module_t *self,
                                  const gchar *section,
                                  const gchar *label,
                                  GtkWidget *widget,
                                  const dt_action_def_t *action_def)
{
  // add to module_so or blending actions list
  dt_action_t *ac = NULL;
  if(section && g_str_has_prefix(section, "blend"))
  {
    const char *subsection = section[strlen("blend")]
      ? section + strlen("blend") + 1 : NULL;
    ac = dt_action_define(&darktable.control->actions_blend,
                          subsection, label, widget, action_def);
  }
  else
  {
    ac = dt_action_define(&self->so->actions, section, label, widget,
                          action_def ? action_def : &_action_def_dummy);
  }

  // to support multi-instance, also save in per instance widget list
  dt_action_target_t *referral = g_malloc0(sizeof(dt_action_target_t));
  referral->action = ac;
  referral->target = widget;
  if(DT_IS_BAUHAUS_WIDGET(widget)
     && dt_bauhaus_widget_get_field(widget))
  {
    if(self->widget_list_bh)
      self->widget_list_bh = g_slist_append(self->widget_list_bh, referral);
    else
    {
      self->widget_list_bh = g_slist_alloc();
      self->widget_list_bh->data = referral;
      self->widget_list = g_slist_concat(self->widget_list, self->widget_list_bh);
    }
  }
  else
    self->widget_list = g_slist_prepend(self->widget_list, referral);

  return ac;
}

static GdkModifierType _mods_fix_primary(GdkModifierType mods)
{
  // FIXME move to darktable.h (?) and use there too in dt_modifier_is
  // and dt_modifiers_include use global variable?
  GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());
  if(mods & GDK_CONTROL_MASK)
    return (mods & ~GDK_CONTROL_MASK)
           | gdk_keymap_get_modifier_mask(keymap, GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
  else
    return mods;
}

void dt_action_define_fallback(dt_action_type_t type, const dt_action_def_t *action_def)
{
  const dt_shortcut_fallback_t *f = action_def->fallbacks;
  if(f)
  {
    const gchar *fallback_path[] = { action_def->name, NULL };
    dt_action_t *fb = dt_action_locate(&darktable.control->actions_fallbacks,
                                       (gchar**)fallback_path, TRUE);
    fb->type = DT_ACTION_TYPE_FALLBACK;
    fb->target = GINT_TO_POINTER(type);

    while(f->mods || f->press || f->button || f->click || f->direction || f->move)
    {
      dt_shortcut_t s = { .mods = _mods_fix_primary(f->mods),
                          .press = f->press,
                          .button = f->button,
                          .click = f->click,
                          .direction = f->direction,
                          .move = f->move,
                          .is_default = TRUE,
                          .element = f->element,
                          .effect = f->effect,
                          .action = fb,
                          .speed = f->speed ? f->speed : 1.0 };

      _insert_shortcut(&s, FALSE, FALSE);

      f++;
    }
  }
}

void dt_shortcut_register(dt_action_t *owner,
                          guint element,
                          guint effect,
                          guint accel_key,
                          GdkModifierType mods)
{
  if(accel_key != 0 && !darktable.control->accel_initialised)
  {
    GdkKeymap *keymap = gdk_keymap_get_for_display(gdk_display_get_default());

    GdkKeymapKey *keys;
    gint n_keys, i = 0;

    if(!gdk_keymap_get_entries_for_keyval(keymap, accel_key, &keys, &n_keys)) return;

    for(int j = 0; j < n_keys; j++)
    {
      gdk_keymap_translate_keyboard_state(keymap, keys[j].keycode, 0, 0,
                                          &keys[j].keycode, NULL, NULL, NULL);

      if(_is_kp_key(keys[j].keycode))
        keys[j].group = 10;

      if(keys[j].group < keys[i].group
         || (keys[j].group == keys[i].group
             && keys[j].level < keys[i].level))
        i = j;
    }

    if(keys[i].level & 1) mods |= GDK_SHIFT_MASK;
    if(keys[i].level & 2) mods |= GDK_MOD5_MASK;

    mods = _mods_fix_primary(mods);

    dt_shortcut_t s = { .key_device = DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE,
                        .key = keys[i].keycode,
                        .mods = mods,
                        .is_default = TRUE,
                        .speed = 1.0,
                        .action = owner,
                        .element = element,
                        .effect = effect };

    _insert_shortcut(&s, FALSE, FALSE);

    g_free(keys);
  }
}

void dt_action_define_preset(dt_action_t *action,
                             const gchar *name)
{
  gchar *path[3] = { "preset", (gchar *)name, NULL };
  dt_action_t *const p = dt_action_locate(action, path, TRUE);
  if(p)
  {
    p->type = DT_ACTION_TYPE_PRESET;
    p->target = (gpointer)TRUE;
  }
}

void dt_action_rename(dt_action_t *action,
                      const gchar *new_name)
{
  if(!action) return;

  g_free((char*)action->id);
  g_free((char*)action->label);

  dt_action_t **previous = (dt_action_t **)&action->owner->target;
  while(*previous)
  {
    if(*previous == action)
    {
      *previous = action->next;
      break;
    }
    previous = &(*previous)->next;
  }

  if(new_name)
  {
    action->id = g_strdup(new_name);
    action->label = dt_util_localize_segmented_name(new_name, TRUE);

    dt_action_insert_sorted(action->owner, action);
  }
  else
  {
    GSequenceIter *iter = g_sequence_get_begin_iter(darktable.control->shortcuts);
    while(!g_sequence_iter_is_end(iter))
    {
      GSequenceIter *const current = iter;
      iter = g_sequence_iter_next(iter); // remove will invalidate

      dt_shortcut_t *s = g_sequence_get(current);
      if(s->action == action)
        _remove_shortcut(current);
    }

    g_free(action);
  }

  dt_shortcuts_save(NULL, FALSE);
}

void dt_action_rename_preset(dt_action_t *action,
                             const gchar *old_name,
                             const gchar *new_name)
{
  gchar *path[3] = { "preset", (gchar *)old_name, NULL };
  dt_action_t *p = dt_action_locate(action, path, FALSE);
  if(p)
  {
    if(!new_name)
    {
      if(_actions_store)
        gtk_tree_model_foreach(GTK_TREE_MODEL(_actions_store),
                               _remove_shortcut_from_store, p);
    }

    dt_action_rename(p, new_name);
  }
}

void dt_action_widget_toast(dt_action_t *action,
                            GtkWidget *widget,
                            const gchar *msg,
                            ...)
{
  if(!darktable.gui->reset)
  {
    va_list ap;
    va_start(ap, msg);
    char *text = g_strdup_vprintf(msg, ap);

    if(!action)
      action = dt_action_widget(widget);
    if(action)
    {
      gchar *instance_name = "";
      gchar *label = NULL;

      if(action->type == DT_ACTION_TYPE_IOP_INSTANCE)
      {
        dt_iop_module_t *module = (dt_iop_module_t *)action;

        action = DT_ACTION(module->so);
        if(*module->multi_name)
          instance_name = module->multi_name_hand_edited
                        ? g_strdup(module->multi_name)
                        : dt_util_localize_segmented_name(module->multi_name, TRUE);

        for(GSList *w = module->widget_list; w; w = w->next)
        {
          dt_action_target_t *referral = w->data;
          if(referral->target == widget)
          {
            if(referral->action->owner == &darktable.control->actions_blend)
            {
              _action_distinct_label(&label, referral->action, NULL);
            }
            else
              action = referral->action;
            break;
          }
        }
      }

      _action_distinct_label(&label, action, instance_name);
      if(*instance_name) g_free(instance_name);

      dt_toast_log("%s : %s", label, text);
      g_free(label);
    }
    else
      dt_toast_log("%s", text);

    g_free(text);
    va_end(ap);
  }
}

float dt_accel_get_speed_multiplier(GtkWidget *widget, guint state)
{
  const int slider_precision = dt_conf_get_int("accel/slider_precision");
  float multiplier
    = dt_conf_get_float(slider_precision == DT_IOP_PRECISION_FINE
                        ? "darkroom/ui/scale_precise_step_multiplier"
                        : slider_precision == DT_IOP_PRECISION_COARSE
                          ? "darkroom/ui/scale_rough_step_multiplier"
                          : "darkroom/ui/scale_step_multiplier");

  if(state != GDK_MODIFIER_MASK)
  {
    dt_shortcut_t s = { .action = &_value_action,
                        .mods   = _key_modifiers_clean(state),
                        .views  = DT_VIEW_FALLBACK };

    dt_action_t *wac = dt_action_widget(widget);
    while(s.action)
    {
      GSequenceIter *speed_adjustment =
        g_sequence_lookup(darktable.control->shortcuts, &s, _shortcut_compare_func, NULL);
      if(speed_adjustment)
      {
        const dt_shortcut_t *const f = g_sequence_get(speed_adjustment);

        multiplier *= f->speed;
      }
      s.action = wac;
      s.mods = 0;
      wac = NULL;
    }
  }

  return multiplier;
}

// FIXME possibly just find correct widget for each shortcut
// execution, rather than updating for each focus change etc
void dt_accel_connect_instance_iop(dt_iop_module_t *module)
{
  const dt_iop_module_t *gui_module = dt_dev_gui_module();
  const gboolean focused = gui_module && gui_module->so == module->so;
  const dt_action_t *const blend = &darktable.control->actions_blend;

  for(GSList *w = module->widget_list; w; w = w->next)
  {
    const dt_action_target_t *const referral = w->data;
    dt_action_t *const ac = referral->action;
    if(focused || (ac->owner != blend && ac->owner->owner != blend))
      g_set_weak_pointer(&ac->target, referral->target);
  }
}

void dt_action_cleanup_instance_iop(dt_iop_module_t *module)
{
  g_slist_free_full(module->widget_list, g_free);
}

GtkWidget *dt_action_button_new(dt_lib_module_t *self,
                                const gchar *label,
                                gpointer callback,
                                gpointer data,
                                const gchar *tooltip,
                                guint accel_key,
                                GdkModifierType mods)
{
  GtkWidget *button = gtk_button_new_with_label(Q_(label));
  gtk_widget_set_hexpand(button, TRUE);
  gtk_label_set_ellipsize(GTK_LABEL(gtk_bin_get_child(GTK_BIN(button))),
                          PANGO_ELLIPSIZE_END);
  if(tooltip) gtk_widget_set_tooltip_text(button, tooltip);
  g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(callback), data);

  if(self)
  {
    dt_action_t *ac = dt_action_define(DT_ACTION(self), NULL, label,
                                       button, &dt_action_def_button);
    dt_shortcut_register(ac, 0, 0, accel_key, mods);
    g_object_set_data(G_OBJECT(button), "module", self);
  }

  return button;
}

GtkWidget *dt_action_entry_new(dt_action_t *ac,
                               const gchar *label,
                               gpointer callback,
                               gpointer data,
                               const gchar *tooltip,
                               const gchar *text)
{
  GtkWidget *entry = dt_ui_entry_new(5);
  gtk_widget_set_hexpand(entry, TRUE);
  if(text)
    gtk_entry_set_text (GTK_ENTRY(entry), text);
  if(tooltip)
    gtk_widget_set_tooltip_text(entry, tooltip);
  g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(callback), data);

  dt_action_define(ac, NULL, label, entry, &dt_action_def_entry);

  return entry;
}

dt_action_t *dt_action_register(dt_action_t *owner,
                                const gchar *label,
                                dt_action_callback_t callback,
                                guint accel_key,
                                GdkModifierType mods)
{
  dt_action_t *ac = dt_action_section(owner, label);
  if(ac->type == DT_ACTION_TYPE_SECTION)
  {
    ac->type = DT_ACTION_TYPE_COMMAND;
    ac->target = callback;
    dt_shortcut_register(ac, 0, 0, accel_key, mods);
  }

  return ac;
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on
