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