Skip to main content

fractal/components/rows/
substring_entry_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{
3    glib,
4    glib::{clone, closure_local},
5    pango,
6};
7
8use crate::components::LoadingButton;
9
10mod imp {
11    use std::{
12        cell::{Cell, RefCell},
13        marker::PhantomData,
14        sync::LazyLock,
15    };
16
17    use glib::subclass::{InitializingObject, Signal};
18
19    use super::*;
20
21    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
22    #[template(resource = "/org/gnome/Fractal/ui/components/rows/substring_entry_row.ui")]
23    #[properties(wrapper_type = super::SubstringEntryRow)]
24    pub struct SubstringEntryRow {
25        #[template_child]
26        header: TemplateChild<gtk::Box>,
27        #[template_child]
28        main_content: TemplateChild<gtk::Box>,
29        #[template_child]
30        entry_box: TemplateChild<gtk::Box>,
31        #[template_child]
32        text: TemplateChild<gtk::Text>,
33        #[template_child]
34        title: TemplateChild<gtk::Label>,
35        #[template_child]
36        edit_icon: TemplateChild<gtk::Image>,
37        #[template_child]
38        entry_prefix_label: TemplateChild<gtk::Label>,
39        #[template_child]
40        entry_suffix_label: TemplateChild<gtk::Label>,
41        #[template_child]
42        add_button: TemplateChild<LoadingButton>,
43        /// The input hints of the entry.
44        #[property(get = Self::input_hints, set = Self::set_input_hints, explicit_notify)]
45        input_hints: PhantomData<gtk::InputHints>,
46        /// The input purpose of the entry.
47        #[property(get = Self::input_purpose, set = Self::set_input_purpose, explicit_notify, builder(gtk::InputPurpose::FreeForm))]
48        input_purpose: PhantomData<gtk::InputPurpose>,
49        /// A list of Pango attributes to apply to the text of the entry.
50        #[property(get = Self::attributes, set = Self::set_attributes, explicit_notify, nullable)]
51        attributes: PhantomData<Option<pango::AttrList>>,
52        /// The placeholder text of the entry.
53        #[property(get = Self::placeholder_text, set = Self::set_placeholder_text, explicit_notify, nullable)]
54        placeholder_text: PhantomData<Option<glib::GString>>,
55        /// The length of the text of the entry.
56        #[property(get = Self::text_length)]
57        text_length: PhantomData<u32>,
58        /// The prefix text of the entry.
59        #[property(get = Self::prefix_text, set = Self::set_prefix_text, explicit_notify)]
60        prefix_text: PhantomData<glib::GString>,
61        /// The suffix text of the entry.
62        #[property(get = Self::suffix_text, set = Self::set_suffix_text, explicit_notify)]
63        suffix_text: PhantomData<glib::GString>,
64        /// Set the accessible description of the entry.
65        ///
66        /// If it is not set, the placeholder text will be used.
67        #[property(get, set = Self::set_accessible_description, explicit_notify, nullable)]
68        accessible_description: RefCell<Option<String>>,
69        /// Whether the add button is hidden.
70        #[property(get = Self::hide_add_button, set = Self::set_hide_add_button, explicit_notify)]
71        hide_add_button: PhantomData<bool>,
72        /// The tooltip text of the add button.
73        #[property(get = Self::add_button_tooltip_text, set = Self::set_add_button_tooltip_text, explicit_notify, nullable)]
74        add_button_tooltip_text: PhantomData<Option<glib::GString>>,
75        /// The accessible label of the add button.
76        #[property(get, set = Self::set_add_button_accessible_label, explicit_notify, nullable)]
77        add_button_accessible_label: RefCell<Option<String>>,
78        /// Whether to prevent the add button from being activated.
79        #[property(get, set = Self::set_inhibit_add, explicit_notify)]
80        inhibit_add: Cell<bool>,
81        /// Whether this row is loading.
82        #[property(get = Self::is_loading, set = Self::set_is_loading, explicit_notify)]
83        is_loading: PhantomData<bool>,
84    }
85
86    #[glib::object_subclass]
87    impl ObjectSubclass for SubstringEntryRow {
88        const NAME: &'static str = "SubstringEntryRow";
89        type Type = super::SubstringEntryRow;
90        type ParentType = adw::PreferencesRow;
91        type Interfaces = (gtk::Editable,);
92
93        fn class_init(klass: &mut Self::Class) {
94            Self::bind_template(klass);
95            Self::bind_template_callbacks(klass);
96        }
97
98        fn instance_init(obj: &InitializingObject<Self>) {
99            obj.init_template();
100        }
101    }
102
103    impl ObjectImpl for SubstringEntryRow {
104        fn signals() -> &'static [Signal] {
105            static SIGNALS: LazyLock<Vec<Signal>> =
106                LazyLock::new(|| vec![Signal::builder("add").build()]);
107            SIGNALS.as_ref()
108        }
109
110        fn properties() -> &'static [glib::ParamSpec] {
111            Self::derived_properties()
112        }
113
114        fn set_property(&self, id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
115            // In case this is a property that's automatically added for Editable
116            // implementations.
117            if !self.delegate_set_property(id, value, pspec) {
118                self.derived_set_property(id, value, pspec);
119            }
120        }
121
122        fn property(&self, id: usize, pspec: &glib::ParamSpec) -> glib::Value {
123            // In case this is a property that's automatically added for Editable
124            // implementations.
125            if let Some(value) = self.delegate_get_property(id, pspec) {
126                value
127            } else {
128                self.derived_property(id, pspec)
129            }
130        }
131
132        fn constructed(&self) {
133            self.parent_constructed();
134            let obj = self.obj();
135
136            obj.init_delegate();
137
138            self.text.buffer().connect_length_notify(clone!(
139                #[weak]
140                obj,
141                move |_| {
142                    obj.notify_text_length();
143                }
144            ));
145        }
146
147        fn dispose(&self) {
148            self.obj().finish_delegate();
149        }
150    }
151
152    impl WidgetImpl for SubstringEntryRow {
153        fn grab_focus(&self) -> bool {
154            self.text.grab_focus()
155        }
156    }
157
158    impl ListBoxRowImpl for SubstringEntryRow {}
159    impl PreferencesRowImpl for SubstringEntryRow {}
160
161    impl EditableImpl for SubstringEntryRow {
162        fn delegate(&self) -> Option<gtk::Editable> {
163            Some(self.text.clone().upcast())
164        }
165    }
166
167    #[gtk::template_callbacks]
168    impl SubstringEntryRow {
169        /// The input hints of the entry.
170        fn input_hints(&self) -> gtk::InputHints {
171            self.text.input_hints()
172        }
173
174        /// Set the input hints of the entry.
175        fn set_input_hints(&self, input_hints: gtk::InputHints) {
176            if self.input_hints() == input_hints {
177                return;
178            }
179
180            self.text.set_input_hints(input_hints);
181            self.obj().notify_input_hints();
182        }
183
184        /// The input purpose of the entry.
185        fn input_purpose(&self) -> gtk::InputPurpose {
186            self.text.input_purpose()
187        }
188
189        /// Set the input purpose of the entry.
190        fn set_input_purpose(&self, input_purpose: gtk::InputPurpose) {
191            if self.input_purpose() == input_purpose {
192                return;
193            }
194
195            self.text.set_input_purpose(input_purpose);
196            self.obj().notify_input_purpose();
197        }
198
199        /// A list of Pango attributes to apply to the text of the entry.
200        fn attributes(&self) -> Option<pango::AttrList> {
201            self.text.attributes()
202        }
203
204        /// Set the list of Pango attributes to apply to the text of the entry.
205        fn set_attributes(&self, attributes: Option<&pango::AttrList>) {
206            if self.attributes().as_ref() == attributes {
207                return;
208            }
209
210            self.text.set_attributes(attributes);
211            self.obj().notify_attributes();
212        }
213
214        /// The placeholder text of the entry.
215        fn placeholder_text(&self) -> Option<glib::GString> {
216            self.text.placeholder_text()
217        }
218
219        /// Set the placeholder text of the entry.
220        fn set_placeholder_text(&self, text: Option<&str>) {
221            if self.placeholder_text().as_deref() == text {
222                return;
223            }
224
225            self.text.set_placeholder_text(text);
226
227            self.update_accessible_description();
228            self.obj().notify_placeholder_text();
229        }
230
231        /// The length of the text of the entry.
232        fn text_length(&self) -> u32 {
233            self.text.text_length().into()
234        }
235
236        /// The prefix text of the entry.
237        fn prefix_text(&self) -> glib::GString {
238            self.entry_prefix_label.label()
239        }
240
241        /// Set the prefix text of the entry.
242        fn set_prefix_text(&self, text: &str) {
243            if self.prefix_text() == text {
244                return;
245            }
246
247            self.entry_prefix_label.set_label(text);
248            self.obj().notify_prefix_text();
249        }
250
251        /// The suffix text of the entry.
252        fn suffix_text(&self) -> glib::GString {
253            self.entry_suffix_label.label()
254        }
255
256        /// Set the suffix text of the entry.
257        fn set_suffix_text(&self, text: &str) {
258            if self.suffix_text() == text {
259                return;
260            }
261
262            self.entry_suffix_label.set_label(text);
263            self.obj().notify_suffix_text();
264        }
265
266        /// Set the accessible description of the entry.
267        fn set_accessible_description(&self, description: Option<String>) {
268            if *self.accessible_description.borrow() == description {
269                return;
270            }
271
272            self.accessible_description.replace(description);
273
274            self.update_accessible_description();
275            self.obj().notify_accessible_description();
276        }
277
278        /// Whether the add button is hidden.
279        fn hide_add_button(&self) -> bool {
280            !self.add_button.is_visible()
281        }
282
283        /// Set whether the add button is hidden.
284        fn set_hide_add_button(&self, hide: bool) {
285            if self.hide_add_button() == hide {
286                return;
287            }
288
289            self.add_button.set_visible(!hide);
290            self.obj().notify_hide_add_button();
291        }
292
293        /// The tooltip text of the add button.
294        fn add_button_tooltip_text(&self) -> Option<glib::GString> {
295            self.add_button.tooltip_text()
296        }
297
298        /// Set the tooltip text of the add button.
299        fn set_add_button_tooltip_text(&self, tooltip_text: Option<&str>) {
300            if self.add_button_tooltip_text().as_deref() == tooltip_text {
301                return;
302            }
303
304            self.add_button.set_tooltip_text(tooltip_text);
305            self.obj().notify_add_button_tooltip_text();
306        }
307
308        /// Set the accessible label of the add button.
309        fn set_add_button_accessible_label(&self, label: Option<String>) {
310            if *self.add_button_accessible_label.borrow() == label {
311                return;
312            }
313
314            if let Some(label) = &label {
315                self.add_button
316                    .update_property(&[gtk::accessible::Property::Label(label)]);
317            } else {
318                self.add_button
319                    .reset_property(gtk::AccessibleProperty::Label);
320            }
321
322            self.add_button_accessible_label.replace(label);
323            self.obj().notify_add_button_accessible_label();
324        }
325
326        /// Set whether to prevent the add button from being activated.
327        fn set_inhibit_add(&self, inhibit: bool) {
328            if self.inhibit_add.get() == inhibit {
329                return;
330            }
331
332            self.inhibit_add.set(inhibit);
333
334            self.update_add_button();
335            self.obj().notify_inhibit_add();
336        }
337
338        /// Whether this row is loading.
339        fn is_loading(&self) -> bool {
340            self.add_button.is_loading()
341        }
342
343        /// Set whether this row is loading.
344        fn set_is_loading(&self, is_loading: bool) {
345            if self.is_loading() == is_loading {
346                return;
347            }
348
349            self.add_button.set_is_loading(is_loading);
350
351            let obj = self.obj();
352            obj.set_sensitive(!is_loading);
353            obj.notify_is_loading();
354        }
355
356        /// Update the accessible description.
357        fn update_accessible_description(&self) {
358            let description = self
359                .accessible_description
360                .borrow()
361                .clone()
362                .or(self.placeholder_text().map(Into::into));
363
364            if let Some(description) = description {
365                self.text
366                    .update_property(&[gtk::accessible::Property::Description(&description)]);
367            } else {
368                self.text
369                    .reset_property(gtk::AccessibleProperty::Description);
370            }
371        }
372
373        /// Whether the text input is focused.
374        fn is_text_focused(&self) -> bool {
375            self.text
376                .state_flags()
377                .contains(gtk::StateFlags::FOCUS_WITHIN)
378        }
379
380        /// Update this row when the text input flags changed.
381        #[template_callback]
382        fn text_state_flags_changed(&self) {
383            let obj = self.obj();
384            let editing = self.is_text_focused();
385
386            if editing {
387                obj.add_css_class("focused");
388            } else {
389                obj.remove_css_class("focused");
390            }
391
392            self.edit_icon.set_visible(!editing);
393        }
394
395        /// Handle when the key navigation in the text input failed.
396        #[template_callback]
397        fn text_keynav_failed(&self, direction: gtk::DirectionType) -> bool {
398            if matches!(
399                direction,
400                gtk::DirectionType::Left | gtk::DirectionType::Right
401            ) {
402                return self.obj().child_focus(direction);
403            }
404
405            // gdk::EVENT_PROPAGATE == 0;
406            false
407        }
408
409        /// Handle when this row is pressed.
410        #[template_callback]
411        fn pressed(&self, _n_press: i32, x: f64, y: f64, gesture: &gtk::Gesture) {
412            let obj = self.obj();
413            let picked = obj.pick(x, y, gtk::PickFlags::DEFAULT);
414
415            if picked.is_some_and(|w| {
416                w != *obj || w != *self.header || w != *self.main_content || w != *self.entry_box
417            }) {
418                gesture.set_state(gtk::EventSequenceState::Denied);
419
420                return;
421            }
422
423            self.text.grab_focus_without_selecting();
424
425            gesture.set_state(gtk::EventSequenceState::Claimed);
426        }
427
428        /// Whether we can activate the add button.
429        fn can_add(&self) -> bool {
430            !self.inhibit_add.get() && !self.obj().text().is_empty()
431        }
432
433        /// Update the state of the add button.
434        #[template_callback]
435        fn update_add_button(&self) {
436            self.add_button.set_sensitive(self.can_add());
437        }
438
439        /// Emit the `add` signal.
440        #[template_callback]
441        fn add(&self) {
442            if !self.can_add() {
443                return;
444            }
445
446            self.obj().emit_by_name::<()>("add", &[]);
447        }
448    }
449}
450
451glib::wrapper! {
452    /// A `AdwPreferencesRow` with an embedded text entry, and a fixed text suffix and prefix.
453    ///
454    /// It also has a built-in "Add" button, making it an almost drop-in replacement to `EntryAddRow`.
455    pub struct SubstringEntryRow(ObjectSubclass<imp::SubstringEntryRow>)
456        @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow,
457        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable, gtk::Editable;
458}
459
460impl SubstringEntryRow {
461    pub fn new() -> Self {
462        glib::Object::new()
463    }
464
465    /// Connect to the signal emitted when the "Add" button is activated.
466    pub fn connect_add<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
467        self.connect_closure(
468            "add",
469            true,
470            closure_local!(move |obj: Self| {
471                f(&obj);
472            }),
473        )
474    }
475}