/* 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
 *
 * pikatoolbutton.c
 * Copyright (C) 2020 Ell
 *
 * 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 <string.h>

#include <gegl.h>
#include <gtk/gtk.h>

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikawidgets/pikawidgets.h"
#include "libpikawidgets/pikawidgets-private.h"

#include "widgets-types.h"

#include "core/pika.h"
#include "core/pika-gui.h"
#include "core/pikacontainer.h"
#include "core/pikacontext.h"
#include "core/pikadisplay.h"
#include "core/pikatoolgroup.h"
#include "core/pikatoolinfo.h"

#include "actions/tools-commands.h"

#include "pikaaccellabel.h"
#include "pikaaction.h"
#include "pikadock.h"
#include "pikatoolbox.h"
#include "pikatoolbutton.h"
#include "pikawidgets-utils.h"
#include "pikawindowstrategy.h"

#include "pika-intl.h"


#define ARROW_SIZE   0.125 /* * 100%       */
#define ARROW_BORDER 3     /* px           */

#define MENU_TIMEOUT 250   /* milliseconds */


enum
{
  PROP_0,
  PROP_TOOLBOX,
  PROP_TOOL_ITEM
};


struct _PikaToolButtonPrivate
{
  PikaToolbox  *toolbox;
  PikaToolItem *tool_item;

  GtkWidget    *palette;

  GtkWidget    *tooltip_widget;

  GtkWidget    *menu;
  GHashTable   *menu_items;
  gint          menu_timeout_id;
  GdkEvent     *menu_timeout_event;
};


/*  local function prototypes  */

static void         pika_tool_button_constructed         (GObject             *object);
static void         pika_tool_button_dispose             (GObject             *object);
static void         pika_tool_button_set_property        (GObject             *object,
                                                          guint                property_id,
                                                          const GValue        *value,
                                                          GParamSpec          *pspec);
static void         pika_tool_button_get_property        (GObject             *object,
                                                          guint                property_id,
                                                          GValue              *value,
                                                          GParamSpec          *pspec);

static void         pika_tool_button_hierarchy_changed   (GtkWidget           *widget,
                                                          GtkWidget           *previous_toplevel);
static gboolean     pika_tool_button_draw                (GtkWidget           *widget,
                                                          cairo_t             *cr);
static gboolean     pika_tool_button_query_tooltip       (GtkWidget           *widget,
                                                          gint                 x,
                                                          gint                 y,
                                                          gboolean             keyboard_mode,
                                                          GtkTooltip          *tooltip);

static void         pika_tool_button_toggled             (GtkToggleToolButton *toggle_tool_button);

static gboolean     pika_tool_button_button_press        (GtkWidget           *widget,
                                                          GdkEventButton      *event,
                                                          PikaToolButton      *tool_button);
static gboolean     pika_tool_button_button_release      (GtkWidget           *widget,
                                                          GdkEventButton      *event,
                                                          PikaToolButton      *tool_button);
static gboolean     pika_tool_button_scroll              (GtkWidget           *widget,
                                                          GdkEventScroll      *event,
                                                          PikaToolButton      *tool_button);

static void         pika_tool_button_tool_changed        (PikaContext         *context,
                                                          PikaToolInfo        *tool_info,
                                                          PikaToolButton      *tool_button);

static void         pika_tool_button_active_tool_changed (PikaToolGroup       *tool_group,
                                                          PikaToolButton      *tool_button);

static void         pika_tool_button_tool_add            (PikaContainer       *container,
                                                          PikaToolInfo        *tool_info,
                                                          PikaToolButton      *tool_button);
static void         pika_tool_button_tool_remove         (PikaContainer       *container,
                                                          PikaToolInfo        *tool_info,
                                                          PikaToolButton      *tool_button);
static void         pika_tool_button_tool_reorder        (PikaContainer       *container,
                                                          PikaToolInfo        *tool_info,
                                                          gint                 new_index,
                                                          PikaToolButton      *tool_button);

static void         pika_tool_button_icon_size_notify    (GtkToolPalette      *palette,
                                                          const GParamSpec    *pspec,
                                                          PikaToolButton      *tool_button);

static void         pika_tool_button_menu_item_activate  (GtkMenuItem         *menu_item,
                                                          PikaAction          *action);

static gboolean     pika_tool_button_menu_leave_notify   (GtkMenu             *menu,
                                                          GdkEventCrossing    *event,
                                                          PikaToolButton      *tool_button);

static gboolean     pika_tool_button_menu_timeout        (PikaToolButton      *tool_button);

static void         pika_tool_button_update              (PikaToolButton      *tool_button);
static void         pika_tool_button_update_toggled      (PikaToolButton      *tool_button);
static void         pika_tool_button_update_menu         (PikaToolButton      *tool_button);

static void         pika_tool_button_add_menu_item       (PikaToolButton      *tool_button,
                                                          PikaToolInfo        *tool_info,
                                                          gint                 index);
static void         pika_tool_button_remove_menu_item    (PikaToolButton      *tool_button,
                                                          PikaToolInfo        *tool_info);

static void         pika_tool_button_reconstruct_menu    (PikaToolButton      *tool_button);
static void         pika_tool_button_destroy_menu        (PikaToolButton      *tool_button);
static gboolean     pika_tool_button_show_menu           (PikaToolButton      *tool_button,
                                                          const GdkEvent      *trigger_event);

static PikaAction * pika_tool_button_get_action          (PikaToolButton      *tool_button,
                                                          PikaToolInfo        *tool_info);


G_DEFINE_TYPE_WITH_PRIVATE (PikaToolButton, pika_tool_button,
                            GTK_TYPE_TOGGLE_TOOL_BUTTON)

#define parent_class pika_tool_button_parent_class


/*  private functions  */

static void
pika_tool_button_class_init (PikaToolButtonClass *klass)
{
  GObjectClass             *object_class             = G_OBJECT_CLASS (klass);
  GtkWidgetClass           *widget_class             = GTK_WIDGET_CLASS (klass);
  GtkToggleToolButtonClass *toggle_tool_button_class = GTK_TOGGLE_TOOL_BUTTON_CLASS (klass);

  object_class->constructed         = pika_tool_button_constructed;
  object_class->dispose             = pika_tool_button_dispose;
  object_class->get_property        = pika_tool_button_get_property;
  object_class->set_property        = pika_tool_button_set_property;

  widget_class->hierarchy_changed   = pika_tool_button_hierarchy_changed;
  widget_class->draw                = pika_tool_button_draw;
  widget_class->query_tooltip       = pika_tool_button_query_tooltip;

  toggle_tool_button_class->toggled = pika_tool_button_toggled;

  g_object_class_install_property (object_class, PROP_TOOLBOX,
                                   g_param_spec_object ("toolbox",
                                                        NULL, NULL,
                                                        PIKA_TYPE_TOOLBOX,
                                                        PIKA_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_TOOL_ITEM,
                                   g_param_spec_object ("tool-item",
                                                        NULL, NULL,
                                                        PIKA_TYPE_TOOL_ITEM,
                                                        PIKA_PARAM_READWRITE));
}

static void
pika_tool_button_init (PikaToolButton *tool_button)
{
  tool_button->priv = pika_tool_button_get_instance_private (tool_button);
}

static void
pika_tool_button_constructed (GObject *object)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (object);
  PikaContext    *context;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  context = pika_toolbox_get_context (tool_button->priv->toolbox);

  /* Make sure the toolbox buttons won't grab focus, which has
   * nearly no practical use, and prevents various actions until
   * you click back in canvas.
   */
  gtk_widget_set_can_focus (gtk_bin_get_child (GTK_BIN (tool_button)), FALSE);

  gtk_widget_add_events (gtk_bin_get_child (GTK_BIN (tool_button)),
                         GDK_SCROLL_MASK);

  g_signal_connect (gtk_bin_get_child (GTK_BIN (tool_button)),
                    "button-press-event",
                    G_CALLBACK (pika_tool_button_button_press),
                    tool_button);
  g_signal_connect (gtk_bin_get_child (GTK_BIN (tool_button)),
                    "button-release-event",
                    G_CALLBACK (pika_tool_button_button_release),
                    tool_button);
  g_signal_connect (gtk_bin_get_child (GTK_BIN (tool_button)),
                    "scroll-event",
                    G_CALLBACK (pika_tool_button_scroll),
                    tool_button);

  g_signal_connect_object (context, "tool-changed",
                           G_CALLBACK (pika_tool_button_tool_changed),
                           tool_button,
                           0);

  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_dispose (GObject *object)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (object);

  pika_tool_button_set_tool_item (tool_button, NULL);

  G_OBJECT_CLASS (parent_class)->dispose (object);
}

static void
pika_tool_button_set_property (GObject      *object,
                               guint         property_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (object);

  switch (property_id)
    {
    case PROP_TOOLBOX:
      tool_button->priv->toolbox = g_value_get_object (value);
      break;

    case PROP_TOOL_ITEM:
      pika_tool_button_set_tool_item (tool_button, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_tool_button_get_property (GObject    *object,
                               guint       property_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (object);

  switch (property_id)
    {
    case PROP_TOOLBOX:
      g_value_set_object (value, tool_button->priv->toolbox);
      break;

    case PROP_TOOL_ITEM:
      g_value_set_object (value, tool_button->priv->tool_item);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
pika_tool_button_hierarchy_changed (GtkWidget *widget,
                                    GtkWidget *previous_toplevel)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (widget);
  GtkWidget      *palette;

  if (GTK_WIDGET_CLASS (parent_class)->hierarchy_changed)
    {
      GTK_WIDGET_CLASS (parent_class)->hierarchy_changed (widget,
                                                          previous_toplevel);
    }

  palette = gtk_widget_get_ancestor (GTK_WIDGET (tool_button),
                                     GTK_TYPE_TOOL_PALETTE);

  if (palette != tool_button->priv->palette)
    {
      if (tool_button->priv->palette)
        {
          g_signal_handlers_disconnect_by_func (
            tool_button->priv->palette,
            pika_tool_button_icon_size_notify,
            tool_button);
        }

      tool_button->priv->palette = palette;

      if (tool_button->priv->palette)
        {
          g_signal_connect (
            tool_button->priv->palette, "notify::icon-size",
            G_CALLBACK (pika_tool_button_icon_size_notify),
            tool_button);
        }
    }

  pika_tool_button_update (tool_button);

  pika_tool_button_reconstruct_menu (tool_button);
}

static gboolean
pika_tool_button_draw (GtkWidget *widget,
                       cairo_t   *cr)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (widget);

  if (GTK_WIDGET_CLASS (parent_class)->draw)
    GTK_WIDGET_CLASS (parent_class)->draw (widget, cr);

  if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
    {
      GtkStyleContext *style = gtk_widget_get_style_context (widget);
      GtkStateFlags    state = gtk_widget_get_state_flags (widget);
      GdkRGBA          fg;
      GtkAllocation    allocation;
      gint             size;
      gint             x1, y1;
      gint             x2, y2;

      gtk_style_context_get_color (style, state, &fg);

      gtk_widget_get_allocation (widget, &allocation);

      size = MIN (allocation.width, allocation.height);

      x1 = SIGNED_ROUND (allocation.width  -
                         (ARROW_BORDER + size * ARROW_SIZE));
      y1 = SIGNED_ROUND (allocation.height -
                         (ARROW_BORDER + size * ARROW_SIZE));

      x2 = SIGNED_ROUND (allocation.width  - ARROW_BORDER);
      y2 = SIGNED_ROUND (allocation.height - ARROW_BORDER);

      cairo_save (cr);

      cairo_move_to (cr, x2, y1);
      cairo_line_to (cr, x2, y2);
      cairo_line_to (cr, x1, y2);
      cairo_close_path (cr);

      gdk_cairo_set_source_rgba (cr, &fg);
      cairo_fill (cr);

      cairo_restore (cr);
    }

  return FALSE;
}

static GtkWidget *
pika_tool_button_query_tooltip_add_tool (PikaToolButton *tool_button,
                                         GtkGrid        *grid,
                                         gint            row,
                                         PikaToolInfo   *tool_info,
                                         const gchar    *label_str,
                                         GtkIconSize     icon_size)
{
  Pika        *pika;
  PikaAction  *action = NULL;
  GtkWidget   *label;
  GtkWidget   *image;
  gchar       *name;

  pika = pika_toolbox_get_context (tool_button->priv->toolbox)->pika;
  name = pika_tool_info_get_action_name (tool_info);

  action = PIKA_ACTION (g_action_map_lookup_action (G_ACTION_MAP (pika->app), name));
  g_free (name);

  image = gtk_image_new_from_icon_name (
    pika_viewable_get_icon_name (PIKA_VIEWABLE (tool_info)),
    icon_size);
  gtk_grid_attach (grid,
                   image,
                   0, row,
                   1, 1);
  gtk_widget_show (image);

  label = gtk_label_new (label_str);
  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
  gtk_grid_attach (grid,
                   label,
                   1, row,
                   1, 1);
  gtk_widget_show (label);

  if (action)
    {
      GtkWidget *accel_label;

      accel_label = pika_accel_label_new (action);
      gtk_widget_set_margin_start (accel_label, 16);
      gtk_label_set_xalign (GTK_LABEL (accel_label), 1.0);
      gtk_grid_attach (grid,
                       accel_label,
                       2, row,
                       1, 1);
      gtk_widget_show (accel_label);
    }

  return label;
}

static gboolean
pika_tool_button_query_tooltip (GtkWidget  *widget,
                                gint        x,
                                gint        y,
                                gboolean    keyboard_mode,
                                GtkTooltip *tooltip)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (widget);

  if (! tool_button->priv->tooltip_widget)
    {
      PikaToolInfo  *tool_info;
      GtkWidget     *grid;
      GtkWidget     *label;
      gchar        **tooltip_labels;
      GtkIconSize    icon_size = GTK_ICON_SIZE_MENU;
      gint           row       = 0;

      tool_info = pika_tool_button_get_tool_info (tool_button);

      if (! tool_info)
        return FALSE;

      if (tool_button->priv->palette)
        {
          icon_size = gtk_tool_palette_get_icon_size (
            GTK_TOOL_PALETTE (tool_button->priv->palette));
        }

      grid = gtk_grid_new ();
      gtk_grid_set_column_spacing (GTK_GRID (grid), 4);
      gtk_widget_show (grid);

      tool_button->priv->tooltip_widget = g_object_ref_sink (grid);

      tooltip_labels = g_strsplit (tool_info->tooltip, ": ", 2);

      label = pika_tool_button_query_tooltip_add_tool (tool_button,
                                                       GTK_GRID (grid),
                                                       row++,
                                                       tool_info,
                                                       tooltip_labels[0],
                                                       icon_size);
      pika_label_set_attributes (GTK_LABEL (label),
                                 PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
                                 -1);

      if (tooltip_labels[0])
        {
          //gtk_table_set_row_spacing (GTK_TABLE (table), 0, 0);

          label = gtk_label_new (tooltip_labels[1]);
          gtk_label_set_xalign (GTK_LABEL (label), 0.0);
          gtk_grid_attach (GTK_GRID (grid),
                           label,
                           1, row++,
                           1, 1);
          gtk_widget_show (label);
        }

      g_strfreev (tooltip_labels);

      if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
        {
          PikaContainer *children;
          gint           n_children;

          children = pika_viewable_get_children (
            PIKA_VIEWABLE (tool_button->priv->tool_item));

          n_children = pika_container_get_n_children (children);

          if (n_children > 1)
            {
              GtkWidget *label;
              gint       i;

              label = gtk_label_new (_("Also in group:"));
              gtk_widget_set_margin_top (label, 4);
              gtk_label_set_xalign (GTK_LABEL (label), 0.0);
              pika_label_set_attributes (GTK_LABEL (label),
                                         PANGO_ATTR_WEIGHT, PANGO_WEIGHT_BOLD,
                                         -1);
              gtk_grid_attach (GTK_GRID (grid),
                               label,
                               0, row++,
                               3, 1);
              gtk_widget_show (label);

              for (i = 0; i < n_children; i++)
                {
                  PikaToolInfo *other_tool_info;

                  other_tool_info = PIKA_TOOL_INFO (
                    pika_container_get_child_by_index (children, i));

                  if (other_tool_info != tool_info)
                    {
                      pika_tool_button_query_tooltip_add_tool (
                        tool_button,
                        GTK_GRID (grid),
                        row++,
                        other_tool_info,
                        other_tool_info->label,
                        icon_size);
                    }
                }
            }
        }
    }

  gtk_tooltip_set_custom (tooltip, tool_button->priv->tooltip_widget);

  return TRUE;
}

static void
pika_tool_button_toggled (GtkToggleToolButton *toggle_tool_button)
{
  PikaToolButton *tool_button = PIKA_TOOL_BUTTON (toggle_tool_button);
  PikaContext    *context;
  PikaToolInfo   *tool_info;

  if (GTK_TOGGLE_TOOL_BUTTON_CLASS (parent_class)->toggled)
    GTK_TOGGLE_TOOL_BUTTON_CLASS (parent_class)->toggled (toggle_tool_button);

  context   = pika_toolbox_get_context (tool_button->priv->toolbox);
  tool_info = pika_tool_button_get_tool_info (tool_button);

  if (tool_info)
    {
      PikaDisplay *display;

      if (gtk_toggle_tool_button_get_active (toggle_tool_button))
        pika_context_set_tool (context, tool_info);
      else if (tool_info == pika_context_get_tool (context))
        gtk_toggle_tool_button_set_active (toggle_tool_button, TRUE);

      /* Give focus to the main image. */
      if ((display = pika_context_get_display (context)) &&
          pika_context_get_image (context))
        {
          pika_display_grab_focus (display);
        }
    }
  else
    {
      gtk_toggle_tool_button_set_active (toggle_tool_button, FALSE);
    }
}

static gboolean
pika_tool_button_button_press (GtkWidget      *widget,
                               GdkEventButton *event,
                               PikaToolButton *tool_button)
{
  if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
    {
      if (gdk_event_triggers_context_menu ((GdkEvent *) event) &&
          pika_tool_button_show_menu (tool_button, (GdkEvent *) event))
        {
          return TRUE;
        }
      else if (event->type == GDK_BUTTON_PRESS && event->button == 1 &&
               ! tool_button->priv->menu_timeout_id)
        {
          GdkEventButton *timeout_event;

          timeout_event        = (GdkEventButton *) gdk_event_copy (
            (GdkEvent *) event);
          timeout_event->time += MENU_TIMEOUT;

          tool_button->priv->menu_timeout_event = (GdkEvent *) timeout_event;

          tool_button->priv->menu_timeout_id = g_timeout_add (
            MENU_TIMEOUT,
            (GSourceFunc) pika_tool_button_menu_timeout,
            tool_button);
        }
    }

  if (event->type == GDK_2BUTTON_PRESS && event->button == 1)
    {
      PikaContext *context;
      PikaDock    *dock;

      context = pika_toolbox_get_context (tool_button->priv->toolbox);
      dock    = PIKA_DOCK (tool_button->priv->toolbox);

      pika_window_strategy_show_dockable_dialog (
        PIKA_WINDOW_STRATEGY (pika_get_window_strategy (context->pika)),
        context->pika,
        pika_dock_get_dialog_factory (dock),
        pika_widget_get_monitor (widget),
        "pika-tool-options");

      return TRUE;
    }

  return FALSE;
}

static gboolean
pika_tool_button_button_release (GtkWidget      *widget,
                                 GdkEventButton *event,
                                 PikaToolButton *tool_button)
{
  if (event->button == 1 && tool_button->priv->menu_timeout_id)
    {
      g_source_remove (tool_button->priv->menu_timeout_id);

      tool_button->priv->menu_timeout_id = 0;

      g_clear_pointer (&tool_button->priv->menu_timeout_event, gdk_event_free);
    }

  return FALSE;
}

static gboolean
pika_tool_button_scroll (GtkWidget      *widget,
                         GdkEventScroll *event,
                         PikaToolButton *tool_button)
{
  PikaToolInfo *tool_info;
  gint          delta;

  tool_info = pika_tool_button_get_tool_info (tool_button);

  switch (event->direction)
    {
    case GDK_SCROLL_UP:
      delta = -1;
      break;

    case GDK_SCROLL_DOWN:
      delta = +1;
      break;

    default:
      return FALSE;
    }

  if (tool_info && PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
    {
      PikaContainer *children;
      gint           n_children;
      gint           index;
      gint           i;

      children = pika_viewable_get_children (
        PIKA_VIEWABLE (tool_button->priv->tool_item));

      n_children = pika_container_get_n_children (children);

      index = pika_container_get_child_index (children,
                                              PIKA_OBJECT (tool_info));

      for (i = 1; i < n_children; i++)
        {
          PikaToolInfo *new_tool_info;
          gint          new_index;

          new_index = (index + i * delta) % n_children;
          if (new_index < 0) new_index += n_children;

          new_tool_info = PIKA_TOOL_INFO (
            pika_container_get_child_by_index (children, new_index));

          if (pika_tool_item_get_visible (PIKA_TOOL_ITEM (new_tool_info)))
            {
              pika_tool_group_set_active_tool_info (
                PIKA_TOOL_GROUP (tool_button->priv->tool_item), new_tool_info);

              break;
            }
        }
    }

  return FALSE;
}

static void
pika_tool_button_tool_changed (PikaContext    *context,
                               PikaToolInfo   *tool_info,
                               PikaToolButton *tool_button)
{
  pika_tool_button_update_toggled (tool_button);
}

static void
pika_tool_button_active_tool_changed (PikaToolGroup  *tool_group,
                                      PikaToolButton *tool_button)
{
  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_tool_add (PikaContainer  *container,
                           PikaToolInfo   *tool_info,
                           PikaToolButton *tool_button)
{
  gint index;

  index = pika_container_get_child_index (container, PIKA_OBJECT (tool_info));

  pika_tool_button_add_menu_item (tool_button, tool_info, index);

  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_tool_remove (PikaContainer  *container,
                              PikaToolInfo   *tool_info,
                              PikaToolButton *tool_button)
{
  pika_tool_button_remove_menu_item (tool_button, tool_info);

  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_tool_reorder (PikaContainer  *container,
                               PikaToolInfo   *tool_info,
                               gint            new_index,
                               PikaToolButton *tool_button)
{
  pika_tool_button_remove_menu_item (tool_button, tool_info);
  pika_tool_button_add_menu_item    (tool_button, tool_info, new_index);

  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_icon_size_notify (GtkToolPalette   *palette,
                                   const GParamSpec *pspec,
                                   PikaToolButton   *tool_button)
{
  pika_tool_button_reconstruct_menu (tool_button);

  pika_tool_button_update (tool_button);
}

static void
pika_tool_button_menu_item_activate (GtkMenuItem *menu_item,
                                     PikaAction  *action)
{
  pika_action_activate (action);
}

static gboolean
pika_tool_button_menu_leave_notify (GtkMenu          *menu,
                                    GdkEventCrossing *event,
                                    PikaToolButton   *tool_button)
{
  if (event->mode == GDK_CROSSING_NORMAL &&
      gtk_widget_get_visible (tool_button->priv->menu))
    {
      pika_tool_button_update_menu (tool_button);
    }

  return FALSE;
}

static gboolean
pika_tool_button_menu_timeout (PikaToolButton *tool_button)
{
  GdkEvent *event = tool_button->priv->menu_timeout_event;

  tool_button->priv->menu_timeout_id    = 0;
  tool_button->priv->menu_timeout_event = NULL;

  pika_tool_button_show_menu (tool_button, event);

  gdk_event_free (event);

  /* work around gtk not properly redrawing the button in the toggled state */
  if (gtk_toggle_tool_button_get_active (GTK_TOGGLE_TOOL_BUTTON (tool_button)))
    {
      gtk_toggle_tool_button_set_active (GTK_TOGGLE_TOOL_BUTTON (tool_button),
                                         FALSE);
    }

  return G_SOURCE_REMOVE;
}

static void
pika_tool_button_update (PikaToolButton *tool_button)
{
  PikaToolInfo *tool_info;

  tool_info = pika_tool_button_get_tool_info (tool_button);

  gtk_tool_button_set_icon_name (
    GTK_TOOL_BUTTON (tool_button),
    tool_info ? pika_viewable_get_icon_name (PIKA_VIEWABLE (tool_info)) :
                NULL);

  g_clear_object (&tool_button->priv->tooltip_widget);

  if (tool_info)
    {
      pika_help_set_help_data (GTK_WIDGET (tool_button),
                               tool_info->tooltip, tool_info->help_id);
    }
  else
    {
      pika_help_set_help_data (GTK_WIDGET (tool_button), NULL, NULL);
    }

  if (tool_info)
    {
      const gchar *tool_name = pika_object_get_name (tool_info);
      gchar       *identifier;

      if (g_str_has_prefix (tool_name, "pika-") &&
          g_str_has_suffix (tool_name, "-tool"))
        {
          /* The PikaToolInfo names are of the form "pika-pencil-tool",
           * and action names are of the form "tools-pencil".
           * To simplify things, I make the tool button identifiers the
           * same as the actions, which make them easier to find.
           */
          gchar *suffix;

          identifier = g_strdup_printf ("tools-%s", tool_name + 5);
          suffix = g_strrstr (identifier, "-tool");
          suffix[0] = '\0';
        }
      else
        {
          identifier = g_strdup (tool_name);
        }

      pika_widget_set_identifier (GTK_WIDGET (tool_button), identifier);
      g_free (identifier);
    }

  pika_tool_button_update_toggled (tool_button);
  pika_tool_button_update_menu    (tool_button);
}

static void
pika_tool_button_update_toggled (PikaToolButton *tool_button)
{
  PikaContext  *context;
  PikaToolInfo *tool_info;

  context = pika_toolbox_get_context (tool_button->priv->toolbox);

  tool_info = pika_tool_button_get_tool_info (tool_button);

  gtk_toggle_tool_button_set_active (
    GTK_TOGGLE_TOOL_BUTTON (tool_button),
    tool_info && tool_info == pika_context_get_tool (context));
}

static void
pika_tool_button_update_menu (PikaToolButton *tool_button)
{
  if (tool_button->priv->menu &&
      gtk_widget_get_visible (tool_button->priv->menu))
    {
      PikaToolInfo *tool_info = pika_tool_button_get_tool_info (tool_button);

      if (tool_info)
        {
          gtk_menu_shell_select_item (
            GTK_MENU_SHELL (tool_button->priv->menu),
            g_hash_table_lookup (tool_button->priv->menu_items, tool_info));
        }
    }
}

static void
pika_tool_button_add_menu_item (PikaToolButton *tool_button,
                                PikaToolInfo   *tool_info,
                                gint            index)
{
  PikaAction  *action;
  GtkWidget   *item;
  GtkWidget   *hbox;
  GtkWidget   *image;
  GtkWidget   *label;
  GtkIconSize  icon_size = GTK_ICON_SIZE_MENU;

  action = pika_tool_button_get_action (tool_button, tool_info);

  if (tool_button->priv->palette)
    {
      icon_size = gtk_tool_palette_get_icon_size (
        GTK_TOOL_PALETTE (tool_button->priv->palette));
    }

  item = gtk_menu_item_new ();
  gtk_menu_shell_insert (GTK_MENU_SHELL (tool_button->priv->menu), item, index);
  g_signal_connect (item, "activate",
                    G_CALLBACK (pika_tool_button_menu_item_activate),
                    action);
  pika_help_set_help_data (item, tool_info->tooltip, tool_info->help_id);

  g_object_bind_property (tool_info, "visible",
                          item,      "visible",
                          G_BINDING_SYNC_CREATE);

  g_object_set_data (G_OBJECT (item), "pika-tool-info", tool_info);

  pika_gtk_container_clear (GTK_CONTAINER (item));

  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
  gtk_container_add (GTK_CONTAINER (item), hbox);
  gtk_widget_show (hbox);

  image = gtk_image_new_from_icon_name (
    pika_viewable_get_icon_name (PIKA_VIEWABLE (tool_info)),
    icon_size);
  gtk_box_pack_start (GTK_BOX (hbox), image, FALSE, FALSE, 0);
  gtk_widget_show (image);

  label = gtk_accel_label_new (tool_info->label);
  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
  gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
  gtk_widget_show (label);

  if (action)
    {
      label = pika_accel_label_new (action);
      gtk_label_set_xalign (GTK_LABEL (label), 1.0);
      gtk_box_pack_start (GTK_BOX (hbox), label, TRUE, TRUE, 0);
      gtk_widget_show (label);

      /* TODO: while porting to GAction, this part seems useless, as the toolbox
       * is perfectly working. But maybe I'm missing something. So leaving this
       * here for further investigation.
       * We probably want to check what pika_ui_manager_connect_proxy() does
       * exactly.
       */
#if 0
      if (ui_manager)
        {
          g_signal_emit_by_name (ui_manager, "connect-proxy",
                                 action, item);
        }
#endif
    }

  g_hash_table_insert (tool_button->priv->menu_items, tool_info, item);
}

static void
pika_tool_button_remove_menu_item (PikaToolButton *tool_button,
                                   PikaToolInfo   *tool_info)
{
  GtkWidget *item;

  item = g_hash_table_lookup (tool_button->priv->menu_items, tool_info);

  gtk_container_remove (GTK_CONTAINER (tool_button->priv->menu), item);

  g_hash_table_remove (tool_button->priv->menu_items, tool_info);
}

static void
pika_tool_button_reconstruct_menu_add_menu_item (PikaToolInfo   *tool_info,
                                                 PikaToolButton *tool_button)
{
  pika_tool_button_add_menu_item (tool_button, tool_info, -1);
}

static void
pika_tool_button_reconstruct_menu (PikaToolButton *tool_button)
{
  pika_tool_button_destroy_menu (tool_button);

  if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
    {
      PikaContainer *children;

      children = pika_viewable_get_children (
        PIKA_VIEWABLE (tool_button->priv->tool_item));

      tool_button->priv->menu = gtk_menu_new ();
      gtk_menu_attach_to_widget (GTK_MENU (tool_button->priv->menu),
                                 GTK_WIDGET (tool_button), NULL);

      g_signal_connect (tool_button->priv->menu, "leave-notify-event",
                        G_CALLBACK (pika_tool_button_menu_leave_notify),
                        tool_button);

      tool_button->priv->menu_items = g_hash_table_new (g_direct_hash,
                                                        g_direct_equal);

      pika_container_foreach (
        children,
        (GFunc) pika_tool_button_reconstruct_menu_add_menu_item,
        tool_button);
    }
}

static void
pika_tool_button_destroy_menu (PikaToolButton *tool_button)
{
  if (tool_button->priv->menu)
    {
      gtk_menu_detach (GTK_MENU (tool_button->priv->menu));
      tool_button->priv->menu = NULL;

      g_clear_pointer (&tool_button->priv->menu_items, g_hash_table_unref);

      if (tool_button->priv->menu_timeout_id)
        {
          g_source_remove (tool_button->priv->menu_timeout_id);

          tool_button->priv->menu_timeout_id = 0;

          g_clear_pointer (&tool_button->priv->menu_timeout_event,
                           gdk_event_free);
        }
    }
}

static gboolean
pika_tool_button_show_menu (PikaToolButton *tool_button,
                            const GdkEvent *trigger_event)
{
  if (! tool_button->priv->menu)
    return FALSE;

  gtk_menu_popup_at_widget (
    GTK_MENU (tool_button->priv->menu),
    GTK_WIDGET (tool_button),
    GDK_GRAVITY_NORTH_EAST,
    GDK_GRAVITY_NORTH_WEST,
    trigger_event);

  pika_tool_button_update_menu (tool_button);

  return TRUE;
}

static PikaAction *
pika_tool_button_get_action (PikaToolButton *tool_button,
                             PikaToolInfo   *tool_info)
{
  PikaAction *action = NULL;

  if (tool_info)
    {
      PikaContext *context;
      Pika        *pika;
      gchar       *name;

      context = pika_toolbox_get_context (tool_button->priv->toolbox);
      pika    = context->pika;
      name    = pika_tool_info_get_action_name (tool_info);

      action = PIKA_ACTION (g_action_map_lookup_action (G_ACTION_MAP (pika->app), name));

      g_free (name);
    }

  return action;
}


/*  public functions  */

GtkToolItem *
pika_tool_button_new (PikaToolbox  *toolbox,
                      PikaToolItem *tool_item)
{
  g_return_val_if_fail (PIKA_IS_TOOLBOX (toolbox), NULL);
  g_return_val_if_fail (tool_item == NULL ||
                        PIKA_IS_TOOL_ITEM (tool_item), NULL);

  return g_object_new (PIKA_TYPE_TOOL_BUTTON,
                       "toolbox",   toolbox,
                       "tool-item", tool_item,
                       NULL);
}

PikaToolbox *
pika_tool_button_get_toolbox (PikaToolButton *tool_button)
{
  g_return_val_if_fail (PIKA_IS_TOOL_BUTTON (tool_button), NULL);

  return tool_button->priv->toolbox;
}

void
pika_tool_button_set_tool_item (PikaToolButton *tool_button,
                                PikaToolItem   *tool_item)
{
  g_return_if_fail (PIKA_IS_TOOL_BUTTON (tool_button));
  g_return_if_fail (tool_item == NULL || PIKA_IS_TOOL_ITEM (tool_item));

  if (tool_item != tool_button->priv->tool_item)
    {
      if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
        {
          PikaContainer *children;

          children = pika_viewable_get_children (
            PIKA_VIEWABLE (tool_button->priv->tool_item));

          g_signal_handlers_disconnect_by_func (
            tool_button->priv->tool_item,
            pika_tool_button_active_tool_changed,
            tool_button);

          g_signal_handlers_disconnect_by_func (
            children,
            pika_tool_button_tool_add,
            tool_button);
          g_signal_handlers_disconnect_by_func (
            children,
            pika_tool_button_tool_remove,
            tool_button);
          g_signal_handlers_disconnect_by_func (
            children,
            pika_tool_button_tool_reorder,
            tool_button);

          pika_tool_button_destroy_menu (tool_button);
        }

      g_set_object (&tool_button->priv->tool_item, tool_item);

      if (PIKA_IS_TOOL_GROUP (tool_button->priv->tool_item))
        {
          PikaContainer *children;

          children = pika_viewable_get_children (
            PIKA_VIEWABLE (tool_button->priv->tool_item));

          g_signal_connect (
            tool_button->priv->tool_item, "active-tool-changed",
            G_CALLBACK (pika_tool_button_active_tool_changed),
            tool_button);

          g_signal_connect (
            children, "add",
            G_CALLBACK (pika_tool_button_tool_add),
            tool_button);
          g_signal_connect (
            children, "remove",
            G_CALLBACK (pika_tool_button_tool_remove),
            tool_button);
          g_signal_connect (
            children, "reorder",
            G_CALLBACK (pika_tool_button_tool_reorder),
            tool_button);

          pika_tool_button_reconstruct_menu (tool_button);
        }

      pika_tool_button_update (tool_button);

      g_object_notify (G_OBJECT (tool_button), "tool-item");
    }
}

PikaToolItem *
pika_tool_button_get_tool_item (PikaToolButton *tool_button)
{
  g_return_val_if_fail (PIKA_IS_TOOL_BUTTON (tool_button), NULL);

  return tool_button->priv->tool_item;
}

PikaToolInfo *
pika_tool_button_get_tool_info (PikaToolButton *tool_button)
{
  g_return_val_if_fail (PIKA_IS_TOOL_BUTTON (tool_button), NULL);

  if (tool_button->priv->tool_item)
    {
      if (PIKA_IS_TOOL_INFO (tool_button->priv->tool_item))
        {
          return PIKA_TOOL_INFO (tool_button->priv->tool_item);
        }
      else
        {
          return pika_tool_group_get_active_tool_info (
            PIKA_TOOL_GROUP (tool_button->priv->tool_item));
        }
    }

  return NULL;
}
