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 #[property(get = Self::input_hints, set = Self::set_input_hints, explicit_notify)]
45 input_hints: PhantomData<gtk::InputHints>,
46 #[property(get = Self::input_purpose, set = Self::set_input_purpose, explicit_notify, builder(gtk::InputPurpose::FreeForm))]
48 input_purpose: PhantomData<gtk::InputPurpose>,
49 #[property(get = Self::attributes, set = Self::set_attributes, explicit_notify, nullable)]
51 attributes: PhantomData<Option<pango::AttrList>>,
52 #[property(get = Self::placeholder_text, set = Self::set_placeholder_text, explicit_notify, nullable)]
54 placeholder_text: PhantomData<Option<glib::GString>>,
55 #[property(get = Self::text_length)]
57 text_length: PhantomData<u32>,
58 #[property(get = Self::prefix_text, set = Self::set_prefix_text, explicit_notify)]
60 prefix_text: PhantomData<glib::GString>,
61 #[property(get = Self::suffix_text, set = Self::set_suffix_text, explicit_notify)]
63 suffix_text: PhantomData<glib::GString>,
64 #[property(get, set = Self::set_accessible_description, explicit_notify, nullable)]
68 accessible_description: RefCell<Option<String>>,
69 #[property(get = Self::hide_add_button, set = Self::set_hide_add_button, explicit_notify)]
71 hide_add_button: PhantomData<bool>,
72 #[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 #[property(get, set = Self::set_add_button_accessible_label, explicit_notify, nullable)]
77 add_button_accessible_label: RefCell<Option<String>>,
78 #[property(get, set = Self::set_inhibit_add, explicit_notify)]
80 inhibit_add: Cell<bool>,
81 #[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 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 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 fn input_hints(&self) -> gtk::InputHints {
171 self.text.input_hints()
172 }
173
174 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 fn input_purpose(&self) -> gtk::InputPurpose {
186 self.text.input_purpose()
187 }
188
189 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 fn attributes(&self) -> Option<pango::AttrList> {
201 self.text.attributes()
202 }
203
204 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 fn placeholder_text(&self) -> Option<glib::GString> {
216 self.text.placeholder_text()
217 }
218
219 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 fn text_length(&self) -> u32 {
233 self.text.text_length().into()
234 }
235
236 fn prefix_text(&self) -> glib::GString {
238 self.entry_prefix_label.label()
239 }
240
241 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 fn suffix_text(&self) -> glib::GString {
253 self.entry_suffix_label.label()
254 }
255
256 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 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 fn hide_add_button(&self) -> bool {
280 !self.add_button.is_visible()
281 }
282
283 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 fn add_button_tooltip_text(&self) -> Option<glib::GString> {
295 self.add_button.tooltip_text()
296 }
297
298 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 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 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 fn is_loading(&self) -> bool {
340 self.add_button.is_loading()
341 }
342
343 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 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 fn is_text_focused(&self) -> bool {
375 self.text
376 .state_flags()
377 .contains(gtk::StateFlags::FOCUS_WITHIN)
378 }
379
380 #[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 #[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 false
407 }
408
409 #[template_callback]
411 fn pressed(&self, _n_press: i32, x: f64, y: f64, gesture: >k::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 fn can_add(&self) -> bool {
430 !self.inhibit_add.get() && !self.obj().text().is_empty()
431 }
432
433 #[template_callback]
435 fn update_add_button(&self) {
436 self.add_button.set_sensitive(self.can_add());
437 }
438
439 #[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 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 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}