1 /*
2 *
3 * Copyright (C) 2013 Colomban Wendling <ban@herbesfolles.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 #include "config.h"
21
22 #include <string.h>
23 #include <glib.h>
24 #include <glib/gi18n-lib.h>
25
26 #include <geanyplugin.h>
27 #include <geany.h>
28 #include <document.h>
29 #include <SciLexer.h>
30
31
32 GeanyPlugin *geany_plugin;
33 GeanyData *geany_data;
34
35
36 PLUGIN_VERSION_CHECK (224)
37
38 PLUGIN_SET_TRANSLATABLE_INFO (
39 LOCALEDIR, GETTEXT_PACKAGE,
40 _("Translation Helper"),
41 _("Improves support for GetText translation files."),
42 VERSION,
43 "Colomban Wendling <ban@herbesfolles.org>"
44 )
45
46
47 enum {
48 GPH_KB_GOTO_PREV,
49 GPH_KB_GOTO_NEXT,
50 GPH_KB_GOTO_PREV_UNTRANSLATED,
51 GPH_KB_GOTO_NEXT_UNTRANSLATED,
52 GPH_KB_GOTO_PREV_FUZZY,
53 GPH_KB_GOTO_NEXT_FUZZY,
54 GPH_KB_GOTO_PREV_UNTRANSLATED_OR_FUZZY,
55 GPH_KB_GOTO_NEXT_UNTRANSLATED_OR_FUZZY,
56 GPH_KB_PASTE_UNTRANSLATED,
57 GPH_KB_REFLOW,
58 GPH_KB_TOGGLE_FUZZY,
59 GPH_KB_SHOW_STATS,
60 GPH_KB_COUNT
61 };
62
63
64 static struct Plugin {
65 gboolean update_headers;
66 /* stats dialog colors */
67 GdkColor color_translated;
68 GdkColor color_fuzzy;
69 GdkColor color_untranslated;
70
71 GeanyKeyGroup *key_group;
72 GtkWidget *menu_item;
73 } plugin = {
74 TRUE,
75 { 0, 0x7373, 0xd2d2, 0x1616 }, /* tango mid green */
76 { 0, 0xeded, 0xd4d4, 0x0000 }, /* tango mid yellow */
77 { 0, 0xcccc, 0x0000, 0x0000 }, /* tango mid red */
78 NULL,
79 NULL
80 };
81
82
83 #define doc_is_po(doc) (DOC_VALID (doc) && \
84 (doc)->file_type && \
85 (doc)->file_type->id == GEANY_FILETYPES_PO)
86
87
88 /* gets the smallest valid position between @a and @b */
89 #define MIN_POS(a, b) ((a) < 0 ? (b) : (b) < 0 ? (a) : MIN ((a), (b)))
90 /* gets the highest valid position between @a and @b */
91 #define MAX_POS(a, b) (MAX ((a), (b)))
92
93
94 /*
95 * find_style:
96 * @sci: a #ScintillaObject
97 * @style: a style ID to search for
98 * @start: start of the search range
99 * @end: end of the search range
100 *
101 * Search for a style in a #ScintillaObject. Backward search is possible if
102 * start is > end. Note that this find the first occurrence of @style in the
103 * search direction, which means that the start of the style will be found when
104 * searching onwards but the end when searching backwards. Also, if the start
105 * position is already on the style to search for this position is returned
106 * rather than one bound.
107 *
108 * Returns: The first found position with style @style, or -1 if not found in
109 * the given range.
110 */
111 static gint
find_style(ScintillaObject * sci,gint style,gint start,gint end)112 find_style (ScintillaObject *sci,
113 gint style,
114 gint start,
115 gint end)
116 {
117 gint pos;
118
119 if (start > end) { /* search backwards */
120 for (pos = start; pos >= end; pos--) {
121 if (sci_get_style_at (sci, pos) == style)
122 break;
123 }
124 if (pos < end)
125 return -1;
126 } else {
127 for (pos = start; pos < end; pos++) {
128 if (sci_get_style_at (sci, pos) == style)
129 break;
130 }
131 if (pos >= end)
132 return -1;
133 }
134
135 return pos;
136 }
137
138 /* like find_style(), but searches for the first style change from @start to
139 * @end. Returns the first position in the search direction with a style
140 * different from the one at @start, or -1 */
141 static gint
find_style_boundary(ScintillaObject * sci,gint start,gint end)142 find_style_boundary (ScintillaObject *sci,
143 gint start,
144 gint end)
145 {
146 gint style = sci_get_style_at (sci, start);
147 gint pos;
148
149 if (start > end) { /* search backwards */
150 for (pos = start; pos >= end; pos--) {
151 if (sci_get_style_at (sci, pos) != style)
152 break;
153 }
154 if (pos < end)
155 return -1;
156 } else {
157 for (pos = start; pos < end; pos++) {
158 if (sci_get_style_at (sci, pos) != style)
159 break;
160 }
161 if (pos >= end)
162 return -1;
163 }
164
165 return pos;
166 }
167
168 /*
169 * find_message:
170 * @doc: A #GeanyDocument
171 * @start: start of the search range
172 * @end: end of the search range
173 *
174 * Finds the start position of the next msgstr in the given range. If start is
175 * > end, searches backwards.
176 *
177 * Returns: The start position of the next msgstr in the given range, or -1 if
178 * not found.
179 */
180 static gint
find_message(GeanyDocument * doc,gint start,gint end)181 find_message (GeanyDocument *doc,
182 gint start,
183 gint end)
184 {
185 if (doc_is_po (doc)) {
186 ScintillaObject *sci = doc->editor->sci;
187 gint pos = find_style (sci, SCE_PO_MSGSTR, start, end);
188
189 /* if searching backwards and already in a msgstr style, search previous
190 * again not to go to current's start */
191 if (pos >= 0 && start > end) {
192 gint style = sci_get_style_at (sci, start);
193
194 /* don't take default style into account, so find previous non-default */
195 if (style == SCE_PO_DEFAULT) {
196 gint style_pos = find_style_boundary (sci, start, end);
197 if (style_pos >= 0) {
198 style = sci_get_style_at (sci, style_pos);
199 }
200 }
201
202 if (style == SCE_PO_MSGSTR ||
203 style == SCE_PO_MSGSTR_TEXT ||
204 style == SCE_PO_MSGSTR_TEXT_EOL) {
205 pos = find_style_boundary (sci, pos, end);
206 if (pos >= 0) {
207 pos = find_style (sci, SCE_PO_MSGSTR, pos, end);
208 }
209 }
210 }
211
212 if (pos >= 0) {
213 pos = find_style (sci, SCE_PO_MSGSTR_TEXT, pos, sci_get_length (sci));
214 if (pos >= 0) {
215 return pos + 1;
216 }
217 }
218 }
219
220 return -1;
221 }
222
223 /*
224 * find_untranslated:
225 * @doc: A #GeanyDocument
226 * @start: start of the search range
227 * @end: end of the search range
228 *
229 * Searches for the next untranslated message in the given range. If start is
230 * > end, searches backwards.
231 *
232 * Returns: The start position of the next untranslated message in the given
233 * range, or -1 if not found.
234 */
235 static gint
find_untranslated(GeanyDocument * doc,gint start,gint end)236 find_untranslated (GeanyDocument *doc,
237 gint start,
238 gint end)
239 {
240 if (doc_is_po (doc)) {
241 ScintillaObject *sci = doc->editor->sci;
242
243 while (start >= 0) {
244 gint pos;
245
246 pos = find_message (doc, start, end);
247 if (pos < 0) {
248 return -1;
249 } else {
250 gint i = pos;
251
252 for (i = pos; i < sci_get_length (sci); i++) {
253 gint style = sci_get_style_at (sci, i);
254
255 if (style == SCE_PO_MSGSTR_TEXT) {
256 if (sci_get_char_at (sci, i) != '"') {
257 /* if any character in the text is not a delimiter, there's a
258 * translation */
259 i = -1;
260 break;
261 }
262 } else if (style != SCE_PO_DEFAULT) {
263 /* if we reached something else than the text and the background,
264 * we're done searching */
265 break;
266 }
267 }
268 if (i >= 0)
269 return pos;
270 }
271
272 start = pos;
273 }
274 }
275
276 return -1;
277 }
278
279 static gint
find_prev_untranslated(GeanyDocument * doc)280 find_prev_untranslated (GeanyDocument *doc)
281 {
282 return find_untranslated (doc, sci_get_current_position (doc->editor->sci),
283 0);
284 }
285
286 static gint
find_next_untranslated(GeanyDocument * doc)287 find_next_untranslated (GeanyDocument *doc)
288 {
289 return find_untranslated (doc, sci_get_current_position (doc->editor->sci),
290 sci_get_length (doc->editor->sci));
291 }
292
293 static gint
find_fuzzy(GeanyDocument * doc,gint start,gint end)294 find_fuzzy (GeanyDocument *doc,
295 gint start,
296 gint end)
297 {
298 if (doc_is_po (doc)) {
299 ScintillaObject *sci = doc->editor->sci;
300
301 if (start > end) {
302 /* if searching backwards, first go to the previous msgstr not to find
303 * the current one */
304 gint style = sci_get_style_at (sci, start);
305
306 if (style == SCE_PO_MSGSTR || style == SCE_PO_MSGSTR_TEXT) {
307 start = find_style (sci, SCE_PO_MSGID, start, end);
308 if (start >= 0) {
309 start = find_style (sci, SCE_PO_MSGSTR, start, end);
310 }
311 }
312 }
313
314 if (start >= 0) {
315 struct Sci_TextToFind ttf;
316
317 ttf.chrg.cpMin = start;
318 ttf.chrg.cpMax = end;
319 ttf.lpstrText = (gchar *)"fuzzy";
320
321 while (sci_find_text (sci, SCFIND_WHOLEWORD | SCFIND_MATCHCASE,
322 &ttf) >= 0) {
323 gint style = sci_get_style_at (sci, (gint) ttf.chrgText.cpMin);
324
325 if (style == SCE_PO_FUZZY || style == SCE_PO_FLAGS) {
326 /* OK, now find the start of the translation */
327 return find_message (doc, (gint) ttf.chrgText.cpMax,
328 start > end ? sci_get_length (sci) : end);
329 }
330
331 ttf.chrg.cpMin = start > end ? ttf.chrgText.cpMin : ttf.chrgText.cpMax;
332 }
333 }
334 }
335
336 return -1;
337 }
338
339 static gint
find_prev_fuzzy(GeanyDocument * doc)340 find_prev_fuzzy (GeanyDocument *doc)
341 {
342 return find_fuzzy (doc, sci_get_current_position (doc->editor->sci), 0);
343 }
344
345 static gint
find_next_fuzzy(GeanyDocument * doc)346 find_next_fuzzy (GeanyDocument *doc)
347 {
348 return find_fuzzy (doc, sci_get_current_position (doc->editor->sci),
349 sci_get_length (doc->editor->sci));
350 }
351
352 /* goto */
353
354 static void
goto_prev(GeanyDocument * doc)355 goto_prev (GeanyDocument *doc)
356 {
357 if (doc_is_po (doc)) {
358 gint pos = find_message (doc, sci_get_current_position (doc->editor->sci),
359 0);
360
361 if (pos >= 0) {
362 editor_goto_pos (doc->editor, pos, FALSE);
363 }
364 }
365 }
366
367 static void
goto_next(GeanyDocument * doc)368 goto_next (GeanyDocument *doc)
369 {
370 if (doc_is_po (doc)) {
371 gint pos = find_message (doc, sci_get_current_position (doc->editor->sci),
372 sci_get_length (doc->editor->sci));
373
374 if (pos >= 0) {
375 editor_goto_pos (doc->editor, pos, FALSE);
376 }
377 }
378 }
379
380 static void
goto_prev_untranslated(GeanyDocument * doc)381 goto_prev_untranslated (GeanyDocument *doc)
382 {
383 if (doc_is_po (doc)) {
384 gint pos = find_prev_untranslated (doc);
385
386 if (pos >= 0) {
387 editor_goto_pos (doc->editor, pos, FALSE);
388 }
389 }
390 }
391
392 static void
goto_next_untranslated(GeanyDocument * doc)393 goto_next_untranslated (GeanyDocument *doc)
394 {
395 if (doc_is_po (doc)) {
396 gint pos = find_next_untranslated (doc);
397
398 if (pos >= 0) {
399 editor_goto_pos (doc->editor, pos, FALSE);
400 }
401 }
402 }
403
404 static void
goto_prev_fuzzy(GeanyDocument * doc)405 goto_prev_fuzzy (GeanyDocument *doc)
406 {
407 if (doc_is_po (doc)) {
408 gint pos = find_prev_fuzzy (doc);
409
410 if (pos >= 0) {
411 editor_goto_pos (doc->editor, pos, FALSE);
412 }
413 }
414 }
415
416 static void
goto_next_fuzzy(GeanyDocument * doc)417 goto_next_fuzzy (GeanyDocument *doc)
418 {
419 if (doc_is_po (doc)) {
420 gint pos = find_next_fuzzy (doc);
421
422 if (pos >= 0) {
423 editor_goto_pos (doc->editor, pos, FALSE);
424 }
425 }
426 }
427
428 static void
goto_prev_untranslated_or_fuzzy(GeanyDocument * doc)429 goto_prev_untranslated_or_fuzzy (GeanyDocument *doc)
430 {
431 if (doc_is_po (doc)) {
432 gint pos1 = find_prev_untranslated (doc);
433 gint pos2 = find_prev_fuzzy (doc);
434 gint pos = MAX_POS (pos1, pos2);
435
436 if (pos >= 0) {
437 editor_goto_pos (doc->editor, pos, FALSE);
438 }
439 }
440 }
441
442 static void
goto_next_untranslated_or_fuzzy(GeanyDocument * doc)443 goto_next_untranslated_or_fuzzy (GeanyDocument *doc)
444 {
445 if (doc_is_po (doc)) {
446 gint pos1 = find_next_untranslated (doc);
447 gint pos2 = find_next_fuzzy (doc);
448 gint pos = MIN_POS (pos1, pos2);
449
450 if (pos >= 0) {
451 editor_goto_pos (doc->editor, pos, FALSE);
452 }
453 }
454 }
455
456 /* basic regex search/replace without captures or back references
457 *
458 * @sci A ScintillaObject
459 * @start Position where to start the search
460 * @end Position where to end the search, or -1 for the buffer's end
461 * @scire The Scintilla regular expression
462 * @repl The replacement text */
463 static gboolean
regex_replace(ScintillaObject * sci,gint start,gint end,const gchar * scire,const gchar * repl)464 regex_replace (ScintillaObject *sci,
465 gint start,
466 gint end,
467 const gchar *scire,
468 const gchar *repl)
469 {
470 struct Sci_TextToFind ttf;
471
472 ttf.chrg.cpMin = start;
473 ttf.chrg.cpMax = end >= 0 ? end : sci_get_length (sci);
474 ttf.lpstrText = (gchar *) scire;
475
476 if (sci_find_text (sci, SCFIND_REGEXP, &ttf) != -1) {
477 sci_set_target_start (sci, (gint) ttf.chrgText.cpMin);
478 sci_set_target_end (sci, (gint) ttf.chrgText.cpMax);
479 sci_replace_target (sci, repl, FALSE);
480
481 return TRUE;
482 }
483
484 return FALSE;
485 }
486
487 /* escapes @str so it is valid to put it inside a message
488 * escapes '\b', '\f', '\n', '\r', '\t', '\v', '\' and '"'
489 * unlike g_strescape(), it doesn't escape non-ASCII characters so keeps
490 * all of UTF-8 */
491 static gchar *
escape_string(const gchar * str)492 escape_string (const gchar *str)
493 {
494 gchar *new = g_malloc (strlen (str) * 2 + 1);
495 gchar *p;
496
497 for (p = new; *str; str++) {
498 switch (*str) {
499 case '\b': *p++ = '\\'; *p++ = 'b'; break;
500 case '\f': *p++ = '\\'; *p++ = 'f'; break;
501 case '\n': *p++ = '\\'; *p++ = 'n'; break;
502 case '\r': *p++ = '\\'; *p++ = 'r'; break;
503 case '\t': *p++ = '\\'; *p++ = 't'; break;
504 case '\v': *p++ = '\\'; *p++ = 'v'; break;
505 case '\\': *p++ = '\\'; *p++ = '\\'; break;
506 case '"': *p++ = '\\'; *p++ = '"'; break;
507 default:
508 *p++ = *str;
509 }
510 }
511 *p = 0;
512
513 return new;
514 }
515
516 static void
update_menu_items_sensitivity(GeanyDocument * doc)517 update_menu_items_sensitivity (GeanyDocument *doc)
518 {
519 gboolean sensitive = doc_is_po (doc);
520 guint i;
521
522 /* since all the document-sensitive items have keybindings and all
523 * keybinginds that have a widget are document-sensitive, just walk
524 * the keybindings list to fetch the widgets */
525 for (i = 0; i < GPH_KB_COUNT; i++) {
526 GeanyKeyBinding *kb = keybindings_get_item (plugin.key_group, i);
527
528 if (kb->menu_item) {
529 gtk_widget_set_sensitive (kb->menu_item, sensitive);
530 }
531 }
532 }
533
534 static void
on_document_activate(GObject * obj,GeanyDocument * doc,gpointer user_data)535 on_document_activate (GObject *obj,
536 GeanyDocument *doc,
537 gpointer user_data)
538 {
539 update_menu_items_sensitivity (doc);
540 }
541
542 static void
on_document_filetype_set(GObject * obj,GeanyDocument * doc,GeanyFiletype * old_ft,gpointer user_data)543 on_document_filetype_set (GObject *obj,
544 GeanyDocument *doc,
545 GeanyFiletype *old_ft,
546 gpointer user_data)
547 {
548 update_menu_items_sensitivity (doc);
549 }
550
551 static void
on_document_close(GObject * obj,GeanyDocument * doc,gpointer user_data)552 on_document_close (GObject *obj,
553 GeanyDocument *doc,
554 gpointer user_data)
555 {
556 GtkNotebook *nb = GTK_NOTEBOOK (geany_data->main_widgets->notebook);
557
558 /* the :document-close signal is emitted before a document gets closed,
559 * so there always still is the current document open (hence the < 2) */
560 if (gtk_notebook_get_n_pages (nb) < 2) {
561 update_menu_items_sensitivity (NULL);
562 }
563 }
564
565 static void
on_kb_goto_prev(guint key_id)566 on_kb_goto_prev (guint key_id)
567 {
568 goto_prev (document_get_current ());
569 }
570
571 static void
on_kb_goto_next(guint key_id)572 on_kb_goto_next (guint key_id)
573 {
574 goto_next (document_get_current ());
575 }
576
577 static void
on_kb_goto_prev_untranslated(guint key_id)578 on_kb_goto_prev_untranslated (guint key_id)
579 {
580 goto_prev_untranslated (document_get_current ());
581 }
582
583 static void
on_kb_goto_next_untranslated(guint key_id)584 on_kb_goto_next_untranslated (guint key_id)
585 {
586 goto_next_untranslated (document_get_current ());
587 }
588
589 static void
on_kb_goto_prev_fuzzy(guint key_id)590 on_kb_goto_prev_fuzzy (guint key_id)
591 {
592 goto_prev_fuzzy (document_get_current ());
593 }
594
595 static void
on_kb_goto_next_fuzzy(guint key_id)596 on_kb_goto_next_fuzzy (guint key_id)
597 {
598 goto_next_fuzzy (document_get_current ());
599 }
600
601 static void
on_kb_goto_prev_untranslated_or_fuzzy(guint key_id)602 on_kb_goto_prev_untranslated_or_fuzzy (guint key_id)
603 {
604 goto_prev_untranslated_or_fuzzy (document_get_current ());
605 }
606
607 static void
on_kb_goto_next_untranslated_or_fuzzy(guint key_id)608 on_kb_goto_next_untranslated_or_fuzzy (guint key_id)
609 {
610 goto_next_untranslated_or_fuzzy (document_get_current ());
611 }
612
613 /*
614 * on_kb_paste_untranslated:
615 * @key_id: unused
616 *
617 * Replaces the msgstr at the current position with it corresponding msgid.
618 */
619 static void
on_kb_paste_untranslated(guint key_id)620 on_kb_paste_untranslated (guint key_id)
621 {
622 GeanyDocument *doc = document_get_current ();
623
624 if (doc_is_po (doc)) {
625 ScintillaObject *sci = doc->editor->sci;
626 gint pos = sci_get_current_position (sci);
627 gint style = sci_get_style_at (sci, pos);
628
629 while (pos > 0 && style == SCE_PO_DEFAULT) {
630 style = sci_get_style_at (sci, --pos);
631 }
632
633 if (style == SCE_PO_MSGID_TEXT ||
634 style == SCE_PO_MSGSTR ||
635 style == SCE_PO_MSGSTR_TEXT) {
636 pos = find_style (sci, SCE_PO_MSGID, pos, 0);
637 if (pos >= 0)
638 style = SCE_PO_MSGID;
639 }
640
641 if (style == SCE_PO_MSGID) {
642 gint start = find_style (sci, SCE_PO_MSGID_TEXT,
643 pos, sci_get_length (sci));
644
645 if (start >= 0) {
646 gchar *msgid;
647 gint end = start;
648
649 /* find msgid range and copy it */
650 for (pos = start + 1; pos < sci_get_length (sci); pos++) {
651 style = sci_get_style_at (sci, pos);
652 if (style == SCE_PO_MSGID_TEXT)
653 end = pos;
654 else if (style != SCE_PO_DEFAULT)
655 break;
656 }
657
658 if (end - start <= 2 /* 2 is because we include the quotes */) {
659 /* don't allow replacing the header (empty) msgid */
660 } else {
661 msgid = sci_get_contents_range (sci, start, end);
662
663 start = find_style (sci, SCE_PO_MSGSTR_TEXT, end,
664 sci_get_length (sci));
665 if (start >= 0) {
666 /* find msgstr range and replace it */
667 end = start;
668 sci_set_target_start (sci, start);
669 for (pos = start; pos < sci_get_length (sci); pos++) {
670 style = sci_get_style_at (sci, pos);
671 if (style == SCE_PO_MSGSTR_TEXT)
672 end = pos;
673 else if (style != SCE_PO_DEFAULT)
674 break;
675 }
676 sci_set_target_end (sci, end);
677 sci_replace_target (sci, msgid, FALSE);
678 scintilla_send_message (sci, SCI_GOTOPOS, (uptr_t) start + 1, 0);
679 }
680 g_free (msgid);
681 }
682 }
683 }
684 }
685 }
686
687 /* finds the start of the msgstr text at @pos. the returned position is the
688 * start of the msgstr text style, so it's on the first opening quote. Returns
689 * -1 if none found */
690 static gint
find_msgstr_start_at(GeanyDocument * doc,gint pos)691 find_msgstr_start_at (GeanyDocument *doc,
692 gint pos)
693 {
694 if (doc_is_po (doc)) {
695 ScintillaObject *sci = doc->editor->sci;
696 gint style = sci_get_style_at (sci, pos);
697
698 /* find the previous non-default style */
699 while (pos > 0 && style == SCE_PO_DEFAULT) {
700 style = sci_get_style_at (sci, --pos);
701 }
702
703 /* if a msgid or msgstr, go to the msgstr keyword */
704 if (style == SCE_PO_MSGID ||
705 style == SCE_PO_MSGID_TEXT ||
706 style == SCE_PO_MSGSTR_TEXT) {
707 pos = find_style (sci, SCE_PO_MSGSTR, pos,
708 style == SCE_PO_MSGSTR_TEXT ? 0 : sci_get_length (sci));
709 if (pos >= 0)
710 style = SCE_PO_MSGSTR;
711 }
712
713 if (style == SCE_PO_MSGSTR) {
714 return find_style (sci, SCE_PO_MSGSTR_TEXT, pos, sci_get_length (sci));
715 }
716 }
717
718 return -1;
719 }
720
721 /* like find_msgstr_start_at() but finds the end rather than the start */
722 static gint
find_msgstr_end_at(GeanyDocument * doc,gint pos)723 find_msgstr_end_at (GeanyDocument *doc,
724 gint pos)
725 {
726 pos = find_msgstr_start_at (doc, pos);
727 if (pos >= 0) {
728 ScintillaObject *sci = doc->editor->sci;
729 gint end = pos;
730
731 for (; pos < sci_get_length (sci); pos++) {
732 gint style = sci_get_style_at (sci, pos);
733
734 if (style == SCE_PO_MSGSTR_TEXT)
735 end = pos;
736 else if (style != SCE_PO_DEFAULT)
737 break;
738 }
739
740 return end;
741 }
742
743 return -1;
744 }
745
746 static GString *
get_msgstr_text_at(GeanyDocument * doc,gint pos)747 get_msgstr_text_at (GeanyDocument *doc,
748 gint pos)
749 {
750 pos = find_msgstr_start_at (doc, pos);
751
752 if (pos >= 0) {
753 ScintillaObject *sci = doc->editor->sci;
754 GString *msgstr = g_string_new (NULL);
755 gint length = sci_get_length (sci);
756
757 while (sci_get_style_at (sci, pos) == SCE_PO_MSGSTR_TEXT) {
758 pos++; /* skip opening quote */
759 while (sci_get_style_at (sci, pos + 1) == SCE_PO_MSGSTR_TEXT) {
760 g_string_append_c (msgstr, sci_get_char_at (sci, pos));
761 pos++;
762 }
763 pos++; /* skip closing quote */
764
765 /* skip until next non-default style */
766 while (pos < length && sci_get_style_at (sci, pos) == SCE_PO_DEFAULT) {
767 pos++;
768 }
769 }
770
771 return msgstr;
772 }
773
774 return NULL;
775 }
776
777 /* finds the start of the msgid text at @pos. the returned position is the
778 * start of the msgid text style, so it's on the first opening quote. Returns
779 * -1 if none found */
780 static gint
find_msgid_start_at(GeanyDocument * doc,gint pos)781 find_msgid_start_at (GeanyDocument *doc,
782 gint pos)
783 {
784 if (doc_is_po (doc)) {
785 ScintillaObject *sci = doc->editor->sci;
786 gint style = sci_get_style_at (sci, pos);
787
788 /* find the previous non-default style */
789 while (pos > 0 && style == SCE_PO_DEFAULT) {
790 style = sci_get_style_at (sci, --pos);
791 }
792
793 /* if a msgid or msgstr, go to the msgstr keyword */
794 if (style == SCE_PO_MSGID_TEXT ||
795 style == SCE_PO_MSGSTR ||
796 style == SCE_PO_MSGSTR_TEXT) {
797 pos = find_style (sci, SCE_PO_MSGID, pos, 0);
798 if (pos >= 0)
799 style = SCE_PO_MSGID;
800 }
801
802 if (style == SCE_PO_MSGID) {
803 return find_style (sci, SCE_PO_MSGID_TEXT, pos, sci_get_length (sci));
804 }
805 }
806
807 return -1;
808 }
809
810 static GString *
get_msgid_text_at(GeanyDocument * doc,gint pos)811 get_msgid_text_at (GeanyDocument *doc,
812 gint pos)
813 {
814 pos = find_msgid_start_at (doc, pos);
815
816 if (pos >= 0) {
817 ScintillaObject *sci = doc->editor->sci;
818 GString *msgid = g_string_new (NULL);
819 gint length = sci_get_length (sci);
820
821 while (sci_get_style_at (sci, pos) == SCE_PO_MSGID_TEXT) {
822 pos++; /* skip opening quote */
823 while (sci_get_style_at (sci, pos + 1) == SCE_PO_MSGID_TEXT) {
824 g_string_append_c (msgid, sci_get_char_at (sci, pos));
825 pos++;
826 }
827 pos++; /* skip closing quote */
828
829 /* skip until next non-default style */
830 while (pos < length && sci_get_style_at (sci, pos) == SCE_PO_DEFAULT) {
831 pos++;
832 }
833 }
834
835 return msgid;
836 }
837
838 return NULL;
839 }
840
841 static const gchar *
find_line_break(const gchar * str)842 find_line_break (const gchar *str)
843 {
844 for (; *str; str++) {
845 if (*str == '\\') {
846 if (str[1] == 'n')
847 return str;
848 else if (str[1])
849 str++;
850 }
851 }
852
853 return NULL;
854 }
855
856 /* cuts @str in human-readable chunks for max @len.
857 * cuts first at \n, then at spaces and punctuation */
858 static gchar **
split_msg(const gchar * str,gsize len)859 split_msg (const gchar *str,
860 gsize len)
861 {
862 GPtrArray *chunks = g_ptr_array_new ();
863
864 while (*str) {
865 GString *chunk = g_string_sized_new (len);
866
867 while (*str) {
868 const gchar *nl = find_line_break (str);
869 const gchar *p = strpbrk (str, " \t\v\r\n?!,.;:-");
870 glong chunk_len = g_utf8_strlen (chunk->str, (gssize) chunk->len);
871
872 if (nl)
873 nl += 2;
874
875 if (! p) /* if there is no separator, use the end of the string */
876 p = strchr (str, 0);
877 else {
878 p++;
879 /* try not to leave a space at the start of a chunk */
880 while (*p == ' ')
881 p++;
882 }
883
884 if (nl && ((gsize)(chunk_len + g_utf8_strlen (str, nl - str)) <= len ||
885 (nl < p && chunk->len == 0))) {
886 g_string_append_len (chunk, str, nl - str);
887 str = nl;
888 break;
889 } else if ((gsize)(chunk_len + g_utf8_strlen (str, p - str)) <= len ||
890 chunk->len == 0) {
891 g_string_append_len (chunk, str, p - str);
892 str = p;
893 } else {
894 /* give up and leave to next chunk */
895 break;
896 }
897 }
898 g_ptr_array_add (chunks, g_string_free (chunk, FALSE));
899 }
900
901 g_ptr_array_add (chunks, NULL);
902
903 return (gchar **) g_ptr_array_free (chunks, FALSE);
904 }
905
906 static void
on_kb_reflow(guint key_id)907 on_kb_reflow (guint key_id)
908 {
909 GeanyDocument *doc = document_get_current ();
910
911 if (doc_is_po (doc)) {
912 ScintillaObject *sci = doc->editor->sci;
913 gint pos = sci_get_current_position (sci);
914 GString *msgstr = get_msgstr_text_at (doc, pos);
915
916 if (msgstr) {
917 gint start = find_msgstr_start_at (doc, pos);
918 gint end = find_msgstr_end_at (doc, pos);
919 glong len = g_utf8_strlen (msgstr->str, (gssize) msgstr->len);
920 /* FIXME: line_break_column isn't supposedly public */
921 gint line_len = geany_data->editor_prefs->line_break_column;
922 gint msgstr_kw_len;
923
924 /* if line break column doesn't have a reasonable value, don't use it */
925 if (line_len < 8) {
926 line_len = 72;
927 }
928
929 sci_start_undo_action (sci);
930 scintilla_send_message (sci, SCI_DELETERANGE,
931 (uptr_t) start, end + 1 - start);
932
933 msgstr_kw_len = start - sci_get_position_from_line (sci, sci_get_line_from_position (sci, start));
934 if (msgstr_kw_len + len + 2 <= line_len &&
935 find_line_break (msgstr->str) == NULL) {
936 /* if all can go in the msgstr line and there's no newline, put it here */
937 gchar *text = g_strconcat ("\"", msgstr->str, "\"", NULL);
938 sci_insert_text (sci, start, text);
939 g_free (text);
940 } else {
941 /* otherwise, put nothing on the msgstr line and split it up through
942 * next ones */
943 gchar **chunks = split_msg (msgstr->str, (gsize)(line_len - 2));
944 guint i;
945
946 sci_insert_text (sci, start, "\"\""); /* nothing on the msgstr line */
947 start += 2;
948 for (i = 0; chunks[i]; i++) {
949 SETPTR (chunks[i], g_strconcat ("\n\"", chunks[i], "\"", NULL));
950 sci_insert_text (sci, start, chunks[i]);
951 start += (gint) strlen (chunks[i]);
952 }
953
954 g_strfreev (chunks);
955 }
956
957 scintilla_send_message (sci, SCI_GOTOPOS, (uptr_t) (start + 1), 0);
958 sci_end_undo_action (sci);
959
960 g_string_free (msgstr, TRUE);
961 }
962 }
963 }
964
965 /* returns the first non-default style on the line, or the default style if
966 * there is no other on that line */
967 static gint
find_first_non_default_style_on_line(ScintillaObject * sci,gint line)968 find_first_non_default_style_on_line (ScintillaObject *sci,
969 gint line)
970 {
971 gint pos = sci_get_position_from_line (sci, line);
972 gint end = sci_get_line_end_position (sci, line);
973 gint style;
974
975 do {
976 style = sci_get_style_at (sci, pos++);
977 } while (style == SCE_PO_DEFAULT && pos < end);
978
979 return style;
980 }
981
982 /* checks whether @line is a primary msgid line, e.g. not a plural form */
983 static gboolean
line_is_primary_msgid(ScintillaObject * sci,gint line)984 line_is_primary_msgid (ScintillaObject *sci,
985 gint line)
986 {
987 gint pos = (gint) scintilla_send_message (sci, SCI_GETLINEINDENTPOSITION,
988 (uptr_t) line, 0);
989
990 return (sci_get_char_at (sci, pos++) == 'm' &&
991 sci_get_char_at (sci, pos++) == 's' &&
992 sci_get_char_at (sci, pos++) == 'g' &&
993 sci_get_char_at (sci, pos++) == 'i' &&
994 sci_get_char_at (sci, pos++) == 'd' &&
995 g_ascii_isspace (sci_get_char_at (sci, pos)));
996 }
997
998 /* parse flags line @line and puts the read flags in @flags
999 * a flags line looks like:
1000 * #, flag-1, flag-2, flag-2, ... */
1001 static void
parse_flags_line(ScintillaObject * sci,gint line,GPtrArray * flags)1002 parse_flags_line (ScintillaObject *sci,
1003 gint line,
1004 GPtrArray *flags)
1005 {
1006 gint start = sci_get_position_from_line (sci, line);
1007 gint end = sci_get_line_end_position (sci, line);
1008 gint pos;
1009 gint ws, we;
1010 gint ch;
1011
1012 pos = start;
1013 /* skip leading space and markers */
1014 while (pos <= end && ((ch = sci_get_char_at (sci, pos)) == '#' ||
1015 ch == ',' || g_ascii_isspace (ch))) {
1016 pos++;
1017 }
1018 /* and read the flags */
1019 for (ws = we = pos; pos <= end; pos++) {
1020 ch = sci_get_char_at (sci, pos);
1021
1022 if (ch == ',' || g_ascii_isspace (ch) || pos >= end) {
1023 if (ws < we) {
1024 g_ptr_array_add (flags, sci_get_contents_range (sci, ws, we + 1));
1025 }
1026 ws = pos + 1;
1027 } else {
1028 we = pos;
1029 }
1030 }
1031 }
1032
1033 static gint
find_msgid_line_at(GeanyDocument * doc,gint pos)1034 find_msgid_line_at (GeanyDocument *doc,
1035 gint pos)
1036 {
1037 ScintillaObject *sci = doc->editor->sci;
1038 gint line = sci_get_line_from_position (sci, pos);
1039 gint style = find_first_non_default_style_on_line (sci, line);
1040
1041 while (line > 0 &&
1042 (style == SCE_PO_DEFAULT ||
1043 (style == SCE_PO_MSGID && ! line_is_primary_msgid (sci, line)) ||
1044 style == SCE_PO_MSGID_TEXT ||
1045 style == SCE_PO_MSGSTR ||
1046 style == SCE_PO_MSGSTR_TEXT)) {
1047 line--;
1048 style = find_first_non_default_style_on_line (sci, line);
1049 }
1050 while (line < sci_get_line_count (sci) &&
1051 (style == SCE_PO_COMMENT ||
1052 style == SCE_PO_PROGRAMMER_COMMENT ||
1053 style == SCE_PO_REFERENCE ||
1054 style == SCE_PO_FLAGS ||
1055 style == SCE_PO_FUZZY)) {
1056 line++;
1057 style = find_first_non_default_style_on_line (sci, line);
1058 }
1059
1060 return (style == SCE_PO_MSGID) ? line : -1;
1061 }
1062
1063 static gint
find_flags_line_at(GeanyDocument * doc,gint pos)1064 find_flags_line_at (GeanyDocument *doc,
1065 gint pos)
1066 {
1067 gint line = find_msgid_line_at (doc, pos);
1068
1069 if (line > 0) {
1070 gint style;
1071
1072 do {
1073 line--;
1074 style = find_first_non_default_style_on_line (doc->editor->sci, line);
1075 } while (line > 0 &&
1076 (style == SCE_PO_COMMENT ||
1077 style == SCE_PO_PROGRAMMER_COMMENT ||
1078 style == SCE_PO_REFERENCE));
1079
1080 if (style != SCE_PO_FLAGS && style != SCE_PO_FUZZY) {
1081 line = -1;
1082 }
1083 }
1084
1085 return line;
1086 }
1087
1088 static GPtrArray *
get_flags_at(GeanyDocument * doc,gint pos)1089 get_flags_at (GeanyDocument *doc,
1090 gint pos)
1091 {
1092 GPtrArray *flags = NULL;
1093 gint line = find_flags_line_at (doc, pos);
1094
1095 if (line >= 0) {
1096 flags = g_ptr_array_new_with_free_func (g_free);
1097 parse_flags_line (doc->editor->sci, line, flags);
1098 }
1099
1100 return flags;
1101 }
1102
1103 /* adds or remove @flag from @flags. returns whether the flag was added */
1104 static gboolean
toggle_flag(GPtrArray * flags,const gchar * flag)1105 toggle_flag (GPtrArray *flags,
1106 const gchar *flag)
1107 {
1108 gboolean add = TRUE;
1109 guint i;
1110
1111 /* search for the flag and remove it */
1112 for (i = 0; i < flags->len; i++) {
1113 if (strcmp (g_ptr_array_index (flags, i), flag) == 0) {
1114 g_ptr_array_remove_index (flags, i);
1115 add = FALSE;
1116 break;
1117 }
1118 }
1119 /* if it wasntt there, add it */
1120 if (add) {
1121 g_ptr_array_add (flags, g_strdup (flag));
1122 }
1123
1124 return add;
1125 }
1126
1127 /* writes a flags line at @pos containgin @flags */
1128 static void
write_flags(ScintillaObject * sci,gint pos,GPtrArray * flags)1129 write_flags (ScintillaObject *sci,
1130 gint pos,
1131 GPtrArray *flags)
1132 {
1133 if (flags->len > 0) {
1134 guint i;
1135
1136 sci_start_undo_action (sci);
1137 sci_insert_text (sci, pos, "#");
1138 pos ++;
1139 for (i = 0; i < flags->len; i++) {
1140 const gchar *flag = g_ptr_array_index (flags, i);
1141
1142 sci_insert_text (sci, pos, ", ");
1143 pos += 2;
1144 sci_insert_text (sci, pos, flag);
1145 pos += (gint) strlen (flag);
1146 }
1147 sci_insert_text (sci, pos, "\n");
1148 sci_end_undo_action (sci);
1149 }
1150 }
1151
1152 static void
delete_line(ScintillaObject * sci,gint line)1153 delete_line (ScintillaObject *sci,
1154 gint line)
1155 {
1156 gint pos = sci_get_position_from_line (sci, line);
1157 gint length = sci_get_line_length (sci, line);
1158
1159 scintilla_send_message (sci, SCI_DELETERANGE, (uptr_t) pos, (sptr_t) length);
1160 }
1161
1162 static void
on_kb_toggle_fuzziness(guint key_id)1163 on_kb_toggle_fuzziness (guint key_id)
1164 {
1165 GeanyDocument *doc = document_get_current ();
1166
1167 if (doc_is_po (doc)) {
1168 ScintillaObject *sci = doc->editor->sci;
1169 gint pos = sci_get_current_position (sci);
1170 gint msgid_line = find_msgid_line_at (doc, pos);
1171 gint flags_line = find_flags_line_at (doc, pos);
1172
1173 if (flags_line >= 0 || msgid_line >= 0) {
1174 GPtrArray *flags = g_ptr_array_new_with_free_func (g_free);
1175
1176 sci_start_undo_action (sci);
1177
1178 if (flags_line >= 0) {
1179 parse_flags_line (sci, flags_line, flags);
1180 delete_line (sci, flags_line);
1181 } else {
1182 flags_line = msgid_line;
1183 }
1184
1185 toggle_flag (flags, "fuzzy");
1186 write_flags (sci, sci_get_position_from_line (sci, flags_line), flags);
1187
1188 sci_end_undo_action (sci);
1189
1190 g_ptr_array_free (flags, TRUE);
1191 }
1192 }
1193 }
1194
1195 static gint
find_header_start(GeanyDocument * doc)1196 find_header_start (GeanyDocument *doc)
1197 {
1198 if (doc_is_po (doc)) {
1199 for (gint line = 0; line < sci_get_line_count (doc->editor->sci); line++) {
1200 if (find_first_non_default_style_on_line (doc->editor->sci, line) == SCE_PO_MSGID) {
1201 gint pos = sci_get_position_from_line (doc->editor->sci, line);
1202 GString *str = get_msgid_text_at (doc, pos);
1203
1204 if (str) {
1205 gboolean is_header = (*str->str == 0);
1206
1207 g_string_free (str, TRUE);
1208 if (is_header) {
1209 return pos;
1210 }
1211 }
1212 }
1213 }
1214 }
1215
1216 return -1;
1217 }
1218
1219 static void
on_document_save(GObject * obj,GeanyDocument * doc,gpointer user_data)1220 on_document_save (GObject *obj,
1221 GeanyDocument *doc,
1222 gpointer user_data)
1223 {
1224 gint header_start;
1225
1226 if (doc_is_po (doc) && plugin.update_headers &&
1227 (header_start = find_header_start (doc)) >= 0) {
1228 gchar *name = escape_string (geany_data->template_prefs->developer);
1229 gchar *mail = escape_string (geany_data->template_prefs->mail);
1230 gchar *date;
1231 gchar *translator;
1232 gchar *generator;
1233
1234 date = utils_get_date_time ("\"PO-Revision-Date: %Y-%m-%d %H:%M%z\\n\"",
1235 NULL);
1236 translator = g_strdup_printf ("\"Last-Translator: %s <%s>\\n\"",
1237 name, mail);
1238 generator = g_strdup_printf ("\"X-Generator: Geany / PoHelper %s\\n\"",
1239 VERSION);
1240
1241 sci_start_undo_action (doc->editor->sci);
1242 regex_replace (doc->editor->sci,
1243 header_start, find_msgstr_end_at (doc, header_start) + 1,
1244 "^\"PO-Revision-Date: .*\"$", date);
1245 regex_replace (doc->editor->sci,
1246 header_start, find_msgstr_end_at (doc, header_start) + 1,
1247 "^\"Last-Translator: .*\"$", translator);
1248 regex_replace (doc->editor->sci,
1249 header_start, find_msgstr_end_at (doc, header_start) + 1,
1250 "^\"X-Generator: .*\"$", generator);
1251 sci_end_undo_action (doc->editor->sci);
1252
1253 g_free (date);
1254 g_free (translator);
1255 g_free (generator);
1256 g_free (name);
1257 g_free (mail);
1258 }
1259 }
1260
1261 typedef struct {
1262 gdouble translated;
1263 gdouble fuzzy;
1264 gdouble untranslated;
1265 } StatsGraphData;
1266
1267 /*
1268 * rounded_rectangle:
1269 * @cr: a Cairo context
1270 * @x: X coordinate of the top-left corner of the rectangle
1271 * @y: Y coordinate of the top-left corner of the rectangle
1272 * @width: width of the rectangle
1273 * @height: height of the rectangle
1274 * @r1: radius of the top-left corner
1275 * @r2: radius of the top-right corner
1276 * @r3: radius of the bottom-right corner
1277 * @r4: radius of the bottom-left corner
1278 *
1279 * Creates a rectangle path with rounded corners.
1280 *
1281 * Warning: The rectangle should be big enough to include the corners,
1282 * otherwise the result will be weird. For example, if all corners
1283 * radius are set to 5, the rectangle should be at least 10x10.
1284 */
1285 static void
rounded_rectangle(cairo_t * cr,gdouble x,gdouble y,gdouble width,gdouble height,gdouble r1,gdouble r2,gdouble r3,gdouble r4)1286 rounded_rectangle (cairo_t *cr,
1287 gdouble x,
1288 gdouble y,
1289 gdouble width,
1290 gdouble height,
1291 gdouble r1,
1292 gdouble r2,
1293 gdouble r3,
1294 gdouble r4)
1295 {
1296 cairo_move_to (cr, x + r1, y);
1297 cairo_arc (cr, x + width - r2, y + r2, r2, -G_PI/2.0, 0);
1298 cairo_arc (cr, x + width - r3, y + height - r3, r3, 0, G_PI/2.0);
1299 cairo_arc (cr, x + r4, y + height - r4, r4, G_PI/2.0, -G_PI);
1300 cairo_arc (cr, x + r1, y + r1, r1, -G_PI, -G_PI/2.0);
1301 cairo_close_path (cr);
1302 }
1303
1304 #if ! GTK_CHECK_VERSION (3, 0, 0) && ! defined (gtk_widget_get_allocated_width)
1305 # define gtk_widget_get_allocated_width(w) (GTK_WIDGET (w)->allocation.width)
1306 #endif
1307 #if ! GTK_CHECK_VERSION (3, 0, 0) && ! defined (gtk_widget_get_allocated_height)
1308 # define gtk_widget_get_allocated_height(w) (GTK_WIDGET (w)->allocation.height)
1309 #endif
1310
1311 static gboolean
stats_graph_draw(GtkWidget * widget,cairo_t * cr,gpointer user_data)1312 stats_graph_draw (GtkWidget *widget,
1313 cairo_t *cr,
1314 gpointer user_data)
1315 {
1316 const StatsGraphData *data = user_data;
1317 const gint width = gtk_widget_get_allocated_width (widget);
1318 const gint height = gtk_widget_get_allocated_height (widget);
1319 const gdouble translated = width * data->translated;
1320 const gdouble fuzzy = width * data->fuzzy;
1321 const gdouble untranslated = width * data->untranslated;
1322 const gdouble r = MIN (width / 4, height / 4);
1323 cairo_pattern_t *pat;
1324
1325 rounded_rectangle (cr, 0, 0, width, height, r, r, r, r);
1326 cairo_clip (cr);
1327
1328 gdk_cairo_set_source_color (cr, &plugin.color_translated);
1329 cairo_rectangle (cr, 0, 0, translated, height);
1330 cairo_fill (cr);
1331
1332 gdk_cairo_set_source_color (cr, &plugin.color_fuzzy);
1333 cairo_rectangle (cr, translated, 0, fuzzy, height);
1334 cairo_fill (cr);
1335
1336 gdk_cairo_set_source_color (cr, &plugin.color_untranslated);
1337 cairo_rectangle (cr, translated + fuzzy, 0, untranslated, height);
1338 cairo_fill (cr);
1339
1340 /* draw a nice thin border */
1341 cairo_set_line_width (cr, 1.0);
1342 cairo_set_source_rgba (cr, 0, 0, 0, 0.2);
1343 rounded_rectangle (cr, 0.5, 0.5, width - 1, height - 1, r, r, r, r);
1344 cairo_stroke (cr);
1345
1346 /* draw a gradient to give the graph a little depth */
1347 pat = cairo_pattern_create_linear (0, 0, 0, height);
1348 cairo_pattern_add_color_stop_rgba (pat, 0, 1, 1, 1, 0.2);
1349 cairo_pattern_add_color_stop_rgba (pat, height, 0, 0, 0, 0.2);
1350 cairo_set_source (cr, pat);
1351 cairo_pattern_destroy (pat);
1352 cairo_rectangle (cr, 0, 0, width, height);
1353 cairo_paint (cr);
1354
1355 return TRUE;
1356 }
1357
1358 static gboolean
stats_graph_query_tooltip(GtkWidget * widget,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip,gpointer user_data)1359 stats_graph_query_tooltip (GtkWidget *widget,
1360 gint x,
1361 gint y,
1362 gboolean keyboard_mode,
1363 GtkTooltip *tooltip,
1364 gpointer user_data)
1365 {
1366 const StatsGraphData *data = user_data;
1367 gchar *markup = NULL;
1368
1369 if (keyboard_mode) {
1370 gchar *translated_str = g_strdup_printf (_("<b>Translated:</b> %.3g%%"),
1371 data->translated * 100);
1372 gchar *fuzzy_str = g_strdup_printf (_("<b>Fuzzy:</b> %.3g%%"),
1373 data->fuzzy * 100);
1374 gchar *untranslated_str = g_strdup_printf (_("<b>Untranslated:</b> %.3g%%"),
1375 data->untranslated * 100);
1376
1377 markup = g_strconcat (translated_str, "\n",
1378 fuzzy_str, "\n",
1379 untranslated_str, NULL);
1380 g_free (translated_str);
1381 g_free (fuzzy_str);
1382 g_free (untranslated_str);
1383 } else {
1384 const gint width = gtk_widget_get_allocated_width (widget);
1385
1386 if (x <= width * data->translated) {
1387 markup = g_strdup_printf (_("<b>Translated:</b> %.3g%%"),
1388 data->translated * 100);
1389 } else if (x <= width * (data->translated + data->fuzzy)) {
1390 markup = g_strdup_printf (_("<b>Fuzzy:</b> %.3g%%"), data->fuzzy * 100);
1391 } else {
1392 markup = g_strdup_printf (_("<b>Untranslated:</b> %.3g%%"),
1393 data->untranslated * 100);
1394 }
1395 }
1396
1397 gtk_tooltip_set_markup (tooltip, markup);
1398 g_free (markup);
1399
1400 return TRUE;
1401 }
1402
1403 #if ! GTK_CHECK_VERSION (3, 0, 0)
1404 static gboolean
on_stats_graph_expose_event(GtkWidget * widget,GdkEvent * event,gpointer data)1405 on_stats_graph_expose_event (GtkWidget *widget,
1406 GdkEvent *event,
1407 gpointer data)
1408 {
1409 cairo_t *cr = gdk_cairo_create (GDK_DRAWABLE (widget->window));
1410 gboolean ret = stats_graph_draw (widget, cr, data);
1411
1412 cairo_destroy (cr);
1413
1414 return ret;
1415 }
1416 #endif
1417
1418 static void
on_color_button_color_notify(GtkWidget * widget,GParamSpec * pspec,gpointer user_data)1419 on_color_button_color_notify (GtkWidget *widget,
1420 GParamSpec *pspec,
1421 gpointer user_data)
1422 {
1423 gtk_color_button_get_color (GTK_COLOR_BUTTON (widget), user_data);
1424 }
1425
1426 static gchar *
get_data_dir_path(const gchar * filename)1427 get_data_dir_path (const gchar *filename)
1428 {
1429 gchar *prefix = NULL;
1430 gchar *path;
1431
1432 #ifdef G_OS_WIN32
1433 prefix = g_win32_get_package_installation_directory_of_module (NULL);
1434 #elif defined(__APPLE__)
1435 if (g_getenv ("GEANY_PLUGINS_SHARE_PATH"))
1436 return g_build_filename( g_getenv ("GEANY_PLUGINS_SHARE_PATH"),
1437 PLUGIN, filename, NULL);
1438 #endif
1439 path = g_build_filename (prefix ? prefix : "", PLUGINDATADIR, filename, NULL);
1440 g_free (prefix);
1441 return path;
1442 }
1443
1444 static void
show_stats_dialog(guint all,guint translated,guint fuzzy,guint untranslated)1445 show_stats_dialog (guint all,
1446 guint translated,
1447 guint fuzzy,
1448 guint untranslated)
1449 {
1450 GError *error = NULL;
1451 gchar *ui_filename = get_data_dir_path ("stats.ui");;
1452 GtkBuilder *builder = gtk_builder_new ();
1453
1454 gtk_builder_set_translation_domain (builder, GETTEXT_PACKAGE);
1455 if (! gtk_builder_add_from_file (builder, ui_filename, &error)) {
1456 g_critical (_("Failed to load UI definition, please check your "
1457 "installation. The error was: %s"), error->message);
1458 g_error_free (error);
1459 } else {
1460 StatsGraphData data;
1461 GObject *dialog;
1462 GObject *drawing_area;
1463
1464 data.translated = all ? (translated * 1.0 / all) : 0;
1465 data.fuzzy = all ? (fuzzy * 1.0 / all) : 0;
1466 data.untranslated = all ? (untranslated * 1.0 / all) : 0;
1467
1468 drawing_area = gtk_builder_get_object (builder, "drawing_area");
1469 #if ! GTK_CHECK_VERSION (3, 0, 0)
1470 g_signal_connect (drawing_area,
1471 "expose-event", G_CALLBACK (on_stats_graph_expose_event),
1472 &data);
1473 #else
1474 g_signal_connect (drawing_area,
1475 "draw", G_CALLBACK (stats_graph_draw),
1476 &data);
1477 #endif
1478 g_signal_connect (drawing_area,
1479 "query-tooltip", G_CALLBACK (stats_graph_query_tooltip),
1480 &data);
1481 gtk_widget_set_has_tooltip (GTK_WIDGET (drawing_area), TRUE);
1482
1483 #define SET_LABEL_N(id, value) \
1484 do { \
1485 GObject *obj__ = gtk_builder_get_object (builder, (id)); \
1486 \
1487 if (! obj__) { \
1488 g_warning ("Object \"%s\" is missing from the UI definition", (id)); \
1489 } else { \
1490 gchar *text__ = g_strdup_printf (_("%u (%.3g%%)"), \
1491 (value), \
1492 all ? ((value) * 100.0 / all) : 0); \
1493 \
1494 gtk_label_set_text (GTK_LABEL (obj__), text__); \
1495 g_free (text__); \
1496 } \
1497 } while (0)
1498
1499 SET_LABEL_N ("n_translated", translated);
1500 SET_LABEL_N ("n_fuzzy", fuzzy);
1501 SET_LABEL_N ("n_untranslated", untranslated);
1502
1503 #undef SET_LABEL_N
1504
1505 #define BIND_COLOR_BTN(id, color) \
1506 do { \
1507 GObject *obj__ = gtk_builder_get_object (builder, (id)); \
1508 \
1509 if (! obj__) { \
1510 g_warning ("Object \"%s\" is missing from the UI definition", (id)); \
1511 } else { \
1512 gtk_color_button_set_color (GTK_COLOR_BUTTON (obj__), (color)); \
1513 g_signal_connect (obj__, "notify::color", \
1514 G_CALLBACK (on_color_button_color_notify), \
1515 (color)); \
1516 /* queue a redraw on the drawing area so it uses the new color */ \
1517 g_signal_connect_swapped (obj__, "notify::color", \
1518 G_CALLBACK (gtk_widget_queue_draw), \
1519 drawing_area); \
1520 } \
1521 } while (0)
1522
1523 BIND_COLOR_BTN ("color_translated", &plugin.color_translated);
1524 BIND_COLOR_BTN ("color_fuzzy", &plugin.color_fuzzy);
1525 BIND_COLOR_BTN ("color_untranslated", &plugin.color_untranslated);
1526
1527 #undef BIND_COLOR_BTN
1528
1529 dialog = gtk_builder_get_object (builder, "dialog");
1530 gtk_window_set_transient_for (GTK_WINDOW (dialog),
1531 GTK_WINDOW (geany_data->main_widgets->window));
1532 gtk_dialog_run (GTK_DIALOG (dialog));
1533 gtk_widget_destroy (GTK_WIDGET (dialog));
1534 }
1535 g_free (ui_filename);
1536 g_object_unref (builder);
1537 }
1538
1539 static void
on_kb_show_stats(guint key_id)1540 on_kb_show_stats (guint key_id)
1541 {
1542 GeanyDocument *doc = document_get_current ();
1543
1544 if (doc_is_po (doc)) {
1545 ScintillaObject *sci = doc->editor->sci;
1546 const gint len = sci_get_length (sci);
1547 gint pos = 0;
1548 guint all = 0;
1549 guint untranslated = 0;
1550 guint fuzzy = 0;
1551
1552 /* don't use find_message() because we want only match one block, not each
1553 * msgstr as there might be plural forms */
1554 while ((pos = find_style (sci, SCE_PO_MSGID, pos, len)) >= 0 &&
1555 (pos = find_style (sci, SCE_PO_MSGSTR, pos, len)) >= 0) {
1556 GString *msgid = get_msgid_text_at (doc, pos);
1557 GString *msgstr = get_msgstr_text_at (doc, pos);
1558
1559 if (msgid->len > 0) {
1560 all++;
1561 if (msgstr->len < 1) {
1562 untranslated++;
1563 } else {
1564 GPtrArray *flags = get_flags_at (doc, pos);
1565
1566 if (flags) {
1567 fuzzy += ! toggle_flag (flags, "fuzzy");
1568
1569 g_ptr_array_free (flags, TRUE);
1570 }
1571 }
1572 }
1573 g_string_free (msgstr, TRUE);
1574 g_string_free (msgid, TRUE);
1575 }
1576
1577 show_stats_dialog (all, all - untranslated - fuzzy, fuzzy, untranslated);
1578 }
1579 }
1580
1581 static const struct Action {
1582 guint id;
1583 const gchar *name;
1584 GeanyKeyCallback callback;
1585 const gchar *label;
1586 const gchar *widget;
1587 } G_actions[] = {
1588 { GPH_KB_GOTO_PREV, "goto-prev",
1589 on_kb_goto_prev,
1590 N_("Go to previous string"), "previous_string" },
1591 { GPH_KB_GOTO_NEXT, "goto-next",
1592 on_kb_goto_next,
1593 N_("Go to next string"), "next_string" },
1594 { GPH_KB_GOTO_PREV_UNTRANSLATED, "goto-prev-untranslated",
1595 on_kb_goto_prev_untranslated,
1596 N_("Go to previous untranslated string"), "previous_untranslated" },
1597 { GPH_KB_GOTO_NEXT_UNTRANSLATED, "goto-next-untranslated",
1598 on_kb_goto_next_untranslated,
1599 N_("Go to next untranslated string"), "next_untranslated" },
1600 { GPH_KB_GOTO_PREV_FUZZY, "goto-prev-fuzzy",
1601 on_kb_goto_prev_fuzzy,
1602 N_("Go to previous fuzzily translated string"), "previous_fuzzy" },
1603 { GPH_KB_GOTO_NEXT_FUZZY, "goto-next-fuzzy",
1604 on_kb_goto_next_fuzzy,
1605 N_("Go to next fuzzily translated string"), "next_fuzzy" },
1606 { GPH_KB_GOTO_PREV_UNTRANSLATED_OR_FUZZY, "goto-prev-untranslated-or-fuzzy",
1607 on_kb_goto_prev_untranslated_or_fuzzy,
1608 N_("Go to previous untranslated or fuzzy string"),
1609 "previous_untranslated_or_fuzzy" },
1610 { GPH_KB_GOTO_NEXT_UNTRANSLATED_OR_FUZZY, "goto-next-untranslated-or-fuzzy",
1611 on_kb_goto_next_untranslated_or_fuzzy,
1612 N_("Go to next untranslated or fuzzy string"),
1613 "next_untranslated_or_fuzzy" },
1614 { GPH_KB_PASTE_UNTRANSLATED, "paste-untranslated",
1615 on_kb_paste_untranslated,
1616 N_("Paste original untranslated string to translation"),
1617 "paste_message_as_translation" },
1618 { GPH_KB_REFLOW, "reflow",
1619 on_kb_reflow,
1620 N_("Reflow the current translation string"), "reflow_translation" },
1621 { GPH_KB_TOGGLE_FUZZY, "toggle-fuzziness",
1622 on_kb_toggle_fuzziness,
1623 N_("Toggle current translation fuzziness"), "toggle_fuzziness" },
1624 { GPH_KB_SHOW_STATS, "show-stats",
1625 on_kb_show_stats,
1626 N_("Show statistics of the current document"), "show_stats" }
1627 };
1628
1629 static void
on_widget_kb_activate(GtkMenuItem * widget,struct Action * action)1630 on_widget_kb_activate (GtkMenuItem *widget,
1631 struct Action *action)
1632 {
1633 action->callback (action->id);
1634 }
1635
1636 static void
on_update_headers_upon_save_toggled(GtkCheckMenuItem * item,gpointer data)1637 on_update_headers_upon_save_toggled (GtkCheckMenuItem *item,
1638 gpointer data)
1639 {
1640 plugin.update_headers = gtk_check_menu_item_get_active (item);
1641 }
1642
1643 static gchar *
get_config_filename(void)1644 get_config_filename (void)
1645 {
1646 return g_build_filename (geany_data->app->configdir, "plugins",
1647 "pohelper", "pohelper.conf", NULL);
1648 }
1649
1650 /* loads @filename in @kf and return %FALSE if failed, emitting a warning
1651 * unless the file was simply missing */
1652 static gboolean
load_keyfile(GKeyFile * kf,const gchar * filename,GKeyFileFlags flags)1653 load_keyfile (GKeyFile *kf,
1654 const gchar *filename,
1655 GKeyFileFlags flags)
1656 {
1657 GError *error = NULL;
1658
1659 if (! g_key_file_load_from_file (kf, filename, flags, &error)) {
1660 if (error->domain != G_FILE_ERROR || error->code != G_FILE_ERROR_NOENT) {
1661 g_warning (_("Failed to load configuration file: %s"), error->message);
1662 }
1663 g_error_free (error);
1664
1665 return FALSE;
1666 }
1667
1668 return TRUE;
1669 }
1670
1671 /* writes @kf in @filename, possibly creating directories to be able to write
1672 * in @filename */
1673 static gboolean
write_keyfile(GKeyFile * kf,const gchar * filename)1674 write_keyfile (GKeyFile *kf,
1675 const gchar *filename)
1676 {
1677 gchar *dirname = g_path_get_dirname (filename);
1678 GError *error = NULL;
1679 gint err;
1680 gchar *data;
1681 gsize length;
1682 gboolean success = FALSE;
1683
1684 data = g_key_file_to_data (kf, &length, NULL);
1685 if ((err = utils_mkdir (dirname, TRUE)) != 0) {
1686 g_critical (_("Failed to create configuration directory \"%s\": %s"),
1687 dirname, g_strerror (err));
1688 } else if (! g_file_set_contents (filename, data, (gssize) length, &error)) {
1689 g_critical (_("Failed to save configuration file: %s"), error->message);
1690 g_error_free (error);
1691 } else {
1692 success = TRUE;
1693 }
1694 g_free (data);
1695 g_free (dirname);
1696
1697 return success;
1698 }
1699
1700 /*
1701 * get_setting_color:
1702 * @kf: a #GKeyFile from which load the color
1703 * @group: the key file group
1704 * @key: the key file key
1705 * @color: (out): the color to fill with the read value. If the key is not
1706 * found, the color isn't updated
1707 *
1708 * Loads a color from a key file entry.
1709 *
1710 * Returns: %TRUE if the color was loaded, %FALSE otherwise.
1711 */
1712 static gboolean
get_setting_color(GKeyFile * kf,const gchar * group,const gchar * key,GdkColor * color)1713 get_setting_color (GKeyFile *kf,
1714 const gchar *group,
1715 const gchar *key,
1716 GdkColor *color)
1717 {
1718 gboolean success = FALSE;
1719 gchar *value = g_key_file_get_value (kf, group, key, NULL);
1720
1721 if (value) {
1722 success = gdk_color_parse (value, color);
1723 g_free (value);
1724 }
1725
1726 return success;
1727 }
1728
1729 static void
set_setting_color(GKeyFile * kf,const gchar * group,const gchar * key,const GdkColor * color)1730 set_setting_color (GKeyFile *kf,
1731 const gchar *group,
1732 const gchar *key,
1733 const GdkColor *color)
1734 {
1735 gchar *value = gdk_color_to_string (color);
1736
1737 g_key_file_set_value (kf, group, key, value);
1738 g_free (value);
1739 }
1740
1741 static void
load_config(void)1742 load_config (void)
1743 {
1744 gchar *filename = get_config_filename ();
1745 GKeyFile *kf = g_key_file_new ();
1746
1747 if (load_keyfile (kf, filename, G_KEY_FILE_NONE)) {
1748 plugin.update_headers = utils_get_setting_boolean (kf, "general",
1749 "update-headers",
1750 plugin.update_headers);
1751 get_setting_color (kf, "colors", "translated", &plugin.color_translated);
1752 get_setting_color (kf, "colors", "fuzzy", &plugin.color_fuzzy);
1753 get_setting_color (kf, "colors", "untranslated", &plugin.color_untranslated);
1754 }
1755 g_key_file_free (kf);
1756 g_free (filename);
1757 }
1758
1759 static void
save_config(void)1760 save_config (void)
1761 {
1762 gchar *filename = get_config_filename ();
1763 GKeyFile *kf = g_key_file_new ();
1764
1765 load_keyfile (kf, filename, G_KEY_FILE_KEEP_COMMENTS);
1766 g_key_file_set_boolean (kf, "general", "update-headers",
1767 plugin.update_headers);
1768 set_setting_color (kf, "colors", "translated", &plugin.color_translated);
1769 set_setting_color (kf, "colors", "fuzzy", &plugin.color_fuzzy);
1770 set_setting_color (kf, "colors", "untranslated", &plugin.color_untranslated);
1771 write_keyfile (kf, filename);
1772
1773 g_key_file_free (kf);
1774 g_free (filename);
1775 }
1776
1777 void
plugin_init(GeanyData * data)1778 plugin_init (GeanyData *data)
1779 {
1780 GtkBuilder *builder;
1781 GError *error = NULL;
1782 gchar *ui_filename;
1783 guint i;
1784
1785 load_config ();
1786
1787 ui_filename = get_data_dir_path ("menus.ui");
1788 builder = gtk_builder_new ();
1789 gtk_builder_set_translation_domain (builder, GETTEXT_PACKAGE);
1790 if (! gtk_builder_add_from_file (builder, ui_filename, &error)) {
1791 g_critical (_("Failed to load UI definition, please check your "
1792 "installation. The error was: %s"), error->message);
1793 g_error_free (error);
1794 g_object_unref (builder);
1795 builder = NULL;
1796 plugin.menu_item = NULL;
1797 } else {
1798 GObject *obj;
1799
1800 plugin.menu_item = GTK_WIDGET (gtk_builder_get_object (builder, "root_item"));
1801 gtk_menu_shell_append (GTK_MENU_SHELL (geany->main_widgets->tools_menu),
1802 plugin.menu_item);
1803
1804 obj = gtk_builder_get_object (builder, "update_headers_upon_save");
1805 gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (obj),
1806 plugin.update_headers);
1807 g_signal_connect (obj, "toggled",
1808 G_CALLBACK (on_update_headers_upon_save_toggled), NULL);
1809 }
1810 g_free (ui_filename);
1811
1812 /* signal handlers */
1813 plugin_signal_connect (geany_plugin, NULL, "document-activate", TRUE,
1814 G_CALLBACK (on_document_activate), NULL);
1815 plugin_signal_connect (geany_plugin, NULL, "document-filetype-set", TRUE,
1816 G_CALLBACK (on_document_filetype_set), NULL);
1817 plugin_signal_connect (geany_plugin, NULL, "document-close", TRUE,
1818 G_CALLBACK (on_document_close), NULL);
1819 plugin_signal_connect (geany_plugin, NULL, "document-before-save", TRUE,
1820 G_CALLBACK (on_document_save), NULL);
1821
1822 /* add keybindings */
1823 plugin.key_group = plugin_set_key_group (geany_plugin, "pohelper",
1824 GPH_KB_COUNT, NULL);
1825
1826 for (i = 0; i < G_N_ELEMENTS (G_actions); i++) {
1827 GtkWidget *widget = NULL;
1828
1829 if (builder && G_actions[i].widget) {
1830 GObject *obj = gtk_builder_get_object (builder, G_actions[i].widget);
1831
1832 if (! obj || ! GTK_IS_MENU_ITEM (obj)) {
1833 g_critical (_("Cannot find widget \"%s\" in the UI definition, "
1834 "please check your installation."), G_actions[i].widget);
1835 } else {
1836 widget = GTK_WIDGET (obj);
1837 g_signal_connect (widget, "activate",
1838 G_CALLBACK (on_widget_kb_activate),
1839 (gpointer) &G_actions[i]);
1840 }
1841 }
1842
1843 keybindings_set_item (plugin.key_group, G_actions[i].id,
1844 G_actions[i].callback, 0, 0, G_actions[i].name,
1845 _(G_actions[i].label), widget);
1846 }
1847 /* initial items sensitivity update */
1848 update_menu_items_sensitivity (document_get_current ());
1849
1850 if (builder) {
1851 g_object_unref (builder);
1852 }
1853 }
1854
1855 void
plugin_cleanup(void)1856 plugin_cleanup (void)
1857 {
1858 if (plugin.menu_item) {
1859 gtk_widget_destroy (plugin.menu_item);
1860 }
1861
1862 save_config ();
1863 }
1864