fractal/components/rows/
combo_loading_row.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::clone, pango, CompositeTemplate};
3
4use crate::{components::LoadingBin, utils::BoundObject};
5
6mod imp {
7 use std::{
8 cell::{Cell, RefCell},
9 marker::PhantomData,
10 };
11
12 use glib::subclass::InitializingObject;
13
14 use super::*;
15
16 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
17 #[template(resource = "/org/gnome/Fractal/ui/components/rows/combo_loading_row.ui")]
18 #[properties(wrapper_type = super::ComboLoadingRow)]
19 pub struct ComboLoadingRow {
20 #[template_child]
21 loading_bin: TemplateChild<LoadingBin>,
22 #[template_child]
23 popover: TemplateChild<gtk::Popover>,
24 #[template_child]
25 list: TemplateChild<gtk::ListBox>,
26 #[property(get, set = Self::set_string_model, explicit_notify, nullable)]
28 string_model: BoundObject<gtk::StringList>,
29 #[property(get, default = gtk::INVALID_LIST_POSITION)]
31 selected: Cell<u32>,
32 #[property(get, set = Self::set_selected_string, explicit_notify, nullable)]
34 selected_string: RefCell<Option<String>>,
35 #[property(get = Self::is_loading, set = Self::set_is_loading)]
37 is_loading: PhantomData<bool>,
38 #[property(get, set = Self::set_read_only, explicit_notify)]
40 read_only: Cell<bool>,
41 selected_handlers: RefCell<Vec<glib::SignalHandlerId>>,
42 }
43
44 #[glib::object_subclass]
45 impl ObjectSubclass for ComboLoadingRow {
46 const NAME: &'static str = "ComboLoadingRow";
47 type Type = super::ComboLoadingRow;
48 type ParentType = adw::ActionRow;
49
50 fn class_init(klass: &mut Self::Class) {
51 Self::bind_template(klass);
52 Self::bind_template_callbacks(klass);
53
54 klass.set_accessible_role(gtk::AccessibleRole::ComboBox);
55 }
56
57 fn instance_init(obj: &InitializingObject<Self>) {
58 obj.init_template();
59 }
60 }
61
62 #[glib::derived_properties]
63 impl ObjectImpl for ComboLoadingRow {}
64
65 impl WidgetImpl for ComboLoadingRow {}
66 impl ListBoxRowImpl for ComboLoadingRow {}
67 impl PreferencesRowImpl for ComboLoadingRow {}
68
69 impl ActionRowImpl for ComboLoadingRow {
70 fn activate(&self) {
71 if !self.is_loading() {
72 self.popover.popup();
73 }
74 }
75 }
76
77 #[gtk::template_callbacks]
78 impl ComboLoadingRow {
79 fn set_string_model(&self, model: Option<gtk::StringList>) {
81 if self.string_model.obj() == model {
82 return;
83 }
84 let obj = self.obj();
85
86 for handler in self.selected_handlers.take() {
87 obj.disconnect(handler);
88 }
89 self.string_model.disconnect_signals();
90
91 self.list.bind_model(
92 model.as_ref(),
93 clone!(
94 #[weak]
95 obj,
96 #[upgrade_or_else]
97 || { gtk::ListBoxRow::new().upcast() },
98 move |item| {
99 let Some(item) = item.downcast_ref::<gtk::StringObject>() else {
100 return gtk::ListBoxRow::new().upcast();
101 };
102
103 let string = item.string();
104 let child = gtk::Box::new(gtk::Orientation::Horizontal, 6);
105
106 let label = gtk::Label::builder()
107 .xalign(0.0)
108 .ellipsize(pango::EllipsizeMode::End)
109 .max_width_chars(40)
110 .valign(gtk::Align::Center)
111 .label(string)
112 .build();
113 child.append(&label);
114
115 let icon = gtk::Image::builder()
116 .accessible_role(gtk::AccessibleRole::Presentation)
117 .icon_name("object-select-symbolic")
118 .build();
119
120 let selected_handler = obj.connect_selected_string_notify(clone!(
121 #[weak]
122 label,
123 #[weak]
124 icon,
125 move |obj| {
126 let is_selected =
127 obj.selected_string().is_some_and(|s| s == label.label());
128 let opacity = if is_selected { 1.0 } else { 0.0 };
129 icon.set_opacity(opacity);
130 }
131 ));
132 obj.imp()
133 .selected_handlers
134 .borrow_mut()
135 .push(selected_handler);
136
137 let is_selected = obj.selected_string().is_some_and(|s| s == label.label());
138 let opacity = if is_selected { 1.0 } else { 0.0 };
139 icon.set_opacity(opacity);
140 child.append(&icon);
141
142 gtk::ListBoxRow::builder().child(&child).build().upcast()
143 }
144 ),
145 );
146
147 if let Some(model) = model {
148 let items_changed_handler = model.connect_items_changed(clone!(
149 #[weak(rename_to = imp)]
150 self,
151 move |_, _, _, _| {
152 imp.update_selected();
153 }
154 ));
155
156 self.string_model.set(model, vec![items_changed_handler]);
157 }
158
159 self.update_selected();
160 obj.notify_string_model();
161 }
162
163 fn set_selected_string(&self, string: Option<String>) {
165 if *self.selected_string.borrow() == string {
166 return;
167 }
168 let obj = self.obj();
169
170 obj.set_subtitle(string.as_deref().unwrap_or_default());
171 self.selected_string.replace(string);
172
173 self.update_selected();
174 obj.notify_selected_string();
175 }
176
177 fn update_selected(&self) {
179 let mut selected = gtk::INVALID_LIST_POSITION;
180
181 if let Some((string_model, selected_string)) = self
182 .string_model
183 .obj()
184 .zip(self.selected_string.borrow().clone())
185 {
186 for (pos, item) in string_model.iter::<glib::Object>().enumerate() {
187 let Some(item) = item.ok().and_downcast::<gtk::StringObject>() else {
188 break;
190 };
191
192 if item.string() == selected_string {
193 selected = pos as u32;
194 break;
195 }
196 }
197 }
198
199 if self.selected.get() == selected {
200 return;
201 }
202
203 self.selected.set(selected);
204 self.obj().notify_selected();
205 }
206
207 fn is_loading(&self) -> bool {
209 self.loading_bin.is_loading()
210 }
211
212 fn set_is_loading(&self, loading: bool) {
214 if self.is_loading() == loading {
215 return;
216 }
217
218 self.loading_bin.set_is_loading(loading);
219 self.obj().notify_is_loading();
220 }
221
222 fn set_read_only(&self, read_only: bool) {
224 if self.read_only.get() == read_only {
225 return;
226 }
227 let obj = self.obj();
228
229 self.read_only.set(read_only);
230
231 obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]);
232 obj.notify_read_only();
233 }
234
235 #[template_callback]
237 fn row_activated(&self, row: >k::ListBoxRow) {
238 let Some(string) = row
239 .child()
240 .and_downcast::<gtk::Box>()
241 .and_then(|b| b.first_child())
242 .and_downcast::<gtk::Label>()
243 .map(|l| l.label())
244 else {
245 return;
246 };
247
248 self.popover.popdown();
249 self.set_selected_string(Some(string.into()));
250 }
251
252 #[template_callback]
254 fn popover_visible(&self) {
255 let obj = self.obj();
256 let is_visible = self.popover.is_visible();
257
258 if is_visible {
259 obj.add_css_class("has-open-popup");
260 } else {
261 obj.remove_css_class("has-open-popup");
262 }
263 }
264 }
265}
266
267glib::wrapper! {
268 pub struct ComboLoadingRow(ObjectSubclass<imp::ComboLoadingRow>)
270 @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow,
271 @implements gtk::Actionable, gtk::Accessible;
272}
273
274impl ComboLoadingRow {
275 pub fn new() -> Self {
276 glib::Object::new()
277 }
278}