/* gtkpopoverbin.c: A single-child container with a popover
 *
 * SPDX-FileCopyrightText: 2025  Emmanuele Bassi
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include "gtkpopoverbin.h"

#include "gtkbinlayout.h"
#include "gtkbuildable.h"
#include "gtkbuilderprivate.h"
#include "gtkpopovermenu.h"
#include "gtkprivate.h"
#include "gtkwidgetprivate.h"
#include "gtkgestureclick.h"
#include "gtkgesturelongpress.h"
#include "gtkshortcutcontroller.h"
#include "gtkshortcutaction.h"
#include "gtkshortcuttrigger.h"

/**
 * GtkPopoverBin:
 *
 * A single child container with a popover.
 *
 * You should use `GtkPopoverBin` whenever you need to present a [class@Gtk.Popover]
 * to the user.
 *
 * ## Actions
 *
 * `GtkPopoverBin` defines the `menu.popup` action, which can be activated
 * to present the popover to the user.
 *
 * ## CSS nodes
 *
 * `GtkPopoverBin` has a single CSS node with the name `popoverbin`.
 *
 * Since: 4.22
 */

struct _GtkPopoverBin
{
  GtkWidget parent_instance;

  GtkWidget *child;
  GtkWidget *popover;

  GMenuModel *menu_model;

  gboolean handle_input;
  GtkEventController *click_gesture;
  GtkEventController *long_press_gesture;
  GtkEventController *shortcut_controller;
};

enum
{
  PROP_CHILD = 1,
  PROP_POPOVER,
  PROP_MENU_MODEL,
  PROP_HANDLE_INPUT,
  N_PROPS
};

static GParamSpec *obj_props[N_PROPS];

static GtkBuildableIface *parent_buildable_iface;

static void gtk_popover_bin_buildable_iface_init (GtkBuildableIface *iface);

G_DEFINE_FINAL_TYPE_WITH_CODE (GtkPopoverBin, gtk_popover_bin, GTK_TYPE_WIDGET,
                               G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
                                                      gtk_popover_bin_buildable_iface_init))

static void
gtk_popover_bin_buildable_add_child (GtkBuildable *buildable,
                                     GtkBuilder   *builder,
                                     GObject      *child,
                                     const char   *type)
{
  if (GTK_IS_WIDGET (child))
    {
      if (GTK_IS_POPOVER (child))
        {
          gtk_buildable_child_deprecation_warning (buildable, builder, NULL, "popover");
          gtk_popover_bin_set_popover (GTK_POPOVER_BIN (buildable), GTK_WIDGET (child));
        }
      else
        {
          gtk_buildable_child_deprecation_warning (buildable, builder, NULL, "child");
          gtk_popover_bin_set_child (GTK_POPOVER_BIN (buildable), GTK_WIDGET (child));
        }
    }
  else
    {
      parent_buildable_iface->add_child (buildable, builder, child, type);
    }
}

static void
gtk_popover_bin_buildable_iface_init (GtkBuildableIface *iface)
{
  parent_buildable_iface = g_type_interface_peek_parent (iface);

  iface->add_child = gtk_popover_bin_buildable_add_child;
}

static void
on_popover_destroy (GtkPopoverBin *self)
{
  gtk_popover_bin_set_popover (self, NULL);
}

static void
on_popover_map (GtkPopoverBin *self)
{
  gtk_widget_add_css_class (self->child, "has-open-popup");
}

static void
on_popover_unmap (GtkPopoverBin *self)
{
  gtk_widget_remove_css_class (self->child, "has-open-popup");
}

static void
gtk_popover_bin_popup_at_position (GtkPopoverBin   *self,
                                   gdouble          x,
                                   gdouble          y)
{
  GdkRectangle rect;

  if (self->popover == NULL)
    return;

  if (x > -0.5 && y > -0.5) {
    rect.x = x;
    rect.y = y;
  } else {
    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
      rect.x = gtk_widget_get_width (GTK_WIDGET (self));
    else
      rect.x = 0.0;

    rect.y = gtk_widget_get_height (GTK_WIDGET (self));
  }

  rect.width = 0.0;
  rect.height = 0.0;

  gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), &rect);

  gtk_popover_popup (GTK_POPOVER (self->popover));
}

static void
pressed_cb (GtkPopoverBin *self,
            int            n_press,
            double         x,
            double         y,
            GtkGesture    *gesture)
{
  GdkEventSequence *current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
  GdkEvent *event = gtk_gesture_get_last_event (gesture, current);

  if (gdk_event_triggers_context_menu (event))
    {
      gtk_popover_bin_popup_at_position (self, x, y);
      gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
      gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));

      return;
    }

  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
}

static void
long_pressed_cb (GtkPopoverBin *self,
                 double         x,
                 double         y,
                 GtkGesture    *gesture)
{
  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
  gtk_popover_bin_popup_at_position (self, x, y);
}

static void
popup_action (GtkWidget  *widget,
              const char *action_name,
              GVariant   *parameters)
{
  GtkPopoverBin *self = GTK_POPOVER_BIN (widget);

  gtk_popover_bin_popup (self);
}

static void
gtk_popover_bin_dispose (GObject *gobject)
{
  GtkPopoverBin *self = GTK_POPOVER_BIN (gobject);

  if (self->popover != NULL)
    {
      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_destroy,
                                            self);
      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_map,
                                            self);
      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_unmap,
                                            self);
    }

  g_clear_pointer (&self->popover, gtk_widget_unparent);
  g_clear_pointer (&self->child, gtk_widget_unparent);

  g_clear_object (&self->menu_model);

  G_OBJECT_CLASS (gtk_popover_bin_parent_class)->dispose (gobject);
}

static void
gtk_popover_bin_set_property (GObject *gobject,
                              unsigned int prop_id,
                              const GValue *value,
                              GParamSpec *pspec)
{
  GtkPopoverBin *self = GTK_POPOVER_BIN (gobject);

  switch (prop_id)
    {
      case PROP_MENU_MODEL:
        gtk_popover_bin_set_menu_model (self, g_value_get_object (value));
        break;

      case PROP_POPOVER:
        gtk_popover_bin_set_popover (self, g_value_get_object (value));
        break;

      case PROP_CHILD:
        gtk_popover_bin_set_child (self, g_value_get_object (value));
        break;

      case PROP_HANDLE_INPUT:
        gtk_popover_bin_set_handle_input (self, g_value_get_boolean (value));
        break;

      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
gtk_popover_bin_get_property (GObject *gobject,
                              unsigned int prop_id,
                              GValue *value,
                              GParamSpec *pspec)
{
  GtkPopoverBin *self = GTK_POPOVER_BIN (gobject);

  switch (prop_id)
    {
      case PROP_MENU_MODEL:
        g_value_set_object (value, self->menu_model);
        break;

      case PROP_POPOVER:
        g_value_set_object (value, self->popover);
        break;

      case PROP_CHILD:
        g_value_set_object (value, self->child);
        break;

      case PROP_HANDLE_INPUT:
        g_value_set_boolean (value, self->handle_input);
        break;

      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
gtk_popover_bin_compute_expand (GtkWidget *widget,
                                gboolean *hexpand_p,
                                gboolean *vexpand_p)
{
  GtkPopoverBin *self = GTK_POPOVER_BIN (widget);
  gboolean hexpand = FALSE, vexpand = FALSE;

  if (self->child != NULL)
    {
      hexpand = gtk_widget_compute_expand (self->child, GTK_ORIENTATION_HORIZONTAL);
      vexpand = gtk_widget_compute_expand (self->child, GTK_ORIENTATION_VERTICAL);
    }

  if (hexpand_p != NULL)
    *hexpand_p = hexpand;
  if (vexpand_p != NULL)
    *vexpand_p = vexpand;
}

static void
gtk_popover_bin_class_init (GtkPopoverBinClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  gobject_class->set_property = gtk_popover_bin_set_property;
  gobject_class->get_property = gtk_popover_bin_get_property;
  gobject_class->dispose = gtk_popover_bin_dispose;

  widget_class->focus = gtk_widget_focus_child;
  widget_class->grab_focus = gtk_widget_grab_focus_child;
  widget_class->compute_expand = gtk_popover_bin_compute_expand;

  /**
   * GtkPopoverBin:menu-model:
   *
   * The `GMenuModel` from which the popup will be created.
   *
   * See [method@Gtk.PopoverBin.set_menu_model] for the interaction
   * with the [property@Gtk.PopoverBin:popover] property.
   *
   * Since: 4.22
   */
  obj_props[PROP_MENU_MODEL] =
      g_param_spec_object ("menu-model", NULL, NULL,
                           G_TYPE_MENU_MODEL,
                           GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkPopoverBin:popover:
   *
   * The `GtkPopover` that will be popped up when calling
   * [method@Gtk.PopoverBin.popup].
   *
   * Since: 4.22
   */
  obj_props[PROP_POPOVER] =
      g_param_spec_object ("popover", NULL, NULL,
                           GTK_TYPE_POPOVER,
                           GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkPopoverBin:child:
   *
   * The child widget of the popover bin.
   *
   * Since: 4.22
   */
  obj_props[PROP_CHILD] =
      g_param_spec_object ("child", NULL, NULL,
                           GTK_TYPE_WIDGET,
                           GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  /**
   * GtkPopoverBin:handle-input:
   *
   * Whether the popover bin will handle input
   * to trigger the popup.
   *
   * Since: 4.22
   */
  obj_props[PROP_HANDLE_INPUT] =
      g_param_spec_boolean ("handle-input", NULL, NULL,
                            FALSE,
                            GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (gobject_class, N_PROPS, obj_props);

  gtk_widget_class_install_action (widget_class, "menu.popup", NULL, popup_action);

  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);

  gtk_widget_class_set_css_name (widget_class, "popoverbin");
}

static void
gtk_popover_bin_init (GtkPopoverBin *self)
{
}

/**
 * gtk_popover_bin_new:
 *
 * Creates a new popover bin widget.
 *
 * Returns: (transfer floating): the newly created popover bin
 *
 * Since: 4.22
 */
GtkWidget *
gtk_popover_bin_new (void)
{
  return g_object_new (GTK_TYPE_POPOVER_BIN, NULL);
}

/**
 * gtk_popover_bin_set_child:
 * @self: a popover bin
 * @child: (nullable): the child of the popover bin
 *
 * Sets the child of the popover bin.
 *
 * Since: 4.22
 */
void
gtk_popover_bin_set_child (GtkPopoverBin *self,
                           GtkWidget     *child)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));
  g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));

  if (self->child == child)
    return;

  g_clear_pointer (&self->child, gtk_widget_unparent);

  if (child != NULL)
    {
      gtk_widget_set_parent (child, GTK_WIDGET (self));
      self->child = child;
    }

  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_CHILD]);
}

/**
 * gtk_popover_bin_get_child:
 * @self: a popover bin
 *
 * Retrieves the child widget of the popover bin.
 *
 * Returns: (transfer none) (nullable): the child widget
 *
 * Since: 4.22
 */
GtkWidget *
gtk_popover_bin_get_child (GtkPopoverBin *self)
{
  g_return_val_if_fail (GTK_IS_POPOVER_BIN (self), NULL);

  return self->child;
}

/**
 * gtk_popover_bin_set_menu_model:
 * @self: a popover bin
 * @model: (nullable): a menu model
 *
 * Sets the menu model used to create the popover that will be
 * presented when calling [method@Gtk.PopoverBin.popup].
 *
 * If @model is `NULL`, the popover will be unset.
 *
 * A [class@Gtk.Popover] will be created from the menu model with
 * [ctor@Gtk.PopoverMenu.new_from_model]. Actions will be connected
 * as documented for this function.
 *
 * If [property@Gtk.PopoverBin:popover] is already set, it will be
 * dissociated from the popover bin, and the property is set to `NULL`.
 *
 * See: [method@Gtk.PopoverBin.set_popover]
 *
 * Since: 4.22
 */
void
gtk_popover_bin_set_menu_model (GtkPopoverBin *self,
                                GMenuModel    *model)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));
  g_return_if_fail (model == NULL || G_IS_MENU_MODEL (model));

  if (self->menu_model == model)
    return;

  g_object_freeze_notify (G_OBJECT (self));

  if (model != NULL)
    g_object_ref (model);

  if (model != NULL)
    {
      GtkWidget *popover;

      popover = gtk_popover_menu_new_from_model (model);
      gtk_popover_set_position (GTK_POPOVER (popover), GTK_POS_BOTTOM);
      gtk_popover_bin_set_popover (self, popover);
    }
  else
    {
      gtk_popover_bin_set_popover (self, NULL);
    }

  self->menu_model = model;
  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_MENU_MODEL]);

  g_object_thaw_notify (G_OBJECT (self));
}

/**
 * gtk_popover_bin_get_menu_model:
 * @self: a popover bin
 *
 * Retrieves the menu model set using [method@Gtk.PopoverBin.set_menu_model].
 *
 * Returns: (transfer none) (nullable): the menu model for the popover
 *
 * Since: 4.22
 */
GMenuModel *
gtk_popover_bin_get_menu_model (GtkPopoverBin *self)
{
  g_return_val_if_fail (GTK_IS_POPOVER_BIN (self), NULL);

  return self->menu_model;
}

/**
 * gtk_popover_bin_set_popover:
 * @self: a popover bin
 * @popover: (nullable) (type Gtk.Popover): a `GtkPopover`
 *
 * Sets the `GtkPopover` that will be presented when calling
 * [method@Gtk.PopoverBin.popup].
 *
 * If @popover is `NULL`, the popover will be unset.
 *
 * If [property@Gtk.PopoverBin:menu-model] is set before calling
 * this function, then the menu model property will be unset.
 *
 * See: [method@Gtk.PopoverBin.set_menu_model]
 *
 * Since: 4.22
 */
void
gtk_popover_bin_set_popover (GtkPopoverBin *self,
                             GtkWidget     *popover)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));
  g_return_if_fail (popover == NULL || GTK_IS_POPOVER (popover));

  g_object_freeze_notify (G_OBJECT (self));

  g_clear_object (&self->menu_model);

  if (self->popover != NULL)
    {
      gtk_widget_set_visible (self->popover, FALSE);

      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_destroy,
                                            self);
      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_map,
                                            self);
      g_signal_handlers_disconnect_by_func (self->popover,
                                            on_popover_unmap,
                                            self);

      gtk_widget_unparent (self->popover);
    }

  self->popover = popover;

  if (popover != NULL)
    {
      gtk_widget_set_parent (self->popover, GTK_WIDGET (self));
      g_signal_connect_swapped (self->popover, "destroy",
                                G_CALLBACK (on_popover_destroy),
                                self);
      g_signal_connect_swapped (self->popover, "map",
                                G_CALLBACK (on_popover_map),
                                self);
      g_signal_connect_swapped (self->popover, "unmap",
                                G_CALLBACK (on_popover_unmap),
                                self);
    }

  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_POPOVER]);
  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_MENU_MODEL]);

  g_object_thaw_notify (G_OBJECT (self));
}

/**
 * gtk_popover_bin_get_popover:
 * @self: a popover bin
 *
 * Retrieves the `GtkPopover` set using [method@Gtk.PopoverBin.set_popover].
 *
 * Returns: (transfer none) (nullable) (type Gtk.Popover): a popover widget
 *
 * Since: 4.22
 */
GtkWidget *
gtk_popover_bin_get_popover (GtkPopoverBin *self)
{
  g_return_val_if_fail (GTK_IS_POPOVER_BIN (self), NULL);

  return self->popover;
}

/**
 * gtk_popover_bin_set_handle_input:
 * @self: a popover bin
 * @handle_input: whether to handle input
 *
 * Enables or disables input handling.
 *
 * If enabled, the popover bin will pop up the
 * popover on right-click or long press, as expected
 * for a context menu.
 *
 * Since: 4.22
 */
void
gtk_popover_bin_set_handle_input (GtkPopoverBin *self,
                                  gboolean       handle_input)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));

  if (self->handle_input == handle_input)
    return;

  self->handle_input = handle_input;

  if (handle_input)
    {
      GtkShortcutTrigger *trigger;
      GtkShortcutAction *action;

      self->click_gesture = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
      gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->click_gesture), 0);
      gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (self->click_gesture), TRUE);
      g_signal_connect_swapped (self->click_gesture, "pressed",
                                G_CALLBACK (pressed_cb), self);
      gtk_widget_add_controller (GTK_WIDGET (self), self->click_gesture);

      self->long_press_gesture = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ());
      gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (self->long_press_gesture), TRUE);
      gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->long_press_gesture), TRUE);
      g_signal_connect_swapped (self->long_press_gesture, "pressed", G_CALLBACK (long_pressed_cb), self);
      gtk_widget_add_controller (GTK_WIDGET (self), self->long_press_gesture);

      self->shortcut_controller = GTK_EVENT_CONTROLLER (gtk_shortcut_controller_new ());
      trigger = gtk_alternative_trigger_new (
                  gtk_keyval_trigger_new (GDK_KEY_Menu, GDK_NO_MODIFIER_MASK),
                  gtk_keyval_trigger_new (GDK_KEY_F10, GDK_SHIFT_MASK));
      action = gtk_named_action_new ("menu.popup");
      gtk_shortcut_controller_add_shortcut (GTK_SHORTCUT_CONTROLLER (self->shortcut_controller),
                                            gtk_shortcut_new (trigger, action));

      gtk_widget_add_controller (GTK_WIDGET (self), self->shortcut_controller);
    }
  else
    {
      gtk_widget_remove_controller (GTK_WIDGET (self), self->click_gesture);
      self->click_gesture = NULL;
      gtk_widget_remove_controller (GTK_WIDGET (self), self->long_press_gesture);
      self->long_press_gesture = NULL;
      gtk_widget_remove_controller (GTK_WIDGET (self), self->shortcut_controller);
      self->shortcut_controller = NULL;
    }

  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_HANDLE_INPUT]);
}

gboolean
gtk_popover_bin_get_handle_input (GtkPopoverBin *self)
{
  g_return_val_if_fail (GTK_IS_POPOVER_BIN (self), FALSE);

  return self->handle_input;
}

/**
 * gtk_popover_bin_popup:
 * @self: a popover bin
 *
 * Presents the popover to the user.
 *
 * Use [method@Gtk.PopoverBin.set_popover] or
 * [method@Gtk.PopoverBin.set_menu_model] to define the popover.
 *
 * See: [method@Gtk.PopoverBin.popdown]
 *
 * Since: 4.22
 */
void
gtk_popover_bin_popup (GtkPopoverBin *self)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));

  if (self->popover == NULL)
    return;

  gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), NULL);
  gtk_popover_popup (GTK_POPOVER (self->popover));
}

/**
 * gtk_popover_bin_popdown:
 * @self: a popover bin
 *
 * Hides the popover from the user.
 *
 * See: [method@Gtk.PopoverBin.popup]
 *
 * Since: 4.22
 */
void
gtk_popover_bin_popdown (GtkPopoverBin *self)
{
  g_return_if_fail (GTK_IS_POPOVER_BIN (self));

  if (self->popover != NULL)
    gtk_popover_popdown (GTK_POPOVER (self->popover));
}
