43e2aada39ac9d0ca148a0a6a5217ff83392a1c5
[geeqie.git] / src / search-and-run.cc
1 /*
2  * Copyright (C) 2019 The Geeqie Team
3  *
4  * Author: Colin Clark
5  *
6  * This program 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 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program 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 along
17  * with this program; if not, write to the Free Software Foundation, Inc.,
18  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19  */
20
21 #include "main.h"
22 #include "search-and-run.h"
23
24 #include "ui-misc.h"
25 #include "window.h"
26
27 enum {
28         SAR_LABEL,
29         SAR_ACTION,
30         SAR_COUNT
31 };
32
33 typedef struct _SarData SarData;
34 struct _SarData
35 {
36         GtkWidget *window;
37         GtkWidget *vbox;
38         GtkWidget *entry_box;
39         GtkEntryCompletion *completion;
40         GtkListStore *command_store;
41         GtkAction *action;
42         LayoutWindow *lw;
43         gboolean match_found;
44 };
45
46 static gint sort_iter_compare_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer UNUSED(data))
47 {
48         gint ret = 0;
49         gchar *label1, *label2;
50
51         gtk_tree_model_get(model, a, SAR_LABEL, &label1, -1);
52         gtk_tree_model_get(model, b, SAR_LABEL, &label2, -1);
53
54         if (label1 == NULL || label2 == NULL)
55                 {
56                 if (label1 == NULL && label2 == NULL)
57                         {
58                         ret = 0;
59                         }
60                 else
61                         {
62                         ret = (label1 == NULL) ? -1 : 1;
63                         }
64                 }
65         else
66                 {
67                 ret = g_utf8_collate(label1, label2);
68                 }
69
70         g_free(label1);
71         g_free(label2);
72
73         return ret;
74 }
75
76 static void command_store_populate(SarData* sar)
77 {
78         GList *groups, *actions;
79         GtkAction *action;
80         const gchar *accel_path;
81         GtkAccelKey key;
82         GtkTreeIter iter;
83         GtkTreeSortable *sortable;
84         gchar *label, *tooltip;
85         gchar *label2, *tooltip2;
86         GString *new_command;
87         gchar *existing_command;
88         gboolean iter_found;
89         gboolean duplicate_command;
90         gchar *accel;
91
92         sar->command_store = gtk_list_store_new(SAR_COUNT, G_TYPE_STRING, G_TYPE_OBJECT);
93
94         sortable = GTK_TREE_SORTABLE(sar->command_store);
95         gtk_tree_sortable_set_sort_func(sortable, SAR_LABEL, sort_iter_compare_func, NULL, NULL);
96
97         gtk_tree_sortable_set_sort_column_id(sortable, SAR_LABEL, GTK_SORT_ASCENDING);
98
99         groups = gtk_ui_manager_get_action_groups(sar->lw->ui_manager);
100         while (groups)
101                 {
102                 actions = gtk_action_group_list_actions(GTK_ACTION_GROUP(groups->data));
103                 while (actions)
104                         {
105                         action = GTK_ACTION(actions->data);
106                         accel_path = gtk_action_get_accel_path(action);
107                         if (accel_path && gtk_accel_map_lookup_entry(accel_path, &key))
108                                 {
109                                 g_object_get(action, "tooltip", &tooltip, "label", &label, NULL);
110                                 accel = gtk_accelerator_get_label(key.accel_key, key.accel_mods);
111
112                                 /* menu items with no tooltip are placeholders */
113                                 if (g_strrstr(accel_path, ".desktop") != NULL || tooltip != NULL)
114                                         {
115                                         if (pango_parse_markup(label, -1, '_', NULL, &label2, NULL, NULL) && label2)
116                                                 {
117                                                 g_free(label);
118                                                 label = label2;
119                                                 }
120                                         if (tooltip)
121                                                 {
122                                                 if (pango_parse_markup(tooltip, -1, '_', NULL, &tooltip2, NULL, NULL) && label2)
123                                                         {
124                                                         g_free(tooltip);
125                                                         tooltip = tooltip2;
126                                                         }
127                                                 }
128
129                                         new_command = g_string_new(NULL);
130                                         if (g_strcmp0(label, tooltip) == 0)
131                                                 {
132                                                 g_string_append_printf(new_command, "%s : %s",label, accel);
133                                                 }
134                                         else
135                                                 {
136                                                 g_string_append_printf(new_command, "%s - %s : %s",label, tooltip, accel);
137                                                 }
138
139                                         iter_found = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(sar->command_store), &iter);
140                                         duplicate_command = FALSE;
141
142                                         while (iter_found)
143                                                 {
144                                                 gtk_tree_model_get(GTK_TREE_MODEL(sar->command_store), &iter, SAR_LABEL, &existing_command, -1);
145                                                 if (g_strcmp0(new_command->str, existing_command) == 0)
146                                                         {
147                                                         g_free(existing_command);
148                                                         duplicate_command = TRUE;
149                                                         break;
150                                                         }
151                                                 g_free(existing_command);
152                                                 iter_found = gtk_tree_model_iter_next(GTK_TREE_MODEL(sar->command_store), &iter);
153                                                 }
154
155                                         if (!duplicate_command )
156                                                 {
157                                                 gtk_list_store_append(sar->command_store, &iter);
158                                                 gtk_list_store_set(sar->command_store, &iter,
159                                                                 SAR_LABEL, new_command->str,
160                                                                 SAR_ACTION, action,
161                                                                 -1);
162                                                 }
163                                         g_free(label);
164                                         g_free(tooltip);
165                                         g_free(accel);
166                                         g_string_free(new_command, TRUE);
167                                         }
168                                 }
169                         actions = actions->next;
170                         }
171                 groups = groups->next;
172                 }
173 }
174
175 static gboolean search_and_run_destroy(gpointer data)
176 {
177         SarData *sar = static_cast<SarData *>(data);
178
179         sar->lw->sar_window = NULL;
180         gtk_widget_destroy(sar->window);
181
182         return TRUE;
183 }
184
185 static gboolean entry_box_activate_cb(GtkWidget *UNUSED(widget), gpointer data)
186 {
187         SarData *sar = static_cast<SarData *>(data);
188
189         if (sar->action)
190                 {
191                 gtk_action_activate(sar->action);
192                 }
193
194         search_and_run_destroy(sar);
195
196         return TRUE;
197 }
198
199 static gboolean keypress_cb(GtkWidget *UNUSED(widget), GdkEventKey *event, gpointer data)
200 {
201         SarData *sar = static_cast<SarData *>(data);
202         gboolean ret = FALSE;
203
204         switch (event->keyval)
205                 {
206                 case GDK_KEY_Escape:
207                         search_and_run_destroy(sar);
208                         ret = TRUE;
209                         break;
210                 case GDK_KEY_Return:
211                         break;
212                 default:
213                         sar->match_found = FALSE;
214                         sar->action = NULL;
215                 }
216
217         return ret;
218 }
219
220 static gboolean match_selected_cb(GtkEntryCompletion *UNUSED(widget), GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
221 {
222         SarData *sar = static_cast<SarData *>(data);
223
224         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_ACTION, &sar->action, -1);
225
226         if (sar->action)
227                 {
228                 gtk_action_activate(sar->action);
229                 }
230
231         g_idle_add((GSourceFunc)search_and_run_destroy, sar);
232
233         return TRUE;
234 }
235
236 static gboolean match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer data)
237 {
238         SarData *sar = static_cast<SarData *>(data);
239         gboolean ret = FALSE;
240         gchar *normalized;
241         GtkTreeModel *model;
242         GtkAction *action;
243         gchar *label;
244         GString *reg_exp_str;
245         GRegex *reg_exp;
246         GError *error = NULL;
247
248         model = gtk_entry_completion_get_model(completion);
249         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_LABEL, &label, -1);
250         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_ACTION, &action, -1);
251
252         normalized = g_utf8_normalize(label, -1, G_NORMALIZE_DEFAULT);
253
254         reg_exp_str = g_string_new("\\b(\?=.*:)");
255         reg_exp_str = g_string_append(reg_exp_str, key);
256
257         reg_exp = g_regex_new(reg_exp_str->str, G_REGEX_CASELESS, GRegexMatchFlags(0), &error);
258         if (error)
259                 {
260                 log_printf("Error: could not compile regular expression %s\n%s\n", reg_exp_str->str, error->message);
261                 g_error_free(error);
262                 error = NULL;
263                 reg_exp = g_regex_new("", GRegexCompileFlags(0), GRegexMatchFlags(0), NULL);
264                 }
265
266         ret = g_regex_match(reg_exp, normalized, GRegexMatchFlags(0), NULL);
267
268         if (sar->match_found == FALSE && ret == TRUE)
269                 {
270                 sar->action = action;
271                 sar->match_found = TRUE;
272                 }
273
274         g_regex_unref(reg_exp);
275         g_string_free(reg_exp_str, TRUE);
276         g_free(normalized);
277
278         return ret;
279 }
280
281 GtkWidget *search_and_run_new(LayoutWindow *lw)
282 {
283         SarData *sar;
284         GdkGeometry geometry;
285
286         sar = g_new0(SarData, 1);
287         sar->lw = lw;
288         sar->window = window_new(GTK_WINDOW_TOPLEVEL, "sar_window", NULL, NULL, _("Search and Run command"));
289         DEBUG_NAME(sar->window);
290
291         geometry.min_width = 500;
292         geometry.max_width = 1500;
293         geometry.min_height = 10;
294         geometry.max_height = 10;
295         gtk_window_set_geometry_hints(GTK_WINDOW(sar->window), NULL, &geometry, static_cast<GdkWindowHints>(GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE));
296
297         gtk_window_set_resizable(GTK_WINDOW(sar->window), TRUE);
298
299         sar->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, PREF_PAD_GAP);
300         gtk_container_add(GTK_CONTAINER(sar->window), sar->vbox);
301         gtk_widget_show(sar->vbox);
302
303         sar->entry_box = gtk_entry_new();
304         gtk_box_pack_start(GTK_BOX(sar->vbox), sar->entry_box, FALSE, FALSE, 0);
305         gtk_widget_show(sar->entry_box);
306         gtk_entry_set_icon_from_stock(GTK_ENTRY(sar->entry_box), GTK_ENTRY_ICON_PRIMARY, GTK_STOCK_FIND);
307         gtk_widget_show(sar->vbox);
308         gtk_widget_set_tooltip_text(sar->entry_box, "Search for commands and run them");
309         g_signal_connect(G_OBJECT(sar->entry_box), "key_press_event", G_CALLBACK(keypress_cb), sar);
310         g_signal_connect(G_OBJECT(sar->entry_box), "activate", G_CALLBACK(entry_box_activate_cb), sar);
311
312         command_store_populate(sar);
313
314         sar->completion = gtk_entry_completion_new();
315         gtk_entry_set_completion(GTK_ENTRY(sar->entry_box), sar->completion);
316         gtk_entry_completion_set_inline_completion(sar->completion, FALSE);
317         gtk_entry_completion_set_inline_selection(sar->completion, FALSE);
318         gtk_entry_completion_set_minimum_key_length(sar->completion, 1);
319         gtk_entry_completion_set_match_func(sar->completion, match_func, sar, NULL);
320         g_signal_connect(G_OBJECT(sar->completion), "match-selected", G_CALLBACK(match_selected_cb), sar);
321         gtk_entry_completion_set_model(sar->completion, GTK_TREE_MODEL(sar->command_store));
322         gtk_entry_completion_set_text_column(sar->completion, SAR_LABEL);
323
324         gtk_widget_show(sar->window);
325         return sar->window;
326 }
327 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */