[libadwaita/entry-row: 2/5] Add AdwEntryRow




commit a932dd539b09d29501e4cfca4f4b0520a5ecc1f1
Author: Maximiliano Sandoval R <msandova protonmail com>
Date:   Tue Jul 20 15:33:52 2021 +0200

    Add AdwEntryRow

 doc/boxed-lists.md                                 |  10 +
 doc/images/entry-row-dark.png                      | Bin 0 -> 2137 bytes
 doc/images/entry-row.png                           | Bin 0 -> 2535 bytes
 doc/libadwaita.toml.in                             |   2 +
 doc/tools/data/entry-row.ui                        |  23 +
 doc/visual-index.md                                |   7 +
 po/POTFILES.in                                     |   1 +
 src/adw-entry-row-private.h                        |  24 +
 src/adw-entry-row.c                                | 723 +++++++++++++++++++++
 src/adw-entry-row.h                                |  53 ++
 src/adw-entry-row.ui                               | 126 ++++
 src/adwaita.gresources.xml                         |   2 +
 src/adwaita.h                                      |   1 +
 .../scalable/actions/adw-entry-apply-symbolic.svg  |   5 +
 src/meson.build                                    |   2 +
 src/stylesheet/widgets/_lists.scss                 |  27 +
 16 files changed, 1006 insertions(+)
---
diff --git a/doc/boxed-lists.md b/doc/boxed-lists.md
index 640c8309..d2c45049 100644
--- a/doc/boxed-lists.md
+++ b/doc/boxed-lists.md
@@ -78,6 +78,16 @@ other rows.
   <img src="combo-row.png" alt="combo-row">
 </picture>
 
+## Entry Rows
+
+[class@EntryRow] is a row with an embedded entry. It can have prefix and suffix
+widgets, and an apply button.
+
+<picture>
+  <source srcset="entry-row-dark.png" media="(prefers-color-scheme: dark)">
+  <img src="entry-row.png" alt="entry-row">
+</picture>
+
 ## Preferences Group
 
 [class@PreferencesGroup] provides a boxed list along with a title and a
diff --git a/doc/images/entry-row-dark.png b/doc/images/entry-row-dark.png
new file mode 100644
index 00000000..ae7c99e4
Binary files /dev/null and b/doc/images/entry-row-dark.png differ
diff --git a/doc/images/entry-row.png b/doc/images/entry-row.png
new file mode 100644
index 00000000..9460af4c
Binary files /dev/null and b/doc/images/entry-row.png differ
diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in
index 14b7db2f..a80efa7c 100644
--- a/doc/libadwaita.toml.in
+++ b/doc/libadwaita.toml.in
@@ -122,6 +122,8 @@ content_images = [
   "images/devel-window-dark.png",
   "images/dim-label.png",
   "images/dim-label-dark.png",
+  "images/entry-row.png",
+  "images/entry-row-dark.png",
   "images/expander-row.png",
   "images/expander-row-dark.png",
   "images/flap-narrow.png",
diff --git a/doc/tools/data/entry-row.ui b/doc/tools/data/entry-row.ui
new file mode 100644
index 00000000..df3e9571
--- /dev/null
+++ b/doc/tools/data/entry-row.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <object class="GtkListBox" id="widget">
+    <property name="margin-top">6</property>
+    <property name="margin-bottom">6</property>
+    <property name="margin-start">6</property>
+    <property name="margin-end">6</property>
+    <property name="selection-mode">none</property>
+    <property name="width-request">400</property>
+    <style>
+      <class name="boxed-list"/>
+    </style>
+    <child>
+      <object class="AdwEntryRow">
+        <property name="title">Title</property>
+        <property name="text">Text</property>
+        <property name="can-focus">False</property>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/doc/visual-index.md b/doc/visual-index.md
index db200c2b..30d6b3ed 100644
--- a/doc/visual-index.md
+++ b/doc/visual-index.md
@@ -49,6 +49,13 @@ Slug: visual-index
   <img src="expander-row.png" alt="expander-row">
 </picture>](class.ExpanderRow.html)
 
+### Entry Row
+
+[<picture>
+  <source srcset="entry-row-dark.png" media="(prefers-color-scheme: dark)">
+  <img src="entry-row.png" alt="entry-row">
+</picture>](class.EntryRow.html)
+
 ## Preferences
 
 ### Preferences Group
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4910cfb8..29355757 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,5 +1,6 @@
 # List of source files containing translatable strings.
 # Please keep this file sorted alphabetically.
+src/adw-entry-row.ui
 src/adw-inspector-page.c
 src/adw-inspector-page.ui
 src/adw-preferences-window.c
diff --git a/src/adw-entry-row-private.h b/src/adw-entry-row-private.h
new file mode 100644
index 00000000..dd827a0e
--- /dev/null
+++ b/src/adw-entry-row-private.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-entry-row.h"
+
+G_BEGIN_DECLS
+
+void adw_entry_row_set_indicator_icon_name (AdwEntryRow *self,
+                                            const char  *icon_name);
+void adw_entry_row_set_indicator_tooltip   (AdwEntryRow *self,
+                                            const char  *tooltip);
+void adw_entry_row_set_show_indicator      (AdwEntryRow *self,
+                                            gboolean     show_indicator);
+
+G_END_DECLS
diff --git a/src/adw-entry-row.c b/src/adw-entry-row.c
new file mode 100644
index 00000000..c7dcb8e7
--- /dev/null
+++ b/src/adw-entry-row.c
@@ -0,0 +1,723 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval <msandova protonmail com>
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+#include "adw-entry-row-private.h"
+
+#include "adw-animation-private.h"
+#include "adw-animation-util.h"
+#include "adw-gizmo-private.h"
+#include "adw-macros-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+
+#define EMPTY_ANIMATION_DURATION 250
+#define TITLE_SPACING 3
+
+/**
+ * AdwEntryRow:
+ *
+ * A [class@Gtk.ListBoxRow] used to present an entry inside lists.
+ *
+ * <picture>
+ *   <source srcset="entry-row-dark.png" media="(prefers-color-scheme: dark)">
+ *   <img src="entry-row.png" alt="entry-row">
+ * </picture>
+ *
+ * An edit icon is shown to indicate whether the underlying [class Gtk Text] has
+ * focus and the widget is [property@Gtk.Editable:editable].
+ *
+ * The `AdwEntryRow` widget can have a [property@Adw.PreferencesRow:title] and
+ * [property@Gtk.Editable:text]. The row can receive additional widgets at its
+ * end, or prefix widgets at its start.
+ *
+ * `AdwEntryRow` text does not have API of its own, but it implements the
+ * [iface@Gtk.Editable] interface and partially exposes the internal
+ * [class Gtk Text] API.
+ *
+ * ## AdwEntryRow as GtkBuildable
+ *
+ * The `AdwEntryRow` implementation of the [iface@Gtk.Buildable] interface
+ * supports adding children at its end by specifying “suffix” or omitting the
+ * “type” attribute of a <child> element.
+ *
+ * It also supports adding a children as a prefix widget by specifying “prefix”
+ * as the “type” attribute of a <child> element.
+ *
+ * # CSS nodes
+ *
+ * `AdwEntryRow` has a main CSS node with name `row` and the `.entry` style
+ * class.
+ *
+ * Since: 1.2
+ */
+
+typedef struct
+{
+  GtkWidget *header;
+  GtkWidget *text;
+  GtkWidget *title;
+  GtkWidget *empty_title;
+  GtkWidget *editable_area;
+  GtkWidget *edit_icon;
+  GtkWidget *apply_button;
+  GtkWidget *indicator;
+  GtkBox *suffixes;
+  GtkBox *prefixes;
+
+  gboolean empty;
+  double empty_progress;
+  AdwAnimation *empty_animation;
+
+  gboolean editing;
+  gboolean show_apply_button;
+  gboolean text_changed;
+  gboolean show_indicator;
+} AdwEntryRowPrivate;
+
+static void adw_entry_row_editable_init (GtkEditableInterface *iface);
+static void adw_entry_row_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwEntryRow, adw_entry_row, ADW_TYPE_PREFERENCES_ROW,
+                         G_ADD_PRIVATE (AdwEntryRow)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_entry_row_buildable_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, adw_entry_row_editable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+  PROP_0,
+  PROP_SHOW_APPLY_BUTTON,
+  PROP_LAST_PROP,
+};
+
+static GParamSpec *props[PROP_LAST_PROP];
+
+enum {
+  SIGNAL_APPLY,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+empty_animation_value_cb (double       value,
+                          AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  priv->empty_progress = value;
+
+  gtk_widget_queue_allocate (priv->editable_area);
+
+  gtk_widget_set_opacity (priv->text, value);
+  gtk_widget_set_opacity (priv->title, value);
+  gtk_widget_set_opacity (priv->empty_title, 1 - value);
+}
+
+static gboolean
+is_text_focused (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  GtkStateFlags flags = gtk_widget_get_state_flags (priv->text);
+
+  return !!(flags & GTK_STATE_FLAG_FOCUS_WITHIN);
+}
+
+static void
+update_empty (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  GtkEntryBuffer *buffer = gtk_text_get_buffer (GTK_TEXT (priv->text));
+  gboolean focused = is_text_focused (self);
+  gboolean editable = gtk_editable_get_editable (GTK_EDITABLE (priv->text));
+  gboolean empty = gtk_entry_buffer_get_length (buffer) == 0;
+
+  gtk_widget_set_visible (priv->edit_icon, !priv->text_changed && (!priv->editing || !editable));
+  gtk_widget_set_sensitive (priv->edit_icon, editable);
+  gtk_widget_set_visible (priv->indicator, priv->editing && priv->show_indicator);
+  gtk_widget_set_visible (priv->apply_button, priv->text_changed);
+
+  priv->empty = empty && !(focused && editable) && !priv->text_changed;
+
+  gtk_widget_queue_allocate (priv->editable_area);
+
+  adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (priv->empty_animation),
+                                      priv->empty_progress);
+  adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (priv->empty_animation),
+                                    priv->empty ? 0 : 1);
+  adw_animation_play (priv->empty_animation);
+}
+
+static void
+text_changed_cb (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  if (priv->show_apply_button && priv->editing)
+    priv->text_changed = TRUE;
+
+  update_empty (self);
+}
+
+static void
+text_state_flags_changed_cb (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  priv->editing = is_text_focused (self);
+
+  if (priv->editing)
+    gtk_widget_add_css_class (GTK_WIDGET (self), "focused");
+  else
+    gtk_widget_remove_css_class (GTK_WIDGET (self), "focused");
+
+  update_empty (self);
+}
+
+static gboolean
+text_keynav_failed_cb (AdwEntryRow      *self,
+                       GtkDirectionType  direction)
+{
+  if (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT)
+    return gtk_widget_child_focus (GTK_WIDGET (self), direction);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+pressed_cb (GtkGesture  *gesture,
+            int          n_press,
+            double       x,
+            double       y,
+            AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  GtkWidget *picked;
+
+  picked = gtk_widget_pick (GTK_WIDGET (self), x, y, GTK_PICK_DEFAULT);
+
+  if (picked != GTK_WIDGET (self) &&
+      picked != priv->header &&
+      picked != priv->indicator &&
+      picked != GTK_WIDGET (priv->prefixes) &&
+      picked != GTK_WIDGET (priv->suffixes)) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  gtk_widget_grab_focus (GTK_WIDGET (priv->text));
+
+  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+apply_button_clicked_cb (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  if (gtk_widget_has_focus (priv->apply_button))
+    gtk_widget_grab_focus (GTK_WIDGET (self));
+
+  priv->text_changed = FALSE;
+  update_empty (self);
+
+  g_signal_emit (self, signals[SIGNAL_APPLY], 0);
+}
+
+static void
+text_activated_cb (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  if (gtk_widget_get_visible (priv->apply_button))
+    apply_button_clicked_cb (self);
+}
+
+static void
+measure_editable_area (GtkWidget      *widget,
+                       GtkOrientation  orientation,
+                       int             for_size,
+                       int            *minimum,
+                       int            *natural,
+                       int            *minimum_baseline,
+                       int            *natural_baseline)
+{
+  AdwEntryRow *self = g_object_get_data (G_OBJECT (widget), "row");
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  int text_min = 0, text_nat = 0;
+  int title_min = 0, title_nat = 0;
+  int empty_min = 0, empty_nat = 0;
+
+  gtk_widget_measure (priv->text, orientation, for_size,
+                      &text_min, &text_nat, NULL, NULL);
+  gtk_widget_measure (priv->title, orientation, for_size,
+                      &title_min, &title_nat, NULL, NULL);
+  gtk_widget_measure (priv->empty_title, orientation, for_size,
+                      &empty_min, &empty_nat, NULL, NULL);
+
+  if (minimum)
+    *minimum = MAX (text_min + TITLE_SPACING + title_min, empty_min);
+
+  if (natural)
+    *natural = MAX (text_nat + TITLE_SPACING + title_nat, empty_nat);
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+allocate_editable_area (GtkWidget *widget,
+                        int        width,
+                        int        height,
+                        int        baseline)
+{
+  AdwEntryRow *self = g_object_get_data (G_OBJECT (widget), "row");
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  gboolean is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+  GskTransform *transform;
+  int empty_height = 0, title_height = 0, text_height = 0, text_baseline = -1;
+  float empty_scale, title_scale, title_offset;
+
+  gtk_widget_measure (priv->title, GTK_ORIENTATION_VERTICAL, width,
+                      NULL, &title_height, NULL, NULL);
+  gtk_widget_measure (priv->empty_title, GTK_ORIENTATION_VERTICAL, width,
+                      NULL, &empty_height, NULL, NULL);
+  gtk_widget_measure (priv->text, GTK_ORIENTATION_VERTICAL, width,
+                      NULL, &text_height, NULL, &text_baseline);
+
+  empty_scale = (float) adw_lerp (1.0, (double) title_height / empty_height, priv->empty_progress);
+  title_scale = (float) adw_lerp ((double) empty_height / title_height, 1.0, priv->empty_progress);
+  title_offset = (float) adw_lerp ((double) (height - empty_height) / 2.0,
+                                   (double) (height - title_height - text_height - TITLE_SPACING) / 2.0,
+                                   priv->empty_progress);
+
+  transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, title_offset));
+  if (is_rtl)
+    transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (width, 0));
+  transform = gsk_transform_scale (transform, empty_scale, empty_scale);
+  if (is_rtl)
+    transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (-width, 0));
+  gtk_widget_allocate (priv->empty_title, width, empty_height, -1, transform);
+
+  transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, title_offset));
+  if (is_rtl)
+    transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (width, 0));
+  transform = gsk_transform_scale (transform, title_scale, title_scale);
+  if (is_rtl)
+    transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (-width, 0));
+  gtk_widget_allocate (priv->title, width, title_height, -1, transform);
+
+  text_baseline += (int) ((double) (height + title_height - text_height + TITLE_SPACING) / 2.0);
+  gtk_widget_allocate (priv->text, width, height, text_baseline, NULL);
+}
+
+static gboolean
+adw_entry_row_grab_focus (GtkWidget *widget)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (widget);
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  return gtk_widget_grab_focus (priv->text);
+}
+
+static void
+adw_entry_row_get_property (GObject     *object,
+                            guint        prop_id,
+                            GValue      *value,
+                            GParamSpec  *pspec)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (object);
+
+  if (gtk_editable_delegate_get_property (object, prop_id, value, pspec))
+    return;
+
+  switch (prop_id) {
+  case PROP_SHOW_APPLY_BUTTON:
+    g_value_set_boolean (value, adw_entry_row_get_show_apply_button (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_entry_row_set_property (GObject       *object,
+                            guint          prop_id,
+                            const GValue  *value,
+                            GParamSpec    *pspec)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (object);
+
+  if (gtk_editable_delegate_set_property (object, prop_id, value, pspec))
+  {
+    switch (prop_id) {
+    case PROP_LAST_PROP + GTK_EDITABLE_PROP_EDITABLE:
+      update_empty (self);
+      break;
+    default:;
+    }
+    return;
+  }
+
+  switch (prop_id) {
+  case PROP_SHOW_APPLY_BUTTON:
+    adw_entry_row_set_show_apply_button (self, g_value_get_boolean (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_entry_row_dispose (GObject *object)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (object);
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  g_clear_object (&priv->empty_animation);
+
+  if (priv->text)
+    gtk_editable_finish_delegate (GTK_EDITABLE (self));
+
+  G_OBJECT_CLASS (adw_entry_row_parent_class)->dispose (object);
+}
+
+static void
+adw_entry_row_class_init (AdwEntryRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = adw_entry_row_get_property;
+  object_class->set_property = adw_entry_row_set_property;
+  object_class->dispose = adw_entry_row_dispose;
+
+  widget_class->focus = adw_widget_focus_child;
+  widget_class->grab_focus = adw_entry_row_grab_focus;
+
+  /**
+   * AdwEntryRow:show-apply-button: (attributes org.gtk.Property.get=adw_entry_row_get_show_apply_button 
org.gtk.Property.set=adw_entry_row_set_show_apply_button)
+   *
+   * Whether to show the apply button.
+   *
+   * See [signal@EntryRow::apply].
+   *
+   * Since: 1.2
+   */
+  props[PROP_SHOW_APPLY_BUTTON] =
+    g_param_spec_boolean ("show-apply-button",
+                          "Show Apply Button",
+                          "Whether to show the apply button",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+  gtk_editable_install_properties (object_class, PROP_LAST_PROP);
+
+  /**
+   * AdwEntryRow::apply:
+   *
+   * Emitted when the apply button is activated.
+   *
+   * See [property@EntryRow:show-apply-button].
+   *
+   * Since: 1.2
+   */
+  signals[SIGNAL_APPLY] =
+    g_signal_new ("apply",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-entry-row.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, header);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, prefixes);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, suffixes);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, editable_area);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, text);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, empty_title);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, title);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, edit_icon);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, apply_button);
+  gtk_widget_class_bind_template_child_private (widget_class, AdwEntryRow, indicator);
+
+  gtk_widget_class_bind_template_callback (widget_class, pressed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, text_state_flags_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, text_keynav_failed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, text_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, update_empty);
+  gtk_widget_class_bind_template_callback (widget_class, text_activated_cb);
+  gtk_widget_class_bind_template_callback (widget_class, apply_button_clicked_cb);
+
+  g_type_ensure (ADW_TYPE_GIZMO);
+}
+
+static void
+adw_entry_row_init (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+  AdwAnimationTarget *target;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+  gtk_editable_init_delegate (GTK_EDITABLE (self));
+
+  adw_gizmo_set_measure_func (ADW_GIZMO (priv->editable_area), (AdwGizmoMeasureFunc) measure_editable_area);
+  adw_gizmo_set_allocate_func (ADW_GIZMO (priv->editable_area), (AdwGizmoAllocateFunc) 
allocate_editable_area);
+  adw_gizmo_set_focus_func (ADW_GIZMO (priv->editable_area), (AdwGizmoFocusFunc) adw_widget_focus_child);
+
+  g_object_set_data (G_OBJECT (priv->editable_area), "row", self);
+
+  priv->empty_progress = 0.0;
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              empty_animation_value_cb,
+                                              self, NULL);
+
+  priv->empty_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), 0, 0,
+                             EMPTY_ANIMATION_DURATION, target);
+
+  update_empty (self);
+}
+
+static void
+adw_entry_row_buildable_add_child (GtkBuildable *buildable,
+                                   GtkBuilder   *builder,
+                                   GObject      *child,
+                                   const char   *type)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (buildable);
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  if (!priv->header)
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+  else if (g_strcmp0 (type, "prefix") == 0)
+    adw_entry_row_add_prefix (self, GTK_WIDGET (child));
+  else if (g_strcmp0 (type, "suffix") == 0)
+    adw_entry_row_add_suffix (self, GTK_WIDGET (child));
+  else if (!type && GTK_IS_WIDGET (child))
+    adw_entry_row_add_suffix (self, GTK_WIDGET (child));
+  else
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_entry_row_buildable_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+  iface->add_child = adw_entry_row_buildable_add_child;
+}
+
+static GtkEditable *
+adw_entry_row_get_delegate (GtkEditable *editable)
+{
+  AdwEntryRow *self = ADW_ENTRY_ROW (editable);
+  AdwEntryRowPrivate *priv = adw_entry_row_get_instance_private (self);
+
+  return GTK_EDITABLE (priv->text);
+}
+
+void
+adw_entry_row_editable_init (GtkEditableInterface *iface)
+{
+  iface->get_delegate = adw_entry_row_get_delegate;
+}
+
+/**
+ * adw_entry_row_new:
+ *
+ * Creates a new `AdwEntryRow`.
+ *
+ * Returns: the newly created `AdwEntryRow`
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_entry_row_new (void)
+{
+  return g_object_new (ADW_TYPE_ENTRY_ROW, NULL);
+}
+
+/**
+ * adw_entry_row_add_prefix:
+ * @self: an entry row
+ * @widget: a widget
+ *
+ * Adds a prefix widget to @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_entry_row_add_prefix (AdwEntryRow *self,
+                          GtkWidget   *widget)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  gtk_box_prepend (priv->prefixes, widget);
+  gtk_widget_show (GTK_WIDGET (priv->prefixes));
+}
+
+/**
+ * adw_entry_row_add_suffix:
+ * @self: an entry row
+ * @widget: a widget
+ *
+ * Adds a suffix widget to @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_entry_row_add_suffix (AdwEntryRow *self,
+                          GtkWidget    *widget)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  gtk_box_append (priv->suffixes, widget);
+  gtk_widget_show (GTK_WIDGET (priv->suffixes));
+}
+
+/**
+ * adw_entry_row_remove:
+ * @self: an entry row
+ * @widget: the child to be removed
+ *
+ * Removes a child from @self.
+ *
+ * Since: 1.2
+ */
+void
+adw_entry_row_remove (AdwEntryRow *self,
+                      GtkWidget   *child)
+{
+  AdwEntryRowPrivate *priv;
+  GtkWidget *parent;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  parent = gtk_widget_get_parent (child);
+
+  if (parent == GTK_WIDGET (priv->prefixes))
+    gtk_box_remove (priv->prefixes, child);
+  else if (parent == GTK_WIDGET (priv->suffixes))
+    gtk_box_remove (priv->suffixes, child);
+  else
+    ADW_CRITICAL_CANNOT_REMOVE_CHILD (self, child);
+}
+
+/**
+ * adw_entry_row_get_show_apply_button: (attributes org.gtk.Method.get_property=show-apply-button)
+ * @self: an entry row
+ *
+ * Gets whether the apply button is shown.
+ *
+ * Returns: whether apply button is shown
+ *
+ * Since: 1.2
+ */
+gboolean
+adw_entry_row_get_show_apply_button (AdwEntryRow *self)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_val_if_fail (ADW_IS_ENTRY_ROW (self), FALSE);
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  return priv->show_apply_button;
+}
+
+/**
+ * adw_entry_row_set_show_apply_button: (attributes org.gtk.Method.set_property=show-apply-button)
+ * @self: an entry row
+ * @show_apply_button: whether apply button is shown
+ *
+ * Sets whether the apply button is shown.
+ *
+ * Since: 1.2
+ */
+void
+adw_entry_row_set_show_apply_button (AdwEntryRow *self,
+                                     gboolean     show_apply_button)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  show_apply_button = !!show_apply_button;
+
+  priv->show_apply_button = show_apply_button;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_APPLY_BUTTON]);
+}
+
+void
+adw_entry_row_set_indicator_icon_name (AdwEntryRow *self,
+                                       const char  *icon_name)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  gtk_image_set_from_icon_name (GTK_IMAGE (priv->indicator), icon_name);
+}
+
+void
+adw_entry_row_set_indicator_tooltip (AdwEntryRow *self,
+                                     const char  *tooltip)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  gtk_widget_set_tooltip_text (priv->indicator, tooltip);
+}
+
+void
+adw_entry_row_set_show_indicator (AdwEntryRow *self,
+                                  gboolean     show_indicator)
+{
+  AdwEntryRowPrivate *priv;
+
+  g_return_if_fail (ADW_IS_ENTRY_ROW (self));
+
+  priv = adw_entry_row_get_instance_private (self);
+
+  show_indicator = !!show_indicator;
+
+  priv->show_indicator = show_indicator;
+
+  update_empty (self);
+}
diff --git a/src/adw-entry-row.h b/src/adw-entry-row.h
new file mode 100644
index 00000000..b4e68ddd
--- /dev/null
+++ b/src/adw-entry-row.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval <msandova protonmail com>
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include "adw-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_ENTRY_ROW (adw_entry_row_get_type())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_DERIVABLE_TYPE (AdwEntryRow, adw_entry_row, ADW, ENTRY_ROW, AdwPreferencesRow)
+
+/**
+ * AdwEntryRowClass
+ * @parent_class: The parent class
+ */
+struct _AdwEntryRowClass
+{
+  AdwPreferencesRowClass parent_class;
+};
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_entry_row_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_2
+void adw_entry_row_add_prefix (AdwEntryRow *self,
+                               GtkWidget   *widget);
+ADW_AVAILABLE_IN_1_2
+void adw_entry_row_add_suffix (AdwEntryRow *self,
+                               GtkWidget   *widget);
+ADW_AVAILABLE_IN_1_2
+void adw_entry_row_remove     (AdwEntryRow *self,
+                               GtkWidget   *widget);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_entry_row_get_show_apply_button (AdwEntryRow *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_entry_row_set_show_apply_button (AdwEntryRow *self,
+                                              gboolean     show_apply_button);
+
+G_END_DECLS
diff --git a/src/adw-entry-row.ui b/src/adw-entry-row.ui
new file mode 100644
index 00000000..979200be
--- /dev/null
+++ b/src/adw-entry-row.ui
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwEntryRow" parent="AdwPreferencesRow">
+    <accessibility>
+      <relation name="labelled-by">title</relation>
+    </accessibility>
+    <property name="activatable">True</property>
+    <child>
+      <object class="GtkGestureClick">
+        <signal name="pressed" handler="pressed_cb"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox" id="header">
+        <property name="valign">center</property>
+        <style>
+          <class name="header"/>
+        </style>
+        <child>
+          <object class="GtkBox" id="prefixes">
+            <property name="visible">False</property>
+            <style>
+              <class name="prefixes"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="AdwGizmo" id="editable_area">
+            <property name="hexpand">True</property>
+            <property name="overflow">hidden</property>
+            <child>
+              <object class="GtkLabel" id="empty_title">
+                <property name="ellipsize">end</property>
+                <property name="halign">start</property>
+                <property name="lines">0</property>
+                <property name="xalign">0</property>
+                <property name="label" bind-source="AdwEntryRow" bind-property="title" 
bind-flags="sync-create"/>
+                <property name="mnemonic-widget">AdwEntryRow</property>
+                <property name="use-underline" bind-source="AdwEntryRow" bind-property="use-underline" 
bind-flags="sync-create"/>
+                <property name="can-target">False</property>
+                <style>
+                  <class name="title"/>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="title">
+                <property name="ellipsize">end</property>
+                <property name="halign">start</property>
+                <property name="xalign">0</property>
+                <property name="opacity">0</property>
+                <property name="label" bind-source="AdwEntryRow" bind-property="title" 
bind-flags="sync-create"/>
+                <property name="mnemonic-widget">AdwEntryRow</property>
+                <property name="use-underline" bind-source="AdwEntryRow" bind-property="use-underline" 
bind-flags="sync-create"/>
+                <property name="can-target">False</property>
+                <style>
+                  <class name="subtitle"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkText" id="text">
+                <property name="enable-undo">True</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="max-length">0</property>
+                <property name="opacity">0</property>
+                <signal name="activate" handler="text_activated_cb" swapped="yes"/>
+                <signal name="state-flags-changed" handler="text_state_flags_changed_cb" swapped="yes"/>
+                <signal name="keynav-failed" handler="text_keynav_failed_cb" swapped="yes"/>
+                <signal name="changed" handler="text_changed_cb" swapped="yes"/>
+                <signal name="notify::editable" handler="update_empty" swapped="yes"/>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage" id="indicator">
+            <property name="visible">False</property>
+            <property name="valign">center</property>
+            <style>
+              <class name="indicator"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="apply_button">
+            <property name="visible">False</property>
+            <property name="valign">center</property>
+            <property name="icon-name">adw-entry-apply-symbolic</property>
+            <property name="tooltip-text" translatable="yes">Apply</property>
+            <property name="focus-on-click">False</property>
+            <signal name="clicked" handler="apply_button_clicked_cb" swapped="yes"/>
+             <style>
+              <class name="suggested-action"/>
+              <class name="circular"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage" id="edit_icon">
+            <property name="valign">center</property>
+            <property name="can-target">False</property>
+            <property name="icon-name">document-edit-symbolic</property>
+            <style>
+              <class name="edit-icon"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="suffixes">
+            <property name="visible">False</property>
+            <style>
+              <class name="suffixes"/>
+            </style>
+          </object>
+        </child>
+     </object>
+    </child>
+    <style>
+      <class name="entry"/>
+    </style>
+  </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 21524a9e..7f4a4236 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -3,6 +3,7 @@
   <gresource prefix="/org/gnome/Adwaita">
     <file>glsl/fade.glsl</file>
     <file>glsl/mask.glsl</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/adw-entry-apply-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-expander-arrow-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/avatar-default-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-icon-missing-symbolic.svg</file>
@@ -10,6 +11,7 @@
   <gresource prefix="/org/gnome/Adwaita/ui">
     <file preprocess="xml-stripblanks">adw-action-row.ui</file>
     <file preprocess="xml-stripblanks">adw-combo-row.ui</file>
+    <file preprocess="xml-stripblanks">adw-entry-row.ui</file>
     <file preprocess="xml-stripblanks">adw-expander-row.ui</file>
     <file preprocess="xml-stripblanks">adw-inspector-page.ui</file>
     <file preprocess="xml-stripblanks">adw-preferences-group.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 3371b1c9..2cb03ee1 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -39,6 +39,7 @@ G_BEGIN_DECLS
 #include "adw-combo-row.h"
 #include "adw-deprecation-macros.h"
 #include "adw-easing.h"
+#include "adw-entry-row.h"
 #include "adw-enum-list-model.h"
 #include "adw-expander-row.h"
 #include "adw-flap.h"
diff --git a/src/icons/scalable/actions/adw-entry-apply-symbolic.svg 
b/src/icons/scalable/actions/adw-entry-apply-symbolic.svg
new file mode 100644
index 00000000..d7322c52
--- /dev/null
+++ b/src/icons/scalable/actions/adw-entry-apply-symbolic.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg"; 
sodipodi:docname="list-row-ok-symbolic.svg" inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" 
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"; 
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"; xmlns:svg="http://www.w3.org/2000/svg";>
+    <sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" 
inkscape:pageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" showgrid="false" 
inkscape:zoom="1" inkscape:cx="8" inkscape:cy="8" inkscape:window-width="1920" inkscape:window-height="1011" 
inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="svg13"/>
+    <path d="m 15.503914 2.3847768 a 1.5 1.5 0 0 0 -2.11914 0.11132 l -7.9433602 8.8242202 l -2.88086 
-2.8808602 a 1.5 1.5 0 0 0 -2.12109997 0 a 1.5 1.5 0 0 0 0 2.1211002 l 3.99999997 4 a 1.50015 1.50015 0 0 0 
2.17578 -0.0566 l 9.0000002 -10.0000002 a 1.5 1.5 0 0 0 -0.11132 -2.11914 z" fill="#2e3436" 
stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
diff --git a/src/meson.build b/src/meson.build
index 19d9231a..886d3edd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -98,6 +98,7 @@ src_headers = [
   'adw-combo-row.h',
   'adw-deprecation-macros.h',
   'adw-easing.h',
+  'adw-entry-row.h',
   'adw-enum-list-model.h',
   'adw-expander-row.h',
   'adw-flap.h',
@@ -158,6 +159,7 @@ src_sources = [
   'adw-clamp-scrollable.c',
   'adw-combo-row.c',
   'adw-easing.c',
+  'adw-entry-row.c',
   'adw-enum-list-model.c',
   'adw-expander-row.c',
   'adw-flap.c',
diff --git a/src/stylesheet/widgets/_lists.scss b/src/stylesheet/widgets/_lists.scss
index 5b43c202..cb01d209 100644
--- a/src/stylesheet/widgets/_lists.scss
+++ b/src/stylesheet/widgets/_lists.scss
@@ -105,6 +105,33 @@ row {
   }
 }
 
+/***************
+ * AdwEntryRow *
+ ***************/
+
+row.entry {
+  @include focus-ring($focus-state: '.focused', $offset: -1px);
+
+  &:not(:selected).activatable.focused:hover,
+  &:not(:selected).activatable.focused:active {
+    background-color: transparent;
+  }
+
+  .edit-icon, .indicator {
+    min-width: 24px;
+    min-height: 24px;
+    padding: 5px;
+  }
+
+  .edit-icon:disabled {
+    opacity: $strong_disabled_opacity;
+  }
+
+  .indicator {
+    opacity: $dimmer_opacity;
+  }
+}
+
 /***************
  * AdwComboRow *
  ***************/


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]