/*
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 .
*/
#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
#include
#include
#ifndef _WIN32
#include
#endif
#ifdef GDK_WINDOWING_QUARTZ
#endif
#include
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(¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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(), ¶ms, 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%s\n%s\n%s", dt_collection_name(special->rule->prop),
_("click or click&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