Skip to main content

fractal/session/sidebar_data/
list_model.rs

1use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*};
2
3use super::SidebarItemList;
4use crate::{
5    session::{IdentityVerification, Room},
6    utils::{BoundObjectWeakRef, FixedSelection, expression},
7};
8
9mod imp {
10    use std::cell::{Cell, OnceCell};
11
12    use super::*;
13
14    #[derive(Debug, Default, glib::Properties)]
15    #[properties(wrapper_type = super::SidebarListModel)]
16    pub struct SidebarListModel {
17        /// The list of items in the sidebar.
18        #[property(get, set = Self::set_item_list, construct_only)]
19        item_list: OnceCell<SidebarItemList>,
20        /// The string filter.
21        #[property(get)]
22        string_filter: gtk::StringFilter,
23        /// Whether the string filter is active.
24        #[property(get)]
25        is_filtered: Cell<bool>,
26        /// The selection model.
27        #[property(get)]
28        selection_model: FixedSelection,
29        /// The selected item, if it has signal handlers.
30        selected_item: BoundObjectWeakRef<glib::Object>,
31        item_type_filter: gtk::CustomFilter,
32    }
33
34    #[glib::object_subclass]
35    impl ObjectSubclass for SidebarListModel {
36        const NAME: &'static str = "SidebarListModel";
37        type Type = super::SidebarListModel;
38    }
39
40    #[glib::derived_properties]
41    impl ObjectImpl for SidebarListModel {
42        fn constructed(&self) {
43            self.parent_constructed();
44
45            // When a verification is replaced, select the replacement automatically.
46            self.selection_model.connect_selected_item_notify(clone!(
47                #[weak(rename_to = imp)]
48                self,
49                move |selection_model| {
50                    imp.selected_item.disconnect_signals();
51
52                    if let Some(item) = &selection_model.selected_item()
53                        && let Some(verification) = item.downcast_ref::<IdentityVerification>()
54                    {
55                        let verification_handler = verification.connect_replaced(clone!(
56                            #[weak]
57                            selection_model,
58                            move |_, new_verification| {
59                                selection_model.set_selected_item(Some(new_verification.clone()));
60                            }
61                        ));
62                        imp.selected_item.set(item, vec![verification_handler]);
63                    }
64                }
65            ));
66
67            // Disable the expanded filters of the items during search.
68            self.string_filter.connect_search_notify(clone!(
69                #[weak(rename_to = imp)]
70                self,
71                move |string_filter| {
72                    imp.set_is_filtered(string_filter.search().is_some_and(|s| !s.is_empty()));
73                }
74            ));
75        }
76    }
77
78    impl SidebarListModel {
79        /// The list of items in the sidebar.
80        fn item_list(&self) -> &SidebarItemList {
81            self.item_list.get().unwrap()
82        }
83
84        /// Set the list of items in the sidebar.
85        fn set_item_list(&self, item_list: SidebarItemList) {
86            let item_list = self.item_list.get_or_init(|| item_list);
87
88            let flattened_model = gtk::FlattenListModel::new(Some(item_list.clone()));
89
90            // When search is active, only show rooms.
91            self.item_type_filter.set_filter_func(clone!(
92                #[weak(rename_to = imp)]
93                self,
94                #[upgrade_or]
95                false,
96                move |item| !imp.is_filtered.get() || item.is::<Room>()
97            ));
98
99            // Set up search.
100            let room_name_expression = Room::this_expression("display-name");
101            self.string_filter
102                .set_match_mode(gtk::StringFilterMatchMode::Substring);
103            self.string_filter
104                .set_expression(Some(expression::normalize_string(room_name_expression)));
105            self.string_filter.set_ignore_case(true);
106            // Default to an empty string to be able to bind to GtkEditable::text.
107            self.string_filter.set_search(Some(""));
108
109            let multi_filter = gtk::EveryFilter::new();
110            multi_filter.append(self.item_type_filter.clone());
111            multi_filter.append(self.string_filter.clone());
112
113            let filter_model = gtk::FilterListModel::new(Some(flattened_model), Some(multi_filter));
114
115            self.selection_model.set_model(Some(filter_model));
116        }
117
118        /// Set whether the string filter is active.
119        fn set_is_filtered(&self, is_filtered: bool) {
120            if self.is_filtered.get() == is_filtered {
121                return;
122            }
123
124            self.is_filtered.set(is_filtered);
125
126            self.obj().notify_is_filtered();
127            self.item_list().inhibit_expanded(is_filtered);
128            self.item_type_filter.changed(gtk::FilterChange::Different);
129        }
130    }
131}
132
133glib::wrapper! {
134    /// A wrapper for the sidebar list model of a `Session`.
135    ///
136    /// It allows to keep the state for selection and filtering.
137    pub struct SidebarListModel(ObjectSubclass<imp::SidebarListModel>);
138}
139
140impl SidebarListModel {
141    /// Create a new `SidebarListModel`.
142    pub fn new(item_list: &SidebarItemList) -> Self {
143        glib::Object::builder()
144            .property("item-list", item_list)
145            .build()
146    }
147}