# HG changeset patch # User Colin Clark # Date 1535286762 -3600 # Node ID c9eded8ad4e0d2b99aa316f3a0b4a347755a21df # Parent 938790283e32d85e6695f734f423ba98e3e96d19 Fix #305: Faster Tagging (Keywords) https://github.com/BestImageViewer/geeqie/issues/305 Keyword autocompletion - see the Info Sidebar section in the Help file for details diff -r 938790283e32 -r c9eded8ad4e0 doc/docbook/GuideOptionsKeywords.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/docbook/GuideOptionsKeywords.xml Sun Aug 26 13:32:42 2018 +0100 @@ -0,0 +1,20 @@ + +
+ Keywords + This section describes the keywords list used for autocompletion. + +
+ Keyword Search + + Pressing the Search button will open a dialog which permits a recursive search to be made for keywords already attached to images. The result of the search is automatically appended to the existing list. +
+
+ Keyword List + + + The list shows all keywords currently used for autocompletion. Text may be copy-pasted into the list or deleted from the list. + + When the list is saved, duplicates will autmatically be removed and the list sorted into alphabetical order. + +
+
diff -r 938790283e32 -r c9eded8ad4e0 doc/docbook/GuideOptionsMain.xml --- a/doc/docbook/GuideOptionsMain.xml Tue Aug 21 17:02:20 2018 +0100 +++ b/doc/docbook/GuideOptionsMain.xml Sun Aug 26 13:32:42 2018 +0100 @@ -27,6 +27,7 @@ + diff -r 938790283e32 -r c9eded8ad4e0 doc/docbook/GuideSidebarsInfo.xml --- a/doc/docbook/GuideSidebarsInfo.xml Tue Aug 21 17:02:20 2018 +0100 +++ b/doc/docbook/GuideSidebarsInfo.xml Sun Aug 26 13:32:42 2018 +0100 @@ -165,6 +165,25 @@ +
+ Keyword Autocompletion + + The text box at the bottom of the keywords pane is used for autocompletion. Any keywords typed into the standard keyword box or the autocompletion box will be remembered as candidates for future autocompletion. + + Frequently used sets of keywords can be entered as comma-delimited lists. + + The list of keywords used for autocompletion can be edited on the + Keywords + tab of the Preferences dialog. + + The menu action "Keyword autocomplete", set to + Alt + K + by default, will shift the keyboard focus to the autocomplete box. Pressing + Alt + K + a second time will shift the keyboard focus back to the previous object. + + +
List panes diff -r 938790283e32 -r c9eded8ad4e0 src/bar_keywords.c --- a/src/bar_keywords.c Tue Aug 21 17:02:20 2018 +0100 +++ b/src/bar_keywords.c Sun Aug 26 13:32:42 2018 +0100 @@ -37,11 +37,17 @@ #include "rcfile.h" #include "layout.h" #include "dnd.h" +#include "secure_save.h" //static void bar_pane_keywords_keyword_update_all(void); static void bar_pane_keywords_changed(GtkTextBuffer *buffer, gpointer data); +static void autocomplete_keywords_list_load(const gchar *path); +static GtkListStore *keyword_store = NULL; +static gboolean autocomplete_keywords_list_save(gchar *path); +static gboolean autocomplete_activate_cb(GtkWidget *widget, gpointer data); + /* *------------------------------------------------------------------- * keyword / comment utils @@ -138,6 +144,8 @@ gint height; GList *expanded_rows; + + GtkWidget *autocomplete; }; typedef struct _ConfDialogData ConfDialogData; @@ -336,6 +344,10 @@ if (gtk_widget_has_focus(pkd->keyword_view)) return gtk_widget_event(pkd->keyword_view, event); + if (gtk_widget_has_focus(pkd->autocomplete)) + { + return gtk_widget_event(pkd->autocomplete, event); + } return FALSE; } @@ -1430,6 +1442,10 @@ static void bar_pane_keywords_destroy(GtkWidget *widget, gpointer data) { PaneKeywordsData *pkd = data; + gchar *path; + + path = g_build_filename(get_rc_dir(), "keywords", NULL); + autocomplete_keywords_list_save(path); string_list_free(pkd->expanded_rows); if (pkd->click_tpath) gtk_tree_path_free(pkd->click_tpath); @@ -1446,13 +1462,15 @@ static GtkWidget *bar_pane_keywords_new(const gchar *id, const gchar *title, const gchar *key, gboolean expanded, gint height) { PaneKeywordsData *pkd; - GtkWidget *hbox; + GtkWidget *hbox, *vbox; GtkWidget *scrolled; GtkTextBuffer *buffer; GtkTreeModel *store; GtkTreeViewColumn *column; GtkCellRenderer *renderer; GtkTreeIter iter; + GtkEntryCompletion *completion; + gchar *path; pkd = g_new0(PaneKeywordsData, 1); @@ -1471,9 +1489,11 @@ pkd->expand_checked = TRUE; pkd->expanded_rows = NULL; + vbox = gtk_vbox_new(FALSE, PREF_PAD_GAP); hbox = gtk_hbox_new(FALSE, PREF_PAD_GAP); + gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0); - pkd->widget = hbox; + pkd->widget = vbox; g_object_set_data(G_OBJECT(pkd->widget), "pane_data", pkd); g_signal_connect(G_OBJECT(pkd->widget), "destroy", G_CALLBACK(bar_pane_keywords_destroy), pkd); @@ -1504,6 +1524,26 @@ gtk_box_pack_start(GTK_BOX(hbox), scrolled, TRUE, TRUE, 0); gtk_widget_show(scrolled); + pkd->autocomplete = gtk_entry_new(); + gtk_box_pack_end(GTK_BOX(vbox), pkd->autocomplete, FALSE, FALSE, 0); + gtk_widget_show(pkd->autocomplete); + gtk_widget_show(vbox); + gtk_widget_set_tooltip_text(pkd->autocomplete, "Keyword autocomplete"); + + path = g_build_filename(get_rc_dir(), "keywords", NULL); + autocomplete_keywords_list_load(path); + + completion = gtk_entry_completion_new(); + gtk_entry_set_completion(GTK_ENTRY(pkd->autocomplete), completion); + gtk_entry_completion_set_inline_completion(completion, TRUE); + gtk_entry_completion_set_inline_selection(completion, TRUE); + g_object_unref(completion); + + gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(keyword_store)); + gtk_entry_completion_set_text_column(completion, 0); + + g_signal_connect(G_OBJECT(pkd->autocomplete), "activate", + G_CALLBACK(autocomplete_activate_cb), pkd); if (!keyword_tree || !gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_tree), &iter)) { @@ -1687,4 +1727,258 @@ log_printf("unknown attribute %s = %s\n", option, value); } } + +/* + *----------------------------------------------------------------------------- + * Autocomplete keywords + *----------------------------------------------------------------------------- + */ + +static gboolean autocomplete_activate_cb(GtkWidget *widget, gpointer data) +{ + PaneKeywordsData *pkd = data; + gchar *entry_text; + GtkTextBuffer *buffer; + GtkTextIter iter; + GtkTreeIter iter_t; + gchar *kw_cr; + gchar *kw_split; + gboolean valid; + gboolean found = FALSE; + gchar *string; + + entry_text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pkd->autocomplete))); + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(pkd->keyword_view)); + + kw_split = strtok(entry_text, ","); + while (kw_split != NULL) + { + kw_cr = g_strconcat(kw_split, "\n", NULL); + g_strchug(kw_cr); + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_buffer_insert(buffer, &iter, kw_cr, -1); + + kw_split = strtok(NULL, ","); + g_free(kw_cr); + } + + g_free(entry_text); + entry_text = g_strdup(gtk_entry_get_text(GTK_ENTRY(pkd->autocomplete))); + + gtk_entry_set_text(GTK_ENTRY(pkd->autocomplete), ""); + + valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_store), &iter_t); + while (valid) + { + gtk_tree_model_get (GTK_TREE_MODEL(keyword_store), &iter_t, 0, &string, -1); + if (g_strcmp0(entry_text, string) == 0) + { + found = TRUE; + break; + } + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_store), &iter_t); + } + + if (!found) + { + gtk_list_store_append (keyword_store, &iter_t); + gtk_list_store_set(keyword_store, &iter_t, 0, entry_text, -1); + } + + g_free(entry_text); + return FALSE; +} + +gint autocomplete_sort_iter_compare_func (GtkTreeModel *model, + GtkTreeIter *a, + GtkTreeIter *b, + gpointer userdata) +{ + gint ret = 0; + gchar *name1, *name2; + + gtk_tree_model_get(model, a, 0, &name1, -1); + gtk_tree_model_get(model, b, 0, &name2, -1); + + if (name1 == NULL || name2 == NULL) + { + if (name1 == NULL && name2 == NULL) + { + ret = 0; + } + else + { + ret = (name1 == NULL) ? -1 : 1; + } + } + else + { + ret = g_utf8_collate(name1,name2); + } + + g_free(name1); + g_free(name2); + + return ret; +} + +static void autocomplete_keywords_list_load(const gchar *path) +{ + FILE *f; + gchar s_buf[1024]; + gchar *pathl; + gint len; + GtkTreeIter iter; + GtkTreeSortable *sortable; + + if (keyword_store) return; + keyword_store = gtk_list_store_new(1, G_TYPE_STRING); + + sortable = GTK_TREE_SORTABLE(keyword_store); + gtk_tree_sortable_set_sort_func(sortable, 0, autocomplete_sort_iter_compare_func, + GINT_TO_POINTER(0), NULL); + + gtk_tree_sortable_set_sort_column_id(sortable, 0, GTK_SORT_ASCENDING); + + pathl = path_from_utf8(path); + f = fopen(pathl, "r"); + g_free(pathl); + + if (!f) + { + log_printf("Warning: keywords file %s not loaded", pathl); + return; + } + + /* first line must start with Keywords comment */ + if (!fgets(s_buf, sizeof(s_buf), f) || + strncmp(s_buf, "#Keywords", 9) != 0) + { + fclose(f); + log_printf("Warning: keywords file %s not loaded", pathl); + return; + } + + while (fgets(s_buf, sizeof(s_buf), f)) + { + if (s_buf[0]=='#') continue; + + len = strlen(s_buf); + if( s_buf[len-1] == '\n' ) + { + s_buf[len-1] = 0; + } + gtk_list_store_append (keyword_store, &iter); + gtk_list_store_set(keyword_store, &iter, 0, g_strdup(s_buf), -1); + } + + fclose(f); +} + +static gboolean autocomplete_keywords_list_save(gchar *path) +{ + SecureSaveInfo *ssi; + gchar *pathl; + gchar *string; + gchar *string_nl; + GtkTreeIter iter; + gboolean valid; + + pathl = path_from_utf8(path); + ssi = secure_open(pathl); + g_free(pathl); + + if (!ssi) + { + log_printf(_("Error: Unable to write keywords list to: %s\n"), path); + return FALSE; + } + + secure_fprintf(ssi, "#Keywords list\n"); + + valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_store), &iter); + + while (valid) + { + gtk_tree_model_get (GTK_TREE_MODEL(keyword_store), &iter, 0, &string, -1); + string_nl = g_strconcat(string, "\n", NULL); + secure_fprintf(ssi, "%s", string_nl); + + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_store), &iter); + + g_free(string_nl); + } + + secure_fprintf(ssi, "#end\n"); + return (secure_close(ssi) == 0); +} + +GList *keyword_list_get() +{ + GList *ret_list = NULL; + gchar *string; + gchar *string_nl; + GtkTreeIter iter; + gboolean valid; + + valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(keyword_store), &iter); + + while (valid) + { + gtk_tree_model_get (GTK_TREE_MODEL(keyword_store), &iter, 0, &string, -1); + string_nl = g_strconcat(string, "\n", NULL); + ret_list = g_list_append(ret_list, string); + valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(keyword_store), &iter); + + g_free(string_nl); + } + + return ret_list; +} + +void keyword_list_set(GList *keyword_list) +{ + GList *ret = NULL; + GtkTreeIter iter; + + if (!keyword_list) return; + + gtk_list_store_clear(keyword_store); + + while (keyword_list) + { + gtk_list_store_append (keyword_store, &iter); + gtk_list_store_set(keyword_store, &iter, 0, keyword_list->data, -1); + + keyword_list = keyword_list->next; + } +} + +gboolean bar_keywords_autocomplete_focus(LayoutWindow *lw) +{ + GtkWidget *pane; + GtkWidget *current_focus; + GList *children; + gboolean ret; + + current_focus = gtk_window_get_focus(GTK_WINDOW(lw->window)); + pane = bar_find_pane_by_id(lw->bar, PANE_KEYWORDS, "keywords"); + + children = gtk_container_get_children(GTK_CONTAINER(pane)); + children = g_list_last(children); + + if (current_focus == children->data) + { + ret = TRUE; + } + else + { + gtk_widget_grab_focus(children->data); + ret = FALSE; + } + + g_list_free(children); + + return ret; +} /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */ diff -r 938790283e32 -r c9eded8ad4e0 src/bar_keywords.h --- a/src/bar_keywords.h Tue Aug 21 17:02:20 2018 +0100 +++ b/src/bar_keywords.h Sun Aug 26 13:32:42 2018 +0100 @@ -28,5 +28,8 @@ /* used in search.c */ GList *keyword_list_pull(GtkWidget *text_widget); +GList *keyword_list_get(); +void keyword_list_set(GList *keyword_list); +gboolean bar_keywords_autocomplete_focus(LayoutWindow *lw); #endif /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */ diff -r 938790283e32 -r c9eded8ad4e0 src/layout_util.c --- a/src/layout_util.c Tue Aug 21 17:02:20 2018 +0100 +++ b/src/layout_util.c Sun Aug 26 13:32:42 2018 +0100 @@ -25,6 +25,7 @@ #include "advanced_exif.h" #include "bar_sort.h" #include "bar.h" +#include "bar_keywords.h" #include "cache_maint.h" #include "collect.h" #include "collect-dlg.h" @@ -1656,6 +1657,25 @@ metadata_write_queue_confirm(TRUE, NULL, NULL); } +static GtkWidget *last_focussed = NULL; +static void layout_menu_keyword_autocomplete_cb(GtkAction *action, gpointer data) +{ + LayoutWindow *lw = data; + GtkWidget *tmp; + gboolean auto_has_focus; + + tmp = gtk_window_get_focus(GTK_WINDOW(lw->window)); + auto_has_focus = bar_keywords_autocomplete_focus(lw); + + if (auto_has_focus) + { + gtk_widget_grab_focus(last_focussed); + } + else + { + last_focussed = tmp; + } +} /* *----------------------------------------------------------------------------- @@ -1888,6 +1908,7 @@ { "Maintenance", PIXBUF_INLINE_ICON_MAINTENANCE, N_("_Cache maintenance..."), NULL, N_("Cache maintenance..."), CB(layout_menu_remove_thumb_cb) }, { "Wallpaper", NULL, N_("Set as _wallpaper"), NULL, N_("Set as wallpaper"), CB(layout_menu_wallpaper_cb) }, { "SaveMetadata", GTK_STOCK_SAVE, N_("_Save metadata"), "S", N_("Save metadata"), CB(layout_menu_metadata_write_cb) }, + { "KeywordAutocomplete", NULL, N_("Keyword autocomplete"), "K", N_("Keyword Autocomplete"), CB(layout_menu_keyword_autocomplete_cb) }, { "ZoomIn", GTK_STOCK_ZOOM_IN, N_("Zoom _in"), "equal", N_("Zoom in"), CB(layout_menu_zoom_in_cb) }, { "ZoomInAlt1", GTK_STOCK_ZOOM_IN, N_("Zoom _in"), "KP_Add", N_("Zoom in"), CB(layout_menu_zoom_in_cb) }, { "ZoomOut", GTK_STOCK_ZOOM_OUT, N_("Zoom _out"), "minus", N_("Zoom out"), CB(layout_menu_zoom_out_cb) }, @@ -2110,6 +2131,7 @@ " " " " " " +" " " " " " " " diff -r 938790283e32 -r c9eded8ad4e0 src/preferences.c --- a/src/preferences.c Tue Aug 21 17:02:20 2018 +0100 +++ b/src/preferences.c Sun Aug 26 13:32:42 2018 +0100 @@ -23,6 +23,7 @@ #include "preferences.h" #include "bar_exif.h" +#include "bar_keywords.h" #include "cache.h" #include "cache_maint.h" #include "editors.h" @@ -36,6 +37,7 @@ #include "img-view.h" #include "layout_config.h" #include "layout_util.h" +#include "metadata.h" #include "pixbuf_util.h" #include "slideshow.h" #include "toolbar.h" @@ -43,6 +45,7 @@ #include "utilops.h" #include "ui_fileops.h" #include "ui_misc.h" +#include "ui_spinner.h" #include "ui_tabcomp.h" #include "ui_utildlg.h" #include "window.h" @@ -63,6 +66,9 @@ static void image_overlay_set_text_colours(); +GtkWidget *keyword_text; +static void config_tab_keywords_save(); + typedef struct _ThumbSize ThumbSize; struct _ThumbSize { @@ -439,6 +445,8 @@ } #endif + config_tab_keywords_save(); + image_options_sync(); if (refresh) @@ -2391,6 +2399,349 @@ gtk_widget_set_tooltip_text(ct_button,"On folder change, read DateTimeOriginal, DateTimeDigitized and Star Rating in the idle loop.\nIf this is not selected, initial loading of the folder will be faster but sorting on these items will be slower"); } +/* keywords tab */ + +typedef struct _KeywordFindData KeywordFindData; +struct _KeywordFindData +{ + GenericDialog *gd; + + GList *list; + GList *list_dir; + + GtkWidget *button_close; + GtkWidget *button_stop; + GtkWidget *button_start; + GtkWidget *progress; + GtkWidget *spinner; + + GtkWidget *group; + GtkWidget *entry; + + gboolean recurse; + + guint idle_id; /* event source id */ +}; + +#define KEYWORD_DIALOG_WIDTH 400 + +static void keywords_find_folder(KeywordFindData *kfd, FileData *dir_fd) +{ + GList *list_d = NULL; + GList *list_f = NULL; + + if (kfd->recurse) + { + filelist_read(dir_fd, &list_f, &list_d); + } + else + { + filelist_read(dir_fd, &list_f, NULL); + } + + list_f = filelist_filter(list_f, FALSE); + list_d = filelist_filter(list_d, TRUE); + + kfd->list = g_list_concat(list_f, kfd->list); + kfd->list_dir = g_list_concat(list_d, kfd->list_dir); +} + +static void keywords_find_reset(KeywordFindData *kfd) +{ + filelist_free(kfd->list); + kfd->list = NULL; + + filelist_free(kfd->list_dir); + kfd->list_dir = NULL; +} + +static void keywords_find_close_cb(GenericDialog *fd, gpointer data) +{ + KeywordFindData *kfd = data; + + if (!gtk_widget_get_sensitive(kfd->button_close)) return; + + keywords_find_reset(kfd); + generic_dialog_close(kfd->gd); + g_free(kfd); +} + +static void keywords_find_finish(KeywordFindData *kfd) +{ + keywords_find_reset(kfd); + + gtk_entry_set_text(GTK_ENTRY(kfd->progress), _("done")); + spinner_set_interval(kfd->spinner, -1); + + gtk_widget_set_sensitive(kfd->group, TRUE); + gtk_widget_set_sensitive(kfd->button_start, TRUE); + gtk_widget_set_sensitive(kfd->button_stop, FALSE); + gtk_widget_set_sensitive(kfd->button_close, TRUE); +} + +static void keywords_find_stop_cb(GenericDialog *fd, gpointer data) +{ + KeywordFindData *kfd = data; + + g_idle_remove_by_data(kfd); + + keywords_find_finish(kfd); +} + +static gboolean keywords_find_file(gpointer data) +{ + KeywordFindData *kfd = data; + GtkTextIter iter; + GtkTextBuffer *buffer; + gchar *tmp; + GList *keywords; + + if (kfd->list) + { + FileData *fd; + + fd = kfd->list->data; + kfd->list = g_list_remove(kfd->list, fd); + + keywords = metadata_read_list(fd, KEYWORD_KEY, METADATA_PLAIN); + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(keyword_text)); + + while (keywords) + { + gtk_text_buffer_get_end_iter(buffer, &iter); + tmp = g_strconcat(keywords->data, "\n", NULL); + gtk_text_buffer_insert(buffer, &iter, tmp, -1); + g_free(tmp); + keywords = keywords->next; + } + + gtk_entry_set_text(GTK_ENTRY(kfd->progress), fd->path); + file_data_unref(fd); + string_list_free(keywords); + + return (TRUE); + } + else if (kfd->list_dir) + { + FileData *fd; + + fd = kfd->list_dir->data; + kfd->list_dir = g_list_remove(kfd->list_dir, fd); + + keywords_find_folder(kfd, fd); + + file_data_unref(fd); + + return TRUE; + } + + keywords_find_finish(kfd); + + return FALSE; +} + +static void keywords_find_start_cb(GenericDialog *fd, gpointer data) +{ + KeywordFindData *kfd = data; + gchar *path; + + if (kfd->list || !gtk_widget_get_sensitive(kfd->button_start)) return; + + path = remove_trailing_slash((gtk_entry_get_text(GTK_ENTRY(kfd->entry)))); + parse_out_relatives(path); + + if (!isdir(path)) + { + warning_dialog(_("Invalid folder"), + _("The specified folder can not be found."), + GTK_STOCK_DIALOG_WARNING, kfd->gd->dialog); + } + else + { + FileData *dir_fd; + + gtk_widget_set_sensitive(kfd->group, FALSE); + gtk_widget_set_sensitive(kfd->button_start, FALSE); + gtk_widget_set_sensitive(kfd->button_stop, TRUE); + gtk_widget_set_sensitive(kfd->button_close, FALSE); + spinner_set_interval(kfd->spinner, SPINNER_SPEED); + + dir_fd = file_data_new_dir(path); + keywords_find_folder(kfd, dir_fd); + file_data_unref(dir_fd); + kfd->idle_id = g_idle_add(keywords_find_file, kfd); + } + + g_free(path); +} + +static void keywords_find_dialog(GtkWidget *widget, const gchar *path) +{ + KeywordFindData *kfd; + GtkWidget *hbox; + GtkWidget *label; + + kfd = g_new0(KeywordFindData, 1); + + kfd->gd = generic_dialog_new(_("Search for keywords"), + "search_for_keywords", + widget, FALSE, + NULL, kfd); + gtk_window_set_default_size(GTK_WINDOW(kfd->gd->dialog), KEYWORD_DIALOG_WIDTH, -1); + kfd->gd->cancel_cb = keywords_find_close_cb; + kfd->button_close = generic_dialog_add_button(kfd->gd, GTK_STOCK_CLOSE, NULL, + keywords_find_close_cb, FALSE); + kfd->button_start = generic_dialog_add_button(kfd->gd, GTK_STOCK_OK, _("S_tart"), + keywords_find_start_cb, FALSE); + kfd->button_stop = generic_dialog_add_button(kfd->gd, GTK_STOCK_STOP, NULL, + keywords_find_stop_cb, FALSE); + gtk_widget_set_sensitive(kfd->button_stop, FALSE); + + generic_dialog_add_message(kfd->gd, NULL, _("Search for keywords"), NULL, FALSE); + + hbox = pref_box_new(kfd->gd->vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, 0); + pref_spacer(hbox, PREF_PAD_INDENT); + kfd->group = pref_box_new(hbox, TRUE, GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP); + + hbox = pref_box_new(kfd->group, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE); + pref_label_new(hbox, _("Folder:")); + + label = tab_completion_new(&kfd->entry, path, NULL, NULL, NULL, NULL); + tab_completion_add_select_button(kfd->entry,_("Select folder") , TRUE); + gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0); + gtk_widget_show(label); + + pref_checkbox_new_int(kfd->group, _("Include subfolders"), FALSE, &kfd->recurse); + + pref_line(kfd->gd->vbox, PREF_PAD_SPACE); + hbox = pref_box_new(kfd->gd->vbox, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_SPACE); + + kfd->progress = gtk_entry_new(); + gtk_widget_set_can_focus(kfd->progress, FALSE); + gtk_editable_set_editable(GTK_EDITABLE(kfd->progress), FALSE); + gtk_entry_set_text(GTK_ENTRY(kfd->progress), _("click start to begin")); + gtk_box_pack_start(GTK_BOX(hbox), kfd->progress, TRUE, TRUE, 0); + gtk_widget_show(kfd->progress); + + kfd->spinner = spinner_new(NULL, -1); + gtk_box_pack_start(GTK_BOX(hbox), kfd->spinner, FALSE, FALSE, 0); + gtk_widget_show(kfd->spinner); + + kfd->list = NULL; + + gtk_widget_show(kfd->gd->dialog); +} + +static void keywords_find_cb(GtkWidget *widget, gpointer data) +{ + const gchar *path = layout_get_path(NULL); + + if (!path || !*path) path = homedir(); + keywords_find_dialog(widget, path); +} + +static void config_tab_keywords_save() +{ + GtkTextIter start, end; + GtkTextBuffer *buffer; + GList *kw_list = NULL; + GList *work; + gchar *buffer_text; + gchar *kw_split; + gboolean found; + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(keyword_text)); + gtk_text_buffer_get_bounds(buffer, &start, &end); + + buffer_text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); + + kw_split = strtok(buffer_text, "\n"); + while (kw_split != NULL) + { + work = kw_list; + found = FALSE; + while (work) + { + if (g_strcmp0(work->data, kw_split) == 0) + { + found = TRUE; + break; + } + work = work->next; + } + if (!found) + { + kw_list = g_list_append(kw_list, g_strdup(kw_split)); + } + kw_split = strtok(NULL, "\n"); + } + + keyword_list_set(kw_list); + + string_list_free(kw_list); + g_free(buffer_text); +} + +static void config_tab_keywords(GtkWidget *notebook) +{ + GtkWidget *hbox; + GtkWidget *vbox; + GtkWidget *group; + GtkWidget *button; + GtkWidget *scrolled; + GtkTextIter iter; + GtkTextBuffer *buffer; + gchar *tmp; + + vbox = scrolled_notebook_page(notebook, _("Keywords")); + + group = pref_group_new(vbox, TRUE, _("Edit keywords autocompletion list"), GTK_ORIENTATION_VERTICAL); + + hbox = pref_box_new(group, FALSE, GTK_ORIENTATION_HORIZONTAL, PREF_PAD_BUTTON_GAP); + + button = pref_button_new(hbox, GTK_STOCK_EXECUTE, _("Search"), FALSE, + G_CALLBACK(keywords_find_cb), keyword_text); + gtk_widget_set_tooltip_text(button, "Search for existing keywords"); + + + keyword_text = gtk_text_view_new(); + gtk_widget_set_size_request(keyword_text, 20, 20); + scrolled = gtk_scrolled_window_new(NULL, NULL); + gtk_box_pack_start(GTK_BOX(group), scrolled, TRUE, TRUE, 0); + gtk_widget_show(scrolled); + + gtk_container_add(GTK_CONTAINER(scrolled), keyword_text); + gtk_widget_show(keyword_text); + + gtk_text_view_set_editable(GTK_TEXT_VIEW(keyword_text), TRUE); + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(keyword_text)); + gtk_text_buffer_create_tag(buffer, "monospace", + "family", "monospace", NULL); + + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(keyword_text), GTK_WRAP_WORD); + gtk_text_buffer_get_start_iter(buffer, &iter); + gtk_text_buffer_create_mark(buffer, "end", &iter, FALSE); + gchar *path; + + path = g_build_filename(get_rc_dir(), "keywords", NULL); + + GList *kwl = keyword_list_get(); + kwl = g_list_first(kwl); + while (kwl) + { + gtk_text_buffer_get_end_iter (buffer, &iter); + tmp = g_strconcat(kwl->data, "\n", NULL); + gtk_text_buffer_insert(buffer, &iter, tmp, -1); + kwl = kwl->next; + g_free(tmp); + } + + gtk_text_buffer_set_modified(buffer, FALSE); + + g_free(path); +} + /* metadata tab */ #ifdef HAVE_LCMS static void intent_menu_cb(GtkWidget *combo, gpointer data) @@ -2878,6 +3229,7 @@ config_tab_accelerators(notebook); config_tab_files(notebook); config_tab_metadata(notebook); + config_tab_keywords(notebook); config_tab_color(notebook); config_tab_stereo(notebook); config_tab_behavior(notebook); diff -r 938790283e32 -r c9eded8ad4e0 web/help/GuideIndex.html --- a/web/help/GuideIndex.html Tue Aug 21 17:02:20 2018 +0100 +++ b/web/help/GuideIndex.html Sun Aug 26 13:32:42 2018 +0100 @@ -592,16 +592,19 @@ 11.6. Metadata
  • -11.7. Color management options +11.7. Keywords +
  • +
  • +11.8. Color management options
  • -11.8. Stereo image management +11.9. Stereo image management
  • -11.9. Behavior Options +11.10. Behavior Options
  • -11.10. Toolbar +11.11. Toolbar
  • diff -r 938790283e32 -r c9eded8ad4e0 web/help/GuideOptionsBehavior.html --- a/web/help/GuideOptionsBehavior.html Tue Aug 21 17:02:20 2018 +0100 +++ b/web/help/GuideOptionsBehavior.html Sun Aug 26 13:32:42 2018 +0100 @@ -435,6 +435,7 @@
  • Keyboard Options
  • Files Options
  • Metadata
  • +
  • Keywords
  • Color management options
  • Stereo image management
  • Behavior Options
  • @@ -459,20 +460,20 @@

    This section describes the options presented under the Behavior Tab of the preferences dialog.

    -

    11.9.1. Delete

    +

    11.10.1. Delete

    Confirm file delete @@ -550,7 +551,7 @@
    -

    11.9.2. Behavior

    +

    11.10.2. Behavior

    Descend folders in tree view @@ -628,7 +629,7 @@
    -

    11.9.3. Navigation

    +

    11.10.3. Navigation

    Progressive keyboard scrolling @@ -669,7 +670,7 @@
    -

    11.9.4. Debugging

    +

    11.10.4. Debugging

    Debug level diff -r 938790283e32 -r c9eded8ad4e0 web/help/GuideOptionsColor.html --- a/web/help/GuideOptionsColor.html Tue Aug 21 17:02:20 2018 +0100 +++ b/web/help/GuideOptionsColor.html Sun Aug 26 13:32:42 2018 +0100 @@ -3,7 +3,7 @@ Color management options - +