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