/* 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;
}
