/*
* Copyright © 2013 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: Ryan Lortie
*/
#include "config.h"
#include "gtkmenutracker.h"
/**
* SECTION:gtkmenutracker
* @Title: GtkMenuTracker
* @Short_description: A helper class for interpreting #GMenuModel
*
* #GtkMenuTracker is a simple object to ease implementations of #GMenuModel.
* Given a #GtkActionObservable (usually a #GActionMuxer) along with a
* #GMenuModel, it will tell you which menu items to create and where to place
* them. If a menu item is removed, it will tell you the position of the menu
* item to remove.
*
* Using #GtkMenuTracker is fairly simple. The only guarantee you must make
* to #GtkMenuTracker is that you must obey all insert signals and track the
* position of items that #GtkMenuTracker gives you. That is, #GtkMenuTracker
* expects positions of all the latter items to change when it calls your
* insertion callback with an early position, as it may ask you to remove
* an item with a readjusted position later.
*
* #GtkMenuTracker will give you a #GtkMenuTrackerItem in your callback. You
* must hold onto this object until a remove signal is emitted. This item
* represents a single menu item, which can be one of three classes: normal item,
* separator, or submenu.
*
* Certain properties on the #GtkMenuTrackerItem are mutable, and you must
* listen for changes in the item. For more details, see the documentation
* for #GtkMenuTrackerItem along with https://live.gnome.org/GApplication/GMenuModel.
*
* The idea of @with_separators is for special cases where menu models may
* be tracked in places where separators are not available, like in toplevel
* "File", "Edit" menu bars. Ignoring separator items is wrong, as #GtkMenuTracker
* expects the position to change, so we must tell #GtkMenuTracker to ignore
* separators itself.
*/
typedef struct _GtkMenuTrackerSection GtkMenuTrackerSection;
struct _GtkMenuTracker
{
GtkActionObservable *observable;
GtkMenuTrackerInsertFunc insert_func;
GtkMenuTrackerRemoveFunc remove_func;
gpointer user_data;
GtkMenuTrackerSection *toplevel;
};
struct _GtkMenuTrackerSection
{
gpointer model; /* may be a GtkMenuTrackerItem or a GMenuModel */
GSList *items;
gchar *action_namespace;
guint separator_label : 1;
guint with_separators : 1;
guint has_separator : 1;
guint is_fake : 1;
gulong handler;
};
static GtkMenuTrackerSection * gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
GMenuModel *model,
gboolean with_separators,
gboolean separator_label,
gint offset,
const gchar *action_namespace);
static void gtk_menu_tracker_section_free (GtkMenuTrackerSection *section);
static GtkMenuTrackerSection *
gtk_menu_tracker_section_find_model (GtkMenuTrackerSection *section,
gpointer model,
gint *offset)
{
GSList *item;
if (section->has_separator)
(*offset)++;
if (section->model == model)
return section;
for (item = section->items; item; item = item->next)
{
GtkMenuTrackerSection *subsection = item->data;
if (subsection)
{
GtkMenuTrackerSection *found_section;
found_section = gtk_menu_tracker_section_find_model (subsection, model, offset);
if (found_section)
return found_section;
}
else
(*offset)++;
}
return FALSE;
}
/* this is responsible for syncing the showing of a separator for a
* single subsection (and its children).
*
* we only ever show separators if we have _actual_ children (ie: we do
* not show a separator if the section contains only empty child
* sections). it's difficult to determine this on-the-fly, so we have
* this separate function to come back later and figure it out.
*
* 'section' is that section.
*
* 'tracker' is passed in so that we can emit callbacks when we decide
* to add/remove separators.
*
* 'offset' is passed in so we know which position to emit in our
* callbacks. ie: if we add a separator right at the top of this
* section then we would emit it with this offset. deeper inside, we
* adjust accordingly.
*
* could_have_separator is true in two situations:
*
* - our parent section had with_separators defined and there are items
* before us (ie: we should add a separator if we have content in
* order to divide us from the items above)
*
* - if we had a 'label' attribute set for this section
*
* parent_model and parent_index are passed in so that we can give them
* to the insertion callback so that it can see the label (and anything
* else that happens to be defined on the section).
*
* we iterate each item in ourselves. for subsections, we recursively
* run ourselves to sync separators. after we are done, we notice if we
* have any items in us or if we are completely empty and sync if our
* separator is shown or not.
*/
static gint
gtk_menu_tracker_section_sync_separators (GtkMenuTrackerSection *section,
GtkMenuTracker *tracker,
gint offset,
gboolean could_have_separator,
GMenuModel *parent_model,
gint parent_index)
{
gboolean should_have_separator;
gint n_items = 0;
GSList *item;
gint i = 0;
for (item = section->items; item; item = item->next)
{
GtkMenuTrackerSection *subsection = item->data;
if (subsection)
{
gboolean could_have_separator;
could_have_separator = (section->with_separators && n_items > 0) || subsection->separator_label;
/* Only pass the parent_model and parent_index in case they may be used to create the separator. */
n_items += gtk_menu_tracker_section_sync_separators (subsection, tracker, offset + n_items,
could_have_separator,
could_have_separator ? section->model : NULL,
could_have_separator ? i : 0);
}
else
n_items++;
i++;
}
should_have_separator = !section->is_fake && could_have_separator && n_items != 0;
if (should_have_separator > section->has_separator)
{
/* Add a separator */
GtkMenuTrackerItem *item;
item = _gtk_menu_tracker_item_new (tracker->observable, parent_model, parent_index, NULL, TRUE);
(* tracker->insert_func) (item, offset, tracker->user_data);
g_object_unref (item);
section->has_separator = TRUE;
}
else if (should_have_separator < section->has_separator)
{
/* Remove a separator */
(* tracker->remove_func) (offset, tracker->user_data);
section->has_separator = FALSE;
}
n_items += section->has_separator;
return n_items;
}
static void
gtk_menu_tracker_item_visibility_changed (GtkMenuTrackerItem *item,
gboolean is_now_visible,
gpointer user_data)
{
GtkMenuTracker *tracker = user_data;
GtkMenuTrackerSection *section;
gboolean was_visible;
gint offset = 0;
/* remember: the item is our model */
section = gtk_menu_tracker_section_find_model (tracker->toplevel, item, &offset);
was_visible = section->items != NULL;
if (is_now_visible == was_visible)
return;
if (is_now_visible)
{
section->items = g_slist_prepend (NULL, NULL);
(* tracker->insert_func) (section->model, offset, tracker->user_data);
}
else
{
section->items = g_slist_delete_link (section->items, section->items);
(* tracker->remove_func) (offset, tracker->user_data);
}
gtk_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
}
static gint
gtk_menu_tracker_section_measure (GtkMenuTrackerSection *section)
{
GSList *item;
gint n_items;
if (section == NULL)
return 1;
n_items = 0;
if (section->has_separator)
n_items++;
for (item = section->items; item; item = item->next)
n_items += gtk_menu_tracker_section_measure (item->data);
return n_items;
}
static void
gtk_menu_tracker_remove_items (GtkMenuTracker *tracker,
GSList **change_point,
gint offset,
gint n_items)
{
gint i;
for (i = 0; i < n_items; i++)
{
GtkMenuTrackerSection *subsection;
gint n;
subsection = (*change_point)->data;
*change_point = g_slist_delete_link (*change_point, *change_point);
n = gtk_menu_tracker_section_measure (subsection);
gtk_menu_tracker_section_free (subsection);
while (n--)
(* tracker->remove_func) (offset, tracker->user_data);
}
}
static void
gtk_menu_tracker_add_items (GtkMenuTracker *tracker,
GtkMenuTrackerSection *section,
GSList **change_point,
gint offset,
GMenuModel *model,
gint position,
gint n_items)
{
while (n_items--)
{
GMenuModel *submenu;
submenu = g_menu_model_get_item_link (model, position + n_items, G_MENU_LINK_SECTION);
g_assert (submenu != model);
if (submenu != NULL)
{
GtkMenuTrackerSection *subsection;
gchar *action_namespace = NULL;
gboolean has_label;
has_label = g_menu_model_get_item_attribute (model, position + n_items,
G_MENU_ATTRIBUTE_LABEL, "s", NULL);
g_menu_model_get_item_attribute (model, position + n_items,
G_MENU_ATTRIBUTE_ACTION_NAMESPACE, "s", &action_namespace);
if (section->action_namespace)
{
gchar *namespace;
namespace = g_strjoin (".", section->action_namespace, action_namespace, NULL);
subsection = gtk_menu_tracker_section_new (tracker, submenu, FALSE, has_label, offset, namespace);
g_free (namespace);
}
else
subsection = gtk_menu_tracker_section_new (tracker, submenu, FALSE, has_label, offset, action_namespace);
*change_point = g_slist_prepend (*change_point, subsection);
g_free (action_namespace);
g_object_unref (submenu);
}
else
{
GtkMenuTrackerItem *item;
item = _gtk_menu_tracker_item_new (tracker->observable, model, position + n_items,
section->action_namespace, FALSE);
/* In the case that the item may disappear we handle that by
* treating the item that we just created as being its own
* subsection. This happens as so:
*
* - the subsection is created without the possibility of
* showing a separator
*
* - the subsection will have either 0 or 1 item in it at all
* times: either the shown item or not (in the case it is
* hidden)
*
* - the created item acts as the "model" for this section
* and we use its "visiblity-changed" signal in the same
* way that we use the "items-changed" signal from a real
* GMenuModel
*
* We almost never use the '->model' stored in the section for
* anything other than lookups and for dropped the ref and
* disconnecting the signal when we destroy the menu, and we
* need to do exactly those things in this case as well.
*
* The only other thing that '->model' is used for is in the
* case that we want to show a separator, but we will never do
* that because separators are not shown for this fake section.
*/
if (_gtk_menu_tracker_item_may_disappear (item))
{
GtkMenuTrackerSection *fake_section;
fake_section = g_slice_new0 (GtkMenuTrackerSection);
fake_section->is_fake = TRUE;
fake_section->model = g_object_ref (item);
fake_section->handler = g_signal_connect (item, "visibility-changed",
G_CALLBACK (gtk_menu_tracker_item_visibility_changed),
tracker);
*change_point = g_slist_prepend (*change_point, fake_section);
if (_gtk_menu_tracker_item_is_visible (item))
{
(* tracker->insert_func) (item, offset, tracker->user_data);
fake_section->items = g_slist_prepend (NULL, NULL);
}
}
else
{
/* In the normal case, we store NULL in the linked list.
* The measurement and lookup code count NULL always as
* exactly 1: an item that will always be there.
*/
(* tracker->insert_func) (item, offset, tracker->user_data);
*change_point = g_slist_prepend (*change_point, NULL);
}
g_object_unref (item);
}
}
}
static void
gtk_menu_tracker_model_changed (GMenuModel *model,
gint position,
gint removed,
gint added,
gpointer user_data)
{
GtkMenuTracker *tracker = user_data;
GtkMenuTrackerSection *section;
GSList **change_point;
gint offset = 0;
gint i;
/* First find which section the changed model corresponds to, and the
* position of that section within the overall menu.
*/
section = gtk_menu_tracker_section_find_model (tracker->toplevel, model, &offset);
/* Next, seek through that section to the change point. This gives us
* the correct GSList** to make the change to and also finds the final
* offset at which we will make the changes (by measuring the number
* of items within each item of the section before the change point).
*/
change_point = §ion->items;
for (i = 0; i < position; i++)
{
offset += gtk_menu_tracker_section_measure ((*change_point)->data);
change_point = &(*change_point)->next;
}
/* We remove items in order and add items in reverse order. This
* means that the offset used for all inserts and removes caused by a
* single change will be the same.
*
* This also has a performance advantage: GtkMenuShell stores the
* menu items in a linked list. In the case where we are creating a
* menu for the first time, adding the items in reverse order means
* that we only ever insert at index zero, prepending the list. This
* means that we can populate in O(n) time instead of O(n^2) that we
* would do by appending.
*/
gtk_menu_tracker_remove_items (tracker, change_point, offset, removed);
gtk_menu_tracker_add_items (tracker, section, change_point, offset, model, position, added);
/* The offsets for insertion/removal of separators will be all over
* the place, however...
*/
gtk_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
}
static void
gtk_menu_tracker_section_free (GtkMenuTrackerSection *section)
{
if (section == NULL)
return;
g_signal_handler_disconnect (section->model, section->handler);
g_slist_free_full (section->items, (GDestroyNotify) gtk_menu_tracker_section_free);
g_free (section->action_namespace);
g_object_unref (section->model);
g_slice_free (GtkMenuTrackerSection, section);
}
static GtkMenuTrackerSection *
gtk_menu_tracker_section_new (GtkMenuTracker *tracker,
GMenuModel *model,
gboolean with_separators,
gboolean separator_label,
gint offset,
const gchar *action_namespace)
{
GtkMenuTrackerSection *section;
section = g_slice_new0 (GtkMenuTrackerSection);
section->model = g_object_ref (model);
section->with_separators = with_separators;
section->action_namespace = g_strdup (action_namespace);
section->separator_label = separator_label;
gtk_menu_tracker_add_items (tracker, section, §ion->items, offset, model, 0, g_menu_model_get_n_items (model));
section->handler = g_signal_connect (model, "items-changed", G_CALLBACK (gtk_menu_tracker_model_changed), tracker);
return section;
}
/*< private >
* gtk_menu_tracker_new:
* @model: the model to flatten
* @with_separators: if the toplevel should have separators (ie: TRUE
* for menus, FALSE for menubars)
* @action_namespace: the passed-in action namespace
* @insert_func: insert callback
* @remove_func: remove callback
* @user_data user data for callbacks
*
* Creates a GtkMenuTracker for @model, holding a ref on @model for as
* long as the tracker is alive.
*
* This flattens out the model, merging sections and inserting
* separators where appropriate. It monitors for changes and performs
* updates on the fly. It also handles action_namespace for subsections
* (but you will need to handle it yourself for submenus).
*
* When the tracker is first created, @insert_func will be called many
* times to populate the menu with the initial contents of @model
* (unless it is empty), before gtk_menu_tracker_new() returns. For
* this reason, the menu that is using the tracker ought to be empty
* when it creates the tracker.
*
* Future changes to @model will result in more calls to @insert_func
* and @remove_func.
*
* The position argument to both functions is the linear 0-based
* position in the menu at which the item in question should be inserted
* or removed.
*
* For @insert_func, @model and @item_index are used to get the
* information about the menu item to insert. @action_namespace is the
* action namespace that actions referred to from that item should place
* themselves in. Note that if the item is a submenu and the
* "action-namespace" attribute is defined on the item, it will _not_ be
* applied to the @action_namespace argument as it is meant for the
* items inside of the submenu, not the submenu item itself.
*
* @is_separator is set to %TRUE in case the item being added is a
* separator. @model and @item_index will still be meaningfully set in
* this case -- to the section menu item corresponding to the separator.
* This is useful if the section specifies a label, for example. If
* there is an "action-namespace" attribute on this menu item then it
* should be ignored by the consumer because #GtkMenuTracker has already
* handled it.
*
* When using #GtkMenuTracker there is no need to hold onto @model or
* monitor it for changes. The model will be unreffed when
* gtk_menu_tracker_free() is called.
*/
GtkMenuTracker *
gtk_menu_tracker_new (GtkActionObservable *observable,
GMenuModel *model,
gboolean with_separators,
const gchar *action_namespace,
GtkMenuTrackerInsertFunc insert_func,
GtkMenuTrackerRemoveFunc remove_func,
gpointer user_data)
{
GtkMenuTracker *tracker;
tracker = g_slice_new (GtkMenuTracker);
tracker->observable = g_object_ref (observable);
tracker->insert_func = insert_func;
tracker->remove_func = remove_func;
tracker->user_data = user_data;
tracker->toplevel = gtk_menu_tracker_section_new (tracker, model, with_separators, FALSE, 0, action_namespace);
gtk_menu_tracker_section_sync_separators (tracker->toplevel, tracker, 0, FALSE, NULL, 0);
return tracker;
}
GtkMenuTracker *
gtk_menu_tracker_new_for_item_submenu (GtkMenuTrackerItem *item,
GtkMenuTrackerInsertFunc insert_func,
GtkMenuTrackerRemoveFunc remove_func,
gpointer user_data)
{
GtkMenuTracker *tracker;
GMenuModel *submenu;
gchar *namespace;
submenu = _gtk_menu_tracker_item_get_submenu (item);
namespace = _gtk_menu_tracker_item_get_submenu_namespace (item);
tracker = gtk_menu_tracker_new (_gtk_menu_tracker_item_get_observable (item), submenu,
TRUE, namespace, insert_func, remove_func, user_data);
g_object_unref (submenu);
g_free (namespace);
return tracker;
}
/*< private >
* gtk_menu_tracker_free:
* @tracker: a #GtkMenuTracker
*
* Frees the tracker, ...
*/
void
gtk_menu_tracker_free (GtkMenuTracker *tracker)
{
gtk_menu_tracker_section_free (tracker->toplevel);
g_object_unref (tracker->observable);
g_slice_free (GtkMenuTracker, tracker);
}