/*
* Copyright © 2011 Red Hat, Inc.
* Copyright © 2011 Canonical Limited
*
* 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, see .
*
* Author: Matthias Clasen
* Ryan Lortie
*/
#include "config.h"
#include "gtkmenushell.h"
#include "gtkmenubar.h"
#include "gtkmenu.h"
#include "gtkseparatormenuitem.h"
#include "gtkmodelmenuitem.h"
#include "gtkapplicationprivate.h"
#define MODEL_MENU_WIDGET_DATA "gtk-model-menu-widget-data"
typedef struct {
GMenuModel *model;
GtkMenuShell *shell;
guint update_idle;
GSList *connected;
gboolean with_separators;
gint n_items;
gchar *action_namespace;
} GtkModelMenuBinding;
static void
gtk_model_menu_binding_items_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer user_data);
static void gtk_model_menu_binding_append_model (GtkModelMenuBinding *binding,
GMenuModel *model,
const gchar *action_namespace,
gboolean with_separators);
static void
gtk_model_menu_binding_free (gpointer data)
{
GtkModelMenuBinding *binding = data;
/* disconnect all existing signal handlers */
while (binding->connected)
{
g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
g_object_unref (binding->connected->data);
binding->connected = g_slist_delete_link (binding->connected, binding->connected);
}
g_object_unref (binding->model);
g_free (binding->action_namespace);
g_slice_free (GtkModelMenuBinding, binding);
}
static void
gtk_model_menu_binding_append_item (GtkModelMenuBinding *binding,
GMenuModel *model,
const gchar *action_namespace,
gint item_index,
gchar **heading)
{
GMenuModel *section;
if ((section = g_menu_model_get_item_link (model, item_index, "section")))
{
gchar *section_namespace = NULL;
g_menu_model_get_item_attribute (model, item_index, "label", "s", heading);
g_menu_model_get_item_attribute (model, item_index, "action-namespace", "s", §ion_namespace);
if (action_namespace)
{
gchar *namespace = g_strjoin (".", action_namespace, section_namespace, NULL);
gtk_model_menu_binding_append_model (binding, section, namespace, FALSE);
g_free (namespace);
}
else
{
gtk_model_menu_binding_append_model (binding, section, section_namespace, FALSE);
}
g_free (section_namespace);
g_object_unref (section);
}
else
{
GtkMenuItem *item;
item = gtk_model_menu_item_new (model, item_index, action_namespace);
gtk_menu_shell_append (binding->shell, GTK_WIDGET (item));
gtk_widget_show (GTK_WIDGET (item));
binding->n_items++;
}
}
static void
gtk_model_menu_binding_append_model (GtkModelMenuBinding *binding,
GMenuModel *model,
const gchar *action_namespace,
gboolean with_separators)
{
gint n, i;
g_signal_connect (model, "items-changed", G_CALLBACK (gtk_model_menu_binding_items_changed), binding);
binding->connected = g_slist_prepend (binding->connected, g_object_ref (model));
/* Deciding if we should show a separator is a bit difficult. There
* are two types of separators:
*
* - section headings (when sections have 'label' property)
*
* - normal separators automatically put between sections
*
* The easiest way to think about it is that a section usually has a
* separator (or heading) immediately before it.
*
* There are three exceptions to this general rule:
*
* - empty sections don't get separators or headings
*
* - sections only get separators and headings at the toplevel of a
* menu (ie: no separators on nested sections or in menubars)
*
* - the first section in the menu doesn't get a normal separator,
* but it can get a header (if it's not empty)
*
* Unfortunately, we cannot simply check the size of the section in
* order to determine if we should place a header: the section may
* contain other sections that are themselves empty. Instead, we need
* to append the section, and check if we ended up with any actual
* content. If we did, then we need to insert before that content.
* We use 'our_position' to keep track of this.
*/
n = g_menu_model_get_n_items (model);
for (i = 0; i < n; i++)
{
gint our_position = binding->n_items;
gchar *heading = NULL;
gtk_model_menu_binding_append_item (binding, model, action_namespace, i, &heading);
if (with_separators && our_position < binding->n_items)
{
GtkWidget *separator = NULL;
if (heading)
{
separator = gtk_menu_item_new_with_label (heading);
gtk_widget_set_sensitive (separator, FALSE);
}
else if (our_position > 0)
separator = gtk_separator_menu_item_new ();
if (separator)
{
gtk_menu_shell_insert (binding->shell, separator, our_position);
gtk_widget_show (separator);
binding->n_items++;
}
}
g_free (heading);
}
}
static void
gtk_model_menu_binding_populate (GtkModelMenuBinding *binding)
{
GList *children;
/* remove current children */
children = gtk_container_get_children (GTK_CONTAINER (binding->shell));
while (children)
{
gtk_container_remove (GTK_CONTAINER (binding->shell), children->data);
children = g_list_delete_link (children, children);
}
binding->n_items = 0;
/* add new items from the model */
gtk_model_menu_binding_append_model (binding, binding->model, binding->action_namespace, binding->with_separators);
}
static gboolean
gtk_model_menu_binding_handle_changes (gpointer user_data)
{
GtkModelMenuBinding *binding = user_data;
/* disconnect all existing signal handlers */
while (binding->connected)
{
g_signal_handlers_disconnect_by_func (binding->connected->data, gtk_model_menu_binding_items_changed, binding);
g_object_unref (binding->connected->data);
binding->connected = g_slist_delete_link (binding->connected, binding->connected);
}
gtk_model_menu_binding_populate (binding);
binding->update_idle = 0;
g_object_unref (binding->shell);
return G_SOURCE_REMOVE;
}
static void
gtk_model_menu_binding_items_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer user_data)
{
GtkModelMenuBinding *binding = user_data;
if (binding->update_idle == 0)
{
binding->update_idle = gdk_threads_add_idle (gtk_model_menu_binding_handle_changes, user_data);
g_object_ref (binding->shell);
}
}
/**
* gtk_menu_shell_bind_model:
* @menu_shell: a #GtkMenuShell
* @model: (allow-none): the #GMenuModel to bind to or %NULL to remove
* binding
* @action_namespace: (allow-none): the namespace for actions in @model
* @with_separators: %TRUE if toplevel items in @shell should have
* separators between them
*
* Establishes a binding between a #GtkMenuShell and a #GMenuModel.
*
* The contents of @shell are removed and then refilled with menu items
* according to @model. When @model changes, @shell is updated.
* Calling this function twice on @shell with different @model will
* cause the first binding to be replaced with a binding to the new
* model. If @model is %NULL then any previous binding is undone and
* all children are removed.
*
* @with_separators determines if toplevel items (eg: sections) have
* separators inserted between them. This is typically desired for
* menus but doesn't make sense for menubars.
*
* If @action_namespace is non-%NULL then the effect is as if all
* actions mentioned in the @model have their names prefixed with the
* namespace, plus a dot. For example, if the action "quit" is
* mentioned and @action_namespace is "app" then the effective action
* name is "app.quit".
*
* For most cases you are probably better off using
* gtk_menu_new_from_model() or gtk_menu_bar_new_from_model() or just
* directly passing the #GMenuModel to gtk_application_set_app_menu() or
* gtk_application_set_menu_bar().
*
* Since: 3.6
*/
void
gtk_menu_shell_bind_model (GtkMenuShell *shell,
GMenuModel *model,
const gchar *action_namespace,
gboolean with_separators)
{
g_return_if_fail (GTK_IS_MENU_SHELL (shell));
g_return_if_fail (model == NULL || G_IS_MENU_MODEL (model));
if (model)
{
GtkModelMenuBinding *binding;
binding = g_slice_new (GtkModelMenuBinding);
binding->model = g_object_ref (model);
binding->shell = shell;
binding->update_idle = 0;
binding->connected = NULL;
binding->with_separators = with_separators;
binding->action_namespace = g_strdup (action_namespace);
g_object_set_data_full (G_OBJECT (shell), "gtk-model-menu-binding", binding, gtk_model_menu_binding_free);
gtk_model_menu_binding_populate (binding);
}
else
{
GList *children;
/* break existing binding */
g_object_set_data (G_OBJECT (shell), "gtk-model-menu-binding", NULL);
/* remove all children */
children = gtk_container_get_children (GTK_CONTAINER (shell));
while (children)
{
gtk_container_remove (GTK_CONTAINER (shell), children->data);
children = g_list_delete_link (children, children);
}
}
}