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 @@ Lists - + + + 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. combo-row +## Entry Rows + +[class@EntryRow] is a row with an embedded entry. It can have prefix and suffix +widgets, and an apply button. + + + + entry-row + + +## 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. + + + + password-entry-row + + ## 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 expander-row ](class.ExpanderRow.html) +### Entry Row + +[ + + entry-row +](class.EntryRow.html) + +### Password Entry Row + +[ + + 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. + * + * + * + * entry-row + * + * + * `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 @@ + + + + + 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. + * + * + * + * password-entry-row + * + * + * 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(); +}