fractal/session/model/sidebar_data/
selection.rs

1use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
2
3use crate::utils::BoundObject;
4
5mod imp {
6    use std::cell::{Cell, RefCell};
7
8    use super::*;
9
10    #[derive(Debug, glib::Properties)]
11    #[properties(wrapper_type = super::Selection)]
12    pub struct Selection {
13        /// The underlying model.
14        #[property(get, set = Self::set_model, explicit_notify, nullable)]
15        pub model: BoundObject<gio::ListModel>,
16        /// The position of the selected item.
17        #[property(get, set = Self::set_selected, explicit_notify, default = gtk::INVALID_LIST_POSITION)]
18        pub selected: Cell<u32>,
19        /// The selected item.
20        #[property(get, set = Self::set_selected_item, explicit_notify, nullable)]
21        pub selected_item: RefCell<Option<glib::Object>>,
22    }
23
24    impl Default for Selection {
25        fn default() -> Self {
26            Self {
27                model: Default::default(),
28                selected: Cell::new(gtk::INVALID_LIST_POSITION),
29                selected_item: Default::default(),
30            }
31        }
32    }
33
34    #[glib::object_subclass]
35    impl ObjectSubclass for Selection {
36        const NAME: &'static str = "SidebarSelection";
37        type Type = super::Selection;
38        type Interfaces = (gio::ListModel, gtk::SelectionModel);
39    }
40
41    #[glib::derived_properties]
42    impl ObjectImpl for Selection {}
43
44    impl ListModelImpl for Selection {
45        fn item_type(&self) -> glib::Type {
46            glib::Object::static_type()
47        }
48
49        fn n_items(&self) -> u32 {
50            self.model.obj().map(|m| m.n_items()).unwrap_or_default()
51        }
52
53        fn item(&self, position: u32) -> Option<glib::Object> {
54            self.model.obj()?.item(position)
55        }
56    }
57
58    impl SelectionModelImpl for Selection {
59        fn selection_in_range(&self, _position: u32, _n_items: u32) -> gtk::Bitset {
60            let bitset = gtk::Bitset::new_empty();
61            let selected = self.selected.get();
62
63            if selected != gtk::INVALID_LIST_POSITION {
64                bitset.add(selected);
65            }
66
67            bitset
68        }
69
70        fn is_selected(&self, position: u32) -> bool {
71            self.selected.get() == position
72        }
73    }
74
75    impl Selection {
76        /// Set the underlying model.
77        fn set_model(&self, model: Option<gio::ListModel>) {
78            let obj = self.obj();
79            let _guard = obj.freeze_notify();
80
81            let model = model.map(|m| m.clone().upcast());
82
83            let old_model = self.model.obj();
84            if old_model == model {
85                return;
86            }
87
88            let n_items_before = old_model.map_or(0, |model| model.n_items());
89            self.model.disconnect_signals();
90
91            if let Some(model) = model {
92                let items_changed_handler = model.connect_items_changed(clone!(
93                    #[weak]
94                    obj,
95                    move |m, p, r, a| {
96                        obj.items_changed_cb(m, p, r, a);
97                    }
98                ));
99
100                self.model.set(model.clone(), vec![items_changed_handler]);
101                obj.items_changed_cb(&model, 0, n_items_before, model.n_items());
102            } else {
103                if self.selected.get() != gtk::INVALID_LIST_POSITION {
104                    self.selected.replace(gtk::INVALID_LIST_POSITION);
105                    obj.notify_selected();
106                }
107                if self.selected_item.borrow().is_some() {
108                    self.selected_item.replace(None);
109                    obj.notify_selected_item();
110                }
111
112                obj.items_changed(0, n_items_before, 0);
113            }
114
115            obj.notify_model();
116        }
117
118        /// Set the selected item by its position.
119        fn set_selected(&self, position: u32) {
120            let old_selected = self.selected.get();
121            if old_selected == position {
122                return;
123            }
124
125            let selected_item = self.model.obj().and_then(|m| m.item(position));
126
127            let selected = if selected_item.is_none() {
128                gtk::INVALID_LIST_POSITION
129            } else {
130                position
131            };
132
133            if old_selected == selected {
134                return;
135            }
136            let obj = self.obj();
137
138            self.selected.replace(selected);
139            self.selected_item.replace(selected_item);
140
141            if old_selected == gtk::INVALID_LIST_POSITION {
142                obj.selection_changed(selected, 1);
143            } else if selected == gtk::INVALID_LIST_POSITION {
144                obj.selection_changed(old_selected, 1);
145            } else if selected < old_selected {
146                obj.selection_changed(selected, old_selected - selected + 1);
147            } else {
148                obj.selection_changed(old_selected, selected - old_selected + 1);
149            }
150
151            obj.notify_selected();
152            obj.notify_selected_item();
153        }
154
155        /// Set the selected item.
156        fn set_selected_item(&self, item: Option<glib::Object>) {
157            if *self.selected_item.borrow() == item {
158                return;
159            }
160            let obj = self.obj();
161
162            let old_selected = self.selected.get();
163            let mut selected = gtk::INVALID_LIST_POSITION;
164
165            if item.is_some() {
166                if let Some(model) = self.model.obj() {
167                    for i in 0..model.n_items() {
168                        let current_item = model.item(i);
169                        if current_item == item {
170                            selected = i;
171                            break;
172                        }
173                    }
174                }
175            }
176
177            self.selected_item.replace(item);
178
179            if old_selected != selected {
180                self.selected.replace(selected);
181
182                if old_selected == gtk::INVALID_LIST_POSITION {
183                    obj.selection_changed(selected, 1);
184                } else if selected == gtk::INVALID_LIST_POSITION {
185                    obj.selection_changed(old_selected, 1);
186                } else if selected < old_selected {
187                    obj.selection_changed(selected, old_selected - selected + 1);
188                } else {
189                    obj.selection_changed(old_selected, selected - old_selected + 1);
190                }
191                obj.notify_selected();
192            }
193
194            obj.notify_selected_item();
195        }
196    }
197}
198
199glib::wrapper! {
200    /// A `GtkSelectionModel` that keeps track of the selected item even if its position changes or it is removed from the list.
201    pub struct Selection(ObjectSubclass<imp::Selection>)
202        @implements gio::ListModel, gtk::SelectionModel;
203}
204
205impl Selection {
206    pub fn new<P: IsA<gio::ListModel>>(model: Option<&P>) -> Selection {
207        let model = model.map(|m| m.clone().upcast());
208        glib::Object::builder().property("model", &model).build()
209    }
210
211    fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
212        let imp = self.imp();
213
214        let _guard = self.freeze_notify();
215
216        let selected = self.selected();
217        let selected_item = self.selected_item();
218
219        if selected_item.is_none() || selected < position {
220            // unchanged
221        } else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
222            imp.selected.replace(selected + added - removed);
223            self.notify_selected();
224        } else {
225            for i in 0..=added {
226                if i == added {
227                    // the item really was deleted
228                    imp.selected.replace(gtk::INVALID_LIST_POSITION);
229                    self.notify_selected();
230                } else {
231                    let item = model.item(position + i);
232                    if item == selected_item {
233                        // the item moved
234                        if selected != position + i {
235                            imp.selected.replace(position + i);
236                            self.notify_selected();
237                        }
238                        break;
239                    }
240                }
241            }
242        }
243
244        self.items_changed(position, removed, added);
245    }
246}
247
248impl Default for Selection {
249    fn default() -> Self {
250        Self::new(gio::ListModel::NONE)
251    }
252}