[fractal/fractal-next] components: Create EntryRow and PasswordEntryRow



commit 7bf16d34f596e657276a9cd25ba6f66694b094de
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Sun Feb 13 14:20:02 2022 +0100

    components: Create EntryRow and PasswordEntryRow

 data/resources/resources.gresource.xml             |   2 +
 data/resources/style.css                           |  84 +++++
 data/resources/ui/components-entry-row.ui          |  69 ++++
 data/resources/ui/components-password-entry-row.ui | 100 ++++++
 src/components/entry_row.rs                        | 374 +++++++++++++++++++++
 src/components/mod.rs                              |   4 +
 src/components/password_entry_row.rs               | 374 +++++++++++++++++++++
 src/utils.rs                                       |  10 +
 8 files changed, 1017 insertions(+)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 2ac70f3fd..c43246189 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -21,7 +21,9 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-auth-dialog.ui">ui/components-auth-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-avatar.ui">ui/components-avatar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-editable-avatar.ui">ui/components-editable-avatar.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-entry-row.ui">ui/components-entry-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-loading-listbox-row.ui">ui/components-loading-listbox-row.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="components-password-entry-row.ui">ui/components-password-entry-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-reaction-chooser.ui">ui/components-reaction-chooser.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="components-video-player.ui">ui/components-video-player.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="content-divider-row.ui">ui/content-divider-row.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index 5dab47a24..2725cc6e3 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -36,6 +36,14 @@ button.opaque.success {
   background-color: @success_bg_color;
 }
 
+.extra-large-icon {
+  -gtk-icon-size: 128px;
+}
+
+.extra-large-icon.error {
+  color: @error_bg_color;
+}
+
 
 /* Components */
 
@@ -83,6 +91,82 @@ button.opaque.success {
   background-color: @yellow_5;
 }
 
+row.entry {
+  transition-property: outline, outline-width, outline-offset, outline-color;
+  transition-duration: 300ms;
+  animation-timing-function: ease-in-out;
+  outline: 0 solid transparent;
+  outline-offset: 2px;
+  border-top: 1px solid transparent;
+  border-left: 1px solid transparent;
+  border-right: 1px solid transparent;
+}
+
+row.entry:focus-within {
+  outline-color: alpha(@accent_color, 0.5);
+  outline-width: 2px;
+  outline-offset: -2px;
+}
+
+row.entry.success {
+  border: 1px solid @success_color;
+}
+
+row.entry.success:focus-within {
+  outline-color: @success_color;
+}
+
+row.entry.warning {
+  border: 1px solid @warning_color;
+}
+
+row.entry.warning:focus-within {
+  outline-color: @warning_color;
+}
+
+row.entry.error {
+  border: 1px solid @error_color;
+}
+
+row.entry.error:focus-within {
+  outline-color: @error_color;
+}
+
+row.entry .hint {
+  font-size: 0.8em;
+}
+
+row.entry .header {
+  margin-top: 6px;
+  margin-bottom: 6px;
+}
+
+row.entry text:disabled,
+row.entry text placeholder {
+  opacity: 0.5;
+}
+
+row.entry levelbar.discrete block {
+  min-height: 5px;
+}
+
+row.entry.accent levelbar.discrete block.filled {
+  background-color: @accent_color;
+}
+
+row.entry.success levelbar.discrete block.filled {
+  background-color: @success_color;
+}
+
+row.entry.warning levelbar.discrete block.filled {
+  background-color: @warning_color;
+}
+
+row.entry.error levelbar.discrete block.filled {
+  background-color: @error_color;
+}
+
+
 /* Login */
 
 login {
diff --git a/data/resources/ui/components-entry-row.ui b/data/resources/ui/components-entry-row.ui
new file mode 100644
index 000000000..495b93916
--- /dev/null
+++ b/data/resources/ui/components-entry-row.ui
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsEntryRow" parent="AdwPreferencesRow">
+    <style>
+      <class name="entry"/>
+    </style>
+    <property name="activatable">false</property>
+    <property name="focusable">false</property>
+    <property name="selectable">false</property>
+    <child>
+      <object class="GtkBox">
+        <style>
+          <class name="header"/>
+        </style>
+        <property name="spacing">12</property>
+        <property name="valign">center</property>
+        <child>
+          <object class="GtkBox">
+            <style>
+              <class name="title"/>
+            </style>
+            <property name="orientation">vertical</property>
+            <property name="hexpand">true</property>
+            <property name="spacing">2</property>
+            <child>
+              <object class="GtkLabel">
+                <style>
+                  <class name="subtitle"/>
+                </style>
+                <property name="xalign">0.0</property>
+                <property name="ellipsize">end</property>
+                <property name="label" bind-source="ComponentsEntryRow" bind-property="title" 
bind-flags="sync-create"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkText" id="entry"/>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <style>
+                      <class name="hint"/>
+                    </style>
+                    <binding name="visible">
+                      <closure type="gboolean" function="string_not_empty">
+                        <lookup name="hint">ComponentsEntryRow</lookup>
+                      </closure>
+                    </binding>
+                    <property name="xalign">0.0</property>
+                    <property name="ellipsize">end</property>
+                    <property name="label" bind-source="ComponentsEntryRow" bind-property="hint" 
bind-flags="sync-create"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="ComponentsActionButton" id="action_button">
+            <property name="icon-name">document-edit-symbolic</property>
+            <property name="action-name">entry-row.activate</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/resources/ui/components-password-entry-row.ui 
b/data/resources/ui/components-password-entry-row.ui
new file mode 100644
index 000000000..4cff111ed
--- /dev/null
+++ b/data/resources/ui/components-password-entry-row.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ComponentsPasswordEntryRow" parent="AdwPreferencesRow">
+    <style>
+      <class name="entry"/>
+    </style>
+    <property name="activatable">false</property>
+    <property name="focusable">false</property>
+    <property name="selectable">false</property>
+    <child>
+      <object class="GtkBox">
+        <style>
+          <class name="header"/>
+        </style>
+        <property name="spacing">12</property>
+        <property name="valign">center</property>
+        <child>
+          <object class="GtkBox">
+            <style>
+              <class name="title"/>
+            </style>
+            <property name="orientation">vertical</property>
+            <property name="hexpand">true</property>
+            <property name="spacing">2</property>
+            <child>
+              <object class="GtkLabel">
+                <style>
+                  <class name="subtitle"/>
+                </style>
+                <property name="xalign">0.0</property>
+                <property name="ellipsize">end</property>
+                <property name="label" bind-source="ComponentsPasswordEntryRow" bind-property="title" 
bind-flags="sync-create"/>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkText" id="entry">
+                    <property name="buffer">
+                      <object class="GtkPasswordEntryBuffer" />
+                    </property>
+                    <property name="input-purpose">GTK_INPUT_PURPOSE_PASSWORD</property>
+                    <property name="visibility">false</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLevelBar" id="progress">
+                    <property name="visible">false</property>
+                    <property name="margin-top">2</property>
+                    <property name="margin-bottom">1</property>
+                    <property name="mode">discrete</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <style>
+                      <class name="hint"/>
+                    </style>
+                    <binding name="visible">
+                      <closure type="gboolean" function="string_not_empty">
+                        <lookup name="hint">ComponentsPasswordEntryRow</lookup>
+                      </closure>
+                    </binding>
+                    <property name="xalign">0.0</property>
+                    <property name="ellipsize">end</property>
+                    <property name="label" bind-source="ComponentsPasswordEntryRow" bind-property="hint" 
bind-flags="sync-create"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="halign">end</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="ComponentsActionButton" id="action_button">
+                <property name="icon-name">document-edit-symbolic</property>
+                <property name="action-name">entry-row.activate</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkToggleButton">
+                <style>
+                  <class name="flat"/>
+                  <class name="circular"/>
+                </style>
+                <property name="valign">center</property>
+                <property name="icon-name">view-reveal-symbolic</property>
+                <property name="active" bind-source="entry" bind-property="visibility" 
bind-flags="sync-create|bidirectional"/>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/components/entry_row.rs b/src/components/entry_row.rs
new file mode 100644
index 000000000..5d42236b4
--- /dev/null
+++ b/src/components/entry_row.rs
@@ -0,0 +1,374 @@
+use adw::subclass::prelude::*;
+use gtk::{
+    gdk, glib,
+    glib::{clone, closure_local},
+    prelude::*,
+    subclass::prelude::*,
+    CompositeTemplate,
+};
+
+use super::{ActionButton, ActionState};
+use crate::utils::TemplateCallbacks;
+
+mod imp {
+    use std::cell::RefCell;
+
+    use glib::subclass::{InitializingObject, Signal};
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-entry-row.ui")]
+    pub struct EntryRow {
+        #[template_child]
+        pub entry: TemplateChild<gtk::Text>,
+        #[template_child]
+        pub action_button: TemplateChild<ActionButton>,
+        /// The hint of the entry.
+        pub hint: RefCell<String>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for EntryRow {
+        const NAME: &'static str = "ComponentsEntryRow";
+        type Type = super::EntryRow;
+        type ParentType = adw::PreferencesRow;
+
+        fn class_init(klass: &mut Self::Class) {
+            ActionButton::static_type();
+            Self::bind_template(klass);
+            TemplateCallbacks::bind_template_callbacks(klass);
+
+            klass.install_action("entry-row.activate", None, move |widget, _, _| {
+                let priv_ = widget.imp();
+                if priv_.action_button.state() == ActionState::Default {
+                    priv_.entry.grab_focus();
+                } else {
+                    widget.emit_by_name::<()>("activated", &[]);
+                }
+            });
+            klass.install_action("entry-row.cancel", None, move |widget, _, _| {
+                widget.emit_by_name::<()>("cancel", &[]);
+            });
+            klass.add_binding_action(
+                gdk::Key::Escape,
+                gdk::ModifierType::empty(),
+                "entry-row.cancel",
+                None,
+            );
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for EntryRow {
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![
+                    Signal::builder(
+                        "focused",
+                        &[bool::static_type().into()],
+                        <()>::static_type().into(),
+                    )
+                    .build(),
+                    Signal::builder("activated", &[], <()>::static_type().into()).build(),
+                    Signal::builder("cancel", &[], <()>::static_type().into()).build(),
+                    Signal::builder("changed", &[], <()>::static_type().into()).build(),
+                ]
+            });
+            SIGNALS.as_ref()
+        }
+
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpecString::new(
+                        "text",
+                        "Text",
+                        "The value of the entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecString::new(
+                        "placeholder-text",
+                        "Placeholder Text",
+                        "The placeholder text for the entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecEnum::new(
+                        "input-purpose",
+                        "Input Purpose",
+                        "Purpose of the entry",
+                        gtk::InputPurpose::static_type(),
+                        0,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecFlags::new(
+                        "input-hints",
+                        "Input Hints",
+                        "Additional hints that allow input methods to fine-tune their behavior",
+                        gtk::InputHints::static_type(),
+                        0,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecString::new(
+                        "hint",
+                        "Hint",
+                        "The hint of the entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "entry-sensitive",
+                        "Entry Sensitive",
+                        "Whether the entry is sensitive",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecEnum::new(
+                        "action-state",
+                        "Action State",
+                        "The state of the entry action button",
+                        ActionState::static_type(),
+                        ActionState::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "action-sensitive",
+                        "Action Sensitive",
+                        "Whether the action button is sensitive",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "text" => obj.set_text(value.get().unwrap()),
+                "placeholder-text" => obj.set_placeholder_text(value.get().unwrap()),
+                "input-purpose" => obj.set_input_purpose(value.get().unwrap()),
+                "input-hints" => obj.set_input_hints(value.get().unwrap()),
+                "hint" => obj.set_hint(value.get().unwrap()),
+                "entry-sensitive" => obj.set_entry_sensitive(value.get().unwrap()),
+                "action-state" => obj.set_action_state(value.get().unwrap()),
+                "action-sensitive" => obj.set_action_sensitive(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "text" => obj.text().to_value(),
+                "placeholder-text" => obj.placeholder_text().to_value(),
+                "input-purpose" => obj.input_purpose().to_value(),
+                "input-hints" => obj.input_hints().to_value(),
+                "hint" => obj.hint().to_value(),
+                "entry-sensitive" => obj.entry_sensitive().to_value(),
+                "action-state" => obj.action_state().to_value(),
+                "action-sensitive" => obj.action_sensitive().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            self.entry
+                .connect_has_focus_notify(clone!(@weak obj => move |entry| {
+                    obj.emit_by_name::<()>("focused", &[&entry.has_focus()]);
+                }));
+            self.entry.connect_changed(clone!(@weak obj => move |_| {
+                obj.emit_by_name::<()>("changed", &[]);
+            }));
+            self.entry.connect_activate(clone!(@weak obj => move |_| {
+                obj.emit_by_name::<()>("activated", &[]);
+            }));
+            self.action_button.set_extra_classes(&["flat"]);
+        }
+    }
+
+    impl WidgetImpl for EntryRow {
+        fn grab_focus(&self, _obj: &Self::Type) -> bool {
+            self.entry.grab_focus()
+        }
+    }
+
+    impl ListBoxRowImpl for EntryRow {}
+    impl PreferencesRowImpl for EntryRow {}
+}
+
+glib::wrapper! {
+    /// An entry usable as an `AdwPreferencesRow`.
+    pub struct EntryRow(ObjectSubclass<imp::EntryRow>)
+        @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, @implements gtk::Accessible;
+}
+
+impl EntryRow {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create EntryRow")
+    }
+
+    pub fn text(&self) -> glib::GString {
+        self.imp().entry.text()
+    }
+
+    pub fn set_text(&self, text: &str) {
+        if self.text() == text {
+            return;
+        }
+
+        self.imp().entry.set_text(text);
+        self.notify("text");
+    }
+
+    pub fn placeholder_text(&self) -> Option<glib::GString> {
+        self.imp().entry.placeholder_text()
+    }
+
+    pub fn set_placeholder_text(&self, text: Option<&str>) {
+        if self.placeholder_text().as_deref() == text {
+            return;
+        }
+
+        self.imp().entry.set_placeholder_text(text);
+        self.notify("placeholder-text");
+    }
+
+    pub fn input_purpose(&self) -> gtk::InputPurpose {
+        self.imp().entry.input_purpose()
+    }
+
+    pub fn set_input_purpose(&self, purpose: gtk::InputPurpose) {
+        if self.input_purpose() == purpose {
+            return;
+        }
+
+        self.imp().entry.set_input_purpose(purpose);
+        self.notify("input-purpose");
+    }
+
+    pub fn input_hints(&self) -> gtk::InputHints {
+        self.imp().entry.input_hints()
+    }
+
+    pub fn set_input_hints(&self, hints: gtk::InputHints) {
+        if self.input_hints() == hints {
+            return;
+        }
+
+        self.imp().entry.set_input_hints(hints);
+        self.notify("input-hints");
+    }
+
+    pub fn hint(&self) -> String {
+        self.imp().hint.borrow().to_owned()
+    }
+
+    pub fn set_hint(&self, hint: &str) {
+        if self.hint() == hint {
+            return;
+        }
+
+        self.imp().hint.replace(hint.to_owned());
+        self.notify("hint");
+    }
+
+    pub fn entry_sensitive(&self) -> bool {
+        self.imp().entry.is_sensitive()
+    }
+
+    pub fn set_entry_sensitive(&self, sensitive: bool) {
+        if self.entry_sensitive() == sensitive {
+            return;
+        }
+
+        self.imp().entry.set_sensitive(sensitive);
+        self.notify("entry-sensitive");
+    }
+
+    pub fn action_state(&self) -> ActionState {
+        self.imp().action_button.state()
+    }
+
+    pub fn set_action_state(&self, state: ActionState) {
+        if self.action_state() == state {
+            return;
+        }
+
+        self.imp().action_button.set_state(state);
+        self.notify("action-state");
+    }
+
+    pub fn action_sensitive(&self) -> bool {
+        self.imp().action_button.is_sensitive()
+    }
+
+    pub fn set_action_sensitive(&self, sensitive: bool) {
+        if self.action_sensitive() == sensitive {
+            return;
+        }
+
+        self.imp().action_button.set_sensitive(sensitive);
+        self.notify("action-sensitive");
+    }
+
+    pub fn connect_focused<F: Fn(&Self, bool) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "focused",
+            true,
+            closure_local!(move |obj: Self, focused: bool| {
+                f(&obj, focused);
+            }),
+        )
+    }
+
+    pub fn connect_activated<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "activated",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+
+    pub fn connect_cancel<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "cancel",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+
+    pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "changed",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+}
+
+impl Default for EntryRow {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 9f127fd75..fbc952162 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -6,9 +6,11 @@ mod badge;
 mod context_menu_bin;
 mod custom_entry;
 mod editable_avatar;
+mod entry_row;
 mod in_app_notification;
 mod label_with_widgets;
 mod loading_listbox_row;
+mod password_entry_row;
 mod pill;
 mod reaction_chooser;
 mod room_title;
@@ -26,9 +28,11 @@ pub use self::{
     context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
     custom_entry::CustomEntry,
     editable_avatar::EditableAvatar,
+    entry_row::EntryRow,
     in_app_notification::InAppNotification,
     label_with_widgets::LabelWithWidgets,
     loading_listbox_row::LoadingListBoxRow,
+    password_entry_row::PasswordEntryRow,
     pill::Pill,
     reaction_chooser::ReactionChooser,
     room_title::RoomTitle,
diff --git a/src/components/password_entry_row.rs b/src/components/password_entry_row.rs
new file mode 100644
index 000000000..ef85ae9a4
--- /dev/null
+++ b/src/components/password_entry_row.rs
@@ -0,0 +1,374 @@
+use adw::subclass::prelude::*;
+use gtk::{
+    gdk, glib,
+    glib::{clone, closure_local},
+    prelude::*,
+    subclass::prelude::*,
+    CompositeTemplate,
+};
+
+use super::{ActionButton, ActionState};
+use crate::utils::TemplateCallbacks;
+
+mod imp {
+    use std::cell::RefCell;
+
+    use glib::subclass::{InitializingObject, Signal};
+    use once_cell::sync::Lazy;
+
+    use super::*;
+
+    #[derive(Debug, Default, CompositeTemplate)]
+    #[template(resource = "/org/gnome/FractalNext/components-password-entry-row.ui")]
+    pub struct PasswordEntryRow {
+        #[template_child]
+        pub entry: TemplateChild<gtk::Text>,
+        #[template_child]
+        pub progress: TemplateChild<gtk::LevelBar>,
+        #[template_child]
+        pub action_button: TemplateChild<ActionButton>,
+        /// The hint of the entry.
+        pub hint: RefCell<String>,
+    }
+
+    #[glib::object_subclass]
+    impl ObjectSubclass for PasswordEntryRow {
+        const NAME: &'static str = "ComponentsPasswordEntryRow";
+        type Type = super::PasswordEntryRow;
+        type ParentType = adw::PreferencesRow;
+
+        fn class_init(klass: &mut Self::Class) {
+            ActionButton::static_type();
+            Self::bind_template(klass);
+            TemplateCallbacks::bind_template_callbacks(klass);
+
+            klass.install_action("entry-row.activate", None, move |widget, _, _| {
+                let priv_ = widget.imp();
+                if priv_.action_button.state() == ActionState::Default {
+                    priv_.entry.grab_focus();
+                } else {
+                    widget.emit_by_name::<()>("activated", &[]);
+                }
+            });
+            klass.install_action("entry-row.cancel", None, move |widget, _, _| {
+                widget.emit_by_name::<()>("cancel", &[]);
+            });
+            klass.add_binding_action(
+                gdk::Key::Escape,
+                gdk::ModifierType::empty(),
+                "entry-row.cancel",
+                None,
+            );
+        }
+
+        fn instance_init(obj: &InitializingObject<Self>) {
+            obj.init_template();
+        }
+    }
+
+    impl ObjectImpl for PasswordEntryRow {
+        fn signals() -> &'static [Signal] {
+            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
+                vec![
+                    Signal::builder(
+                        "focused",
+                        &[bool::static_type().into()],
+                        <()>::static_type().into(),
+                    )
+                    .build(),
+                    Signal::builder("activated", &[], <()>::static_type().into()).build(),
+                    Signal::builder("cancel", &[], <()>::static_type().into()).build(),
+                    Signal::builder("changed", &[], <()>::static_type().into()).build(),
+                ]
+            });
+            SIGNALS.as_ref()
+        }
+
+        fn properties() -> &'static [glib::ParamSpec] {
+            static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+                vec![
+                    glib::ParamSpecString::new(
+                        "text",
+                        "Text",
+                        "The value of the entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecString::new(
+                        "hint",
+                        "Hint",
+                        "The hint of the entry",
+                        None,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "entry-sensitive",
+                        "Entry Sensitive",
+                        "Whether the entry is sensitive",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecEnum::new(
+                        "action-state",
+                        "Action State",
+                        "The state of the entry action button",
+                        ActionState::static_type(),
+                        ActionState::default() as i32,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "action-sensitive",
+                        "Action Sensitive",
+                        "Whether the action button is sensitive",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecBoolean::new(
+                        "progress-visible",
+                        "Progress Visible",
+                        "Whether the progress is visible",
+                        true,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                    glib::ParamSpecDouble::new(
+                        "progress-value",
+                        "Progress Value",
+                        "The value of the progress bar",
+                        f64::MIN,
+                        f64::MAX,
+                        0.0,
+                        glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+                    ),
+                ]
+            });
+
+            PROPERTIES.as_ref()
+        }
+
+        fn set_property(
+            &self,
+            obj: &Self::Type,
+            _id: usize,
+            value: &glib::Value,
+            pspec: &glib::ParamSpec,
+        ) {
+            match pspec.name() {
+                "text" => obj.set_text(value.get().unwrap()),
+                "hint" => obj.set_hint(value.get().unwrap()),
+                "entry-sensitive" => obj.set_entry_sensitive(value.get().unwrap()),
+                "action-state" => obj.set_action_state(value.get().unwrap()),
+                "action-sensitive" => obj.set_action_sensitive(value.get().unwrap()),
+                "progress-visible" => obj.set_progress_visible(value.get().unwrap()),
+                "progress-value" => obj.set_progress_value(value.get().unwrap()),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+            match pspec.name() {
+                "text" => obj.text().to_value(),
+                "hint" => obj.hint().to_value(),
+                "entry-sensitive" => obj.entry_sensitive().to_value(),
+                "action-state" => obj.action_state().to_value(),
+                "action-sensitive" => obj.action_sensitive().to_value(),
+                "progress-visible" => obj.progress_visible().to_value(),
+                "progress-value" => obj.progress_value().to_value(),
+                _ => unimplemented!(),
+            }
+        }
+
+        fn constructed(&self, obj: &Self::Type) {
+            self.parent_constructed(obj);
+
+            self.entry
+                .connect_has_focus_notify(clone!(@weak obj => move |entry| {
+                    obj.emit_by_name::<()>("focused", &[&entry.has_focus()]);
+                }));
+            self.entry.connect_changed(clone!(@weak obj => move |_| {
+                obj.emit_by_name::<()>("changed", &[]);
+            }));
+            self.entry.connect_activate(clone!(@weak obj => move |_| {
+                obj.emit_by_name::<()>("activated", &[]);
+            }));
+            self.action_button.set_extra_classes(&["flat"]);
+        }
+    }
+
+    impl WidgetImpl for PasswordEntryRow {
+        fn grab_focus(&self, _obj: &Self::Type) -> bool {
+            self.entry.grab_focus()
+        }
+    }
+
+    impl ListBoxRowImpl for PasswordEntryRow {}
+    impl PreferencesRowImpl for PasswordEntryRow {}
+}
+
+glib::wrapper! {
+    /// A password entry usable as an `AdwPreferencesRow`.
+    pub struct PasswordEntryRow(ObjectSubclass<imp::PasswordEntryRow>)
+        @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, @implements gtk::Accessible;
+}
+
+impl PasswordEntryRow {
+    pub fn new() -> Self {
+        glib::Object::new(&[]).expect("Failed to create PasswordEntryRow")
+    }
+
+    pub fn text(&self) -> glib::GString {
+        self.imp().entry.text()
+    }
+
+    pub fn set_text(&self, text: &str) {
+        if self.text() == text {
+            return;
+        }
+
+        self.imp().entry.set_text(text);
+        self.notify("text");
+    }
+
+    pub fn hint(&self) -> String {
+        self.imp().hint.borrow().to_owned()
+    }
+
+    pub fn set_hint(&self, hint: &str) {
+        if self.hint() == hint {
+            return;
+        }
+
+        self.imp().hint.replace(hint.to_owned());
+        self.notify("hint");
+    }
+
+    pub fn entry_sensitive(&self) -> bool {
+        self.imp().entry.is_sensitive()
+    }
+
+    pub fn set_entry_sensitive(&self, sensitive: bool) {
+        if self.entry_sensitive() == sensitive {
+            return;
+        }
+
+        self.imp().entry.set_sensitive(sensitive);
+        self.notify("entry-sensitive");
+    }
+
+    pub fn action_state(&self) -> ActionState {
+        self.imp().action_button.state()
+    }
+
+    pub fn set_action_state(&self, state: ActionState) {
+        if self.action_state() == state {
+            return;
+        }
+
+        self.imp().action_button.set_state(state);
+        self.notify("action-state");
+    }
+
+    pub fn action_sensitive(&self) -> bool {
+        self.imp().action_button.is_sensitive()
+    }
+
+    pub fn set_action_sensitive(&self, sensitive: bool) {
+        if self.action_sensitive() == sensitive {
+            return;
+        }
+
+        self.imp().action_button.set_sensitive(sensitive);
+        self.notify("action-sensitive");
+    }
+
+    pub fn progress_visible(&self) -> bool {
+        self.imp().progress.is_visible()
+    }
+
+    pub fn set_progress_visible(&self, visible: bool) {
+        if self.progress_visible() == visible {
+            return;
+        }
+
+        self.imp().progress.set_visible(visible);
+        self.notify("progress-visible");
+    }
+
+    /// Set the steps of the progress bar.
+    ///
+    /// Each step is defined as a unique name. Use the
+    /// `LEVEL_BAR_OFFSET_*` variables as names to benefit from the default
+    /// colors.
+    ///
+    /// If one of the `accent`, `success`, `warning` or `error` style class is
+    /// applied on the row, the progress bar will use the same color.
+    ///
+    /// The progress value will have to be set between `0.0` and `steps.len()`.
+    pub fn define_progress_steps(&self, steps: &[&str]) {
+        let progress = &self.imp().progress;
+        progress.set_min_value(0.0);
+        progress.set_max_value(steps.len() as f64);
+
+        for (value, name) in steps.iter().enumerate() {
+            progress.add_offset_value(name, value as f64 + 1.0);
+        }
+    }
+
+    pub fn progress_value(&self) -> f64 {
+        self.imp().progress.value()
+    }
+
+    pub fn set_progress_value(&self, value: f64) {
+        if self.progress_value() == value {
+            return;
+        }
+
+        self.imp().progress.set_value(value);
+        self.notify("progress-value");
+    }
+
+    pub fn connect_focused<F: Fn(&Self, bool) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "focused",
+            true,
+            closure_local!(move |obj: Self, focused: bool| {
+                f(&obj, focused);
+            }),
+        )
+    }
+
+    pub fn connect_activated<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "activated",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+
+    pub fn connect_cancel<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "cancel",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+
+    pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
+        self.connect_closure(
+            "changed",
+            true,
+            closure_local!(move |obj: Self| {
+                f(&obj);
+            }),
+        )
+    }
+}
+
+impl Default for PasswordEntryRow {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/utils.rs b/src/utils.rs
index 450ea9aab..4cafc6753 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -235,3 +235,13 @@ pub async fn timeout_future<T>(
         _ => Err(TimeoutFuture::Timeout),
     }
 }
+
+pub struct TemplateCallbacks {}
+
+#[gtk::template_callbacks(functions)]
+impl TemplateCallbacks {
+    #[template_callback]
+    fn string_not_empty(string: Option<&str>) -> bool {
+        !string.unwrap_or_default().is_empty()
+    }
+}


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