Branch: refs/heads/master Author: Colomban Wendling ban@herbesfolles.org Committer: Colomban Wendling ban@herbesfolles.org Date: Mon, 17 Jun 2013 15:06:36 UTC Commit: c4a2dc18d85fb694a5b21bec54781b5cfaec265b https://github.com/geany/geany-plugins/commit/c4a2dc18d85fb694a5b21bec54781b...
Log Message: ----------- pohelper: Add a stats dialog
Modified Paths: -------------- po/POTFILES.in pohelper/data/Makefile.am pohelper/data/menus.ui pohelper/data/stats.ui pohelper/src/gph-plugin.c
Modified: po/POTFILES.in 3 lines changed, 2 insertions(+), 1 deletions(-) =================================================================== @@ -211,11 +211,12 @@ gproject/src/gproject-sidebar.c markdown/src/conf.c markdown/src/plugin.c
- # Pairtaghighlighter pairtaghighlighter/src/pair_tag_highlighter.c + # PoHelper [type: gettext/glade]pohelper/data/menus.ui +[type: gettext/glade]pohelper/data/stats.ui pohelper/src/gph-plugin.c
# Pretty-printer
Modified: pohelper/data/Makefile.am 2 lines changed, 1 insertions(+), 1 deletions(-) =================================================================== @@ -1,4 +1,4 @@ include $(top_srcdir)/build/vars.docs.mk plugin = pohelper
-dist_plugindata_DATA = menus.ui +dist_plugindata_DATA = menus.ui stats.ui
Modified: pohelper/data/menus.ui 16 lines changed, 16 insertions(+), 0 deletions(-) =================================================================== @@ -224,6 +224,22 @@ </object> </child> <child> + <object class="GtkMenuItem" id="show_stats"> + <property name="use_action_appearance">False</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">Show statistics of the current document</property> + <property name="label" translatable="yes">_Show stats</property> + <property name="use_underline">True</property> + </object> + </child> + <child> + <object class="GtkSeparatorMenuItem" id="separator6"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + </child> + <child> <object class="GtkCheckMenuItem" id="update_headers_upon_save"> <property name="use_action_appearance">False</property> <property name="visible">True</property>
Modified: pohelper/data/stats.ui 222 lines changed, 222 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.16"/> + <!-- interface-naming-policy toplevel-contextual --> + <object class="GtkDialog" id="dialog"> + <property name="can_focus">False</property> + <property name="border_width">5</property> + <property name="title" translatable="yes">Translation statistics</property> + <property name="modal">True</property> + <property name="type_hint">dialog</property> + <child internal-child="vbox"> + <object class="GtkVBox" id="dialog-vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">2</property> + <child internal-child="action_area"> + <object class="GtkHButtonBox" id="dialog-action_area1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="layout_style">end</property> + <child> + <object class="GtkButton" id="button1"> + <property name="label">gtk-close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="has_focus">True</property> + <property name="can_default">True</property> + <property name="receives_default">True</property> + <property name="use_stock">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="border_width">6</property> + <property name="spacing">5</property> + <child> + <object class="GtkDrawingArea" id="drawing_area"> + <property name="height_request">24</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">3</property> + <property name="n_columns">3</property> + <property name="column_spacing">6</property> + <child> + <object class="GtkColorButton" id="color_translated"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="title" translatable="yes">Choose a color for translated strings</property> + <property name="color">#7373d2d21616</property> + </object> + <packing> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="color_fuzzy"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="title" translatable="yes">Choose a color for fuzzily translated strings</property> + <property name="color">#ededd4d40000</property> + </object> + <packing> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkColorButton" id="color_untranslated"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="relief">none</property> + <property name="title" translatable="yes">Choose a color for untranslated strings</property> + <property name="color">#cccc00000000</property> + </object> + <packing> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="x_options">GTK_FILL</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Translated:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Fuzzy:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label3"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">1</property> + <property name="label" translatable="yes">Untranslated:</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="n_translated"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="n_fuzzy"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">1</property> + <property name="bottom_attach">2</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="n_untranslated"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="label">0</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="right_attach">3</property> + <property name="top_attach">2</property> + <property name="bottom_attach">3</property> + <property name="y_options">GTK_FILL</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + <action-widgets> + <action-widget response="-7">button1</action-widget> + </action-widgets> + </object> +</interface>
Modified: pohelper/src/gph-plugin.c 388 lines changed, 387 insertions(+), 1 deletions(-) =================================================================== @@ -57,16 +57,24 @@ enum { GPH_KB_PASTE_UNTRANSLATED, GPH_KB_REFLOW, GPH_KB_TOGGLE_FUZZY, + GPH_KB_SHOW_STATS, GPH_KB_COUNT };
static struct Plugin { gboolean update_headers; + /* stats dialog colors */ + GdkColor color_translated; + GdkColor color_fuzzy; + GdkColor color_untranslated;
GtkWidget *menu_item; } plugin = { TRUE, + { 0, 0x7373, 0xd2d2, 0x1616 }, /* tango mid green */ + { 0, 0xeded, 0xd4d4, 0x0000 }, /* tango mid yellow */ + { 0, 0xcccc, 0x0000, 0x0000 }, /* tango mid red */ NULL };
@@ -724,6 +732,70 @@ get_msgstr_text_at (GeanyDocument *doc, return NULL; }
+/* finds the start of the msgid text at @pos. the returned position is the + * start of the msgid text style, so it's on the first opening quote. Returns + * -1 if none found */ +static gint +find_msgid_start_at (GeanyDocument *doc, + gint pos) +{ + if (doc_is_po (doc)) { + ScintillaObject *sci = doc->editor->sci; + gint style = sci_get_style_at (sci, pos); + + /* find the previous non-default style */ + while (pos > 0 && style == SCE_PO_DEFAULT) { + style = sci_get_style_at (sci, --pos); + } + + /* if a msgid or msgstr, go to the msgstr keyword */ + if (style == SCE_PO_MSGID_TEXT || + style == SCE_PO_MSGSTR || + style == SCE_PO_MSGSTR_TEXT) { + pos = find_style (sci, SCE_PO_MSGID, pos, 0); + if (pos >= 0) + style = SCE_PO_MSGID; + } + + if (style == SCE_PO_MSGID) { + return find_style (sci, SCE_PO_MSGID_TEXT, pos, sci_get_length (sci)); + } + } + + return -1; +} + +static GString * +get_msgid_text_at (GeanyDocument *doc, + gint pos) +{ + pos = find_msgid_start_at (doc, pos); + + if (pos >= 0) { + ScintillaObject *sci = doc->editor->sci; + GString *msgid = g_string_new (NULL); + gint length = sci_get_length (sci); + + while (sci_get_style_at (sci, pos) == SCE_PO_MSGID_TEXT) { + pos++; /* skip opening quote */ + while (sci_get_style_at (sci, pos + 1) == SCE_PO_MSGID_TEXT) { + g_string_append_c (msgid, sci_get_char_at (sci, pos)); + pos++; + } + pos++; /* skip closing quote */ + + /* skip until next non-default style */ + while (pos < length && sci_get_style_at (sci, pos) == SCE_PO_DEFAULT) { + pos++; + } + } + + return msgid; + } + + return NULL; +} + /* cuts @str in human-readable chunks for max @len. * cuts first at \n, then at spaces and punctuation */ static gchar ** @@ -948,6 +1020,21 @@ find_flags_line_at (GeanyDocument *doc, return line; }
+static GPtrArray * +get_flags_at (GeanyDocument *doc, + gint pos) +{ + GPtrArray *flags = NULL; + gint line = find_flags_line_at (doc, pos); + + if (line >= 0) { + flags = g_ptr_array_new (); + parse_flags_line (doc->editor->sci, line, flags); + } + + return flags; +} + /* adds or remove @flag from @flags. returns whether the flag was added */ static gboolean toggle_flag (GPtrArray *flags, @@ -1041,6 +1128,255 @@ on_kb_toggle_fuzziness (guint key_id) } }
+typedef struct { + gdouble translated; + gdouble fuzzy; + gdouble untranslated; +} StatsGraphData; + +/* + * rounded_rectangle: + * @cr: a Cairo context + * @x: X coordinate of the top-left corner of the rectangle + * @y: Y coordinate of the top-left corner of the rectangle + * @width: width of the rectangle + * @height: height of the rectangle + * @r1: radius of the top-left corner + * @r2: radius of the top-right corner + * @r3: radius of the bottom-right corner + * @r4: radius of the bottom-left corner + * + * Creates a rectangle path with rounded corners. + * + * Warning: The rectangle should be big enough to include the corners, + * otherwise the result will be weird. For example, if all corners + * radius are set to 5, the rectangle should be at least 10x10. + */ +static void +rounded_rectangle (cairo_t *cr, + gdouble x, + gdouble y, + gdouble width, + gdouble height, + gdouble r1, + gdouble r2, + gdouble r3, + gdouble r4) +{ + cairo_move_to (cr, x + r1, y); + cairo_arc (cr, x + width - r2, y + r2, r2, -G_PI/2.0, 0); + cairo_arc (cr, x + width - r3, y + height - r3, r3, 0, G_PI/2.0); + cairo_arc (cr, x + r4, y + height - r4, r4, G_PI/2.0, -G_PI); + cairo_arc (cr, x + r1, y + r1, r1, -G_PI, -G_PI/2.0); + cairo_close_path (cr); +} + +#if ! GTK_CHECK_VERSION (3, 0, 0) && ! defined (gtk_widget_get_allocated_width) +# define gtk_widget_get_allocated_width(w) (GTK_WIDGET (w)->allocation.width) +#endif +#if ! GTK_CHECK_VERSION (3, 0, 0) && ! defined (gtk_widget_get_allocated_height) +# define gtk_widget_get_allocated_height(w) (GTK_WIDGET (w)->allocation.height) +#endif + +static gboolean +stats_graph_draw (GtkWidget *widget, + cairo_t *cr, + gpointer user_data) +{ + const StatsGraphData *data = user_data; + const gint width = gtk_widget_get_allocated_width (widget); + const gint height = gtk_widget_get_allocated_height (widget); + const gdouble translated = width * data->translated; + const gdouble fuzzy = width * data->fuzzy; + const gdouble untranslated = width * data->untranslated; + const gdouble r = MIN (width / 4, height / 4); + cairo_pattern_t *pat; + + rounded_rectangle (cr, 0, 0, width, height, r, r, r, r); + cairo_clip (cr); + + gdk_cairo_set_source_color (cr, &plugin.color_translated); + cairo_rectangle (cr, 0, 0, translated, height); + cairo_fill (cr); + + gdk_cairo_set_source_color (cr, &plugin.color_fuzzy); + cairo_rectangle (cr, translated, 0, fuzzy, height); + cairo_fill (cr); + + gdk_cairo_set_source_color (cr, &plugin.color_untranslated); + cairo_rectangle (cr, translated + fuzzy, 0, untranslated, height); + cairo_fill (cr); + + /* draw a nice thin border */ + cairo_set_line_width (cr, 1.0); + cairo_set_source_rgba (cr, 0, 0, 0, 0.2); + rounded_rectangle (cr, 0.5, 0.5, width - 1, height - 1, r, r, r, r); + cairo_stroke (cr); + + /* draw a gradient to give the graph a little depth */ + pat = cairo_pattern_create_linear (0, 0, 0, height); + cairo_pattern_add_color_stop_rgba (pat, 0, 1, 1, 1, 0.2); + cairo_pattern_add_color_stop_rgba (pat, height, 0, 0, 0, 0.2); + cairo_set_source (cr, pat); + cairo_pattern_destroy (pat); + cairo_rectangle (cr, 0, 0, width, height); + cairo_paint (cr); + + return TRUE; +} + +#if ! GTK_CHECK_VERSION (3, 0, 0) +static gboolean +on_stats_graph_expose_event (GtkWidget *widget, + GdkEvent *event, + gpointer data) +{ + cairo_t *cr = gdk_cairo_create (GDK_DRAWABLE (widget->window)); + gboolean ret = stats_graph_draw (widget, cr, data); + + cairo_destroy (cr); + + return ret; +} +#endif + +static void +on_color_button_color_notify (GtkWidget *widget, + GParamSpec *pspec, + gpointer user_data) +{ + gtk_color_button_get_color (GTK_COLOR_BUTTON (widget), user_data); +} + +static void +show_stats_dialog (guint all, + guint translated, + guint untranslated, + guint fuzzy) +{ + GError *error = NULL; + GtkBuilder *builder = gtk_builder_new (); + + gtk_builder_set_translation_domain (builder, GETTEXT_PACKAGE); + if (! gtk_builder_add_from_file (builder, PKGDATADIR"/pohelper/stats.ui", + &error)) { + g_critical (_("Failed to load UI definition, please check your " + "installation. The error was: %s"), error->message); + g_error_free (error); + } else { + StatsGraphData data; + GObject *dialog; + GObject *drawing_area; + + data.translated = all ? (translated * 1.0 / all) : 0; + data.fuzzy = all ? (fuzzy * 1.0 / all) : 0; + data.untranslated = all ? (untranslated * 1.0 / all) : 0; + + drawing_area = gtk_builder_get_object (builder, "drawing_area"); +#if ! GTK_CHECK_VERSION (3, 0, 0) + g_signal_connect (drawing_area, + "expose-event", G_CALLBACK (on_stats_graph_expose_event), + &data); +#else + g_signal_connect (drawing_area, + "draw", G_CALLBACK (stats_graph_draw), + &data); +#endif + + #define SET_LABEL_N(id, value) \ + do { \ + GObject *obj__ = gtk_builder_get_object (builder, (id)); \ + \ + if (! obj__) { \ + g_warning ("Object "%s" is missing from the UI definition", (id)); \ + } else { \ + gchar *text__ = g_strdup_printf (_("%u (%.1f%%)"), \ + (value), \ + all ? ((value) * 100.0 / all) : 0); \ + \ + gtk_label_set_text (GTK_LABEL (obj__), text__); \ + g_free (text__); \ + } \ + } while (0) + + SET_LABEL_N ("n_translated", translated); + SET_LABEL_N ("n_fuzzy", fuzzy); + SET_LABEL_N ("n_untranslated", untranslated); + + #undef SET_LABEL_N + + #define BIND_COLOR_BTN(id, color) \ + do { \ + GObject *obj__ = gtk_builder_get_object (builder, (id)); \ + \ + if (! obj__) { \ + g_warning ("Object "%s" is missing from the UI definition", (id)); \ + } else { \ + gtk_color_button_set_color (GTK_COLOR_BUTTON (obj__), (color)); \ + g_signal_connect (obj__, "notify::color", \ + G_CALLBACK (on_color_button_color_notify), \ + (color)); \ + /* queue a redraw on the drawing area so it uses the new color */ \ + g_signal_connect_swapped (obj__, "notify::color", \ + G_CALLBACK (gtk_widget_queue_draw), \ + drawing_area); \ + } \ + } while (0) + + BIND_COLOR_BTN ("color_translated", &plugin.color_translated); + BIND_COLOR_BTN ("color_fuzzy", &plugin.color_fuzzy); + BIND_COLOR_BTN ("color_untranslated", &plugin.color_untranslated); + + #undef BIND_COLOR_BTN + + dialog = gtk_builder_get_object (builder, "dialog"); + gtk_window_set_transient_for (GTK_WINDOW (dialog), + GTK_WINDOW (geany_data->main_widgets->window)); + gtk_dialog_run (GTK_DIALOG (dialog)); + gtk_widget_destroy (GTK_WIDGET (dialog)); + } + g_object_unref (builder); +} + +static void +on_kb_show_stats (guint key_id) +{ + GeanyDocument *doc = document_get_current (); + + if (doc_is_po (doc)) { + const gint len = sci_get_length (doc->editor->sci); + gint pos = 0; + guint all = 0; + guint untranslated = 0; + guint fuzzy = 0; + + while ((pos = find_message (doc, pos, len)) >= 0) { + GString *msgid = get_msgid_text_at (doc, pos); + GString *msgstr = get_msgstr_text_at (doc, pos); + + if (msgid->len > 0) { + all++; + if (msgstr->len < 1) { + untranslated++; + } else { + GPtrArray *flags = get_flags_at (doc, pos); + + if (flags) { + fuzzy += ! toggle_flag (flags, "fuzzy"); + + g_ptr_array_foreach (flags, (GFunc) g_free, NULL); + g_ptr_array_free (flags, TRUE); + } + } + } + g_string_free (msgstr, TRUE); + g_string_free (msgid, TRUE); + } + + show_stats_dialog (all, all - untranslated - fuzzy, fuzzy, untranslated); + } +} + static const struct Action { guint id; const gchar *name; @@ -1083,7 +1419,10 @@ static const struct Action { N_("Reflow the current translation string"), "reflow_translation" }, { GPH_KB_TOGGLE_FUZZY, "toggle-fuzziness", on_kb_toggle_fuzziness, - N_("Toggle current translation fuzziness"), "toggle_fuzziness" } + N_("Toggle current translation fuzziness"), "toggle_fuzziness" }, + { GPH_KB_SHOW_STATS, "show-stats", + on_kb_show_stats, + N_("Show statistics of the current document"), "show_stats" } };
static void @@ -1157,6 +1496,47 @@ write_keyfile (GKeyFile *kf, return success; }
+/* + * get_setting_color: + * @kf: a #GKeyFile from which load the color + * @group: the key file group + * @key: the key file key + * @color: (out): the color to fill with the read value. If the key is not + * found, the color isn't updated + * + * Loads a color from a key file entry. + * + * Returns: %TRUE if the color was loaded, %FALSE otherwise. + */ +static gboolean +get_setting_color (GKeyFile *kf, + const gchar *group, + const gchar *key, + GdkColor *color) +{ + gboolean success = FALSE; + gchar *value = g_key_file_get_value (kf, group, key, NULL); + + if (value) { + success = gdk_color_parse (value, color); + g_free (value); + } + + return success; +} + +static void +set_setting_color (GKeyFile *kf, + const gchar *group, + const gchar *key, + const GdkColor *color) +{ + gchar *value = gdk_color_to_string (color); + + g_key_file_set_value (kf, group, key, value); + g_free (value); +} + static void load_config (void) { @@ -1167,6 +1547,9 @@ load_config (void) plugin.update_headers = utils_get_setting_boolean (kf, "general", "update-headers", plugin.update_headers); + get_setting_color (kf, "colors", "translated", &plugin.color_translated); + get_setting_color (kf, "colors", "fuzzy", &plugin.color_fuzzy); + get_setting_color (kf, "colors", "untranslated", &plugin.color_untranslated); } g_key_file_free (kf); g_free (filename); @@ -1181,6 +1564,9 @@ save_config (void) load_keyfile (kf, filename, G_KEY_FILE_KEEP_COMMENTS); g_key_file_set_boolean (kf, "general", "update-headers", plugin.update_headers); + set_setting_color (kf, "colors", "translated", &plugin.color_translated); + set_setting_color (kf, "colors", "fuzzy", &plugin.color_fuzzy); + set_setting_color (kf, "colors", "untranslated", &plugin.color_untranslated); write_keyfile (kf, filename);
g_key_file_free (kf);
-------------- This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).