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 "features.hpp"
20 
21 #include <glibmm/pattern.h>
22 #include <gtkmm/textview.h>
23 
24 #ifdef WITH_GTKSOURCEVIEW2
25 # include <gtksourceview/gtksourcebuffer.h>
26 #endif
27 
28 #ifdef WITH_GTKSPELL
29 # include <obby/format_string.hpp>
30 # include <gtkmm/messagedialog.h>
31 # include <gtkspell/gtkspell.h>
32 #endif
33 
34 #include "common.hpp"
35 #include "preferences.hpp"
36 #include "docwindow.hpp"
37 
38 namespace
39 {
wrap_mode_from_preferences(const Gobby::Preferences & pref)40 	GtkWrapMode wrap_mode_from_preferences(const Gobby::Preferences& pref)
41 	{
42 		if(pref.view.wrap_text)
43 		{
44 			if(pref.view.wrap_words)
45 				return GTK_WRAP_CHAR;
46 			else
47 				return GTK_WRAP_WORD;
48 		}
49 		else
50 		{
51 			return GTK_WRAP_NONE;
52 		}
53 	}
54 }
55 
DocWindow(LocalDocumentInfo & info,const Preferences & preferences)56 Gobby::DocWindow::DocWindow(LocalDocumentInfo& info,
57                             const Preferences& preferences):
58 	m_view(GTK_SOURCE_VIEW(gtk_source_view_new())),
59 	m_info(info), m_doc(info.get_content() ),
60 	m_preferences(preferences), /*m_editing(false),*/
61 	m_title(info.get_title() ),
62 	m_scrolly(0.0),
63 	m_scroll_restore(false)
64 {
65 	if(!info.is_subscribed() )
66 	{
67 		throw std::logic_error(
68 			"Gobby::DocWindow::DocWindow:\n"
69 			"Local user is not subscribed"
70 		);
71 	}
72 
73 	GtkSourceBuffer* buffer = m_doc.get_buffer();
74 	gtk_text_view_set_buffer(GTK_TEXT_VIEW(m_view), GTK_TEXT_BUFFER(buffer));
75 
76 	Glib::RefPtr<Gtk::TextBuffer> cpp_buffer =
77 		Glib::wrap(GTK_TEXT_BUFFER(buffer), true);
78 
79 #ifdef WITH_GTKSOURCEVIEW2
80 	// Set source language by filename
81 	gtk_source_buffer_set_highlight_syntax(buffer, FALSE);
82 #else
83 	gtk_source_buffer_set_highlight(buffer, FALSE);
84 #endif
85 
86 	// Enable indent-on-tab
87 	gtk_source_view_set_indent_on_tab(m_view, TRUE);
88 
89 	for(Preferences::FileList::iterator iter = preferences.files.begin();
90 	    iter != preferences.files.end();
91 	    ++ iter)
92 	{
93 		Glib::PatternSpec spec(iter.pattern());
94 		if(spec.match(info.get_title()) )
95 		{
96 			gtk_source_buffer_set_language(buffer, iter.language());
97 #ifdef WITH_GTKSOURCEVIEW2
98 			gtk_source_buffer_set_highlight_syntax(buffer, TRUE);
99 #else
100 			gtk_source_buffer_set_highlight(buffer, TRUE);
101 #endif
102 		}
103 	}
104 
105 #ifdef WITH_GTKSOURCEVIEW2
106 	// Set a theme so we see anything.
107 	// TODO: This should be temporary code until gtksourceview2 sets a default
108 	// theme.
109 /*	GtkSourceStyleManager* sm = gtk_source_style_manager_new();
110 	GtkSourceStyleScheme* scheme = gtk_source_style_manager_get_scheme(sm, "gvim");
111 	gtk_source_buffer_set_style_scheme(buffer, scheme);
112 	g_object_unref(G_OBJECT(sm));*/
113 #endif
114 
115 	cpp_buffer->signal_mark_set().connect(
116 		sigc::mem_fun(*this, &DocWindow::on_mark_set)
117 	);
118 
119 	cpp_buffer->signal_changed().connect(
120 		sigc::mem_fun(*this, &DocWindow::on_changed)
121 	);
122 
123 	m_doc.local_insert_event().connect(
124 		sigc::mem_fun(*this, &DocWindow::on_local_insert)
125 	);
126 
127 	m_doc.local_erase_event().connect(
128 		sigc::mem_fun(*this, &DocWindow::on_local_erase)
129 	);
130 
131 	m_doc.remote_insert_before_event().connect(
132 		sigc::mem_fun(*this, &DocWindow::on_remote_insert_before)
133 	);
134 
135 	m_doc.remote_erase_before_event().connect(
136 		sigc::mem_fun(*this, &DocWindow::on_remote_erase_before)
137 	);
138 
139 	m_doc.remote_insert_after_event().connect(
140 		sigc::mem_fun(*this, &DocWindow::on_remote_insert_after)
141 	);
142 
143 	m_doc.remote_erase_after_event().connect(
144 		sigc::mem_fun(*this, &DocWindow::on_remote_erase_after)
145 	);
146 
147 	apply_preferences();
148 	gtk_text_buffer_set_modified(GTK_TEXT_BUFFER(buffer), !m_doc.empty());
149 
150 	set_shadow_type(Gtk::SHADOW_IN);
151 	set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
152 	gtk_container_add(GTK_CONTAINER(gobj()), GTK_WIDGET(m_view));
153 
154 #ifdef WITH_GTKSPELL
155 	// Set up spell checking.
156 	GError *error = NULL;
157 	if(gtkspell_new_attach(GTK_TEXT_VIEW(m_view), NULL, &error) == NULL)
158 	{
159 		obby::format_string str(_("GtkSpell error: %0%") );
160 		str << error->message;
161 		g_error_free(error);
162 
163 		// Initialization failed, show error message.
164 		Gtk::MessageDialog dlg(
165 			str.str(),
166 			false,
167 			Gtk::MESSAGE_ERROR,
168 			Gtk::BUTTONS_CLOSE,
169 			true);
170 		dlg.run();
171 	}
172 #endif
173 }
174 
get_cursor_position(unsigned int & row,unsigned int & col)175 void Gobby::DocWindow::get_cursor_position(unsigned int& row,
176                                            unsigned int& col)
177 {
178 	Glib::RefPtr<Gtk::TextBuffer> cpp_buffer = Glib::wrap(
179 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view)), true);
180 
181 	Glib::RefPtr<Gtk::TextMark> mark = cpp_buffer->get_insert();
182 
183 	// Gtk::TextBuffer::Mark::get_iter is not const. Why not? It prevents
184 	// this function from being const.
185 	Gtk::TextBuffer::iterator iter = mark->get_iter();
186 
187 	// Row is trivial
188 	row = iter.get_line(); col = 0;
189 	int chars = iter.get_line_offset();
190 
191 	// Tab characters expand to more than one column
192 	unsigned int tabs = m_preferences.editor.tab_width;
193 	for(iter.set_line_offset(0); iter.get_line_offset() < chars; ++ iter)
194 	{
195 		unsigned int width = 1;
196 		if(*iter == '\t')
197 		{
198 			width = (tabs - iter.get_line_offset() % tabs) % tabs;
199 			if(width == 0) width = tabs;
200 		}
201 
202 		col += width;
203 	}
204 }
205 
set_selection(const Gtk::TextIter & begin,const Gtk::TextIter & end)206 void Gobby::DocWindow::set_selection(const Gtk::TextIter& begin,
207                                      const Gtk::TextIter& end)
208 {
209 	gtk_text_buffer_select_range(
210 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view)),
211 		begin.gobj(),
212 		end.gobj()
213 		);
214 
215 	gtk_text_view_scroll_to_mark(
216 		GTK_TEXT_VIEW(m_view),
217 		gtk_text_buffer_get_insert(gtk_text_view_get_buffer(
218 			GTK_TEXT_VIEW(m_view))),
219 		0.1, FALSE, 0.0, 0.0);
220 }
221 
disable()222 void Gobby::DocWindow::disable()
223 {
224 	gtk_widget_set_sensitive(GTK_WIDGET(m_view), FALSE);
225 }
226 
get_selected_text() const227 Glib::ustring Gobby::DocWindow::get_selected_text() const
228 {
229 	GtkTextIter start, end;
230 	gtk_text_buffer_get_selection_bounds(
231 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view)), &start, &end);
232 
233 	Gtk::TextIter start_cpp(&start), end_cpp(&end);
234 	return start_cpp.get_slice(end_cpp);
235 }
236 
get_title() const237 const Glib::ustring& Gobby::DocWindow::get_title() const
238 {
239 	return m_title;
240 }
241 
get_modified() const242 bool Gobby::DocWindow::get_modified() const
243 {
244 	return gtk_text_buffer_get_modified(
245 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view))) == TRUE;
246 }
247 
grab_focus()248 void Gobby::DocWindow::grab_focus()
249 {
250 	Gtk::ScrolledWindow::grab_focus();
251 	gtk_widget_grab_focus(GTK_WIDGET(m_view));
252 }
253 
get_language() const254 GtkSourceLanguage* Gobby::DocWindow::get_language() const
255 {
256 	return gtk_source_buffer_get_language(GTK_SOURCE_BUFFER(
257 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view))));
258 }
259 
set_language(GtkSourceLanguage * language)260 void Gobby::DocWindow::set_language(GtkSourceLanguage* language)
261 {
262 	GtkSourceBuffer* buffer = GTK_SOURCE_BUFFER(
263 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view)));
264 
265 	gtk_source_buffer_set_language(buffer, language);
266 
267 #ifdef WITH_GTKSOURCEVIEW2
268 	gtk_source_buffer_set_highlight_syntax(buffer, language != NULL);
269 #else
270 	gtk_source_buffer_set_highlight(buffer, language != NULL);
271 #endif
272 
273 	m_signal_language_changed.emit();
274 }
275 
get_preferences() const276 const Gobby::Preferences& Gobby::DocWindow::get_preferences() const
277 {
278 	return m_preferences;
279 }
280 
set_preferences(const Preferences & preferences)281 void Gobby::DocWindow::set_preferences(const Preferences& preferences)
282 {
283 	m_preferences = preferences;
284 	apply_preferences();
285 }
286 
get_content() const287 Glib::ustring Gobby::DocWindow::get_content() const
288 {
289 	Glib::RefPtr<Gtk::TextBuffer> buffer = Glib::wrap(
290 		gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view)), true);
291 
292 	return buffer->get_text();
293 }
294 
295 Gobby::DocWindow::signal_cursor_moved_type
cursor_moved_event() const296 Gobby::DocWindow::cursor_moved_event() const
297 {
298 	return m_signal_cursor_moved;
299 }
300 
301 Gobby::DocWindow::signal_content_changed_type
content_changed_event() const302 Gobby::DocWindow::content_changed_event() const
303 {
304 	return m_signal_content_changed;
305 }
306 
307 Gobby::DocWindow::signal_language_changed_type
language_changed_event() const308 Gobby::DocWindow::language_changed_event() const
309 {
310 	return m_signal_language_changed;
311 }
312 
get_info() const313 const Gobby::LocalDocumentInfo& Gobby::DocWindow::get_info() const
314 {
315 	return m_info;
316 }
317 
get_info()318 Gobby::LocalDocumentInfo& Gobby::DocWindow::get_info()
319 {
320 	return m_info;
321 }
322 
get_document() const323 const Gobby::Document& Gobby::DocWindow::get_document() const
324 {
325 	return m_doc;
326 }
327 
on_mark_set(const Gtk::TextIter & location,const Glib::RefPtr<Gtk::TextMark> & mark)328 void Gobby::DocWindow::on_mark_set(const Gtk::TextIter& location,
329                                    const Glib::RefPtr<Gtk::TextMark>& mark)
330 {
331 	// Mark was deleted
332 	if(!mark) return;
333 
334 	if(mark->gobj() == gtk_text_buffer_get_insert(
335 	   gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view))))
336 	{
337 		m_signal_cursor_moved.emit();
338 	}
339 }
340 
on_changed()341 void Gobby::DocWindow::on_changed()
342 {
343 	// Cursor may have moved.
344 	// TODO: Check if the cursor really moved
345 	m_signal_cursor_moved.emit();
346 	m_signal_content_changed.emit();
347 }
348 
on_local_insert(obby::position pos,const std::string & error)349 void Gobby::DocWindow::on_local_insert(obby::position pos,
350                                        const std::string& error)
351 {
352 	m_info.insert(pos, error);
353 }
354 
on_local_erase(obby::position pos,obby::position len)355 void Gobby::DocWindow::on_local_erase(obby::position pos,
356                                       obby::position len)
357 {
358 	m_info.erase(pos, len);
359 }
360 
on_remote_insert_before(obby::position,const std::string & text)361 void Gobby::DocWindow::on_remote_insert_before(obby::position,
362                                                const std::string& text)
363 {
364 	store_scroll();
365 }
366 
on_remote_erase_before(obby::position pos,obby::position len)367 void Gobby::DocWindow::on_remote_erase_before(obby::position pos,
368                                               obby::position len)
369 {
370 	store_scroll();
371 }
372 
on_remote_insert_after(obby::position,const std::string & text)373 void Gobby::DocWindow::on_remote_insert_after(obby::position,
374                                               const std::string& text)
375 {
376 	restore_scroll();
377 }
378 
on_remote_erase_after(obby::position pos,obby::position len)379 void Gobby::DocWindow::on_remote_erase_after(obby::position pos,
380                                              obby::position len)
381 {
382 	restore_scroll();
383 }
384 
apply_preferences()385 void Gobby::DocWindow::apply_preferences()
386 {
387 	GtkTextBuffer* buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(m_view));
388 
389 #ifdef WITH_GTKSOURCEVIEW2
390 	gtk_source_view_set_tab_width(GTK_SOURCE_VIEW(m_view),
391 		m_preferences.editor.tab_width);
392 #else
393 	gtk_source_view_set_tabs_width(GTK_SOURCE_VIEW(m_view),
394 		m_preferences.editor.tab_width);
395 #endif
396 
397 	gtk_source_view_set_insert_spaces_instead_of_tabs(GTK_SOURCE_VIEW(m_view),
398 		m_preferences.editor.tab_spaces);
399 	gtk_source_view_set_auto_indent(GTK_SOURCE_VIEW(m_view),
400 		m_preferences.editor.indentation_auto);
401 #ifdef WITH_GTKSOURCEVIEW2
402 	gtk_source_view_set_smart_home_end(GTK_SOURCE_VIEW(m_view),
403 		m_preferences.editor.homeend_smart ?
404 		GTK_SOURCE_SMART_HOME_END_ALWAYS :
405 		GTK_SOURCE_SMART_HOME_END_DISABLED);
406 #else
407 	gtk_source_view_set_smart_home_end(GTK_SOURCE_VIEW(m_view),
408 		m_preferences.editor.homeend_smart);
409 #endif
410 
411 	gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(m_view),
412 		wrap_mode_from_preferences(m_preferences));
413 	gtk_source_view_set_show_line_numbers(GTK_SOURCE_VIEW(m_view),
414 		m_preferences.view.linenum_display);
415 	gtk_source_view_set_highlight_current_line(GTK_SOURCE_VIEW(m_view),
416 		m_preferences.view.curline_highlight);
417 #ifdef WITH_GTKSOURCEVIEW2
418 	gtk_source_view_set_show_right_margin(GTK_SOURCE_VIEW(m_view),
419 		m_preferences.view.margin_display);
420 	gtk_source_view_set_right_margin_position(GTK_SOURCE_VIEW(m_view),
421 		m_preferences.view.margin_pos);
422 	gtk_source_buffer_set_highlight_matching_brackets(GTK_SOURCE_BUFFER(buffer),
423 		m_preferences.view.bracket_highlight);
424 #else
425 	gtk_source_view_set_show_margin(GTK_SOURCE_VIEW(m_view),
426 		m_preferences.view.margin_display);
427 	gtk_source_view_set_margin(GTK_SOURCE_VIEW(m_view),
428 		m_preferences.view.margin_pos);
429 	gtk_source_buffer_set_check_brackets(GTK_SOURCE_BUFFER(buffer),
430 		m_preferences.view.bracket_highlight);
431 #endif
432 
433 	gtk_widget_modify_font(GTK_WIDGET(m_view), m_preferences.font.desc.gobj());
434 
435 	// Cursor position may have changed because of new tab width
436 	// TODO: Only emit if the position really changed
437 	m_signal_cursor_moved.emit();
438 }
439 
store_scroll()440 void Gobby::DocWindow::store_scroll()
441 {
442 	Gdk::Rectangle curs_rect;
443 	int x, y;
444 
445 	Gtk::TextView* cpp_view = Glib::wrap(GTK_TEXT_VIEW(m_view));
446 	cpp_view->get_iter_location(cpp_view->get_buffer()->get_insert()->get_iter(), curs_rect);
447 	cpp_view->buffer_to_window_coords(
448 		Gtk::TEXT_WINDOW_WIDGET,
449 		curs_rect.get_x(), curs_rect.get_y(),
450 		x, y);
451 
452 	m_scrolly = y / static_cast<double>((cpp_view->get_height() - curs_rect.get_height()));
453 	m_scroll_restore = true;
454 }
455 
restore_scroll()456 void Gobby::DocWindow::restore_scroll()
457 {
458 	if(m_scroll_restore)
459 	{
460 		Gtk::TextView* cpp_view = Glib::wrap(GTK_TEXT_VIEW(m_view));
461 		if(m_scrolly >= 0.0 && m_scrolly <= 1.0)
462 			cpp_view->scroll_to(
463 				cpp_view->get_buffer()->get_insert(),
464 				0, 0,
465 				m_scrolly);
466 
467 		m_scroll_restore = false;
468 	}
469 }
470 
471