Blob Blame History Raw
/* dzl-suggestion-entry.c
 *
 * Copyright (C) 2017 Christian Hergert <chergert@redhat.com>
 *
 * This file is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This file 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 Lesser 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 <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "dzl-suggestion-entry"

#include "config.h"

#include <glib/gi18n.h>

#include "suggestions/dzl-suggestion.h"
#include "suggestions/dzl-suggestion-entry.h"
#include "suggestions/dzl-suggestion-entry-buffer.h"
#include "suggestions/dzl-suggestion-popover.h"
#include "suggestions/dzl-suggestion-private.h"
#include "util/dzl-util-private.h"

#if 0
# define _TRACE_LEVEL (1<<G_LOG_LEVEL_USER_SHIFT)
# define _TRACE(...) do { g_log(G_LOG_DOMAIN, _TRACE_LEVEL, __VA_ARGS__); } while (0)
# define DZL_TRACE_MSG(m,...) _TRACE("   MSG: %s():%u: "m, G_STRFUNC, __LINE__, __VA_ARGS__)
# define DZL_ENTRY _TRACE(" ENTRY: %s(): %u", G_STRFUNC, __LINE__)
# define DZL_EXIT do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return; } while (0)
# define DZL_RETURN(r) do { _TRACE("  EXIT: %s(): %u", G_STRFUNC, __LINE__); return (r); } while (0)
# define DZL_GOTO(_l) do { _TRACE("  GOTO: %s(): %u: %s", G_STRFUNC, __LINE__, #_l); goto _l; } while (0)
#else
# define DZL_TRACE_MSG(m,...) do { } while (0)
# define DZL_ENTRY            do { } while (0)
# define DZL_EXIT             return
# define DZL_RETURN(r)        return (r)
# define DZL_GOTO(_l)         goto _l
#endif

typedef struct
{
  DzlSuggestionPopover      *popover;
  DzlSuggestionEntryBuffer  *buffer;
  GListModel                *model;

  gulong                     changed_handler;

  DzlSuggestionPositionFunc  func;
  gpointer                   func_data;
  GDestroyNotify             func_data_destroy;
} DzlSuggestionEntryPrivate;

enum {
  PROP_0,
  PROP_MODEL,
  PROP_TYPED_TEXT,
  N_PROPS
};

enum {
  ACTIVATE_SUGGESTION,
  HIDE_SUGGESTIONS,
  MOVE_SUGGESTION,
  SHOW_SUGGESTIONS,
  SUGGESTION_ACTIVATED,
  N_SIGNALS
};

static void buildable_iface_init (GtkBuildableIface    *iface);
static void editable_iface_init  (GtkEditableInterface *iface);

G_DEFINE_TYPE_WITH_CODE (DzlSuggestionEntry, dzl_suggestion_entry, GTK_TYPE_ENTRY,
                         G_ADD_PRIVATE (DzlSuggestionEntry)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, editable_iface_init))

static GParamSpec *properties [N_PROPS];
static guint signals [N_SIGNALS];
static guint changed_signal_id;
static GtkEditableInterface *editable_parent_iface;

static void
dzl_suggestion_entry_show_suggestions (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  _dzl_suggestion_entry_reposition (self, priv->popover);
  dzl_suggestion_popover_popup (priv->popover);

  DZL_EXIT;
}

static void
dzl_suggestion_entry_hide_suggestions (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  dzl_suggestion_popover_popdown (priv->popover);

  DZL_EXIT;
}

static gboolean
dzl_suggestion_entry_focus_out_event (GtkWidget     *widget,
                                      GdkEventFocus *event)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));
  g_assert (event != NULL);

  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);

  return GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->focus_out_event (widget, event);
}

static void
dzl_suggestion_entry_hierarchy_changed (GtkWidget *widget,
                                        GtkWidget *old_toplevel)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));
  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));

  if (priv->popover != NULL)
    {
      GtkWidget *toplevel = gtk_widget_get_ancestor (widget, GTK_TYPE_WINDOW);

      gtk_window_set_transient_for (GTK_WINDOW (priv->popover), GTK_WINDOW (toplevel));
    }
}

static gboolean
dzl_suggestion_entry_key_press_event (GtkWidget   *widget,
                                      GdkEventKey *key)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  /*
   * If Tab was pressed, and there is uncommitted suggested text,
   * commit it and stop propagation of the key press.
   */
  if (key->keyval == GDK_KEY_Tab && (key->state & GDK_MODIFIER_MASK) == 0)
    {
      const gchar *typed_text;
      DzlSuggestion *suggestion;

      typed_text = dzl_suggestion_entry_buffer_get_typed_text (priv->buffer);
      suggestion = dzl_suggestion_popover_get_selected (priv->popover);

      if (typed_text != NULL && suggestion != NULL)
        {
          g_autofree gchar *replace = dzl_suggestion_replace_typed_text (suggestion, typed_text);

          g_signal_handler_block (self, priv->changed_handler);

          if (replace != NULL)
            gtk_entry_set_text (GTK_ENTRY (self), replace);
          else
            dzl_suggestion_entry_buffer_commit (priv->buffer);
          gtk_editable_set_position (GTK_EDITABLE (self), -1);

          g_signal_handler_unblock (self, priv->changed_handler);

          return GDK_EVENT_STOP;
        }
    }

  return GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->key_press_event (widget, key);
}

static void
dzl_suggestion_entry_update_attrs (DzlSuggestionEntry *self)
{
  PangoAttribute *attr;
  PangoAttrList *list;
  const gchar *typed_text;
  const gchar *text;
  GdkRGBA rgba;

  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  gdk_rgba_parse (&rgba, "#666666");

  text = gtk_entry_get_text (GTK_ENTRY (self));
  typed_text = dzl_suggestion_entry_get_typed_text (self);

  list = pango_attr_list_new ();
  attr = pango_attr_foreground_new (rgba.red * 0xFFFF, rgba.green * 0xFFFF, rgba.blue * 0xFFFF);
  attr->start_index = strlen (typed_text);
  attr->end_index = strlen (text);
  pango_attr_list_insert (list, attr);
  gtk_entry_set_attributes (GTK_ENTRY (self), list);
  pango_attr_list_unref (list);

  DZL_EXIT;
}

static void
dzl_suggestion_entry_changed (GtkEditable *editable)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)editable;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);
  DzlSuggestion *suggestion;
  const gchar *text;

  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  /*
   * If we aren't focused, just ignore everything. One such example might be
   * updating an URI in a webbrowser.
   */
  if (!gtk_widget_has_focus (GTK_WIDGET (editable)))
    return;

  g_signal_handler_block (self, priv->changed_handler);

  text = dzl_suggestion_entry_buffer_get_typed_text (priv->buffer);

  if (text == NULL || *text == '\0')
    {
      dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, NULL);
      g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
      DZL_GOTO (finish);
    }

  g_signal_emit (self, signals [SHOW_SUGGESTIONS], 0);

  suggestion = dzl_suggestion_popover_get_selected (priv->popover);

  if (suggestion != NULL)
    {
      g_object_ref (suggestion);
      dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);
      g_object_unref (suggestion);
    }

finish:
  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TYPED_TEXT]);

  g_signal_handler_unblock (self, priv->changed_handler);

  dzl_suggestion_entry_update_attrs (self);

  DZL_EXIT;
}

static void
dzl_suggestion_entry_suggestion_activated (DzlSuggestionEntry   *self,
                                           DzlSuggestion        *suggestion,
                                           DzlSuggestionPopover *popover)
{
  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));
  g_assert (DZL_IS_SUGGESTION (suggestion));
  g_assert (DZL_IS_SUGGESTION_POPOVER (popover));

  g_signal_emit (self, signals [SUGGESTION_ACTIVATED], 0, suggestion);
  g_signal_emit (self, signals [HIDE_SUGGESTIONS], 0);
  gtk_entry_set_text (GTK_ENTRY (self), "");

  DZL_EXIT;
}

static void
dzl_suggestion_entry_move_suggestion (DzlSuggestionEntry *self,
                                      gint                amount)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  dzl_suggestion_popover_move_by (priv->popover, amount);
}

static void
dzl_suggestion_entry_activate_suggestion (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  DZL_ENTRY;

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  dzl_suggestion_popover_activate_selected (priv->popover);

  DZL_EXIT;
}

static void
dzl_suggestion_entry_constructed (GObject *object)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)object;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  G_OBJECT_CLASS (dzl_suggestion_entry_parent_class)->constructed (object);

  gtk_entry_set_buffer (GTK_ENTRY (self), GTK_ENTRY_BUFFER (priv->buffer));
}

static void
dzl_suggestion_entry_destroy (GtkWidget *widget)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)widget;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  if (priv->func_data_destroy != NULL)
    {
      GDestroyNotify notify = g_steal_pointer (&priv->func_data_destroy);
      gpointer notify_data = g_steal_pointer (&priv->func_data);

      notify (notify_data);
    }

  if (priv->popover != NULL)
    gtk_widget_destroy (GTK_WIDGET (priv->popover));

  g_clear_object (&priv->model);

  g_assert (priv->popover == NULL);

  GTK_WIDGET_CLASS (dzl_suggestion_entry_parent_class)->destroy (widget);
}

static void
dzl_suggestion_entry_get_property (GObject    *object,
                                   guint       prop_id,
                                   GValue     *value,
                                   GParamSpec *pspec)
{
  DzlSuggestionEntry *self = DZL_SUGGESTION_ENTRY (object);

  switch (prop_id)
    {
    case PROP_MODEL:
      g_value_set_object (value, dzl_suggestion_entry_get_model (self));
      break;

    case PROP_TYPED_TEXT:
      g_value_set_string (value, dzl_suggestion_entry_get_typed_text (self));
      break;

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

static void
dzl_suggestion_entry_set_property (GObject      *object,
                                   guint         prop_id,
                                   const GValue *value,
                                   GParamSpec   *pspec)
{
  DzlSuggestionEntry *self = DZL_SUGGESTION_ENTRY (object);

  switch (prop_id)
    {
    case PROP_MODEL:
      dzl_suggestion_entry_set_model (self, g_value_get_object (value));
      break;

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

static void
dzl_suggestion_entry_class_init (DzlSuggestionEntryClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GtkBindingSet *bindings;

  object_class->constructed = dzl_suggestion_entry_constructed;
  object_class->get_property = dzl_suggestion_entry_get_property;
  object_class->set_property = dzl_suggestion_entry_set_property;

  widget_class->destroy = dzl_suggestion_entry_destroy;
  widget_class->focus_out_event = dzl_suggestion_entry_focus_out_event;
  widget_class->hierarchy_changed = dzl_suggestion_entry_hierarchy_changed;
  widget_class->key_press_event = dzl_suggestion_entry_key_press_event;

  klass->hide_suggestions = dzl_suggestion_entry_hide_suggestions;
  klass->show_suggestions = dzl_suggestion_entry_show_suggestions;
  klass->move_suggestion = dzl_suggestion_entry_move_suggestion;

  properties [PROP_MODEL] =
    g_param_spec_object ("model",
                         "Model",
                         "The model to be visualized",
                         G_TYPE_LIST_MODEL,
                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  properties [PROP_TYPED_TEXT] =
    g_param_spec_string ("typed-text",
                         "Typed Text",
                         "Typed text into the entry, does not include suggested text",
                         NULL,
                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));

  g_object_class_install_properties (object_class, N_PROPS, properties);

  signals [HIDE_SUGGESTIONS] =
    g_signal_new ("hide-suggestions",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (DzlSuggestionEntryClass, hide_suggestions),
                  NULL, NULL, NULL, G_TYPE_NONE, 0);

  /**
   * DzlSuggestionEntry::move-suggestion:
   * @self: A #DzlSuggestionEntry
   * @amount: The number of items to move
   *
   * This moves the selected suggestion in the popover by the value
   * provided. -1 moves up one row, 1, moves down a row.
   */
  signals [MOVE_SUGGESTION] =
    g_signal_new ("move-suggestion",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (DzlSuggestionEntryClass, move_suggestion),
                  NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_INT);

  signals [SHOW_SUGGESTIONS] =
    g_signal_new ("show-suggestions",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (DzlSuggestionEntryClass, show_suggestions),
                  NULL, NULL, NULL, G_TYPE_NONE, 0);

  signals [SUGGESTION_ACTIVATED] =
    g_signal_new ("suggestion-activated",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (DzlSuggestionEntryClass, suggestion_activated),
                  NULL, NULL, NULL, G_TYPE_NONE, 1, DZL_TYPE_SUGGESTION);

  widget_class->activate_signal = signals [ACTIVATE_SUGGESTION] =
    g_signal_new_class_handler ("activate-suggestion",
                                G_TYPE_FROM_CLASS (klass),
                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                                G_CALLBACK (dzl_suggestion_entry_activate_suggestion),
                                NULL, NULL, NULL, G_TYPE_NONE, 0);

  bindings = gtk_binding_set_by_class (klass);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "hide-suggestions", 0);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_space, GDK_CONTROL_MASK, "show-suggestions", 0);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Up, 0, "move-suggestion", 1, G_TYPE_INT, -1);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Down, 0, "move-suggestion", 1, G_TYPE_INT, 1);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Up, 0, "move-suggestion", 1, G_TYPE_INT, -10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Prior, 0, "move-suggestion", 1, G_TYPE_INT, -10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Next, 0, "move-suggestion", 1, G_TYPE_INT, 10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_KP_Page_Down, 0, "move-suggestion", 1, G_TYPE_INT, 10);
  gtk_binding_entry_add_signal (bindings, GDK_KEY_Return, 0, "activate-suggestion", 0);

  changed_signal_id = g_signal_lookup ("changed", GTK_TYPE_ENTRY);
}

static void
dzl_suggestion_entry_init (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  priv->func = dzl_suggestion_entry_default_position_func;

  priv->changed_handler =
    g_signal_connect_after (self,
                            "changed",
                            G_CALLBACK (dzl_suggestion_entry_changed),
                            NULL);

  priv->popover = g_object_new (DZL_TYPE_SUGGESTION_POPOVER,
                                "destroy-with-parent", TRUE,
                                "modal", FALSE,
                                "relative-to", self,
                                "type", GTK_WINDOW_POPUP,
                                NULL);
  g_signal_connect_object (priv->popover,
                           "suggestion-activated",
                           G_CALLBACK (dzl_suggestion_entry_suggestion_activated),
                           self,
                           G_CONNECT_SWAPPED);
  g_signal_connect (priv->popover,
                    "destroy",
                    G_CALLBACK (gtk_widget_destroyed),
                    &priv->popover);
  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
                               "suggestion");

  priv->buffer = dzl_suggestion_entry_buffer_new ();
}

GtkWidget *
dzl_suggestion_entry_new (void)
{
  return g_object_new (DZL_TYPE_SUGGESTION_ENTRY, NULL);
}

static GObject *
dzl_suggestion_entry_get_internal_child (GtkBuildable *buildable,
                                         GtkBuilder   *builder,
                                         const gchar  *childname)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)buildable;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  if (g_strcmp0 (childname, "popover") == 0)
    return G_OBJECT (priv->popover);

  return NULL;
}

static void
buildable_iface_init (GtkBuildableIface *iface)
{
  iface->get_internal_child = dzl_suggestion_entry_get_internal_child;
}

static void
dzl_suggestion_entry_set_selection_bounds (GtkEditable *editable,
                                           gint         start_pos,
                                           gint         end_pos)
{
  DzlSuggestionEntry *self = (DzlSuggestionEntry *)editable;
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_assert (DZL_IS_SUGGESTION_ENTRY (self));

  g_signal_handler_block (self, priv->changed_handler);

  if (end_pos < 0)
    end_pos = gtk_entry_buffer_get_length (GTK_ENTRY_BUFFER (priv->buffer));

  if (end_pos > (gint)dzl_suggestion_entry_buffer_get_typed_length (priv->buffer))
    dzl_suggestion_entry_buffer_commit (priv->buffer);

  editable_parent_iface->set_selection_bounds (editable, start_pos, end_pos);

  g_signal_handler_unblock (self, priv->changed_handler);
}

static void
editable_iface_init (GtkEditableInterface *iface)
{
  editable_parent_iface = g_type_interface_peek_parent (iface);

  iface->set_selection_bounds = dzl_suggestion_entry_set_selection_bounds;
}


/**
 * dzl_suggestion_entry_get_model:
 * @self: a #DzlSuggestionEntry
 *
 * Gets the model being visualized.
 *
 * Returns: (nullable) (transfer none): A #GListModel or %NULL.
 */
GListModel *
dzl_suggestion_entry_get_model (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL);

  return priv->model;
}

void
dzl_suggestion_entry_set_model (DzlSuggestionEntry *self,
                                GListModel         *model)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  DZL_ENTRY;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));
  g_return_if_fail (!model || g_type_is_a (g_list_model_get_item_type (model), DZL_TYPE_SUGGESTION));

  if (g_set_object (&priv->model, model))
    {
      DZL_TRACE_MSG ("Model has %u items",
                     model ? g_list_model_get_n_items (model) : 0);
      dzl_suggestion_popover_set_model (priv->popover, model);
      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODEL]);
      dzl_suggestion_entry_update_attrs (self);
    }

  DZL_EXIT;
}

/**
 * dzl_suggestion_entry_get_suggestion:
 * @self: a #DzlSuggestionEntry
 *
 * Gets the currently selected suggestion.
 *
 * Returns: (nullable) (transfer none): An #DzlSuggestion or %NULL.
 */
DzlSuggestion *
dzl_suggestion_entry_get_suggestion (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL);

  return dzl_suggestion_popover_get_selected (priv->popover);
}

void
dzl_suggestion_entry_set_suggestion (DzlSuggestionEntry *self,
                                     DzlSuggestion      *suggestion)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  DZL_ENTRY;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));
  g_return_if_fail (!suggestion || DZL_IS_SUGGESTION_ENTRY (suggestion));

  dzl_suggestion_popover_set_selected (priv->popover, suggestion);
  dzl_suggestion_entry_buffer_set_suggestion (priv->buffer, suggestion);

  DZL_EXIT;
}

const gchar *
dzl_suggestion_entry_get_typed_text (DzlSuggestionEntry *self)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);

  g_return_val_if_fail (DZL_IS_SUGGESTION_ENTRY (self), NULL);

  return dzl_suggestion_entry_buffer_get_typed_text (priv->buffer);
}

void
dzl_suggestion_entry_default_position_func (DzlSuggestionEntry *self,
                                            GdkRectangle       *area,
                                            gboolean           *is_absolute,
                                            gpointer            user_data)
{
  GtkAllocation alloc;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));
  g_return_if_fail (area != NULL);
  g_return_if_fail (is_absolute != NULL);

  *is_absolute = FALSE;

  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);

  area->y += alloc.height;
  area->height = 300;
}

/**
 * dzl_suggestion_entry_window_position_func:
 *
 * This is a #DzlSuggestionPositionFunc that can be used to make the suggestion
 * popover the full width of the window. It is similar to what you might find
 * in a web browser.
 */
void
dzl_suggestion_entry_window_position_func (DzlSuggestionEntry *self,
                                           GdkRectangle       *area,
                                           gboolean           *is_absolute,
                                           gpointer            user_data)
{
  GtkWidget *toplevel;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));
  g_return_if_fail (area != NULL);
  g_return_if_fail (is_absolute != NULL);

  toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);

  if (toplevel != NULL)
    {
      GtkWidget *child = gtk_bin_get_child (GTK_BIN (toplevel));
      GtkAllocation alloc;
      gint x, y;
      gint height = 300;

      gtk_widget_translate_coordinates (child, toplevel, 0, 0, &x, &y);
      gtk_widget_get_allocation (child, &alloc);
      gtk_window_get_size (GTK_WINDOW (toplevel), NULL, &height);

      area->x = x;
      area->y = y;
      area->width = alloc.width;
      area->height = MAX (300, height / 2);

      /* If our widget would get obscurred, adjust it */
      gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
      gtk_widget_translate_coordinates (GTK_WIDGET (self), toplevel,
                                        0, alloc.height, NULL, &y);
      if (y > area->y)
        area->y = y;

      *is_absolute = TRUE;

      return;
    }

  dzl_suggestion_entry_default_position_func (self, area, is_absolute, NULL);
}

/**
 * dzl_suggestion_entry_set_position_func:
 * @self: a #DzlSuggestionEntry
 * @func: (scope async) (closure func_data) (destroy func_data_destroy) (nullable):
 *   A function to call to position the popover, or %NULL to set the default.
 * @func_data: (nullable): closure data for @func
 * @func_data_destroy: (nullable): a destroy notify for @func_data
 *
 * Sets a position func to position the popover.
 *
 * In @func, you should set the height of the rectangle to the maximum height
 * that the popover should be allowed to grow.
 *
 * Since: 3.26
 */
void
dzl_suggestion_entry_set_position_func (DzlSuggestionEntry        *self,
                                        DzlSuggestionPositionFunc  func,
                                        gpointer                   func_data,
                                        GDestroyNotify             func_data_destroy)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);
  GDestroyNotify notify = NULL;
  gpointer notify_data = NULL;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));

  if (func == NULL)
    {
      func = dzl_suggestion_entry_default_position_func;
      func_data = NULL;
      func_data_destroy = NULL;
    }

  if (priv->func_data_destroy != NULL)
    {
      notify = priv->func_data_destroy;
      notify_data = priv->func_data;
    }

  priv->func = func;
  priv->func_data = func_data;
  priv->func_data_destroy = func_data_destroy;

  if (notify)
    notify (notify_data);
}

void
_dzl_suggestion_entry_reposition (DzlSuggestionEntry   *self,
                                  DzlSuggestionPopover *popover)
{
  DzlSuggestionEntryPrivate *priv = dzl_suggestion_entry_get_instance_private (self);
  GtkWidget *toplevel;
  GdkWindow *window;
  GtkAllocation alloc;
  gboolean is_absolute = FALSE;
  gint x;
  gint y;

  g_return_if_fail (DZL_IS_SUGGESTION_ENTRY (self));
  g_return_if_fail (DZL_IS_SUGGESTION_POPOVER (popover));

  if (!gtk_widget_get_realized (GTK_WIDGET (self)) ||
      !gtk_widget_get_realized (GTK_WIDGET (popover)))
    return;

  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
  window = gtk_widget_get_window (toplevel);

  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);

  alloc.x = 0;
  alloc.y = 0;

  priv->func (self, &alloc, &is_absolute, priv->func_data);

  _dzl_suggestion_popover_set_max_height (priv->popover, alloc.height);

  if (!is_absolute)
    {
      gtk_widget_translate_coordinates (GTK_WIDGET (self), toplevel, 0, 0, &x, &y);
      alloc.x += x;
      alloc.y += y;
    }

  gdk_window_get_position (window, &x, &y);
  alloc.x += x;
  alloc.y += y;

  _dzl_suggestion_popover_adjust_margin (popover, &alloc);

  gtk_widget_set_size_request (GTK_WIDGET (popover), alloc.width, -1);
  gtk_window_move (GTK_WINDOW (popover), alloc.x, alloc.y);
}