fractal/session/model/sidebar_data/
list_model.rs

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