1 /* gobby - A GTKmm driven libobby client
2 * Copyright (C) 2005 0x539 dev group
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (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 GNU
12 * General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public
15 * License along with this program; if not, write to the Free
16 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19 #include <gtksourceview/gtksourceiter.h>
20 #include <gtkmm/stock.h>
21 #include <gtkmm/messagedialog.h>
22
23 #include "common.hpp"
24 #include "document.hpp"
25 #include "window.hpp"
26 #include "finddialog.hpp"
27
28 namespace
29 {
30 typedef gboolean (*gtk_source_iter_search_func)(
31 const GtkTextIter*,
32 const gchar*,
33 GtkSourceSearchFlags,
34 GtkTextIter*,
35 GtkTextIter*,
36 const GtkTextIter*
37 );
38 }
39
FindDialog(Gobby::Window & parent)40 Gobby::FindDialog::FindDialog(Gobby::Window& parent):
41 ToolWindow(parent),
42 m_gobby(parent),
43 m_label_find(_("Find what:"), Gtk::ALIGN_LEFT),
44 m_label_replace(_("Replace with:"), Gtk::ALIGN_LEFT),
45 m_check_whole_word(_("Match whole word only")),
46 m_check_case(_("Match case")),
47 m_check_regex(_("Match as regular expression")),
48 m_frame_direction(_("Direction")),
49 m_radio_up(m_group_direction, _("_Up"), true),
50 m_radio_down(m_group_direction, _("_Down"), true),
51 m_btn_find(Gtk::Stock::FIND),
52 m_btn_replace(_("_Replace"), true),
53 m_btn_replace_all(_("Replace _all"), true),
54 m_btn_close(Gtk::Stock::CLOSE),
55 m_regex("")
56 {
57 Gtk::Image* replace_img = Gtk::manage(
58 new Gtk::Image(
59 Gtk::Stock::FIND_AND_REPLACE,
60 Gtk::ICON_SIZE_BUTTON
61 )
62 );
63
64 Gtk::Image* replace_all_img = Gtk::manage(
65 new Gtk::Image(
66 Gtk::Stock::FIND_AND_REPLACE,
67 Gtk::ICON_SIZE_BUTTON
68 )
69 );
70
71 m_btn_replace.set_image(*replace_img);
72 m_btn_replace_all.set_image(*replace_all_img);
73
74 m_box_main.set_spacing(12);
75 m_box_main.pack_start(m_box_left);
76 m_box_main.pack_start(m_separator, Gtk::PACK_SHRINK);
77 m_box_main.pack_start(m_box_btns, Gtk::PACK_SHRINK);
78 add(m_box_main);
79
80 m_box_left.pack_start(m_table_entries);
81 m_box_left.pack_start(m_hbox);
82
83 m_table_entries.set_spacings(5);
84 m_table_entries.attach(m_label_find, 0, 1, 0, 1,
85 Gtk::SHRINK | Gtk::FILL, Gtk::EXPAND);
86 m_table_entries.attach(m_label_replace, 0, 1, 1, 2,
87 Gtk::SHRINK | Gtk::FILL, Gtk::EXPAND);
88 m_table_entries.attach(m_entry_find, 1, 2, 0, 1,
89 Gtk::EXPAND | Gtk::FILL, Gtk::EXPAND);
90 m_table_entries.attach(m_entry_replace, 1, 2, 1, 2,
91 Gtk::EXPAND | Gtk::FILL, Gtk::EXPAND);
92
93 m_hbox.pack_start(m_box_options);
94 m_hbox.pack_start(m_frame_direction, Gtk::PACK_SHRINK);
95 m_hbox.set_spacing(10);
96
97 m_box_options.pack_start(m_check_whole_word, Gtk::PACK_EXPAND_WIDGET);
98 m_box_options.pack_start(m_check_case, Gtk::PACK_EXPAND_WIDGET);
99 m_box_options.pack_start(m_check_regex, Gtk::PACK_EXPAND_WIDGET);
100
101 m_frame_direction.add(m_box_direction);
102 m_box_direction.set_border_width(4);
103 m_box_direction.pack_start(m_radio_up, Gtk::PACK_EXPAND_WIDGET);
104 m_box_direction.pack_start(m_radio_down, Gtk::PACK_EXPAND_WIDGET);
105
106 m_box_btns.set_spacing(5);
107 m_box_btns.pack_start(m_btn_find, Gtk::PACK_EXPAND_PADDING);
108 m_box_btns.pack_start(m_btn_replace, Gtk::PACK_EXPAND_PADDING);
109 m_box_btns.pack_start(m_btn_replace_all, Gtk::PACK_EXPAND_PADDING);
110 m_box_btns.pack_start(m_btn_close, Gtk::PACK_EXPAND_PADDING);
111
112 m_entry_find.signal_changed().connect(
113 sigc::mem_fun(*this, &FindDialog::update_regex));
114 m_check_case.signal_toggled().connect(
115 sigc::mem_fun(*this, &FindDialog::update_regex));
116 m_check_regex.signal_toggled().connect(
117 sigc::mem_fun(*this, &FindDialog::update_regex));
118
119 m_entry_find.signal_activate().connect(
120 sigc::mem_fun(*this, &FindDialog::on_find) );
121 m_entry_replace.signal_activate().connect(
122 sigc::mem_fun(*this, &FindDialog::on_replace) );
123
124 m_radio_down.set_active(true);
125
126 m_btn_close.signal_clicked().connect(
127 sigc::mem_fun(*this, &FindDialog::hide));
128
129 m_btn_find.signal_clicked().connect(
130 sigc::mem_fun(*this, &FindDialog::on_find));
131 m_btn_replace.signal_clicked().connect(
132 sigc::mem_fun(*this, &FindDialog::on_replace) );
133 m_btn_replace_all.signal_clicked().connect(
134 sigc::mem_fun(*this, &FindDialog::on_replace_all) );
135
136 GTK_WIDGET_SET_FLAGS(m_btn_find.gobj(), GTK_CAN_DEFAULT);
137 set_default(m_btn_find);
138
139 set_border_width(16);
140
141 set_resizable(false);
142 show_all_children();
143
144 m_check_regex.hide();
145 set_search_only(true);
146 }
147
set_search_only(bool search_only)148 void Gobby::FindDialog::set_search_only(bool search_only)
149 {
150 void(Gtk::Widget::*show_func)();
151 show_func = search_only ? &Gtk::Widget::hide : &Gtk::Widget::show;
152
153 sigc::bind(show_func, sigc::ref(m_entry_replace) )();
154 sigc::bind(show_func, sigc::ref(m_label_replace) )();
155 sigc::bind(show_func, sigc::ref(m_btn_replace) )();
156 sigc::bind(show_func, sigc::ref(m_btn_replace_all) )();
157
158 set_title(search_only ? _("Search") : _("Search and replace") );
159 }
160
on_show()161 void Gobby::FindDialog::on_show()
162 {
163 ToolWindow::on_show();
164 m_entry_find.grab_focus();
165 }
166
on_find()167 void Gobby::FindDialog::on_find()
168 {
169 if(m_check_regex.get_active() && m_regex_changed)
170 compile_regex();
171
172 DocWindow* doc = get_document();
173 if(doc == NULL) return;
174
175 Glib::RefPtr<Gtk::TextBuffer> buf =
176 Glib::wrap(GTK_TEXT_BUFFER(doc->get_document().get_buffer()), true);
177
178 bool result = search_sel(buf->get_insert()->get_iter() );
179 if(!result)
180 {
181 obby::format_string str(
182 _("\"%0%\" has not been found in the document.")
183 );
184
185 str << m_entry_find.get_text();
186
187 Gtk::MessageDialog dlg(
188 *this,
189 str.str(),
190 false,
191 Gtk::MESSAGE_INFO,
192 Gtk::BUTTONS_OK,
193 true
194 );
195
196 dlg.run();
197 }
198 }
199
on_replace()200 void Gobby::FindDialog::on_replace()
201 {
202 if(m_check_regex.get_active() && m_regex_changed)
203 compile_regex();
204
205 DocWindow* doc = get_document();
206 if(doc == NULL) return;
207
208 Glib::RefPtr<Gtk::TextBuffer> buf =
209 Glib::wrap(GTK_TEXT_BUFFER(doc->get_document().get_buffer()), true);
210
211 // Get selected string
212 Glib::ustring sel_str = doc->get_selected_text();
213 Glib::ustring find_str = m_entry_find.get_text();
214
215 // Lowercase both if we are comparing insensitive
216 if(!m_check_case.get_active() )
217 {
218 sel_str.lowercase();
219 find_str.lowercase();
220 }
221
222 // Replace them if they are the same
223 if(sel_str == find_str)
224 {
225 // Replace occurence
226 buf->erase_selection();
227 buf->insert_at_cursor(m_entry_replace.get_text() );
228
229 // ... and find the next
230 search_sel(buf->get_insert()->get_iter() );
231 }
232 else
233 {
234 // Search the first occurence
235 on_find();
236 }
237 }
238
on_replace_all()239 void Gobby::FindDialog::on_replace_all()
240 {
241 if(m_check_regex.get_active() && m_regex_changed)
242 compile_regex();
243
244 DocWindow* doc = get_document();
245 if(doc == NULL) return;
246
247 Glib::RefPtr<Gtk::TextBuffer> buf =
248 Glib::wrap(GTK_TEXT_BUFFER(doc->get_document().get_buffer()), true);
249
250 Gtk::TextIter begin = buf->begin();
251
252 unsigned int replace_count = 0;
253 Gtk::TextIter match_start, match_end;
254 while(search_range(begin, NULL, match_start, match_end) )
255 {
256 begin = buf->erase(match_start, match_end);
257 begin = buf->insert(begin, m_entry_replace.get_text() );
258
259 ++ replace_count;
260 }
261
262 Glib::ustring msg;
263 if(replace_count == 0)
264 {
265 msg = _("No occurence has been replaced");
266 }
267 else
268 {
269 obby::format_string str(
270 ngettext(
271 "%0% occurence has been replaced",
272 "%0% occurences have been replaced",
273 replace_count
274 )
275 );
276
277 str << replace_count;
278 msg = str.str();
279 }
280
281 Gtk::MessageDialog dlg(
282 *this,
283 msg,
284 false,
285 Gtk::MESSAGE_INFO,
286 Gtk::BUTTONS_OK,
287 true
288 );
289
290 dlg.run();
291 }
292
get_document()293 Gobby::DocWindow* Gobby::FindDialog::get_document()
294 {
295 DocWindow* doc = m_gobby.get_current_document();
296
297 if(doc == NULL)
298 {
299 Gtk::MessageDialog dlg(
300 *this,
301 _("No document currently opened"),
302 false,
303 Gtk::MESSAGE_ERROR,
304 Gtk::BUTTONS_OK,
305 true
306 );
307
308 dlg.run();
309 }
310
311 return doc;
312 }
313
search_sel(const Gtk::TextIter & from)314 bool Gobby::FindDialog::search_sel(const Gtk::TextIter& from)
315 {
316 DocWindow* doc = get_document();
317 if(doc == NULL) return false;
318
319 Gtk::TextIter match_start, match_end;
320 if(search_wrap(from, match_start, match_end) )
321 {
322 if(m_radio_down.get_active() )
323 doc->set_selection(match_end, match_start);
324 else
325 doc->set_selection(match_start, match_end);
326
327 return true;
328 }
329
330 return false;
331 }
332
search_wrap(const Gtk::TextIter & from,Gtk::TextIter & match_start,Gtk::TextIter & match_end)333 bool Gobby::FindDialog::search_wrap(const Gtk::TextIter& from,
334 Gtk::TextIter& match_start,
335 Gtk::TextIter& match_end)
336 {
337 Glib::RefPtr<Gtk::TextBuffer> buf = from.get_buffer();
338 Gtk::TextIter start_pos(from);
339
340 bool result = search_range(start_pos, NULL, match_start, match_end);
341 if(result == true) return true;
342
343 Gtk::TextIter restart_pos;
344 if (m_radio_down.get_active())
345 restart_pos = buf->begin();
346 else
347 restart_pos = buf->end();
348
349 // Limit to search to: Normally the position where we started.
350 Gtk::TextIter* relimit = &start_pos;
351 if(m_radio_down.get_active() )
352 {
353 start_pos.forward_chars(m_entry_find.get_text().length() );
354 if(start_pos == buf->end() )
355 relimit = NULL;
356 }
357
358 return search_range(restart_pos, relimit, match_start, match_end);
359 }
360
search_range(const Gtk::TextIter & from,const Gtk::TextIter * to,Gtk::TextIter & match_start,Gtk::TextIter & match_end)361 bool Gobby::FindDialog::search_range(const Gtk::TextIter& from,
362 const Gtk::TextIter* to,
363 Gtk::TextIter& match_start,
364 Gtk::TextIter& match_end)
365 {
366 Gtk::TextIter start_pos(from);
367 while(search_once(start_pos, to, match_start, match_end) )
368 {
369 if(m_check_whole_word.get_active() )
370 {
371 if(!match_start.starts_word() || !match_end.ends_word())
372 {
373 if(m_radio_down.get_active() )
374 start_pos = match_end;
375 else
376 start_pos = match_start;
377
378 continue;
379 }
380 }
381
382 return true;
383 }
384
385 return false;
386 }
387
search_once(const Gtk::TextIter & from,const Gtk::TextIter * to,Gtk::TextIter & match_start,Gtk::TextIter & match_end)388 bool Gobby::FindDialog::search_once(const Gtk::TextIter& from,
389 const Gtk::TextIter* to,
390 Gtk::TextIter& match_start,
391 Gtk::TextIter& match_end)
392 {
393 if(m_check_regex.get_active() )
394 {
395 Glib::RefPtr<Gtk::TextBuffer> buf = from.get_buffer();
396
397 Gtk::TextIter start_pos, limit;
398 if(m_radio_up.get_active() )
399 {
400 limit = from;
401
402 if(to == NULL)
403 start_pos = buf->begin();
404 else
405 start_pos = *to;
406 }
407 else if(m_radio_down.get_active() )
408 {
409 start_pos = from;
410
411 if(to == NULL)
412 limit = buf->end();
413 else
414 limit = *to;
415 }
416
417 Gtk::TextIter begin = buf->end(), end = buf->end();
418 Gtk::TextIter cur_line = start_pos, next_line = start_pos;
419 for(;;)
420 {
421 next_line.forward_line();
422
423 // Get current line of text
424 Glib::ustring line = cur_line.get_slice(next_line);
425
426 // Trim trailing text after limit
427 if(limit.get_line() == cur_line.get_line() )
428 {
429 if(!limit.ends_line() )
430 {
431 line.erase(
432 limit.get_line_offset() -
433 cur_line.get_line_offset()
434 );
435 }
436 }
437
438 regex::match_options options =
439 regex::match_options::NONE;
440
441 if(!cur_line.starts_line() )
442 options |= regex::match_options::NOT_BOL;
443
444 if(cur_line.get_line() == limit.get_line() &&
445 !limit.ends_line() )
446 options |= regex::match_options::NOT_EOL;
447
448 std::pair<std::size_t, std::size_t> pos;
449 bool result = m_regex.find(
450 line.c_str(),
451 pos,
452 options
453 );
454
455 if(result == true)
456 {
457 begin = end = cur_line;
458 begin.set_line_index(
459 begin.get_line_index() + pos.first
460 );
461
462 end.set_line_index(
463 end.get_line_index() + pos.second
464 );
465
466 // Match after limit
467 if(end > limit) break;
468
469 // First match is result if searching forward
470 if(m_radio_down.get_active() )
471 {
472 match_start = begin;
473 match_end = end;
474
475 return true;
476 }
477 }
478
479 cur_line = next_line;
480 if(cur_line > limit || cur_line == buf->end() )
481 break;
482 }
483
484 if(m_radio_up.get_active() )
485 {
486 // No match for backward search
487 if(begin == buf->end() && end == buf->end() )
488 return false;
489
490 match_start = begin;
491 match_end = end;
492
493 return true;
494 }
495
496 // No match for forward search
497 return false;
498 }
499 else
500 {
501 GtkSourceSearchFlags flags = GtkSourceSearchFlags(0);
502 if(!m_check_case.get_active() )
503 flags = GTK_SOURCE_SEARCH_CASE_INSENSITIVE;
504
505 gtk_source_iter_search_func search_func =
506 gtk_source_iter_forward_search;
507
508 if(m_radio_up.get_active() )
509 search_func = gtk_source_iter_backward_search;
510
511 Glib::ustring find_str = m_entry_find.get_text();
512 GtkTextIter match_start_gtk, match_end_gtk;
513 gboolean result = search_func(
514 from.gobj(),
515 find_str.c_str(),
516 flags,
517 &match_start_gtk,
518 &match_end_gtk,
519 to != NULL ? to->gobj() : NULL
520 );
521
522 if(result == TRUE)
523 {
524 match_start = Gtk::TextIter(&match_start_gtk);
525 match_end = Gtk::TextIter(&match_end_gtk);
526
527 return true;
528 }
529
530 return false;
531 }
532 }
533
update_regex()534 void Gobby::FindDialog::update_regex()
535 {
536 if (m_check_regex.get_active())
537 m_regex_changed = true;
538 else
539 m_regex_changed = false;
540 }
541
compile_regex()542 void Gobby::FindDialog::compile_regex()
543 {
544 if (m_check_case.get_active())
545 {
546 m_regex.reset(
547 m_entry_find.get_text().c_str(),
548 regex::compile_options::EXTENDED
549 );
550 }
551 else
552 {
553 m_regex.reset(
554 m_entry_find.get_text().c_str(),
555 regex::compile_options::EXTENDED |
556 regex::compile_options::IGNORE_CASE
557 );
558 }
559
560 m_regex_changed = false;
561 }
562
563