Ctrl K
ComunidadGuías…

src/libs/filtering.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2025-2026 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 "bauhaus/bauhaus.h"
#include "common/collection.h"
#include "common/darktable.h"
#include "common/metadata.h"
#include "control/conf.h"
#include "control/control.h"
#include "control/jobs.h"
#include "dtgtk/button.h"
#include "dtgtk/range.h"
#include "dtgtk/rating_stars.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "libs/collect.h"
#include "libs/lib.h"
#include "libs/lib_api.h"
#include "views/view.h"
#include <glib-2.0/gio/gio.h>
#include <glib-2.0/gio/gmenu.h>
#include <glib-2.0/glib-object.h>
#ifndef _WIN32
#include <gio/gunixmounts.h>
#endif
#ifdef GDK_WINDOWING_QUARTZ
#endif

#include <locale.h>

DT_MODULE(1)

#define PARAM_STRING_SIZE 256 // FIXME: is this enough !?


static const dt_introspection_type_enum_tuple_t _collection_sort_names[]
  = { { N_("filename"), DT_COLLECTION_SORT_FILENAME },
      { N_("full path"), DT_COLLECTION_SORT_PATH },
      { N_("aspect ratio"), DT_COLLECTION_SORT_ASPECT_RATIO },

      { N_("capture time"), DT_COLLECTION_SORT_DATETIME },
      { N_("import time"), DT_COLLECTION_SORT_IMPORT_TIMESTAMP },
      { N_("modification time"), DT_COLLECTION_SORT_CHANGE_TIMESTAMP },
      { N_("export time"), DT_COLLECTION_SORT_EXPORT_TIMESTAMP },
      { N_("print time"), DT_COLLECTION_SORT_PRINT_TIMESTAMP },

      { N_("rating"), DT_COLLECTION_SORT_RATING },
      { N_("color label"), DT_COLLECTION_SORT_COLOR },
      { N_("title"), DT_COLLECTION_SORT_TITLE },
      { N_("description"), DT_COLLECTION_SORT_DESCRIPTION },

      { N_("group"), DT_COLLECTION_SORT_GROUP },
      { N_("id"), DT_COLLECTION_SORT_ID },
      { N_("custom sort"), DT_COLLECTION_SORT_CUSTOM_ORDER },
      { N_("shuffle"), DT_COLLECTION_SORT_SHUFFLE },
      { } };

typedef enum _preset_save_type_t
{
  _PRESET_NONE = 0,
  _PRESET_FILTERS = 1 << 0,
  _PRESET_SORT = 1 << 1,
  _PRESET_ERASE_TOPBAR = 1 << 2,
  _PRESET_TOPBAR = 1 << 3,
  _PRESET_ALL = _PRESET_FILTERS | _PRESET_SORT | _PRESET_ERASE_TOPBAR
} _preset_save_type_t;

typedef struct _widgets_sort_t
{
  dt_collection_sort_t sortid;
  GtkWidget *box;
  GtkWidget *sort;
  GtkWidget *direction;
  GtkWidget *close;

  int num;
  gboolean top;
  struct dt_lib_filtering_t *lib;
} _widgets_sort_t;

typedef struct dt_lib_filtering_rule_t
{
  int num;

  dt_collection_properties_t prop;

  GtkWidget *w_main;
  GtkWidget *w_operator;
  GtkWidget *w_prop;
  GtkWidget *w_btn_box;
  GtkWidget *w_close;
  GtkWidget *w_off;
  GtkWidget *w_pin;

  GtkWidget *w_widget_box;
  char raw_text[PARAM_STRING_SIZE];
  GtkWidget *w_special_box;
  void *w_specific;      // structure which contains all the widgets specific to the rule type
  // and we have the same for the top bar duplicate widgets
  GtkWidget *w_special_box_top;
  void *w_specific_top;  // structure which contains all the widgets specific to the rule type
  int manual_widget_set; // when we update manually the widget, we don't want events to be handled
  gboolean cleaning;     // if we have started a gui_cleanup (we don't want certain event to occurs)

  gboolean topbar;

  struct dt_lib_filtering_t *lib;
} dt_lib_filtering_rule_t;

typedef struct dt_lib_filtering_t
{
  dt_lib_filtering_rule_t rule[DT_COLLECTION_MAX_RULES];
  int nb_rules;

  GtkWidget *rules_box;
  GtkWidget *rules_sw;
  GtkWidget *topbar_popup;

  _widgets_sort_t sort[DT_COLLECTION_MAX_RULES];
  int nb_sort;
  _widgets_sort_t sorttop;
  GtkWidget *sort_box;
  gboolean manual_sort_set;
  gboolean leaving;

  struct dt_lib_filtering_params_t *params;

  gchar *last_where_ext;
} dt_lib_filtering_t;

typedef struct dt_lib_filtering_params_rule_t
{
  uint32_t item : 16;
  uint32_t mode : 16;
  uint32_t off : 16;
  uint32_t topbar : 16;
  char string[PARAM_STRING_SIZE];
} dt_lib_filtering_params_rule_t;

typedef struct dt_lib_filtering_params_sort_t
{
  uint32_t item : 16;
  uint32_t order : 16;
} dt_lib_filtering_params_sort_t;

typedef struct dt_lib_filtering_params_t
{
  uint32_t rules;
  dt_lib_filtering_params_rule_t rule[DT_COLLECTION_MAX_RULES];
  uint32_t sorts;
  dt_lib_filtering_params_sort_t sort[DT_COLLECTION_MAX_RULES];
  uint32_t preset_type;
} dt_lib_filtering_params_t;

typedef struct _widgets_range_t
{
  dt_lib_filtering_rule_t *rule;

  GtkWidget *range_select;
} _widgets_range_t;

typedef enum _tree_cols_t
{
  TREE_COL_TEXT = 0,
  TREE_COL_TOOLTIP,
  TREE_COL_PATH,
  TREE_COL_COUNT,
  TREE_NUM_COLS
} _tree_cols_t;

static void _filters_gui_update(dt_lib_module_t *self);
static void _sort_gui_update(dt_lib_module_t *self);
static void _filtering_gui_update(dt_lib_module_t *self);
static void _dt_collection_updated(gpointer instance, dt_collection_change_t query_change,
                                   dt_collection_properties_t changed_property, gpointer imgs, int next,
                                   gpointer self);
static void _rule_set_raw_text(dt_lib_filtering_rule_t *rule, const gchar *text, const gboolean signal);

static void _range_changed(GtkWidget *widget, gpointer user_data);
static void _range_widget_add_to_rule(dt_lib_filtering_rule_t *rule, _widgets_range_t *special, const gboolean top);
static void _sort_append_sort(GSimpleAction *action, GVariant *parameter, gpointer user_data);

typedef void (*_widget_init_func)(dt_lib_filtering_rule_t *rule, const dt_collection_properties_t prop,
                                  const gchar *text, dt_lib_module_t *self, gboolean top);

typedef gboolean (*_widget_update_func)(dt_lib_filtering_rule_t *rule);
typedef struct _filter_t
{
  dt_collection_properties_t prop;
  _widget_init_func widget_init;
  _widget_update_func update;
} _filter_t;


// filters definitions
#include "libs/filters/aperture.c"
#include "libs/filters/colors.c"
#include "libs/filters/date.c"
#include "libs/filters/exposure.c"
#include "libs/filters/exposure_bias.c"
#include "libs/filters/filename.c"
#include "libs/filters/focal.c"
#include "libs/filters/history.c"
#include "libs/filters/iso.c"
#include "libs/filters/local_copy.c"
#include "libs/filters/duplicates.c"
#include "libs/filters/misc.c"
#include "libs/filters/module_order.c"
#include "libs/filters/rating.c"
#include "libs/filters/rating_range.c"
#include "libs/filters/ratio.c"
#include "libs/filters/search.c"

static _filter_t filters[]
    = { { DT_COLLECTION_PROP_COLORLABEL, _colors_widget_init, _colors_update },
        { DT_COLLECTION_PROP_FILENAME, _filename_widget_init, _filename_update },
        { DT_COLLECTION_PROP_TEXTSEARCH, _search_widget_init, _search_update },
        { DT_COLLECTION_PROP_DAY, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_MONTH, _month_widget_init, _month_update },
        { DT_COLLECTION_PROP_CHANGE_TIMESTAMP, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_EXPORT_TIMESTAMP, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_TIME, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_IMPORT_TIMESTAMP, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_PRINT_TIMESTAMP, _date_widget_init, _date_update },
        { DT_COLLECTION_PROP_ASPECT_RATIO, _ratio_widget_init, _ratio_update },
        { DT_COLLECTION_PROP_RATING_RANGE, _rating_range_widget_init, _rating_range_update },
        { DT_COLLECTION_PROP_APERTURE, _aperture_widget_init, _aperture_update },
        { DT_COLLECTION_PROP_FOCAL_LENGTH, _focal_widget_init, _focal_update },
        { DT_COLLECTION_PROP_ISO, _iso_widget_init, _iso_update },
        { DT_COLLECTION_PROP_EXPOSURE, _exposure_widget_init, _exposure_update },
        { DT_COLLECTION_PROP_EXPOSURE_BIAS, _exposure_bias_widget_init, _exposure_bias_update },
        { DT_COLLECTION_PROP_GROUP_ID, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_DUPLICATES, _duplicates_widget_init, _duplicates_update },
        { DT_COLLECTION_PROP_DIMENSIONS, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_LOCAL_COPY, _local_copy_widget_init, _local_copy_update },
        { DT_COLLECTION_PROP_HISTORY, _history_widget_init, _history_update },
        { DT_COLLECTION_PROP_ORDER, _module_order_widget_init, _module_order_update },
        { DT_COLLECTION_PROP_RATING, _rating_widget_init, _rating_update },
        { DT_COLLECTION_PROP_LENS, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_CAMERA, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_WHITEBALANCE, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_FLASH, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_EXPOSURE_PROGRAM, _misc_widget_init, _misc_update },
        { DT_COLLECTION_PROP_METERING_MODE, _misc_widget_init, _misc_update } };

static _filter_t *_filters_get(const dt_collection_properties_t prop)
{
  const int nb = sizeof(filters) / sizeof(_filter_t);
  for(int i = 0; i < nb; i++)
  {
    if(filters[i].prop == prop) return &filters[i];
  }
  return NULL;
}

const char *name(dt_lib_module_t *self)
{
  return _("collection filters");
}

const char *description(dt_lib_module_t *self)
{
  return _("refine the set of images to display or edit.\n"
           "filters can be pinned to the top toolbar, where\n"
           "they will also be visible in the darkroom");
}

void init_presets(dt_lib_module_t *self)
{
  dt_lib_filtering_params_t params;

#define CLEAR_PARAMS(t, r, s)                                                                                     \
  {                                                                                                               \
    memset(&params, 0, sizeof(params));                                                                           \
    params.preset_type = t;                                                                                       \
    params.rules = 1;                                                                                             \
    params.rule[0].mode = 0;                                                                                      \
    params.rule[0].off = 0;                                                                                       \
    params.rule[0].topbar = 0;                                                                                    \
    params.rule[0].item = r;                                                                                      \
    params.sorts = 1;                                                                                             \
    params.sort[0].item = s;                                                                                      \
    params.sort[0].order = 0;                                                                                     \
  }

  // initial preset
  CLEAR_PARAMS(_PRESET_ALL, DT_COLLECTION_PROP_RATING_RANGE, DT_COLLECTION_SORT_DATETIME);
  params.rules = 3;
  params.rule[0].topbar = 1;
  params.rule[1].item = DT_COLLECTION_PROP_COLORLABEL;
  params.rule[1].mode = 0;
  params.rule[1].off = 0;
  params.rule[1].topbar = 1;
  params.rule[2].item = DT_COLLECTION_PROP_TEXTSEARCH;
  params.rule[2].mode = 0;
  params.rule[2].off = 0;
  params.rule[2].topbar = 1;
  dt_lib_presets_add(_("initial setting"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  // based on aspect-ratio
  CLEAR_PARAMS(_PRESET_FILTERS, DT_COLLECTION_PROP_ASPECT_RATIO, DT_COLLECTION_SORT_DATETIME);
  g_strlcpy(params.rule[0].string, "[1;1]", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("square"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  CLEAR_PARAMS(_PRESET_FILTERS, DT_COLLECTION_PROP_ASPECT_RATIO, DT_COLLECTION_SORT_DATETIME);
  g_strlcpy(params.rule[0].string, ">=1.01", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("landscape"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  CLEAR_PARAMS(_PRESET_FILTERS, DT_COLLECTION_PROP_ASPECT_RATIO, DT_COLLECTION_SORT_DATETIME);
  g_strlcpy(params.rule[0].string, "<=0.99", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("portrait"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  // presets based on import
  CLEAR_PARAMS(_PRESET_FILTERS | _PRESET_SORT, DT_COLLECTION_PROP_IMPORT_TIMESTAMP,
               DT_COLLECTION_SORT_IMPORT_TIMESTAMP);
  g_strlcpy(params.rule[0].string, "[-0000:00:01 00:00:00;now]", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("imported: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  CLEAR_PARAMS(_PRESET_FILTERS | _PRESET_SORT, DT_COLLECTION_PROP_IMPORT_TIMESTAMP,
               DT_COLLECTION_SORT_IMPORT_TIMESTAMP);
  g_strlcpy(params.rule[0].string, "[-0000:00:30 00:00:00;now]", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("imported: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params),
                     TRUE, 0);

  // presets based on image metadata (image taken)
  CLEAR_PARAMS(_PRESET_FILTERS | _PRESET_SORT, DT_COLLECTION_PROP_TIME, DT_COLLECTION_SORT_DATETIME);
  g_strlcpy(params.rule[0].string, "[-0000:00:01 00:00:00;now]", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("taken: last 24h"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

  CLEAR_PARAMS(_PRESET_FILTERS | _PRESET_SORT, DT_COLLECTION_PROP_TIME, DT_COLLECTION_SORT_DATETIME);
  g_strlcpy(params.rule[0].string, "[-0000:00:30 00:00:00;now]", PARAM_STRING_SIZE);
  dt_lib_presets_add(_("taken: last 30 days"), self->plugin_name, self->version(), &params, sizeof(params), TRUE, 0);

#undef CLEAR_PARAMS
}

static void _filtering_reset(const _preset_save_type_t reset)
{
  if((reset & _PRESET_FILTERS) && (reset & _PRESET_ERASE_TOPBAR))
  {
    // easy case : we remove all rules
    dt_conf_set_int("plugins/lighttable/filtering/num_rules", 0);
  }
  else if(reset & _PRESET_FILTERS)
  {
    // for the filtering rules, we
    // - remove the unpinned ones
    // - reset the pinned ones
    const int nb_rules
        = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_rules"), 0, DT_COLLECTION_MAX_RULES);
    int nb_removed = 0;
    for(int i = 0; i < nb_rules; i++)
    {
      char confname[200] = { 0 };
      // read the topbar state
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i - nb_removed);
      if(dt_conf_get_int(confname))
      {
        // we "just" reset the filter
        snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i - nb_removed);
        dt_conf_set_int(confname, 0);
        snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i - nb_removed);
        dt_conf_set_string(confname, "");
      }
      else
      {
        // we remove the filter and move up the next ones
        for(int j = i + 1; j < nb_rules; j++)
        {
          snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", j - nb_removed);
          const int mode = dt_conf_get_int(confname);
          snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", j - nb_removed);
          const int item = dt_conf_get_int(confname);
          snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", j - nb_removed);
          const int off = dt_conf_get_int(confname);
          snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", j - nb_removed);
          const int top = dt_conf_get_int(confname);
          snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", j - nb_removed);
          gchar *string = dt_conf_get_string(confname);
          if(string)
          {
            snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", j - nb_removed - 1);
            dt_conf_set_int(confname, mode);
            snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", j - nb_removed - 1);
            dt_conf_set_int(confname, item);
            snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", j - nb_removed - 1);
            dt_conf_set_int(confname, off);
            snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", j - nb_removed - 1);
            dt_conf_set_int(confname, top);
            snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", j - nb_removed - 1);
            dt_conf_set_string(confname, string);
            g_free(string);
          }
        }
        nb_removed++;
      }
    }
    dt_conf_set_int("plugins/lighttable/filtering/num_rules", nb_rules - nb_removed);
  }
  else if(reset & _PRESET_TOPBAR)
  {
    // let's reset only topbar filters
    const int nb_rules
        = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_rules"), 0, DT_COLLECTION_MAX_RULES);
    for(int i = 0; i < nb_rules; i++)
    {
      char confname[200] = { 0 };
      // read the topbar state
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i);
      if(dt_conf_get_int(confname))
      {
        // we "just" reset the filter
        snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i);
        dt_conf_set_int(confname, 0);
        snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i);
        dt_conf_set_string(confname, "");
      }
    }
  }

  if(reset & _PRESET_SORT)
  {
    // we reset the sorting orders
    dt_conf_set_int("plugins/lighttable/filtering/num_sort", 1);
    dt_conf_set_int("plugins/lighttable/filtering/sort0", 0);
    dt_conf_set_int("plugins/lighttable/filtering/sortorder0", 0);
  }
}

/* Update the params struct with active ruleset */
static void _filters_update_params(dt_lib_filtering_t *d)
{
  /* reset params */
  dt_lib_filtering_params_t *p = d->params;
  memset(p, 0, sizeof(dt_lib_filtering_params_t));
  p->preset_type = _PRESET_ALL;

  /* for each active rule set update params */
  p->rules = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_rules"), 0, DT_COLLECTION_MAX_RULES);
  char confname[200] = { 0 };
  for(int i = 0; i < p->rules; i++)
  {
    /* get item */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", i);
    p->rule[i].item = dt_conf_get_int(confname);

    /* get mode */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i);
    p->rule[i].mode = dt_conf_get_int(confname);

    /* get on-off */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", i);
    p->rule[i].off = dt_conf_get_int(confname);

    /* get topbar */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i);
    p->rule[i].topbar = dt_conf_get_int(confname);

    /* get string */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i);
    const char *string = dt_conf_get_string_const(confname);
    if(string != NULL)
    {
      g_strlcpy(p->rule[i].string, string, PARAM_STRING_SIZE);
    }
  }

  /* for each sort orders set update params */
  p->sorts = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_sort"), 0, DT_COLLECTION_MAX_RULES);
  for(int i = 0; i < p->sorts; i++)
  {
    /* get item */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", i);
    p->sort[i].item = dt_conf_get_int(confname);

    /* get sort order */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", i);
    p->sort[i].order = dt_conf_get_int(confname);
  }
}

static void _history_save(dt_lib_filtering_t *d, const gboolean sort)
{
  // get the string of the rules
  char buf[4096] = { 0 };
  if(sort)
    dt_collection_sort_serialize(buf, sizeof(buf));
  else
    dt_collection_serialize(buf, sizeof(buf), TRUE);

  // compare to last saved history
  char confname[200] = { 0 };
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory0", (sort) ? "sort_" : "");
  gchar *str = dt_conf_get_string(confname);
  if(!g_strcmp0(str, buf))
  {
    g_free(str);
    return;
  }
  g_free(str);

  // remove all subsequent history that have the same values
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory_max", (sort) ? "sort_" : "");
  const int nbmax = dt_conf_get_int(confname);
  int move = 0;
  for(int i = 1; i < nbmax; i++)
  {
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory%1d", (sort) ? "sort_" : "", i);
    gchar *string = dt_conf_get_string(confname);

    if(!g_strcmp0(string, buf))
    {
      move++;
      dt_conf_set_string(confname, "");
    }
    else if(move > 0)
    {
      dt_conf_set_string(confname, "");
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory%1d", (sort) ? "sort_" : "",
               i - move);
      dt_conf_set_string(confname, string);
    }
    g_free(string);
  }

  // move all history entries +1 (and delete the last one)
  for(int i = nbmax - 2; i >= 0; i--)
  {
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory%1d", (sort) ? "sort_" : "", i);
    gchar *string = dt_conf_get_string(confname);

    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory%1d", (sort) ? "sort_" : "", i + 1);
    dt_conf_set_string(confname, string);
    g_free(string);
  }

  // save current history
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/%shistory0", (sort) ? "sort_" : "");
  dt_conf_set_string(confname, buf);
}

void *get_params(dt_lib_module_t *self, int *size)
{
  _filters_update_params(self->data);

  /* allocate a copy of params to return, freed by caller */
  *size = sizeof(dt_lib_filtering_params_t);
  void *p = malloc(*size);
  memcpy(p, ((dt_lib_filtering_t *)self->data)->params, *size);
  return p;
}

int set_params(dt_lib_module_t *self, const void *params, int size)
{
  /* update conf settings from params */
  dt_lib_filtering_params_t *p = (dt_lib_filtering_params_t *)params;

  // reset conf value
  _filtering_reset(p->preset_type);

  char confname[200] = { 0 };
  const int nb_rules_ini = dt_conf_get_int("plugins/lighttable/filtering/num_rules");
  int nb_rules_skipped = 0;

  for(uint32_t i = 0; i < p->rules; i++)
  {
    // if we don't have erased the topbar, be sure that the rule don't already exist in topbar
    int pos = i + nb_rules_ini - nb_rules_skipped;
    for(int j = 0; j < nb_rules_ini; j++)
    {
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", j);
      if(p->rule[i].item == dt_conf_get_int(confname))
      {
        pos = j;
        nb_rules_skipped++;
        // force params value to be ok for topbar
        p->rule[i].topbar = TRUE;
        p->rule[i].mode = 0;
        p->rule[i].off = FALSE;
      }
    }
    /* set item */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", pos);
    dt_conf_set_int(confname, p->rule[i].item);

    /* set mode */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", pos);
    dt_conf_set_int(confname, p->rule[i].mode);

    /* set on-off */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", pos);
    dt_conf_set_int(confname, p->rule[i].off);

    /* set topbar */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", pos);
    dt_conf_set_int(confname, p->rule[i].topbar);

    /* set string */
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", pos);
    dt_conf_set_string(confname, p->rule[i].string);
  }

  /* set number of rules */
  g_strlcpy(confname, "plugins/lighttable/filtering/num_rules", sizeof(confname));
  dt_conf_set_int(confname, p->rules + nb_rules_ini - nb_rules_skipped);

  if(p->preset_type & _PRESET_SORT)
  {
    for(uint32_t i = 0; i < p->sorts; i++)
    {
      /* set item */
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1u", i);
      dt_conf_set_int(confname, p->sort[i].item);

      /* set order */
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1u", i);
      dt_conf_set_int(confname, p->sort[i].order);
    }

    /* set number of sorts */
    g_strlcpy(confname, "plugins/lighttable/filtering/num_sort", sizeof(confname));
    dt_conf_set_int(confname, p->sorts);
  }

  /* update internal params */
  _filters_update_params(self->data);
  _history_save(self->data, TRUE);
  _history_save(self->data, FALSE);

  /* update ui */
  _filters_gui_update(self);
  _sort_gui_update(self);

  /* update view */
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL);
  return 0;
}


dt_view_type_flags_t views(dt_lib_module_t *self)
{
  return DT_VIEW_LIGHTTABLE | DT_VIEW_MAP | DT_VIEW_PRINT;
}

uint32_t container(dt_lib_module_t *self)
{
  return DT_UI_CONTAINER_PANEL_LEFT_CENTER;
}

static void _conf_update_rule(dt_lib_filtering_rule_t *rule)
{
  const dt_lib_collect_mode_t mode = MAX(0, dt_bauhaus_combobox_get(rule->w_operator));
  const gboolean off = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(rule->w_off));

  char confname[200] = { 0 };
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", rule->num);
  dt_conf_set_string(confname, rule->raw_text);
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", rule->num);
  dt_conf_set_int(confname, rule->prop);
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", rule->num);
  dt_conf_set_int(confname, mode);
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", rule->num);
  dt_conf_set_int(confname, off);
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", rule->num);
  dt_conf_set_int(confname, rule->topbar);

  _history_save(rule->lib, FALSE);
}

static void _event_rule_changed(GtkWidget *entry, dt_lib_filtering_rule_t *rule)
{
  if(rule->manual_widget_set) return;

  // update the config files
  _conf_update_rule(rule);

  // update the query without throwing signal everywhere
  dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(_dt_collection_updated),
                                  darktable.view_manager->proxy.module_collect.module);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, rule->prop, NULL);
  dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(_dt_collection_updated),
                                    darktable.view_manager->proxy.module_collect.module);
}

static void _rule_set_raw_text(dt_lib_filtering_rule_t *rule, const gchar *text, const gboolean signal)
{
  snprintf(rule->raw_text, sizeof(rule->raw_text), "%s", (text == NULL) ? "" : text);
  if(signal) _event_rule_changed(NULL, rule);
}

static void _range_changed(GtkWidget *widget, gpointer user_data)
{
  _widgets_range_t *special = (_widgets_range_t *)user_data;
  if(special->rule->manual_widget_set) return;
  if(special->rule->lib->leaving) return;

  // we recreate the right raw text and put it in the raw entry
  gchar *txt = dtgtk_range_select_get_raw_text(DTGTK_RANGE_SELECT(special->range_select));
  _rule_set_raw_text(special->rule, txt, TRUE);
  g_free(txt);

  // synchronize the other widget if any
  _widgets_range_t *dest = NULL;
  if(special == special->rule->w_specific_top)
    dest = special->rule->w_specific;
  else
    dest = special->rule->w_specific_top;

  if(dest)
  {
    special->rule->manual_widget_set++;
    dtgtk_range_select_set_selection_from_raw_text(DTGTK_RANGE_SELECT(dest->range_select), special->rule->raw_text,
                                                   FALSE);
    special->rule->manual_widget_set--;
  }
}

static void _range_widget_add_to_rule(dt_lib_filtering_rule_t *rule, _widgets_range_t *special, const gboolean top)
{
  special->rule = rule;

  // we create the static part of the tooltip
  gchar *txt = g_strdup_printf("\n<b>%s</b>\n%s\n%s", dt_collection_name(special->rule->prop),
                               _("click or click&#38;drag to select one or multiple values"),
                               _("right-click opens a menu to select the available values"));
  if(DTGTK_RANGE_SELECT(special->range_select)->cur_help)
    g_free(DTGTK_RANGE_SELECT(special->range_select)->cur_help);
  DTGTK_RANGE_SELECT(special->range_select)->cur_help = txt;

  gtk_box_pack_start(GTK_BOX((top) ? rule->w_special_box_top : rule->w_special_box), special->range_select, TRUE,
                     TRUE, 0);
  g_signal_connect(G_OBJECT(special->range_select), "value-changed", G_CALLBACK(_range_changed), special);
  if(top)
  {
    dt_gui_add_class(gtk_bin_get_child(GTK_BIN(special->range_select)), "dt_quick_filter");
  }

  if(top)
    rule->w_specific_top = special;
  else
    rule->w_specific = special;
}

static gboolean _widget_update(dt_lib_filtering_rule_t *rule)
{
  _filter_t *f = _filters_get(rule->prop);
  if(f) return f->update(rule);
  return FALSE;
}

static gboolean _widget_init_special(dt_lib_filtering_rule_t *rule, const gchar *text, dt_lib_module_t *self,
                                     gboolean top)
{
  // remove eventual existing box
  if(!top && rule->w_special_box)
    gtk_widget_destroy(rule->w_special_box);
  else if(rule->w_special_box_top)
    gtk_widget_destroy(rule->w_special_box_top);

  // recreate the box
  if(!top)
    rule->w_special_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  else
    rule->w_special_box_top = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  GtkWidget *special_box = (top) ? rule->w_special_box_top : rule->w_special_box;
  if(!top)
    gtk_box_pack_start(GTK_BOX(rule->w_widget_box), rule->w_special_box, TRUE, TRUE, 0);
  else
    g_object_ref(G_OBJECT(rule->w_special_box_top));

  _filter_t *f = _filters_get(rule->prop);
  if(f)
    f->widget_init(rule, rule->prop, text, self, top);
  else
    return FALSE;

  gtk_widget_show_all(special_box);
  return TRUE;
}

static void _event_rule_change_type(GtkWidget *widget, dt_lib_module_t *self)
{
  const int mode = GPOINTER_TO_INT(dt_bauhaus_combobox_get_data(widget));
  dt_lib_filtering_rule_t *rule = (dt_lib_filtering_rule_t *)g_object_get_data(G_OBJECT(widget), "rule");

  if(mode == rule->prop) return;

  dt_collection_properties_t old = rule->prop;
  rule->prop = mode;

  // re-init the special widgets
  _widget_init_special(rule, "", self, FALSE);
  _widget_update(rule);

  // reset the raw entry
  _rule_set_raw_text(rule, "", FALSE);

  // update the config files
  _conf_update_rule(rule);

  // update the query without throwing signal everywhere
  dt_control_signal_block_by_func(darktable.signals, G_CALLBACK(_dt_collection_updated),
                                  darktable.view_manager->proxy.module_collect.module);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, old, NULL);
  dt_control_signal_unblock_by_func(darktable.signals, G_CALLBACK(_dt_collection_updated),
                                    darktable.view_manager->proxy.module_collect.module);
}

static void _event_append_rule(GSimpleAction *action,
                               GVariant *parameter,
                               gpointer user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_filtering_t *d = self->data;

  GVariant *v_mode = g_variant_get_child_value(parameter, 0);
  GVariant *v_top  = g_variant_get_child_value(parameter, 1);

  // add new rule
  const int mode = g_variant_get_int32(v_mode);
  const int top  = g_variant_get_int32(v_top);

  g_variant_unref(v_mode);
  g_variant_unref(v_top);

  char confname[200] = { 0 };

  if(mode >= 0)
  {
    // add an empty rule
    if(d->nb_rules >= DT_COLLECTION_MAX_RULES)
    {
      dt_control_log(_("you can't have more than %d rules"), DT_COLLECTION_MAX_RULES);
      return;
    }
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", d->nb_rules);
    dt_conf_set_int(confname, mode);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", d->nb_rules);
    dt_conf_set_int(confname, DT_LIB_COLLECT_MODE_AND);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", d->nb_rules);
    dt_conf_set_int(confname, 0);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", d->nb_rules);
    dt_conf_set_int(confname, top);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", d->nb_rules);
    dt_conf_set_string(confname, "");
    d->nb_rules++;
    dt_conf_set_int("plugins/lighttable/filtering/num_rules", d->nb_rules);

    _filters_gui_update(self);
    dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, mode, NULL);
  }
}

static void _popup_add_sort_item(GMenu *menu,
                                 const gchar *name,
                                 const int id)
{
  GMenuItem *item = g_menu_item_new(name, NULL);
  g_menu_item_set_action_and_target_value(item,
                                          "sort.activate",
                                          g_variant_new("i", id, 0));
  g_menu_append_item(menu, item);
  g_object_unref(item);
}

static void _popup_add_filter_item(GMenu *menu,
                                   const gchar *name,
                                   const int id,
                                   gpointer data)
{
  if(!_filters_get(id)) return;

  GMenuItem *item = g_menu_item_new(name, NULL);
  g_menu_item_set_action_and_target_value(item,
                                          "filter.activate",
                                          g_variant_new("(ii)", id, 0));
  g_menu_append_item(menu, item);
  g_object_unref(item);
}

static gboolean _rule_show_popup(GtkWidget *widget,
                                 dt_lib_filtering_rule_t *rule,
                                 dt_lib_module_t *self)
{
  GActionGroup *action_group = gtk_widget_get_action_group(widget, "filter");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _event_append_rule, "(ii)", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    self);
    gtk_widget_insert_action_group(widget, 
                                   "filter",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

#define ADD_COLLECT_ENTRY(menu, value)                                                                            \
  _popup_add_filter_item(menu, dt_collection_name(value), value, rule);

  // we show a popup with all the possible rules
  // note that only rules with defined filters will be shown
  GMenu *menu = g_menu_new();
  GMenu *section;

  // the different categories
  section = g_menu_new();
  g_menu_append_section(menu, _("files"), G_MENU_MODEL(section));
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_FILMROLL);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_FOLDERS);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_FILENAME);
  g_object_unref(section);

  section = g_menu_new();
  g_menu_append_section(menu, _("metadata"), G_MENU_MODEL(section));
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_TAG);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_RATING_RANGE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_RATING);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_COLORLABEL);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_TEXTSEARCH);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_GEOTAGGING);
  g_object_unref(section);

  section = g_menu_new();
  g_menu_append_section(menu, _("times"), G_MENU_MODEL(section));
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_DAY);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_MONTH);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_TIME);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_IMPORT_TIMESTAMP);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_CHANGE_TIMESTAMP);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_EXPORT_TIMESTAMP);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_PRINT_TIMESTAMP);
  g_object_unref(section);

  section = g_menu_new();
  g_menu_append_section(menu, _("capture details"), G_MENU_MODEL(section));
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_CAMERA);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_LENS);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_APERTURE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_EXPOSURE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_EXPOSURE_BIAS);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_FOCAL_LENGTH);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_ISO);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_ASPECT_RATIO);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_WHITEBALANCE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_FLASH);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_EXPOSURE_PROGRAM);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_METERING_MODE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_DIMENSIONS);
  g_object_unref(section);

  section = g_menu_new();
  g_menu_append_section(menu, _("darktable"), G_MENU_MODEL(section));
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_GROUP_ID);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_DUPLICATES);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_LOCAL_COPY);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_HISTORY);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_MODULE);
  ADD_COLLECT_ENTRY(section, DT_COLLECTION_PROP_ORDER);
  g_object_unref(section);

  GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
  g_object_unref(menu);
  gtk_popover_popup(GTK_POPOVER(popover_menu));

  return TRUE;
#undef ADD_COLLECT_ENTRY
}

static void _rule_populate_prop_combo_add(GtkWidget *w, const dt_collection_properties_t prop)
{
  if(!_filters_get(prop)) return;
  dt_bauhaus_combobox_add_full(w, dt_collection_name(prop), DT_BAUHAUS_COMBOBOX_ALIGN_MIDDLE,
                               GUINT_TO_POINTER(prop), NULL, TRUE);
}

static void _populate_rules_combo(GtkWidget *w)
{
#define ADD_COLLECT_ENTRY(value) _rule_populate_prop_combo_add(w, value);
  gtk_widget_set_tooltip_text(w, _("rule property"));

  dt_bauhaus_combobox_add_section(w, _("files"));
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILMROLL);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOLDERS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILENAME);

  dt_bauhaus_combobox_add_section(w, _("metadata"));
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TAG);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_RATING_RANGE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_RATING);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_COLORLABEL);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TEXTSEARCH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GEOTAGGING);

  dt_bauhaus_combobox_add_section(w, _("times"));
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DAY);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_MONTH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TIME);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_IMPORT_TIMESTAMP);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_CHANGE_TIMESTAMP);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPORT_TIMESTAMP);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_PRINT_TIMESTAMP);

  dt_bauhaus_combobox_add_section(w, _("capture details"));
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_CAMERA);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LENS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_APERTURE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_BIAS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOCAL_LENGTH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ISO);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ASPECT_RATIO);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_WHITEBALANCE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FLASH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_PROGRAM);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_METERING_MODE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DIMENSIONS);

  dt_bauhaus_combobox_add_section(w, _("darktable"));
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GROUP_ID);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DUPLICATES);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LOCAL_COPY);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_HISTORY);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_MODULE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ORDER);

#undef ADD_COLLECT_ENTRY
}

static void _rule_populate_prop_combo(dt_lib_filtering_rule_t *rule)
{
  GtkWidget *w = rule->w_prop;
  // first we cleanup existing entries
  dt_bauhaus_combobox_clear(w);

  // in the case of a pinned rule, we only add the selected entry
  if(rule->topbar)
  {
    _rule_populate_prop_combo_add(w, rule->prop);
    gtk_widget_set_tooltip_text(w, _("rule property\nthis can't be changed as the rule is pinned to the toolbar"));
    rule->manual_widget_set++;
    dt_bauhaus_combobox_set_from_value(rule->w_prop, rule->prop);
    rule->manual_widget_set--;
    return;
  }
  // otherwise we add all implemented rules
  _populate_rules_combo(w);

  rule->manual_widget_set++;
  dt_bauhaus_combobox_set_from_value(rule->w_prop, rule->prop);
  rule->manual_widget_set--;
}

static void _event_rule_append(GtkWidget *widget, gpointer user_data)
{
  _rule_show_popup(widget, NULL, (dt_lib_module_t *)user_data);
}

static void _topbar_reset(dt_lib_module_t *self)
{
  _filtering_reset(_PRESET_TOPBAR);

  _filters_gui_update(self);

  dt_collection_set_query_flags(darktable.collection, COLLECTION_QUERY_FULL);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL);
}

static void _topbar_reset_press_cb(GtkGestureSingle *gesture, int n_press,
                                      double x, double y,
                                      dt_lib_module_t *self)
{
  //reset the filters
  _topbar_reset(self);
  //close the popup
  dt_lib_filtering_t *d = self->data;
  gtk_widget_destroy(d->topbar_popup);
}

static void _topbar_label_press_cb(GtkGestureSingle *gesture, int n_press,
                                      double x, double y,
                                      dt_lib_module_t *self)
{
  //reset on double-click
  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY && n_press >= 2)
    _topbar_reset(self);
}

static void _topbar_update(dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;

  // first, we cleanup the filter box
  GtkWidget *fbox = dt_view_filter_get_filters_box(darktable.view_manager);
  GList *childrens = gtk_container_get_children(GTK_CONTAINER(fbox));
  for(GList *l = childrens; l; l = g_list_next(l))
  {
    g_object_ref(G_OBJECT(l->data));
    gtk_container_remove(GTK_CONTAINER(fbox), GTK_WIDGET(l->data));
  }
  g_list_free(childrens);

  // and we add all the special widgets with a top structure
  int nb = 0;
  for(int i = 0; i < d->nb_rules; i++)
  {
    if(d->rule[i].topbar)
    {
      // we create the widget if needed
      if(!d->rule[i].w_special_box_top)
      {
        _widget_init_special(&d->rule[i], d->rule[i].raw_text, self, TRUE);
        _widget_update(&d->rule[i]);
      }
      // we add the filter label if it's the first filter
      if(nb == 0)
      {
        GtkWidget *evtb = gtk_event_box_new();
        GtkWidget *label = gtk_label_new(C_("quickfilter", "filter"));
        gtk_container_add(GTK_CONTAINER(evtb), label);
        dt_gui_connect_click_all(evtb, _topbar_label_press_cb, NULL, self);
        gtk_box_pack_start(GTK_BOX(fbox), evtb, TRUE, TRUE, 0);
        gtk_widget_show_all(evtb);
      }
      gtk_box_pack_start(GTK_BOX(fbox), d->rule[i].w_special_box_top, FALSE, TRUE, 0);
      gtk_widget_show_all(d->rule[i].w_special_box_top);
      nb++;
    }
    else if(d->rule[i].w_special_box_top)
    {
      // we destroy the widget if needed
      gtk_widget_destroy(d->rule[i].w_special_box_top);
      d->rule[i].w_special_box_top = NULL;
      g_free(d->rule[i].w_specific_top);
      d->rule[i].w_specific_top = NULL;
    }
  }
}

static void _widget_header_update(dt_lib_filtering_rule_t *rule)
{
  gtk_widget_set_sensitive(rule->w_close, !rule->topbar);
  gtk_widget_set_sensitive(rule->w_off, !rule->topbar);

  if(rule->topbar)
  {
    if(gtk_widget_get_visible(rule->w_pin))
      gtk_widget_set_tooltip_text(rule->w_pin, _("this rule is pinned to the top toolbar\nclick to un-pin"));
    gtk_widget_set_tooltip_text(rule->w_off, _("you can't disable the rule as it is pinned to the toolbar"));
    gtk_widget_set_tooltip_text(rule->w_close, _("you can't remove the rule as it is pinned to the toolbar"));
  }
  else
  {
    if(gtk_widget_get_visible(rule->w_pin))
      gtk_widget_set_tooltip_text(rule->w_pin, _("click to pin this rule to the top toolbar"));
    gtk_widget_set_tooltip_text(rule->w_close, _("remove this collect rule"));
    if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(rule->w_off)))
      gtk_widget_set_tooltip_text(rule->w_off, _("this rule is enabled"));
    else
      gtk_widget_set_tooltip_text(rule->w_off, _("this rule is disabled"));
  }

  _rule_populate_prop_combo(rule);
}

static void _rule_topbar_toggle(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_filtering_rule_t *rule = (dt_lib_filtering_rule_t *)g_object_get_data(G_OBJECT(widget), "rule");
  if(rule->manual_widget_set) return;

  if(gtk_widget_get_visible(rule->w_pin))
    rule->topbar = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(rule->w_pin));
  else
    rule->topbar = FALSE;
  // if the rule is pinned, then we force it to on
  if(rule->topbar && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(rule->w_off)))
  {
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(rule->w_off), TRUE);
  }
  _conf_update_rule(rule);
  _topbar_update(self);

  // update the rule header
  _widget_header_update(rule);
}

static void _event_rule_disable(GtkWidget *widget, dt_lib_filtering_rule_t *rule)
{
  if(rule->manual_widget_set) return;
  _event_rule_changed(widget, rule);

  // update the rule header
  _widget_header_update(rule);
}

static void _rule_remove_config_entries(dt_lib_module_t *self, dt_lib_filtering_rule_t *rule)
{
  dt_lib_filtering_t *d = rule->lib;
  if(d->nb_rules <= 0) return;
  d->nb_rules--;
  dt_conf_set_int("plugins/lighttable/filtering/num_rules", d->nb_rules);

  // move up all still active rules by one.
  for(int i = rule->num; i < DT_COLLECTION_MAX_RULES - 1; i++)
  {
    char confname[200] = { 0 };
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i + 1);
    const int mode = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", i + 1);
    const int item = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", i + 1);
    const int off = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i + 1);
    const int top = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i + 1);
    gchar *string = dt_conf_get_string(confname);
    if(string)
    {
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i);
      dt_conf_set_int(confname, mode);
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", i);
      dt_conf_set_int(confname, item);
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", i);
      dt_conf_set_int(confname, off);
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i);
      dt_conf_set_int(confname, top);
      snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i);
      dt_conf_set_string(confname, string);
      g_free(string);
    }
  }

  _filters_gui_update(self);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, rule->prop, NULL);
}

static void _event_rule_close_cb(GtkGestureSingle *gesture, int n_press,
                                    double x, double y,
                                    dt_lib_module_t *self)
{
  GtkWidget *widget = dt_gui_get_widget(gesture);
  dt_lib_filtering_rule_t *rule = (dt_lib_filtering_rule_t *)g_object_get_data(G_OBJECT(widget), "rule");
  if(rule->manual_widget_set) return;

  if(!rule->topbar)
    _rule_remove_config_entries(self, rule);
}

static gboolean _rule_available_for_topbar(const dt_collection_properties_t prop)
{
  // we don't want to allow date filters for topbar as the design of the bar is not useful as it
  if(prop == DT_COLLECTION_PROP_DAY || prop == DT_COLLECTION_PROP_MONTH || prop == DT_COLLECTION_PROP_TIME
     || prop == DT_COLLECTION_PROP_CHANGE_TIMESTAMP || prop == DT_COLLECTION_PROP_EXPORT_TIMESTAMP
     || prop == DT_COLLECTION_PROP_PRINT_TIMESTAMP || prop == DT_COLLECTION_PROP_IMPORT_TIMESTAMP)
    return FALSE;

  return TRUE;
}

// initialise or update a rule widget. Return if the a new widget has been created
static gboolean _widget_init(dt_lib_filtering_rule_t *rule, const dt_collection_properties_t prop,
                             const gchar *text, const dt_lib_collect_mode_t mode, gboolean off, gboolean top,
                             const int pos, dt_lib_module_t *self)
{
  rule->manual_widget_set++;

  const gboolean newmain = (rule->w_main == NULL);
  const gboolean newprop = (prop != rule->prop);
  GtkWidget *hbox = NULL;

  rule->prop = prop;
  rule->topbar = top;

  if(newmain)
  {
    // the main box
    rule->w_main = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_name(rule->w_main, "collect-rule-widget");

    // the first line
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(rule->w_main), hbox, TRUE, TRUE, 0);
    gtk_widget_set_name(hbox, "collect-header-box");

    // operator type
    rule->w_operator = dt_bauhaus_combobox_new(NULL);
    dt_bauhaus_widget_set_quad_visibility(rule->w_operator, FALSE);
    dt_bauhaus_combobox_add_aligned(rule->w_operator, _("and"), DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
    dt_bauhaus_combobox_add_aligned(rule->w_operator, _("or"), DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
    dt_bauhaus_combobox_add_aligned(rule->w_operator, _("and not"), DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
    dt_bauhaus_combobox_set_selected_text_align(rule->w_operator, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
    gtk_widget_set_tooltip_text(rule->w_operator, _("define how this rule should interact with the previous one"));
    gtk_box_pack_start(GTK_BOX(hbox), rule->w_operator, FALSE, FALSE, 0);
    g_signal_connect(G_OBJECT(rule->w_operator), "value-changed", G_CALLBACK(_event_rule_changed), rule);
  }

  dt_bauhaus_combobox_set(rule->w_operator, mode);
  gtk_widget_set_sensitive(rule->w_operator, pos > 0);

  // property
  if(newmain)
  {
    rule->w_prop = dt_bauhaus_combobox_new(NULL);
    dt_bauhaus_combobox_set_selected_text_align(rule->w_prop, DT_BAUHAUS_COMBOBOX_ALIGN_MIDDLE);
    dt_bauhaus_widget_set_quad_visibility(rule->w_prop, FALSE);
    _rule_populate_prop_combo(rule);
    g_object_set_data(G_OBJECT(rule->w_prop), "rule", rule);
    dt_bauhaus_combobox_set_from_value(rule->w_prop, prop);
    g_signal_connect(G_OBJECT(rule->w_prop), "value-changed", G_CALLBACK(_event_rule_change_type), self);
    gtk_box_pack_start(GTK_BOX(hbox), rule->w_prop, TRUE, TRUE, 0);
  }
  else if(newprop)
  {
    _rule_populate_prop_combo(rule);
    dt_bauhaus_combobox_set_from_value(rule->w_prop, prop);
  }

  if(newmain)
  {
    rule->w_btn_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(hbox), rule->w_btn_box, FALSE, FALSE, 0);

    // on-off button
    rule->w_off = dtgtk_togglebutton_new(dtgtk_cairo_paint_switch, 0, NULL);
    dt_gui_add_class(rule->w_off, "dt_transparent_background");
    g_object_set_data(G_OBJECT(rule->w_off), "rule", rule);
    g_signal_connect(G_OBJECT(rule->w_off), "toggled", G_CALLBACK(_event_rule_disable), rule);
    gtk_box_pack_end(GTK_BOX(rule->w_btn_box), rule->w_off, FALSE, FALSE, 0);

    // pin button
    rule->w_pin = dtgtk_togglebutton_new(dtgtk_cairo_paint_pin, 0, NULL);
    dt_gui_add_class(rule->w_pin, "dt_transparent_background");
    g_object_set_data(G_OBJECT(rule->w_pin), "rule", rule);
    g_signal_connect(G_OBJECT(rule->w_pin), "toggled", G_CALLBACK(_rule_topbar_toggle), self);
    dt_gui_add_class(rule->w_pin, "dt_dimmed");
    gtk_box_pack_end(GTK_BOX(rule->w_btn_box), rule->w_pin, FALSE, FALSE, 0);
    gtk_widget_set_no_show_all(rule->w_pin, TRUE);

    // remove button
    rule->w_close = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL);
    g_object_set_data(G_OBJECT(rule->w_close), "rule", rule);
    dt_gui_connect_click_all(rule->w_close, _event_rule_close_cb, NULL, self);
    gtk_box_pack_end(GTK_BOX(rule->w_btn_box), rule->w_close, FALSE, FALSE, 0);
  }

  gtk_widget_set_visible(rule->w_pin, (top || _rule_available_for_topbar(prop)));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(rule->w_off), top || !off);
  if(gtk_widget_get_visible(rule->w_pin)) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(rule->w_pin), top);
  _widget_header_update(rule);

  if(newmain)
  {
    // the second line
    rule->w_widget_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(rule->w_main), rule->w_widget_box, TRUE, TRUE, 0);
  }

  _rule_set_raw_text(rule, text, FALSE);

  // initialize the specific entries if any
  _widget_init_special(rule, text, self, FALSE);

  rule->manual_widget_set--;
  return newmain;
}

static void _widget_special_destroy(dt_lib_filtering_rule_t *rule)
{
  if(rule->w_special_box)
  {
    gtk_widget_destroy(rule->w_special_box);
    rule->w_special_box = NULL;
    g_free(rule->w_specific);
    rule->w_specific = NULL;
  }
  if(rule->w_special_box_top)
  {
    gtk_widget_destroy(rule->w_special_box_top);
    rule->w_special_box_top = NULL;
    g_free(rule->w_specific_top);
    rule->w_specific_top = NULL;
  }
}

static void _filters_gui_update(dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;

  DT_ENTER_GUI_UPDATE();
  d->nb_rules = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_rules"), 0, DT_COLLECTION_MAX_RULES);
  char confname[200] = { 0 };

  // create or update defined rules
  for(int i = 0; i < d->nb_rules; i++)
  {
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/item%1d", i);
    const dt_collection_properties_t prop = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/string%1d", i);
    gchar *txt = dt_conf_get_string(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/mode%1d", i);
    const dt_lib_collect_mode_t rmode = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/top%1d", i);
    const int top = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/off%1d", i);
    const int off = (top) ? FALSE : dt_conf_get_int(confname);
    // cleanup previous special widgets
    _widget_special_destroy(&d->rule[i]);
    // recreate main widget
    if(_widget_init(&d->rule[i], prop, txt, rmode, off, top, i, self))
      gtk_box_pack_start(GTK_BOX(d->rules_box), d->rule[i].w_main, FALSE, TRUE, 0);
    gtk_widget_show_all(d->rule[i].w_main);

    // if needed, we also load the duplicate widget for the topbar
    if(top)
    {
      _widget_init_special(&d->rule[i], txt, self, TRUE);
    }
    g_free(txt);
    _widget_update(&d->rule[i]);
  }

  // remove all remaining rules
  for(int i = d->nb_rules; i < DT_COLLECTION_MAX_RULES; i++)
  {
    d->rule[i].prop = 0;
    if(d->rule[i].w_main)
    {
      gtk_widget_destroy(d->rule[i].w_main);
      d->rule[i].w_main = NULL;
      d->rule[i].w_special_box = NULL;
    }
  }

  // update topbar
  _topbar_update(self);

  DT_LEAVE_GUI_UPDATE();
}

static void _filtering_gui_update(dt_lib_module_t *self)
{
  _filters_gui_update(self);
  _sort_gui_update(self);
}

void gui_reset(dt_lib_module_t *self)
{
  GdkKeymap *kmap = gdk_keymap_get_for_display(gdk_display_get_default());
  guint state = gdk_keymap_get_modifier_state(kmap);
  if(state & GDK_CONTROL_MASK)
  {
    // we remove all rules
    _filtering_reset(_PRESET_ALL);
  }
  else
  {
    _filtering_reset(_PRESET_FILTERS | _PRESET_SORT);
  }

  _filters_gui_update(self);
  _sort_gui_update(self);

  dt_collection_set_query_flags(darktable.collection, COLLECTION_QUERY_FULL);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_UNDEF, NULL);
}

int position(const dt_lib_module_t *self)
{
  return 350;
}

static void _dt_collection_updated(gpointer instance, dt_collection_change_t query_change,
                                   dt_collection_properties_t changed_property, gpointer imgs, int next,
                                   gpointer self)
{
  dt_lib_module_t *dm = (dt_lib_module_t *)self;
  dt_lib_filtering_t *d = dm->data;

  gchar *where_ext = dt_collection_get_extended_where(darktable.collection, 99999);
  if(g_strcmp0(where_ext, d->last_where_ext))
  {
    g_free(d->last_where_ext);
    d->last_where_ext = where_ext;
  }
  else
    g_free(where_ext);

  for(int i = 0; i <= d->nb_rules; i++)
  {
    _widget_update(&d->rule[i]);
  }
}

static void _history_pretty_print(const char *buf, char *out, size_t outsize)
{
  memset(out, 0, outsize);

  if(!buf || buf[0] == '\0') return;

  int num_rules = 0;
  char str[400] = { 0 };
  int mode, item, off, top;
  int c;
  sscanf(buf, "%d", &num_rules);
  while(buf[0] != '\0' && buf[0] != ':') buf++;
  if(buf[0] == ':') buf++;

for(int k = 0; k < num_rules; k++)
  {
    const int n = sscanf(buf, "%d:%d:%d:%d:%399[^$]", &mode, &item, &off, &top, str);

    if(n == 5)
    {
      if(k > 0)
      {
        c = g_strlcpy(out, "   ", outsize);
        out += c;
        outsize -= c;
        switch(mode)
        {
          case DT_LIB_COLLECT_MODE_AND:
            c = g_strlcpy(out, _("AND"), outsize);
            out += c;
            outsize -= c;
            break;
          case DT_LIB_COLLECT_MODE_OR:
            c = g_strlcpy(out, _("OR"), outsize);
            out += c;
            outsize -= c;
            break;
          default: // case DT_LIB_COLLECT_MODE_AND_NOT:
            c = g_strlcpy(out, _("BUT NOT"), outsize);
            out += c;
            outsize -= c;
            break;
        }
        c = g_strlcpy(out, "   ", outsize);
        out += c;
        outsize -= c;
      }
      int i = 0;
      while(str[i] != '\0' && str[i] != '$') i++;
      if(str[i] == '$') str[i] = '\0';

      gchar *pretty = NULL;
      if(item == DT_COLLECTION_PROP_COLORLABEL)
        pretty = _colors_pretty_print(str);
      else if(item == DT_COLLECTION_PROP_MONTH)
        pretty = _month_pretty_print(str);
      else if(!g_strcmp0(str, "%"))
        pretty = g_strdup(_("all"));
      else
        pretty = g_strdup(str);

      if(off)
      {
        c = snprintf(out, outsize, "%s %s %s",
                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", _(" (off)"), pretty);
      }
      else
      {
        c = snprintf(out, outsize, "%s %s",
                     item < DT_COLLECTION_PROP_LAST ? dt_collection_name(item) : "???", pretty);
      }

      g_free(pretty);
      out += c;
      outsize -= c;
    }
    while(buf[0] != '$' && buf[0] != '\0') buf++;
    if(buf[0] == '$') buf++;
  }
}

static void _event_history_apply(GSimpleAction *action,
                                 GVariant *parameter,
                                 gpointer user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;

  const int hid = g_variant_get_int32(parameter);
  if(hid < 0 || hid >= dt_conf_get_int("plugins/lighttable/filtering/history_max")) return;

  char confname[200];
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/history%1d", hid);
  gchar *line = dt_conf_get_string(confname);
  if(line && line[0] != '\0')
  {
    dt_collection_deserialize(line, TRUE);
    _filters_gui_update(self);
  }
  g_free(line);
}

static void _event_history_show(GtkWidget *widget, dt_lib_module_t *self)
{
  GActionGroup *action_group = gtk_widget_get_action_group(widget, "event_history");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _event_history_apply, "i", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    self);
    gtk_widget_insert_action_group(widget, 
                                   "event_history",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

  // we show a popup with all the history entries
  GMenu *menu = g_menu_new();

  const int maxitems = dt_conf_get_int("plugins/lighttable/filtering/history_max");

  for(int i = 0; i < maxitems; i++)
  {
    char confname[200];
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/history%1d", i);
    gchar *line = dt_conf_get_string(confname);
    if(line && line[0] != '\0')
    {
      char str[2048] = { 0 };
      _history_pretty_print(line, str, sizeof(str));
      GMenuItem *item = g_menu_item_new(str, NULL);
      g_menu_item_set_action_and_target_value(item,
                                              "event_history.activate",
                                              g_variant_new("i", i));
      g_menu_append_item(menu, item);
      g_object_unref(item);
      g_free(line);
    }
    else
    {
      g_free(line);
      break;
    }
  }

  GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
  g_object_unref(menu);
  gtk_popover_popup(GTK_POPOVER(popover_menu));
}

static void _topbar_populate_prop_combo_add(GtkWidget *w, const dt_collection_properties_t prop,
                                            dt_lib_filtering_t *d)
{
  // if the filter is not implemented, we skip it
  if(!_filters_get(prop)) return;
  // if the filter is already in the topbar, we skip it too
  for(int i = 0; i < d->nb_rules; i++)
  {
    if(d->rule[i].topbar && d->rule[i].prop == prop) return;
  }

  dt_bauhaus_combobox_add_full(w, dt_collection_name(prop), DT_BAUHAUS_COMBOBOX_ALIGN_RIGHT,
                               GUINT_TO_POINTER(prop), NULL, TRUE);
}

static void _topbar_populate_rules_combo(GtkWidget *w, dt_lib_filtering_t *d)
{
  dt_bauhaus_combobox_add_full(w, "", DT_BAUHAUS_COMBOBOX_ALIGN_LEFT, GUINT_TO_POINTER(-1), NULL, TRUE);

#define ADD_COLLECT_ENTRY(value) _topbar_populate_prop_combo_add(w, value, d);
  gtk_widget_set_tooltip_text(w, _("rule property"));

  dt_bauhaus_combobox_add_section(w, _("files"));
  int nb = dt_bauhaus_combobox_length(w);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILMROLL);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOLDERS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FILENAME);
  // if we have not added any entry, remove the section
  if(nb == dt_bauhaus_combobox_length(w)) dt_bauhaus_combobox_remove_at(w, nb - 1);

  dt_bauhaus_combobox_add_section(w, _("metadata"));
  nb = dt_bauhaus_combobox_length(w);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TAG);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_RATING_RANGE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_RATING);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_COLORLABEL);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_TEXTSEARCH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GEOTAGGING);
  // if we have not added any entry, remove the section
  if(nb == dt_bauhaus_combobox_length(w)) dt_bauhaus_combobox_remove_at(w, nb - 1);

  dt_bauhaus_combobox_add_section(w, _("capture details"));
  nb = dt_bauhaus_combobox_length(w);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_CAMERA);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LENS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_APERTURE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_BIAS);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FOCAL_LENGTH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ISO);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ASPECT_RATIO);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_WHITEBALANCE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_FLASH);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_EXPOSURE_PROGRAM);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_METERING_MODE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DIMENSIONS);
  // if we have not added any entry, remove the section
  if(nb == dt_bauhaus_combobox_length(w)) dt_bauhaus_combobox_remove_at(w, nb - 1);

  dt_bauhaus_combobox_add_section(w, _("darktable"));
  nb = dt_bauhaus_combobox_length(w);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_GROUP_ID);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_DUPLICATES);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_LOCAL_COPY);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_HISTORY);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_MODULE);
  ADD_COLLECT_ENTRY(DT_COLLECTION_PROP_ORDER);
  // if we have not added any entry, remove the section
  if(nb == dt_bauhaus_combobox_length(w)) dt_bauhaus_combobox_remove_at(w, nb - 1);

#undef ADD_COLLECT_ENTRY
}

static void _topbar_rule_remove_cb(GtkGestureSingle *gesture, int n_press,
                                       double x, double y,
                                       dt_lib_module_t *self)
{
  GtkWidget *widget = dt_gui_get_widget(gesture);
  dt_lib_filtering_rule_t *rule = (dt_lib_filtering_rule_t *)g_object_get_data(G_OBJECT(widget), "rule");
  if(rule->manual_widget_set) return;
  dt_lib_filtering_t *d = self->data;

  // unpin the rule
  rule->topbar = FALSE;
  _topbar_update(self);

  // remove the rule
  _rule_remove_config_entries(self, rule);

  // reconstruct the combobox
  GtkWidget *hb = gtk_widget_get_parent(widget);
  GtkWidget *popover_vbox = gtk_widget_get_parent(hb);
  GList *childs = gtk_container_get_children(GTK_CONTAINER(popover_vbox));
  GtkWidget *combo = g_list_last(childs)->data;
  g_list_free(childs);
  dt_bauhaus_combobox_clear(combo);
  _topbar_populate_rules_combo(combo, d);

  // remove the entry from the popover
  gtk_container_remove(GTK_CONTAINER(popover_vbox), hb);
}

static GtkWidget *_topbar_menu_new_rule(dt_lib_filtering_rule_t *rule, dt_lib_module_t *self)
{
  GtkWidget *hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  GtkWidget *lb = gtk_label_new(dt_collection_name(rule->prop));
  gtk_box_pack_start(GTK_BOX(hb), lb, TRUE, TRUE, 0);
  GtkWidget *btr = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL);
  g_object_set_data(G_OBJECT(btr), "rule", rule);
  dt_gui_connect_click_all(btr, _topbar_rule_remove_cb, NULL, self);
  gtk_box_pack_start(GTK_BOX(hb), btr, FALSE, TRUE, 0);
  return hb;
}

static void _topbar_rule_add(GtkWidget *widget, dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;

  const int prop = GPOINTER_TO_INT(dt_bauhaus_combobox_get_data(widget));
  if(prop < 0) return;

  // verify the number of rules
  if(d->nb_rules >= DT_COLLECTION_MAX_RULES)
  {
    dt_control_log(_("you can't add more rules."));
    dt_bauhaus_combobox_set(widget, 0);
    return;
  }

  GVariant *v = g_variant_new("(ii)", prop, 1);
  _event_append_rule(NULL, v, self);

  // reset the combobox
  dt_bauhaus_combobox_set(widget, 0);
  dt_bauhaus_combobox_clear(widget);
  _topbar_populate_rules_combo(widget, d);

  // add a new item to the popover list
  gtk_box_pack_start(GTK_BOX(gtk_widget_get_parent(widget)),
                     _topbar_menu_new_rule(&d->rule[d->nb_rules - 1], self), TRUE, TRUE, 0);
  gtk_widget_show_all(gtk_widget_get_parent(widget));
}

static void _topbar_show_pref_menu(dt_lib_module_t *self, GtkWidget *bt)
{
  dt_lib_filtering_t *d = self->data;

  // initialize the popover
  d->topbar_popup = gtk_popover_new(bt);
  g_object_set(G_OBJECT(d->topbar_popup), "transitions-enabled", FALSE, NULL);

  GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_container_add(GTK_CONTAINER(d->topbar_popup), vbox);

  // fill the popover with all pinned rules
  GtkWidget *vbox2 = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  GtkWidget *lb = gtk_label_new(_("shown filters"));
  dt_gui_add_class(lb, "dt_section_label");
  gtk_box_pack_start(GTK_BOX(vbox2), lb, TRUE, TRUE, 0);

  for(int i = 0; i < d->nb_rules; i++)
  {
    if(d->rule[i].topbar)
    {
      gtk_box_pack_start(GTK_BOX(vbox2), _topbar_menu_new_rule(&d->rule[i], self), TRUE, TRUE, 0);
    }
  }

  // the "add new rule" part
  GtkWidget *nr = dt_bauhaus_combobox_new(NULL);
  dt_bauhaus_combobox_mute_scrolling(nr);
  dt_bauhaus_widget_set_label(nr, NULL, _("new filter"));
  _topbar_populate_rules_combo(nr, d);
  g_signal_connect(G_OBJECT(nr), "value-changed", G_CALLBACK(_topbar_rule_add), self);
  gtk_box_pack_end(GTK_BOX(vbox2), nr, TRUE, TRUE, 0);

  gtk_box_pack_start(GTK_BOX(vbox), vbox2, TRUE, TRUE, 0);
  // the actions part of the popover
  lb = gtk_label_new(_("actions"));
  dt_gui_add_class(lb, "dt_section_label");
  gtk_box_pack_start(GTK_BOX(vbox), lb, TRUE, TRUE, 0);
  GtkWidget *btr = gtk_button_new_with_label(_("reset quickfilters"));
  dt_gui_add_class(btr, "dt_transparent_background");
  dt_gui_connect_click_all(btr, _topbar_reset_press_cb, NULL, self);
  gtk_box_pack_start(GTK_BOX(vbox), btr, TRUE, TRUE, 0);

  // show the popover
  GdkDevice *pointer = gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_display_get_default()));

  int x, y;
  GdkWindow *pointer_window = gdk_device_get_window_at_position(pointer, &x, &y);
  gpointer pointer_widget = NULL;
  if(pointer_window) gdk_window_get_user_data(pointer_window, &pointer_widget);

  GdkRectangle rect = { gtk_widget_get_allocated_width(bt) / 2, gtk_widget_get_allocated_height(bt), 1, 1 };

  if(pointer_widget && bt != pointer_widget)
    gtk_widget_translate_coordinates(pointer_widget, bt, x, y, &rect.x, &rect.y);

  gtk_popover_set_pointing_to(GTK_POPOVER(d->topbar_popup), &rect);

  gtk_widget_show_all(d->topbar_popup);
}

// save a sort rule inside the conf
static void _conf_update_sort(_widgets_sort_t *sort)
{
  const gboolean order = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(sort->direction));
  const int sortid = GPOINTER_TO_UINT(dt_bauhaus_combobox_get_data(sort->sort));

  char confname[200] = { 0 };
  // if it's the last sort order, remember previous value for last order
  if(sort->num == sort->lib->nb_sort - 1)
  {
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", sort->num);
    const dt_collection_sort_t lastsort = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", sort->num);
    const int lastsortorder = dt_conf_get_int(confname);
    if(lastsort != sortid)
    {
      dt_conf_set_int("plugins/lighttable/filtering/lastsort", lastsort);
      dt_conf_set_int("plugins/lighttable/filtering/lastsortorder", lastsortorder);
    }
  }

  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", sort->num);
  dt_conf_set_int(confname, sortid);
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", sort->num);
  dt_conf_set_int(confname, order);

  _history_save(sort->lib, TRUE);
}

// update the sort asc/desc arrow
static void _sort_update_arrow(GtkWidget *widget)
{
  const gboolean reverse = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
  if(reverse)
    dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(widget), dtgtk_cairo_paint_sortby, CPF_DIRECTION_DOWN, NULL);
  else
    dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(widget), dtgtk_cairo_paint_sortby, CPF_DIRECTION_UP, NULL);
  gtk_widget_queue_draw(widget);
}

// set the sort order to the collection and update the query
static void _sort_update_query(_widgets_sort_t *sort)
{
  // if needed, we sync the filter bar
  if(sort->num == 0)
  {
    _widgets_sort_t *dest = (sort->top) ? &sort->lib->sort[0] : &sort->lib->sorttop;
    sort->lib->manual_sort_set++;
    const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(sort->direction));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dest->direction), active);
    _sort_update_arrow(dest->direction);
    const int val = GPOINTER_TO_UINT(dt_bauhaus_combobox_get_data(sort->sort));
    dt_bauhaus_combobox_set_from_value(dest->sort, val);
    sort->lib->manual_sort_set--;
  }

  // we save the sort in conf
  _conf_update_sort(sort);

  /* sometimes changes */
  dt_collection_set_query_flags(darktable.collection, COLLECTION_QUERY_FULL);

  /* updates query */
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_SORT, NULL);
}

static void _sort_reverse_changed(GtkDarktableToggleButton *widget, _widgets_sort_t *sort)
{
  if(sort->lib->manual_sort_set) return;

  _sort_update_arrow(GTK_WIDGET(widget));
  _sort_update_query(sort);
}

static void _sort_combobox_changed(GtkWidget *widget, gpointer user_data)
{
  _widgets_sort_t *sort = (_widgets_sort_t *)user_data;
  if(sort->lib->manual_sort_set) return;

  _sort_update_query(sort);
}

// this proxy function is primary called when the sort part of the filter bar is changed
static void _proxy_reset_filter(dt_lib_module_t *self, gboolean smart_filter)
{
  dt_lib_filtering_t *d = self->data;

  // reset each rule. we only throw the signal for the last one
  for(int i = 0; i < d->nb_rules; i++)
  {
    _rule_set_raw_text(&d->rule[i], "", (i == d->nb_rules - 1));
    _widget_update(&d->rule[i]);
    _conf_update_rule(&d->rule[i]);
  }
}

static void _sort_close_cb(GtkGestureSingle *gesture, int n_press,
                              double x, double y,
                              dt_lib_module_t *self)
{
  GtkWidget *widget = dt_gui_get_widget(gesture);
  _widgets_sort_t *sort = (_widgets_sort_t *)g_object_get_data(G_OBJECT(widget), "sort");
  if(sort->lib->manual_sort_set) return;

  // decrease the nb of active rules
  dt_lib_filtering_t *d = sort->lib;
  if(d->nb_sort <= 1) return;
  d->nb_sort--;
  dt_conf_set_int("plugins/lighttable/filtering/num_sort", d->nb_sort);

  // move up all still active rules by one.
  for(int i = sort->num; i < DT_COLLECTION_MAX_RULES - 1; i++)
  {
    char confname[200] = { 0 };
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", i + 1);
    const int sortid = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", i + 1);
    const int sortorder = dt_conf_get_int(confname);

    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", i);
    dt_conf_set_int(confname, sortid);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", i);
    dt_conf_set_int(confname, sortorder);
  }

  _history_save(d, TRUE);
  _sort_gui_update(self);
  dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_SORT, NULL);
}

static gboolean _sort_init(_widgets_sort_t *sort, const dt_collection_sort_t sortid, const int sortorder,
                           const int num, dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;
  d->manual_sort_set++;
  sort->num = num;
  sort->sortid = sortid;

  const gboolean top = (sort == &d->sorttop);

  const gboolean ret = (!sort->box);

  if(!sort->box)
  {
    sort->lib = d;
    sort->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    // we only allow shortcut for the first sort order, always visible
    if(num == 0)
      sort->sort = dt_bauhaus_combobox_new_action(DT_ACTION(self));
    else
      sort->sort = dt_bauhaus_combobox_new(NULL);
    dt_action_t *ac = dt_bauhaus_widget_set_label(sort->sort, NULL, _("sort order"));
    dt_bauhaus_widget_hide_label(sort->sort);
    dt_bauhaus_combobox_mute_scrolling(sort->sort);
    gtk_widget_set_tooltip_text(sort->sort, _("determine the sort order of shown images"));
    g_signal_connect(G_OBJECT(sort->sort), "value-changed", G_CALLBACK(_sort_combobox_changed), sort);
    gtk_box_pack_start(GTK_BOX(sort->box), sort->sort, TRUE, TRUE, 0);

    dt_bauhaus_combobox_add_section(sort->sort, _("files"));
    dt_bauhaus_combobox_add_introspection(sort->sort, ac, _collection_sort_names, DT_COLLECTION_SORT_FILENAME, DT_COLLECTION_SORT_ASPECT_RATIO);
    dt_bauhaus_combobox_add_section(sort->sort, _("times"));
    dt_bauhaus_combobox_add_introspection(sort->sort, ac, _collection_sort_names, DT_COLLECTION_SORT_DATETIME, DT_COLLECTION_SORT_PRINT_TIMESTAMP);
    dt_bauhaus_combobox_add_section(sort->sort, _("metadata"));
    dt_bauhaus_combobox_add_introspection(sort->sort, ac, _collection_sort_names, DT_COLLECTION_SORT_RATING, DT_COLLECTION_SORT_DESCRIPTION);
    dt_bauhaus_combobox_add_section(sort->sort, _("darktable"));
    dt_bauhaus_combobox_add_introspection(sort->sort, ac, _collection_sort_names, DT_COLLECTION_SORT_GROUP, DT_COLLECTION_SORT_SHUFFLE);

    /* reverse order checkbutton */
    sort->direction = dtgtk_togglebutton_new(dtgtk_cairo_paint_sortby, CPF_DIRECTION_UP, NULL);
    gtk_widget_set_halign(sort->direction, GTK_ALIGN_START);
    gtk_box_pack_start(GTK_BOX(sort->box), sort->direction, FALSE, TRUE, 0);
    g_signal_connect(G_OBJECT(sort->direction), "toggled", G_CALLBACK(_sort_reverse_changed), sort);
    dt_gui_add_class(sort->direction, "dt_ignore_fg_state");
    if(num == 0)
      dt_action_define(DT_ACTION(self), NULL, _("sort direction"), sort->direction, &dt_action_def_toggle);

    sort->close = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL,
      &(dtgtk_button_config_t){
        .tooltip = _("remove this sort order"),
      });
    gtk_widget_set_no_show_all(sort->close, TRUE);
    g_object_set_data(G_OBJECT(sort->close), "sort", sort);
    dt_gui_connect_click_all(sort->close, _sort_close_cb, NULL, self);
    gtk_box_pack_start(GTK_BOX(sort->box), sort->close, FALSE, FALSE, 0);
  }

  dt_bauhaus_combobox_set_from_value(sort->sort, sortid);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(sort->direction), sortorder);
  gtk_widget_set_visible(sort->close, (sort->lib->nb_sort > 1) && !top);
  _sort_update_arrow(sort->direction);

  gtk_widget_show_all(sort->box);

  d->manual_sort_set--;
  return ret;
}

static void _sort_gui_update(dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;

  DT_ENTER_GUI_UPDATE();
  d->nb_sort = CLAMP(dt_conf_get_int("plugins/lighttable/filtering/num_sort"), 0, DT_COLLECTION_MAX_RULES);
  char confname[200] = { 0 };

  // handle the case where no sort item is already defined
  if(d->nb_sort == 0)
  {
    // set conf values
    dt_conf_set_int("plugins/lighttable/filtering/num_sort", 1);
    dt_conf_set_int("plugins/lighttable/filtering/sort0", DT_COLLECTION_SORT_FILENAME);
    dt_conf_set_int("plugins/lighttable/filtering/sortorder0", 0);
    d->nb_sort = 1;
  }

  // create or update defined rules
  for(int i = 0; i < d->nb_sort; i++)
  {
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", i);
    const dt_collection_sort_t sort = dt_conf_get_int(confname);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", i);
    const int sortorder = dt_conf_get_int(confname);

    // recreate main widget
    if(_sort_init(&d->sort[i], sort, sortorder, i, self))
      gtk_grid_attach(GTK_GRID(d->sort_box), d->sort[i].box, 1, i, 1, 1);

    // we also put the first sort item to the topbar
    if(i == 0)
    {
      d->sorttop.top = TRUE;
      GtkWidget *sort_topbox = dt_view_filter_get_sort_box(darktable.view_manager);
      if(sort_topbox && _sort_init(&d->sorttop, sort, sortorder, i, self))
      {
        gtk_box_pack_start(GTK_BOX(sort_topbox), d->sorttop.box, FALSE, TRUE, 0);
      }
    }
  }

  // remove all remaining rules
  for(int i = d->nb_sort; i < DT_COLLECTION_MAX_RULES; i++)
  {
    d->sort[i].sortid = 0;
    if(d->sort[i].box)
    {
      gtk_widget_destroy(d->sort[i].box);
      d->sort[i].box = NULL;
    }
  }

  DT_LEAVE_GUI_UPDATE();
}

static void _sort_append_sort(GSimpleAction *action,
                              GVariant *parameter,
                              gpointer user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  dt_lib_filtering_t *d = self->data;

  // add new rule
  const int sortid = g_variant_get_int32(parameter);
  char confname[200] = { 0 };

  if(sortid >= 0)
  {
    // add an empty rule
    if(d->nb_sort >= DT_COLLECTION_MAX_RULES)
    {
      dt_control_log(_("you can't have more than %d sort orders"), DT_COLLECTION_MAX_RULES);
      return;
    }
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort%1d", d->nb_sort);
    dt_conf_set_int(confname, sortid);
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sortorder%1d", d->nb_sort);
    dt_conf_set_int(confname, 0);
    d->nb_sort++;
    dt_conf_set_int("plugins/lighttable/filtering/num_sort", d->nb_sort);

    _history_save(d, TRUE);
    _sort_gui_update(self);
    dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD, DT_COLLECTION_PROP_SORT, NULL);
  }
}

static void _sort_show_add_popup(GtkWidget *widget, dt_lib_module_t *self)
{
  GActionGroup *action_group = gtk_widget_get_action_group(widget, "sort");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _sort_append_sort, "i", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    self);
    gtk_widget_insert_action_group(widget, 
                                   "sort",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

  GMenu *menu = g_menu_new();

  // we show a popup with all the possible sort
  for(const dt_introspection_type_enum_tuple_t *list = _collection_sort_names; list->name; list++)
    _popup_add_sort_item(menu, Q_(list->name), list->value);

  GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
  g_object_unref(menu);
  gtk_popover_popup(GTK_POPOVER(popover_menu));
}

static void _sort_history_pretty_print(const char *buf, char *out, size_t outsize)
{
  memset(out, 0, outsize);

  if(!buf || buf[0] == '\0') return;

  int num_rules = 0;
  int sortid, sortorder;
  int c;
  sscanf(buf, "%d", &num_rules);
  while(buf[0] != '\0' && buf[0] != ':') buf++;
  if(buf[0] == ':') buf++;

  for(int k = 0; k < num_rules; k++)
  {
    const int n = sscanf(buf, "%d:%d", &sortid, &sortorder);

    if(n == 2)
    {
      const dt_introspection_type_enum_tuple_t *list = _collection_sort_names;
      while(list->name && list->value != sortid) list++;

      c = snprintf(out, outsize, "%s%s (%s)", (k > 0) ? " - " : "", _(list->name),
                   (sortorder) ? _("DESC") : _("ASC"));
      out += c;
      outsize -= c;
    }
    while(buf[0] != '$' && buf[0] != '\0') buf++;
    if(buf[0] == '$') buf++;
  }
}

static void _sort_history_apply(GSimpleAction *action,
                                GVariant *parameter,
                                gpointer user_data)
{
  dt_lib_module_t *self = (dt_lib_module_t *)user_data;

  const int hid = g_variant_get_int32(parameter);
  if(hid < 0 || hid >= dt_conf_get_int("plugins/lighttable/filtering/sort_history_max")) return;

  char confname[200];
  snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort_history%1d", hid);
  gchar *line = dt_conf_get_string(confname);
  if(line && line[0] != '\0')
  {
    dt_collection_sort_deserialize(line);
    _sort_gui_update(self);
  }
  g_free(line);
}

static void _dt_images_order_change(gpointer instance, gpointer order, gpointer self)
{
  gchar *txt = (gchar *)order;
  if(txt)
  {
    dt_collection_sort_deserialize(txt);
    _sort_gui_update(self);
  }
}

static void _sort_history_show(GtkWidget *widget, dt_lib_module_t *self)
{
  GActionGroup *action_group = gtk_widget_get_action_group(widget, "sort_history");
  if(action_group == NULL)
  {
    GActionEntry action_entries[] =
    {
      { "activate", _sort_history_apply, "i", NULL },
    };

    action_group = G_ACTION_GROUP(g_simple_action_group_new());
    g_action_map_add_action_entries(G_ACTION_MAP(action_group),
                                    action_entries,
                                    G_N_ELEMENTS(action_entries),
                                    self);
    gtk_widget_insert_action_group(widget, 
                                   "sort_history",
                                   G_ACTION_GROUP(action_group));
    g_object_unref(action_group);
  }

  // we show a popup with all the history entries
  GMenu *menu = g_menu_new();

  const int maxitems = dt_conf_get_int("plugins/lighttable/filtering/sort_history_max");

  for(int i = 0; i < maxitems; i++)
  {
    char confname[200];
    snprintf(confname, sizeof(confname), "plugins/lighttable/filtering/sort_history%1d", i);
    gchar *line = dt_conf_get_string(confname);
    if(line && line[0] != '\0')
    {
      char str[2048] = { 0 };
      _sort_history_pretty_print(line, str, sizeof(str));
      GMenuItem *item = g_menu_item_new(str, NULL);
      g_menu_item_set_action_and_target_value(item,
                                              "sort_history.activate",
                                              g_variant_new("i", i));
      g_menu_append_item(menu, item);
      g_object_unref(item);
      g_free(line);
    }
    else
    {
      g_free(line);
      break;
    }
  }

  GtkWidget *popover_menu = dt_gui_popover_menu_from_model(widget, menu);
  g_object_unref(menu);
  gtk_popover_popup(GTK_POPOVER(popover_menu));
}

void gui_init(dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = calloc(1, sizeof(dt_lib_filtering_t));

  self->data = (void *)d;
  self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_widget_set_name(self->widget, "module-filtering");
  dt_gui_add_class(self->widget, "dt_big_btn_canvas");
  dt_gui_add_help_link(self->widget, self->plugin_name);

  d->nb_rules = 0;
  d->params = (dt_lib_filtering_params_t *)g_malloc0(sizeof(dt_lib_filtering_params_t));

  const int nb = sizeof(filters) / sizeof(_filter_t);
  for(int i = 0; i < nb; i++)
  {
    dt_lib_filtering_rule_t temp_rule = {0};
    temp_rule.w_special_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

    filters[i].widget_init(&temp_rule, filters[i].prop, "", self, FALSE);

    gtk_widget_destroy(temp_rule.w_special_box);
    g_free(temp_rule.w_specific);
  }

  for(int i = 0; i < DT_COLLECTION_MAX_RULES; i++)
  {
    d->rule[i].num = i;
    d->rule[i].lib = d;
  }

  // the box to insert the collect rules
  d->rules_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_box_pack_start(GTK_BOX(self->widget), d->rules_box, FALSE, TRUE, 0);

  // the bottom buttons for the rules
  GtkWidget *bhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_box_set_homogeneous(GTK_BOX(bhbox), TRUE);
  gtk_box_pack_start(GTK_BOX(self->widget), bhbox, TRUE, TRUE, 0);
  GtkWidget *btn = dt_action_button_new(self, N_("new rule"), G_CALLBACK(_event_rule_append), self,
                                        _("append new rule to collect images"), 0, 0);
  gtk_box_pack_start(GTK_BOX(bhbox), btn, TRUE, TRUE, 0);
  btn = dt_action_button_new(self, N_("history"), G_CALLBACK(_event_history_show), self,
                             _("revert to a previous set of rules"), 0, 0);
  gtk_box_pack_start(GTK_BOX(bhbox), btn, TRUE, TRUE, 0);
  gtk_widget_show_all(bhbox);

  // the sorting part
  GtkWidget *spacer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_box_pack_start(GTK_BOX(self->widget), spacer, TRUE, TRUE, 0);
  d->sort_box = gtk_grid_new();
  gtk_grid_attach(GTK_GRID(d->sort_box), gtk_label_new(_("sort by")), 0, 0, 1, 1);
  gtk_widget_set_name(d->sort_box, "filter-sort-box");
  gtk_box_pack_start(GTK_BOX(self->widget), d->sort_box, TRUE, TRUE, 0);

  // the bottom buttons for the sort
  bhbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
  gtk_box_set_homogeneous(GTK_BOX(bhbox), TRUE);
  gtk_box_pack_start(GTK_BOX(self->widget), bhbox, TRUE, TRUE, 0);
  btn = dt_action_button_new(self, N_("new sort"), G_CALLBACK(_sort_show_add_popup), self,
                             _("append new sort to order images"), 0, 0);
  gtk_box_pack_start(GTK_BOX(bhbox), btn, TRUE, TRUE, 0);
  btn = dt_action_button_new(self, N_("history"), G_CALLBACK(_sort_history_show), self,
                             _("revert to a previous set of sort orders"), 0, 0);
  gtk_box_pack_start(GTK_BOX(bhbox), btn, TRUE, TRUE, 0);
  gtk_widget_show_all(bhbox);

  /* setup proxy */
  darktable.view_manager->proxy.module_filtering.module = self;
  darktable.view_manager->proxy.module_filtering.update = _filtering_gui_update;
  darktable.view_manager->proxy.module_filtering.reset_filter = _proxy_reset_filter;
  darktable.view_manager->proxy.module_filtering.show_pref_menu = _topbar_show_pref_menu;

  d->last_where_ext = dt_collection_get_extended_where(darktable.collection, 99999);

  // test if the filter toolbar module is already loaded and update the gui in this case
  // otherwise, the filter toolbar module will do it in it's gui_init()
  if(darktable.view_manager->proxy.filter.module) _filtering_gui_update(self);

  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _dt_collection_updated);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_IMAGES_ORDER_CHANGE, _dt_images_order_change);
}

void gui_cleanup(dt_lib_module_t *self)
{
  dt_lib_filtering_t *d = self->data;

  for(int i = 0; i < DT_COLLECTION_MAX_RULES; i++)
  {
    d->rule[i].cleaning = TRUE;
  }

  darktable.view_manager->proxy.module_filtering.module = NULL;
  free(d->params);

  /* TODO: Make sure we are cleaning up all allocations */

  free(self->data);
  self->data = NULL;
}

void view_enter(struct dt_lib_module_t *self, struct dt_view_t *old_view, struct dt_view_t *new_view)
{
  dt_lib_filtering_t *d = self->data;
  d->leaving = FALSE;
  // if we enter lighttable view, then we need to populate the filter topbar
  // we do it here because we are sure that both libs are loaded at this point
  _topbar_update(self);

  // we change the tooltip of the reset button here, as we are sure the header is defined now
  gtk_widget_set_tooltip_text(self->reset_button, _("reset\nctrl+click to remove pinned rules too"));
}

void view_leave(struct dt_lib_module_t *self, struct dt_view_t *old_view, struct dt_view_t *new_view)
{
  if(!new_view)
  {
    // we are leaving dt, so we want to avoid pb with focus and such
    dt_lib_filtering_t *d = self->data;
    d->leaving = TRUE;
  }
}
// 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