/* 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