/*
This file is part of darktable,
Copyright (C) 2010-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 "common/gdk_event_utils.h"
#include "common/collection.h"
#include "common/selection.h"
#include "common/darktable.h"
#include "common/debug.h"
#include "common/tags.h"
#include "control/conf.h"
#include "control/control.h"
#include "dtgtk/button.h"
#include "gui/preferences_dialogs.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "gui/drag_and_drop.h"
#include "libs/lib.h"
#include "libs/lib_api.h"
#include "views/view.h"
#include
#include
#include
#include
#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif
#include
#include
#define FLOATING_ENTRY_WIDTH DT_PIXEL_APPLY_DPI(250)
DT_MODULE(1)
static void _lib_tagging_tag_redo(dt_action_t *action);
static void _lib_tagging_tag_show(dt_action_t *action);
typedef struct dt_lib_tagging_t
{
char keyword[1024];
GtkEntry *entry;
GtkWidget *clear_button;
GtkTreeView *attached_view, *dictionary_view;
GtkWidget *attach_button, *detach_button, *new_button, *import_button, *export_button;
GtkWidget *toggle_tree_button, *toggle_suggestion_button, *toggle_sort_button, *toggle_hide_button, *toggle_dttags_button;
GtkListStore *attached_liststore, *dictionary_liststore;
GtkTreeStore *dictionary_treestore;
GtkTreeModelFilter *dictionary_listfilter, *dictionary_treefilter;
GtkWidget *floating_tag_window;
GList *floating_tag_imgs;
gboolean tree_flag, suggestion_flag, sort_count_flag, hide_path_flag, dttags_flag;
char *collection;
char *last_tag;
struct
{
gchar *tagname;
GtkTreePath *path;
GtkTreePath *lastpath;
guint expand_timeout;
guint scroll_timeout;
int last_y;
gboolean root;
gboolean tag_source;
} drag;
gboolean update_selected_tags;
} dt_lib_tagging_t;
typedef struct dt_tag_op_t
{
gint tagid;
char *newtagname;
char *oldtagname;
gboolean tree_flag;
} dt_tag_op_t;
typedef enum dt_lib_tagging_cols_t
{
DT_LIB_TAGGING_COL_TAG = 0,
DT_LIB_TAGGING_COL_ID,
DT_LIB_TAGGING_COL_PATH,
DT_LIB_TAGGING_COL_SYNONYM,
DT_LIB_TAGGING_COL_COUNT,
DT_LIB_TAGGING_COL_SEL,
DT_LIB_TAGGING_COL_FLAGS,
DT_LIB_TAGGING_COL_VISIBLE,
DT_LIB_TAGGING_NUM_COLS
} dt_lib_tagging_cols_t;
typedef enum dt_tag_sort_id
{
DT_TAG_SORT_PATH_ID,
DT_TAG_SORT_NAME_ID,
DT_TAG_SORT_COUNT_ID
} dt_tag_sort_id;
static void _save_last_tag_used(const char *tags,
dt_lib_tagging_t *d);
const char *name(dt_lib_module_t *self)
{
return _("tagging");
}
const char *description(dt_lib_module_t *self)
{
return _("add or remove keywords for\n"
"the currently selected images");
}
dt_view_type_flags_t views(dt_lib_module_t *self)
{
if(dt_conf_get_bool("plugins/darkroom/tagging/visible"))
return DT_VIEW_LIGHTTABLE | DT_VIEW_DARKROOM | DT_VIEW_MAP | DT_VIEW_TETHERING;
else
return DT_VIEW_LIGHTTABLE | DT_VIEW_MAP | DT_VIEW_TETHERING;
}
uint32_t container(dt_lib_module_t *self)
{
return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
}
static gboolean _is_user_tag(GtkTreeModel *model,
GtkTreeIter *iter)
{
char *path;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
const gboolean user_tag = !g_str_has_prefix(path, "darktable|") || g_str_has_prefix(path, "darktable|style|");
g_free(path);
return user_tag;
}
static void _unselect_all_in_view(GtkTreeView *view)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
gtk_tree_selection_unselect_all(selection);
}
static void _update_atdetach_buttons(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
const gboolean has_act_on = (dt_act_on_get_images_nb(FALSE, FALSE) > 0);
const gint dict_tags_sel_cnt = gtk_tree_selection_count_selected_rows
(gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view)));
GtkTreeSelection *selection = gtk_tree_view_get_selection
(GTK_TREE_VIEW(d->attached_view));
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
GtkTreeIter iter;
gboolean attached_tags_sel = FALSE;
if(gtk_tree_selection_get_selected(selection, &model, &iter))
// check this is a darktable tag
attached_tags_sel = _is_user_tag(model, &iter);
gtk_widget_set_sensitive(GTK_WIDGET(d->attach_button),
has_act_on && dict_tags_sel_cnt > 0);
gtk_widget_set_sensitive(GTK_WIDGET(d->detach_button),
has_act_on && attached_tags_sel);
}
static void _propagate_sel_to_parents(GtkTreeModel *model,
GtkTreeIter *iter)
{
guint sel;
GtkTreeIter parent, child = *iter;
while(gtk_tree_model_iter_parent(model, &parent, &child))
{
gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
if(sel == DT_TS_NO_IMAGE)
gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
DT_LIB_TAGGING_COL_SEL, DT_TS_SOME_IMAGES, -1);
child = parent;
}
}
static gboolean _set_matching_tag_visibility(GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
gboolean visible;
gchar *tagname = NULL;
gchar *synonyms = NULL;
gtk_tree_model_get(model, iter,
DT_LIB_TAGGING_COL_PATH, &tagname,
DT_LIB_TAGGING_COL_SYNONYM, &synonyms, -1);
if(!d->keyword[0])
visible = TRUE;
else
{
if(synonyms && synonyms[0]) dt_util_str_cat(&tagname, ", %s", synonyms);
gchar *haystack = g_utf8_strdown(tagname, -1);
gchar *needle = g_utf8_strdown(d->keyword, -1);
visible = (g_strrstr(haystack, needle) != NULL);
g_free(haystack);
g_free(needle);
}
if(d->tree_flag)
gtk_tree_store_set(GTK_TREE_STORE(model), iter,
DT_LIB_TAGGING_COL_VISIBLE, visible, -1);
else
gtk_list_store_set(GTK_LIST_STORE(model), iter,
DT_LIB_TAGGING_COL_VISIBLE, visible, -1);
g_free(tagname);
g_free(synonyms);
return FALSE;
}
static gboolean _tree_reveal_func(GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
gpointer data)
{
gboolean state;
GtkTreeIter parent, child = *iter;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_VISIBLE, &state, -1);
if(!state) return FALSE;
while(gtk_tree_model_iter_parent(model, &parent, &child))
{
gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_VISIBLE, &state, -1);
gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
DT_LIB_TAGGING_COL_VISIBLE, TRUE, -1);
child = parent;
}
return FALSE;
}
static void _sort_attached_list(dt_lib_module_t *self,
const gboolean force)
{
dt_lib_tagging_t *d = self->data;
if(force && d->sort_count_flag)
{
// ugly but when sorted by count _tree_tagname_show() is not triggered
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->attached_liststore),
DT_TAG_SORT_NAME_ID, GTK_SORT_ASCENDING);
}
const gint sort = d->sort_count_flag
? DT_TAG_SORT_COUNT_ID
: d->hide_path_flag ? DT_TAG_SORT_NAME_ID : DT_TAG_SORT_PATH_ID;
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->attached_liststore),
sort, GTK_SORT_ASCENDING);
}
static void _sort_dictionary_list(dt_lib_module_t *self,
const gboolean force)
{
dt_lib_tagging_t *d = self->data;
if(!d->tree_flag)
{
if(force && d->sort_count_flag)
{
// ugly but when sorted by count _tree_tagname_show() is not triggered
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_liststore),
DT_TAG_SORT_NAME_ID, GTK_SORT_ASCENDING);
}
const gint sort = d->sort_count_flag
? DT_TAG_SORT_COUNT_ID
: d->hide_path_flag ? DT_TAG_SORT_NAME_ID : DT_TAG_SORT_PATH_ID;
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_liststore),
sort, GTK_SORT_ASCENDING);
}
else
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(d->dictionary_treestore),
DT_TAG_SORT_PATH_ID, GTK_SORT_ASCENDING);
}
// find a tag on the tree
static gboolean _find_tag_iter_tagname(GtkTreeModel *model,
GtkTreeIter *iter,
const char *tagname,
const gboolean needle)
{
gboolean found = FALSE;
if(!tagname) return found;
char *path;
do
{
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
if(needle)
{
gchar *haystack = g_utf8_strdown(path, -1);
found = g_strstr_len(haystack, strlen(haystack), tagname) != NULL;
g_free(haystack);
}
else
found = !g_strcmp0(tagname, path);
g_free(path);
if(found) return found;
GtkTreeIter child, parent = *iter;
if(gtk_tree_model_iter_children(model, &child, &parent))
{
found = _find_tag_iter_tagname(model, &child, tagname, needle);
if(found)
{
*iter = child;
return found;
}
}
} while(gtk_tree_model_iter_next(model, iter));
return found;
}
static void _show_iter_on_view(GtkTreeView *view,
GtkTreeIter *iter,
const gboolean select)
{
GtkTreeModel *model = gtk_tree_view_get_model(view);
GtkTreePath *path = gtk_tree_model_get_path(model, iter);
gtk_tree_view_expand_to_path(view, path);
gtk_tree_view_scroll_to_cell(view, path, NULL, TRUE, 0.5, 0.5);
gtk_tree_path_free(path);
if(select)
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
gtk_tree_selection_select_iter(selection, iter);
}
}
// make the tag visible on view
static void _show_tag_on_view(GtkTreeView *view,
const char *tagname,
const gboolean needle,
const gboolean select)
{
if(tagname)
{
char *lt = g_strdup(tagname);
char *t = g_strstrip(lt);
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(view);
if(gtk_tree_model_get_iter_first(model, &iter))
{
if(_find_tag_iter_tagname(model, &iter, t, needle))
_show_iter_on_view(view, &iter, select);
}
g_free(lt);
}
}
static void _show_keyword_on_view(GtkTreeView *view,
const char *keyword,
const gboolean select)
{
gchar *needle = g_utf8_strdown(keyword, -1);
_show_tag_on_view(view, needle, TRUE, select);
g_free(needle);
}
static gboolean _select_previous_user_attached_tag(const int index,
GtkTreeView *view)
{
if(index == 0)
return FALSE;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(view);
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
int i = 0;
for(; valid && i < index -1; i++)
valid = gtk_tree_model_iter_next(model, &iter);
for(; valid; valid = gtk_tree_model_iter_previous(model, &iter))
{
if(_is_user_tag(model, &iter))
{
_show_iter_on_view(view, &iter, TRUE);
return TRUE;
}
}
return FALSE;
}
static gboolean _select_next_user_attached_tag(const int index,
GtkTreeView *view)
{
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(view);
gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
int i = 0;
for(; valid && i < index; i++)
valid = gtk_tree_model_iter_next(model, &iter);
for(; valid; valid = gtk_tree_model_iter_next(model, &iter))
{
if(_is_user_tag(model, &iter))
{
_show_iter_on_view(view, &iter, TRUE);
return TRUE;
}
}
if(index)
return _select_previous_user_attached_tag(index, view);
return FALSE;
}
static void _init_treeview(dt_lib_module_t *self,
const int which)
{
dt_lib_tagging_t *d = self->data;
GList *tags = NULL;
uint32_t count;
GtkTreeIter iter;
GtkTreeView *view;
GtkTreeModel *store;
GtkTreeModel *model;
gboolean no_sel = FALSE;
if(which == 0) // tags of selected images
{
const dt_imgid_t imgsel = dt_control_get_mouse_over_id();
no_sel = dt_is_valid_imgid(imgsel) || dt_selected_images_count() == 1;
count = dt_tag_get_attached(imgsel, &tags, d->dttags_flag ? FALSE : TRUE);
view = d->attached_view;
model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
store = model;
g_object_ref(model);
gtk_tree_view_set_model(GTK_TREE_VIEW(view), NULL);
}
else // dictionary_view tags of typed text
{
if(!d->tree_flag && d->suggestion_flag)
count = dt_tag_get_suggestions(&tags);
else
count = dt_tag_get_with_usage(&tags);
view = d->dictionary_view;
if(d->tree_flag)
store = GTK_TREE_MODEL(d->dictionary_treestore);
else
store = GTK_TREE_MODEL(d->dictionary_liststore);
// Destroy the filter before clearing the underlying store: the
// bulk row-deleted emissions corrupt the filter's internal
// GSequence cache and crash in node_get_pos (same defensive
// pattern as libs/collect.c). The filter is recreated once the
// store has been repopulated.
model = NULL;
g_object_ref(store);
gtk_tree_view_set_model(GTK_TREE_VIEW(view), NULL);
if(d->tree_flag)
d->dictionary_treefilter = NULL;
else
d->dictionary_listfilter = NULL;
}
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
GTK_SORT_ASCENDING);
if(which && d->tree_flag)
{
gtk_tree_store_clear(GTK_TREE_STORE(store));
{
char **last_tokens = NULL;
int last_tokens_length = 0;
GtkTreeIter last_parent = { 0 };
GList *sorted_tags = dt_sort_tag(tags, 0); // ordered by full tag name
tags = sorted_tags;
for(GList *taglist = tags; taglist; taglist = g_list_next(taglist))
{
const gchar *tag = ((dt_tag_t *)taglist->data)->tag;
if(tag == NULL) continue;
char **tokens;
tokens = g_strsplit(tag, "|", -1);
if(tokens)
{
// find the number of common parts at the beginning of tokens and last_tokens
GtkTreeIter parent = last_parent;
const int tokens_length = g_strv_length(tokens);
int common_length = 0;
if(last_tokens)
{
while(tokens[common_length] && last_tokens[common_length] &&
!g_strcmp0(tokens[common_length], last_tokens[common_length]))
{
common_length++;
}
// point parent iter to where the entries should be added
for(int i = common_length; i < last_tokens_length; i++)
{
gtk_tree_model_iter_parent(GTK_TREE_MODEL(store), &parent, &last_parent);
last_parent = parent;
}
}
// insert everything from tokens past the common part
char *pth = NULL;
for(int i = 0; i < common_length; i++)
dt_util_str_cat(&pth, "%s|", tokens[i]);
for(char **token = &tokens[common_length]; *token; token++)
{
dt_util_str_cat(&pth, "%s|", *token);
gchar *pth2 = g_strdup(pth);
pth2[strlen(pth2) - 1] = '\0';
gtk_tree_store_insert_with_values
(GTK_TREE_STORE(store), &iter,
common_length > 0 ? &parent : NULL, -1,
DT_LIB_TAGGING_COL_TAG, *token,
DT_LIB_TAGGING_COL_ID, (token == &tokens[tokens_length-1]) ?
((dt_tag_t *)taglist->data)->id : 0,
DT_LIB_TAGGING_COL_PATH, pth2,
DT_LIB_TAGGING_COL_COUNT, (token == &tokens[tokens_length-1]) ?
((dt_tag_t *)taglist->data)->count : 0,
DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)taglist->data)->select,
DT_LIB_TAGGING_COL_FLAGS, ((dt_tag_t *)taglist->data)->flags,
DT_LIB_TAGGING_COL_SYNONYM, ((dt_tag_t *)taglist->data)->synonym,
DT_LIB_TAGGING_COL_VISIBLE, TRUE,
-1);
if(((dt_tag_t *)taglist->data)->select)
_propagate_sel_to_parents(GTK_TREE_MODEL(store), &iter);
common_length++;
parent = iter;
g_free(pth2);
}
g_free(pth);
// remember things for the next round
if(last_tokens) g_strfreev(last_tokens);
last_tokens = tokens;
last_parent = parent;
last_tokens_length = tokens_length;
}
}
g_strfreev(last_tokens);
}
// recreate the filter on the freshly populated store
model = gtk_tree_model_filter_new(store, NULL);
gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model),
DT_LIB_TAGGING_COL_VISIBLE);
d->dictionary_treefilter = GTK_TREE_MODEL_FILTER(model);
if(d->keyword[0])
{
gtk_tree_model_foreach(store,
(GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_tree_reveal_func, NULL);
}
gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
g_object_unref(model);
g_object_unref(store);
}
else
{
gtk_list_store_clear(GTK_LIST_STORE(store));
if(count > 0 && tags)
{
for(GList *tag = tags; tag; tag = g_list_next(tag))
{
const char *subtag = g_strrstr(((dt_tag_t *)tag->data)->tag, "|");
gtk_list_store_insert_with_values
(GTK_LIST_STORE(store), NULL, -1,
DT_LIB_TAGGING_COL_TAG, !subtag ? ((dt_tag_t *)tag->data)->tag : subtag + 1,
DT_LIB_TAGGING_COL_ID, ((dt_tag_t *)tag->data)->id,
DT_LIB_TAGGING_COL_PATH, ((dt_tag_t *)tag->data)->tag,
DT_LIB_TAGGING_COL_COUNT, ((dt_tag_t *)tag->data)->count,
DT_LIB_TAGGING_COL_SEL, no_sel ? DT_TS_NO_IMAGE :
((dt_tag_t *)tag->data)->select,
DT_LIB_TAGGING_COL_FLAGS, ((dt_tag_t *)tag->data)->flags,
DT_LIB_TAGGING_COL_SYNONYM, ((dt_tag_t *)tag->data)->synonym,
DT_LIB_TAGGING_COL_VISIBLE, TRUE,
-1);
}
}
if(which)
{
// dictionary_view list mode: recreate the filter on the
// freshly populated store (see comment in the dictionary
// setup branch above for the rationale).
model = gtk_tree_model_filter_new(store, NULL);
gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model),
DT_LIB_TAGGING_COL_VISIBLE);
d->dictionary_listfilter = GTK_TREE_MODEL_FILTER(model);
if(d->keyword[0])
gtk_tree_model_foreach(store,
(GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
g_object_unref(model);
g_object_unref(store);
}
else
{
// attached_view: model is the liststore itself, no filter
gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
g_object_unref(model);
}
}
if(which)
_sort_dictionary_list(self, FALSE);
else
_sort_attached_list(self, FALSE);
// Free result...
dt_tag_free_result(&tags);
}
static void _tree_tagname_show(GtkTreeViewColumn *col,
GtkCellRenderer *renderer,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data,
const gboolean dictionary_view)
{
dt_lib_module_t *self = (dt_lib_module_t *)data;
dt_lib_tagging_t *d = self->data;
guint tagid;
gchar *name;
gchar *path;
guint count;
gchar *coltext;
gint flags;
gtk_tree_model_get(model, iter,
DT_LIB_TAGGING_COL_ID, &tagid,
DT_LIB_TAGGING_COL_TAG, &name,
DT_LIB_TAGGING_COL_COUNT, &count,
DT_LIB_TAGGING_COL_FLAGS, &flags,
DT_LIB_TAGGING_COL_PATH, &path, -1);
const gboolean hide = dictionary_view
? (d->tree_flag ? TRUE : d->hide_path_flag)
: d->hide_path_flag;
const gboolean istag = !(flags & DT_TF_CATEGORY) && tagid;
if((dictionary_view && !count) || (!dictionary_view && count <= 1))
{
coltext = g_markup_printf_escaped
(istag ? "%s" : "%s", hide ? name : path);
}
else
{
coltext = g_markup_printf_escaped
(istag ? "%s (%d)" : "%s (%d)", hide ? name : path, count);
}
g_object_set(renderer, "markup", coltext, NULL);
g_free(coltext);
g_free(name);
g_free(path);
}
static void _tree_tagname_show_attached(GtkTreeViewColumn *col,
GtkCellRenderer *renderer,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data)
{
_tree_tagname_show(col, renderer, model, iter, data, 0);
}
static void _tree_tagname_show_dictionary(GtkTreeViewColumn *col,
GtkCellRenderer *renderer,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data)
{
_tree_tagname_show(col, renderer, model, iter, data, 1);
}
static void _tree_select_show(GtkTreeViewColumn *col,
GtkCellRenderer *renderer,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer data)
{
guint tagid;
guint select;
gboolean active = FALSE;
gboolean inconsistent = FALSE;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_ID, &tagid,
DT_LIB_TAGGING_COL_SEL, &select, -1);
if(!tagid)
{
if(select != DT_TS_NO_IMAGE) inconsistent = TRUE;
}
else
{
if(select == DT_TS_ALL_IMAGES) active = TRUE;
else if(select == DT_TS_SOME_IMAGES) inconsistent = TRUE;
}
g_object_set(renderer, "active", active, "inconsistent", inconsistent, NULL);
}
void gui_update(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
if(d->update_selected_tags)
_init_treeview(self, 0);
d->update_selected_tags = FALSE;
_update_atdetach_buttons(self);
}
static void _lib_tagging_redraw_callback(gpointer instance,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
d->update_selected_tags = TRUE;
dt_lib_gui_queue_update(self);
}
static void _lib_tagging_tags_changed_callback(gpointer instance,
dt_lib_module_t *self)
{
_init_treeview(self, 0);
_init_treeview(self, 1);
}
static void _collection_updated_callback(gpointer instance,
const dt_collection_change_t query_change,
const dt_collection_properties_t changed_property,
gpointer imgs,
const int next,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
d->collection[0] = '\0';
dt_lib_gui_queue_update(self);
}
static void _raise_signal_tag_changed(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
// when collection is on tag any attach & detach becomes very slow
// speeding up when jumping from tag collection to the other
// the cost is that tag collection doesn't reflect the tag changes real time
if(!d->collection[0])
{
// raises change only for other modules
dt_control_signal_block_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
dt_control_signal_block_by_func(darktable.signals,
G_CALLBACK(_lib_tagging_tags_changed_callback), self);
DT_CONTROL_SIGNAL_RAISE(DT_SIGNAL_TAG_CHANGED);
dt_control_signal_unblock_by_func(darktable.signals,
G_CALLBACK(_lib_tagging_tags_changed_callback), self);
dt_control_signal_unblock_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
}
}
// find a tag on the tree
static gboolean _find_tag_iter_tagid(GtkTreeModel *model,
GtkTreeIter *iter,
const gint tagid)
{
gint tag;
do
{
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_ID, &tag, -1);
if(tag == tagid)
{
return TRUE;
}
GtkTreeIter child, parent = *iter;
if(gtk_tree_model_iter_children(model, &child, &parent))
if(_find_tag_iter_tagid(model, &child, tagid))
{
*iter = child;
return TRUE;
}
} while(gtk_tree_model_iter_next(model, iter));
return FALSE;
}
// calculate the indeterminated state (1) where needed on the tree
static void _calculate_sel_on_path(GtkTreeModel *model,
GtkTreeIter *iter,
const gboolean root)
{
GtkTreeIter child, parent = *iter;
do
{
gint sel = DT_TS_NO_IMAGE;
gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
if(sel == DT_TS_ALL_IMAGES)
{
_propagate_sel_to_parents(model, &parent);
}
if(gtk_tree_model_iter_children(model, &child, &parent))
_calculate_sel_on_path(model, &child, FALSE);
} while(!root && gtk_tree_model_iter_next(model, &parent));
}
// reset the indeterminated selection (1) on the tree
static void _reset_sel_on_path(GtkTreeModel *model,
GtkTreeIter *iter,
const gboolean root)
{
GtkTreeIter child, parent = *iter;
do
{
if(gtk_tree_model_iter_children(model, &child, &parent))
{
gint sel = DT_TS_NO_IMAGE;
gtk_tree_model_get(model, &parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
if(sel == DT_TS_SOME_IMAGES)
{
gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE, -1);
}
_reset_sel_on_path(model, &child, FALSE);
}
} while(!root && gtk_tree_model_iter_next(model, &parent));
}
// reset all selection (1 & 2) on the tree
static void _reset_sel_on_path_full(GtkTreeModel *model,
GtkTreeIter *iter,
const gboolean root)
{
GtkTreeIter child, parent = *iter;
do
{
if(GTK_IS_TREE_STORE(model))
{
gtk_tree_store_set(GTK_TREE_STORE(model), &parent,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE, -1);
if(gtk_tree_model_iter_children(model, &child, &parent))
_reset_sel_on_path_full(model, &child, FALSE);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(model), &parent,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE, -1);
}
} while(!root && gtk_tree_model_iter_next(model, &parent));
}
// try to find a node fully attached (2) which is the root of the
// update loop. If not the full tree will be used
static void _find_root_iter_iter(GtkTreeModel *model,
GtkTreeIter *iter,
GtkTreeIter *parent)
{
guint sel;
GtkTreeIter child = *iter;
while(gtk_tree_model_iter_parent(model, parent, &child))
{
gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_SEL, &sel, -1);
if(sel == DT_TS_ALL_IMAGES)
{
char *path = NULL;
gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_PATH, &path, -1);
g_free(path);
return; // no need to go further
}
child = *parent;
}
*parent = child; // last before root
char *path = NULL;
gtk_tree_model_get(model, parent, DT_LIB_TAGGING_COL_PATH, &path, -1);
g_free(path);
}
// with tag detach update the tree selection
static void _calculate_sel_on_tree(GtkTreeModel *model,
GtkTreeIter *iter)
{
GtkTreeIter parent;
if(iter)
{
// only on sub-tree
_find_root_iter_iter(model, iter, &parent);
_reset_sel_on_path(model, &parent, TRUE);
_calculate_sel_on_path(model, &parent, TRUE);
}
else
{
// on full tree
if(gtk_tree_model_get_iter_first(model, &parent))
{
_reset_sel_on_path(model, &parent, FALSE);
_calculate_sel_on_path(model, &parent, FALSE);
}
}
}
// get the new selected images and update the tree selection
static void _update_sel_on_tree(GtkTreeModel *model)
{
GList *tags = NULL;
dt_tag_get_attached(-1, &tags, TRUE);
GtkTreeIter parent;
if(gtk_tree_model_get_iter_first(model, &parent))
{
_reset_sel_on_path_full(model, &parent, FALSE);
for(GList *tag = tags; tag; tag = g_list_next(tag))
{
GtkTreeIter iter = parent;
if(_find_tag_iter_tagid(model, &iter, ((dt_tag_t *)tag->data)->id))
{
if(GTK_IS_TREE_STORE(model))
{
gtk_tree_store_set(GTK_TREE_STORE(model), &iter,
DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)tag->data)->select, -1);
_propagate_sel_to_parents(model, &iter);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(model), &iter,
DT_LIB_TAGGING_COL_SEL, ((dt_tag_t *)tag->data)->select, -1);
}
}
}
}
if(tags)
dt_tag_free_result(&tags);
}
// delete a tag in the tree (tree or list)
static void _delete_tree_tag(GtkTreeModel *model,
GtkTreeIter *iter,
const gboolean tree)
{
guint tagid = 0;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(tree)
{
if(tagid)
{
gtk_tree_store_set(GTK_TREE_STORE(model), iter,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE,
DT_LIB_TAGGING_COL_ID, 0,
DT_LIB_TAGGING_COL_COUNT, 0, -1);
_calculate_sel_on_tree(model, iter);
GtkTreeIter child, parent = *iter;
if(!gtk_tree_model_iter_children(model, &child, &parent))
gtk_tree_store_remove(GTK_TREE_STORE(model), iter);
}
}
else
{
gtk_list_store_remove(GTK_LIST_STORE(model), iter);
}
}
// delete a branch of the tag tree
static void _delete_tree_path(GtkTreeModel *model,
GtkTreeIter *iter,
const gboolean root,
const gboolean tree)
{
if(tree) // the treeview is tree. It handles the hierarchy itself (parent / child)
{
GtkTreeIter child, parent = *iter;
gboolean valid = TRUE;
do
{
if(gtk_tree_model_iter_children(model, &child, &parent))
_delete_tree_path(model, &child, FALSE, tree);
GtkTreeIter tobedel = parent;
valid = gtk_tree_model_iter_next(model, &parent);
if(root)
{
gtk_tree_store_set(GTK_TREE_STORE(model), &tobedel,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE,
DT_LIB_TAGGING_COL_COUNT, 0, -1);
char *path2 = NULL;
gtk_tree_model_get(model, &tobedel, DT_LIB_TAGGING_COL_PATH, &path2, -1);
g_free(path2);
_calculate_sel_on_tree(model, &tobedel);
}
char *path = NULL;
gtk_tree_model_get(model, &tobedel, DT_LIB_TAGGING_COL_PATH, &path, -1);
g_free(path);
gtk_tree_store_remove(GTK_TREE_STORE(model), &tobedel);
} while(!root && valid);
}
else // treeview is a list. The hierarchy of tags is found with the
// root (left part) of tagname
{
GtkTreeIter child;
char *path = NULL;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &path, -1);
guint pathlen = strlen(path);
gboolean valid = gtk_tree_model_get_iter_first(model, &child);
while(valid)
{
char *path2 = NULL;
gtk_tree_model_get(model, &child, DT_LIB_TAGGING_COL_PATH, &path2, -1);
GtkTreeIter tobedel = child;
valid = gtk_tree_model_iter_next (model, &child);
if(strlen(path2) >= pathlen)
{
char letter = path2[pathlen];
path2[pathlen] = '\0';
if(g_strcmp0(path, path2) == 0)
{
path2[pathlen] = letter;
gtk_list_store_remove(GTK_LIST_STORE(model), &tobedel);
}
}
g_free(path2);
}
g_free(path);
}
}
static void _lib_selection_changed_callback(gpointer instance,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
if(!d->tree_flag && d->suggestion_flag)
{
_init_treeview(self, 1);
}
else
_update_sel_on_tree(d->tree_flag ? GTK_TREE_MODEL(d->dictionary_treestore)
: GTK_TREE_MODEL(d->dictionary_liststore));
d->update_selected_tags = TRUE;
dt_lib_gui_queue_update(self);
}
static void _set_keyword(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
const gchar *beg = g_strrstr(gtk_entry_get_text(d->entry), ",");
if(!beg)
beg = gtk_entry_get_text(d->entry);
else
{
if(*beg == ',') beg++;
if(*beg == ' ') beg++;
}
g_strlcpy(d->keyword, beg, sizeof(d->keyword));
}
static gboolean _update_tag_name_per_name(GtkTreeModel *model,
GtkTreePath *path,
GtkTreeIter *iter,
dt_tag_op_t *to)
{
char *tagname;
char *newtagname = to->newtagname;
char *oldtagname = to->oldtagname;
gtk_tree_model_get(model, iter, DT_LIB_TAGGING_COL_PATH, &tagname, -1);
if(g_str_has_prefix(tagname, oldtagname))
{
if(strlen(tagname) == strlen(oldtagname))
{
// rename the tag itself
if(to->tree_flag)
{
char *subtag = g_strrstr(to->newtagname, "|");
subtag = (!subtag) ? newtagname : subtag + 1;
gtk_tree_store_set(GTK_TREE_STORE(model), iter,
DT_LIB_TAGGING_COL_PATH, newtagname,
DT_LIB_TAGGING_COL_TAG, subtag, -1);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(model), iter,
DT_LIB_TAGGING_COL_PATH, newtagname,
DT_LIB_TAGGING_COL_TAG, newtagname, -1);
}
}
else if(strlen(tagname) > strlen(oldtagname)
&& tagname[strlen(oldtagname)] == '|')
{
// rename similar path
char *newpath = g_strconcat(newtagname, &tagname[strlen(oldtagname)] , NULL);
if(to->tree_flag)
{
gtk_tree_store_set(GTK_TREE_STORE(model), iter,
DT_LIB_TAGGING_COL_PATH, newpath, -1);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(model), iter,
DT_LIB_TAGGING_COL_PATH, newpath,
DT_LIB_TAGGING_COL_TAG, newpath, -1);
}
g_free(newpath);
}
}
g_free(tagname);
return FALSE;
}
static void _update_attached_count(const int tagid, GtkTreeView *view,
const gboolean tree_flag)
{
const guint count = dt_tag_images_count(tagid);
GtkTreeModel *model = gtk_tree_view_get_model(view);
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
GtkTreeIter iter;
if(gtk_tree_model_get_iter_first(store, &iter))
{
if(_find_tag_iter_tagid(store, &iter, tagid))
{
if(tree_flag)
{
gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_ALL_IMAGES, -1);
_calculate_sel_on_tree(GTK_TREE_MODEL(store), &iter);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(store), &iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_ALL_IMAGES, -1);
}
}
}
}
void init_presets(dt_lib_module_t *self)
{
}
void *get_params(dt_lib_module_t *self, int *size)
{
char *params = NULL;
*size = 0;
GList *tags = NULL;
const guint count = dt_tag_get_attached(-1, &tags, TRUE);
if(count)
{
for(GList *taglist = tags; taglist; taglist = g_list_next(taglist))
{
dt_util_str_cat(¶ms, "%d,", ((dt_tag_t *)taglist->data)->id);
}
dt_tag_free_result(&tags);
if(params == NULL)
return NULL;
*size = strlen(params);
params[*size-1]='\0';
}
return params;
}
int set_params(dt_lib_module_t *self,
const void *params,
const int size)
{
if(!params || !size) return 1;
dt_lib_tagging_t *d = self->data;
const char *buf = (char *)params;
if(buf && buf[0])
{
gchar **tokens = g_strsplit(buf, ",", 0);
if(tokens)
{
GList *tags = NULL;
gchar **entry = tokens;
while(*entry)
{
const guint tagid = strtoul(*entry, NULL, 0);
tags = g_list_prepend(tags, GINT_TO_POINTER(tagid));
entry++;
}
g_strfreev(tokens);
GList *imgs = dt_act_on_get_images(FALSE, FALSE, FALSE);
dt_tag_set_tags(tags, imgs, TRUE, FALSE, TRUE);
g_list_free(imgs);
gboolean change = FALSE;
for(GList *tag = tags; tag; tag = g_list_next(tag))
{
_update_attached_count(GPOINTER_TO_INT(tag->data),
d->dictionary_view,
d->tree_flag);
change = TRUE;
}
if(change)
{
_init_treeview(self, 0);
_raise_signal_tag_changed(self);
dt_image_synch_xmp(-1);
}
g_list_free(tags);
}
}
return 0;
}
static void _attach_selected_tag(dt_lib_module_t *self,
dt_lib_tagging_t *d)
{
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeSelection *selection = gtk_tree_view_get_selection(d->dictionary_view);
if(!gtk_tree_selection_get_selected(selection, &model, &iter)
&& !gtk_tree_model_get_iter_first(model, &iter))
return;
guint tagid;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(tagid <= 0) return;
// attach tag on images to act on
dt_gui_cursor_set_busy();
if(dt_tag_attach(tagid, -1, TRUE, TRUE))
{
/** record last tag used */
_save_last_tag_used(dt_tag_get_name(tagid), d);
_init_treeview(self, 0);
if(d->tree_flag || !d->suggestion_flag)
{
const uint32_t count = dt_tag_images_count(tagid);
GtkTreeIter store_iter;
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
&store_iter, &iter);
if(d->tree_flag)
{
gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_ALL_IMAGES, -1);
_propagate_sel_to_parents(GTK_TREE_MODEL(store), &store_iter);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_ALL_IMAGES, -1);
}
}
else
{
_init_treeview(self, 1);
}
_raise_signal_tag_changed(self);
dt_image_synch_xmp(-1);
}
dt_gui_cursor_clear_busy();
}
static void _detach_selected_tag(GtkTreeView *view,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeSelection *selection = gtk_tree_view_get_selection(view);
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
guint tagid;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(tagid <= 0) return;
GList *imgs = dt_act_on_get_images(FALSE, TRUE, FALSE);
if(!imgs) return;
GList *affected_images = dt_tag_get_images_from_list(imgs, tagid);
if(affected_images)
{
dt_gui_cursor_set_busy();
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
gint *indices = gtk_tree_path_get_indices(path);
const int index = indices[0];
gtk_tree_path_free(path);
const gboolean res = dt_tag_detach_images(tagid, affected_images, TRUE);
_init_treeview(self, 0);
if(!_select_next_user_attached_tag(index, view))
gtk_widget_grab_focus(GTK_WIDGET(d->entry));
if(d->tree_flag || !d->suggestion_flag)
{
const guint count = dt_tag_images_count(tagid);
model = gtk_tree_view_get_model(d->dictionary_view);
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
if(gtk_tree_model_get_iter_first(store, &iter))
{
if(_find_tag_iter_tagid(store, &iter, tagid))
{
if(d->tree_flag)
{
gtk_tree_store_set(GTK_TREE_STORE(store), &iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE, -1);
_calculate_sel_on_tree(GTK_TREE_MODEL(store), &iter);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(store), &iter,
DT_LIB_TAGGING_COL_COUNT, count,
DT_LIB_TAGGING_COL_SEL, DT_TS_NO_IMAGE, -1);
}
}
}
}
else
{
_init_treeview(self, 1);
}
if(res)
{
_raise_signal_tag_changed(self);
dt_image_synch_xmps(affected_images);
}
g_list_free(affected_images);
dt_gui_cursor_clear_busy();
}
g_list_free(imgs);
}
static void _attach_button_clicked(GtkButton *button,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
_attach_selected_tag(self, d);
}
static void _detach_button_clicked(GtkButton *button,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
_detach_selected_tag(d->attached_view, self);
}
static void _pop_menu_attached_attach_to_all(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
GtkTreeSelection *selection =
gtk_tree_view_get_selection(GTK_TREE_VIEW(d->attached_view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter))
return;
guint tagid;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(tagid <= 0) return;
// attach tag on images to act on
const gboolean res = dt_tag_attach(tagid, -1, TRUE, TRUE);
/** record last tag used */
_save_last_tag_used(dt_tag_get_name(tagid), d);
_init_treeview(self, 0);
const uint32_t count = dt_tag_images_count(tagid);
model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
if(gtk_tree_model_get_iter_first(model, &iter))
{
if(_find_tag_iter_tagid(model, &iter, tagid))
{
GtkTreeIter store_iter;
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
&store_iter, &iter);
if(d->tree_flag)
{
gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_COUNT, count, -1);
}
else
{
gtk_list_store_set(GTK_LIST_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_COUNT, count, -1);
}
}
}
if(res)
{
_raise_signal_tag_changed(self);
dt_image_synch_xmp(-1);
}
}
static void _pop_menu_attached_detach(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
_detach_selected_tag(d->attached_view, self);
}
static void _pop_menu_attached_find(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
gchar *name;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeSelection *selection = gtk_tree_view_get_selection(d->attached_view);
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_TAG, &name, -1);
gtk_entry_set_text(d->entry, name);
g_free(name);
}
static void _pop_menu_attached_clipboard(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
gchar *path;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeSelection *selection = gtk_tree_view_get_selection(d->attached_view);
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_PATH, &path, -1);
gtk_clipboard_set_text(gtk_clipboard_get_default(gdk_display_get_default()), path, -1);
g_free(path);
}
static void _pop_menu_attached(GtkWidget *treeview,
GdkEventButton *event,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GActionGroup *action_group = gtk_widget_get_action_group(treeview, "tags_attached");
if(action_group == NULL)
{
GActionEntry action_entries[] =
{
{ "attach_all", _pop_menu_attached_attach_to_all, NULL, NULL },
{ "detach", _pop_menu_attached_detach, NULL, NULL },
{ "find", _pop_menu_attached_find, NULL, NULL },
{ "clipboard", _pop_menu_attached_clipboard, NULL, 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(treeview,
"tags_attached",
G_ACTION_GROUP(action_group));
g_object_unref(action_group);
}
GMenu *menu = g_menu_new();
GMenu *section = menu;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
GtkTreeSelection *selection =
gtk_tree_view_get_selection(GTK_TREE_VIEW(d->attached_view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
guint sel;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_SEL, &sel, -1);
if(sel == DT_TS_SOME_IMAGES)
{
g_menu_append(section, _("attach tag to all"), "tags_attached.attach_all");
section = g_menu_new();
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
}
}
g_menu_append(section, _("detach tag"), "tags_attached.detach");
g_menu_append(section, _("find tag"), "tags_attached.find");
g_menu_append(section, _("copy to clipboard"), "tags_attached.clipboard");
GdkRectangle rect;
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
GtkTreeViewColumn *column = gtk_tree_view_get_column(GTK_TREE_VIEW(treeview), DT_LIB_TAGGING_COL_ID);
gtk_tree_view_get_background_area(GTK_TREE_VIEW(treeview), path, column, &rect);
// popup the menu
GtkWidget *popover_menu = dt_gui_popover_menu_from_model(treeview, menu);
g_object_unref(menu);
gtk_popover_set_pointing_to(GTK_POPOVER(popover_menu), &rect);
gtk_popover_popup(GTK_POPOVER(popover_menu));
}
static void _click_on_view_attached(GtkGestureSingle *gesture,
int n_press,
double x,
double y,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkWidget *view = dt_gui_get_widget(gesture);
_unselect_all_in_view(d->dictionary_view);
const guint button = gtk_gesture_single_get_current_button(gesture);
if((n_press == 1 && button == GDK_BUTTON_SECONDARY)
|| (n_press == 2 && button == GDK_BUTTON_PRIMARY)
|| (n_press == 1 && button == GDK_BUTTON_PRIMARY))
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
GtkTreePath *path = NULL;
// Get tree path for row that was clicked
if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
(gint)x, (gint)y,
&path, NULL, NULL, NULL))
{
gboolean valid_tag = FALSE;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
GtkTreeIter iter;
if(gtk_tree_model_get_iter(model, &iter, path))
// check this is a darktable tag
valid_tag = _is_user_tag(model, &iter);
if(valid_tag)
{
gtk_tree_selection_select_path(selection, path);
dt_lib_gui_queue_update(self);
if(n_press == 1 && button == GDK_BUTTON_SECONDARY)
{
dt_gui_claim(gesture);
const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL);
_pop_menu_attached(view, (GdkEventButton *)event, self);
gtk_tree_path_free(path);
return;
}
else if(n_press == 2 && button == GDK_BUTTON_PRIMARY)
{
dt_gui_claim(gesture);
_detach_selected_tag(d->attached_view, self);
gtk_tree_path_free(path);
return;
}
}
else
{ // prevent to select a darktable tag
dt_gui_claim(gesture);
gtk_tree_path_free(path);
return;
}
}
gtk_tree_path_free(path);
}
}
static gboolean _attached_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
_unselect_all_in_view(d->dictionary_view);
GtkWidget *view = dt_gui_get_widget(controller);
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->attached_view));
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
switch(keyval)
{
case GDK_KEY_Delete:
case GDK_KEY_KP_Delete:
_detach_selected_tag(GTK_TREE_VIEW(view), self);
gtk_tree_path_free(path);
return TRUE;
default:
break;
}
gtk_tree_path_free(path);
}
if(keyval == GDK_KEY_Tab)
{
gtk_tree_selection_unselect_all(selection);
gtk_widget_grab_focus(GTK_WIDGET(d->entry));
return TRUE;
}
else if(keyval == GDK_KEY_ISO_Left_Tab)
{
gtk_tree_selection_unselect_all(selection);
return TRUE;
}
return FALSE;
}
static void _new_button_clicked(GtkButton *button, dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
const gchar *tag = gtk_entry_get_text(d->entry);
if(!tag || tag[0] == '\0') return;
GList *imgs = dt_act_on_get_images(FALSE, TRUE, FALSE);
// workaround: if hovered image instead of selected, aborts
if(imgs && !imgs->next)
{
GList *sels = dt_selection_get_list(darktable.selection, FALSE, FALSE);
if(sels
&& (sels->next
|| (!sels->next
&& GPOINTER_TO_INT(sels->data) != GPOINTER_TO_INT(imgs->data))))
{
g_list_free(sels);
g_list_free(imgs);
return;
}
g_list_free(sels);
}
const gboolean res = dt_tag_attach_string_list(tag, imgs, TRUE);
if(res) dt_image_synch_xmps(imgs);
g_list_free(imgs);
/** record last tag used */
_save_last_tag_used(tag, d);
/** clear input box */
gtk_entry_set_text(d->entry, "");
_init_treeview(self, 0);
_init_treeview(self, 1);
char *tagname = strrchr(d->last_tag, ',');
if(res) _raise_signal_tag_changed(self);
_show_tag_on_view(GTK_TREE_VIEW(d->dictionary_view),
tagname ? tagname + 1 : d->last_tag, FALSE, TRUE);
}
static gboolean _enter_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkWidget *entry = dt_gui_get_widget(controller);
switch(keyval)
{
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
_new_button_clicked(NULL, self);
break;
case GDK_KEY_Escape:
gtk_window_set_focus(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)), NULL);
break;
case GDK_KEY_Down:
case GDK_KEY_KP_Down:
case GDK_KEY_Tab:
{
_unselect_all_in_view(d->attached_view);
if(d->keyword[0])
_show_keyword_on_view(d->dictionary_view, d->keyword, TRUE);
gtk_widget_grab_focus(GTK_WIDGET(d->dictionary_view));
return TRUE;
}
case GDK_KEY_ISO_Left_Tab:
{
_unselect_all_in_view(d->dictionary_view);
if(_select_next_user_attached_tag(0, d->attached_view))
{
gtk_entry_set_text(GTK_ENTRY(entry), "");
gtk_widget_grab_focus(GTK_WIDGET(d->attached_view));
}
return TRUE;
break;
}
case GDK_KEY_Up:
case GDK_KEY_KP_Up:
return TRUE; // keep focus
break;
default:
break;
}
return FALSE;
}
static void _clear_entry_button_callback(GtkButton *button,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
/** clear input box */
gtk_entry_set_text(d->entry, "");
}
static void _tag_name_changed(GtkEntry *entry,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
_set_keyword(self);
GtkTreeModel *model = gtk_tree_view_get_model(d->dictionary_view);
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_foreach(store,
(GtkTreeModelForeachFunc)_set_matching_tag_visibility, self);
if(d->tree_flag && d->keyword[0])
{
gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_tree_reveal_func, NULL);
_show_keyword_on_view(d->dictionary_view, d->keyword, FALSE);
}
}
static void _pop_menu_dictionary_delete_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
int res = GTK_RESPONSE_YES;
char *tagname;
gint tagid;
gchar *text;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(!tagid) return;
const guint img_count = dt_tag_remove(tagid, FALSE);
if(img_count > 0 || dt_conf_get_bool("plugins/lighttable/tagging/ask_before_delete_tag"))
{
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("delete tag?"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_delete"), GTK_RESPONSE_YES, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_NONE);
text = g_strdup_printf(_("selected: %s"), tagname);
GtkWidget *label1 = gtk_label_new(text);
g_free(text);
text = g_markup_printf_escaped
(ngettext("do you really want to delete the tag `%s'?\n"
"%d image is assigned this tag!",
"do you really want to delete the tag `%s'?\n"
"%d images are assigned this tag!", img_count), tagname, img_count);
GtkWidget *label2 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label2), text);
g_free(text);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
dt_gui_dialog_add(GTK_DIALOG(dialog), label1, label2);
gtk_widget_show_all(dialog);
res = gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
}
if(res != GTK_RESPONSE_YES)
{
g_free(tagname);
return;
}
GList *tagged_images = dt_tag_get_images(tagid);
dt_tag_remove(tagid, TRUE);
dt_control_log(_("tag %s removed"), tagname);
GtkTreeIter store_iter;
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
&store_iter, &iter);
_delete_tree_tag(GTK_TREE_MODEL(store), &store_iter, d->tree_flag);
_init_treeview(self, 0);
dt_image_synch_xmps(tagged_images);
g_list_free(tagged_images);
g_free(tagname);
_raise_signal_tag_changed(self);
}
static void _pop_menu_dictionary_delete_node(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
int res = GTK_RESPONSE_YES;
char *tagname;
gint tagid;
gchar *text;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
DT_LIB_TAGGING_COL_ID, &tagid, -1);
gint tag_count = 0;
gint img_count = 0;
dt_tag_count_tags_images(tagname, &tag_count, &img_count);
if(tag_count == 0) return;
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("delete node?"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_delete"), GTK_RESPONSE_YES, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_NONE);
text = g_strdup_printf(_("selected: %s"), tagname);
GtkWidget *label1 = gtk_label_new(text);
g_free(text);
text = g_strdup_printf(ngettext("%d tag will be deleted",
"%d tags will be deleted", tag_count),
tag_count);
GtkWidget *label2 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label2), text);
g_free(text);
text = g_strdup_printf(ngettext("%d image will be updated",
"%d images will be updated", img_count),
img_count);
GtkWidget *label3 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label3), text);
g_free(text);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
dt_gui_dialog_add(GTK_DIALOG(dialog), label1,
dt_gui_hbox(label2, gtk_label_new(" "), label3));
gtk_widget_show_all(dialog);
res = gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
if(res != GTK_RESPONSE_YES)
{
g_free(tagname);
return;
}
GList *tag_family = NULL;
GList *tagged_images = NULL;
dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
dt_control_signal_block_by_func(darktable.signals,
G_CALLBACK(_lib_tagging_tags_changed_callback), self);
tag_count = dt_tag_remove_list(tag_family);
dt_control_signal_unblock_by_func(darktable.signals,
G_CALLBACK(_lib_tagging_tags_changed_callback), self);
dt_control_log(_("%d tags removed"), tag_count);
GtkTreeIter store_iter;
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
&store_iter, &iter);
_delete_tree_path(GTK_TREE_MODEL(store), &store_iter, TRUE, d->tree_flag);
_init_treeview(self, 0);
dt_tag_free_result(&tag_family);
dt_image_synch_xmps(tagged_images);
g_list_free(tagged_images);
_raise_signal_tag_changed(self);
g_free(tagname);
}
static void _name_changed(GtkEntry *entry,
GtkDialog *dialog)
{
const gchar *name = gtk_entry_get_text(entry);
gtk_dialog_set_response_sensitive(dialog, GTK_RESPONSE_YES, name && *name);
}
// create tag allows the user to create a single tag, which can be an
// element of the hierarchy or not
static void _pop_menu_dictionary_create_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
char *tagname;
char *path;
gint tagid;
gchar *text;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_TAG, &tagname,
DT_LIB_TAGGING_COL_PATH, &path, DT_LIB_TAGGING_COL_ID, &tagid, -1);
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("create tag"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_save"), GTK_RESPONSE_YES, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_YES);
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
gtk_dialog_set_response_sensitive(GTK_DIALOG(dialog), GTK_RESPONSE_YES, FALSE);
g_signal_connect(entry, "changed", G_CALLBACK(_name_changed), dialog);
GtkWidget *category;
GtkWidget *private;
GtkWidget *parent;
GtkTextBuffer *buffer = NULL;
text = g_strdup_printf(_("add to: \"%s\" "), path);
parent = gtk_check_button_new_with_label(text);
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(parent), TRUE);
g_free(text);
category = gtk_check_button_new_with_label(_("category"));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(category), FALSE);
private = gtk_check_button_new_with_label(_("private"));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(private), FALSE);
GtkWidget *synonyms = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(synonyms), GTK_WRAP_WORD);
buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(synonyms));
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
dt_gui_dialog_add(GTK_DIALOG(dialog),
dt_gui_hbox(gtk_label_new(_("name: ")),
dt_gui_expand(entry)),
private, category, parent,
dt_gui_hbox(gtk_label_new(_("synonyms: ")), dt_gui_expand(synonyms)));
gtk_widget_show_all(dialog);
if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
{
const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
char *message = NULL;
if(!newtag[0])
message = _("empty tag is not allowed, aborting");
char *new_tagname = NULL;
const gboolean root = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(parent));
if(!root)
{
new_tagname = g_strdup(path);
dt_util_str_cat(&new_tagname, "|%s", newtag);
}
else new_tagname = g_strdup(newtag);
if(dt_tag_exists(new_tagname, NULL))
message = _("tag name already exists. aborting.");
if(message)
{
GtkWidget *warning_dialog =
gtk_message_dialog_new(GTK_WINDOW(dialog),
GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run(GTK_DIALOG(warning_dialog));
gtk_widget_destroy(warning_dialog);
gtk_widget_destroy(dialog);
g_free(tagname);
return;
}
guint new_tagid = 0;
if(dt_tag_new(new_tagname, &new_tagid))
{
const gint new_flags = ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(category))
? DT_TF_CATEGORY
: 0)
| (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(private))
? DT_TF_PRIVATE
: 0));
if(new_tagid)
dt_tag_set_flags(new_tagid, new_flags);
GtkTextIter start, end;
gtk_text_buffer_get_start_iter(buffer, &start);
gtk_text_buffer_get_end_iter(buffer, &end);
gchar *new_synonyms_list = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
if(new_tagid && new_synonyms_list && new_synonyms_list[0])
dt_tag_set_synonyms(new_tagid, new_synonyms_list);
g_free(new_synonyms_list);
_init_treeview(self, 1);
_show_tag_on_view(view, new_tagname, FALSE, TRUE);
}
g_free(new_tagname);
}
_init_treeview(self, 0);
gtk_widget_destroy(dialog);
g_free(tagname);
}
// edit tag allows the user to rename a single tag, which can be an
// element of the hierarchy and change other parameters
static void _pop_menu_dictionary_edit_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
char *tagname;
char *synonyms_list;
gint tagid;
gchar *text;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
DT_LIB_TAGGING_COL_SYNONYM, &synonyms_list, DT_LIB_TAGGING_COL_ID, &tagid, -1);
char *subtag = g_strrstr(tagname, "|");
if(subtag) subtag = subtag + 1;
gint tag_count;
gint img_count;
dt_tag_count_tags_images(tagname, &tag_count, &img_count);
if(tag_count == 0)
{
g_free(tagname);
g_free(synonyms_list);
return;
}
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("edit"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_save"), GTK_RESPONSE_YES, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_YES);
text = g_strdup_printf(_("selected: %s"), tagname);
GtkWidget *label1 = gtk_label_new(text);
g_free(text);
text = g_strdup_printf(ngettext("%d tag will be updated",
"%d tags will be updated", tag_count),
tag_count);
GtkWidget *label2 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label2), text);
g_free(text);
text = g_strdup_printf(ngettext("%d image will be updated",
"%d images will be updated", img_count),
img_count);
GtkWidget *label3 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label3), text);
g_free(text);
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
g_signal_connect(entry, "changed", G_CALLBACK(_name_changed), dialog);
gtk_entry_set_text(GTK_ENTRY(entry), subtag ? subtag : tagname);
dt_gui_dialog_add(GTK_DIALOG(dialog),
label1,
dt_gui_hbox(label2, gtk_label_new(" "), label3),
dt_gui_hbox(gtk_label_new(_("name: ")), dt_gui_expand(entry)));
gint flags = 0;
GtkWidget *category = NULL;
GtkWidget *private = NULL;
GtkTextBuffer *buffer = NULL;
if(tagid)
{
flags = dt_tag_get_flags(tagid);
category = gtk_check_button_new_with_label(_("category"));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(category), flags & DT_TF_CATEGORY);
private = gtk_check_button_new_with_label(_("private"));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(private), flags & DT_TF_PRIVATE);
GtkWidget *synonyms = gtk_text_view_new();
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(synonyms), GTK_WRAP_WORD);
buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(synonyms));
if(synonyms_list) gtk_text_buffer_set_text(buffer, synonyms_list, -1);
dt_gui_dialog_add(GTK_DIALOG(dialog), private, category,
dt_gui_hbox(gtk_label_new(_("synonyms: ")),
dt_gui_expand(synonyms)));
}
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
gtk_widget_show_all(dialog);
if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
{
const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
if(g_strcmp0(newtag, subtag ? subtag : tagname) != 0)
{
// tag name has changed
char *message = NULL;
if(!newtag[0])
message = _("empty tag is not allowed, aborting");
if(strchr(newtag, '|') != 0)
message = _("'|' character is not allowed for renaming tag.\n"
"to modify the hierarchy use rename path instead. Aborting.");
if(message)
{
GtkWidget *warning_dialog = gtk_message_dialog_new(GTK_WINDOW(dialog),
GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
"%s", message);
gtk_dialog_run(GTK_DIALOG(warning_dialog));
gtk_widget_destroy(warning_dialog);
gtk_widget_destroy(dialog);
g_free(tagname);
return;
}
GList *tag_family = NULL;
GList *tagged_images = NULL;
dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
const int tagname_len = strlen(tagname);
char *new_prefix_tag;
if(subtag)
{
const int subtag_len = strlen(subtag);
const char letter = tagname[tagname_len - subtag_len];
tagname[tagname_len - subtag_len] = '\0';
new_prefix_tag = g_strconcat(tagname, newtag, NULL);
tagname[tagname_len - subtag_len] = letter;
}
else
new_prefix_tag = (char *)newtag;
// check if one of the new tagnames already exists.
gboolean tagname_exists = FALSE;
for(GList *taglist = tag_family;
taglist && !tagname_exists;
taglist = g_list_next(taglist))
{
char *new_tagname = g_strconcat(new_prefix_tag,
&((dt_tag_t *)taglist->data)->tag[tagname_len],
NULL);
tagname_exists = dt_tag_exists(new_tagname, NULL);
if(tagname_exists)
{
GtkWidget *warning_dialog = gtk_message_dialog_new
(GTK_WINDOW(dialog),
GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
_("at least one new tag name (%s) already exists, aborting"),
new_tagname);
gtk_dialog_run(GTK_DIALOG(warning_dialog));
gtk_widget_destroy(warning_dialog);
g_free(new_tagname);
if(subtag) g_free(new_prefix_tag);
gtk_widget_destroy(dialog);
g_free(tagname);
return;
};
g_free(new_tagname);
}
// rename related tags
for(GList *taglist = tag_family; taglist; taglist = g_list_next(taglist))
{
char *new_tagname = g_strconcat(new_prefix_tag,
&((dt_tag_t *)taglist->data)->tag[tagname_len],
NULL);
dt_tag_rename(((dt_tag_t *)taglist->data)->id, new_tagname);
g_free(new_tagname);
}
// update the store
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
dt_tag_op_t *to = g_try_malloc(sizeof(dt_tag_op_t));
if(to)
{
to->tree_flag = d->tree_flag;
to->oldtagname = tagname;
to->newtagname = new_prefix_tag;
gint sort_column;
GtkSortType sort_order;
gtk_tree_sortable_get_sort_column_id(GTK_TREE_SORTABLE(store),
&sort_column, &sort_order);
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store),
GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID,
GTK_SORT_ASCENDING);
gtk_tree_model_foreach(store,
(GtkTreeModelForeachFunc)_update_tag_name_per_name, to);
gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store),
sort_column, sort_order);
g_free(to);
}
if(subtag) g_free(new_prefix_tag);
_raise_signal_tag_changed(self);
dt_tag_free_result(&tag_family);
dt_image_synch_xmps(tagged_images);
g_list_free(tagged_images);
}
if(tagid)
{
gint new_flags = ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(category))
? DT_TF_CATEGORY
: 0)
| (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(private))
? DT_TF_PRIVATE
: 0));
GtkTextIter start, end;
gtk_text_buffer_get_start_iter(buffer, &start);
gtk_text_buffer_get_end_iter(buffer, &end);
gchar *new_synonyms_list = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
// refresh iter
gtk_tree_selection_get_selected(selection, &model, &iter);
GtkTreeIter store_iter;
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_model_filter_convert_iter_to_child_iter(GTK_TREE_MODEL_FILTER(model),
&store_iter, &iter);
if(new_flags != (flags & (DT_TF_CATEGORY | DT_TF_PRIVATE)))
{
new_flags = (flags & ~(DT_TF_CATEGORY | DT_TF_PRIVATE)) | new_flags;
dt_tag_set_flags(tagid, new_flags);
if(!d->tree_flag)
gtk_list_store_set(GTK_LIST_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_FLAGS, new_flags, -1);
else
gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_FLAGS, new_flags, -1);
}
if(new_synonyms_list && g_strcmp0(synonyms_list, new_synonyms_list) != 0)
{
dt_tag_set_synonyms(tagid, new_synonyms_list);
if(!d->tree_flag)
gtk_list_store_set(GTK_LIST_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_SYNONYM, new_synonyms_list, -1);
else
gtk_tree_store_set(GTK_TREE_STORE(store), &store_iter,
DT_LIB_TAGGING_COL_SYNONYM, new_synonyms_list, -1);
}
g_free(new_synonyms_list);
}
}
_init_treeview(self, 0);
gtk_widget_destroy(dialog);
g_free(synonyms_list);
g_free(tagname);
}
static gboolean _apply_rename_path(GtkWidget *dialog,
const char *tagname,
const char *newtag,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
gboolean success = FALSE;
GList *tag_family = NULL;
GList *tagged_images = NULL;
dt_tag_get_tags_images(tagname, &tag_family, &tagged_images);
// check if one of the new tagnames already exists.
const int tagname_len = strlen(tagname);
gboolean tagname_exists = FALSE;
for(GList *taglist = tag_family;
taglist && !tagname_exists;
taglist = g_list_next(taglist))
{
char *new_tagname = g_strconcat(newtag,
&((dt_tag_t *)taglist->data)->tag[tagname_len],
NULL);
tagname_exists = dt_tag_exists(new_tagname, NULL);
if(tagname_exists)
{
GtkWidget *win;
if(!dialog)
win = dt_ui_main_window(darktable.gui->ui);
else
win = dialog;
GtkWidget *warning_dialog = gtk_message_dialog_new
(GTK_WINDOW(win), GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE,
_("at least one new tag name (%s) already exists, aborting"),
new_tagname);
gtk_dialog_run(GTK_DIALOG(warning_dialog));
gtk_widget_destroy(warning_dialog);
}
g_free(new_tagname);
}
if(!tagname_exists)
{
for(GList *taglist = tag_family; taglist; taglist = g_list_next(taglist))
{
char *new_tagname = g_strconcat(newtag,
&((dt_tag_t *)taglist->data)->tag[tagname_len],
NULL);
dt_tag_rename(((dt_tag_t *)taglist->data)->id, new_tagname);
g_free(new_tagname);
}
_init_treeview(self, 0);
_init_treeview(self, 1);
dt_image_synch_xmps(tagged_images);
_raise_signal_tag_changed(self);
_show_tag_on_view(d->dictionary_view, newtag, FALSE, TRUE);
success = TRUE;
}
dt_tag_free_result(&tag_family);
g_list_free(tagged_images);
return success;
}
// rename path allows the user to redefine a hierarchy
static void _pop_menu_dictionary_change_path(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
char *tagname;
gint tagid;
gchar *text;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname,
DT_LIB_TAGGING_COL_ID, &tagid, -1);
gint tag_count = 0;
gint img_count = 0;
dt_tag_count_tags_images(tagname, &tag_count, &img_count);
if(tag_count == 0) return;
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("change path"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_save"), GTK_RESPONSE_YES, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_YES);
text = g_strdup_printf(_("selected: %s"), tagname);
GtkWidget *label1 = gtk_label_new(text);
g_free(text);
text = g_strdup_printf(ngettext("%d tag will be updated",
"%d tags will be updated", tag_count),
tag_count);
GtkWidget *label2 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label2), text);
g_free(text);
text = g_strdup_printf(ngettext("%d image will be updated",
"%d images will be updated", img_count),
img_count);
GtkWidget *label3 = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(label3), text);
g_free(text);
GtkWidget *entry = gtk_entry_new();
gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
gtk_entry_set_text(GTK_ENTRY(entry), tagname);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
dt_gui_dialog_add(GTK_DIALOG(dialog), label1,
dt_gui_hbox(label2, gtk_label_new(" "), label3),
dt_gui_expand(entry));
gtk_widget_show_all(dialog);
if(gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES)
{
const char *newtag = gtk_entry_get_text(GTK_ENTRY(entry));
if(g_strcmp0(newtag, tagname) == 0)
return; // no change
char *message = NULL;
if(!newtag[0])
message = _("empty tag is not allowed, aborting");
if(strchr(newtag, '|') == &newtag[0]
|| strchr(newtag, '|') == &newtag[strlen(newtag)-1]
|| strstr(newtag, "||"))
message = _("'|' misplaced, empty tag is not allowed, aborting");
if(message)
{
GtkWidget *warning_dialog = gtk_message_dialog_new
(GTK_WINDOW(dialog),
GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", message);
gtk_dialog_run(GTK_DIALOG(warning_dialog));
gtk_widget_destroy(warning_dialog);
gtk_widget_destroy(dialog);
g_free(tagname);
return;
}
_apply_rename_path(dialog, tagname, newtag, self);
}
gtk_widget_destroy(dialog);
g_free(tagname);
}
static void _pop_menu_dictionary_goto_tag_collection(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
GtkTreeSelection *selection = gtk_tree_view_get_selection
(GTK_TREE_VIEW(d->dictionary_view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
char *path;
guint count;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_PATH, &path,
DT_LIB_TAGGING_COL_COUNT, &count, -1);
if(count)
{
if(!d->collection[0]) dt_collection_serialize(d->collection, 4096, FALSE);
gchar *tag_collection = g_strdup_printf("1:0:%d:%s$",
DT_COLLECTION_PROP_TAG, path);
dt_control_signal_block_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
dt_collection_deserialize(tag_collection, FALSE);
dt_control_signal_unblock_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
g_free(tag_collection);
}
g_free(path);
}
}
static void _pop_menu_dictionary_goto_collection_back(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
if(d->collection[0])
{
dt_control_signal_block_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
dt_collection_deserialize(d->collection, FALSE);
dt_control_signal_unblock_by_func(darktable.signals,
G_CALLBACK(_collection_updated_callback), self);
d->collection[0] = '\0';
}
}
static void _pop_menu_dictionary_copy_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
GtkTreeSelection *selection =
gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
char *tag;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tag, -1);
gtk_entry_set_text(d->entry, tag);
g_free(tag);
gtk_entry_grab_focus_without_selecting(d->entry);
}
}
static void _pop_menu_dictionary_clipboard(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
GtkTreeSelection *selection =
gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
char *tag;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tag, -1);
gtk_clipboard_set_text(gtk_clipboard_get_default(gdk_display_get_default()), tag, -1);
g_free(tag);
}
}
static void _pop_menu_dictionary_attach_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
_attach_selected_tag(self, d);
}
static void _pop_menu_dictionary_detach_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
_detach_selected_tag(d->dictionary_view, self);
}
static void _pop_menu_dictionary_set_as_tag(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
dt_lib_tagging_t *d = self->data;
char *tagname;
guint new_tagid;
GtkTreeIter iter;
GtkTreeModel *model = NULL;
GtkTreeView *view = d->dictionary_view;
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &tagname, -1);
dt_tag_new(tagname, &new_tagid);
dt_control_log(_("tag %s created"), tagname);
_init_treeview(self, 1);
_show_tag_on_view(d->dictionary_view, tagname, FALSE, TRUE);
g_free(tagname);
}
static void _pop_menu_dictionary(GtkWidget *treeview,
GdkEventButton *event,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GActionGroup *action_group = gtk_widget_get_action_group(treeview, "tags_dictionary");
if(action_group == NULL)
{
GActionEntry action_entries[] =
{
{ "attach", _pop_menu_dictionary_attach_tag, NULL, NULL },
{ "detach", _pop_menu_dictionary_detach_tag, NULL, NULL },
{ "create", _pop_menu_dictionary_create_tag, NULL, NULL },
{ "delete", _pop_menu_dictionary_delete_tag, NULL, NULL },
{ "delete_node", _pop_menu_dictionary_delete_node, NULL, NULL },
{ "edit", _pop_menu_dictionary_edit_tag, NULL, NULL },
{ "path", _pop_menu_dictionary_change_path, NULL, NULL },
{ "setastag", _pop_menu_dictionary_set_as_tag, NULL, NULL },
{ "copytoentry", _pop_menu_dictionary_copy_tag, NULL, NULL },
{ "clipboard", _pop_menu_dictionary_clipboard, NULL, NULL },
{ "goto", _pop_menu_dictionary_goto_tag_collection, NULL, NULL },
{ "back", _pop_menu_dictionary_goto_collection_back, NULL, 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(treeview,
"tags_dictionary",
G_ACTION_GROUP(action_group));
g_object_unref(action_group);
}
GtkTreeIter iter, child;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
GtkTreeSelection *selection =
gtk_tree_view_get_selection(GTK_TREE_VIEW(d->dictionary_view));
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
guint tagid, count;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_ID, &tagid,
DT_LIB_TAGGING_COL_COUNT, &count, -1);
GMenu *menu = g_menu_new();
GMenu *section = menu;
if(tagid)
{
g_menu_append(section, _("attach tag"), "tags_dictionary.attach");
g_menu_append(section, _("detach tag"), "tags_dictionary.detach");
section = g_menu_new();
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
}
if(d->tree_flag || !d->suggestion_flag)
{
g_menu_append(section, _("create tag..."), "tags_dictionary.create");
if(tagid)
g_menu_append(section, _("delete tag"), "tags_dictionary.delete");
if(gtk_tree_model_iter_children(model, &child, &iter))
g_menu_append(section, _("delete node"), "tags_dictionary.delete_node");
g_menu_append(section, _("edit"), "tags_dictionary.edit");
}
if(d->tree_flag)
g_menu_append(section, _("change path..."), "tags_dictionary.path");
if(d->tree_flag && !tagid)
{
section = g_menu_new();
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
g_menu_append(section, _("set as tag"), "tags_dictionary.setastag");
}
if(!d->suggestion_flag)
{
section = g_menu_new();
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
}
g_menu_append(section, _("copy to entry"), "tags_dictionary.copytoentry");
g_menu_append(section, _("copy to clipboard"), "tags_dictionary.clipboard");
if(d->collection[0])
{
char *collection = g_try_malloc(4096);
if(collection)
{
dt_collection_serialize(collection, 4096, FALSE);
if(g_strcmp0(d->collection, collection) == 0) d->collection[0] = '\0';
g_free(collection);
}
}
if(count || d->collection[0])
{
section = g_menu_new();
g_menu_append_section(menu, NULL, G_MENU_MODEL(section));
if(count)
g_menu_append(section, _("go to tag collection"), "tags_dictionary.goto");
if(d->collection[0])
g_menu_append(section, _("go back to work"), "tags_dictionary.back");
}
GdkRectangle rect;
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
GtkTreeViewColumn *column = gtk_tree_view_get_column(GTK_TREE_VIEW(treeview), DT_LIB_TAGGING_COL_ID);
gtk_tree_view_get_background_area(GTK_TREE_VIEW(treeview), path, column, &rect);
// popup the menu
GtkWidget *popover_menu = dt_gui_popover_menu_from_model(treeview, menu);
g_object_unref(menu);
gtk_popover_set_pointing_to(GTK_POPOVER(popover_menu), &rect);
gtk_popover_popup(GTK_POPOVER(popover_menu));
}
}
static void _click_on_view_dictionary(GtkGestureSingle *gesture,
int n_press,
double x,
double y,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkWidget *view = dt_gui_get_widget(gesture);
_unselect_all_in_view(d->attached_view);
const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL);
const GdkModifierType state = event ? dt_gdk_event_get_state(event) : 0;
const guint button = gtk_gesture_single_get_current_button(gesture);
const gboolean shift_pressed = dt_modifier_is(state, GDK_SHIFT_MASK);
if((button == GDK_BUTTON_SECONDARY && n_press == 1) // contextual menu
|| (d->tree_flag && button == GDK_BUTTON_PRIMARY && n_press == 1) // expand & drag
|| (n_press == 2 && button == GDK_BUTTON_PRIMARY)) // attach
{
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
GtkTreePath *path = NULL;
// Get tree path for row that was clicked
if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view),
(gint)x, (gint)y,
&path, NULL, NULL, NULL))
{
if(d->tree_flag
&& button == GDK_BUTTON_PRIMARY && n_press == 1
&& !shift_pressed)
{
GtkTreeModel *model = gtk_tree_view_get_model(d->dictionary_view);
GtkTreeIter iter;
gtk_tree_model_get_iter(model, &iter, path);
char *tagname;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_PATH, &tagname, -1);
if(d->drag.tagname) g_free(d->drag.tagname);
d->drag.tagname = tagname;
if(d->drag.path) gtk_tree_path_free(d->drag.path);
d->drag.path = path;
if(d->drag.lastpath) gtk_tree_path_free(d->drag.lastpath);
d->drag.lastpath = NULL;
/* event is borrowed (gesture) */
return;
}
else
{
gtk_tree_selection_select_path(selection, path);
dt_lib_gui_queue_update(self);
if(button == GDK_BUTTON_SECONDARY && n_press == 1)
{
dt_gui_claim(gesture);
_pop_menu_dictionary(view, (GdkEventButton *)event, self);
gtk_tree_path_free(path);
/* event is borrowed (gesture) */
return;
}
else if(d->tree_flag
&& button == GDK_BUTTON_PRIMARY && n_press == 1
&& shift_pressed)
{
dt_gui_claim(gesture);
gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path, TRUE);
gtk_tree_path_free(path);
/* event is borrowed (gesture) */
return;
}
else if(n_press == 2 && button == GDK_BUTTON_PRIMARY)
{
dt_gui_claim(gesture);
_attach_selected_tag(self, d);
gtk_tree_path_free(path);
/* event is borrowed (gesture) */
return;
}
}
}
gtk_tree_path_free(path);
}
/* event is borrowed (gesture) */
}
static gboolean _dictionary_key_pressed(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
_unselect_all_in_view(d->attached_view);
GtkTreeIter iter;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
GtkWidget *view = dt_gui_get_widget(controller);
GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
gboolean res = FALSE;
if(gtk_tree_selection_get_selected(selection, &model, &iter))
{
GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
switch(keyval)
{
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
{
_attach_selected_tag(self, d);
if(dt_modifier_is(state, GDK_SHIFT_MASK))
{
gtk_tree_selection_unselect_all(selection);
gtk_entry_set_text(GTK_ENTRY(d->entry), "");
gtk_widget_grab_focus(GTK_WIDGET(d->entry));
res = TRUE;
}
break;
}
case GDK_KEY_Left:
if(path)
{
if(dt_modifier_is(state, GDK_SHIFT_MASK))
gtk_tree_view_collapse_all(GTK_TREE_VIEW(view));
else
gtk_tree_view_collapse_row(GTK_TREE_VIEW(view), path);
res = TRUE;
}
break;
case GDK_KEY_Right:
if(path)
{
gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path,
dt_modifier_is(state, GDK_SHIFT_MASK));
res = TRUE;
}
break;
default:
break;
}
gtk_tree_path_free(path);
}
if(keyval == GDK_KEY_Tab)
{
gtk_tree_selection_unselect_all(selection);
res = TRUE;
}
else if(keyval == GDK_KEY_ISO_Left_Tab)
{
gtk_tree_selection_unselect_all(selection);
gtk_widget_grab_focus(GTK_WIDGET(d->entry));
res = TRUE;
}
return res;
}
static gboolean _row_tooltip_setup(GtkWidget *treeview,
const gint x,
const gint y,
const gboolean kb_mode,
GtkTooltip* tooltip, dt_lib_module_t *self)
{
gboolean res = FALSE;
GtkTreePath *path = NULL;
// Get tree path mouse position
if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(treeview), x, y, &path, NULL, NULL, NULL))
{
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
GtkTreeIter iter;
if(gtk_tree_model_get_iter(model, &iter, path))
{
char *tagname;
guint tagid;
guint flags;
char *synonyms = NULL;
gtk_tree_model_get(model, &iter,
DT_LIB_TAGGING_COL_ID, &tagid,
DT_LIB_TAGGING_COL_TAG, &tagname,
DT_LIB_TAGGING_COL_FLAGS, &flags,
DT_LIB_TAGGING_COL_SYNONYM, &synonyms, -1);
if(tagid)
{
if((flags & DT_TF_PRIVATE) || (synonyms && synonyms[0]))
{
gchar *text = g_strdup_printf(_("%s"), tagname);
dt_util_str_cat(&text, " %s\n",
(flags & DT_TF_PRIVATE) ? _("(private)") : "");
dt_util_str_cat(&text, "synonyms: %s",
(synonyms && synonyms[0]) ? synonyms : " - ");
gtk_tooltip_set_text(tooltip, text);
g_free(text);
res = TRUE;
}
}
g_free(synonyms);
g_free(tagname);
}
}
gtk_tree_path_free(path);
return res;
}
static void _import_button_clicked(GtkButton *button,
dt_lib_module_t *self)
{
const char *last_dirname =
dt_conf_get_string_const("plugins/lighttable/tagging/last_import_export_location");
if(!last_dirname || !*last_dirname)
{
last_dirname = g_get_home_dir();
}
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkFileChooserNative *filechooser = gtk_file_chooser_native_new
(_("select a keyword file"),
GTK_WINDOW(win),
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_import"), _("_cancel"));
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);
if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
{
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
char *dirname = g_path_get_dirname(filename);
dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
ssize_t count = dt_tag_import(filename);
if(count < 0)
dt_control_log(_("error importing tags"));
else
dt_control_log(_("%zd tags imported"), count);
g_free(filename);
g_free(dirname);
}
g_object_unref(filechooser);
_init_treeview(self, 1);
}
static void _export_button_clicked(GtkButton *button,
dt_lib_module_t *self)
{
GDateTime *now = g_date_time_new_now_local();
char *export_filename =
g_date_time_format(now, "darktable_tags_%F_%H-%M.txt");
const char *last_dirname =
dt_conf_get_string_const("plugins/lighttable/tagging/last_import_export_location");
if(!last_dirname || !*last_dirname)
{
last_dirname = g_get_home_dir();
}
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkFileChooserNative *filechooser = gtk_file_chooser_native_new
(_("select file to export to"),
GTK_WINDOW(win),
GTK_FILE_CHOOSER_ACTION_SAVE,
_("_export"), _("_cancel"));
gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(filechooser), TRUE);
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(filechooser), export_filename);
if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
{
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
char *dirname = g_path_get_dirname(filename);
dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
const ssize_t count = dt_tag_export(filename);
if(count < 0)
dt_control_log(_("error exporting tags"));
else
dt_control_log(_("%zd tags exported"), count);
g_free(filename);
g_free(dirname);
}
g_date_time_unref(now);
g_free(export_filename);
g_object_unref(filechooser);
}
static void _update_layout(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view));
DT_ENTER_GUI_UPDATE();
d->suggestion_flag = dt_conf_get_bool("plugins/lighttable/tagging/nosuggestion");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_suggestion_button),
d->suggestion_flag);
d->tree_flag = dt_conf_get_bool("plugins/lighttable/tagging/treeview");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_tree_button),
d->tree_flag);
if(d->tree_flag)
{
if(model == GTK_TREE_MODEL(d->dictionary_listfilter))
{
g_object_ref(model);
gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), NULL);
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_list_store_clear(GTK_LIST_STORE(store));
gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view),
GTK_TREE_MODEL(d->dictionary_treefilter));
g_object_unref(d->dictionary_treefilter);
}
gtk_widget_set_sensitive(GTK_WIDGET(d->toggle_suggestion_button), FALSE);
}
else
{
if(model == GTK_TREE_MODEL(d->dictionary_treefilter))
{
g_object_ref(model);
gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view), NULL);
GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model));
gtk_tree_store_clear(GTK_TREE_STORE(store));
gtk_tree_view_set_model(GTK_TREE_VIEW(d->dictionary_view),
GTK_TREE_MODEL(d->dictionary_listfilter));
g_object_unref(d->dictionary_listfilter);
}
gtk_widget_set_sensitive(GTK_WIDGET(d->toggle_suggestion_button), TRUE);
}
// drag & drop
if(d->tree_flag)
gtk_drag_source_set(GTK_WIDGET(d->dictionary_view), GDK_BUTTON1_MASK,
target_list_tags, n_targets_tags, GDK_ACTION_MOVE);
else
gtk_drag_source_unset(GTK_WIDGET(d->dictionary_view));
d->sort_count_flag = dt_conf_get_bool("plugins/lighttable/tagging/listsortedbycount");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_sort_button),
d->sort_count_flag);
d->hide_path_flag = dt_conf_get_bool("plugins/lighttable/tagging/hidehierarchy");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_hide_button),
d->hide_path_flag);
d->dttags_flag = dt_conf_get_bool("plugins/lighttable/tagging/dttags");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_dttags_button),
d->dttags_flag);
DT_LEAVE_GUI_UPDATE();
}
static void _toggle_suggestion_button_callback(GtkToggleButton *source,
dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/nosuggestion");
dt_conf_set_bool("plugins/lighttable/tagging/nosuggestion", new_state);
_update_layout(self);
_init_treeview(self, 1);
}
static void _toggle_tree_button_callback(GtkToggleButton *source,
dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/treeview");
dt_conf_set_bool("plugins/lighttable/tagging/treeview", new_state);
_update_layout(self);
_init_treeview(self, 1);
}
static gint _sort_tree_count_func(GtkTreeModel *model,
GtkTreeIter *a,
GtkTreeIter *b,
dt_lib_module_t *self)
{
guint count_a = 0;
guint count_b = 0;
gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_COUNT, &count_a, -1);
gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_COUNT, &count_b, -1);
return (count_b - count_a);
}
static inline gint _compare_utf8_no_case(const char *a, const char *b)
{
char *a_nc_nat = g_utf8_collate_key_for_filename(a, -1);
char *b_nc_nat = g_utf8_collate_key_for_filename(b, -1);
const gint sort = g_strcmp0(a_nc_nat, b_nc_nat);
g_free(a_nc_nat);
g_free(b_nc_nat);
return sort;
}
static gint _sort_tree_tag_func(GtkTreeModel *model,
GtkTreeIter *a,
GtkTreeIter *b,
dt_lib_module_t *self)
{
char *tag_a = NULL;
char *tag_b = NULL;
gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_TAG, &tag_a, -1);
gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_TAG, &tag_b, -1);
if(tag_a == NULL) tag_a = g_strdup("");
if(tag_b == NULL) tag_b = g_strdup("");
const gint sort = _compare_utf8_no_case(tag_a, tag_b);
g_free(tag_a);
g_free(tag_b);
return sort;
}
static gint _sort_tree_path_func(GtkTreeModel *model,
GtkTreeIter *a,
GtkTreeIter *b,
dt_lib_module_t *self)
{
char *tag_a = NULL;
char *tag_b = NULL;
gtk_tree_model_get(model, a, DT_LIB_TAGGING_COL_PATH, &tag_a, -1);
gtk_tree_model_get(model, b, DT_LIB_TAGGING_COL_PATH, &tag_b, -1);
if(tag_a)
{
for(char *letter = tag_a; *letter; letter++)
if(*letter == '|') *letter = '\1';
}
else
tag_a = g_strdup("");
if(tag_b)
{
for(char *letter = tag_b; *letter; letter++)
if(*letter == '|') *letter = '\1';
}
else
tag_b = g_strdup("");
const gint sort = _compare_utf8_no_case(tag_a, tag_b);
g_free(tag_a);
g_free(tag_b);
return sort;
}
static void _toggle_sort_button_callback(GtkToggleButton *source,
dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const gboolean new_state =
!dt_conf_get_bool("plugins/lighttable/tagging/listsortedbycount");
dt_conf_set_bool("plugins/lighttable/tagging/listsortedbycount", new_state);
_update_layout(self);
_sort_attached_list(self, FALSE);
_sort_dictionary_list(self, FALSE);
}
static void _toggle_hide_button_callback(GtkToggleButton *source,
dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/hidehierarchy");
dt_conf_set_bool("plugins/lighttable/tagging/hidehierarchy", new_state);
_update_layout(self);
_sort_attached_list(self, TRUE);
_sort_dictionary_list(self, TRUE);
}
static void _toggle_dttags_button_callback(GtkToggleButton *source,
dt_lib_module_t *self)
{
DT_GUARD_GUI_UPDATE();
const gboolean new_state = !dt_conf_get_bool("plugins/lighttable/tagging/dttags");
dt_conf_set_bool("plugins/lighttable/tagging/dttags", new_state);
dt_lib_tagging_t *d = self->data;
d->dttags_flag = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d->toggle_dttags_button));
_init_treeview(self, 0);
}
void gui_reset(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
// clear entry box and query
gtk_entry_set_text(d->entry, "");
_set_keyword(self);
_init_treeview(self, 1);
dt_lib_gui_queue_update(self);
}
int position(const dt_lib_module_t *self)
{
return 500;
}
static gboolean _match_selected_func(GtkEntryCompletion *completion,
GtkTreeModel *model,
GtkTreeIter *iter,
gpointer user_data)
{
const int column = gtk_entry_completion_get_text_column(completion);
char *tag = NULL;
if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING) return TRUE;
GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
if(!GTK_IS_EDITABLE(e))
{
return FALSE;
}
gtk_tree_model_get(model, iter, column, &tag, -1);
gint cut_off, cur_pos = gtk_editable_get_position(e);
gchar *currentText = gtk_editable_get_chars(e, 0, -1);
const gchar *lastTag = g_strrstr(currentText, ",");
if(lastTag == NULL)
{
cut_off = 0;
}
else
{
cut_off = (int)(g_utf8_strlen(currentText, -1) - g_utf8_strlen(lastTag, -1))+1;
}
free(currentText);
gtk_editable_delete_text(e, cut_off, cur_pos);
cur_pos = cut_off;
gtk_editable_insert_text(e, tag, -1, &cur_pos);
gtk_editable_set_position(e, cur_pos);
g_free(tag); // release result of gtk_tree_model_get
return TRUE;
}
static gboolean _completion_match_func(GtkEntryCompletion *completion,
const gchar *key,
GtkTreeIter *iter,
gpointer user_data)
{
gboolean res = FALSE;
GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
if(!GTK_IS_EDITABLE(e))
{
return FALSE;
}
const gint cur_pos = gtk_editable_get_position(e);
const gboolean onLastTag = (g_strstr_len(&key[cur_pos], -1, ",") == NULL);
if(!onLastTag)
{
return FALSE;
}
GtkTreeModel *model = gtk_entry_completion_get_model(completion);
const int column = gtk_entry_completion_get_text_column(completion);
if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING)
{
return FALSE;
}
const gchar *lastTag = g_strrstr(key, ",");
if(lastTag != NULL)
{
lastTag++;
}
else
{
lastTag = key;
}
if(lastTag[0] == '\0' || key[0] == '\0')
{
return FALSE;
}
char *tag = NULL;
gtk_tree_model_get(model, iter, column, &tag, -1);
if(tag)
{
char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
if(normalized)
{
char *casefold = g_utf8_casefold(normalized, -1);
if(casefold)
{
res = g_strstr_len(casefold, -1, lastTag) != NULL;
}
g_free(casefold);
}
g_free(normalized);
g_free(tag);
}
return res;
}
static void _tree_selection_changed(GtkTreeSelection *treeselection,
gpointer data)
{
dt_lib_gui_queue_update((dt_lib_module_t *)data);
}
static void _dnd_clear_root(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
if(!d->drag.root) return;
GtkTreeModel *model = GTK_TREE_MODEL(d->dictionary_treestore);
GtkTreeIter iter;
gtk_tree_model_get_iter_first(model, &iter);
char *name;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &name, -1);
if(name && name[0] == '\0')
gtk_tree_store_remove(d->dictionary_treestore, &iter);
g_free(name);
d->drag.root = FALSE;
}
static void _event_dnd_begin(GtkWidget *widget,
GdkDragContext *context,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkTreeView *tree = GTK_TREE_VIEW(widget);
if(d->drag.path)
{
cairo_surface_t *row_pix = gtk_tree_view_create_row_drag_icon(tree, d->drag.path);
// FIXME: try to put the anchor point on left bottom corner as for images
// cairo_surface_set_device_offset(row_pix,
// // the + 1 is for the black border in the icon
// - (double)(cairo_image_surface_get_width(row_pix) + 1),
// - (double)(cairo_image_surface_get_height(row_pix)+ 1));
gtk_drag_set_icon_surface(context, row_pix);
cairo_surface_destroy(row_pix);
gtk_tree_path_free(d->drag.path);
d->drag.path = NULL;
d->drag.tag_source = TRUE;
}
}
static void _event_dnd_get(GtkWidget *widget,
GdkDragContext *context,
GtkSelectionData *selection_data,
const guint target_type,
const guint time,
dt_lib_module_t *self)
{
if(target_type == DND_TARGET_TAG)
gtk_selection_data_set(selection_data, gtk_selection_data_get_target(selection_data),
_DWORD, NULL, 0);
}
static void _event_dnd_received(GtkWidget *widget,
GdkDragContext *context,
const gint x,
const gint y,
GtkSelectionData *selection_data,
const guint target_type,
const guint time,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkTreeView *tree = GTK_TREE_VIEW(widget);
// disable the default handler
g_signal_stop_emission_by_name(tree, "drag-data-received");
gboolean success = FALSE;
if(target_type == DND_TARGET_TAG)
{
GtkTreePath *path = NULL;
if(gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
{
char *name;
GtkTreeModel *model = gtk_tree_view_get_model(tree);
GtkTreeIter iter;
gtk_tree_model_get_iter(model, &iter, path);
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_PATH, &name, -1);
_dnd_clear_root(self);
const gboolean root = (name && name[0] == '\0');
char *leave = g_strrstr(d->drag.tagname, "|");
if(leave) leave++;
dt_util_str_cat(&name, "%s%s", root ? "" : "|", leave ? leave : d->drag.tagname);
_apply_rename_path(NULL, d->drag.tagname, name, self);
g_free(name);
g_free(d->drag.tagname);
d->drag.tagname = NULL;
gtk_tree_path_free(path); // release result of gtk_tree_view_get_path_at_pos above
success = TRUE;
}
}
else if((target_type == DND_TARGET_IMGID) && (selection_data != NULL))
{
GtkTreePath *path = NULL;
const int imgs_nb = gtk_selection_data_get_length(selection_data) / sizeof(dt_imgid_t);
if(imgs_nb && gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
{
const dt_imgid_t *imgt = (dt_imgid_t *)gtk_selection_data_get_data(selection_data);
GList *imgs = NULL;
for(int i = 0; i < imgs_nb; i++)
{
imgs = g_list_prepend(imgs, GINT_TO_POINTER(imgt[i]));
}
GtkTreeModel *model = gtk_tree_view_get_model(tree);
GtkTreeIter iter;
gtk_tree_model_get_iter(model, &iter, path);
gint tagid;
gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
if(tagid)
dt_tag_attach_images(tagid, imgs, TRUE);
g_list_free(imgs);
_update_attached_count(tagid, d->dictionary_view, d->tree_flag);
_init_treeview(self, 0);
_raise_signal_tag_changed(self);
dt_image_synch_xmp(-1);
gtk_tree_path_free(path); // release result of gtk_tree_view_get_path_at_pos above
success = TRUE;
}
}
gtk_drag_finish(context, success, FALSE, time);
}
static gboolean _dnd_expand_timeout(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
if(d->drag.lastpath)
{
gtk_tree_view_expand_row(d->dictionary_view, d->drag.lastpath, FALSE);
}
return FALSE;
}
static gboolean _dnd_scroll_timeout(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
if(d->drag.scroll_timeout)
{
GdkRectangle visible;
gtk_tree_view_get_visible_rect(d->dictionary_view, &visible);
gint top_ty;
gtk_tree_view_convert_bin_window_to_tree_coords(d->dictionary_view,
0, 0, NULL, &top_ty);
// temporary drop root
if(d->drag.tag_source && !d->drag.root && d->drag.last_y < 5 && top_ty < 1)
{
GtkTreeIter iter;
gtk_tree_store_prepend(d->dictionary_treestore, &iter, NULL);
gtk_tree_store_set(d->dictionary_treestore, &iter,
DT_LIB_TAGGING_COL_TAG, _("drop to root"),
DT_LIB_TAGGING_COL_ID, 0,
DT_LIB_TAGGING_COL_PATH, "",
DT_LIB_TAGGING_COL_COUNT, 0,
DT_LIB_TAGGING_COL_SEL, 0,
DT_LIB_TAGGING_COL_FLAGS, 0,
DT_LIB_TAGGING_COL_VISIBLE, TRUE,
-1);
d->drag.root = TRUE;
}
else if(d->drag.root && d->drag.last_y >= 20)
_dnd_clear_root(self);
if(d->drag.last_y < 5)
{
// scroll up
gtk_tree_view_scroll_to_point(d->dictionary_view, 0,
top_ty - 25 < 0 ? 0 : top_ty - 25);
}
else if(d->drag.last_y > visible.height - 5)
// scroll down
gtk_tree_view_scroll_to_point(d->dictionary_view, 0, top_ty + 25);
return TRUE;
}
return FALSE;
}
static gboolean _event_dnd_motion(GtkWidget *widget,
GdkDragContext *context,
const gint x,
const gint y,
const guint time,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkTreeView *tree = GTK_TREE_VIEW(widget);
GtkTreePath *path = NULL;
if(gtk_tree_view_get_path_at_pos(tree, x, y, &path, NULL, NULL, NULL))
{
if(!d->drag.lastpath
|| ((d->drag.lastpath)
&& gtk_tree_path_compare(d->drag.lastpath, path) != 0))
{
GtkTreeViewColumn *col = gtk_tree_view_get_column(d->dictionary_view, 0);
const int sel_width = gtk_tree_view_column_get_width(col);
if(x >= sel_width)
{
if(!gtk_tree_view_row_expanded(tree, path))
d->drag.expand_timeout =
g_timeout_add(200, (GSourceFunc)_dnd_expand_timeout, self);
}
}
GtkTreeSelection *selection = gtk_tree_view_get_selection(d->dictionary_view);
gtk_tree_selection_select_path(selection, path);
d->drag.last_y = y;
if(d->drag.scroll_timeout == 0)
{
d->drag.scroll_timeout = g_timeout_add(100, (GSourceFunc)_dnd_scroll_timeout, self);
}
}
// FIXME - no row highlihted... workartound with selection above
// gtk_tree_view_set_drag_dest_row(tree, path, GTK_TREE_VIEW_DROP_INTO_OR_BEFORE);
if(d->drag.lastpath)
gtk_tree_path_free(d->drag.lastpath);
d->drag.lastpath = path;
return TRUE;
}
static void _event_dnd_end(GtkWidget *widget,
GdkDragContext *context,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
// FIXME (see gtk_tree_view_set_drag_dest_row above)
// gtk_tree_view_set_drag_dest_row(d->dictionary_view, NULL, GTK_TREE_VIEW_DROP_BEFORE);
GtkTreeSelection *selection = gtk_tree_view_get_selection(d->dictionary_view);
gtk_tree_selection_unselect_all(selection);
if(d->drag.scroll_timeout)
g_source_remove(d->drag.scroll_timeout);
d->drag.scroll_timeout = 0;
d->drag.tag_source = FALSE;
_dnd_clear_root(self);
}
void gui_init(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = calloc(sizeof(dt_lib_tagging_t),1);
self->data = (void *)d;
d->last_tag = NULL;
self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
GtkBox *box, *hbox;
GtkWidget *button;
GtkWidget *w;
GtkTreeView *view;
GtkTreeModel *model;
GtkListStore *liststore;
GtkTreeStore *treestore;
GtkTreeViewColumn *col;
GtkCellRenderer *renderer;
// attached_view
box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
view = GTK_TREE_VIEW(gtk_tree_view_new());
w = dt_ui_resize_wrap(GTK_WIDGET(view), 200,
"plugins/lighttable/tagging/heightattachedwindow");
gtk_box_pack_start(box, w, TRUE, TRUE, 0);
d->attached_view = view;
gtk_tree_view_set_enable_search(view, FALSE);
gtk_tree_view_set_headers_visible(view, FALSE);
liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS,
G_TYPE_STRING, G_TYPE_UINT, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT,
G_TYPE_UINT, G_TYPE_BOOLEAN);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_PATH_ID, (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_NAME_ID, (GtkTreeIterCompareFunc)_sort_tree_tag_func, self, NULL);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_COUNT_ID, (GtkTreeIterCompareFunc)_sort_tree_count_func, self, NULL);
d->attached_liststore = liststore;
g_object_set(G_OBJECT(view), "has-tooltip", TRUE, NULL);
g_signal_connect(G_OBJECT(view), "query-tooltip",
G_CALLBACK(_row_tooltip_setup), (gpointer)self);
col = gtk_tree_view_column_new();
gtk_tree_view_append_column(view, col);
renderer = gtk_cell_renderer_toggle_new();
gtk_tree_view_column_pack_start(col, renderer, TRUE);
gtk_tree_view_column_set_cell_data_func(col, renderer, _tree_select_show, NULL, NULL);
g_object_set(renderer, "indicator-size", 8, NULL); // too big by default
col = gtk_tree_view_column_new();
gtk_tree_view_append_column(view, col);
renderer = gtk_cell_renderer_text_new();
g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
gtk_tree_view_column_pack_start(col, renderer, TRUE);
gtk_tree_view_column_set_cell_data_func(col, renderer,
_tree_tagname_show_attached,
(gpointer)self, NULL);
gtk_tree_selection_set_mode(gtk_tree_view_get_selection(view), GTK_SELECTION_SINGLE);
gtk_tree_view_set_model(view, GTK_TREE_MODEL(liststore));
g_object_unref(liststore);
gtk_widget_set_tooltip_text
(GTK_WIDGET(view),
_("attached tags"
"\npress Delete or double-click to detach"
"\nright-click for other actions on attached tag,"
"\nTab to give the focus to entry"));
dt_gui_connect_click_all(view, _click_on_view_attached, NULL, self);
dt_gui_connect_key(view, _attached_key_pressed, self);
g_signal_connect(gtk_tree_view_get_selection(view), "changed",
G_CALLBACK(_tree_selection_changed), self);
// attach/detach buttons
hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
d->attach_button = dt_action_button_new(self, N_("attach"),
_attach_button_clicked, self,
_("attach tag to all selected images"), 0, 0);
gtk_box_pack_start(hbox, d->attach_button, TRUE, TRUE, 0);
d->detach_button = dt_action_button_new(self, N_("detach"),
_detach_button_clicked, self,
_("detach tag from all selected images"), 0, 0);
gtk_box_pack_start(hbox, d->detach_button, TRUE, TRUE, 0);
dt_action_t *toggle = dt_action_section(DT_ACTION(self), N_("toggle"));
#define NEW_TOGGLE_BUTTON(paint, callback, tooltip, action) \
button = dtgtk_togglebutton_new(paint, 0, NULL); \
gtk_widget_set_tooltip_text(button, tooltip); \
gtk_box_pack_end(hbox, button, FALSE, TRUE, 0); \
g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(callback), self); \
dt_action_define(toggle, NULL, action, button, &dt_action_def_toggle);
d->toggle_hide_button = NEW_TOGGLE_BUTTON
(dtgtk_cairo_paint_minus_simple, _toggle_hide_button_callback,
_("toggle list with / without hierarchy"), N_("hide"));
d->toggle_sort_button = NEW_TOGGLE_BUTTON
(dtgtk_cairo_paint_sorting, _toggle_sort_button_callback,
_("toggle sort by name or by count"), N_("sort"));
d->toggle_dttags_button = NEW_TOGGLE_BUTTON
(dtgtk_cairo_paint_check_mark, _toggle_dttags_button_callback,
_("toggle show or not darktable tags"), N_("dttags"));
d->dttags_flag = FALSE;
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->toggle_dttags_button), FALSE);
gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
// dictionary_view
box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0));
gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
// text entry
w = dt_ui_entry_new(0);
gtk_entry_set_text(GTK_ENTRY(w), "");
gtk_widget_set_tooltip_text
(w,
_("enter tag name"
"\npress Enter to create a new tag and attach it on selected images"
"\npress Tab or Down key to go to the first matching tag"
"\npress shift+Tab to select the first attached user tag"));
gtk_box_pack_start(hbox, w, TRUE, TRUE, 0);
gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_RELEASE_MASK);
g_signal_connect(G_OBJECT(w), "changed",
G_CALLBACK(_tag_name_changed), (gpointer)self);
dt_gui_connect_key(w, _enter_key_pressed, self);
d->entry = GTK_ENTRY(w);
button = dtgtk_button_new_full(dtgtk_cairo_paint_multiply_small, 0, NULL,
&(dtgtk_button_config_t){
.tooltip = _("clear entry"),
.action = DT_ACTION(self),
.action_label = N_("clear entry"),
.action_def = &dt_action_def_button,
.clicked_cb = G_CALLBACK(_clear_entry_button_callback),
.clicked_data = (gpointer)self,
});
gtk_box_pack_end(hbox, button, FALSE, TRUE, 0);
gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
dt_gui_add_class(GTK_WIDGET(box), "dt_spacing_sw");
d->clear_button = button;
// dictionary_view tree view
view = GTK_TREE_VIEW(gtk_tree_view_new());
w = dt_ui_resize_wrap(GTK_WIDGET(view), 200,
"plugins/lighttable/tagging/heightdictionarywindow");
gtk_box_pack_start(box, w, TRUE, TRUE, 0);
d->dictionary_view = view;
gtk_tree_view_set_enable_search(view, FALSE);
gtk_tree_view_set_headers_visible(view, FALSE);
liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT,
G_TYPE_STRING, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT,
G_TYPE_UINT, G_TYPE_BOOLEAN);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_PATH_ID, (GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_NAME_ID, (GtkTreeIterCompareFunc)_sort_tree_tag_func, self, NULL);
gtk_tree_sortable_set_sort_func
(GTK_TREE_SORTABLE(liststore),
DT_TAG_SORT_COUNT_ID, (GtkTreeIterCompareFunc)_sort_tree_count_func, self, NULL);
d->dictionary_liststore = liststore;
model = gtk_tree_model_filter_new(GTK_TREE_MODEL(liststore), NULL);
gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model),
DT_LIB_TAGGING_COL_VISIBLE);
d->dictionary_listfilter = GTK_TREE_MODEL_FILTER(model);
treestore = gtk_tree_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING,
G_TYPE_UINT, G_TYPE_STRING,
G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT,
G_TYPE_UINT, G_TYPE_BOOLEAN);
gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(treestore), DT_TAG_SORT_PATH_ID,
(GtkTreeIterCompareFunc)_sort_tree_path_func, self, NULL);
d->dictionary_treestore = treestore;
model = gtk_tree_model_filter_new(GTK_TREE_MODEL(treestore), NULL);
gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model),
DT_LIB_TAGGING_COL_VISIBLE);
d->dictionary_treefilter = GTK_TREE_MODEL_FILTER(model);
col = gtk_tree_view_column_new();
gtk_tree_view_append_column(view, col);
renderer = gtk_cell_renderer_toggle_new();
gtk_tree_view_column_pack_start(col, renderer, TRUE);
gtk_cell_renderer_toggle_set_activatable(GTK_CELL_RENDERER_TOGGLE(renderer), TRUE);
gtk_tree_view_column_set_cell_data_func(col, renderer, _tree_select_show, NULL, NULL);
g_object_set(renderer, "indicator-size", 8, NULL); // too big by default
col = gtk_tree_view_column_new();
gtk_tree_view_append_column(view, col);
renderer = gtk_cell_renderer_text_new();
g_object_set(renderer, "ellipsize", PANGO_ELLIPSIZE_MIDDLE, (gchar *)0);
gtk_tree_view_column_pack_start(col, renderer, TRUE);
gtk_tree_view_column_set_cell_data_func(col, renderer,
_tree_tagname_show_dictionary, (gpointer)self, NULL);
gtk_tree_view_set_expander_column(view, col);
gtk_tree_selection_set_mode(gtk_tree_view_get_selection(view), GTK_SELECTION_SINGLE);
gtk_widget_set_tooltip_text
(GTK_WIDGET(view),
_("tag dictionary,"
"\nEnter or double-click to attach selected tag on selected images"
"\nshift+Enter idem plus gives the focus to entry"
"\nshift+click to fully expand the selected tag"
"\nright-click for other actions on selected tag"
"\nshift+Tab to give the focus to entry"));
dt_gui_connect_click_all(view, _click_on_view_dictionary, NULL, self);
dt_gui_connect_key(view, _dictionary_key_pressed, self);
gtk_tree_view_set_model(view, GTK_TREE_MODEL(d->dictionary_listfilter));
g_object_unref(d->dictionary_listfilter);
g_object_set(G_OBJECT(view), "has-tooltip", TRUE, NULL);
g_signal_connect(G_OBJECT(view), "query-tooltip",
G_CALLBACK(_row_tooltip_setup), (gpointer)self);
g_signal_connect(gtk_tree_view_get_selection(view), "changed",
G_CALLBACK(_tree_selection_changed), self);
// drag & drop
{
d->drag.path = NULL;
d->drag.tagname = NULL;
d->drag.scroll_timeout = 0;
d->drag.expand_timeout = 0;
d->drag.root = FALSE;
d->drag.tag_source = FALSE;
gtk_drag_dest_set(GTK_WIDGET(d->dictionary_view), GTK_DEST_DEFAULT_ALL,
target_list_tags_dest, n_targets_tags_dest, GDK_ACTION_MOVE);
g_signal_connect(d->dictionary_view, "drag-data-get",
G_CALLBACK(_event_dnd_get), self);
g_signal_connect(d->dictionary_view, "drag-data-received",
G_CALLBACK(_event_dnd_received), self);
g_signal_connect_after(d->dictionary_view, "drag-begin",
G_CALLBACK(_event_dnd_begin), self);
g_signal_connect_after(d->dictionary_view, "drag-end",
G_CALLBACK(_event_dnd_end), self);
g_signal_connect(d->dictionary_view, "drag-motion",
G_CALLBACK(_event_dnd_motion), self);
}
// buttons
hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));
d->new_button = dt_action_button_new(self, N_("new"),
_new_button_clicked,
self, _("create a new tag with the\nname you entered"), 0, 0);
gtk_box_pack_start(hbox, d->new_button, TRUE, TRUE, 0);
d->import_button = dt_action_button_new(self, N_("import..."),
_import_button_clicked,
self,
_("import tags from a Lightroom keyword file"),
0, 0);
gtk_box_pack_start(hbox, d->import_button, TRUE, TRUE, 0);
d->export_button = dt_action_button_new(self, N_("export..."),
_export_button_clicked,
self,
_("export all tags to a Lightroom keyword file"),
0, 0);
gtk_box_pack_start(hbox, d->export_button, TRUE, TRUE, 0);
d->toggle_tree_button = NEW_TOGGLE_BUTTON(dtgtk_cairo_paint_treelist,
_toggle_tree_button_callback,
_("toggle list / tree view"), N_("tree"));
d->toggle_suggestion_button = NEW_TOGGLE_BUTTON
(dtgtk_cairo_paint_plus_simple,
_toggle_suggestion_button_callback,
_("toggle list with / without suggestion"),
N_("suggestion"));
#undef NEW_TOGGLE_BUTTON
gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
/* connect to mouse over id */
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, _lib_tagging_redraw_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_TAG_CHANGED, _lib_tagging_tags_changed_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_SELECTION_CHANGED, _lib_selection_changed_callback);
DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_COLLECTION_CHANGED, _collection_updated_callback);
d->collection = g_malloc(4096);
_update_layout(self);
_init_treeview(self, 0);
_set_keyword(self);
_init_treeview(self, 1);
dt_action_register(DT_ACTION(self), N_("tag"),
_lib_tagging_tag_show, GDK_KEY_t, GDK_CONTROL_MASK);
dt_action_register(DT_ACTION(self), N_("redo last tag"),
_lib_tagging_tag_redo, GDK_KEY_t, GDK_MOD1_MASK);
}
void gui_cleanup(dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
g_free(d->collection);
if(d->drag.tagname) g_free(d->drag.tagname);
if(d->drag.path) gtk_tree_path_free(d->drag.path);
free(self->data);
self->data = NULL;
}
// enter and escape end the floating entry themselves, and destroying its
// window then takes the focus away from it as well
static void _lib_tagging_tag_destroy(GtkEventController *controller,
GtkWidget *window)
{
if(g_object_get_data(G_OBJECT(dt_gui_get_widget(controller)), "done")) return;
gtk_widget_destroy(window);
}
// http://stackoverflow.com/questions/4631388/transparent-floating-gtkentry
static gboolean _lib_tagging_tag_key_press(GtkEventControllerKey *controller,
guint keyval,
guint keycode,
GdkModifierType state,
dt_lib_module_t *self)
{
dt_lib_tagging_t *d = self->data;
GtkWidget *entry = NULL;
switch(keyval)
{
case GDK_KEY_Escape:
entry = dt_gui_get_widget(controller);
g_object_set_data(G_OBJECT(entry), "done", GINT_TO_POINTER(TRUE));
g_list_free(d->floating_tag_imgs);
gtk_widget_destroy(d->floating_tag_window);
gtk_window_present(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
return TRUE;
case GDK_KEY_Tab:
return TRUE;
case GDK_KEY_Return:
case GDK_KEY_KP_Enter:
{
entry = dt_gui_get_widget(controller);
g_object_set_data(G_OBJECT(entry), "done", GINT_TO_POINTER(TRUE));
dt_gui_cursor_set_busy();
const gchar *tag = gtk_entry_get_text(GTK_ENTRY(entry));
const gboolean res = dt_tag_attach_string_list(tag, d->floating_tag_imgs, TRUE);
if(res) dt_image_synch_xmps(d->floating_tag_imgs);
dt_gui_cursor_clear_busy();
g_list_free(d->floating_tag_imgs);
/** record last tag used */
_save_last_tag_used(tag, d);
_init_treeview(self, 0);
_init_treeview(self, 1);
gtk_widget_destroy(d->floating_tag_window);
gtk_window_present(GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
if(res) _raise_signal_tag_changed(self);
return TRUE;
}
}
return FALSE; /* event not handled */
}
static void _lib_tagging_tag_redo(dt_action_t *action)
{
dt_lib_module_t *self = dt_action_lib(action);
dt_lib_tagging_t *d = self->data;
if(d->last_tag)
{
GList *imgs = dt_act_on_get_images(FALSE, TRUE, FALSE);
const gboolean res = dt_tag_attach_string_list(d->last_tag, imgs, TRUE);
if(res) dt_image_synch_xmps(imgs);
g_list_free(imgs);
_init_treeview(self, 0);
_init_treeview(self, 1);
if(res) _raise_signal_tag_changed(self);
}
}
static void _lib_tagging_tag_show(dt_action_t *action)
{
dt_lib_module_t *self = dt_action_lib(action);
dt_lib_tagging_t *d = self->data;
if(d->tree_flag)
{
dt_control_log
(_("tag shortcut is not active with tag tree view. please switch to list view"));
return; // doesn't work properly with tree treeview
}
d->floating_tag_imgs = dt_act_on_get_images(FALSE, TRUE, FALSE);
GtkWidget *window = dt_ui_main_window(darktable.gui->ui);
GtkWidget *center = dt_ui_center(darktable.gui->ui);
const gboolean on_wayland = dt_gui_get_session_type() == DT_GUI_SESSION_WAYLAND;
if(on_wayland)
{
// Wayland: popover
d->floating_tag_window = gtk_popover_new(center);
gtk_popover_set_modal(GTK_POPOVER(d->floating_tag_window), TRUE);
gtk_popover_set_position(GTK_POPOVER(d->floating_tag_window), GTK_POS_LEFT);
}
else
{
// Other backends: transient undecorated window
d->floating_tag_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(d->floating_tag_window);
#endif
gtk_widget_set_can_focus(d->floating_tag_window, TRUE);
gtk_window_set_decorated(GTK_WINDOW(d->floating_tag_window), FALSE);
gtk_window_set_type_hint(GTK_WINDOW(d->floating_tag_window),
GDK_WINDOW_TYPE_HINT_POPUP_MENU);
gtk_window_set_transient_for(GTK_WINDOW(d->floating_tag_window), GTK_WINDOW(window));
}
GtkWidget *entry = gtk_entry_new();
gtk_widget_set_size_request(entry, FLOATING_ENTRY_WIDTH, -1);
gtk_widget_add_events(entry, GDK_FOCUS_CHANGE_MASK);
GtkEntryCompletion *completion = gtk_entry_completion_new();
gtk_entry_completion_set_model
(completion,
gtk_tree_view_get_model(GTK_TREE_VIEW(d->dictionary_view)));
gtk_entry_completion_set_text_column(completion, DT_LIB_TAGGING_COL_PATH);
gtk_entry_completion_set_inline_completion(completion, TRUE);
gtk_entry_completion_set_popup_set_width(completion, FALSE);
g_signal_connect(G_OBJECT(completion), "match-selected",
G_CALLBACK(_match_selected_func), self);
gtk_entry_completion_set_match_func(completion, _completion_match_func, NULL, NULL);
gtk_entry_set_completion(GTK_ENTRY(entry), completion);
// the entry now holds its own reference to the completion; drop ours so
// the completion (and the reference it keeps on the current dictionary
// filter) is released when the floating entry is destroyed. Leaking it
// pins stale GtkTreeModelFilters to the dictionary store and makes every
// later rebuild fan out to all of them (quadratic slowdown over a session).
g_object_unref(completion);
gtk_widget_set_name(entry, "tag-completion");
gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
gtk_container_add(GTK_CONTAINER(d->floating_tag_window), entry);
dt_gui_connect_focus(entry, NULL, _lib_tagging_tag_destroy, d->floating_tag_window);
dt_gui_connect_key(entry, _lib_tagging_tag_key_press, self);
gtk_widget_show_all(d->floating_tag_window);
gtk_widget_grab_focus(entry);
// Position the widget on screen. Note that the Wayland popover is
// placed on the right of the lighttable whereas the floating window
// on x11 is at the bottom.
// FIXME: The reason is that on Wayland/Gtk3 the popup completion Window
// does not open correctly at the top of the entry when there is no
// available spaces at the bottom.
// This is to be revisited when moving to Gtk4.
if(on_wayland)
{
GtkAllocation a;
gtk_widget_get_allocation(center, &a);
GdkRectangle rect;
rect.x = MAX(0, a.width - 1);
rect.y = MAX(0, (a.height / 2));
rect.width = FLOATING_ENTRY_WIDTH;
rect.height = 1;
gtk_popover_set_pointing_to(GTK_POPOVER(d->floating_tag_window), &rect);
}
else
{
gint px, py, w, h;
gdk_window_get_origin(gtk_widget_get_window(center), &px, &py);
w = gdk_window_get_width(gtk_widget_get_window(center));
h = gdk_window_get_height(gtk_widget_get_window(center));
const gint x = px + 0.5 * (w - FLOATING_ENTRY_WIDTH);
const gint y = py + h - 50;
gtk_window_move(GTK_WINDOW(d->floating_tag_window), x, y);
gtk_window_present(GTK_WINDOW(d->floating_tag_window));
}
}
static int _get_recent_tags_list_length()
{
const int length = dt_conf_get_int("plugins/lighttable/tagging/nb_recent_tags");
if(length == -1) return length;
else if(length >= 10/2) return length * 2;
else return 10;
}
static void _size_recent_tags_list()
{
const char *list = dt_conf_get_string_const("plugins/lighttable/tagging/recent_tags");
if(!list[0])
return;
const int length = _get_recent_tags_list_length();
if(length == -1)
{
dt_conf_set_string("plugins/lighttable/tagging/recent_tags", "");
return;
}
char *p = (char *)list;
int nb = 1;
for(; *p != '\0'; p++)
{
if(*p == ',') nb++;
}
if(nb > length)
{
nb -= length;
char *list2 = g_strdup(list);
for(; nb > 0; nb--)
{
p = g_strrstr(list2, "','");
if(p) *p = '\0';
}
dt_conf_set_string("plugins/lighttable/tagging/recent_tags", list2);
g_free(list2);
}
}
static void _menuitem_preferences(GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
dt_lib_module_t *self = (dt_lib_module_t *)user_data;
GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
GtkWidget *dialog = gtk_dialog_new_with_buttons(_("tagging settings"), GTK_WINDOW(win),
GTK_DIALOG_DESTROY_WITH_PARENT,
_("_cancel"), GTK_RESPONSE_NONE,
_("_save"), GTK_RESPONSE_ACCEPT, NULL);
dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
dt_prefs_init_dialog_tagging(dialog);
#ifdef GDK_WINDOWING_QUARTZ
dt_osx_disallow_fullscreen(dialog);
#endif
gtk_widget_show_all(dialog);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
dt_lib_tagging_t *d = self->data;
_size_recent_tags_list();
if(!d->tree_flag && d->suggestion_flag)
{
_init_treeview(self, 1);
dt_lib_gui_queue_update(self);
}
}
void set_preferences(GMenu *menu, GActionGroup *action_group, dt_lib_module_t *self)
{
GSimpleAction *action = g_simple_action_new("preferences", NULL);
g_signal_connect(action, "activate", G_CALLBACK(_menuitem_preferences), self);
g_action_map_add_action(G_ACTION_MAP(action_group), G_ACTION(action));
g_menu_append(menu, _("preferences..."), "presets.preferences");
}
static void _save_last_tag_used(const char *tagnames,
dt_lib_tagging_t *d)
{
g_free(d->last_tag);
d->last_tag = g_strdup(tagnames);
const int nb_recent = _get_recent_tags_list_length();
if(nb_recent != -1)
{
GList *ntags = dt_util_str_to_glist(",", tagnames);
if(ntags)
{
const char *sl = dt_conf_get_string_const("plugins/lighttable/tagging/recent_tags");
// use "','" instead of "," to use the list as is in the query (dt_tag_get_suggestions)
GList *tags = dt_util_str_to_glist("','", sl);
for(GList *tag = ntags; tag; tag = g_list_next(tag))
{
char *escaped = sqlite3_mprintf("%q", (char *)tag->data);
GList *found = g_list_find_custom(tags, escaped, (GCompareFunc)g_strcmp0);
if(found)
{
tags = g_list_remove_link(tags, found);
g_free(found->data);
g_list_free(found);
}
tags = g_list_prepend(tags, g_strdup(escaped));
sqlite3_free(escaped);
}
g_list_free_full(ntags, g_free);
char *nsl = dt_util_glist_to_str("','", tags);
dt_conf_set_string("plugins/lighttable/tagging/recent_tags", nsl);
g_free(nsl);
if(g_list_length(tags) > nb_recent)
_size_recent_tags_list();
g_list_free_full(tags, g_free);
}
}
}
// 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