/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimppopup.c
 * Copyright (C) 2003-2014 Michael Natterer <mitch@gimp.org>
 *
 * 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 <gdk/gdkkeysyms.h>

#include "libgimpwidgets/gimpwidgets.h"

#include "widgets-types.h"

#include "gimppopup.h"


enum
{
  CANCEL,
  CONFIRM,
  LAST_SIGNAL
};

typedef struct _GimpPopupPrivate
{
  GtkWidget *parent;
} GimpPopupPrivate;


static gboolean gimp_popup_map_event    (GtkWidget      *widget,
                                         GdkEventAny    *event);
static gboolean gimp_popup_button_press (GtkWidget      *widget,
                                         GdkEventButton *bevent);
static gboolean gimp_popup_key_press    (GtkWidget      *widget,
                                         GdkEventKey    *kevent);

static void     gimp_popup_real_cancel  (GimpPopup      *popup);
static void     gimp_popup_real_confirm (GimpPopup      *popup);


G_DEFINE_TYPE_WITH_PRIVATE (GimpPopup, gimp_popup, GTK_TYPE_WINDOW)

#define parent_class gimp_popup_parent_class

static guint popup_signals[LAST_SIGNAL];


static void
gimp_popup_class_init (GimpPopupClass *klass)
{
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GtkBindingSet  *binding_set;

  popup_signals[CANCEL] =
    g_signal_new ("cancel",
                  G_OBJECT_CLASS_TYPE (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (GimpPopupClass, cancel),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  popup_signals[CONFIRM] =
    g_signal_new ("confirm",
                  G_OBJECT_CLASS_TYPE (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (GimpPopupClass, confirm),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 0);

  widget_class->map_event          = gimp_popup_map_event;
  widget_class->button_press_event = gimp_popup_button_press;
  widget_class->key_press_event    = gimp_popup_key_press;

  klass->cancel                    = gimp_popup_real_cancel;
  klass->confirm                   = gimp_popup_real_confirm;

  binding_set = gtk_binding_set_by_class (klass);

  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0,
                                "cancel", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return, 0,
                                "confirm", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Enter, 0,
                                "confirm", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_ISO_Enter, 0,
                                "confirm", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, 0,
                                "confirm", 0);
  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, 0,
                                "confirm", 0);
}

static void
gimp_popup_init (GimpPopup *popup)
{
}

static void
gimp_popup_grab_notify (GtkWidget *widget,
                        gboolean   was_grabbed)
{
  GtkWidget *grab_widget;
  GtkWidget *iter;

  if (was_grabbed)
    return;

  grab_widget = gtk_grab_get_current ();

  /* ignore grabs on one of our children, like a scrollbar */
  if (gtk_widget_is_ancestor (grab_widget, widget))
    return;

  /* ignore grabs from windows / menus shown by
   * internal widgets */
  iter = gtk_widget_get_toplevel (grab_widget);
  while (iter)
    {
      if (iter == widget)
        return;

      if (GTK_IS_WINDOW (iter))
        iter = (GtkWidget*) gtk_window_get_transient_for ((GtkWindow*) iter);
      else
        break;
    }

  g_signal_emit (widget, popup_signals[CANCEL], 0);
}

static gboolean
gimp_popup_grab_broken_event (GtkWidget          *widget,
                              GdkEventGrabBroken *event)
{
  gimp_popup_grab_notify (widget, FALSE);

  return FALSE;
}

static gboolean
gimp_popup_map_event (GtkWidget   *widget,
                      GdkEventAny *event)
{
  GdkDisplay *display = gtk_widget_get_display (widget);

  GTK_WIDGET_CLASS (parent_class)->map_event (widget, event);

  /*  grab with owner_events == TRUE so the popup's widgets can
   *  receive events. we filter away events outside this toplevel
   *  away in button_press()
   */
  if (gdk_seat_grab (gdk_display_get_default_seat (display),
                     gtk_widget_get_window (widget),
                     GDK_SEAT_CAPABILITY_ALL,
                     TRUE,
                     NULL,
                     (GdkEvent *) event,
                     NULL, NULL) == GDK_GRAB_SUCCESS)
    {
      gtk_grab_add (widget);

      g_signal_connect (widget, "grab-notify",
                        G_CALLBACK (gimp_popup_grab_notify),
                        widget);
      g_signal_connect (widget, "grab-broken-event",
                        G_CALLBACK (gimp_popup_grab_broken_event),
                        widget);

      return FALSE;
    }

  /*  if we could not grab, destroy the popup instead of leaving it
   *  around uncloseable.
   */
  g_signal_emit (widget, popup_signals[CANCEL], 0);

  return FALSE;
}

static gboolean
gimp_popup_button_press (GtkWidget      *widget,
                         GdkEventButton *bevent)
{
  GtkWidget *event_widget;
  gboolean   cancel  = FALSE;
  gboolean   confirm = FALSE;

  event_widget = gtk_get_event_widget ((GdkEvent *) bevent);

  if (event_widget == widget)
    {
      GtkAllocation allocation;

      gtk_widget_get_allocation (widget, &allocation);

      /*  the event was on the popup, which can either be really on the
       *  popup or outside gimp (owner_events == TRUE, see map())
       */
      if (bevent->x < 0                ||
          bevent->y < 0                ||
          bevent->x > allocation.width ||
          bevent->y > allocation.height)
        {
          /*  the event was outsde gimp  */

          cancel = TRUE;
        }
    }
  else if (gtk_widget_get_toplevel (event_widget) != widget)
    {
      GimpPopupPrivate *priv;
      /*  the event was on a gimp widget, but not inside the popup  */

      priv = gimp_popup_get_instance_private (GIMP_POPUP (widget));
      if (event_widget == priv->parent || gtk_widget_is_ancestor (event_widget, priv->parent))
        confirm = TRUE;
      else
        cancel = TRUE;
    }

  if (cancel)
    g_signal_emit (widget, popup_signals[CANCEL], 0);
  else if (confirm)
    g_signal_emit (widget, popup_signals[CONFIRM], 0);

  return (cancel || confirm);
}

static gboolean
gimp_popup_key_press (GtkWidget   *widget,
                      GdkEventKey *kevent)
{
  GtkWidget *focus            = gtk_window_get_focus (GTK_WINDOW (widget));
  gboolean   activate_binding = TRUE;

  if (focus &&
      (GTK_IS_EDITABLE (focus) ||
       GTK_IS_TEXT_VIEW (focus)) &&
      (kevent->keyval == GDK_KEY_space ||
       kevent->keyval == GDK_KEY_KP_Space))
    {
      /*  if a text widget has the focus, and the key was space,
       *  don't manually activate the binding to allow entering the
       *  space in the focus widget.
       */
      activate_binding = FALSE;
    }

  if (activate_binding)
    {
      GtkBindingSet *binding_set;

      binding_set = gtk_binding_set_by_class (g_type_class_peek (GIMP_TYPE_POPUP));

      /*  invoke the popup's binding entries manually, because
       *  otherwise the focus widget (GtkTreeView e.g.) would consume
       *  it
       */
      if (gtk_binding_set_activate (binding_set,
                                    kevent->keyval,
                                    kevent->state,
                                    G_OBJECT (widget)))
        {
          return TRUE;
        }
    }

  return GTK_WIDGET_CLASS (parent_class)->key_press_event (widget, kevent);
}

static void
gimp_popup_real_cancel (GimpPopup *popup)
{
  GtkWidget *widget = GTK_WIDGET (popup);

  if (gtk_grab_get_current () == widget)
    gtk_grab_remove (widget);

  gtk_widget_destroy (widget);
}

static void
gimp_popup_real_confirm (GimpPopup *popup)
{
  GtkWidget *widget = GTK_WIDGET (popup);

  if (gtk_grab_get_current () == widget)
    gtk_grab_remove (widget);

  gtk_widget_destroy (widget);
}

/**
 * gimp_popup_show:
 * @popup:
 * @widget: the parent widget which will determine @popup location.
 *
 * Shows @popup above @widget. Its placement will be determined relatively to
 * @widget.
 * Moreover clicking outside @popup but within @widget will be considered a
 * "confirm" signal. Typically @widget would be a button. A typical UX could be:
 * clicking it once would show the popup; clicking it again would confirm the
 * choice within the popup.
 */
void
gimp_popup_show (GimpPopup *popup,
                 GtkWidget *widget)
{
  GimpPopupPrivate *priv;
  GdkDisplay       *display;
  GdkMonitor       *monitor;
  GtkRequisition    requisition;
  GtkAllocation     allocation;
  GdkRectangle      rect;
  gint              orig_x;
  gint              orig_y;
  gint              x;
  gint              y;

  g_return_if_fail (GIMP_IS_POPUP (popup));
  g_return_if_fail (GTK_IS_WIDGET (widget));

  priv = gimp_popup_get_instance_private (popup);
  priv->parent = widget;

  gtk_widget_get_preferred_size (GTK_WIDGET (popup), &requisition, NULL);

  gtk_widget_get_allocation (widget, &allocation);
  gdk_window_get_origin (gtk_widget_get_window (widget), &orig_x, &orig_y);

  if (! gtk_widget_get_has_window (widget))
    {
      orig_x += allocation.x;
      orig_y += allocation.y;
    }

  display = gtk_widget_get_display (widget);

  monitor = gdk_display_get_monitor_at_point (display, orig_x, orig_y);
  gdk_monitor_get_workarea (monitor, &rect);

  if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
    {
      x = orig_x + allocation.width - requisition.width;

      if (x < rect.x)
        x -= allocation.width - requisition.width;
    }
  else
    {
      x = orig_x;

      if (x + requisition.width > rect.x + rect.width)
        x += allocation.width - requisition.width;
    }

  y = orig_y + allocation.height;

  if (y + requisition.height > rect.y + rect.height)
    y = orig_y - requisition.height;

  gtk_window_set_screen (GTK_WINDOW (popup), gtk_widget_get_screen (widget));
  gtk_window_set_transient_for (GTK_WINDOW (popup),
                                GTK_WINDOW (gtk_widget_get_toplevel (widget)));

  gtk_window_move (GTK_WINDOW (popup), x, y);
  gtk_widget_show (GTK_WIDGET (popup));
}
