597 lines
22 KiB
C
597 lines
22 KiB
C
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Author: Ryan Lortie <desrt@desrt.ca>
|
|
*/
|
|
|
|
#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 section_could_have_separator;
|
|
|
|
section_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,
|
|
section_could_have_separator,
|
|
section_could_have_separator ? section->model : NULL,
|
|
section_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 *menuitem;
|
|
|
|
menuitem = _gtk_menu_tracker_item_new (tracker->observable, parent_model, parent_index, NULL, TRUE);
|
|
(* tracker->insert_func) (menuitem, offset, tracker->user_data);
|
|
g_object_unref (menuitem);
|
|
|
|
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);
|
|
}
|