GtkBuilder: change format of menus

Change the format of GtkBuilder <menu> to be more in-line with the style
of the rest of GtkBuilder so that we can do translation in a consistent
way.

The format is now substantially more difficult to hand-write, but tools
should be along soon.

There is an xslt program attached to the bug to help you convert your
existing .ui files from the old format to the new one.

https://bugzilla.gnome.org/show_bug.cgi?id=668696
This commit is contained in:
Ryan Lortie 2012-01-25 18:23:25 -05:00
parent c76cccd437
commit eed307713b
7 changed files with 526 additions and 76 deletions

View File

@ -18,8 +18,7 @@
</object>
</child>
<child>
<object class="GtkSeparatorToolItem" id="sep">
</object>
<object class="GtkSeparatorToolItem" id="sep"/>
</child>
<child>
<object class="GtkToolButton" id="logo">
@ -97,6 +96,9 @@
</child>
</object>
<menu id="toolmenu">
<item label='File1' action='win.file1'/>
<item>
<attribute name="label">File1</attribute>
<attribute name="action">win.file1</attribute>
</item>
</menu>
</interface>

View File

@ -2,39 +2,106 @@
<interface>
<menu id="appmenu">
<section>
<item label="_New" action="app.new" accel="&lt;Primary&gt;n"/>
<item label="_Open" action="app.open"/>
<item label="_Save" action="app.save" accel="&lt;Primary&gt;s"/>
<item label="Save _As..." action="app.save-as" accel="&lt;Primary&gt;s"/>
<item>
<attribute name="label" translatable="yes">_New</attribute>
<attribute name="action">app.new</attribute>
<attribute name="accel">&lt;Primary&gt;n</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Open</attribute>
<attribute name="action">app.open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Save</attribute>
<attribute name="action">app.save</attribute>
<attribute name="accel">&lt;Primary&gt;s</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save _As...</attribute>
<attribute name="action">app.save-as</attribute>
<attribute name="accel">&lt;Primary&gt;s</attribute>
</item>
</section>
<section>
<item label="_Quit" action="app.quit" accel="&lt;Primary&gt;q"/>
<item>
<attribute name="label" translatable="yes">_Quit</attribute>
<attribute name="action">app.quit</attribute>
<attribute name="accel">&lt;Primary&gt;q</attribute>
</item>
</section>
</menu>
<menu id="menubar">
<submenu label="_Preferences">
<submenu>
<attribute name="label" translatable="yes">_Preferences</attribute>
<section>
<item label="_Prefer Dark Theme" action="app.dark"/>
<item label="_Hide Titlebar when maximized" action="win.titlebar"/>
<submenu label="_Color">
<item>
<attribute name="label" translatable="yes">_Prefer Dark Theme</attribute>
<attribute name="action">app.dark</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Hide Titlebar when maximized</attribute>
<attribute name="action">win.titlebar</attribute>
</item>
<submenu>
<attribute name="label" translatable="yes">_Color</attribute>
<section>
<item label="_Red" action="app.color" target="red" accel="&lt;Primary&gt;r"/>
<item label="_Green" action="app.color" target="green" accel="&lt;Primary&gt;g"/>
<item label="_Blue" action="app.color" target="blue" accel="&lt;Primary&gt;b"/>
<item>
<attribute name="label" translatable="yes">_Red</attribute>
<attribute name="action">app.color</attribute>
<attribute name="target">red</attribute>
<attribute name="accel">&lt;Primary&gt;r</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Green</attribute>
<attribute name="action">app.color</attribute>
<attribute name="target">green</attribute>
<attribute name="accel">&lt;Primary&gt;g</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Blue</attribute>
<attribute name="action">app.color</attribute>
<attribute name="target">blue</attribute>
<attribute name="accel">&lt;Primary&gt;b</attribute>
</item>
</section>
</submenu>
<submenu label="_Shape">
<submenu>
<attribute name="label" translatable="yes">_Shape</attribute>
<section>
<item label="_Square" action="win.shape" target="square" accel="&lt;Primary&gt;s"/>
<item label="_Rectangle" action="win.shape" target="rectangle" accel="&lt;Primary&gt;r"/>
<item label="_Oval" action="win.shape" target="oval" accel="&lt;Primary&gt;o"/>
<item>
<attribute name="label" translatable="yes">_Square</attribute>
<attribute name="action">win.shape</attribute>
<attribute name="target">square</attribute>
<attribute name="accel">&lt;Primary&gt;s</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Rectangle</attribute>
<attribute name="action">win.shape</attribute>
<attribute name="target">rectangle</attribute>
<attribute name="accel">&lt;Primary&gt;r</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Oval</attribute>
<attribute name="action">win.shape</attribute>
<attribute name="target">oval</attribute>
<attribute name="accel">&lt;Primary&gt;o</attribute>
</item>
</section>
</submenu>
<item label="_Bold" action="win.bold" accel="&lt;Primary&gt;b"/>
<item>
<attribute name="label" translatable="yes">_Bold</attribute>
<attribute name="action">win.bold</attribute>
<attribute name="accel">&lt;Primary&gt;b</attribute>
</item>
</section>
</submenu>
<submenu label="_Help">
<item label="_About" action="win.about" accel="&lt;Primary&gt;a"/>
<submenu>
<attribute name="label" translatable="yes">_Help</attribute>
<item>
<attribute name="label" translatable="yes">_About</attribute>
<attribute name="action">win.about</attribute>
<attribute name="accel">&lt;Primary&gt;a</attribute>
</item>
</submenu>
</menu>
</interface>

View File

@ -275,25 +275,50 @@ bloat_pad_startup (GApplication *application)
"<interface>"
" <menu id='app-menu'>"
" <section>"
" <item label='_New Window' action='app.new' accel='&lt;Primary&gt;n'/>"
" <item>"
" <attribute name='label'>_New Window</attribute>"
" <attribute name='action'>app.new</attribute>"
" <attribute name='accel'>&lt;Primary&gt;n</attribute>"
" </item>"
" </section>"
" <section>"
" <item label='_About Bloatpad' action='app.about'/>"
" <item>"
" <attribute name='label'>_About Bloatpad</attribute>"
" <attribute name='action'>app.about</attribute>"
" </item>"
" </section>"
" <section>"
" <item label='_Quit' action='app.quit' accel='&lt;Primary&gt;q'/>"
" <item>"
" <attribute name='label'>_Quit</attribute>"
" <attribute name='action'>app.quit</attribute>"
" <attribute name='accel'>&lt;Primary&gt;q</attribute>"
" </item>"
" </section>"
" </menu>"
" <menu id='menubar'>"
" <submenu label='_Edit'>"
" <submenu>"
" <attribute name='label'>_Edit</attribute>"
" <section>"
" <item label='_Copy' action='win.copy' accel='&lt;Primary&gt;c'/>"
" <item label='_Paste' action='win.paste' accel='&lt;Primary&gt;v'/>"
" <item>"
" <attribute name='label'>_Copy</attribute>"
" <attribute name='action'>win.copy</attribute>"
" <attribute name='accel'>&lt;Primary&gt;c</attribute>"
" </item>"
" <item>"
" <attribute name='label'>_Parse</attribute>"
" <attribute name='action'>win.parse</attribute>"
" <attribute name='accel'>&lt;Primary&gt;v</attribute>"
" </item>"
" </section>"
" </submenu>"
" <submenu label='_View'>"
" <submenu>"
" <attribute name='label'>_View</attribute>"
" <section>"
" <item label='_Fullscreen' action='win.fullscreen'/>"
" <item>"
" <attribute name='label'>_Fullscreen</attribute>"
" <attribute name='action'>win.fullscreen</attribute>"
" <attribute name='accel'>F11</attribute>"
" </item>"
" </section>"
" </submenu>"
" </menu>"

View File

@ -567,6 +567,7 @@ gtk_base_c_sources = \
gtkbuildable.c \
gtkbuilder.c \
gtkbuilderparser.c \
gtkbuilder-menus.c \
gtkbutton.c \
gtkcalendar.c \
gtkcellarea.c \

391
gtk/gtkbuilder-menus.c Normal file
View File

@ -0,0 +1,391 @@
/*
* Copyright © 2011, 2012 Canonical Ltd.
*
* This library 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
* licence, or (at your option) any later version.
*
* This library 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 this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
* USA.
*
* Author: Ryan Lortie <desrt@desrt.ca>
*/
#include "config.h"
#include "gtkbuilderprivate.h"
#include "gtkintl.h"
#include <gio/gio.h>
#include <string.h>
struct frame
{
GMenu *menu;
GMenuItem *item;
struct frame *prev;
};
typedef struct
{
ParserData *parser_data;
struct frame frame;
/* attributes */
gchar *attribute;
GVariantType *type;
GString *string;
/* translation */
gchar *context;
gboolean translatable;
} GtkBuilderMenuState;
static void
gtk_builder_menu_push_frame (GtkBuilderMenuState *state,
GMenu *menu,
GMenuItem *item)
{
struct frame *new;
new = g_slice_new (struct frame);
*new = state->frame;
state->frame.menu = menu;
state->frame.item = item;
state->frame.prev = new;
}
static void
gtk_builder_menu_pop_frame (GtkBuilderMenuState *state)
{
struct frame *prev = state->frame.prev;
if (state->frame.item)
{
g_assert (prev->menu != NULL);
g_menu_append_item (prev->menu, state->frame.item);
g_object_unref (state->frame.item);
}
state->frame = *prev;
g_slice_free (struct frame, prev);
}
static void
gtk_builder_menu_start_element (GMarkupParseContext *context,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
gpointer user_data,
GError **error)
{
GtkBuilderMenuState *state = user_data;
#define COLLECT(first, ...) \
g_markup_collect_attributes (element_name, \
attribute_names, attribute_values, error, \
first, __VA_ARGS__, G_MARKUP_COLLECT_INVALID)
#define OPTIONAL G_MARKUP_COLLECT_OPTIONAL
#define BOOLEAN G_MARKUP_COLLECT_BOOLEAN
#define STRING G_MARKUP_COLLECT_STRING
if (state->frame.menu)
{
/* Can have '<item>', '<submenu>' or '<section>' here. */
if (g_str_equal (element_name, "item"))
{
GMenuItem *item;
item = g_menu_item_new (NULL, NULL);
gtk_builder_menu_push_frame (state, NULL, item);
return;
}
else if (g_str_equal (element_name, "submenu"))
{
const gchar *id;
if (COLLECT (STRING | OPTIONAL, "id", &id))
{
GMenuItem *item;
GMenu *menu;
menu = g_menu_new ();
item = g_menu_item_new_submenu (NULL, G_MENU_MODEL (menu));
gtk_builder_menu_push_frame (state, menu, item);
if (id != NULL)
_gtk_builder_add_object (state->parser_data->builder, id, G_OBJECT (menu));
}
return;
}
else if (g_str_equal (element_name, "section"))
{
const gchar *id;
if (COLLECT (STRING | OPTIONAL, "id", &id))
{
GMenuItem *item;
GMenu *menu;
menu = g_menu_new ();
item = g_menu_item_new_section (NULL, G_MENU_MODEL (menu));
gtk_builder_menu_push_frame (state, menu, item);
if (id != NULL)
_gtk_builder_add_object (state->parser_data->builder, id, G_OBJECT (menu));
}
return;
}
}
if (state->frame.item)
{
/* Can have '<attribute>' or '<link>' here. */
if (g_str_equal (element_name, "attribute"))
{
const gchar *typestr;
const gchar *name;
const gchar *context;
if (COLLECT (STRING, "name", &name,
OPTIONAL | BOOLEAN, "translatable", &state->translatable,
OPTIONAL | STRING, "context", &context,
OPTIONAL | STRING, "comments", NULL, /* ignore, just for translators */
OPTIONAL | STRING, "type", &typestr))
{
if (typestr && !g_variant_type_string_is_valid (typestr))
{
g_set_error (error, G_VARIANT_PARSE_ERROR,
G_VARIANT_PARSE_ERROR_INVALID_TYPE_STRING,
"Invalid GVariant type string '%s'", typestr);
return;
}
state->type = typestr ? g_variant_type_new (typestr) : NULL;
state->string = g_string_new (NULL);
state->attribute = g_strdup (name);
state->context = g_strdup (context);
gtk_builder_menu_push_frame (state, NULL, NULL);
}
return;
}
if (g_str_equal (element_name, "link"))
{
const gchar *name;
const gchar *id;
if (COLLECT (STRING, "name", &name,
STRING | OPTIONAL, "id", &id))
{
GMenu *menu;
menu = g_menu_new ();
g_menu_item_set_link (state->frame.item, name, G_MENU_MODEL (menu));
gtk_builder_menu_push_frame (state, menu, NULL);
if (id != NULL)
_gtk_builder_add_object (state->parser_data->builder, id, G_OBJECT (menu));
}
return;
}
}
{
const GSList *element_stack;
element_stack = g_markup_parse_context_get_element_stack (context);
if (element_stack->next)
g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
_("Element <%s> not allowed inside <%s>"),
element_name, (const gchar *) element_stack->next->data);
else
g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
_("Element <%s> not allowed at toplevel"), element_name);
}
}
static void
gtk_builder_menu_end_element (GMarkupParseContext *context,
const gchar *element_name,
gpointer user_data,
GError **error)
{
GtkBuilderMenuState *state = user_data;
gtk_builder_menu_pop_frame (state);
if (state->string)
{
GVariant *value;
gchar *text;
text = g_string_free (state->string, FALSE);
state->string = NULL;
/* do the translation if necessary */
if (state->translatable && state->parser_data->domain)
{
const gchar *translated;
if (state->context)
translated = g_dpgettext2 (state->parser_data->domain, state->context, text);
else
translated = g_dgettext (state->parser_data->domain, text);
if (translated != text)
{
/* it's safe because we know that translated != text */
g_free (text);
text = g_strdup (translated);
}
}
if (state->type == NULL)
/* No type string specified -> it's a normal string. */
g_menu_item_set_attribute (state->frame.item, state->attribute, "s", text);
/* Else, we try to parse it according to the type string. If
* error is set here, it will follow us out, ending the parse.
*
* We still need to free everything, though, so ignore it here.
*/
else if ((value = g_variant_parse (state->type, text, NULL, NULL, error)))
{
g_menu_item_set_attribute_value (state->frame.item, state->attribute, value);
g_variant_unref (value);
}
if (state->type)
{
g_variant_type_free (state->type);
state->type = NULL;
}
g_free (state->context);
state->context = NULL;
g_free (state->attribute);
state->attribute = NULL;
g_free (text);
}
}
static void
gtk_builder_menu_text (GMarkupParseContext *context,
const gchar *text,
gsize text_len,
gpointer user_data,
GError **error)
{
GtkBuilderMenuState *state = user_data;
gint i;
for (i = 0; i < text_len; i++)
if (!g_ascii_isspace (text[i]))
{
if (state->string)
g_string_append_len (state->string, text, text_len);
else
g_set_error (error, G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT,
_("text may not appear inside <%s>"),
g_markup_parse_context_get_element (context));
break;
}
}
static void
gtk_builder_menu_error (GMarkupParseContext *context,
GError *error,
gpointer user_data)
{
GtkBuilderMenuState *state = user_data;
while (state->frame.prev)
{
struct frame *prev = state->frame.prev;
state->frame = *prev;
g_slice_free (struct frame, prev);
}
if (state->string)
g_string_free (state->string, TRUE);
if (state->type)
g_variant_type_free (state->type);
g_free (state->attribute);
g_free (state->context);
g_slice_free (GtkBuilderMenuState, state);
}
static GMarkupParser gtk_builder_menu_subparser =
{
gtk_builder_menu_start_element,
gtk_builder_menu_end_element,
gtk_builder_menu_text,
NULL, /* passthrough */
gtk_builder_menu_error
};
void
_gtk_builder_menu_start (ParserData *parser_data,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
GError **error)
{
GtkBuilderMenuState *state;
gchar *id;
state = g_slice_new0 (GtkBuilderMenuState);
state->parser_data = parser_data;
g_markup_parse_context_push (parser_data->ctx, &gtk_builder_menu_subparser, state);
if (COLLECT (STRING, "id", &id))
{
GMenu *menu;
menu = g_menu_new ();
_gtk_builder_add_object (state->parser_data->builder, id, G_OBJECT (menu));
gtk_builder_menu_push_frame (state, menu, NULL);
}
}
void
_gtk_builder_menu_end (ParserData *parser_data)
{
GtkBuilderMenuState *state;
state = g_markup_parse_context_pop (parser_data->ctx);
gtk_builder_menu_pop_frame (state);
g_assert (state->frame.prev == NULL);
g_assert (state->frame.item == NULL);
g_assert (state->frame.menu == NULL);
g_slice_free (GtkBuilderMenuState, state);
}

View File

@ -832,34 +832,6 @@ parse_custom (GMarkupParseContext *context,
return TRUE;
}
static gboolean
parse_menu (GMarkupParseContext *context,
const gchar *element_name,
const gchar **names,
const gchar **values,
gpointer user_data,
GError **error)
{
gchar *id;
ParserData *data = user_data;
MenuInfo *menu_info;
if (!g_markup_collect_attributes (element_name, names, values, error,
G_MARKUP_COLLECT_STRING, "id", &id,
G_MARKUP_COLLECT_INVALID))
return FALSE;
menu_info = g_slice_new0 (MenuInfo);
menu_info->tag.name = element_name;
menu_info->id = g_strdup (id);
menu_info->objects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
state_push (data, menu_info);
g_menu_markup_parser_start_menu (context, data->domain, menu_info->objects);
return TRUE;
}
static void
start_element (GMarkupParseContext *context,
const gchar *element_name,
@ -921,7 +893,7 @@ start_element (GMarkupParseContext *context,
else if (strcmp (element_name, "interface") == 0)
parse_interface (data, element_name, names, values, error);
else if (strcmp (element_name, "menu") == 0)
parse_menu (context, element_name, names, values, data, error);
_gtk_builder_menu_start (data, element_name, names, values, error);
else if (strcmp (element_name, "placeholder") == 0)
{
/* placeholder has no special treatmeant, but it needs an
@ -995,23 +967,7 @@ end_element (GMarkupParseContext *context,
}
else if (strcmp (element_name, "menu") == 0)
{
MenuInfo *menu_info;
GObject *menu;
GHashTableIter iter;
const gchar *id;
menu_info = state_pop_info (data, MenuInfo);
menu = (GObject*)g_menu_markup_parser_end_menu (context);
_gtk_builder_add_object (data->builder, menu_info->id, menu);
g_object_unref (menu);
g_hash_table_iter_init (&iter, menu_info->objects);
while (g_hash_table_iter_next (&iter, (gpointer*)&id, (gpointer*)&menu))
{
_gtk_builder_add_object (data->builder, id, menu);
}
free_menu_info (menu_info);
_gtk_builder_menu_end (data);
}
else if (data->requested_objects && !data->inside_requested_object)
{

View File

@ -154,4 +154,12 @@ gchar * _gtk_builder_get_resource_path (GtkBuilder *builder,
gchar * _gtk_builder_get_absolute_filename (GtkBuilder *builder,
const gchar *string);
void _gtk_builder_menu_start (ParserData *parser_data,
const gchar *element_name,
const gchar **attribute_names,
const gchar **attribute_values,
GError **error);
void _gtk_builder_menu_end (ParserData *parser_data);
#endif /* __GTK_BUILDER_PRIVATE_H__ */