1# Terminator by Chris Jones <cmsj@tenshu.net>
2# GPL v2 only
3"""searchbar.py - classes necessary to provide a terminal search bar"""
4
5import gi
6from gi.repository import Gtk, Gdk
7gi.require_version('Vte', '2.91')  # vte-0.38 (gnome-3.14)
8from gi.repository import Vte
9from gi.repository import GObject
10from gi.repository import GLib
11
12from .translation import _
13from .config import Config
14from . import regex
15from .util import dbg
16
17# pylint: disable-msg=R0904
18class Searchbar(Gtk.HBox):
19    """Class implementing the Searchbar widget"""
20
21    __gsignals__ = {
22        'end-search': (GObject.SignalFlags.RUN_LAST, None, ()),
23    }
24
25    entry = None
26    next = None
27    prev = None
28    wrap = None
29
30    vte = None
31    config = None
32
33    searchstring = None
34    searchre = None
35
36    def __init__(self):
37        """Class initialiser"""
38        GObject.GObject.__init__(self)
39
40        # default regex flags are not CASELESS
41        self.regex_flags_pcre2 = regex.FLAGS_PCRE2
42        self.regex_flags_glib = regex.FLAGS_GLIB
43
44        self.config = Config()
45
46        self.get_style_context().add_class("terminator-terminal-searchbar")
47
48        # Search text
49        self.entry = Gtk.Entry()
50        self.entry.set_activates_default(True)
51        self.entry.show()
52        self.entry.connect('activate', self.do_search)
53        self.entry.connect('key-press-event', self.search_keypress)
54
55        # Label
56        label = Gtk.Label(label=_('Search:'))
57        label.show()
58
59        # Close Button
60        close = Gtk.Button()
61        close.set_relief(Gtk.ReliefStyle.NONE)
62        close.set_focus_on_click(False)
63        icon = Gtk.Image()
64        icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
65        close.add(icon)
66        close.set_name('terminator-search-close-button')
67        if hasattr(close, 'set_tooltip_text'):
68            close.set_tooltip_text(_('Close Search bar'))
69        close.connect('clicked', self.end_search)
70        close.show_all()
71
72        # Next Button
73        self.next = Gtk.Button.new_with_label('Next')
74        self.next.show()
75        self.next.set_sensitive(False)
76        self.next.connect('clicked', self.next_search)
77
78        # Previous Button
79        self.prev = Gtk.Button.new_with_label('Prev')
80        self.prev.show()
81        self.prev.set_sensitive(False)
82        self.prev.connect('clicked', self.prev_search)
83
84        # Match Case checkbox
85        self.match_case = Gtk.CheckButton.new_with_label('Match Case')
86        self.match_case.show()
87        self.match_case.set_sensitive(True)
88        self.match_case.set_active(self.config.base.get_item('case_sensitive'))
89        self.match_case.connect('toggled', self.match_case_toggled)
90
91        # Wrap checkbox
92        self.wrap = Gtk.CheckButton.new_with_label('Wrap')
93        self.wrap.show()
94        self.wrap.set_sensitive(True)
95        self.wrap.set_active(True)
96        self.wrap.connect('toggled', self.wrap_toggled)
97
98        # Invert Search checkbox
99        self.invert_search = Gtk.CheckButton.new_with_label('Invert Search')
100        self.invert_search.show()
101        self.search_is_inverted = self.config.base.get_item('invert_search')
102        self.invert_search.set_active(self.search_is_inverted)
103        self.invert_search.connect('toggled', self.wrap_invert_search)
104
105        self.pack_start(label, False, True, 0)
106        self.pack_start(self.entry, True, True, 0)
107        self.pack_start(self.prev, False, False, 0)
108        self.pack_start(self.next, False, False, 0)
109        self.pack_start(self.wrap, False, False, 0)
110        self.pack_start(self.match_case, False, False, 0)
111        self.pack_start(self.invert_search, False, False, 0)
112        self.pack_end(close, False, False, 0)
113
114        self.hide()
115        self.set_no_show_all(True)
116
117    def wrap_invert_search(self, toggled):
118        self.search_is_inverted = toggled.get_active()
119        self.invert_search.set_active(toggled.get_active())
120        self.config.base.set_item('invert_search', toggled.get_active())
121        self.config.save()
122
123    def wrap_toggled(self, toggled):
124        toggled_state = toggled.get_active()
125        self.vte.search_set_wrap_around(toggled_state)
126        if toggled_state:
127            self.prev.set_sensitive(True)
128            self.next.set_sensitive(True)
129
130    def match_case_toggled(self, toggled):
131        """Handles Match Case checkbox toggles"""
132
133        toggled_state = toggled.get_active()
134        if not toggled_state:
135            #  Add the CASELESS regex flags when the checkbox is not checked.
136            try:
137                self.regex_flags_pcre2 = (regex.FLAGS_PCRE2 | regex.PCRE2_CASELESS)
138            except TypeError:
139                # if PCRE2 support is not available
140                pass
141
142            # The code will fall back to use this GLib regex when PCRE2 is not available
143            self.regex_flags_glib = (regex.FLAGS_GLIB | regex.GLIB_CASELESS)
144        else:
145            # Default state of the check box is unchecked. CASELESS regex flags are not added.
146            self.regex_flags_pcre2 = regex.FLAGS_PCRE2
147            self.regex_flags_glib = regex.FLAGS_GLIB
148
149        self.config.base.set_item('case_sensitive', toggled_state)
150        self.config.save()
151        self.do_search(self.entry) #  Start a new search everytime the check box is toggled.
152
153    def get_vte(self):
154        """Find our parent widget"""
155        parent = self.get_parent()
156        if parent:
157            self.vte = parent.vte
158            #turn on wrap by default
159            self.vte.search_set_wrap_around(True)
160
161    # pylint: disable-msg=W0613
162    def search_keypress(self, widget, event):
163        """Handle keypress events"""
164        key = Gdk.keyval_name(event.keyval)
165        if key == 'Escape':
166            self.end_search()
167        else:
168            self.prev.set_sensitive(False)
169            self.next.set_sensitive(False)
170
171    def start_search(self):
172        """Show ourselves"""
173        if not self.vte:
174            self.get_vte()
175
176        self.show()
177        self.entry.grab_focus()
178
179    def do_search(self, widget):
180        """Trap and re-emit the clicked signal"""
181        dbg('entered do_search')
182        searchtext = self.entry.get_text()
183        dbg('searchtext: %s' % searchtext)
184        if searchtext == '':
185            return
186
187        self.searchre = None
188        if regex.FLAGS_PCRE2:
189            try:
190                self.searchre = Vte.Regex.new_for_search(searchtext, len(searchtext), self.regex_flags_pcre2)
191                dbg('search RE: %s' % self.searchre)
192                self.vte.search_set_regex(self.searchre, 0)
193            except GLib.Error:
194                # happens when PCRE2 support is not builtin (Ubuntu < 19.10)
195                pass
196
197        if not self.searchre:
198            # fall back to old GLib regex
199            self.searchre = GLib.Regex(searchtext, self.regex_flags_glib, 0)
200            dbg('search RE: %s' % self.searchre)
201            self.vte.search_set_gregex(self.searchre, 0)
202
203        self.next.set_sensitive(True)
204        self.prev.set_sensitive(True)
205        # switch search direction based on the inversion checkbox
206        if not self.search_is_inverted:
207            self.next_search(None)
208        else:
209            self.prev_search(None)
210
211    def next_search(self, widget):
212        """Search forwards and jump to the next result, if any"""
213        found_result = self.vte.search_find_next()
214        if not self.wrap.get_active():
215            self.next.set_sensitive(found_result)
216        else:
217            self.next.set_sensitive(True)
218        self.prev.set_sensitive(True)
219        return
220
221    def prev_search(self, widget):
222        """Jump back to the previous search"""
223        found_result = self.vte.search_find_previous()
224        if not self.wrap.get_active():
225            self.prev.set_sensitive(found_result)
226        else:
227            self.prev.set_sensitive(True)
228        self.next.set_sensitive(True)
229        return
230
231    def end_search(self, widget=None):
232        """Trap and re-emit the end-search signal"""
233        self.searchstring = None
234        self.searchre = None
235        self.emit('end-search')
236
237    def get_search_term(self):
238        """Return the currently set search term"""
239        return(self.entry.get_text())
240
241GObject.type_register(Searchbar)
242