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