1 /*
2 * Copyright (C) 2016 Sebastian Geiger
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "tilda-search-box.h"
19 #include "tilda-enum-types.h"
20 #include "vte-util.h"
21
22 #define PCRE2_CODE_UNIT_WIDTH 0
23 #include <pcre2.h>
24
25 #define GRESOURCE "/org/tilda/"
26
27 struct _TildaSearchBox
28 {
29 GtkBox parent;
30
31 GtkWidget *label;
32 GtkWidget *entry;
33 GtkWidget *button_next;
34 GtkWidget *button_prev;
35 GtkWidget *check_match_case;
36 GtkWidget *check_regex;
37
38 TildaSearchDirection last_direction;
39 gboolean last_search_successful;
40 };
41
42 enum
43 {
44 SIGNAL_SEARCH,
45 SIGNAL_SEARCH_GREGEX,
46 SIGNAL_FOCUS_OUT,
47
48 LAST_SIGNAL
49 };
50
51 static guint signals[LAST_SIGNAL] = { 0 };
52
G_DEFINE_TYPE(TildaSearchBox,tilda_search_box,GTK_TYPE_BOX)53 G_DEFINE_TYPE (TildaSearchBox, tilda_search_box, GTK_TYPE_BOX)
54
55 static void
56 search_gregex (TildaSearchBox *search,
57 TildaSearchDirection direction)
58 {
59 GtkEntry *entry;
60 GtkEntryBuffer *buffer;
61 GtkToggleButton *toggle_button;
62
63 GRegexCompileFlags compile_flags;
64 gboolean wrap_on_search;
65 gboolean is_regex;
66 const gchar *text;
67 gchar *pattern;
68 gboolean search_result;
69
70 GError *error;
71 GRegex *regex;
72
73 compile_flags = G_REGEX_OPTIMIZE;
74 wrap_on_search = FALSE;
75 toggle_button = GTK_TOGGLE_BUTTON (search->check_regex);
76 is_regex = gtk_toggle_button_get_active (toggle_button);
77 entry = GTK_ENTRY (search->entry);
78 buffer = GTK_ENTRY_BUFFER (gtk_entry_get_buffer (entry));
79 text = gtk_entry_buffer_get_text (buffer);
80
81 if (!search->last_search_successful)
82 wrap_on_search = TRUE;
83
84 if (is_regex)
85 {
86 compile_flags |= G_REGEX_MULTILINE;
87 pattern = g_strdup (text);
88 }
89 else
90 pattern = g_regex_escape_string (text, -1);
91
92 toggle_button = GTK_TOGGLE_BUTTON (search->check_match_case);
93 if (!gtk_toggle_button_get_active (toggle_button))
94 {
95 compile_flags |= G_REGEX_CASELESS;
96 }
97
98 error = NULL;
99 regex = g_regex_new (pattern, compile_flags,
100 G_REGEX_MATCH_NEWLINE_ANY,
101 &error);
102 g_free (pattern);
103
104 if (error)
105 {
106 GtkLabel *label = GTK_LABEL (search->label);
107 gtk_label_set_text (label, error->message);
108 gtk_widget_set_visible (search->label, TRUE);
109 g_error_free (error);
110 return;
111 }
112
113 g_signal_emit (search, signals[SIGNAL_SEARCH_GREGEX], 0,
114 regex, direction, wrap_on_search, &search_result);
115
116 search->last_direction = direction;
117
118 gtk_widget_set_visible (search->label, !search_result);
119 search->last_search_successful = search_result;
120
121 g_regex_unref(regex);
122 }
123
124 static void
search_vte_regex(TildaSearchBox * search,TildaSearchDirection direction)125 search_vte_regex (TildaSearchBox *search,
126 TildaSearchDirection direction)
127 {
128 GtkEntry *entry;
129 GtkEntryBuffer *buffer;
130 GtkToggleButton *toggle_button;
131
132 guint32 compile_flags;
133 gboolean wrap_on_search;
134 gboolean is_regex;
135 gboolean match_case;
136 const gchar *text;
137 gchar *pattern;
138 size_t pattern_length;
139 gboolean search_result;
140
141 GError *error;
142 VteRegex *regex;
143
144 compile_flags = 0;
145 wrap_on_search = FALSE;
146 toggle_button = GTK_TOGGLE_BUTTON (search->check_regex);
147 is_regex = gtk_toggle_button_get_active (toggle_button);
148 entry = GTK_ENTRY (search->entry);
149 buffer = GTK_ENTRY_BUFFER (gtk_entry_get_buffer (entry));
150 text = gtk_entry_buffer_get_text (buffer);
151
152 if (!search->last_search_successful)
153 wrap_on_search = TRUE;
154
155 if (is_regex)
156 {
157 compile_flags |= PCRE2_MULTILINE;
158 pattern = g_strdup (text);
159 }
160 else
161 pattern = g_regex_escape_string (text, -1);
162
163 pattern_length = strlen (pattern);
164
165 toggle_button = GTK_TOGGLE_BUTTON (search->check_match_case);
166 match_case = gtk_toggle_button_get_active (toggle_button);
167
168 if (!match_case)
169 {
170 compile_flags |= PCRE2_CASELESS;
171 }
172
173 compile_flags |= PCRE2_MULTILINE;
174
175 error = NULL;
176
177 regex = vte_regex_new_for_search (pattern, pattern_length, compile_flags, &error);
178
179 g_free (pattern);
180
181 if (error)
182 {
183 GtkLabel *label = GTK_LABEL (search->label);
184 gtk_label_set_text (label, error->message);
185 gtk_widget_set_visible (search->label, TRUE);
186 g_error_free (error);
187 return;
188 }
189
190 g_signal_emit (search, signals[SIGNAL_SEARCH], 0,
191 regex, direction, wrap_on_search, &search_result);
192
193 search->last_direction = direction;
194
195 gtk_widget_set_visible (search->label, !search_result);
196 search->last_search_successful = search_result;
197
198 vte_regex_unref (regex);
199 }
200
201 static void
search(TildaSearchBox * search,TildaSearchDirection direction)202 search (TildaSearchBox *search,
203 TildaSearchDirection direction)
204 {
205 if (VTE_CHECK_VERSION_RUMTIME (0, 56, 1)) {
206 search_vte_regex (search, direction);
207 } else {
208 search_gregex (search, direction);
209 }
210 }
211
212 static gboolean
entry_key_press_cb(TildaSearchBox * box,GdkEvent * event,GtkWidget * widget)213 entry_key_press_cb (TildaSearchBox *box,
214 GdkEvent *event,
215 GtkWidget *widget)
216 {
217 GdkEventKey *event_key;
218
219 event_key = (GdkEventKey*) event;
220
221 if (event_key->keyval == GDK_KEY_Return) {
222 search (box, box->last_direction);
223 return GDK_EVENT_STOP;
224 }
225
226 /* If the search entry has focus the user can hide the search bar by pressing escape. */
227 if (event_key->keyval == GDK_KEY_Escape)
228 {
229 if (gtk_widget_has_focus (box->entry))
230 {
231 g_signal_emit (box, signals[SIGNAL_FOCUS_OUT], 0);
232 gtk_widget_set_visible (GTK_WIDGET (box), FALSE);
233
234 return GDK_EVENT_STOP;
235 }
236 }
237
238 return GDK_EVENT_PROPAGATE;
239 }
240
241 static gboolean
entry_changed_cb(TildaSearchBox * box,GtkEditable * editable)242 entry_changed_cb (TildaSearchBox *box,
243 GtkEditable *editable)
244 {
245 gtk_widget_hide (box->label);
246 box->last_search_successful = TRUE;
247
248 return GDK_EVENT_STOP;
249 }
250
251 static void
button_next_cb(TildaSearchBox * box,GtkWidget * widget)252 button_next_cb (TildaSearchBox *box,
253 GtkWidget *widget)
254 {
255 /* The default is to search forward */
256 search (box, SEARCH_FORWARD);
257 }
258
259 static void
button_prev_cb(TildaSearchBox * box,GtkWidget * widget)260 button_prev_cb (TildaSearchBox *box,
261 GtkWidget *widget)
262 {
263 search (box, SEARCH_BACKWARD);
264 }
265
266 void
tilda_search_box_toggle(TildaSearchBox * box)267 tilda_search_box_toggle (TildaSearchBox *box)
268 {
269 gboolean visible = !gtk_widget_get_visible(GTK_WIDGET (box));
270
271 if (visible)
272 gtk_widget_grab_focus (box->entry);
273 else
274 g_signal_emit (box, signals[SIGNAL_FOCUS_OUT], 0);
275
276 gtk_widget_set_visible(GTK_WIDGET (box), visible);
277 }
278
279 static void
tilda_search_box_class_init(TildaSearchBoxClass * box_class)280 tilda_search_box_class_init (TildaSearchBoxClass *box_class)
281 {
282 GtkWidgetClass *widget_class;
283 const gchar *resource_name;
284
285 widget_class = GTK_WIDGET_CLASS (box_class);
286
287 /**
288 * TildaSearchBox::search:
289 * @widget: the widget that received the signal
290 * @regex: the regular expression entered by the user
291 * @direction: the direction for the search
292 * @wrap_over: if the search should wrap over
293 *
294 * This signal is, emitted when the user performed a search action, such
295 * as clicking on the prev or next buttons or hitting enter.
296 *
297 * This widget does not actually perform the search, but leaves it to the
298 * users of this signal to perform the search based on the information
299 * provided to the signal handler.
300 *
301 * The user function needs to return %TRUE or %FALSE depending on
302 * whether the search was successful (i.e. a match was found).
303 *
304 * Returns: %TRUE if the search found a match, %FALSE otherwise.
305 */
306 signals[SIGNAL_SEARCH] =
307 g_signal_new ("search", TILDA_TYPE_SEARCH_BOX, G_SIGNAL_RUN_LAST, 0,
308 NULL, NULL, NULL, G_TYPE_BOOLEAN,
309 3, VTE_TYPE_REGEX, TILDA_TYPE_SEARCH_DIRECTION, G_TYPE_BOOLEAN);
310
311 /**
312 * TildaSearchBox::search-gregex:
313 * @widget: the widget that received the signal
314 * @regex: the regular expression entered by the user
315 * @direction: the direction for the search
316 * @wrap_over: if the search should wrap over
317 *
318 * This signal is, emitted when the user performed a search action, such
319 * as clicking on the prev or next buttons or hitting enter.
320 *
321 * This widget does not actually perform the search, but leaves it to the
322 * users of this signal to perform the search based on the information
323 * provided to the signal handler.
324 *
325 * The user function needs to return %TRUE or %FALSE depending on
326 * whether the search was successful (i.e. a match was found).
327 *
328 * Returns: %TRUE if the search found a match, %FALSE otherwise.
329 */
330 signals[SIGNAL_SEARCH_GREGEX] =
331 g_signal_new ("search-gregex", TILDA_TYPE_SEARCH_BOX, G_SIGNAL_RUN_LAST, 0,
332 NULL, NULL, NULL, G_TYPE_BOOLEAN,
333 3, G_TYPE_REGEX, TILDA_TYPE_SEARCH_DIRECTION, G_TYPE_BOOLEAN);
334
335 /**
336 * TildaSearchBox::focus-out:
337 * @widget: the widget that received the signal
338 *
339 * This signal is emitted, when the search bar is being toggled
340 * and this would cause the search bar to hide. Hiding the search bar
341 * means that it will loose focus and that some other widget should get
342 * focused.
343 *
344 * This signal should be used to grab the focus of another widget.
345 */
346 signals[SIGNAL_FOCUS_OUT] =
347 g_signal_new ("focus-out", TILDA_TYPE_SEARCH_BOX, G_SIGNAL_RUN_LAST, 0,
348 NULL, NULL, NULL, G_TYPE_NONE, 0);
349
350 resource_name = GRESOURCE "tilda-search-box.ui";
351 gtk_widget_class_set_template_from_resource (widget_class, resource_name);
352
353 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
354 label);
355 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
356 entry);
357 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
358 button_next);
359 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
360 button_prev);
361 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
362 check_match_case);
363 gtk_widget_class_bind_template_child (widget_class, TildaSearchBox,
364 check_regex);
365 }
366
367 static void
tilda_search_box_init(TildaSearchBox * box)368 tilda_search_box_init (TildaSearchBox *box)
369 {
370 TildaSearchBox *search_box;
371
372 gtk_widget_init_template (GTK_WIDGET (box));
373
374 search_box = TILDA_SEARCH_BOX (box);
375
376 search_box->last_direction = SEARCH_BACKWARD;
377
378 /* Initialize to true to prevent search from
379 * wrapping around on first search. */
380 search_box->last_search_successful = TRUE;
381
382 gtk_widget_set_name (GTK_WIDGET (search_box), "search");
383
384 g_signal_connect_swapped (G_OBJECT(search_box->entry), "key-press-event",
385 G_CALLBACK(entry_key_press_cb), search_box);
386 g_signal_connect_swapped (G_OBJECT (search_box->entry), "changed",
387 G_CALLBACK (entry_changed_cb), search_box);
388 g_signal_connect_swapped (G_OBJECT (search_box->button_next), "clicked",
389 G_CALLBACK (button_next_cb), search_box);
390 g_signal_connect_swapped (G_OBJECT (search_box->button_prev), "clicked",
391 G_CALLBACK (button_prev_cb), search_box);
392 }
393
394 GtkWidget*
tilda_search_box_new(void)395 tilda_search_box_new (void)
396 {
397 return g_object_new (TILDA_TYPE_SEARCH_BOX,
398 "orientation", GTK_ORIENTATION_VERTICAL,
399 NULL);
400 }
401