993f421ae077cca612ea1288bcd988bfae340226
[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 struct SarData
34 {
35         GtkWidget *window;
36         GtkListStore *command_store;
37         GtkAction *action;
38         LayoutWindow *lw;
39         gboolean match_found;
40         GtkBuilder *builder;
41 };
42
43 static gint sort_iter_compare_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer UNUSED(data))
44 {
45         gint ret = 0;
46         gchar *label1, *label2;
47
48         gtk_tree_model_get(model, a, SAR_LABEL, &label1, -1);
49         gtk_tree_model_get(model, b, SAR_LABEL, &label2, -1);
50
51         if (label1 == nullptr || label2 == nullptr)
52                 {
53                 if (label1 == nullptr && label2 == nullptr)
54                         {
55                         ret = 0;
56                         }
57                 else
58                         {
59                         ret = (label1 == nullptr) ? -1 : 1;
60                         }
61                 }
62         else
63                 {
64                 ret = g_utf8_collate(label1, label2);
65                 }
66
67         g_free(label1);
68         g_free(label2);
69
70         return ret;
71 }
72
73 static void command_store_populate(SarData* sar)
74 {
75         GList *groups, *actions;
76         GtkAction *action;
77         const gchar *accel_path;
78         GtkAccelKey key;
79         GtkTreeIter iter;
80         GtkTreeSortable *sortable;
81         gchar *label, *tooltip;
82         gchar *label2, *tooltip2;
83         GString *new_command;
84         gchar *existing_command;
85         gboolean iter_found;
86         gboolean duplicate_command;
87         gchar *accel;
88
89         sar->command_store = GTK_LIST_STORE(gtk_builder_get_object(sar->builder, "command_store"));
90         sortable = GTK_TREE_SORTABLE(sar->command_store);
91         gtk_tree_sortable_set_sort_func(sortable, SAR_LABEL, sort_iter_compare_func, nullptr, nullptr);
92
93         gtk_tree_sortable_set_sort_column_id(sortable, SAR_LABEL, GTK_SORT_ASCENDING);
94
95         groups = gtk_ui_manager_get_action_groups(sar->lw->ui_manager);
96         while (groups)
97                 {
98                 actions = gtk_action_group_list_actions(GTK_ACTION_GROUP(groups->data));
99                 while (actions)
100                         {
101                         action = GTK_ACTION(actions->data);
102                         accel_path = gtk_action_get_accel_path(action);
103                         if (accel_path && gtk_accel_map_lookup_entry(accel_path, &key))
104                                 {
105                                 g_object_get(action, "tooltip", &tooltip, "label", &label, NULL);
106                                 accel = gtk_accelerator_get_label(key.accel_key, key.accel_mods);
107
108                                 /* menu items with no tooltip are placeholders */
109                                 if (g_strrstr(accel_path, ".desktop") != nullptr || tooltip != nullptr)
110                                         {
111                                         if (pango_parse_markup(label, -1, '_', nullptr, &label2, nullptr, nullptr) && label2)
112                                                 {
113                                                 g_free(label);
114                                                 label = label2;
115                                                 }
116                                         if (tooltip)
117                                                 {
118                                                 if (pango_parse_markup(tooltip, -1, '_', nullptr, &tooltip2, nullptr, nullptr) && label2)
119                                                         {
120                                                         g_free(tooltip);
121                                                         tooltip = tooltip2;
122                                                         }
123                                                 }
124
125                                         new_command = g_string_new(nullptr);
126                                         if (g_strcmp0(label, tooltip) == 0)
127                                                 {
128                                                 g_string_append_printf(new_command, "%s : %s",label, accel);
129                                                 }
130                                         else
131                                                 {
132                                                 g_string_append_printf(new_command, "%s - %s : %s",label, tooltip, accel);
133                                                 }
134
135                                         iter_found = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(sar->command_store), &iter);
136                                         duplicate_command = FALSE;
137
138                                         while (iter_found)
139                                                 {
140                                                 gtk_tree_model_get(GTK_TREE_MODEL(sar->command_store), &iter, SAR_LABEL, &existing_command, -1);
141                                                 if (g_strcmp0(new_command->str, existing_command) == 0)
142                                                         {
143                                                         g_free(existing_command);
144                                                         duplicate_command = TRUE;
145                                                         break;
146                                                         }
147                                                 g_free(existing_command);
148                                                 iter_found = gtk_tree_model_iter_next(GTK_TREE_MODEL(sar->command_store), &iter);
149                                                 }
150
151                                         if (!duplicate_command )
152                                                 {
153                                                 gtk_list_store_append(sar->command_store, &iter);
154                                                 gtk_list_store_set(sar->command_store, &iter,
155                                                                 SAR_LABEL, new_command->str,
156                                                                 SAR_ACTION, action,
157                                                                 -1);
158                                                 }
159                                         g_free(label);
160                                         g_free(tooltip);
161                                         g_free(accel);
162                                         g_string_free(new_command, TRUE);
163                                         }
164                                 }
165                         actions = actions->next;
166                         }
167                 groups = groups->next;
168                 }
169 }
170
171 static gboolean search_and_run_destroy(gpointer data)
172 {
173         auto sar = static_cast<SarData *>(data);
174
175         sar->lw->sar_window = nullptr;
176         g_object_unref(gtk_builder_get_object(sar->builder, "completion"));
177         g_object_unref(gtk_builder_get_object(sar->builder, "command_store"));
178         gtk_widget_destroy(sar->window);
179         g_free(sar);
180
181         return G_SOURCE_REMOVE;
182 }
183
184 static gboolean entry_box_activate_cb(GtkWidget *UNUSED(widget), gpointer data)
185 {
186         auto sar = static_cast<SarData *>(data);
187
188         if (sar->action)
189                 {
190                 gtk_action_activate(sar->action);
191                 }
192
193         search_and_run_destroy(sar);
194
195         return TRUE;
196 }
197
198 static gboolean keypress_cb(GtkWidget *UNUSED(widget), GdkEventKey *event, gpointer data)
199 {
200         auto sar = static_cast<SarData *>(data);
201         gboolean ret = FALSE;
202
203         switch (event->keyval)
204                 {
205                 case GDK_KEY_Escape:
206                         search_and_run_destroy(sar);
207                         ret = TRUE;
208                         break;
209                 case GDK_KEY_Return:
210                         break;
211                 default:
212                         sar->match_found = FALSE;
213                         sar->action = nullptr;
214                 }
215
216         return ret;
217 }
218
219 static gboolean match_selected_cb(GtkEntryCompletion *UNUSED(widget), GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
220 {
221         auto sar = static_cast<SarData *>(data);
222
223         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_ACTION, &sar->action, -1);
224
225         if (sar->action)
226                 {
227                 gtk_action_activate(sar->action);
228                 }
229
230         g_idle_add(static_cast<GSourceFunc>(search_and_run_destroy), sar);
231
232         return TRUE;
233 }
234
235 static gboolean match_func(GtkEntryCompletion *completion, const gchar *key, GtkTreeIter *iter, gpointer data)
236 {
237         auto sar = static_cast<SarData *>(data);
238         gboolean ret = FALSE;
239         gchar *normalized;
240         GtkTreeModel *model;
241         GtkAction *action;
242         gchar *label;
243         GString *reg_exp_str;
244         GRegex *reg_exp;
245         GError *error = nullptr;
246
247         model = gtk_entry_completion_get_model(completion);
248         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_LABEL, &label, -1);
249         gtk_tree_model_get(GTK_TREE_MODEL(model), iter, SAR_ACTION, &action, -1);
250
251         normalized = g_utf8_normalize(label, -1, G_NORMALIZE_DEFAULT);
252
253         reg_exp_str = g_string_new("\\b(\?=.*:)");
254         reg_exp_str = g_string_append(reg_exp_str, key);
255
256         reg_exp = g_regex_new(reg_exp_str->str, G_REGEX_CASELESS, static_cast<GRegexMatchFlags>(0), &error);
257         if (error)
258                 {
259                 log_printf("Error: could not compile regular expression %s\n%s\n", reg_exp_str->str, error->message);
260                 g_error_free(error);
261                 error = nullptr;
262                 reg_exp = g_regex_new("", static_cast<GRegexCompileFlags>(0), static_cast<GRegexMatchFlags>(0), nullptr);
263                 }
264
265         ret = g_regex_match(reg_exp, normalized, static_cast<GRegexMatchFlags>(0), nullptr);
266
267         if (sar->match_found == FALSE && ret == TRUE)
268                 {
269                 sar->action = action;
270                 sar->match_found = TRUE;
271                 }
272
273         g_regex_unref(reg_exp);
274         g_string_free(reg_exp_str, TRUE);
275         g_free(normalized);
276
277         return ret;
278 }
279
280 GtkWidget *search_and_run_new(LayoutWindow *lw)
281 {
282         SarData *sar;
283         gchar *ui_path;
284
285         sar = g_new0(SarData, 1);
286         sar->lw = lw;
287
288         ui_path = g_build_filename(GQ_RESOURCE_PATH_UI, "search-and-run.ui", nullptr);
289
290         sar->builder = gtk_builder_new_from_resource(ui_path);
291         command_store_populate(sar);
292
293         sar->window = GTK_WIDGET(gtk_builder_get_object(sar->builder, "search_and_run"));
294         DEBUG_NAME(sar->window);
295
296         gtk_entry_completion_set_match_func(GTK_ENTRY_COMPLETION(gtk_builder_get_object(sar->builder, "completion")), match_func, sar, nullptr);
297
298         g_signal_connect(G_OBJECT(gtk_builder_get_object(sar->builder, "completion")), "match-selected", G_CALLBACK(match_selected_cb), sar);
299         g_signal_connect(G_OBJECT(gtk_builder_get_object(sar->builder, "entry")), "key_press_event", G_CALLBACK(keypress_cb), sar);
300         g_signal_connect(G_OBJECT(gtk_builder_get_object(sar->builder, "entry")), "activate", G_CALLBACK(entry_box_activate_cb), sar);
301
302         gtk_widget_show(sar->window);
303         g_free(ui_path);
304
305         return sar->window;
306 }
307
308 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */