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