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