/* Copyright 2015 Red Hat, Inc. * * GTK+ is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * GLib is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with GTK+; see the file COPYING. If not, * see . * * Author: Matthias Clasen */ #include #include #include #include #include "gtkbuilderprivate.h" typedef struct { GtkBuilder *builder; GList *classes; gboolean packing; gboolean packing_started; gboolean cell_packing; gboolean cell_packing_started; gint in_child; gint child_started; gchar **attribute_names; gchar **attribute_values; GString *value; gboolean unclosed_starttag; gint indent; } MyParserData; static GParamSpec * get_property_pspec (MyParserData *data, const gchar *class_name, const gchar *property_name) { GType type; GObjectClass *class; GParamSpec *pspec; gchar *canonical_name; type = g_type_from_name (class_name); if (type == G_TYPE_INVALID) return NULL; class = g_type_class_ref (type); canonical_name = g_strdup (property_name); g_strdelimit (canonical_name, "_", '-'); if (data->packing) pspec = gtk_container_class_find_child_property (class, canonical_name); else if (data->cell_packing) { GObjectClass *cell_class; /* We're just assuming that the cell layout is using a GtkCellAreaBox. */ cell_class = g_type_class_ref (GTK_TYPE_CELL_AREA_BOX); pspec = gtk_cell_area_class_find_cell_property (GTK_CELL_AREA_CLASS (cell_class), canonical_name); g_type_class_unref (cell_class); } else pspec = g_object_class_find_property (class, canonical_name); g_free (canonical_name); g_type_class_unref (class); return pspec; } static gboolean value_is_default (MyParserData *data, const gchar *class_name, const gchar *property_name, const gchar *value_string) { GValue value = { 0, }; gboolean ret; GError *error = NULL; GParamSpec *pspec; pspec = get_property_pspec (data, class_name, property_name); if (pspec == NULL) { if (data->packing) g_printerr (_("Packing property %s::%s not found\n"), class_name, property_name); else if (data->cell_packing) g_printerr (_("Cell property %s::%s not found\n"), class_name, property_name); else g_printerr (_("Property %s::%s not found\n"), class_name, property_name); return FALSE; } else if (g_type_is_a (G_PARAM_SPEC_VALUE_TYPE (pspec), G_TYPE_OBJECT)) return FALSE; if (!gtk_builder_value_from_string (data->builder, pspec, value_string, &value, &error)) { g_printerr (_("Couldn't parse value for %s::%s: %s\n"), class_name, property_name, error->message); g_error_free (error); ret = FALSE; } else ret = g_param_value_defaults (pspec, &value); g_value_reset (&value); return ret; } static gboolean property_is_boolean (MyParserData *data, const gchar *class_name, const gchar *property_name) { GParamSpec *pspec; pspec = get_property_pspec (data, class_name, property_name); if (pspec) return G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN; return FALSE; } static const gchar * canonical_boolean_value (MyParserData *data, const gchar *string) { GValue value = G_VALUE_INIT; gboolean b = FALSE; if (gtk_builder_value_from_string_type (data->builder, G_TYPE_BOOLEAN, string, &value, NULL)) b = g_value_get_boolean (&value); return b ? "1" : "0"; } /* A number of properties unfortunately can't be omitted even * if they are nominally set to their default value. In many * cases, this is due to subclasses not overriding the default * value from the superclass. */ static gboolean needs_explicit_setting (MyParserData *data, const gchar *class_name, const gchar *property_name) { struct _Prop { const char *class; const char *property; gboolean packing; } props[] = { { "GtkAboutDialog", "program-name", 0 }, { "GtkCalendar", "year", 0 }, { "GtkCalendar", "month", 0 }, { "GtkCalendar", "day", 0 }, { "GtkPlacesSidebar", "show-desktop", 0 }, { "GtkRadioButton", "draw-indicator", 0 }, { "GtkGrid", "left-attach", 1 }, { "GtkGrid", "top-attach", 1 }, { "GtkWidget", "hexpand", 0 }, { "GtkWidget", "vexpand", 0 }, { "GtkContainer", "border-width", 0 }, { "GtkVBox", "expand", 1 }, { "GtkHBox", "expand", 1 }, { NULL, NULL, 0 } }; gchar *canonical_name; gboolean found; gint k; canonical_name = g_strdup (property_name); g_strdelimit (canonical_name, "_", '-'); found = FALSE; for (k = 0; props[k].class; k++) { if (strcmp (class_name, props[k].class) == 0 && strcmp (canonical_name, props[k].property) == 0 && data->packing == props[k].packing) { found = TRUE; break; } } g_free (canonical_name); return found; } static void maybe_start_packing (MyParserData *data) { if (data->packing) { if (!data->packing_started) { g_print ("%*s\n", data->indent, ""); data->indent += 2; data->packing_started = TRUE; } } } static void maybe_start_cell_packing (MyParserData *data) { if (data->cell_packing) { if (!data->cell_packing_started) { g_print ("%*s\n", data->indent, ""); data->indent += 2; data->cell_packing_started = TRUE; } } } static void maybe_start_child (MyParserData *data) { if (data->in_child > 0) { if (data->child_started < data->in_child) { g_print ("%*s\n", data->indent, ""); data->indent += 2; data->child_started += 1; } } } static void maybe_emit_property (MyParserData *data) { gint i; gboolean bound; gboolean translatable; gchar *escaped; const gchar *class_name; const gchar *property_name; const gchar *value_string; class_name = (const gchar *)data->classes->data; property_name = ""; value_string = (const gchar *)data->value->str; bound = FALSE; translatable = FALSE; for (i = 0; data->attribute_names[i]; i++) { if (strcmp (data->attribute_names[i], "bind-source") == 0 || strcmp (data->attribute_names[i], "bind_source") == 0) bound = TRUE; else if (strcmp (data->attribute_names[i], "translatable") == 0) translatable = TRUE; else if (strcmp (data->attribute_names[i], "name") == 0) property_name = (const gchar *)data->attribute_values[i]; } if (!translatable && !bound && !needs_explicit_setting (data, class_name, property_name)) { for (i = 0; data->attribute_names[i]; i++) { if (strcmp (data->attribute_names[i], "name") == 0) { if (data->classes == NULL) break; if (value_is_default (data, class_name, property_name, value_string)) return; } } } maybe_start_packing (data); maybe_start_cell_packing (data); g_print ("%*sindent, ""); for (i = 0; data->attribute_names[i]; i++) { if (!translatable && (strcmp (data->attribute_names[i], "comments") == 0 || strcmp (data->attribute_names[i], "context") == 0)) continue; escaped = g_markup_escape_text (data->attribute_values[i], -1); g_print (" %s=\"%s\"", data->attribute_names[i], escaped); g_free (escaped); } if (bound) { g_print ("/>\n"); } else { g_print (">"); if (property_is_boolean (data, class_name, property_name)) { g_print ("%s", canonical_boolean_value (data, value_string)); } else { escaped = g_markup_escape_text (value_string, -1); g_print ("%s", escaped); g_free (escaped); } g_print ("\n"); } } static void maybe_close_starttag (MyParserData *data) { if (data->unclosed_starttag) { g_print (">\n"); data->unclosed_starttag = FALSE; } } static gboolean stack_is (GMarkupParseContext *context, ...) { va_list args; gchar *s, *p; const GSList *stack; stack = g_markup_parse_context_get_element_stack (context); va_start (args, context); s = va_arg (args, gchar *); while (s) { if (stack == NULL) { va_end (args); return FALSE; } p = (gchar *)stack->data; if (strcmp (s, p) != 0) { va_end (args); return FALSE; } s = va_arg (args, gchar *); stack = stack->next; } va_end (args); return TRUE; } static void start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { gint i; gchar *escaped; MyParserData *data = user_data; maybe_close_starttag (data); if (strcmp (element_name, "property") == 0) { g_assert (data->attribute_names == NULL); g_assert (data->attribute_values == NULL); g_assert (data->value == NULL); data->attribute_names = g_strdupv ((gchar **)attribute_names); data->attribute_values = g_strdupv ((gchar **)attribute_values); data->value = g_string_new (""); return; } else if (strcmp (element_name, "packing") == 0) { data->packing = TRUE; data->packing_started = FALSE; return; } else if (strcmp (element_name, "cell-packing") == 0) { data->cell_packing = TRUE; data->cell_packing_started = FALSE; return; } else if (strcmp (element_name, "child") == 0) { data->in_child += 1; if (attribute_names[0] == NULL) return; data->child_started += 1; } else if (strcmp (element_name, "attribute") == 0) { /* attribute in label has no content */ if (data->classes == NULL || strcmp ((gchar *)data->classes->data, "GtkLabel") != 0) data->value = g_string_new (""); } else if (stack_is (context, "item", "items", NULL) || stack_is (context, "action-widget", "action-widgets", NULL) || stack_is (context, "mime-type", "mime-types", NULL) || stack_is (context, "pattern", "patterns", NULL) || stack_is (context, "application", "applications", NULL) || stack_is (context, "col", "row", "data", NULL) || stack_is (context, "mark", "marks", NULL) || stack_is (context, "action", "accessibility", NULL)) { data->value = g_string_new (""); } else if (strcmp (element_name, "placeholder") == 0) { return; } else if (strcmp (element_name, "object") == 0 || strcmp (element_name, "template") == 0) { maybe_start_child (data); for (i = 0; attribute_names[i]; i++) { if (strcmp (attribute_names[i], "class") == 0) { data->classes = g_list_prepend (data->classes, g_strdup (attribute_values[i])); break; } } } g_print ("%*s<%s", data->indent, "", element_name); for (i = 0; attribute_names[i]; i++) { escaped = g_markup_escape_text (attribute_values[i], -1); g_print (" %s=\"%s\"", attribute_names[i], escaped); g_free (escaped); } data->unclosed_starttag = TRUE; data->indent += 2; } static void end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { MyParserData *data = user_data; if (strcmp (element_name, "property") == 0) { maybe_emit_property (data); g_clear_pointer (&data->attribute_names, g_strfreev); g_clear_pointer (&data->attribute_values, g_strfreev); g_string_free (data->value, TRUE); data->value = NULL; return; } else if (strcmp (element_name, "packing") == 0) { data->packing = FALSE; if (!data->packing_started) return; } else if (strcmp (element_name, "cell-packing") == 0) { data->cell_packing = FALSE; if (!data->cell_packing_started) return; } else if (strcmp (element_name, "child") == 0) { data->in_child -= 1; if (data->child_started == data->in_child) return; data->child_started -= 1; } else if (strcmp (element_name, "placeholder") == 0) { return; } else if (strcmp (element_name, "object") == 0 || strcmp (element_name, "template") == 0) { g_free (data->classes->data); data->classes = g_list_delete_link (data->classes, data->classes); } if (data->value != NULL) { gchar *escaped; if (data->unclosed_starttag) g_print (">"); escaped = g_markup_escape_text (data->value->str, -1); g_print ("%s\n", escaped, element_name); g_free (escaped); g_string_free (data->value, TRUE); data->value = NULL; } else { if (data->unclosed_starttag) g_print ("/>\n"); else g_print ("%*s\n", data->indent - 2, "", element_name); } data->indent -= 2; data->unclosed_starttag = FALSE; } static void text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { MyParserData *data = user_data; if (data->value) { g_string_append_len (data->value, text, text_len); return; } } static void passthrough (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { MyParserData *data = user_data; maybe_close_starttag (data); g_print ("%*s%s\n", data->indent, "", text); } GMarkupParser parser = { start_element, end_element, text, passthrough, NULL }; static void do_simplify (const gchar *filename) { GMarkupParseContext *context; GError *error = NULL; gchar *buffer; MyParserData data; if (!g_file_get_contents (filename, &buffer, NULL, &error)) { g_printerr (_("Can't load file: %s\n"), error->message); exit (1); } data.builder = gtk_builder_new (); data.classes = NULL; data.attribute_names = NULL; data.attribute_values = NULL; data.value = NULL; data.packing = FALSE; data.packing_started = FALSE; data.cell_packing = FALSE; data.cell_packing_started = FALSE; data.in_child = 0; data.child_started = 0; data.unclosed_starttag = FALSE; data.indent = 0; context = g_markup_parse_context_new (&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &data, NULL); if (!g_markup_parse_context_parse (context, buffer, -1, &error)) { g_printerr (_("Can't parse file: %s\n"), error->message); exit (1); } } static GType make_fake_type (const gchar *type_name, const gchar *parent_name) { GType parent_type; GTypeQuery query; parent_type = g_type_from_name (parent_name); if (parent_type == G_TYPE_INVALID) { g_printerr ("Failed to lookup template parent type %s\n", parent_name); exit (1); } g_type_query (parent_type, &query); return g_type_register_static_simple (parent_type, type_name, query.class_size, NULL, query.instance_size, NULL, 0); } static void do_validate_template (const gchar *filename, const gchar *type_name, const gchar *parent_name) { GType template_type; GtkWidget *widget; GtkBuilder *builder; GError *error = NULL; gint ret; /* Only make a fake type if it doesn't exist yet. * This lets us e.g. validate the GtkFileChooserWidget template. */ template_type = g_type_from_name (type_name); if (template_type == G_TYPE_INVALID) template_type = make_fake_type (type_name, parent_name); widget = g_object_new (template_type, NULL); if (!widget) { g_printerr ("Failed to create an instance of the template type %s\n", type_name); exit (1); } builder = gtk_builder_new (); ret = gtk_builder_extend_with_template (builder, widget, template_type, " ", 1, &error); if (ret) ret = gtk_builder_add_from_file (builder, filename, &error); g_object_unref (builder); if (ret == 0) { g_printerr ("%s\n", error->message); exit (1); } } static gboolean parse_template_error (const gchar *message, gchar **class_name, gchar **parent_name) { gchar *p; if (!strstr (message, "Not expecting to handle a template")) return FALSE; p = strstr (message, "(class '"); if (p) { *class_name = g_strdup (p + strlen ("(class '")); p = strstr (*class_name, "'"); if (p) *p = '\0'; } p = strstr (message, ", parent '"); if (p) { *parent_name = g_strdup (p + strlen (", parent '")); p = strstr (*parent_name, "'"); if (p) *p = '\0'; } return TRUE; } static void do_validate (const gchar *filename) { GtkBuilder *builder; GError *error = NULL; gint ret; gchar *class_name = NULL; gchar *parent_name = NULL; builder = gtk_builder_new (); ret = gtk_builder_add_from_file (builder, filename, &error); g_object_unref (builder); if (ret == 0) { if (g_error_matches (error, GTK_BUILDER_ERROR, GTK_BUILDER_ERROR_UNHANDLED_TAG) && parse_template_error (error->message, &class_name, &parent_name)) { do_validate_template (filename, class_name, parent_name); } else { g_printerr ("%s\n", error->message); exit (1); } } } static const gchar * object_get_name (GObject *object) { if (GTK_IS_BUILDABLE (object)) return gtk_buildable_get_name (GTK_BUILDABLE (object)); else return g_object_get_data (object, "gtk-builder-name"); } static void do_enumerate (const gchar *filename) { GtkBuilder *builder; GError *error = NULL; gint ret; GSList *list, *l; GObject *object; const gchar *name; builder = gtk_builder_new (); ret = gtk_builder_add_from_file (builder, filename, &error); if (ret == 0) { g_printerr ("%s\n", error->message); exit (1); } list = gtk_builder_get_objects (builder); for (l = list; l; l = l->next) { object = l->data; name = object_get_name (object); if (g_str_has_prefix (name, "___") && g_str_has_suffix (name, "___")) continue; g_print ("%s (%s)\n", name, g_type_name_from_instance ((GTypeInstance*)object)); } g_slist_free (list); g_object_unref (builder); } static void usage (void) { g_print (_("Usage:\n" " gtk-builder-tool [COMMAND] FILE\n" "\n" "Commands:\n" " validate Validate the file\n" " simplify Simplify the file\n" " enumerate List all named objects\n" "\n" "Perform various tasks on GtkBuilder .ui files.\n")); exit (1); } int main (int argc, char *argv[]) { g_set_prgname ("gtk-builder-tool"); gtk_init (NULL, NULL); gtk_test_register_all_types (); if (argc < 3) usage (); if (strcmp (argv[1], "validate") == 0) do_validate (argv[2]); else if (strcmp (argv[1], "simplify") == 0) do_simplify (argv[2]); else if (strcmp (argv[1], "enumerate") == 0) do_enumerate (argv[2]); else usage (); return 0; }