1/*
2 * Copyright (C) 2011-2012 Lucas Baudin <xapantu@gmail.com>
3 *               2013      Mario Guerriero <mario@elementaryos.org>
4 *
5 * This file is part of Code.
6 *
7 * Code is free software: you can redistribute it and/or modify it
8 * under the terms of the GNU General Public License as published by the
9 * Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * Code is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
15 * See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program.  If not, see <http://www.gnu.org/licenses/>.
19 */
20
21namespace Scratch.Widgets {
22    public class SearchBar : Gtk.FlowBox {
23        public weak MainWindow window { get; construct; }
24
25        private Gtk.Button tool_arrow_up;
26        private Gtk.Button tool_arrow_down;
27
28        /**
29         * Is the search cyclic? e.g., when you are at the bottom, if you press
30         * "Down", it will go at the start of the file to search for the content
31         * of the search entry.
32         **/
33
34        private Gtk.ToggleButton case_sensitive_button;
35        public Gtk.ToggleButton tool_cycle_search {get; construct;}
36
37        public Gtk.SearchEntry search_entry;
38        public Gtk.SearchEntry replace_entry;
39
40        private Gtk.Button replace_tool_button;
41        private Gtk.Button replace_all_tool_button;
42
43        private Scratch.Widgets.SourceView? text_view = null;
44        private Gtk.TextBuffer? text_buffer = null;
45        private Gtk.SourceSearchContext search_context = null;
46
47        public signal void search_empty ();
48
49        /**
50         * Create a new SearchBar widget.
51         *
52         * following actions : Fetch, ShowGoTo, ShowRreplace, or null.
53         **/
54        public SearchBar (MainWindow window) {
55            Object (window: window);
56        }
57
58        construct {
59            get_style_context ().add_class ("search-bar");
60
61            search_entry = new Gtk.SearchEntry ();
62            search_entry.hexpand = true;
63            search_entry.placeholder_text = _("Find");
64
65            var app_instance = (Scratch.Application) GLib.Application.get_default ();
66
67            tool_arrow_down = new Gtk.Button.from_icon_name ("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
68            tool_arrow_down.clicked.connect (search_next);
69            tool_arrow_down.sensitive = false;
70            tool_arrow_down.tooltip_markup = Granite.markup_accel_tooltip (
71                app_instance.get_accels_for_action (
72                    Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_FIND_NEXT
73                ),
74                _("Search next")
75            );
76
77            tool_arrow_up = new Gtk.Button.from_icon_name ("go-up-symbolic", Gtk.IconSize.SMALL_TOOLBAR);
78            tool_arrow_up.clicked.connect (search_previous);
79            tool_arrow_up.sensitive = false;
80            tool_arrow_up.tooltip_markup = Granite.markup_accel_tooltip (
81                app_instance.get_accels_for_action (
82                    Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_FIND_PREVIOUS
83                ),
84                _("Search previous")
85            );
86
87            tool_cycle_search = new Gtk.ToggleButton () {
88                image = new Gtk.Image.from_icon_name ("media-playlist-repeat-symbolic", Gtk.IconSize.SMALL_TOOLBAR),
89                tooltip_text = _("Cyclic Search")
90            };
91
92            case_sensitive_button = new Gtk.ToggleButton () {
93                image = new Gtk.Image.from_icon_name ("font-select-symbolic", Gtk.IconSize.SMALL_TOOLBAR)
94            };
95            case_sensitive_button.bind_property (
96                "active",
97                case_sensitive_button, "tooltip-text",
98                BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE, // Need to SYNC_CREATE so tooltip present before toggled
99                (binding, active_val, ref tooltip_val) => {
100                    ((Gtk.Widget)(binding.target)).set_tooltip_text ( //tooltip_val.set_string () does not work (?)
101                        active_val.get_boolean () ? _("Case Sensitive") : _("Case Insensitive")
102                    );
103                }
104            );
105            case_sensitive_button.clicked.connect (on_search_entry_text_changed);
106
107            var search_grid = new Gtk.Grid ();
108            search_grid.margin = 3;
109            search_grid.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED);
110            search_grid.add (search_entry);
111            search_grid.add (tool_arrow_down);
112            search_grid.add (tool_arrow_up);
113            search_grid.add (tool_cycle_search);
114            search_grid.add (case_sensitive_button);
115
116            var search_flow_box_child = new Gtk.FlowBoxChild ();
117            search_flow_box_child.can_focus = false;
118            search_flow_box_child.add (search_grid);
119
120            replace_entry = new Gtk.SearchEntry ();
121            replace_entry.hexpand = true;
122            replace_entry.placeholder_text = _("Replace With");
123            replace_entry.set_icon_from_icon_name (Gtk.EntryIconPosition.PRIMARY, "edit-symbolic");
124
125            replace_tool_button = new Gtk.Button.with_label (_("Replace"));
126            replace_tool_button.clicked.connect (on_replace_entry_activate);
127
128            replace_all_tool_button = new Gtk.Button.with_label (_("Replace all"));
129            replace_all_tool_button.clicked.connect (on_replace_all_entry_activate);
130
131            var replace_grid = new Gtk.Grid ();
132            replace_grid.margin = 3;
133            replace_grid.get_style_context ().add_class (Gtk.STYLE_CLASS_LINKED);
134            replace_grid.add (replace_entry);
135            replace_grid.add (replace_tool_button);
136            replace_grid.add (replace_all_tool_button);
137
138            var replace_flow_box_child = new Gtk.FlowBoxChild ();
139            replace_flow_box_child.can_focus = false;
140            replace_flow_box_child.add (replace_grid);
141
142            // Connecting to some signals
143            search_entry.changed.connect (on_search_entry_text_changed);
144            search_entry.key_press_event.connect (on_search_entry_key_press);
145            search_entry.focus_in_event.connect (on_search_entry_focused_in);
146            search_entry.icon_release.connect ((p0, p1) => {
147                if (p0 == Gtk.EntryIconPosition.PRIMARY) {
148                    search_next ();
149                }
150            });
151            replace_entry.activate.connect (on_replace_entry_activate);
152            replace_entry.key_press_event.connect (on_replace_entry_key_press);
153
154            var entry_path = new Gtk.WidgetPath ();
155            entry_path.append_type (typeof (Gtk.Widget));
156
157            var entry_context = new Gtk.StyleContext ();
158            entry_context.set_path (entry_path);
159            entry_context.add_class ("entry");
160
161            selection_mode = Gtk.SelectionMode.NONE;
162            column_spacing = 6;
163            max_children_per_line = 2;
164            add (search_flow_box_child);
165            add (replace_flow_box_child);
166
167            update_replace_tool_sensitivities (search_entry.text, false);
168        }
169
170        public void set_text_view (Scratch.Widgets.SourceView? text_view) {
171            if (text_view == null) {
172                warning ("No SourceView is associated with SearchManager!");
173                return;
174            }
175
176            this.text_view = text_view;
177            this.text_buffer = text_view.get_buffer ();
178            this.search_context = new Gtk.SourceSearchContext (text_buffer as Gtk.SourceBuffer, null);
179            search_context.settings.wrap_around = tool_cycle_search.active;
180            search_context.settings.regex_enabled = false;
181            search_context.settings.search_text = search_entry.text;
182
183            // Determine the search entry color
184            bool found = (search_entry.text != "" && search_entry.text in this.text_buffer.text);
185            if (found) {
186                tool_arrow_down.sensitive = true;
187                tool_arrow_up.sensitive = false;
188                search_entry.get_style_context ().remove_class (Gtk.STYLE_CLASS_ERROR);
189                search_entry.primary_icon_name = "edit-find-symbolic";
190            } else {
191                if (search_entry.text != "") {
192                    search_entry.get_style_context ().add_class (Gtk.STYLE_CLASS_ERROR);
193                    search_entry.primary_icon_name = "dialog-error-symbolic";
194                }
195
196                tool_arrow_down.sensitive = false;
197                tool_arrow_up.sensitive = false;
198            }
199        }
200
201        private void on_replace_entry_activate () {
202            if (text_buffer == null) {
203                warning ("No valid buffer to replace");
204                return;
205            }
206
207            Gtk.TextIter? start_iter, end_iter;
208            text_buffer.get_iter_at_offset (out start_iter, text_buffer.cursor_position);
209
210            if (search_for_iter (start_iter, out end_iter)) {
211                string replace_string = replace_entry.text;
212                try {
213                    search_context.replace (start_iter, end_iter, replace_string, replace_string.length);
214                    bool matches = search ();
215                    update_replace_tool_sensitivities (search_entry.text, matches);
216                    update_tool_arrows (search_entry.text);
217                    debug ("Replace \"%s\" with \"%s\"", search_entry.text, replace_entry.text);
218                } catch (Error e) {
219                    critical (e.message);
220                }
221            }
222        }
223
224        private void on_replace_all_entry_activate () {
225            if (text_buffer == null || this.window.get_current_document () == null) {
226                debug ("No valid buffer to replace");
227                return;
228            }
229
230            string replace_string = replace_entry.text;
231            this.window.get_current_document ().toggle_changed_handlers (false);
232            try {
233                search_context.replace_all (replace_string, replace_string.length);
234                update_tool_arrows (search_entry.text);
235                update_replace_tool_sensitivities (search_entry.text, false);
236            } catch (Error e) {
237                critical (e.message);
238            }
239
240            this.window.get_current_document ().toggle_changed_handlers (true);
241        }
242
243        public void set_search_string (string to_search) {
244            search_entry.text = to_search;
245        }
246
247        private void on_search_entry_text_changed () {
248            var search_string = search_entry.text;
249            search_context.settings.search_text = search_string;
250            bool case_sensitive = is_case_sensitive (search_string);
251            search_context.settings.case_sensitive = case_sensitive;
252
253            bool matches = search ();
254            update_replace_tool_sensitivities (search_entry.text, matches);
255            update_tool_arrows (search_entry.text);
256
257            if (search_entry.text == "") {
258                search_empty ();
259            }
260        }
261
262        private void update_replace_tool_sensitivities (string search_text, bool matches) {
263            replace_tool_button.sensitive = matches && search_text != "";
264            replace_all_tool_button.sensitive = matches && search_text != "";
265        }
266
267        private bool on_search_entry_focused_in (Gdk.EventFocus event) {
268            if (text_buffer == null) {
269                return false;
270            }
271
272            Gtk.TextIter? start_iter, end_iter;
273            text_buffer.get_iter_at_offset (out start_iter, text_buffer.cursor_position);
274
275            end_iter = start_iter;
276            bool case_sensitive = is_case_sensitive (search_entry.text);
277            bool found = start_iter.forward_search (search_entry.text,
278                                                    case_sensitive ? 0 : Gtk.TextSearchFlags.CASE_INSENSITIVE,
279                                                    out start_iter, out end_iter, null);
280            if (found) {
281                search_entry.get_style_context ().remove_class (Gtk.STYLE_CLASS_ERROR);
282                search_entry.primary_icon_name = "edit-find-symbolic";
283                return true;
284            } else {
285                if (search_entry.text != "") {
286                    search_entry.get_style_context ().add_class (Gtk.STYLE_CLASS_ERROR);
287                    search_entry.primary_icon_name = "dialog-error-symbolic";
288                }
289
290                return false;
291            }
292        }
293
294        public bool search () {
295            /* So, first, let's check we can really search something. */
296            string search_string = search_entry.text;
297            search_context.highlight = false;
298            search_context.highlight = false;
299
300            if (text_buffer == null || text_buffer.text == "" || search_string == "") {
301                debug ("Can't search anything in an inexistant buffer and/or without anything to search.");
302                search_entry.primary_icon_name = "edit-find-symbolic";
303                return false;
304            }
305
306            search_context.highlight = true;
307
308            Gtk.TextIter? start_iter, end_iter;
309            text_buffer.get_iter_at_offset (out start_iter, text_buffer.cursor_position);
310
311            if (search_for_iter (start_iter, out end_iter)) {
312                search_entry.get_style_context ().remove_class (Gtk.STYLE_CLASS_ERROR);
313                search_entry.primary_icon_name = "edit-find-symbolic";
314            } else {
315                text_buffer.get_start_iter (out start_iter);
316                if (search_for_iter (start_iter, out end_iter)) {
317                    search_entry.get_style_context ().remove_class (Gtk.STYLE_CLASS_ERROR);
318                    search_entry.primary_icon_name = "edit-find-symbolic";
319                } else {
320                    debug ("Not found: \"%s\"", search_string);
321                    start_iter.set_offset (-1);
322                    text_buffer.select_range (start_iter, start_iter);
323                    search_entry.get_style_context ().add_class (Gtk.STYLE_CLASS_ERROR);
324                    search_entry.primary_icon_name = "dialog-error-symbolic";
325                    return false;
326                }
327            }
328
329            return true;
330        }
331
332        public void highlight_none () {
333            search_context.highlight = false;
334        }
335
336        private bool search_for_iter (Gtk.TextIter? start_iter, out Gtk.TextIter? end_iter) {
337            end_iter = start_iter;
338            bool found = search_context.forward (start_iter, out start_iter, out end_iter, null);
339            if (found) {
340                text_buffer.select_range (start_iter, end_iter);
341                text_view.scroll_to_iter (start_iter, 0, false, 0, 0);
342            }
343
344            return found;
345        }
346
347        private bool search_for_iter_backward (Gtk.TextIter? start_iter, out Gtk.TextIter? end_iter) {
348            end_iter = start_iter;
349            bool found = search_context.backward (start_iter, out start_iter, out end_iter, null);
350            if (found) {
351                text_buffer.select_range (start_iter, end_iter);
352                text_view.scroll_to_iter (start_iter, 0, false, 0, 0);
353            }
354
355            return found;
356        }
357
358        public void search_previous () {
359            /* Get selection range */
360            Gtk.TextIter? start_iter, end_iter;
361            if (text_buffer != null) {
362                string search_string = search_entry.text;
363                text_buffer.get_selection_bounds (out start_iter, out end_iter);
364                if (!search_for_iter_backward (start_iter, out end_iter) && tool_cycle_search.active) {
365                    text_buffer.get_end_iter (out start_iter);
366                    search_for_iter_backward (start_iter, out end_iter);
367                }
368
369                update_tool_arrows (search_string);
370            }
371        }
372
373        public void search_next () {
374            /* Get selection range */
375            Gtk.TextIter? start_iter, end_iter, end_iter_tmp;
376            if (text_buffer != null) {
377                string search_string = search_entry.text;
378                text_buffer.get_selection_bounds (out start_iter, out end_iter);
379                if (!search_for_iter (end_iter, out end_iter_tmp) && tool_cycle_search.active) {
380                    text_buffer.get_start_iter (out start_iter);
381                    search_for_iter (start_iter, out end_iter);
382                }
383
384                update_tool_arrows (search_string);
385            }
386        }
387
388        private void update_tool_arrows (string search_string) {
389            /* We don't need to compute the sensitive states of these widgets
390             * if they don't exist. */
391            if (tool_arrow_up != null && tool_arrow_down != null) {
392                if (search_string == "") {
393                    tool_arrow_up.sensitive = false;
394                    tool_arrow_down.sensitive = false;
395                } else if (text_buffer != null) {
396                    Gtk.TextIter? start_iter, end_iter;
397                    Gtk.TextIter? tmp_start_iter, tmp_end_iter;
398
399                    bool is_in_start, is_in_end;
400
401                    text_buffer.get_start_iter (out tmp_start_iter);
402                    text_buffer.get_end_iter (out tmp_end_iter);
403
404                    text_buffer.get_selection_bounds (out start_iter, out end_iter);
405
406                    is_in_start = start_iter.compare (tmp_start_iter) == 0;
407                    is_in_end = end_iter.compare (tmp_end_iter) == 0;
408
409                    if (!is_in_end) {
410                        tool_arrow_down.sensitive = search_context.forward (
411                            end_iter, out tmp_start_iter, out tmp_end_iter, null
412                        );
413                    } else {
414                        tool_arrow_down.sensitive = false;
415                    }
416
417                    if (!is_in_start) {
418                        tool_arrow_up.sensitive = search_context.backward (
419                            start_iter, out tmp_start_iter, out end_iter, null
420                        );
421                    } else {
422                        tool_arrow_up.sensitive = false;
423                    }
424                }
425            }
426        }
427
428        private bool on_search_entry_key_press (Gdk.EventKey event) {
429            /* We don't need to perform search if there is nothing to search... */
430            if (search_entry.text == "") {
431                return false;
432            }
433
434            string key = Gdk.keyval_name (event.keyval);
435            if (Gdk.ModifierType.SHIFT_MASK in event.state) {
436                key = "<Shift>" + key;
437            }
438
439            switch (key) {
440                case "<Shift>Return":
441                case "Up":
442                    search_previous ();
443                    return true;
444                case "Return":
445                case "Down":
446                    search_next ();
447                    return true;
448                case "Escape":
449                    text_view.grab_focus ();
450                    return true;
451                case "Tab":
452                    if (search_entry.is_focus) {
453                        replace_entry.grab_focus ();
454                    }
455
456                    return true;
457            }
458
459            return false;
460        }
461
462        private bool on_replace_entry_key_press (Gdk.EventKey event) {
463            /* We don't need to perform search if there is nothing to search… */
464            if (search_entry.text == "") {
465                return false;
466            }
467
468            switch (Gdk.keyval_name (event.keyval)) {
469                case "Up":
470                    search_previous ();
471                    return true;
472                case "Down":
473                    search_next ();
474                    return true;
475                case "Escape":
476                    text_view.grab_focus ();
477                    return true;
478                case "Tab":
479                    if (replace_entry.is_focus) {
480                        search_entry.grab_focus ();
481                    }
482
483                    return true;
484            }
485
486            return false;
487        }
488
489        private bool is_case_sensitive (string search_string) {
490            return case_sensitive_button.active ||
491                   !((search_string.up () == search_string) || (search_string.down () == search_string));
492        }
493    }
494}
495