PIKApp/app/widgets/pikamenumodel.c

1313 lines
46 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* pikamenu_model.c
* Copyright (C) 2023 Jehan
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl.h>
#include <gtk/gtk.h>
#include "widgets-types.h"
#include "libpikabase/pikabase.h"
#include "libpikacolor/pikacolor.h"
#include "libpikawidgets/pikawidgets.h"
#include "core/pika.h"
#include "pikaaction.h"
#include "pikaactiongroup.h"
#include "pikamenumodel.h"
#include "pikamenushell.h"
#include "pikaradioaction.h"
#include "pikauimanager.h"
#include "pikawidgets-utils.h"
/**
* PikaMenuModel:
*
* PikaMenuModel implements GMenuModel. We initialize an object from another
* GMenuModel (usually a GMenu), auto-fill with various data from the
* PikaAction, when they are not in GAction API, e.g. labels, but action
* visibility.
*
* The object will also synchronize automatically with changes from the actions,
* but also PikaUIManager for dynamic contents and will trigger an
* "items-changed" when necessary. This allows for such variant to be used in
* GTK API which has no knowledge of PikaAction or PikaUIManager enhancements.
*/
enum
{
PROP_0,
PROP_MANAGER,
PROP_MODEL,
PROP_PATH,
PROP_IS_SECTION,
PROP_TITLE,
PROP_COLOR,
};
struct _PikaMenuModelPrivate
{
PikaUIManager *manager;
GMenuModel *model;
gchar *path;
gboolean is_section;
/* If this PikaMenuModel represents a submenu for a bigger menu, this object
* will not be NULL.
*/
GMenuItem *submenu_item;
PikaRGB *submenu_color;
GList *items;
GHashTable *named_sections;
};
static void pika_menu_model_finalize (GObject *object);
static void pika_menu_model_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void pika_menu_model_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static GVariant * pika_menu_model_get_item_attribute_value (GMenuModel *model,
gint item_index,
const gchar *attribute,
const GVariantType *expected_type);
static void pika_menu_model_get_item_attributes (GMenuModel *model,
gint item_index,
GHashTable **attributes);
static GMenuModel * pika_menu_model_get_item_link (GMenuModel *model,
gint item_index,
const gchar *link);
static void pika_menu_model_get_item_links (GMenuModel *model,
gint item_index,
GHashTable **links);
static gint pika_menu_model_get_n_items (GMenuModel *model);
static gint pika_menu_model_get_position (PikaMenuModel *model,
const gchar *action_name,
gboolean *visible);;
static gboolean pika_menu_model_is_mutable (GMenuModel *model);
static void pika_menu_model_initialize (PikaMenuModel *model,
GMenuModel *gmodel);
static gchar * pika_menu_model_handles_subpath (PikaMenuModel *model,
const gchar *canonical_path,
const gchar *mnemonic_path);
static GMenuItem * pika_menu_model_get_item (PikaMenuModel *model,
gint idx);
static GMenuItem * pika_menu_model_get_menu_item_rec (PikaMenuModel *model,
const gchar *path,
PikaMenuModel **menu,
GMenuItem *item);
static void pika_menu_model_notify_group_label (PikaRadioAction *action,
const GParamSpec *pspec,
GMenuItem *item);
static void pika_menu_model_action_notify_visible (PikaAction *action,
GParamSpec *pspec,
PikaMenuModel *model);
static void pika_menu_model_action_notify_label (PikaAction *action,
GParamSpec *pspec,
GMenuItem *item);
static gboolean pika_menu_model_ui_added (PikaUIManager *manager,
const gchar *path,
const gchar *action_name,
gboolean top,
PikaMenuModel *model);
static gboolean pika_menu_model_ui_removed (PikaUIManager *manager,
const gchar *path,
const gchar *action_name,
PikaMenuModel *model);
G_DEFINE_TYPE_WITH_CODE (PikaMenuModel, pika_menu_model, G_TYPE_MENU_MODEL,
G_ADD_PRIVATE (PikaMenuModel))
#define parent_class pika_menu_model_parent_class
/* Class functions */
static void
pika_menu_model_class_init (PikaMenuModelClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass);
object_class->finalize = pika_menu_model_finalize;
object_class->get_property = pika_menu_model_get_property;
object_class->set_property = pika_menu_model_set_property;
model_class->get_item_attribute_value = pika_menu_model_get_item_attribute_value;
model_class->get_item_attributes = pika_menu_model_get_item_attributes;
model_class->get_item_link = pika_menu_model_get_item_link;
model_class->get_item_links = pika_menu_model_get_item_links;
model_class->get_n_items = pika_menu_model_get_n_items;
model_class->is_mutable = pika_menu_model_is_mutable;
g_object_class_install_property (object_class, PROP_MANAGER,
g_param_spec_object ("manager",
NULL, NULL,
PIKA_TYPE_UI_MANAGER,
PIKA_PARAM_READWRITE |
G_PARAM_CONSTRUCT));
g_object_class_install_property (object_class, PROP_MODEL,
g_param_spec_object ("model",
NULL, NULL,
G_TYPE_MENU_MODEL,
PIKA_PARAM_READWRITE));
g_object_class_install_property (object_class, PROP_PATH,
g_param_spec_string ("path",
NULL, NULL, NULL,
PIKA_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class, PROP_IS_SECTION,
g_param_spec_boolean ("section",
NULL, NULL, FALSE,
PIKA_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
/* Titles are only relevant if the model is that of a submenu. */
g_object_class_install_property (object_class, PROP_TITLE,
g_param_spec_string ("title",
NULL, NULL, NULL,
PIKA_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY));
g_object_class_install_property (object_class, PROP_COLOR,
pika_param_spec_rgb ("color",
NULL, NULL,
TRUE, &(PikaRGB) {},
PIKA_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY));
}
static void
pika_menu_model_init (PikaMenuModel *model)
{
model->priv = pika_menu_model_get_instance_private (model);
model->priv->items = NULL;
model->priv->path = NULL;
model->priv->is_section = FALSE;
model->priv->submenu_item = NULL;
model->priv->submenu_color = NULL;
model->priv->named_sections = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
}
static void
pika_menu_model_finalize (GObject *object)
{
PikaMenuModel *model = PIKA_MENU_MODEL (object);
g_clear_weak_pointer (&model->priv->manager);
g_clear_object (&model->priv->model);
g_list_free_full (model->priv->items, g_object_unref);
g_free (model->priv->path);
g_free (model->priv->submenu_color);
g_hash_table_destroy (model->priv->named_sections);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
void
pika_menu_model_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
PikaMenuModel *model = PIKA_MENU_MODEL (object);
switch (property_id)
{
case PROP_MANAGER:
g_value_set_object (value, model->priv->manager);
break;
case PROP_MODEL:
g_value_set_object (value, model->priv->model);
break;
case PROP_TITLE:
{
gchar *title;
g_menu_item_get_attribute (model->priv->submenu_item, G_MENU_ATTRIBUTE_LABEL, "s", &title);
g_value_set_string (value, title);
g_free (title);
}
break;
case PROP_COLOR:
g_value_set_boxed (value, model->priv->submenu_color);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
void
pika_menu_model_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
PikaMenuModel *model = PIKA_MENU_MODEL (object);
switch (property_id)
{
case PROP_MANAGER:
g_set_weak_pointer (&model->priv->manager, g_value_get_object (value));
break;
case PROP_MODEL:
model->priv->model = g_value_dup_object (value);
pika_menu_model_initialize (model, model->priv->model);
break;
case PROP_PATH:
model->priv->path = g_value_dup_string (value);
break;
case PROP_IS_SECTION:
model->priv->is_section = g_value_get_boolean (value);
break;
case PROP_TITLE:
pika_menu_model_set_title (model, model->priv->path,
g_value_get_string (value));
break;
case PROP_COLOR:
pika_menu_model_set_color (model, model->priv->path,
g_value_get_boxed (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static GVariant*
pika_menu_model_get_item_attribute_value (GMenuModel *model,
gint item_index,
const gchar *attribute,
const GVariantType *expected_type)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
GMenuItem *item;
item = pika_menu_model_get_item (m, item_index);
return g_menu_item_get_attribute_value (item, attribute, expected_type);
}
static void
pika_menu_model_get_item_attributes (GMenuModel *model,
gint item_index,
GHashTable **attributes)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
GMenuItem *item;
GVariant *value;
item = pika_menu_model_get_item (m, item_index);
*attributes = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
(GDestroyNotify) g_variant_unref);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_LABEL, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_LABEL, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_ACTION, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_ACTION, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_ICON, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_ICON, value);
value = g_menu_item_get_attribute_value (item, G_MENU_LINK_SUBMENU, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_LINK_SUBMENU, value);
value = g_menu_item_get_attribute_value (item, G_MENU_LINK_SECTION, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_LINK_SECTION, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_TARGET, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_TARGET, value);
value = g_menu_item_get_attribute_value (item, "hidden-when", NULL);
if (value)
g_hash_table_insert (*attributes, "hidden-when", value);
}
static GMenuModel*
pika_menu_model_get_item_link (GMenuModel *model,
gint item_index,
const gchar *link)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
GMenuItem *item;
item = pika_menu_model_get_item (m, item_index);
return g_menu_item_get_link (item, link);
}
static void
pika_menu_model_get_item_links (GMenuModel *model,
gint item_index,
GHashTable **links)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
GMenuModel *subsection;
GMenuModel *submenu;
GMenuItem *item;
*links = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
(GDestroyNotify) g_object_unref);
item = pika_menu_model_get_item (m, item_index);
subsection = g_menu_item_get_link (item, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (item, G_MENU_LINK_SUBMENU);
if (subsection)
g_hash_table_insert (*links, G_MENU_LINK_SECTION, g_object_ref (subsection));
if (submenu)
g_hash_table_insert (*links, G_MENU_LINK_SUBMENU, g_object_ref (submenu));
g_clear_object (&subsection);
g_clear_object (&submenu);
}
static gint
pika_menu_model_get_n_items (GMenuModel *model)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
return pika_menu_model_get_position (m, NULL, NULL);
}
/* This function has 2 usage:
* - Either you call it with @action_name == NULL, then it returns the
* total number of visible items in this model.
* - Or it returns the position of @action_name and its visible state.
*/
static gint
pika_menu_model_get_position (PikaMenuModel *model,
const gchar *action_name,
gboolean *visible)
{
GList *iter;
gint len = 0;
for (iter = model->priv->items; iter; iter = iter->next)
{
GMenuModel *subsection;
GMenuModel *submenu;
const gchar *cur_action_name = NULL;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (iter->data, G_MENU_LINK_SUBMENU);
if (subsection || submenu)
{
len++;
}
/* Count neither placeholders (items with no action name), nor invisible
* actions.
*/
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &cur_action_name))
{
PikaAction *cur_action = NULL;
const gchar *real_action_name = NULL;
if (cur_action_name != NULL)
{
real_action_name = strstr (cur_action_name, ".");
if (real_action_name != NULL)
real_action_name++;
else
real_action_name = cur_action_name;
cur_action = pika_ui_manager_find_action (model->priv->manager,
NULL, cur_action_name);
}
if (cur_action_name != NULL &&
g_strcmp0 (action_name, real_action_name) == 0)
{
if (visible)
{
if (cur_action != NULL)
*visible = pika_action_is_visible (cur_action);
else
/* This may happen when editing a menu item for an action
* which got removed.
*/
*visible = FALSE;
}
break;
}
else if (cur_action != NULL && pika_action_is_visible (cur_action))
{
len++;
}
}
g_clear_object (&subsection);
g_clear_object (&submenu);
}
g_return_val_if_fail (action_name == NULL || iter != NULL, -1);
return len;
}
static gboolean
pika_menu_model_is_mutable (GMenuModel* model)
{
return TRUE;
}
/* Public functions. */
PikaMenuModel *
pika_menu_model_new (PikaUIManager *manager,
GMenuModel *model)
{
g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL);
return g_object_new (PIKA_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
NULL);
}
PikaMenuModel *
pika_menu_model_get_submodel (PikaMenuModel *model,
const gchar *path)
{
PikaMenuModel *submodel;
gchar *submenus;
gchar *submenu;
gchar *subsubmenus;
submodel = g_object_ref (model);
if (path == NULL)
return submodel;
submenus = g_strdup (path);
subsubmenus = submenus;
while (subsubmenus && strlen (subsubmenus) > 0)
{
gint n_items;
gint i;
submenu = subsubmenus;
while (*submenu == '/')
submenu++;
subsubmenus = strstr (submenu, "/");
if (subsubmenus)
*(subsubmenus++) = '\0';
if (strlen (submenu) == 0)
break;
n_items = g_menu_model_get_n_items (G_MENU_MODEL (submodel));
for (i = 0; i < n_items; i++)
{
GMenuModel *subsubmodel;
gchar *label = NULL;
gchar *canon_label = NULL;
subsubmodel = g_menu_model_get_item_link (G_MENU_MODEL (submodel), i, G_MENU_LINK_SUBMENU);
g_menu_model_get_item_attribute (G_MENU_MODEL (submodel), i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
if (label)
canon_label = pika_utils_make_canonical_menu_label (label);
if (subsubmodel && g_strcmp0 (canon_label, submenu) == 0)
{
g_object_unref (submodel);
submodel = PIKA_MENU_MODEL (subsubmodel);
g_free (label);
g_free (canon_label);
break;
}
g_clear_object (&subsubmodel);
g_free (label);
g_free (canon_label);
}
g_return_val_if_fail (i < n_items, NULL);
}
g_free (submenus);
return submodel;
}
const gchar *
pika_menu_model_get_path (PikaMenuModel *model)
{
return model->priv->path;
}
void
pika_menu_model_set_title (PikaMenuModel *model,
const gchar *path,
const gchar *title)
{
GMenuItem *item;
PikaMenuModel *submenu = NULL;
item = pika_menu_model_get_menu_item_rec (model, path, &submenu, NULL);
if (item != NULL)
{
g_menu_item_set_label (item, title);
g_object_notify (G_OBJECT (submenu), "title");
}
}
void
pika_menu_model_set_color (PikaMenuModel *model,
const gchar *path,
const PikaRGB *color)
{
GMenuItem *item;
PikaMenuModel *submenu = NULL;
item = pika_menu_model_get_menu_item_rec (model, path, &submenu, NULL);
if (item != NULL)
{
if (color == NULL)
g_clear_pointer (&submenu->priv->submenu_color, g_free);
else if (submenu->priv->submenu_color == NULL)
submenu->priv->submenu_color = g_new (PikaRGB, 1);
if (color != NULL)
*submenu->priv->submenu_color = *color;
g_object_notify (G_OBJECT (submenu), "color");
}
}
/* Private functions. */
static PikaMenuModel *
pika_menu_model_new_section (PikaUIManager *manager,
GMenuModel *model,
const gchar *path)
{
g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL);
return g_object_new (PIKA_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
"path", path,
"section", TRUE,
NULL);
}
static PikaMenuModel *
pika_menu_model_new_submenu (PikaUIManager *manager,
GMenuModel *model,
const gchar *path)
{
g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL);
return g_object_new (PIKA_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
"path", path,
NULL);
}
static void
pika_menu_model_initialize (PikaMenuModel *model,
GMenuModel *gmodel)
{
gint n_items;
g_return_if_fail (PIKA_IS_MENU_MODEL (model));
g_return_if_fail (gmodel == NULL || G_IS_MENU_MODEL (gmodel));
n_items = gmodel != NULL ? g_menu_model_get_n_items (gmodel) : 0;
for (int i = 0; i < n_items; i++)
{
GMenuModel *subsection;
GMenuModel *submenu;
gchar *label = NULL;
gchar *action_name = NULL;
GMenuItem *item = NULL;
subsection = g_menu_model_get_item_link (G_MENU_MODEL (gmodel), i, G_MENU_LINK_SECTION);
submenu = g_menu_model_get_item_link (G_MENU_MODEL (gmodel), i, G_MENU_LINK_SUBMENU);
g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
G_MENU_ATTRIBUTE_LABEL, "s", &label);
g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
G_MENU_ATTRIBUTE_ACTION, "s", &action_name);
if (subsection != NULL)
{
PikaMenuModel *submodel;
gchar *section_name = NULL;
submodel = pika_menu_model_new_section (model->priv->manager, subsection,
model->priv->path);
item = g_menu_item_new_section (label, G_MENU_MODEL (submodel));
if (g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
"section-name", "s", &section_name))
g_hash_table_insert (model->priv->named_sections,
(gpointer) section_name,
item);
g_object_unref (submodel);
}
else if (submenu != NULL && label == NULL)
{
PikaMenuModel *submodel;
PikaAction *action;
gchar *canon_label;
const gchar *group_label;
gchar *path;
g_return_if_fail (action_name != NULL);
action = pika_ui_manager_find_action (model->priv->manager, NULL, action_name);
/* As a special case, when a submenu has no label, we expect it to
* have an action attribute, which must be for a radio action. In such
* a case, we'll use the radio actions' group label as submenu title.
* See e.g.: menus/gradient-editor-menu.ui
*/
g_return_if_fail (PIKA_IS_RADIO_ACTION (action));
group_label = pika_radio_action_get_group_label (PIKA_RADIO_ACTION (action));
canon_label = pika_utils_make_canonical_menu_label (group_label);
path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
g_free (canon_label);
submodel = pika_menu_model_new_submenu (model->priv->manager, submenu, path);
item = g_menu_item_new_submenu (group_label, G_MENU_MODEL (submodel));
g_signal_connect_object (action, "notify::group-label",
G_CALLBACK (pika_menu_model_notify_group_label),
item, 0);
g_object_unref (submodel);
g_free (path);
}
else if (submenu != NULL)
{
PikaMenuModel *submodel;
gchar *canon_label;
gchar *path;
const gchar *en_label;
g_return_if_fail (label != NULL);
en_label = g_object_get_data (G_OBJECT (submenu),
"pika-ui-manager-menu-model-en-label");
g_return_if_fail (en_label != NULL);
canon_label = pika_utils_make_canonical_menu_label (en_label);
path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
g_free (canon_label);
submodel = pika_menu_model_new_submenu (model->priv->manager, submenu, path);
item = g_menu_item_new_submenu (label, G_MENU_MODEL (submodel));
submodel->priv->submenu_item = item;
g_object_unref (submodel);
g_free (path);
}
else
{
PikaAction *action;
gchar *label_variant = NULL;
item = g_menu_item_new_from_model (G_MENU_MODEL (gmodel), i);
g_return_if_fail (action_name != NULL);
action = pika_ui_manager_find_action (model->priv->manager, NULL, action_name);
if (model->priv->manager->store_action_paths)
/* Special-case the main menu manager when constructing it as
* this is the only one which should set the menu path.
*/
pika_action_set_menu_path (action, pika_menu_model_get_path (model));
g_signal_connect_object (action,
"notify::visible",
G_CALLBACK (pika_menu_model_action_notify_visible),
model, 0);
g_menu_item_get_attribute (item, "label-variant", "s",
&label_variant);
if (g_strcmp0 (label_variant, "long") == 0)
g_menu_item_set_label (item, pika_action_get_label (action));
else
g_menu_item_set_label (item, pika_action_get_short_label (action));
g_signal_connect_object (action,
"notify::short-label",
G_CALLBACK (pika_menu_model_action_notify_label),
item, 0);
g_signal_connect_object (action,
"notify::label",
G_CALLBACK (pika_menu_model_action_notify_label),
item, 0);
/* We want PikaRadioAction to be GTK_MENU_TRACKER_ITEM_ROLE_RADIO,
* in order to be displayed as radio menu items (as used to be
* GtkRadioAction) even in the gtk_application_set_menubar() code path.
*
* GTK do this by checking that the item has a "target" parameter,
* which we don't add in our .ui file. Instead let's do this
* programmatically, hence avoiding human errors.
* See: gtk/gtkmenutrackeritem.c
*/
if (PIKA_IS_RADIO_ACTION (action))
{
gint target;
g_object_get (action, "value", &target, NULL);
g_menu_item_set_attribute (item, G_MENU_ATTRIBUTE_TARGET, "i", target);
}
g_free (label_variant);
}
if (item)
model->priv->items = g_list_append (model->priv->items, item);
g_free (label);
g_free (action_name);
g_clear_object (&subsection);
g_clear_object (&submenu);
}
if (! model->priv->is_section)
{
g_signal_connect_object (model->priv->manager, "ui-added",
G_CALLBACK (pika_menu_model_ui_added),
model, 0);
g_signal_connect_object (model->priv->manager, "ui-removed",
G_CALLBACK (pika_menu_model_ui_removed),
model, 0);
pika_ui_manager_foreach_ui (model->priv->manager,
(PikaUIMenuCallback) pika_menu_model_ui_added,
model);
}
}
/*
* Returns the directory to create as a new submodel to @model, with
* @canonical_path being the fully canonical path name (all slashes unique,
* leading slash, no trailing slash, section name removed and no mnemonic), e.g.
* "/some/path/name" and @mnemonic_path is the fully canonical path name, except
* that it contains mnemonics, e.g. "/_some/p_ath/_name".
*
* The code relies on these canonicalized characteristics so you must make sure
* to feed properly formatted paths.
*
* The return value is the canonical path of the first submenu to create,
* including mnemonics, e.g. if @model was for path "/some", then this function
* returns "p_ath".
*/
static gchar *
pika_menu_model_handles_subpath (PikaMenuModel *model,
const gchar *canonical_path,
const gchar *mnemonic_path)
{
gchar *new_dir;
gchar *end_new_dir;
gint n_slash = 0;
if (model->priv->path != NULL &&
(! g_str_has_prefix (canonical_path, model->priv->path) ||
canonical_path[strlen (model->priv->path)] != '/'))
{
return FALSE;
}
for (GList *iter = model->priv->items; iter; iter = iter->next)
{
GMenuModel *submenu = NULL;
GMenuModel *subsection = NULL;
GMenuItem *item = iter->data;
subsection = g_menu_item_get_link (item, G_MENU_LINK_SECTION);
if (subsection != NULL)
{
/* Checking if a subsection is a sub-path only gives a partial view of
* the whole menu. So we only handle the negative result (which means
* we found a submenu model which will handle this path instead).
*/
new_dir = pika_menu_model_handles_subpath (PIKA_MENU_MODEL (subsection),
canonical_path, mnemonic_path);
if (new_dir == NULL)
{
g_clear_object (&subsection);
return NULL;
}
else
{
g_free (new_dir);
}
}
else if ((submenu = g_menu_item_get_link (item, G_MENU_LINK_SUBMENU)) != NULL)
{
gchar *subpath;
subpath = g_strdup_printf ("%s/", PIKA_MENU_MODEL (submenu)->priv->path);
if (g_strcmp0 (canonical_path, PIKA_MENU_MODEL (submenu)->priv->path) == 0 ||
g_str_has_prefix (canonical_path, subpath))
{
/* A submodel will handle the new path. */
g_free (subpath);
g_clear_object (&subsection);
g_clear_object (&submenu);
return NULL;
}
g_free (subpath);
}
g_clear_object (&subsection);
g_clear_object (&submenu);
}
if (model->priv->path != NULL)
{
n_slash = 1;
new_dir = model->priv->path;
while ((new_dir = strstr (new_dir + 1, "/")) != NULL)
n_slash++;
}
new_dir = (gchar *) mnemonic_path + 1;
while (n_slash-- > 0)
new_dir = strstr (new_dir, "/") + 1;
end_new_dir = strstr (new_dir, "/");
if (end_new_dir)
new_dir = g_strndup (new_dir, end_new_dir - new_dir);
else
new_dir = g_strdup (new_dir);
return new_dir;
}
static GMenuItem *
pika_menu_model_get_item (PikaMenuModel *model,
gint idx)
{
PikaMenuModel *m = PIKA_MENU_MODEL (model);
gint cur = -1;
for (GList *iter = m->priv->items; iter; iter = iter->next)
{
GMenuModel *subsection;
GMenuModel *submenu;
const gchar *action_name = NULL;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (iter->data, G_MENU_LINK_SUBMENU);
if (subsection || submenu)
{
cur++;
}
/* Do not count invisible actions. */
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &action_name))
{
PikaAction *action;
action = pika_ui_manager_find_action (model->priv->manager, NULL,
action_name);
if (pika_action_is_visible (action))
cur++;
}
g_clear_object (&subsection);
g_clear_object (&submenu);
if (cur == idx)
return iter->data;
}
return NULL;
}
static GMenuItem *
pika_menu_model_get_menu_item_rec (PikaMenuModel *model,
const gchar *path,
PikaMenuModel **menu,
GMenuItem *item)
{
g_return_val_if_fail (item == model->priv->submenu_item, NULL);
g_return_val_if_fail (menu != NULL && *menu == NULL, NULL);
if (pika_utils_are_menu_path_identical (path, model->priv->path, NULL, NULL, NULL))
{
*menu = model;
return item;
}
else
{
GList *iter;
GMenuItem *found = NULL;
for (iter = model->priv->items; iter; iter = iter->next)
{
GMenuItem *subitem = iter->data;
GMenuModel *submenu = NULL;
GMenuModel *section = NULL;
submenu = g_menu_item_get_link (subitem, G_MENU_LINK_SUBMENU);
section = g_menu_item_get_link (subitem, G_MENU_LINK_SECTION);
if (section != NULL)
{
found = pika_menu_model_get_menu_item_rec (PIKA_MENU_MODEL (section), path, menu, NULL);
}
else if (submenu != NULL)
{
gchar *subpath;
subpath = g_strdup_printf ("%s/", PIKA_MENU_MODEL (submenu)->priv->path);
if (g_strcmp0 (path, PIKA_MENU_MODEL (submenu)->priv->path) == 0 ||
g_str_has_prefix (path, subpath))
{
found = pika_menu_model_get_menu_item_rec (PIKA_MENU_MODEL (submenu), path, menu, subitem);
}
g_free (subpath);
}
g_clear_object (&submenu);
g_clear_object (&section);
if (found != NULL)
break;
}
return found;
}
}
static void
pika_menu_model_notify_group_label (PikaRadioAction *action,
const GParamSpec *pspec,
GMenuItem *item)
{
g_menu_item_set_label (item, pika_radio_action_get_group_label (action));
}
static void
pika_menu_model_action_notify_visible (PikaAction *action,
GParamSpec *pspec,
PikaMenuModel *model)
{
gint pos;
gboolean visible;
pos = pika_menu_model_get_position (model, pika_action_get_name (action),
&visible);
if (visible)
g_menu_model_items_changed (G_MENU_MODEL (model), pos, 0, 1);
else
g_menu_model_items_changed (G_MENU_MODEL (model), pos, 1, 0);
}
static void
pika_menu_model_action_notify_label (PikaAction *action,
GParamSpec *pspec,
GMenuItem *item)
{
gchar *label_variant = NULL;
g_return_if_fail (PIKA_IS_ACTION (action));
g_return_if_fail (G_IS_MENU_ITEM (item));
g_menu_item_get_attribute (item, "label-variant", "s", &label_variant);
if (g_strcmp0 (label_variant, "long") == 0)
g_menu_item_set_label (item, pika_action_get_label (action));
else
g_menu_item_set_label (item, pika_action_get_short_label (action));
g_free (label_variant);
}
static gboolean
pika_menu_model_ui_added (PikaUIManager *manager,
const gchar *path,
const gchar *action_name,
gboolean top,
PikaMenuModel *model)
{
gchar *canonical_path = NULL;
gchar *mnemonic_path = NULL;
gchar *section_name = NULL;
gboolean added = FALSE;
PikaMenuModel *mod_model = g_object_ref (model);
gchar *new_dir;
if (pika_utils_are_menu_path_identical (path, model->priv->path, &canonical_path,
&mnemonic_path, &section_name))
{
GApplication *app = model->priv->manager->pika->app;
GAction *action;
gchar *detailed_action_name;
const gchar *action_prefix = "app";
GMenuItem *item;
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name);
if (action == NULL)
{
action = (GAction *) pika_ui_manager_find_action (manager, NULL, action_name);
if (action != NULL)
{
PikaActionGroup *group;
group = pika_action_get_group (PIKA_ACTION (action));
if (group != NULL)
action_prefix = pika_action_group_get_name (group);
}
}
g_return_val_if_fail (action != NULL, FALSE);
added = TRUE;
if (section_name != NULL)
{
GMenuItem *section_item;
section_item = g_hash_table_lookup (model->priv->named_sections, section_name);
if (section_item)
{
g_object_unref (mod_model);
mod_model = PIKA_MENU_MODEL (g_menu_item_get_link (section_item, G_MENU_LINK_SECTION));
}
}
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (pika_menu_model_action_notify_visible),
mod_model);
detailed_action_name = g_strdup_printf ("%s.%s", action_prefix, g_action_get_name (action));
item = g_menu_item_new (pika_action_get_short_label (PIKA_ACTION (action)), detailed_action_name);
/* TODO: add also G_MENU_ATTRIBUTE_ICON attribute? */
g_free (detailed_action_name);
if (model->priv->manager->store_action_paths)
pika_action_set_menu_path (PIKA_ACTION (action), pika_menu_model_get_path (model));
if (top)
{
mod_model->priv->items = g_list_prepend (mod_model->priv->items, item);
}
else
{
mod_model->priv->items = g_list_append (mod_model->priv->items, item);
}
if (added)
{
gint position = pika_menu_model_get_position (mod_model, action_name, NULL);
g_signal_connect_object (action,
"notify::visible",
G_CALLBACK (pika_menu_model_action_notify_visible),
mod_model, 0);
g_signal_connect_object (action,
"notify::short-label",
G_CALLBACK (pika_menu_model_action_notify_label),
item, 0);
g_signal_connect_object (action,
"notify::label",
G_CALLBACK (pika_menu_model_action_notify_label),
item, 0);
g_menu_model_items_changed (G_MENU_MODEL (mod_model), position, 0, 1);
}
else
{
g_object_unref (item);
}
}
else if ((new_dir = pika_menu_model_handles_subpath (model, canonical_path, mnemonic_path)))
{
PikaMenuModel *submodel;
GMenuItem *item;
gchar *canon_label;
gchar *submodel_path;
canon_label = pika_utils_make_canonical_menu_label (new_dir);
submodel_path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
submodel = pika_menu_model_new_submenu (model->priv->manager, NULL, submodel_path);
item = g_menu_item_new_submenu (new_dir, G_MENU_MODEL (submodel));
if (model->priv->path == NULL)
model->priv->items = g_list_insert (model->priv->items, item,
g_list_length (model->priv->items) - 2);
else
model->priv->items = g_list_append (model->priv->items, item);
g_free (canon_label);
g_object_unref (submodel);
g_free (submodel_path);
g_free (new_dir);
g_menu_model_items_changed (G_MENU_MODEL (model),
pika_menu_model_get_position (model, NULL, NULL),
1, 0);
}
g_clear_object (&mod_model);
g_free (canonical_path);
g_free (mnemonic_path);
g_free (section_name);
return added;
}
static gboolean
pika_menu_model_ui_removed (PikaUIManager *manager,
const gchar *path,
const gchar *action_name,
PikaMenuModel *model)
{
gchar *section_name = NULL;
gboolean removed = FALSE;
if (pika_utils_are_menu_path_identical (path, model->priv->path, NULL, NULL, &section_name))
{
GApplication *app = model->priv->manager->pika->app;
GMenuItem *item = NULL;
GMenuModel *subsection = NULL;
GAction *action;
GList *iter;
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name);
removed = TRUE;
for (iter = model->priv->items; iter; iter = iter->next)
{
const gchar *action;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
if (subsection != NULL)
{
if (pika_menu_model_ui_removed (manager, path, action_name,
PIKA_MENU_MODEL (subsection)))
break;
else
g_clear_object (&subsection);
}
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &action) &&
/* "action" attribute will start with "app." prefix. */
g_strcmp0 (action + 4, action_name) == 0)
{
item = iter->data;
break;
}
}
if (item)
{
gint position;
position = pika_menu_model_get_position (model, action_name, NULL);
if (action != NULL)
{
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (pika_menu_model_action_notify_visible),
model);
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (pika_menu_model_action_notify_label),
item);
}
g_object_unref (item);
model->priv->items = g_list_delete_link (model->priv->items, iter);
g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 0);
}
else
{
removed = FALSE;
if (subsection == NULL && ! model->priv->is_section)
g_warning ("%s: no item for action name '%s'.", G_STRFUNC, action_name);
}
/* else: removed in a subsection. */
g_clear_object (&subsection);
}
g_free (section_name);
return removed;
}