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