diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui
index f2b4339ccaadb154be6013c36c60f9ea709a3a9e..4ce61d67dc2fe9e2834bbaf402221102a9ce595f 100644
--- a/demo/adw-demo-window.ui
+++ b/demo/adw-demo-window.ui
@@ -138,7 +138,9 @@
diff --git a/demo/pages/avatar/adw-demo-page-avatar.c b/demo/pages/avatar/adw-demo-page-avatar.c
index fb935e063980d5533d9fb75b005b5e8b71a858b7..e5f0d42acb8089367819b11b6def010e05abb564 100644
--- a/demo/pages/avatar/adw-demo-page-avatar.c
+++ b/demo/pages/avatar/adw-demo-page-avatar.c
@@ -7,7 +7,7 @@ struct _AdwDemoPageAvatar
AdwBin parent_instance;
AdwAvatar *avatar;
- GtkEntry *text;
+ AdwEntryRow *text;
GtkLabel *file_chooser_label;
GtkListBox *contacts;
};
diff --git a/demo/pages/avatar/adw-demo-page-avatar.ui b/demo/pages/avatar/adw-demo-page-avatar.ui
index b3acea242680d2c4cc507933c05aec270eb39715..0bd9ff944d916bae07005dab50562c2c80e93812 100644
--- a/demo/pages/avatar/adw-demo-page-avatar.ui
+++ b/demo/pages/avatar/adw-demo-page-avatar.ui
@@ -66,13 +66,8 @@
-
+
Text
-
-
- center
-
-
diff --git a/demo/pages/lists/adw-demo-page-lists.c b/demo/pages/lists/adw-demo-page-lists.c
index 594fa2ca9363ca3ddea29fa65d741c210ffaff01..b3a782f0f3d0dd0e5d8945affa73934d6d18abc7 100644
--- a/demo/pages/lists/adw-demo-page-lists.c
+++ b/demo/pages/lists/adw-demo-page-lists.c
@@ -7,14 +7,40 @@ struct _AdwDemoPageLists
AdwBin parent_instance;
};
+enum {
+ SIGNAL_ADD_TOAST,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
G_DEFINE_TYPE (AdwDemoPageLists, adw_demo_page_lists, ADW_TYPE_BIN)
+static void
+entry_apply_cb (AdwDemoPageLists *self)
+{
+ AdwToast *toast = adw_toast_new ("Changes applied");
+
+ g_signal_emit (self, signals[SIGNAL_ADD_TOAST], 0, toast);
+}
+
static void
adw_demo_page_lists_class_init (AdwDemoPageListsClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ signals[SIGNAL_ADD_TOAST] =
+ g_signal_new ("add-toast",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ ADW_TYPE_TOAST);
+
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Adwaita1/Demo/ui/pages/lists/adw-demo-page-lists.ui");
+
+ gtk_widget_class_bind_template_callback (widget_class, entry_apply_cb);
}
static void
diff --git a/demo/pages/lists/adw-demo-page-lists.ui b/demo/pages/lists/adw-demo-page-lists.ui
index 3286dbe71d60db529b376acad47c5dc6e73b2671..03780e56ba72bcd4ba07168c8468e7aa0a8b4397 100644
--- a/demo/pages/lists/adw-demo-page-lists.ui
+++ b/demo/pages/lists/adw-demo-page-lists.ui
@@ -66,6 +66,47 @@
+
+
+ Entry Rows
+
+
+ Entry Row
+ True
+
+
+
+
+
+
+
+
+ Entry With Confirmation
+ True
+
+
+
+
+
+ Entry With Suffix
+
+
+ center
+ edit-copy-symbolic
+
+
+
+
+
+
+
+ Password Entry
+
+
+
+
Combo Rows
diff --git a/doc/boxed-lists.md b/doc/boxed-lists.md
index 640c83092608f868cfeb645c521b9e780d6a9ade..6c9b912e65c1ade283a71bb1c2b308806ddfa84b 100644
--- a/doc/boxed-lists.md
+++ b/doc/boxed-lists.md
@@ -78,6 +78,27 @@ other rows.
+## Entry Rows
+
+[class@EntryRow] is a row with an embedded entry. It can have prefix and suffix
+widgets, and an apply button.
+
+
+
+
+
+
+## Password Entry Rows
+
+[class@PasswordEntryRow] is a variant of [class@EntryRow] tailored for entering
+secrets. It conceals the text and provides a button to show it, along with a
+Caps Lock indicator.
+
+
+
+
+
+
## Preferences Group
[class@PreferencesGroup] provides a boxed list along with a title and a
diff --git a/doc/images/combo-row-dark.png b/doc/images/combo-row-dark.png
index 2d3960542fe4161e4e6e3dfa977cc564e5358a82..056001934d18cf456d507e889d6bc594b093cad6 100644
Binary files a/doc/images/combo-row-dark.png and b/doc/images/combo-row-dark.png differ
diff --git a/doc/images/combo-row.png b/doc/images/combo-row.png
index 6a6496fcc9b3ba5ee96d7a58686e3f15c4340a9f..9f57e74b1ddde8f557cc299a51f56315cbca1a7f 100644
Binary files a/doc/images/combo-row.png and b/doc/images/combo-row.png differ
diff --git a/doc/images/entry-row-dark.png b/doc/images/entry-row-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..ae7c99e491ff210e199614b4b00d45e52ae0783f
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 0000000000000000000000000000000000000000..9460af4c8052d27cf3b753efa7f83cf74e58006e
Binary files /dev/null and b/doc/images/entry-row.png differ
diff --git a/doc/images/password-entry-row-dark.png b/doc/images/password-entry-row-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..607ea433642203e7614ff4c8dc7c8edabce0ed02
Binary files /dev/null and b/doc/images/password-entry-row-dark.png differ
diff --git a/doc/images/password-entry-row.png b/doc/images/password-entry-row.png
new file mode 100644
index 0000000000000000000000000000000000000000..bfff08799dec959bb208fb26d9161fa03cb8a50f
Binary files /dev/null and b/doc/images/password-entry-row.png differ
diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in
index 14b7db2f67036db151c6c4613acd9b66cb8dae9d..76ae6c2dbe886144d1bacb3e03c5041e5c4d32d4 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",
@@ -152,6 +154,8 @@ content_images = [
"images/osd-progress-bar-dark.png",
"images/osd-toolbar.png",
"images/osd-toolbar-dark.png",
+ "images/password-entry-row.png",
+ "images/password-entry-row-dark.png",
"images/popover-menu-list.png",
"images/popover-menu-list-dark.png",
"images/preferences-group.png",
diff --git a/doc/tools/data/entry-row.ui b/doc/tools/data/entry-row.ui
new file mode 100644
index 0000000000000000000000000000000000000000..df3e9571a35bd682c26aaa2d62f3785b0f3670e2
--- /dev/null
+++ b/doc/tools/data/entry-row.ui
@@ -0,0 +1,23 @@
+
+
+
+
+
+ 6
+ 6
+ 6
+ 6
+ none
+ 400
+
+
+
+ Title
+ Text
+ False
+
+
+
+
diff --git a/doc/tools/data/password-entry-row.ui b/doc/tools/data/password-entry-row.ui
new file mode 100644
index 0000000000000000000000000000000000000000..cbd82ac9cd8b82760e2cdc187271e705f8b5e93a
--- /dev/null
+++ b/doc/tools/data/password-entry-row.ui
@@ -0,0 +1,23 @@
+
+
+
+
+
+ 6
+ 6
+ 6
+ 6
+ none
+ 400
+
+
+
+ Title
+ A long password
+ False
+
+
+
+
diff --git a/doc/visual-index.md b/doc/visual-index.md
index db200c2b7f91b19d64cb1c86c74a34aa1b6735d7..b2597403258ad878c20c7ca58964745df809759d 100644
--- a/doc/visual-index.md
+++ b/doc/visual-index.md
@@ -49,6 +49,20 @@ Slug: visual-index
](class.ExpanderRow.html)
+### Entry Row
+
+[
+
+
+](class.EntryRow.html)
+
+### Password Entry Row
+
+[
+
+
+](class.PasswordEntryRow.html)
+
## Preferences
### Preferences Group
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 4910cfb83d602df078490352d6893cc9ae9e9599..31db95b2dad9414b38ffd052cb83bb995ed847d7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,6 +1,8 @@
# 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-password-entry-row.c
src/adw-preferences-window.c
src/adw-preferences-window.ui
diff --git a/src/adw-entry-row-private.h b/src/adw-entry-row-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..dd827a0e38be350c9432ac06559b58c5be6a508d
--- /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 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 0000000000000000000000000000000000000000..502d6e989fda66a75d415f80bde6ef2a3a137dd3
--- /dev/null
+++ b/src/adw-entry-row.c
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ * 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 150
+#define TITLE_SPACING 3
+
+/**
+ * AdwEntryRow:
+ *
+ * A [class@Gtk.ListBoxRow] with an embedded text entry.
+ *
+ *
+ *
+ *
+ *
+ *
+ * `AdwEntryRow` has a title that doubles as placeholder text. It shows an icon
+ * indicating that it's editable and can receive additional widgets before or
+ * after the editable part.
+ *
+ * If [property@EntryRow:show-apply-button] is set to `TRUE`, `AdwEntryRow` can
+ * show an apply button when editing its contents. This can be useful if
+ * changing its contents can result in an expensive operation, such as network
+ * activity.
+ *
+ * `AdwEntryRow` provides only minimal API and should be used with the
+ * [iface@Gtk.Editable] API.
+ *
+ * See also [class@PasswordEntryRow].
+ *
+ * ## AdwEntryRow as GtkBuildable
+ *
+ * The `AdwEntryRow` implementation of the [iface@Gtk.Buildable] interface
+ * supports adding a child at its end by specifying “suffix” or omitting the
+ * “type” attribute of a element.
+ *
+ * It also supports adding a child as a prefix widget by specifying “prefix” as
+ * the “type” attribute of a element.
+ *
+ * ## CSS nodes
+ *
+ * `AdwEntryRow` has a single 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.
+ *
+ * When set to `TRUE`, typing text in the entry will reveal an apply button.
+ * Clicking it or pressing the Enter key will hide the button and
+ * emit the [signal@EntryRow::apply] signal.
+ *
+ * This is useful if changing the entry contents can trigger an expensive
+ * operation, e.g. network activity, to avoid triggering it after typing every
+ * character.
+ *
+ * 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 pressed.
+ *
+ * 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 @self can show the apply button.
+ *
+ * Returns: whether to show the apply button
+ *
+ * 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 to show the apply button
+ *
+ * Sets whether @self can show the apply button.
+ *
+ * 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;
+
+ if (priv->show_apply_button == show_apply_button)
+ return;
+
+ 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 0000000000000000000000000000000000000000..b4e68ddd0d54c42ca1fe5ff9e0974df43f88f5ed
--- /dev/null
+++ b/src/adw-entry-row.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only 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 0000000000000000000000000000000000000000..979200be080505c14f1745fd70783263520c5dcb
--- /dev/null
+++ b/src/adw-entry-row.ui
@@ -0,0 +1,126 @@
+
+
+
+
+
+ title
+
+ True
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/adw-gizmo-private.h b/src/adw-gizmo-private.h
index ce667c15e08f117dcbaaa21e098569f7ecc93b40..f9dd638a056061f9e9fb6ca9bbb8c7704ad5d45d 100644
--- a/src/adw-gizmo-private.h
+++ b/src/adw-gizmo-private.h
@@ -58,4 +58,17 @@ GtkWidget *adw_gizmo_new_with_role (const char *css_name,
AdwGizmoFocusFunc focus_func,
AdwGizmoGrabFocusFunc grab_focus_func) G_GNUC_WARN_UNUSED_RESULT;
+void adw_gizmo_set_measure_func (AdwGizmo *self,
+ AdwGizmoMeasureFunc measure_func);
+void adw_gizmo_set_allocate_func (AdwGizmo *self,
+ AdwGizmoAllocateFunc allocate_func);
+void adw_gizmo_set_snapshot_func (AdwGizmo *self,
+ AdwGizmoSnapshotFunc snapshot_func);
+void adw_gizmo_set_contains_func (AdwGizmo *self,
+ AdwGizmoContainsFunc contains_func);
+void adw_gizmo_set_focus_func (AdwGizmo *self,
+ AdwGizmoFocusFunc focus_func);
+void adw_gizmo_set_grab_focus_func (AdwGizmo *self,
+ AdwGizmoGrabFocusFunc grab_focus_func);
+
G_END_DECLS
diff --git a/src/adw-gizmo.c b/src/adw-gizmo.c
index cdebb73eedc5a6111f831a66f54a4d7f4633498a..df1695e01462cc3be24c37d6c53c9e400dc7607a 100644
--- a/src/adw-gizmo.c
+++ b/src/adw-gizmo.c
@@ -189,3 +189,53 @@ adw_gizmo_new_with_role (const char *css_name,
return GTK_WIDGET (gizmo);
}
+
+void
+adw_gizmo_set_measure_func (AdwGizmo *self,
+ AdwGizmoMeasureFunc measure_func)
+{
+ self->measure_func = measure_func;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+void
+adw_gizmo_set_allocate_func (AdwGizmo *self,
+ AdwGizmoAllocateFunc allocate_func)
+{
+ self->allocate_func = allocate_func;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+void
+adw_gizmo_set_snapshot_func (AdwGizmo *self,
+ AdwGizmoSnapshotFunc snapshot_func)
+{
+ self->snapshot_func = snapshot_func;
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+void
+adw_gizmo_set_contains_func (AdwGizmo *self,
+ AdwGizmoContainsFunc contains_func)
+{
+ self->contains_func = contains_func;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+void
+adw_gizmo_set_focus_func (AdwGizmo *self,
+ AdwGizmoFocusFunc focus_func)
+{
+ self->focus_func = focus_func;
+}
+
+void
+adw_gizmo_set_grab_focus_func (AdwGizmo *self,
+ AdwGizmoGrabFocusFunc grab_focus_func)
+{
+ self->grab_focus_func = grab_focus_func;
+}
diff --git a/src/adw-password-entry-row.c b/src/adw-password-entry-row.c
new file mode 100644
index 0000000000000000000000000000000000000000..ee97b2d137cb83317cf647f4ec9f2f177dea9a4c
--- /dev/null
+++ b/src/adw-password-entry-row.c
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+#include
+
+#include "adw-password-entry-row.h"
+
+#include "adw-entry-row-private.h"
+#include "adw-macros-private.h"
+
+/**
+ * AdwPasswordEntryRow:
+ *
+ * A [class@EntryRow] tailored for entering secrets.
+ *
+ *
+ *
+ *
+ *
+ *
+ * It does not show its contents in clear text, does not allow to copy it to the
+ * clipboard, and shows a warning when Caps Lock is engaged. If the underlying
+ * platform allows it, `AdwPasswordEntryRow` will also place the text in a
+ * non-pageable memory area, to avoid it being written out to disk by the
+ * operating system.
+ *
+ * It offer a way to reveal the contents in clear text.
+ *
+ * ## CSS Nodes
+ *
+ * `AdwPasswordEntryRow` has a single CSS node with name `row` that carries
+ * `.entry` and `.password` style classes.
+ *
+ * Since: 1.2
+ */
+
+struct _AdwPasswordEntryRow
+{
+ AdwEntryRow parent_instance;
+
+ GtkWidget *show_text_toggle;
+
+ GdkDevice *keyboard;
+};
+
+G_DEFINE_FINAL_TYPE (AdwPasswordEntryRow, adw_password_entry_row, ADW_TYPE_ENTRY_ROW)
+
+static void
+update_caps_lock (AdwPasswordEntryRow *self)
+{
+ GtkEditable *delegate = gtk_editable_get_delegate (GTK_EDITABLE (self));
+
+ adw_entry_row_set_show_indicator (ADW_ENTRY_ROW (self),
+ !gtk_text_get_visibility (GTK_TEXT (delegate)) &&
+ gdk_device_get_caps_lock_state (self->keyboard));
+}
+
+static void
+notify_visibility_cb (AdwPasswordEntryRow *self)
+{
+ GtkEditable *delegate = gtk_editable_get_delegate (GTK_EDITABLE (self));
+
+ if (gtk_text_get_visibility (GTK_TEXT (delegate))) {
+ gtk_button_set_icon_name (GTK_BUTTON (self->show_text_toggle),
+ "view-conceal-symbolic");
+ gtk_widget_set_tooltip_text (self->show_text_toggle, _("Hide Text"));
+ } else {
+ gtk_button_set_icon_name (GTK_BUTTON (self->show_text_toggle),
+ "view-reveal-symbolic");
+ gtk_widget_set_tooltip_text (self->show_text_toggle, _("Show Text"));
+ }
+
+ if (self->keyboard)
+ update_caps_lock (self);
+}
+
+static void
+notify_has_focus_cb (AdwPasswordEntryRow *self)
+{
+ if (self->keyboard)
+ update_caps_lock (self);
+}
+
+static void
+show_text_clicked_cb (AdwPasswordEntryRow *self)
+{
+ GtkEditable *delegate = gtk_editable_get_delegate (GTK_EDITABLE (self));
+ gboolean visible = gtk_text_get_visibility (GTK_TEXT (delegate));
+
+ gtk_text_set_visibility (GTK_TEXT (delegate), !visible);
+}
+
+static void
+adw_password_entry_row_realize (GtkWidget *widget)
+{
+ AdwPasswordEntryRow *self = ADW_PASSWORD_ENTRY_ROW (widget);
+ GdkSeat *seat;
+
+ GTK_WIDGET_CLASS (adw_password_entry_row_parent_class)->realize (widget);
+
+ seat = gdk_display_get_default_seat (gtk_widget_get_display (widget));
+ if (seat)
+ self->keyboard = gdk_seat_get_keyboard (seat);
+
+ if (self->keyboard) {
+ g_signal_connect_swapped (self->keyboard, "notify::caps-lock-state",
+ G_CALLBACK (update_caps_lock), self);
+ update_caps_lock (self);
+ }
+}
+
+static void
+adw_password_entry_row_dispose (GObject *object)
+{
+ AdwPasswordEntryRow *self = ADW_PASSWORD_ENTRY_ROW (object);
+
+ if (self->keyboard)
+ g_signal_handlers_disconnect_by_func (self->keyboard, update_caps_lock, self);
+
+ G_OBJECT_CLASS (adw_password_entry_row_parent_class)->dispose (object);
+}
+
+static void
+adw_password_entry_row_class_init (AdwPasswordEntryRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_password_entry_row_dispose;
+
+ widget_class->realize = adw_password_entry_row_realize;
+}
+
+static void
+adw_password_entry_row_init (AdwPasswordEntryRow *self)
+{
+ GtkEditable *delegate;
+ GMenu *menu;
+ GMenu *section;
+ GMenuItem *item;
+
+ self->show_text_toggle = gtk_button_new ();
+ gtk_widget_set_valign (self->show_text_toggle, GTK_ALIGN_CENTER);
+ gtk_widget_set_focus_on_click (self->show_text_toggle, FALSE);
+ gtk_widget_add_css_class (self->show_text_toggle, "flat");
+ adw_entry_row_add_suffix (ADW_ENTRY_ROW (self), self->show_text_toggle);
+
+ delegate = gtk_editable_get_delegate (GTK_EDITABLE (self));
+
+ g_assert (GTK_IS_TEXT (delegate));
+
+ gtk_text_set_visibility (GTK_TEXT (delegate), FALSE);
+ gtk_text_set_buffer (GTK_TEXT (delegate), gtk_password_entry_buffer_new ());
+
+ g_signal_connect_swapped (delegate, "notify::has-focus",
+ G_CALLBACK (notify_has_focus_cb), self);
+ g_signal_connect_swapped (delegate, "notify::visibility",
+ G_CALLBACK (notify_visibility_cb), self);
+ g_signal_connect_swapped (self->show_text_toggle, "clicked",
+ G_CALLBACK (show_text_clicked_cb), self);
+
+ adw_entry_row_set_indicator_icon_name (ADW_ENTRY_ROW (self), "caps-lock-symbolic");
+ adw_entry_row_set_indicator_tooltip (ADW_ENTRY_ROW (self), _("Caps Lock is on"));
+
+ gtk_widget_add_css_class (GTK_WIDGET (self), "password");
+
+ notify_visibility_cb (self);
+
+ menu = g_menu_new ();
+ section = g_menu_new ();
+ item = g_menu_item_new (_("_Show Text"), "misc.toggle-visibility");
+ g_menu_item_set_attribute (item, "touch-icon", "s", "view-reveal-symbolic");
+ g_menu_append_item (section, item);
+
+ g_menu_append_section (menu, NULL, G_MENU_MODEL (section));
+
+ gtk_text_set_extra_menu (GTK_TEXT (delegate), G_MENU_MODEL (menu));
+
+ g_object_unref (item);
+ g_object_unref (section);
+ g_object_unref (menu);
+}
+
+/**
+ * adw_password_entry_row_new:
+ *
+ * Creates a new `AdwPasswordEntryRow`.
+ *
+ * Returns: the newly created `AdwPasswordEntryRow`
+ *
+ * Since: 1.2
+ */
+GtkWidget *
+adw_password_entry_row_new (void)
+{
+ return g_object_new (ADW_TYPE_PASSWORD_ENTRY_ROW, NULL);
+}
diff --git a/src/adw-password-entry-row.h b/src/adw-password-entry-row.h
new file mode 100644
index 0000000000000000000000000000000000000000..4b9623c72dfff5f82147498ec25a846e1002b6ec
--- /dev/null
+++ b/src/adw-password-entry-row.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include
+
+#include "adw-entry-row.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_PASSWORD_ENTRY_ROW (adw_password_entry_row_get_type())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_FINAL_TYPE (AdwPasswordEntryRow, adw_password_entry_row, ADW, PASSWORD_ENTRY_ROW, AdwEntryRow)
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_password_entry_row_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+G_END_DECLS
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 21524a9edca65dc6a9975040e036ff4c18f75785..7f4a423664f1b53d4eed5b0419104d27da8d8eec 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -3,6 +3,7 @@
glsl/fade.glsl
glsl/mask.glsl
+ icons/scalable/actions/adw-entry-apply-symbolic.svg
icons/scalable/actions/adw-expander-arrow-symbolic.svg
icons/scalable/status/avatar-default-symbolic.svg
icons/scalable/status/adw-tab-icon-missing-symbolic.svg
@@ -10,6 +11,7 @@
adw-action-row.ui
adw-combo-row.ui
+ adw-entry-row.ui
adw-expander-row.ui
adw-inspector-page.ui
adw-preferences-group.ui
diff --git a/src/adwaita.h b/src/adwaita.h
index 3371b1c947519dbc57dabdeb52967da8bada20be..aa1ca1da99e2ce8e8a8c8b654d731f7a6033d3ad 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"
@@ -47,6 +48,7 @@ G_BEGIN_DECLS
#include "adw-leaflet.h"
#include "adw-main.h"
#include "adw-navigation-direction.h"
+#include "adw-password-entry-row.h"
#include "adw-preferences-group.h"
#include "adw-preferences-page.h"
#include "adw-preferences-row.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 0000000000000000000000000000000000000000..df63d41e2718275710770034b1f3a1b5900733f7
--- /dev/null
+++ b/src/icons/scalable/actions/adw-entry-apply-symbolic.svg
@@ -0,0 +1,45 @@
+
+
diff --git a/src/meson.build b/src/meson.build
index 19d9231a31c31ad23eb8fe5e8a70cea8feb4caee..0f6c5217ded2f5499528a080f4fc7181cdfe6065 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',
@@ -106,6 +107,7 @@ src_headers = [
'adw-leaflet.h',
'adw-main.h',
'adw-navigation-direction.h',
+ 'adw-password-entry-row.h',
'adw-preferences-group.h',
'adw-preferences-page.h',
'adw-preferences-row.h',
@@ -158,6 +160,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',
@@ -166,6 +169,7 @@ src_sources = [
'adw-leaflet.c',
'adw-main.c',
'adw-navigation-direction.c',
+ 'adw-password-entry-row.c',
'adw-preferences-group.c',
'adw-preferences-page.c',
'adw-preferences-row.c',
diff --git a/src/stylesheet/widgets/_lists.scss b/src/stylesheet/widgets/_lists.scss
index 5b43c202db7ed098d1486e7c1113a79414a0d68e..0bf0713504ddc6277eadb26f819e5e67396dc30a 100644
--- a/src/stylesheet/widgets/_lists.scss
+++ b/src/stylesheet/widgets/_lists.scss
@@ -85,7 +85,7 @@ row {
> box.header {
margin-left: 12px;
margin-right: 12px;
- border-spacing: 12px;
+ border-spacing: 6px;
min-height: 50px;
> .icon:disabled {
@@ -100,11 +100,44 @@ row {
> .prefixes,
> .suffixes {
- border-spacing: 12px;
+ border-spacing: 6px;
+ }
+
+ > .icon,
+ > .prefixes {
+ &:dir(ltr) { margin-right: 6px; }
+ &:dir(rtl) { margin-left: 6px; }
}
}
}
+/***************
+ * 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 *
***************/
diff --git a/tests/meson.build b/tests/meson.build
index 6c2a206639e7b11e65b3f68fb18f749f0f3ca52d..032934464b2e9ae201020b844126cbbff5055975 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -35,10 +35,12 @@ test_names = [
'test-carousel-indicator-lines',
'test-combo-row',
'test-easing',
+ 'test-entry-row',
'test-expander-row',
'test-flap',
'test-header-bar',
'test-leaflet',
+ 'test-password-entry-row',
'test-preferences-group',
'test-preferences-page',
'test-preferences-row',
diff --git a/tests/test-entry-row.c b/tests/test-entry-row.c
new file mode 100644
index 0000000000000000000000000000000000000000..24e1c3c149e194dd8496076d6e6d894d04307c5e
--- /dev/null
+++ b/tests/test-entry-row.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko
+ */
+
+#include
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_adw_entry_row_add_remove (void)
+{
+ AdwEntryRow *row = g_object_ref_sink (ADW_ENTRY_ROW (adw_entry_row_new ()));
+ GtkWidget *prefix, *suffix;
+
+ g_assert_nonnull (row);
+
+ prefix = gtk_check_button_new ();
+ g_assert_nonnull (prefix);
+
+ suffix = gtk_check_button_new ();
+ g_assert_nonnull (suffix);
+
+ adw_entry_row_add_prefix (row, prefix);
+ adw_entry_row_add_suffix (row, suffix);
+
+ adw_entry_row_remove (row, prefix);
+ adw_entry_row_remove (row, suffix);
+
+ g_assert_finalize_object (row);
+}
+
+static void
+test_adw_entry_row_show_apply_button (void)
+{
+ AdwEntryRow *row = g_object_ref_sink (ADW_ENTRY_ROW (adw_entry_row_new ()));
+ gboolean show_apply_button;
+
+ g_assert_nonnull (row);
+
+ notified = 0;
+ g_signal_connect (row, "notify::show-apply-button", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (row, "show-apply-button", &show_apply_button, NULL);
+ g_assert_false (show_apply_button);
+
+ adw_entry_row_set_show_apply_button (row, FALSE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_entry_row_set_show_apply_button (row, TRUE);
+ g_assert_true (adw_entry_row_get_show_apply_button (row));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (row, "show-apply-button", FALSE, NULL);
+ g_assert_false (adw_entry_row_get_show_apply_button (row));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (row);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func("/Adwaita/EntryRow/add_remove", test_adw_entry_row_add_remove);
+ g_test_add_func("/Adwaita/EntryRow/show_apply_button", test_adw_entry_row_show_apply_button);
+
+ return g_test_run();
+}
diff --git a/tests/test-password-entry-row.c b/tests/test-password-entry-row.c
new file mode 100644
index 0000000000000000000000000000000000000000..372babb0392af0f7400b971d3c4e6325861893a8
--- /dev/null
+++ b/tests/test-password-entry-row.c
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko
+ */
+
+#include
+
+static void
+test_adw_password_entry_row_new (void)
+{
+ GtkWidget *row = g_object_ref_sink (adw_password_entry_row_new ());
+ g_assert_nonnull (row);
+
+ g_assert_finalize_object (row);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func("/Adwaita/PasswordEntryRow/new", test_adw_password_entry_row_new);
+
+ return g_test_run();
+}