fractal/session/view/content/room_details/addresses_subpage/
completion_popover.rs1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{gdk, gio, glib, glib::clone, pango, CompositeTemplate};
3use tracing::error;
4
5use crate::utils::BoundObjectWeakRef;
6
7mod imp {
8 use std::cell::RefCell;
9
10 use glib::subclass::InitializingObject;
11
12 use super::*;
13
14 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
15 #[template(
16 resource = "/org/gnome/Fractal/ui/session/view/content/room_details/addresses_subpage/completion_popover.ui"
17 )]
18 #[properties(wrapper_type = super::CompletionPopover)]
19 pub struct CompletionPopover {
20 #[template_child]
21 list: TemplateChild<gtk::ListBox>,
22 #[property(get, set = Self::set_entry, explicit_notify, nullable)]
24 entry: BoundObjectWeakRef<gtk::Editable>,
25 entry_controller: RefCell<Option<gtk::EventControllerKey>>,
27 entry_binding: RefCell<Option<glib::Binding>>,
28 #[property(get, set = Self::set_model, explicit_notify, nullable)]
32 model: RefCell<Option<gio::ListModel>>,
33 #[property(get)]
35 filter: gtk::StringFilter,
36 #[property(get)]
38 filtered_list: gtk::FilterListModel,
39 }
40
41 #[glib::object_subclass]
42 impl ObjectSubclass for CompletionPopover {
43 const NAME: &'static str = "RoomDetailsAddressesSubpageCompletionPopover";
44 type Type = super::CompletionPopover;
45 type ParentType = gtk::Popover;
46
47 fn class_init(klass: &mut Self::Class) {
48 Self::bind_template(klass);
49 Self::bind_template_callbacks(klass);
50 }
51
52 fn instance_init(obj: &InitializingObject<Self>) {
53 obj.init_template();
54 }
55 }
56
57 #[glib::derived_properties]
58 impl ObjectImpl for CompletionPopover {
59 fn constructed(&self) {
60 self.parent_constructed();
61
62 self.filter
63 .set_expression(Some(gtk::StringObject::this_expression("string")));
64 self.filtered_list.set_filter(Some(&self.filter));
65
66 self.filtered_list.connect_items_changed(clone!(
67 #[weak(rename_to = imp)]
68 self,
69 move |_, _, _, _| {
70 imp.update_completion();
71 }
72 ));
73
74 self.list.bind_model(Some(&self.filtered_list), |item| {
75 let Some(item) = item.downcast_ref::<gtk::StringObject>() else {
76 error!("Completion has item that is not a GtkStringObject");
77 return adw::Bin::new().upcast();
78 };
79
80 let label = gtk::Label::builder()
81 .label(item.string())
82 .ellipsize(pango::EllipsizeMode::End)
83 .halign(gtk::Align::Start)
84 .build();
85
86 gtk::ListBoxRow::builder().child(&label).build().upcast()
87 });
88 }
89
90 fn dispose(&self) {
91 if let Some(entry) = self.entry.obj() {
92 if let Some(controller) = self.entry_controller.take() {
93 entry.remove_controller(&controller);
94 }
95 }
96
97 if let Some(binding) = self.entry_binding.take() {
98 binding.unbind();
99 }
100 }
101 }
102
103 impl WidgetImpl for CompletionPopover {}
104 impl PopoverImpl for CompletionPopover {}
105
106 #[gtk::template_callbacks]
107 impl CompletionPopover {
108 fn set_entry(&self, entry: Option<>k::Editable>) {
110 let prev_entry = self.entry.obj();
111
112 if prev_entry.as_ref() == entry {
113 return;
114 }
115 let obj = self.obj();
116
117 if let Some(entry) = prev_entry {
118 if let Some(controller) = self.entry_controller.take() {
119 entry.remove_controller(&controller);
120 }
121
122 obj.unparent();
123 }
124 if let Some(binding) = self.entry_binding.take() {
125 binding.unbind();
126 }
127 self.entry.disconnect_signals();
128
129 if let Some(entry) = entry {
130 let key_events = gtk::EventControllerKey::new();
131 key_events.connect_key_pressed(clone!(
132 #[weak(rename_to = imp)]
133 self,
134 #[upgrade_or]
135 glib::Propagation::Proceed,
136 move |_, key, _, modifier| imp.key_pressed(key, modifier)
137 ));
138
139 entry.add_controller(key_events.clone());
140 self.entry_controller.replace(Some(key_events));
141
142 let search_binding = entry
143 .bind_property("text", &self.filter, "search")
144 .sync_create()
145 .build();
146 self.entry_binding.replace(Some(search_binding));
147
148 let changed_handler = entry.connect_changed(clone!(
149 #[weak(rename_to = imp)]
150 self,
151 move |_| {
152 imp.update_completion();
153 }
154 ));
155
156 let state_flags_handler = entry.connect_state_flags_changed(clone!(
157 #[weak(rename_to = imp)]
158 self,
159 move |_, _| {
160 imp.update_completion();
161 }
162 ));
163
164 obj.set_parent(entry);
165 self.entry
166 .set(entry, vec![changed_handler, state_flags_handler]);
167 }
168
169 obj.notify_entry();
170 }
171
172 fn set_model(&self, model: Option<gio::ListModel>) {
174 if *self.model.borrow() == model {
175 return;
176 }
177
178 self.filtered_list.set_model(model.as_ref());
179
180 self.model.replace(model);
181 self.obj().notify_model();
182 }
183
184 fn update_completion(&self) {
186 let Some(entry) = self.entry.obj() else {
187 return;
188 };
189 let obj = self.obj();
190
191 let n_items = self.filtered_list.n_items();
192
193 if n_items == 0 {
195 if obj.is_visible() {
196 obj.popdown();
197 }
198
199 return;
200 }
201
202 if n_items == 1 {
205 if let Some(item) = self
206 .filtered_list
207 .item(0)
208 .and_downcast::<gtk::StringObject>()
209 {
210 if item.string() == entry.text() {
211 if obj.is_visible() {
212 obj.popdown();
213 }
214
215 return;
216 }
217 }
218 }
219
220 let entry_has_focus = entry.state_flags().contains(gtk::StateFlags::FOCUS_WITHIN);
222 if entry_has_focus {
223 if !obj.is_visible() {
224 obj.popup();
225 }
226 } else if obj.is_visible() {
227 obj.popdown();
228 }
229 }
230
231 fn selected_row_index(&self) -> Option<usize> {
233 let selected_row = self.list.selected_row()?;
234 let n_rows = i32::try_from(self.filtered_list.n_items()).unwrap_or(i32::MAX);
235
236 for idx in 0..n_rows {
237 let Some(row) = self.list.row_at_index(idx) else {
238 break;
239 };
240
241 if row == selected_row {
242 return Some(idx.try_into().unwrap_or_default());
243 }
244 }
245
246 None
247 }
248
249 fn select_row_at_index(&self, idx: Option<usize>) {
251 if self.selected_row_index() == idx
252 || idx >= Some(self.filtered_list.n_items() as usize)
253 {
254 return;
255 }
256
257 let row =
258 idx.and_then(|idx| self.list.row_at_index(idx.try_into().unwrap_or(i32::MAX)));
259 self.list.select_row(row.as_ref());
260 }
261
262 fn selected_text(&self) -> Option<glib::GString> {
264 Some(
265 self.list
266 .selected_row()?
267 .child()?
268 .downcast_ref::<gtk::Label>()?
269 .label(),
270 )
271 }
272
273 pub(super) fn activate_selected_row(&self) -> bool {
277 if !self.obj().is_visible() {
278 return false;
279 }
280 let Some(entry) = self.entry.obj() else {
281 return false;
282 };
283
284 let Some(selected_text) = self.selected_text() else {
285 return false;
286 };
287
288 if selected_text == entry.text() {
289 return false;
291 }
292
293 let Some(row) = self.list.selected_row() else {
294 return false;
295 };
296
297 row.activate();
298 true
299 }
300
301 fn key_pressed(&self, key: gdk::Key, modifier: gdk::ModifierType) -> glib::Propagation {
303 if !modifier.is_empty() {
304 return glib::Propagation::Proceed;
305 }
306
307 let obj = self.obj();
308
309 if obj.is_visible() {
310 if matches!(key, gdk::Key::Tab) {
311 self.update_completion();
312 return glib::Propagation::Stop;
313 }
314
315 return glib::Propagation::Proceed;
316 }
317
318 if matches!(
319 key,
320 gdk::Key::Return | gdk::Key::KP_Enter | gdk::Key::ISO_Enter
321 ) {
322 self.activate_selected_row();
324 return glib::Propagation::Stop;
325 } else if matches!(key, gdk::Key::Up | gdk::Key::KP_Up) {
326 let idx = self.selected_row_index().unwrap_or_default();
328 if idx > 0 {
329 self.select_row_at_index(Some(idx - 1));
330 }
331 return glib::Propagation::Stop;
332 } else if matches!(key, gdk::Key::Down | gdk::Key::KP_Down) {
333 let new_idx = if let Some(idx) = self.selected_row_index() {
335 idx + 1
336 } else {
337 0
338 };
339 let max = self.filtered_list.n_items() as usize;
340
341 if new_idx < max {
342 self.select_row_at_index(Some(new_idx));
343 }
344 return glib::Propagation::Stop;
345 } else if matches!(key, gdk::Key::Escape) {
346 obj.popdown();
348 return glib::Propagation::Stop;
349 }
350
351 glib::Propagation::Proceed
352 }
353
354 #[template_callback]
356 fn row_activated(&self, row: >k::ListBoxRow) {
357 let Some(label) = row.child().and_downcast::<gtk::Label>() else {
358 return;
359 };
360 let Some(entry) = self.entry.obj() else {
361 return;
362 };
363
364 entry.set_text(&label.label());
365
366 self.obj().popdown();
367 entry.grab_focus();
368 }
369 }
370}
371
372glib::wrapper! {
373 pub struct CompletionPopover(ObjectSubclass<imp::CompletionPopover>)
375 @extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
376}
377
378impl CompletionPopover {
379 pub fn new() -> Self {
380 glib::Object::new()
381 }
382
383 pub(crate) fn activate_selected_row(&self) -> bool {
387 self.imp().activate_selected_row()
388 }
389}
390
391impl Default for CompletionPopover {
392 fn default() -> Self {
393 Self::new()
394 }
395}