1 /*
2  * Copyright (C) 2005-2006 Evgeniy <dushistov@mail.ru>
3  * Copyright 2011 kubtek <kubtek@mail.com>
4  *
5  * This file is part of StarDict.
6  *
7  * StarDict is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * StarDict is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with StarDict.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 #ifdef HAVE_CONFIG_H
22 #  include "config.h"
23 #endif
24 
25 #include <cstring>
26 #include <iostream>
27 #include <gtk/gtk.h>
28 #include <glib/gi18n.h>
29 
30 #include "conf.h"
31 #include "lib/utils.h"
32 #include "stardict.h"
33 #include "lib/xml_str.h"
34 
35 #include "articleview.h"
36 #include "desktop.h"
37 
38 //#define DEBUG
39 //#define DDEBUG
40 
41 /* Helper class.
42  * Provides a means to generate a unique mark name and insert it at the
43  * specified point. All marks inserted by this class are deleted when this
44  * object is destroyed. */
45 class Marks {
46 public:
Marks(PangoWidgetBase * pango_view)47 	Marks(PangoWidgetBase* pango_view)
48 	:
49 		mark_num_(-1),
50 		pango_view_(pango_view),
51 		insert_last_where_mark_(),
52 		insert_last_offset_(-1),
53 		insert_last_gravity_(false),
54 		insert_last_mark_num_(-1),
55 		clone_last_where_mark_(),
56 		clone_last_gravity_(false),
57 		clone_last_mark_num_(-1)
58 	{
59 	}
~Marks(void)60 	~Marks(void)
61 	{
62 		int size = static_cast<int>(marks_list_.size());
63 		for(int i=0; i<size; ++i) {
64 			if(marks_list_[i] > 0)
65 				pango_view_->delete_mark(gen_mark_name(i).c_str());
66 		}
67 	}
68 	// return value: name of the mark inserted
append_mark(bool left_gravity=true)69 	std::string append_mark(bool left_gravity = true)
70 	{
71 		std::string mark(gen_mark_name(++mark_num_));
72 #ifdef DDEBUG
73 		std::cout << "article::append_mark " << mark << std::endl;
74 #endif
75 		pango_view_->append_mark(mark.c_str(), left_gravity);
76 		marks_list_.push_back(1);
77 #ifdef DEBUG
78 		if(mark_num_ + 1 != static_cast<int>(marks_list_.size()))
79 			g_warning("article::Marks. incorrect list size.");
80 #endif
81 		return mark;
82 	}
83 	// return value: name of the mark inserted
insert_mark(const std::string & where_mark_name,int char_offset=0,bool left_gravity=true)84 	std::string insert_mark(const std::string& where_mark_name,
85 		int char_offset = 0, bool left_gravity = true)
86 	{
87 		if(where_mark_name == insert_last_where_mark_
88 			&& char_offset == insert_last_offset_
89 			&& left_gravity == insert_last_gravity_
90 			&& marks_list_[insert_last_mark_num_] > 0) {
91 			++marks_list_[insert_last_mark_num_];
92 #ifdef DDEBUG
93 		std::cout << "article::insert_mark hit" << std::endl;
94 #endif
95 			return gen_mark_name(insert_last_mark_num_);
96 		}
97 		std::string mark(gen_mark_name(++mark_num_));
98 		insert_last_where_mark_ = where_mark_name;
99 		insert_last_offset_ = char_offset;
100 		insert_last_gravity_ = left_gravity;
101 		insert_last_mark_num_ = mark_num_;
102 #ifdef DDEBUG
103 		std::cout << "article::insert_mark " << mark << std::endl;
104 #endif
105 		pango_view_->insert_mark(mark.c_str(), where_mark_name.c_str(), char_offset,
106 			left_gravity);
107 		marks_list_.push_back(1);
108 #ifdef DEBUG
109 		if(mark_num_ + 1 != static_cast<int>(marks_list_.size()))
110 			g_warning("article::Marks. incorrect list size.");
111 #endif
112 		return mark;
113 	}
114 	/* In is not the same as insert_mark(where_mark_name, 0, left_gravity)! */
clone_mark(const std::string & where_mark_name,bool left_gravity=true)115 	std::string clone_mark(const std::string& where_mark_name,
116 		bool left_gravity = true)
117 	{
118 		if(where_mark_name == clone_last_where_mark_
119 			&& left_gravity == clone_last_gravity_
120 			&& marks_list_[clone_last_mark_num_] > 0) {
121 			++marks_list_[clone_last_mark_num_];
122 #ifdef DDEBUG
123 		std::cout << "article::clone_mark hit" << std::endl;
124 #endif
125 			return gen_mark_name(clone_last_mark_num_);
126 		}
127 		std::string mark(gen_mark_name(++mark_num_));
128 		clone_last_where_mark_ = where_mark_name;
129 		clone_last_gravity_ = left_gravity;
130 		clone_last_mark_num_ = mark_num_;
131 #ifdef DDEBUG
132 		std::cout << "article::clone_mark " << mark << std::endl;
133 #endif
134 		pango_view_->clone_mark(mark.c_str(), where_mark_name.c_str(), left_gravity);
135 		marks_list_.push_back(1);
136 #ifdef DEBUG
137 		if(mark_num_ + 1 != static_cast<int>(marks_list_.size()))
138 			g_warning("article::Marks. incorrect list size.");
139 #endif
140 		return mark;
141 	}
142 private:
gen_mark_name(int num) const143 	std::string gen_mark_name(int num) const
144 	{
145 		glib::CharStr gmark(g_strdup_printf("_ArticleView_mark_%d", num));
146 		return get_impl(gmark);
147 	}
148 private:
149 	typedef std::vector<int> MarksList;
150 	/* marks_list_[i] - number of references to mark i, use gen_mark_name
151 	 * function to get mark name: gen_mark_name(i) */
152 	MarksList marks_list_;
153 	int mark_num_; // last mark number used
154 	PangoWidgetBase* pango_view_;
155 
156 	std::string insert_last_where_mark_;
157 	int insert_last_offset_;
158 	bool insert_last_gravity_;
159 	int insert_last_mark_num_;
160 
161 	std::string clone_last_where_mark_;
162 	bool clone_last_gravity_;
163 	int clone_last_mark_num_;
164 };
165 
166 /* resource data structure
167  * A resource file is loaded only if necessary.
168  * We postpone loading files till a user requests to use them.
169  * That speeds up article loading, saves memory. */
170 class ResData {
171 public:
ResData(size_t iLib_,const std::string & key_)172 	ResData(size_t iLib_, const std::string& key_)
173 	:
174 		iLib(iLib_),
175 		key(key_)
176 	{
177 
178 	}
get_url(void)179 	const char* get_url(void)
180 	{
181 		if(file.empty())
182 			file = gpAppFrame->oLibs.GetStorageFilePath(iLib, key);
183 		return file.get_url();
184 	}
get_content(void)185 	const char * get_content(void)
186 	{
187 		return gpAppFrame->oLibs.GetStorageFileContent(iLib, key);
188 	}
get_key(void) const189 	const std::string& get_key(void) const
190 	{
191 		return key;
192 	}
193 private:
194 	size_t iLib;
195 	std::string key;
196 	FileHolder file;
197 };
198 
199 struct ArticleView::ParseResultItemWithMark {
200 	ParseResultItem* item;
201 	std::string mark;
202 	int char_offset;
203 	// true if a tmp char is added after the mark
204 	bool tmp_char;
ParseResultItemWithMarkArticleView::ParseResultItemWithMark205 	ParseResultItemWithMark(ParseResultItem* item, int char_offset,
206 		bool tmp_char=false)
207 	: item(item), char_offset(char_offset), tmp_char(tmp_char)
208 	{
209 	}
210 };
211 
append_and_mark_orig_word(const std::string & mark,const gchar * origword,const LinksPosList & links)212 void ArticleView::append_and_mark_orig_word(const std::string& mark,
213 					    const gchar *origword,
214 					    const LinksPosList& links)
215 {
216 	if(mark.empty())
217 		return;
218 	if (conf->get_bool_at("dictionary/markup_search_word") &&
219 	    origword && *origword) {
220 		std::string res;
221 		XMLCharData xmlcd;
222 		xmlcd.assign_xml(mark.c_str());
223 		const char* const cd_str = xmlcd.get_char_data_str();
224 		if(cd_str) {
225 			const size_t cd_str_len = xmlcd.get_char_data_str_length();
226 			const size_t olen = strlen(origword);
227 			const char* cd_b, *cd_p;
228 			const char* const start_tag = "<span background=\"yellow\">";
229 			const char* const end_tag = "</span>";
230 			cd_b = cd_str;
231 			while(cd_b < cd_str + cd_str_len && (cd_p = strstr(cd_b, origword))) {
232 				xmlcd.copy_xml(res, cd_b - cd_str, cd_p - cd_str);
233 				xmlcd.mark_substring(res, start_tag, end_tag, cd_p - cd_str, olen);
234 				cd_b = cd_p + olen;
235 			}
236 			xmlcd.copy_xml(res, cd_b - cd_str, cd_str_len);
237 		} else // mark does not contain char data, only markup
238 			res = mark;
239 		if (links.empty())
240 			append_pango_text(res.c_str());
241 		else
242 			append_pango_text_with_links(res, links);
243 	} else {
244 		if (links.empty())
245 			append_pango_text(mark.c_str());
246 		else
247 			append_pango_text_with_links(mark, links);
248 	}
249 }
250 
AppendData(gchar * data,const gchar * oword,const gchar * real_oword)251 void ArticleView::AppendData(gchar *data, const gchar *oword,
252 			     const gchar *real_oword)
253 {
254 	std::string mark;
255 
256 	guint32 sec_size=0;
257 	const guint32 data_size=get_uint32(data);
258 	data+=sizeof(guint32);
259 	const gchar *p=data;
260 	bool first_time = true;
261 	size_t iPlugin;
262 	size_t nPlugins = gpAppFrame->oStarDictPlugins->ParseDataPlugins.nplugins();
263 	unsigned int parsed_size;
264 	ParseResult parse_result;
265 	while (guint32(p - data)<data_size) {
266 		if (first_time)
267 			first_time=false;
268 		else
269 			mark+= "\n";
270 		for (iPlugin = 0; iPlugin < nPlugins; iPlugin++) {
271 			parse_result.clear();
272 			if (gpAppFrame->oStarDictPlugins->ParseDataPlugins.parse(iPlugin, p, &parsed_size, parse_result, oword)) {
273 				p += parsed_size;
274 				break;
275 			}
276 		}
277 		if (iPlugin != nPlugins) {
278 			append_and_mark_orig_word(mark, real_oword, LinksPosList());
279 			mark.clear();
280 			append_data_parse_result(real_oword, parse_result);
281 			parse_result.clear();
282 			continue;
283 		}
284 		switch (*p) {
285 			case 'm':
286 			//case 'l': //TODO: convert from local encoding to utf-8
287 				p++;
288 				sec_size = strlen(p);
289 				if (sec_size) {
290 					gchar *m_str = g_markup_escape_text(p, sec_size);
291 					mark+=m_str;
292 					g_free(m_str);
293 				}
294 				sec_size++;
295 				break;
296 			case 'g':
297 				p++;
298 				sec_size=strlen(p);
299 				if (sec_size) {
300 					mark+=p;
301 				}
302 				sec_size++;
303 				break;
304 			case 'x':
305 				p++;
306 				sec_size = strlen(p) + 1;
307 				mark+= _("XDXF data parsing plug-in is not found!");
308 				break;
309 			case 'k':
310 				p++;
311 				sec_size = strlen(p) + 1;
312 				mark+= _("PowerWord data parsing plug-in is not found!");
313 				break;
314 			case 'w':
315 				p++;
316 				sec_size = strlen(p) + 1;
317 				mark+= _("Wiki data parsing plug-in is not found!");
318 				break;
319 			case 'h':
320 				p++;
321 				sec_size = strlen(p) + 1;
322 				mark+= _("HTML data parsing plug-in is not found!");
323 				break;
324 			case 'n':
325 				p++;
326 				sec_size = strlen(p) + 1;
327 				mark+= _("WordNet data parsing plug-in is not found!");
328 				break;
329 			case 't':
330 				p++;
331 				sec_size = strlen(p);
332 				if (sec_size) {
333 					mark += "[<span foreground=\"blue\">";
334 					gchar *m_str = g_markup_escape_text(p, sec_size);
335 					mark += m_str;
336 					g_free(m_str);
337 					mark += "</span>]";
338 				}
339 				sec_size++;
340 				break;
341 			case 'y':
342 				p++;
343 				sec_size = strlen(p);
344 				if (sec_size) {
345 					mark += "[<span foreground=\"red\">";
346 					gchar *m_str = g_markup_escape_text(p, sec_size);
347 					mark += m_str;
348 					g_free(m_str);
349 					mark += "</span>]";
350 				}
351 				sec_size++;
352 				break;
353 			case 'r':
354 				p++;
355 				sec_size = strlen(p);
356 				if(sec_size) {
357 					append_and_mark_orig_word(mark, real_oword, LinksPosList());
358 					mark.clear();
359 					append_resource_file_list(p);
360 				}
361 				sec_size++;
362 				break;
363 			/*case 'W':
364 				{
365 				p++;
366 				sec_size=g_ntohl(get_uint32(p));
367 				//TODO: sound button.
368 				sec_size += sizeof(guint32);
369 				}
370 				break;*/
371 			case 'P':
372 				{
373 				p++;
374 				sec_size=g_ntohl(get_uint32(p));
375 				if (sec_size) {
376 					if (for_float_win) {
377 						append_and_mark_orig_word(mark, real_oword, LinksPosList());
378 						mark.clear();
379 						append_pixbuf(NULL);
380 					} else {
381 						GdkPixbufLoader* loader = gdk_pixbuf_loader_new();
382 						gdk_pixbuf_loader_write(loader, (const guchar *)(p+sizeof(guint32)), sec_size, NULL);
383 						gdk_pixbuf_loader_close(loader, NULL);
384 						GdkPixbuf* pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
385 						if (pixbuf) {
386 							append_and_mark_orig_word(mark, real_oword, LinksPosList());
387 							mark.clear();
388 							append_pixbuf(pixbuf);
389 						} else {
390 							mark += _("<span foreground=\"red\">[Load image error!]</span>");
391 						}
392 						g_object_unref(loader);
393 					}
394 				} else {
395 					mark += _("<span foreground=\"red\">[Missing Image]</span>");
396 				}
397 				sec_size += sizeof(guint32);
398 				}
399 				break;
400 			default:
401 				if (g_ascii_isupper(*p)) {
402 					p++;
403 					sec_size=g_ntohl(get_uint32(p));
404 					sec_size += sizeof(guint32);
405 				} else {
406 					p++;
407 					sec_size = strlen(p)+1;
408 				}
409 				mark += _("Unknown data type, please upgrade StarDict!");
410 				break;
411 		}
412 		p += sec_size;
413 	}
414 
415 	append_and_mark_orig_word(mark, real_oword, LinksPosList());
416 }
417 
AppendNewline()418 void ArticleView::AppendNewline()
419 {
420 	append_pango_text("\n");
421 }
422 
AppendDataSeparate()423 void ArticleView::AppendDataSeparate()
424 {
425 	append_pango_text("<span foreground=\"gray\"><s>     </s></span>\n");
426 }
427 
SetDictIndex(InstantDictIndex index)428 void ArticleView::SetDictIndex(InstantDictIndex index)
429 {
430 	dict_index = index;
431 }
432 
AppendHeaderMark()433 void ArticleView::AppendHeaderMark()
434 {
435 	if (!for_float_win) {
436 		gchar *mark = g_strdup_printf("%d", bookindex);
437 		pango_view_->append_mark(mark);
438 		g_free(mark);
439 		bookindex++;
440 	}
441 }
442 
AppendHeader(const char * dict_name,const char * dict_link)443 void ArticleView::AppendHeader(const char *dict_name, const char *dict_link)
444 {
445 	if (bookname_style == BookNameStyle_Default) {
446 	} else if (bookname_style == BookNameStyle_OneBlankLine) {
447 		if(++headerindex > 0)
448 			append_pango_text("\n");
449 	} else {
450 		if(++headerindex > 0)
451 			append_pango_text("\n\n");
452 	}
453 	AppendHeaderMark();
454 	if (dict_link) {
455 		std::string mark;
456 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
457 			mark= "<span foreground=\"blue\">&lt;--- <u>";
458 		} else {
459 			mark= "<span foreground=\"blue\"><u>";
460 		}
461 		LinksPosList links_list;
462 		std::string link(dict_link);
463 		links_list.push_back(LinkDesc(5, g_utf8_strlen(dict_name, -1), link));
464 		gchar *m_str = g_markup_escape_text(dict_name, -1);
465 		mark += m_str;
466 		g_free(m_str);
467 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
468 			mark += "</u> ---&gt;</span>\n";
469 		} else {
470 			mark += "</u></span>\n";
471 		}
472 		append_pango_text_with_links(mark, links_list);
473 	} else {
474 		std::string mark= "<span foreground=\"blue\">";
475 #ifdef CONFIG_GPE
476 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
477 			mark+= "&lt;- ";
478 		} else {
479 		}
480 #else
481 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
482 			mark+= "&lt;--- ";
483 		} else {
484 		}
485 #endif
486 		gchar *m_str = g_markup_escape_text(dict_name, -1);
487 		mark += m_str;
488 		g_free(m_str);
489 #ifdef CONFIG_GPE
490 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
491 			mark += " -&gt;</span>\n";
492 		} else {
493 			mark += "</span>\n";
494 		}
495 #else
496 		if ((bookname_style == BookNameStyle_Default) || (bookname_style == BookNameStyle_OneBlankLine)) {
497 			mark += " ---&gt;</span>\n";
498 		} else {
499 			mark += "</span>\n";
500 		}
501 #endif
502 		append_pango_text(mark.c_str());
503 	}
504 }
505 
AppendWord(const gchar * word)506 void ArticleView::AppendWord(const gchar *word)
507 {
508 	std::string mark;
509 	mark += for_float_win ? "<span foreground=\"purple\">" : "<b><span size=\"x-large\">";
510 	gchar *m_str = g_markup_escape_text(word, -1);
511 	mark += m_str;
512 	g_free(m_str);
513 	mark += for_float_win ? "</span>\n" : "</span></b>\n";
514 	append_pango_text(mark.c_str());
515 }
516 
connect_on_link(const sigc::slot<void,const std::string &> & s)517 void ArticleView::connect_on_link(const sigc::slot<void, const std::string &>& s)
518 {
519 	pango_view_->on_link_click_.connect(s);
520 }
521 
append_resource_file_list(const gchar * p)522 void ArticleView::append_resource_file_list(const gchar *p)
523 {
524 	const gchar *b = p, *e;
525 	std::string type, key;
526 	bool new_line_before = true, new_line_after;
527 	bool loaded;
528 	while(*b) {
529 		e = strchr(b, ':');
530 		if(!e) {
531 			pango_view_->append_pango_text(
532 				_("<span foreground=\"red\">[Resource file list: incorrect format!]</span>"));
533 			break;
534 		}
535 		type.assign(b, e-b);
536 		b = e+1;
537 		e = strchr(b, '\n');
538 		if(!e)
539 			e = strchr(b, '\0');
540 		key.assign(b, e-b);
541 		b = (*e) ? e + 1 : e;
542 		loaded = false;
543 		new_line_after = false;
544 		if(type == "img") {
545 			if(!new_line_before)
546 				pango_view_->append_text("\n");
547 			new_line_after = true;
548 			append_data_res_image(key, "", loaded);
549 		} else if(type == "snd") {
550 			append_data_res_sound(key, "", loaded);
551 		} else if(type == "vdo") {
552 			append_data_res_video(key, "", loaded);
553 		} else if(type == "att") {
554 			append_data_res_attachment(key, "", loaded);
555 		} else {
556 			pango_view_->append_pango_text(
557 				_("<span foreground=\"red\">[Resource file list: incorrect format!]</span>"));
558 			break;
559 		}
560 		if(!loaded) {
561 			glib::CharStr str(g_markup_escape_text(key.c_str(), -1));
562 			std::string mark;
563 			mark += "\n<span foreground=\"red\">";
564 			mark += get_impl(str);
565 			mark += "</span>";
566 			pango_view_->append_pango_text(mark.c_str());
567 			new_line_after = true;
568 		}
569 		if(new_line_after)
570 			pango_view_->append_text("\n");
571 		new_line_before = new_line_after;
572 	}
573 }
574 
append_data_parse_result(const gchar * real_oword,ParseResult & parse_result)575 void ArticleView::append_data_parse_result(const gchar *real_oword,
576 	ParseResult& parse_result)
577 {
578 	/* Why ParseResultItem's cannot be inserted into the pango_view_ in the
579 	 * order they appear in the parse_result list?
580 	 *
581 	 * They can, but that limits use of the pango markup language when markup
582 	 * intermixed with objects that are not expressed in pango markup.
583 	 * For instance we cannot handle the following piece of data:
584 	 * markup: "<span foreground=\"purple\">some text"
585 	 * res: some image
586 	 * markup: "text continues</span>"
587 	 * The first markup string cannot be committed because it is not a valid
588 	 * markup - span tag is not closed. But if we piece two markup strings
589 	 * together, commit markup and then insert the resource, everything will be
590 	 * fine.
591 	 *
592 	 * Here is an outline of the rules parse_result list must adhere to.
593 	 *
594 	 * - each list item with pango markup must contain only complete tags. Tag
595 	 * may not be opened in one list item and closed in another. For example
596 	 * this list is not allowed:
597 	 * markup: "<span foreground=\"purple\" "
598 	 * markup: "size=\"x-large\">"
599 	 *
600 	 * - after combining all list items with pango markup the resultant string
601 	 * must constitute a valid markup (correct order of tags, each tag must have
602 	 * a corresponding closing tag and so on). For example, the following text
603 	 * is not allowed: "<b> bla bla </b><i>text end".
604 	 * Note: list item may contain incomplete markup like "<b>text begins".
605 	 *
606 	 * - Delayed insert items must generate only valid markup. Items
607 	 * representing images, widgets generate a pango-formated error message
608 	 * if the primary object cannot be inserted.
609 	 *
610 	 *
611 	 * Position in the pango markup string cannot be exactly specified by
612 	 * character offset only. That is especially important for LabelPangoWidget
613 	 * where markup is stored in a string. In order to insert delayed insert
614 	 * items in the correct place it was decided to add a temporary character
615 	 * in the string in the place where the delayed item must be inserted.
616 	 * Temporary characters are deleted at the end.
617 	 *
618 	 * In addition to character offsets marks are used to specify position in
619 	 * the text. Marks must be used instead of char offsets when possible.
620 	 * Marks are preferred over offsets because they preserve position across
621 	 * text modifications, they have a notion of right and left gravity.
622 	 * In general we do not know how many character would be inserted in the
623 	 * text after the call the method insert_pixbuf. Normally it is only one,
624 	 * but it may be any number in case of error.
625 	 *
626 	 * */
627 	Marks marks(pango_view_.get());
628 	std::string start_mark = marks.append_mark(true);
629 	std::list<ParseResultItemWithMark> delayed_insert_list;
630 	// compose markup
631 	{
632 		std::string markup_str;
633 		int char_offset = 0;
634 		const char tmp_char = 'x'; // may be any unicode char excluding '<', '&', '>'
635 
636 		for (std::list<ParseResultItem>::iterator it = parse_result.item_list.begin();
637 			it != parse_result.item_list.end(); ++it) {
638 			switch (it->type) {
639 				case ParseResultItemType_mark:
640 					char_offset += xml_utf8_strlen(it->mark->pango.c_str());
641 					markup_str += it->mark->pango;
642 					break;
643 				case ParseResultItemType_link:
644 				{
645 					/* links do not insert any text, so exact mark position is
646 					 * not important. */
647 					ParseResultItemWithMark item(&*it, char_offset);
648 					delayed_insert_list.push_back(item);
649 					char_offset += xml_utf8_strlen(it->link->pango.c_str());
650 					markup_str += it->link->pango;
651 					break;
652 				}
653 				case ParseResultItemType_res:
654 				case ParseResultItemType_widget:
655 				{
656 					ParseResultItemWithMark item1(NULL, char_offset, true);
657 					delayed_insert_list.push_back(item1);
658 					char_offset += 1;
659 					markup_str += tmp_char;
660 					ParseResultItemWithMark item2(&*it, char_offset, true);
661 					delayed_insert_list.push_back(item2);
662 					char_offset += 1;
663 					markup_str += tmp_char;
664 					break;
665 				}
666 				case ParseResultItemType_FormatBeg:
667 				case ParseResultItemType_FormatEnd:
668 				{
669 					/* formats do not insert any text, so exact mark position is
670 					 * not important. */
671 					ParseResultItemWithMark item2(&*it, char_offset);
672 					delayed_insert_list.push_back(item2);
673 					break;
674 				}
675 				default:
676 					g_warning("Unsupported item type.");
677 					break;
678 			}
679 		}
680 		append_and_mark_orig_word(markup_str, real_oword, LinksPosList());
681 		markup_str.clear();
682 		pango_view_->flush();
683 	}
684 	// Marks that precede tmp chars. One mark - one char next to it that must be
685 	// deleted. Different marks do not refer to the same char.
686 	std::list<std::string> tmp_char_mark_list;
687 	// insert marks
688 	for(std::list<ParseResultItemWithMark>::iterator it = delayed_insert_list.begin();
689 		it != delayed_insert_list.end(); ) {
690 		it->mark = marks.insert_mark(start_mark, it->char_offset, false);
691 		if(it->tmp_char)
692 			tmp_char_mark_list.push_back(it->mark);
693 		if(it->item)
694 			++it;
695 		else
696 			it = delayed_insert_list.erase(it);
697 	}
698 #ifdef DDEBUG
699 	std::cout << "ArticleView::append_data_parse_result. marks inserted."
700 		<< std::endl;
701 #endif
702 	// insert delayed items
703 	for(std::list<ParseResultItemWithMark>::iterator it = delayed_insert_list.begin();
704 		it != delayed_insert_list.end(); ) {
705 		bool EraseCurrent = true;
706 		switch(it->item->type) {
707 		case ParseResultItemType_link:
708 			pango_view_->insert_pango_text_with_links("", it->item->link->links_list,
709 				it->mark.c_str());
710 			break;
711 		case ParseResultItemType_res:
712 		{
713 			bool loaded = false;
714 			if (it->item->res->type == "image") {
715 				append_data_res_image(it->item->res->key, it->mark, loaded);
716 			} else if (it->item->res->type == "sound") {
717 				append_data_res_sound(it->item->res->key, it->mark, loaded);
718 			} else if (it->item->res->type == "video") {
719 				append_data_res_video(it->item->res->key, it->mark, loaded);
720 			} else {
721 				append_data_res_attachment(it->item->res->key, it->mark, loaded);
722 			}
723 			if (!loaded) {
724 				std::string tmark;
725 				tmark += "<span foreground=\"red\">";
726 				glib::CharStr m_str(g_markup_escape_text(it->item->res->key.c_str(), -1));
727 				tmark += get_impl(m_str);
728 				tmark += "</span>";
729 				pango_view_->insert_pango_text(tmark.c_str(), it->mark.c_str());
730 			}
731 			break;
732 		}
733 		case ParseResultItemType_widget:
734 			pango_view_->insert_widget(it->item->widget->widget, it->mark.c_str());
735 			break;
736 		case ParseResultItemType_FormatBeg:
737 			// change gravity of the mark
738 			it->mark = marks.clone_mark(it->mark, true);
739 			EraseCurrent = false;
740 			break;
741 		case ParseResultItemType_FormatEnd:
742 		{
743 			// find paired ParseResultItemType_FormatBeg item
744 			std::list<ParseResultItemWithMark>::reverse_iterator it2(it);
745 			for(; it2 != delayed_insert_list.rend(); ++it2)
746 				if(it2->item->type == ParseResultItemType_FormatBeg)
747 					break;
748 			if(it2 != delayed_insert_list.rend() && it2->item->format_beg->type
749 				== it->item->format_end->type) {
750 				if(it->item->format_end->type == ParseResultItemFormatType_Indent)
751 					pango_view_->indent_region(it2->mark.c_str(), 0, it->mark.c_str());
752 				std::list<ParseResultItemWithMark>::iterator it3(it2.base());
753 				delayed_insert_list.erase(--it3);
754 			} else
755 				g_warning("Not paired ParseResultItemType_FormatEnd item");
756 			break;
757 		}
758 		default:
759 			g_assert_not_reached();
760 			break;
761 		}
762 		if(EraseCurrent)
763 			it = delayed_insert_list.erase(it);
764 		else
765 			++it;
766 	}
767 	// remove tmp chars
768 	for(std::list<std::string>::iterator it = tmp_char_mark_list.begin();
769 		it != tmp_char_mark_list.end(); ++it) {
770 #ifdef DDEBUG
771 		std::cout << "tmp char mark " << *it << std::endl;
772 #endif
773 		pango_view_->delete_text(it->c_str(), 1, 0);
774 	}
775 	pango_view_->reindent();
776 	if(!delayed_insert_list.empty())
777 		g_warning("delayed_insert_list is not empty. "
778 			"parse_result contains not paired items.");
779 }
780 
append_data_res_image(const std::string & key,const std::string & mark,bool & loaded)781 void ArticleView::append_data_res_image(
782 	const std::string& key, const std::string& mark,
783 	bool& loaded)
784 {
785 	if (for_float_win) {
786 		loaded = true;
787 		if(mark.empty())
788 			pango_view_->append_pixbuf(NULL, key.c_str());
789 		else
790 			pango_view_->insert_pixbuf(NULL, key.c_str(), mark.c_str());
791 	} else {
792 		GdkPixbuf* pixbuf = NULL;
793 		if (dict_index.type == InstantDictType_LOCAL) {
794 			StorageType type = gpAppFrame->oLibs.GetStorageType(dict_index.index);
795 			if (type == StorageType_DATABASE || type == StorageType_FILE) {
796 				if(const char* content = gpAppFrame->oLibs.GetStorageFileContent
797 					(dict_index.index, key)) {
798 					const guint32 size = get_uint32(content);
799 					const guchar *data = (const guchar *)(content+sizeof(guint32));
800 					GdkPixbufLoader* loader = gdk_pixbuf_loader_new();
801 					gdk_pixbuf_loader_write(loader, data, size, NULL);
802 					gdk_pixbuf_loader_close(loader, NULL);
803 					pixbuf = gdk_pixbuf_loader_get_pixbuf(loader);
804 					if(pixbuf)
805 						g_object_ref(G_OBJECT(pixbuf));
806 					g_object_unref(loader);
807 				}
808 			}
809 		}
810 		if (pixbuf) {
811 			loaded = true;
812 			if(mark.empty())
813 				pango_view_->append_pixbuf(pixbuf, key.c_str());
814 			else
815 				pango_view_->insert_pixbuf(pixbuf, key.c_str(), mark.c_str());
816 			g_object_unref(pixbuf);
817 		}
818 	}
819 }
820 
append_data_res_sound(const std::string & key,const std::string & mark,bool & loaded)821 void ArticleView::append_data_res_sound(
822 	const std::string& key, const std::string& mark,
823 	bool& loaded)
824 {
825 	if (for_float_win) {
826 		loaded = true;
827 		if(mark.empty())
828 			pango_view_->append_widget(NULL);
829 		else
830 			pango_view_->insert_widget(NULL, mark.c_str());
831 	} else {
832 		GtkWidget *widget = NULL;
833 		if (dict_index.type == InstantDictType_LOCAL) {
834 			StorageType type = gpAppFrame->oLibs.GetStorageType(dict_index.index);
835 			if (type == StorageType_DATABASE || type == StorageType_FILE) {
836 				ResData *pResData
837 					= new ResData(dict_index.index, key);
838 				GtkWidget *button = NULL;
839 				GtkWidget *image = NULL;
840 				image = gtk_image_new_from_pixbuf(get_impl(
841 					gpAppFrame->oAppSkin.pronounce));
842 				button = gtk_button_new();
843 				/* We need an event box to associate a custom cursor with the
844 				 * button. */
845 				widget = gtk_event_box_new();
846 				g_object_ref_sink(G_OBJECT(widget));
847 				gtk_container_add(GTK_CONTAINER(button), image);
848 				gtk_container_add(GTK_CONTAINER(widget), button);
849 #ifdef DEBUG
850 				/* when the tooltip appears a number of gtk-warnings are generated:
851 				 * Gtk-WARNING **: IA__gtk_text_view_window_to_buffer_coords:
852 				 * can't get coords for private windows */
853 				gtk_widget_set_tooltip_text(GTK_WIDGET(widget),
854 					pResData->get_key().c_str());
855 #endif
856 				gtk_event_box_set_above_child(GTK_EVENT_BOX(widget), FALSE);
857 				gtk_widget_show_all(widget);
858 				g_signal_connect(G_OBJECT(button), "destroy",
859 					G_CALLBACK(on_resource_button_destroy), (gpointer)pResData);
860 				g_signal_connect(G_OBJECT(button), "clicked",
861 					G_CALLBACK(on_sound_button_clicked), (gpointer)pResData);
862 				g_signal_connect(G_OBJECT(button), "realize",
863 					G_CALLBACK(on_resource_button_realize), (gpointer)widget);
864 			}
865 		}
866 		if(widget) {
867 			loaded = true;
868 			if(mark.empty())
869 				pango_view_->append_widget(widget);
870 			else
871 				pango_view_->insert_widget(widget, mark.c_str());
872 			g_object_unref(widget);
873 		}
874 	}
875 }
876 
append_data_res_video(const std::string & key,const std::string & mark,bool & loaded)877 void ArticleView::append_data_res_video(
878 	const std::string& key, const std::string& mark,
879 	bool& loaded)
880 {
881 	if (for_float_win) {
882 		loaded = true;
883 		if(mark.empty())
884 			pango_view_->append_widget(NULL);
885 		else
886 			pango_view_->insert_widget(NULL, mark.c_str());
887 	} else {
888 		GtkWidget *widget = NULL;
889 		if (dict_index.type == InstantDictType_LOCAL) {
890 			StorageType type = gpAppFrame->oLibs.GetStorageType(dict_index.index);
891 			if (type == StorageType_DATABASE || type == StorageType_FILE) {
892 				ResData *pResData
893 					= new ResData(dict_index.index, key);
894 				GtkWidget *button = NULL;
895 				GtkWidget *image = NULL;
896 				image = gtk_image_new_from_pixbuf(get_impl(
897 					gpAppFrame->oAppSkin.video));
898 				button = gtk_button_new();
899 				/* We need an event box to associate a custom cursor with the
900 				 * button. */
901 				widget = gtk_event_box_new();
902 				g_object_ref_sink(G_OBJECT(widget));
903 				gtk_container_add(GTK_CONTAINER(button), image);
904 				gtk_container_add(GTK_CONTAINER(widget), button);
905 #ifdef DEBUG
906 				/* when the tooltip appears a number of gtk-warnings are generated:
907 				 * Gtk-WARNING **: IA__gtk_text_view_window_to_buffer_coords:
908 				 * can't get coords for private windows */
909 				gtk_widget_set_tooltip_text(GTK_WIDGET(widget),
910 					pResData->get_key().c_str());
911 #endif
912 				gtk_event_box_set_above_child(GTK_EVENT_BOX(widget), FALSE);
913 				gtk_widget_show_all(widget);
914 				g_signal_connect(G_OBJECT(button), "destroy",
915 					G_CALLBACK(on_resource_button_destroy), (gpointer)pResData);
916 				g_signal_connect(G_OBJECT(button), "clicked",
917 					G_CALLBACK(on_video_button_clicked), (gpointer)pResData);
918 				g_signal_connect(G_OBJECT(button), "realize",
919 					G_CALLBACK(on_resource_button_realize), (gpointer)widget);
920 			}
921 		}
922 		if(widget) {
923 			loaded = true;
924 			if(mark.empty())
925 				pango_view_->append_widget(widget);
926 			else
927 				pango_view_->insert_widget(widget, mark.c_str());
928 			g_object_unref(widget);
929 		}
930 	}
931 }
932 
append_data_res_attachment(const std::string & key,const std::string & mark,bool & loaded)933 void ArticleView::append_data_res_attachment(
934 	const std::string& key, const std::string& mark,
935 	bool& loaded)
936 {
937 	if (for_float_win) {
938 		loaded = true;
939 		if(mark.empty())
940 			pango_view_->append_widget(NULL);
941 		else
942 			pango_view_->insert_widget(NULL, mark.c_str());
943 	} else {
944 		GtkWidget *widget = NULL;
945 		if (dict_index.type == InstantDictType_LOCAL) {
946 			StorageType type = gpAppFrame->oLibs.GetStorageType(dict_index.index);
947 			if (type == StorageType_DATABASE || type == StorageType_FILE) {
948 				ResData *pResData
949 					= new ResData(dict_index.index, key);
950 				GtkWidget *button = NULL;
951 				GtkWidget *image = NULL;
952 				image = gtk_image_new_from_pixbuf(get_impl(
953 					gpAppFrame->oAppSkin.attachment));
954 				button = gtk_button_new();
955 				/* We need an event box to associate a custom cursor with the
956 				 * button. */
957 				widget = gtk_event_box_new();
958 				g_object_ref_sink(G_OBJECT(widget));
959 				gtk_container_add(GTK_CONTAINER(button), image);
960 				gtk_container_add(GTK_CONTAINER(widget), button);
961 #ifdef DEBUG
962 				/* when the tooltip appears a number of gtk-warnings are generated:
963 				 * Gtk-WARNING **: IA__gtk_text_view_window_to_buffer_coords:
964 				 * can't get coords for private windows */
965 				gtk_widget_set_tooltip_text(GTK_WIDGET(widget),
966 					pResData->get_key().c_str());
967 #endif
968 				gtk_event_box_set_above_child(GTK_EVENT_BOX(widget), FALSE);
969 				gtk_widget_show_all(widget);
970 				g_signal_connect(G_OBJECT(button), "destroy",
971 					G_CALLBACK(on_resource_button_destroy), (gpointer)pResData);
972 				g_signal_connect(G_OBJECT(button), "clicked",
973 					G_CALLBACK(on_attachment_button_clicked), (gpointer)pResData);
974 				g_signal_connect(G_OBJECT(button), "realize",
975 					G_CALLBACK(on_resource_button_realize), (gpointer)widget);
976 			}
977 		}
978 		if(widget) {
979 			loaded = true;
980 			if(mark.empty())
981 				pango_view_->append_widget(widget);
982 			else
983 				pango_view_->insert_widget(widget, mark.c_str());
984 			g_object_unref(widget);
985 		}
986 	}
987 }
988 
on_resource_button_destroy(GtkWidget * object,gpointer user_data)989 void ArticleView::on_resource_button_destroy(GtkWidget *object, gpointer user_data)
990 {
991 	delete (ResData*)user_data;
992 }
993 
on_sound_button_clicked(GtkWidget * object,gpointer user_data)994 void ArticleView::on_sound_button_clicked(GtkWidget *object, gpointer user_data)
995 {
996 	ResData *pResData = (ResData*)user_data;
997 	if(const char *filename = pResData->get_url())
998 		play_sound_file(filename);
999 	else
1000 		g_warning("Unable to load resource: %s", pResData->get_key().c_str());
1001 }
1002 
on_video_button_clicked(GtkWidget * object,gpointer user_data)1003 void ArticleView::on_video_button_clicked(GtkWidget *object, gpointer user_data)
1004 {
1005 	ResData *pResData = (ResData*)user_data;
1006 	if(const char *filename = pResData->get_url())
1007 		play_video_file(filename);
1008 	else
1009 		g_warning("Unable to load resource: %s", pResData->get_key().c_str());
1010 }
1011 
on_attachment_button_clicked(GtkWidget * object,gpointer user_data)1012 void ArticleView::on_attachment_button_clicked(GtkWidget *object, gpointer user_data)
1013 {
1014 	ResData *pResData = (ResData*)user_data;
1015 	const std::string& key = pResData->get_key();
1016 	std::string::size_type pos = key.rfind(DB_DIR_SEPARATOR);
1017 	// in utf-8, yet a file name
1018 	const char* default_file_name = key.c_str() + (pos == std::string::npos ? 0 : pos+1);
1019 	GtkWidget *dialog;
1020 	dialog = gtk_file_chooser_dialog_new(_("Save File"),
1021 		GTK_WINDOW(gpAppFrame->window),
1022 		GTK_FILE_CHOOSER_ACTION_SAVE,
1023 		GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
1024 		GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
1025 		NULL);
1026 	gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
1027 	gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),
1028 		gpAppFrame->last_selected_directory.c_str());
1029 	gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), default_file_name);
1030 	if(gtk_dialog_run(GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
1031 		glib::CharStr filename(gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)));
1032 		glib::CharStr selected_dir(g_path_get_dirname(get_impl(filename)));
1033 		gpAppFrame->last_selected_directory = get_impl(selected_dir);
1034 		if(const char* content = pResData->get_content()) {
1035 			const guint32 size = get_uint32(content);
1036 			const gchar *data = (const gchar *)(content+sizeof(guint32));
1037 			if(!g_file_set_contents(get_impl(filename), data, size, NULL))
1038 				g_warning("Fail to save file %s", get_impl(filename));
1039 		} else {
1040 			g_warning("Unable to load resource: %s", pResData->get_key().c_str());
1041 		}
1042 	}
1043 	gtk_widget_destroy(dialog);
1044 }
1045 
on_resource_button_realize(GtkWidget * object,gpointer user_data)1046 void ArticleView::on_resource_button_realize(GtkWidget *object, gpointer user_data)
1047 {
1048 	/* Event boxes are not automatically realized by GTK+, they must be realized
1049 	 * explicitly. You need to make sure that an event box is already added as
1050 	 * a child to a top-level widget before realizing it. A child realize event
1051 	 * handler is a good place for that (imho). */
1052 	GtkWidget *eventbox = GTK_WIDGET(user_data);
1053 	gtk_widget_realize(eventbox);
1054 	GdkCursor *cursor = gdk_cursor_new(GDK_HAND2);
1055 	gdk_window_set_cursor(gtk_widget_get_window(eventbox), cursor);
1056 #if GTK_MAJOR_VERSION >= 3
1057 	g_object_unref(cursor);
1058 #else
1059 	gdk_cursor_unref(cursor);
1060 #endif
1061 }
1062