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