"Fossies" - the Fresh Open Source Software Archive

Member "darktable-2.6.3/src/libs/tagging.c" (20 Oct 2019, 30256 Bytes) of package /linux/misc/darktable-2.6.3.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "tagging.c" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3.0.0.rc1_vs_3.0.0.rc2.

    1 /*
    2     This file is part of darktable,
    3     copyright (c) 2009--2010 johannes hanika.
    4     copyright (c) 2011 henrik andersson.
    5 
    6     darktable is free software: you can redistribute it and/or modify
    7     it under the terms of the GNU General Public License as published by
    8     the Free Software Foundation, either version 3 of the License, or
    9     (at your option) any later version.
   10 
   11     darktable is distributed in the hope that it will be useful,
   12     but WITHOUT ANY WARRANTY; without even the implied warranty of
   13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   14     GNU General Public License for more details.
   15 
   16     You should have received a copy of the GNU General Public License
   17     along with darktable.  If not, see <http://www.gnu.org/licenses/>.
   18 */
   19 #include "common/collection.h"
   20 #include "common/darktable.h"
   21 #include "common/debug.h"
   22 #include "common/tags.h"
   23 #include "control/conf.h"
   24 #include "control/control.h"
   25 #include "dtgtk/button.h"
   26 #include "gui/accelerators.h"
   27 #include "gui/gtk.h"
   28 #include "libs/lib.h"
   29 #include "libs/lib_api.h"
   30 #include "views/view.h"
   31 #ifdef GDK_WINDOWING_QUARTZ
   32 #include "osx/osx.h"
   33 #endif
   34 #include <gdk/gdkkeysyms.h>
   35 #include <math.h>
   36 
   37 #define FLOATING_ENTRY_WIDTH DT_PIXEL_APPLY_DPI(150)
   38 
   39 DT_MODULE(1)
   40 
   41 static gboolean _lib_tagging_tag_show(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval,
   42                                       GdkModifierType modifier, dt_lib_module_t *self);
   43 
   44 typedef struct dt_lib_tagging_t
   45 {
   46   char keyword[1024];
   47   GtkEntry *entry;
   48   GtkTreeView *current, *related;
   49   int imgsel;
   50 
   51   GtkWidget *attach_button, *detach_button, *new_button, *delete_button, *import_button, *export_button;
   52 
   53   GtkWidget *floating_tag_window;
   54   int floating_tag_imgid;
   55 } dt_lib_tagging_t;
   56 
   57 typedef enum dt_lib_tagging_cols_t
   58 {
   59   DT_LIB_TAGGING_COL_TAG = 0,
   60   DT_LIB_TAGGING_COL_ID,
   61   DT_LIB_TAGGING_NUM_COLS
   62 } dt_lib_tagging_cols_t;
   63 
   64 const char *name(dt_lib_module_t *self)
   65 {
   66   return _("tagging");
   67 }
   68 
   69 const char **views(dt_lib_module_t *self)
   70 {
   71   static const char *v1[] = {"lighttable", "darkroom", "map", "tethering", NULL};
   72   static const char *v2[] = {"lighttable", "map", "tethering", NULL};
   73 
   74   if(dt_conf_get_bool("plugins/darkroom/tagging/visible"))
   75     return v1;
   76   else
   77     return v2;
   78 }
   79 
   80 uint32_t container(dt_lib_module_t *self)
   81 {
   82   const dt_view_t *cv = dt_view_manager_get_current_view(darktable.view_manager);
   83   if(cv->view((dt_view_t *)cv) == DT_VIEW_DARKROOM)
   84     return DT_UI_CONTAINER_PANEL_LEFT_CENTER;
   85   else
   86     return DT_UI_CONTAINER_PANEL_RIGHT_CENTER;
   87 }
   88 
   89 void init_key_accels(dt_lib_module_t *self)
   90 {
   91   dt_accel_register_lib(self, NC_("accel", "attach"), 0, 0);
   92   dt_accel_register_lib(self, NC_("accel", "detach"), 0, 0);
   93   dt_accel_register_lib(self, NC_("accel", "new"), 0, 0);
   94   dt_accel_register_lib(self, NC_("accel", "delete"), 0, 0);
   95   dt_accel_register_lib(self, NC_("accel", "tag"), GDK_KEY_t, GDK_CONTROL_MASK);
   96 }
   97 
   98 void connect_key_accels(dt_lib_module_t *self)
   99 {
  100   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  101 
  102   dt_accel_connect_button_lib(self, "attach", d->attach_button);
  103   dt_accel_connect_button_lib(self, "detach", d->detach_button);
  104   dt_accel_connect_button_lib(self, "new", d->new_button);
  105   dt_accel_connect_button_lib(self, "delete", d->delete_button);
  106   dt_accel_connect_lib(self, "tag", g_cclosure_new(G_CALLBACK(_lib_tagging_tag_show), self, NULL));
  107 }
  108 
  109 static void update(dt_lib_module_t *self, int which)
  110 {
  111   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  112   GList *tags = NULL;
  113   uint32_t count;
  114 
  115   if(which == 0) // tags of selected images
  116   {
  117     int imgsel = dt_control_get_mouse_over_id();
  118     d->imgsel = imgsel;
  119     count = dt_tag_get_attached(imgsel, &tags, FALSE);
  120   }
  121   else // related tags of typed text
  122     count = dt_tag_get_suggestions(d->keyword, &tags);
  123 
  124   GtkTreeIter iter;
  125   GtkTreeView *view;
  126   if(which == 0)
  127     view = d->current;
  128   else
  129     view = d->related;
  130   GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(view));
  131   g_object_ref(model);
  132   gtk_tree_view_set_model(GTK_TREE_VIEW(view), NULL);
  133   gtk_list_store_clear(GTK_LIST_STORE(model));
  134 
  135   if(count > 0 && tags)
  136   {
  137     GList *tag = tags;
  138     do
  139     {
  140       gtk_list_store_append(GTK_LIST_STORE(model), &iter);
  141       gtk_list_store_set(GTK_LIST_STORE(model), &iter, DT_LIB_TAGGING_COL_TAG, ((dt_tag_t *)tag->data)->tag,
  142                          DT_LIB_TAGGING_COL_ID, ((dt_tag_t *)tag->data)->id, -1);
  143     } while((tag = g_list_next(tag)) != NULL);
  144   }
  145 
  146   // Free result...
  147   dt_tag_free_result(&tags);
  148 
  149   gtk_tree_view_set_model(GTK_TREE_VIEW(view), model);
  150   g_object_unref(model);
  151 }
  152 
  153 static void set_keyword(dt_lib_module_t *self, dt_lib_tagging_t *d)
  154 {
  155   const gchar *beg = g_strrstr(gtk_entry_get_text(d->entry), ",");
  156   if(!beg)
  157     beg = gtk_entry_get_text(d->entry);
  158   else
  159   {
  160     if(*beg == ',') beg++;
  161     if(*beg == ' ') beg++;
  162   }
  163   snprintf(d->keyword, sizeof(d->keyword), "%s", beg);
  164   update(self, 1);
  165 }
  166 
  167 static void attach_selected_tag(dt_lib_module_t *self, dt_lib_tagging_t *d)
  168 {
  169   GtkTreeIter iter;
  170   GtkTreeModel *model = NULL;
  171   GtkTreeView *view = d->related;
  172   GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  173   if(!gtk_tree_selection_get_selected(selection, &model, &iter)
  174      && !gtk_tree_model_get_iter_first(model, &iter))
  175     return;
  176   guint tagid;
  177   gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
  178 
  179   int imgsel = -1;
  180   if(tagid <= 0) return;
  181 
  182   imgsel = dt_view_get_image_to_act_on();
  183 
  184   dt_tag_attach(tagid, imgsel);
  185   dt_image_synch_xmp(imgsel);
  186 
  187   dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  188 }
  189 
  190 static void detach_selected_tag(dt_lib_module_t *self, dt_lib_tagging_t *d)
  191 {
  192   GtkTreeIter iter;
  193   GtkTreeModel *model = NULL;
  194   GtkTreeView *view = d->current;
  195   GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  196   if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
  197   guint tagid;
  198   gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
  199 
  200   int imgsel = -1;
  201   if(tagid <= 0) return;
  202 
  203   imgsel = dt_view_get_image_to_act_on();
  204   GList *affected_images = dt_tag_get_images_from_selection(imgsel, tagid);
  205 
  206   dt_tag_detach(tagid, imgsel);
  207 
  208   // we have to check the conf option as dt_image_synch_xmp() doesn't when called for a single image
  209   if(dt_conf_get_bool("write_sidecar_files"))
  210   {
  211     for(GList *image_iter = affected_images; image_iter; image_iter = g_list_next(image_iter))
  212     {
  213       int imgid = GPOINTER_TO_INT(image_iter->data);
  214       dt_image_synch_xmp(imgid);
  215     }
  216   }
  217 
  218   g_list_free(affected_images);
  219 
  220   dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  221 }
  222 
  223 static void attach_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, gpointer user_data)
  224 {
  225   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  226   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  227   attach_selected_tag(self, d);
  228   update(self, 0);
  229 }
  230 
  231 static void detach_activated(GtkTreeView *view, GtkTreePath *path, GtkTreeViewColumn *col, gpointer user_data)
  232 {
  233   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  234   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  235   detach_selected_tag(self, d);
  236   update(self, 0);
  237 }
  238 
  239 static void attach_button_clicked(GtkButton *button, gpointer user_data)
  240 {
  241   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  242   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  243   attach_selected_tag(self, d);
  244   update(self, 0);
  245 }
  246 
  247 static void detach_button_clicked(GtkButton *button, gpointer user_data)
  248 {
  249   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  250   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  251   detach_selected_tag(self, d);
  252   update(self, 0);
  253 }
  254 
  255 static void new_button_clicked(GtkButton *button, gpointer user_data)
  256 {
  257   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  258   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  259   const gchar *tag = gtk_entry_get_text(d->entry);
  260 
  261   /** attach tag to selected images  */
  262   dt_tag_attach_string_list(tag, -1);
  263   dt_image_synch_xmp(-1);
  264 
  265   update(self, 1);
  266   update(self, 0);
  267 
  268   /** clear input box */
  269   gtk_entry_set_text(d->entry, "");
  270 
  271   dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  272 }
  273 
  274 static void entry_activated(GtkButton *button, gpointer user_data)
  275 {
  276   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  277   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  278   const gchar *tag = gtk_entry_get_text(d->entry);
  279   if(!tag || tag[0] == '\0') return;
  280 
  281   /** attach tag to selected images  */
  282   dt_tag_attach_string_list(tag, -1);
  283   dt_image_synch_xmp(-1);
  284 
  285   update(self, 1);
  286   update(self, 0);
  287   gtk_entry_set_text(d->entry, "");
  288 
  289   dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  290 }
  291 
  292 static void tag_name_changed(GtkEntry *entry, gpointer user_data)
  293 {
  294   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  295   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  296   set_keyword(self, d);
  297 }
  298 
  299 static void delete_button_clicked(GtkButton *button, gpointer user_data)
  300 {
  301   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  302   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  303 
  304   int res = GTK_RESPONSE_YES;
  305 
  306   guint tagid;
  307   GtkTreeIter iter;
  308   GtkTreeModel *model = NULL;
  309   GtkTreeView *view = d->related;
  310   GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  311   if(!gtk_tree_selection_get_selected(selection, &model, &iter)) return;
  312   gtk_tree_model_get(model, &iter, DT_LIB_TAGGING_COL_ID, &tagid, -1);
  313 
  314   // First check how many images are affected by the remove
  315   int count = dt_tag_remove(tagid, FALSE);
  316   if(count > 0 && dt_conf_get_bool("plugins/lighttable/tagging/ask_before_delete_tag"))
  317   {
  318     GtkWidget *dialog;
  319     GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  320     gchar *tagname = dt_tag_get_name(tagid);
  321     dialog = gtk_message_dialog_new(
  322         GTK_WINDOW(win), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO,
  323         ngettext("do you really want to delete the tag `%s'?\n%d image is assigned this tag!",
  324                  "do you really want to delete the tag `%s'?\n%d images are assigned this tag!", count),
  325         tagname, count);
  326 #ifdef GDK_WINDOWING_QUARTZ
  327     dt_osx_disallow_fullscreen(dialog);
  328 #endif
  329     gtk_window_set_title(GTK_WINDOW(dialog), _("delete tag?"));
  330     res = gtk_dialog_run(GTK_DIALOG(dialog));
  331     gtk_widget_destroy(dialog);
  332     free(tagname);
  333   }
  334   if(res != GTK_RESPONSE_YES) return;
  335 
  336   GList *tagged_images = NULL;
  337   sqlite3_stmt *stmt;
  338   DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), "SELECT imgid FROM main.tagged_images WHERE tagid=?1",
  339                               -1, &stmt, NULL);
  340   DT_DEBUG_SQLITE3_BIND_INT(stmt, 1, tagid);
  341   while(sqlite3_step(stmt) == SQLITE_ROW)
  342   {
  343     tagged_images = g_list_append(tagged_images, GINT_TO_POINTER(sqlite3_column_int(stmt, 0)));
  344   }
  345   sqlite3_finalize(stmt);
  346 
  347   dt_tag_remove(tagid, TRUE);
  348 
  349   GList *list_iter;
  350   if((list_iter = g_list_first(tagged_images)) != NULL)
  351   {
  352     do
  353     {
  354       dt_image_synch_xmp(GPOINTER_TO_INT(list_iter->data));
  355     } while((list_iter = g_list_next(list_iter)) != NULL);
  356   }
  357   g_list_free(g_list_first(tagged_images));
  358 
  359   update(self, 0);
  360   update(self, 1);
  361 
  362   dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  363 }
  364 
  365 static void import_button_clicked(GtkButton *button, gpointer user_data)
  366 {
  367   char *last_dirname = dt_conf_get_string("plugins/lighttable/tagging/last_import_export_location");
  368   if(!last_dirname || !*last_dirname)
  369   {
  370     g_free(last_dirname);
  371     last_dirname = g_strdup(g_get_home_dir());
  372   }
  373 
  374   GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  375   GtkWidget *filechooser = gtk_file_chooser_dialog_new(_("Select a keyword file"), GTK_WINDOW(win),
  376                                                        GTK_FILE_CHOOSER_ACTION_OPEN,
  377                                                        _("_cancel"), GTK_RESPONSE_CANCEL,
  378                                                        _("_import"), GTK_RESPONSE_ACCEPT, (char *)NULL);
  379 #ifdef GDK_WINDOWING_QUARTZ
  380   dt_osx_disallow_fullscreen(filechooser);
  381 #endif
  382   gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
  383   gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);
  384 
  385   if(gtk_dialog_run(GTK_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  386   {
  387     char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
  388     char *dirname = g_path_get_dirname(filename);
  389     dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
  390     ssize_t count = dt_tag_import(filename);
  391     if(count < 0)
  392       dt_control_log(_("error importing tags"));
  393     else
  394       dt_control_log(_("%zd tags imported"), count);
  395     g_free(filename);
  396     g_free(dirname);
  397   }
  398 
  399   g_free(last_dirname);
  400   gtk_widget_destroy(filechooser);
  401 }
  402 
  403 static void export_button_clicked(GtkButton *button, gpointer user_data)
  404 {
  405   GDateTime *now = g_date_time_new_now_local();
  406   char *export_filename = g_date_time_format(now, "darktable_tags_%F_%R.txt");
  407   char *last_dirname = dt_conf_get_string("plugins/lighttable/tagging/last_import_export_location");
  408   if(!last_dirname || !*last_dirname)
  409   {
  410     g_free(last_dirname);
  411     last_dirname = g_strdup(g_get_home_dir());
  412   }
  413 
  414   GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  415   GtkWidget *filechooser = gtk_file_chooser_dialog_new(_("Select file to export to"), GTK_WINDOW(win),
  416                                                        GTK_FILE_CHOOSER_ACTION_SAVE,
  417                                                        _("_cancel"), GTK_RESPONSE_CANCEL,
  418                                                        _("_export"), GTK_RESPONSE_ACCEPT, (char *)NULL);
  419 #ifdef GDK_WINDOWING_QUARTZ
  420   dt_osx_disallow_fullscreen(filechooser);
  421 #endif
  422   gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(filechooser), TRUE);
  423   gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), last_dirname);
  424   gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(filechooser), export_filename);
  425 
  426   if(gtk_dialog_run(GTK_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  427   {
  428     char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
  429     char *dirname = g_path_get_dirname(filename);
  430     dt_conf_set_string("plugins/lighttable/tagging/last_import_export_location", dirname);
  431     ssize_t count = dt_tag_export(filename);
  432     if(count < 0)
  433       dt_control_log(_("error exporting tags"));
  434     else
  435       dt_control_log(_("%zd tags exported"), count);
  436     g_free(filename);
  437     g_free(dirname);
  438   }
  439 
  440   g_date_time_unref(now);
  441   g_free(last_dirname);
  442   g_free(export_filename);
  443   gtk_widget_destroy(filechooser);
  444 }
  445 
  446 void gui_reset(dt_lib_module_t *self)
  447 {
  448   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  449   // clear entry box and query
  450   gtk_entry_set_text(d->entry, "");
  451   set_keyword(self, d);
  452 }
  453 
  454 int position()
  455 {
  456   return 500;
  457 }
  458 
  459 static void _lib_tagging_redraw_callback(gpointer instance, gpointer user_data)
  460 {
  461   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  462   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  463   int imgsel = dt_control_get_mouse_over_id();
  464   if(imgsel != d->imgsel) update(self, 0);
  465 }
  466 
  467 static void _lib_tagging_tags_changed_callback(gpointer instance, gpointer user_data)
  468 {
  469   dt_lib_module_t *self = (dt_lib_module_t *)user_data;
  470   update(self, 1);
  471 }
  472 
  473 void gui_init(dt_lib_module_t *self)
  474 {
  475   dt_lib_tagging_t *d = (dt_lib_tagging_t *)malloc(sizeof(dt_lib_tagging_t));
  476   self->data = (void *)d;
  477   d->imgsel = -1;
  478 
  479   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5);
  480   dt_gui_add_help_link(self->widget, dt_get_help_url(self->plugin_name));
  481   //   gtk_widget_set_size_request(self->widget, DT_PIXEL_APPLY_DPI(100), -1);
  482 
  483   GtkBox *box, *hbox;
  484   GtkWidget *button;
  485   GtkWidget *w;
  486   GtkListStore *liststore;
  487 
  488   // left side, current
  489   box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 5));
  490 
  491   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 0);
  492   w = gtk_scrolled_window_new(NULL, NULL);
  493   gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100));
  494   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  495   gtk_box_pack_start(box, w, TRUE, TRUE, 0);
  496   d->current = GTK_TREE_VIEW(gtk_tree_view_new());
  497   gtk_tree_view_set_headers_visible(d->current, FALSE);
  498   liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT);
  499   GtkTreeViewColumn *col = gtk_tree_view_column_new();
  500   gtk_tree_view_append_column(d->current, col);
  501   GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
  502   gtk_tree_view_column_pack_start(col, renderer, TRUE);
  503   gtk_tree_view_column_add_attribute(col, renderer, "text", DT_LIB_TAGGING_COL_TAG);
  504   gtk_tree_selection_set_mode(gtk_tree_view_get_selection(d->current), GTK_SELECTION_SINGLE);
  505   gtk_tree_view_set_model(d->current, GTK_TREE_MODEL(liststore));
  506   g_object_unref(liststore);
  507   gtk_widget_set_tooltip_text(GTK_WIDGET(d->current), _("attached tags,\ndoubleclick to detach"));
  508   dt_gui_add_help_link(GTK_WIDGET(d->current), "tagging.html#tagging_usage");
  509   g_signal_connect(G_OBJECT(d->current), "row-activated", G_CALLBACK(detach_activated), (gpointer)self);
  510   gtk_container_add(GTK_CONTAINER(w), GTK_WIDGET(d->current));
  511 
  512   // attach/detach buttons
  513   hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5));
  514 
  515   button = gtk_button_new_with_label(_("attach"));
  516   d->attach_button = button;
  517   gtk_widget_set_tooltip_text(button, _("attach tag to all selected images"));
  518   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  519   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  520   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(attach_button_clicked), (gpointer)self);
  521 
  522   button = gtk_button_new_with_label(_("detach"));
  523   d->detach_button = button;
  524   gtk_widget_set_tooltip_text(button, _("detach tag from all selected images"));
  525   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  526   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(detach_button_clicked), (gpointer)self);
  527   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  528 
  529   gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
  530 
  531   // right side, related
  532   box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 5));
  533   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(box), TRUE, TRUE, 5);
  534 
  535   // text entry and new button
  536   w = gtk_entry_new();
  537   gtk_widget_set_tooltip_text(w, _("enter tag name"));
  538   dt_gui_add_help_link(w, "tagging.html#tagging_usage");
  539   gtk_box_pack_start(box, w, TRUE, TRUE, 0);
  540   gtk_widget_add_events(GTK_WIDGET(w), GDK_KEY_RELEASE_MASK);
  541   // g_signal_connect(G_OBJECT(w), "key-release-event",
  542   g_signal_connect(G_OBJECT(w), "changed", G_CALLBACK(tag_name_changed), (gpointer)self);
  543   g_signal_connect(G_OBJECT(w), "activate", G_CALLBACK(entry_activated), (gpointer)self);
  544   d->entry = GTK_ENTRY(w);
  545   dt_gui_key_accel_block_on_focus_connect(GTK_WIDGET(d->entry));
  546 
  547   // related tree view
  548   w = gtk_scrolled_window_new(NULL, NULL);
  549   gtk_widget_set_size_request(w, -1, DT_PIXEL_APPLY_DPI(100));
  550   gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
  551   gtk_box_pack_start(box, w, TRUE, TRUE, 0);
  552   d->related = GTK_TREE_VIEW(gtk_tree_view_new());
  553   gtk_tree_view_set_headers_visible(d->related, FALSE);
  554   liststore = gtk_list_store_new(DT_LIB_TAGGING_NUM_COLS, G_TYPE_STRING, G_TYPE_UINT);
  555   col = gtk_tree_view_column_new();
  556   gtk_tree_view_append_column(d->related, col);
  557   renderer = gtk_cell_renderer_text_new();
  558   gtk_tree_view_column_pack_start(col, renderer, TRUE);
  559   gtk_tree_view_column_add_attribute(col, renderer, "text", DT_LIB_TAGGING_COL_TAG);
  560   gtk_tree_selection_set_mode(gtk_tree_view_get_selection(d->related), GTK_SELECTION_SINGLE);
  561   gtk_tree_view_set_model(d->related, GTK_TREE_MODEL(liststore));
  562   g_object_unref(liststore);
  563   gtk_widget_set_tooltip_text(GTK_WIDGET(d->related), _("related tags,\ndoubleclick to attach"));
  564   dt_gui_add_help_link(GTK_WIDGET(d->related), "tagging.html#tagging_usage");
  565   g_signal_connect(G_OBJECT(d->related), "row-activated", G_CALLBACK(attach_activated), (gpointer)self);
  566   gtk_container_add(GTK_CONTAINER(w), GTK_WIDGET(d->related));
  567 
  568   // attach and delete buttons
  569   hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5));
  570 
  571   button = gtk_button_new_with_label(_("new"));
  572   d->new_button = button;
  573   gtk_widget_set_tooltip_text(button, _("create a new tag with the\nname you entered"));
  574   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  575   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  576   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(new_button_clicked), (gpointer)self);
  577 
  578   button = gtk_button_new_with_label(_("delete"));
  579   d->delete_button = button;
  580   gtk_widget_set_tooltip_text(button, _("delete selected tag"));
  581   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  582   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  583   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(delete_button_clicked), (gpointer)self);
  584 
  585   button = gtk_button_new_with_label(C_("verb", "import"));
  586   d->import_button = button;
  587   gtk_widget_set_tooltip_text(button, _("import tags from a Lightroom keyword file"));
  588   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  589   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  590   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(import_button_clicked), (gpointer)self);
  591 
  592   button = gtk_button_new_with_label(C_("verb", "export"));
  593   d->export_button = button;
  594   gtk_widget_set_tooltip_text(button, _("export all tags to a Lightroom keyword file"));
  595   dt_gui_add_help_link(button, "tagging.html#tagging_usage");
  596   gtk_box_pack_start(hbox, button, FALSE, TRUE, 0);
  597   g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(export_button_clicked), (gpointer)self);
  598 
  599   gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0);
  600 
  601   // add entry completion
  602   GtkEntryCompletion *completion = gtk_entry_completion_new();
  603   gtk_entry_completion_set_model(completion, gtk_tree_view_get_model(GTK_TREE_VIEW(d->related)));
  604   gtk_entry_completion_set_text_column(completion, 0);
  605   gtk_entry_completion_set_inline_completion(completion, TRUE);
  606   gtk_entry_set_completion(d->entry, completion);
  607 
  608   /* connect to mouse over id */
  609   dt_control_signal_connect(darktable.signals, DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE,
  610                             G_CALLBACK(_lib_tagging_redraw_callback), self);
  611   dt_control_signal_connect(darktable.signals, DT_SIGNAL_TAG_CHANGED,
  612                             G_CALLBACK(_lib_tagging_tags_changed_callback), self);
  613 
  614   update(self, 0);
  615   set_keyword(self, d);
  616 }
  617 
  618 void gui_cleanup(dt_lib_module_t *self)
  619 {
  620   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  621   dt_gui_key_accel_block_on_focus_disconnect(GTK_WIDGET(d->entry));
  622   dt_control_signal_disconnect(darktable.signals, G_CALLBACK(_lib_tagging_redraw_callback), self);
  623   dt_control_signal_disconnect(darktable.signals, G_CALLBACK(_lib_tagging_tags_changed_callback), self);
  624   free(self->data);
  625   self->data = NULL;
  626 }
  627 
  628 // http://stackoverflow.com/questions/4631388/transparent-floating-gtkentry
  629 static gboolean _lib_tagging_tag_key_press(GtkWidget *entry, GdkEventKey *event, dt_lib_module_t *self)
  630 {
  631   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  632   switch(event->keyval)
  633   {
  634     case GDK_KEY_Escape:
  635       gtk_widget_destroy(d->floating_tag_window);
  636       return TRUE;
  637     case GDK_KEY_Tab:
  638       return TRUE;
  639     case GDK_KEY_Return:
  640     case GDK_KEY_KP_Enter:
  641     {
  642       const gchar *tag = gtk_entry_get_text(GTK_ENTRY(entry));
  643       // both these functions can deal with -1 for all selected images. no need for extra code in here!
  644       dt_tag_attach_string_list(tag, d->floating_tag_imgid);
  645       dt_image_synch_xmp(d->floating_tag_imgid);
  646       update(self, 1);
  647       update(self, 0);
  648       gtk_widget_destroy(d->floating_tag_window);
  649       dt_control_signal_raise(darktable.signals, DT_SIGNAL_TAG_CHANGED);
  650       return TRUE;
  651     }
  652   }
  653   return FALSE; /* event not handled */
  654 }
  655 
  656 static gboolean _lib_tagging_tag_destroy(GtkWidget *widget, GdkEvent *event, gpointer user_data)
  657 {
  658   gtk_widget_destroy(GTK_WIDGET(user_data));
  659   return FALSE;
  660 }
  661 
  662 static gboolean _match_selected_func(GtkEntryCompletion *completion, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data)
  663 {
  664   char *tag = NULL;
  665   int column = gtk_entry_completion_get_text_column(completion);
  666 
  667   if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING) return TRUE;
  668 
  669   GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
  670   if(!GTK_IS_EDITABLE(e))
  671   {
  672     return FALSE;
  673   }
  674 
  675   gtk_tree_model_get(model, iter, column, &tag, -1);
  676   
  677   gint cut_off, cur_pos = gtk_editable_get_position(e);
  678 
  679   gchar *currentText = gtk_editable_get_chars(e, 0, -1);
  680   const gchar *lastTag = g_strrstr(currentText, ",");
  681   if(lastTag == NULL)
  682   {
  683     cut_off = 0;
  684   }
  685   else
  686   {
  687     cut_off = (int)(g_utf8_strlen(currentText, -1) - g_utf8_strlen(lastTag, -1))+1;
  688   }
  689   free(currentText);
  690 
  691   gtk_editable_delete_text(e, cut_off, cur_pos);
  692   cur_pos = cut_off;
  693   gtk_editable_insert_text(e, tag, -1, &cur_pos);
  694   gtk_editable_set_position(e, cur_pos);
  695   return TRUE;
  696 }      
  697 
  698 static gboolean _completion_match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter,
  699                                        gpointer user_data)
  700 {
  701   gboolean res = FALSE;
  702 
  703   GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
  704 
  705   if(!GTK_IS_EDITABLE(e))
  706   {
  707     return FALSE;
  708   }
  709   
  710   gint cur_pos = gtk_editable_get_position(e);
  711   gboolean onLastTag = (g_strstr_len(&key[cur_pos], -1, ",") == NULL);
  712   if(!onLastTag)
  713   {
  714     return FALSE;
  715   }
  716   
  717   
  718   char *tag = NULL;
  719   GtkTreeModel *model = gtk_entry_completion_get_model(completion);
  720   int column = gtk_entry_completion_get_text_column(completion);
  721 
  722   if(gtk_tree_model_get_column_type(model, column) != G_TYPE_STRING) return FALSE;
  723 
  724   gtk_tree_model_get(model, iter, column, &tag, -1);
  725 
  726   const gchar *lastTag = g_strrstr(key, ",");
  727   if(lastTag != NULL)
  728   {
  729     lastTag++;
  730   }
  731   else
  732   {
  733     lastTag = key;
  734   }
  735   if(lastTag[0] == '\0' && key[0] != '\0')
  736   {
  737     return FALSE;
  738   }
  739 
  740   if(tag)
  741   {
  742     char *normalized = g_utf8_normalize(tag, -1, G_NORMALIZE_ALL);
  743     if(normalized)
  744     {
  745       char *casefold = g_utf8_casefold(normalized, -1);
  746       if(casefold)
  747       {
  748         res = g_strstr_len(casefold, -1, lastTag) != NULL;
  749       }
  750       g_free(casefold);
  751     }
  752     g_free(normalized);
  753     g_free(tag);
  754   }
  755 
  756   return res;
  757 }
  758 
  759 static gboolean _lib_tagging_tag_show(GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval,
  760                                       GdkModifierType modifier, dt_lib_module_t *self)
  761 {
  762   int mouse_over_id = -1;
  763   int zoom = dt_conf_get_int("plugins/lighttable/images_in_row");
  764 
  765   // the order is:
  766   // if(zoom == 1) => currently shown image
  767   // else if(selection not empty) => selected images
  768   // else if(cursor over image) => hovered image
  769   // else => return
  770   if(zoom == 1 || dt_collection_get_selected_count(darktable.collection) == 0)
  771   {
  772     mouse_over_id = dt_control_get_mouse_over_id();
  773     if(mouse_over_id < 0) return TRUE;
  774   }
  775 
  776   dt_lib_tagging_t *d = (dt_lib_tagging_t *)self->data;
  777   d->floating_tag_imgid = mouse_over_id;
  778 
  779   gint x, y;
  780   gint px, py, w, h;
  781   GtkWidget *window = dt_ui_main_window(darktable.gui->ui);
  782   GtkWidget *center = dt_ui_center(darktable.gui->ui);
  783   gdk_window_get_origin(gtk_widget_get_window(center), &px, &py);
  784 
  785   w = gdk_window_get_width(gtk_widget_get_window(center));
  786   h = gdk_window_get_height(gtk_widget_get_window(center));
  787 
  788   x = px + 0.5 * (w - FLOATING_ENTRY_WIDTH);
  789   y = py + h - 50;
  790 
  791   /* put the floating box at the mouse pointer */
  792   //   gint pointerx, pointery;
  793   //   GdkDevice *device =
  794   //   gdk_device_manager_get_client_pointer(gdk_display_get_device_manager(gtk_widget_get_display(widget)));
  795   //   gdk_window_get_device_position (gtk_widget_get_window (widget), device, &pointerx, &pointery, NULL);
  796   //   x = px + pointerx + 1;
  797   //   y = py + pointery + 1;
  798 
  799   d->floating_tag_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  800 #ifdef GDK_WINDOWING_QUARTZ
  801   dt_osx_disallow_fullscreen(d->floating_tag_window);
  802 #endif
  803   /* stackoverflow.com/questions/1925568/how-to-give-keyboard-focus-to-a-pop-up-gtk-window */
  804   gtk_widget_set_can_focus(d->floating_tag_window, TRUE);
  805   gtk_window_set_decorated(GTK_WINDOW(d->floating_tag_window), FALSE);
  806   gtk_window_set_type_hint(GTK_WINDOW(d->floating_tag_window), GDK_WINDOW_TYPE_HINT_POPUP_MENU);
  807   gtk_window_set_transient_for(GTK_WINDOW(d->floating_tag_window), GTK_WINDOW(window));
  808   gtk_widget_set_opacity(d->floating_tag_window, 0.8);
  809   gtk_window_move(GTK_WINDOW(d->floating_tag_window), x, y);
  810 
  811 
  812   GtkWidget *entry = gtk_entry_new();
  813   gtk_widget_set_size_request(entry, FLOATING_ENTRY_WIDTH, -1);
  814   gtk_widget_add_events(entry, GDK_FOCUS_CHANGE_MASK);
  815 
  816   GtkEntryCompletion *completion = gtk_entry_completion_new();
  817   gtk_entry_completion_set_model(completion, gtk_tree_view_get_model(GTK_TREE_VIEW(d->related)));
  818   gtk_entry_completion_set_text_column(completion, 0);
  819   gtk_entry_completion_set_inline_completion(completion, TRUE);
  820   gtk_entry_completion_set_popup_set_width(completion, FALSE);
  821   g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(_match_selected_func), self);
  822   gtk_entry_completion_set_match_func(completion, _completion_match_func, NULL, NULL);
  823   gtk_entry_set_completion(GTK_ENTRY(entry), completion);
  824 
  825   gtk_editable_select_region(GTK_EDITABLE(entry), 0, -1);
  826   gtk_container_add(GTK_CONTAINER(d->floating_tag_window), entry);
  827   g_signal_connect(entry, "focus-out-event", G_CALLBACK(_lib_tagging_tag_destroy), d->floating_tag_window);
  828   g_signal_connect(entry, "key-press-event", G_CALLBACK(_lib_tagging_tag_key_press), self);
  829 
  830   gtk_widget_show_all(d->floating_tag_window);
  831   gtk_widget_grab_focus(entry);
  832   gtk_window_present(GTK_WINDOW(d->floating_tag_window));
  833 
  834   return TRUE;
  835 }
  836 
  837 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
  838 // vim: shiftwidth=2 expandtab tabstop=2 cindent
  839 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;