fractal/session/view/content/explore/
servers_popover.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{CompositeTemplate, glib};
3use ruma::ServerName;
4use tracing::error;
5
6use super::{ExploreServer, ExploreServerList, ExploreServerRow};
7use crate::session::model::Session;
8
9mod imp {
10    use std::marker::PhantomData;
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/session/view/content/explore/servers_popover.ui")]
18    #[properties(wrapper_type = super::ExploreServersPopover)]
19    pub struct ExploreServersPopover {
20        #[template_child]
21        pub(super) listbox: TemplateChild<gtk::ListBox>,
22        #[template_child]
23        server_entry: TemplateChild<gtk::Entry>,
24        /// The current session.
25        #[property(get, set = Self::set_session, explicit_notify)]
26        session: glib::WeakRef<Session>,
27        /// The server list.
28        #[property(get)]
29        server_list: ExploreServerList,
30        /// The selected server, if any.
31        #[property(get = Self::selected_server)]
32        selected_server: PhantomData<Option<ExploreServer>>,
33    }
34
35    #[glib::object_subclass]
36    impl ObjectSubclass for ExploreServersPopover {
37        const NAME: &'static str = "ExploreServersPopover";
38        type Type = super::ExploreServersPopover;
39        type ParentType = gtk::Popover;
40
41        fn class_init(klass: &mut Self::Class) {
42            Self::bind_template(klass);
43            Self::bind_template_callbacks(klass);
44
45            klass.install_action("explore-servers-popover.add-server", None, |obj, _, _| {
46                obj.imp().add_server();
47            });
48
49            klass.install_action(
50                "explore-servers-popover.remove-server",
51                Some(&String::static_variant_type()),
52                |obj, _, variant| {
53                    let Some(value) = variant.and_then(String::from_variant) else {
54                        error!("Could not remove server without a server name");
55                        return;
56                    };
57                    let Ok(server_name) = ServerName::parse(&value) else {
58                        error!("Could not remove server with an invalid server name");
59                        return;
60                    };
61
62                    obj.imp().remove_server(&server_name);
63                },
64            );
65        }
66
67        fn instance_init(obj: &InitializingObject<Self>) {
68            obj.init_template();
69        }
70    }
71
72    #[glib::derived_properties]
73    impl ObjectImpl for ExploreServersPopover {
74        fn constructed(&self) {
75            self.parent_constructed();
76
77            self.listbox.bind_model(Some(&self.server_list), |obj| {
78                let Some(server) = obj.downcast_ref::<ExploreServer>() else {
79                    error!("explore servers GtkListBox did not receive an ExploreServer");
80                    return adw::Bin::new().upcast();
81                };
82
83                ExploreServerRow::new(server).upcast()
84            });
85
86            self.update_add_server_state();
87        }
88    }
89
90    impl WidgetImpl for ExploreServersPopover {}
91    impl PopoverImpl for ExploreServersPopover {}
92
93    #[gtk::template_callbacks]
94    impl ExploreServersPopover {
95        /// Set the current session.
96        fn set_session(&self, session: &Session) {
97            if self.session.upgrade().as_ref() == Some(session) {
98                return;
99            }
100
101            self.session.set(Some(session));
102            self.server_list.set_session(session);
103
104            // Select the first server by default.
105            self.listbox
106                .select_row(self.listbox.row_at_index(0).as_ref());
107
108            self.obj().notify_session();
109        }
110
111        /// Handle when the selected server has changed.
112        #[template_callback]
113        fn selected_server_changed(&self) {
114            self.obj().notify_selected_server();
115        }
116
117        /// Handle when the user selected a server.
118        #[template_callback]
119        fn server_activated(&self) {
120            self.obj().popdown();
121        }
122
123        /// The server that is currently selected, if any.
124        fn selected_server(&self) -> Option<ExploreServer> {
125            self.listbox
126                .selected_row()
127                .and_downcast_ref()
128                .and_then(ExploreServerRow::server)
129        }
130
131        /// Whether the server currently in the text entry can be added.
132        fn can_add_server(&self) -> bool {
133            let Ok(server_name) = ServerName::parse(self.server_entry.text()) else {
134                return false;
135            };
136
137            // Don't allow duplicates
138            !self.server_list.contains_matrix_server(&server_name)
139        }
140
141        /// Update the state of the action to add a server according to the
142        /// current state.
143        #[template_callback]
144        fn update_add_server_state(&self) {
145            self.obj()
146                .action_set_enabled("explore-servers-popover.add-server", self.can_add_server());
147        }
148
149        /// Add the server currently in the text entry.
150        #[template_callback]
151        fn add_server(&self) {
152            if !self.can_add_server() {
153                return;
154            }
155
156            let Ok(server_name) = ServerName::parse(self.server_entry.text()) else {
157                return;
158            };
159            self.server_entry.set_text("");
160
161            self.server_list.add_custom_server(server_name);
162
163            // Select the new server, it should be the last row in the list.
164            let index = i32::try_from(self.server_list.n_items()).unwrap_or(i32::MAX);
165            self.listbox
166                .select_row(self.listbox.row_at_index(index - 1).as_ref());
167        }
168
169        /// Remove the given server.
170        fn remove_server(&self, server_name: &ServerName) {
171            // If the selected server is gonna be removed, select the first one.
172            if self
173                .selected_server()
174                .as_ref()
175                .and_then(|server| server.server())
176                .is_some_and(|s| s == server_name)
177            {
178                self.listbox
179                    .select_row(self.listbox.row_at_index(0).as_ref());
180            }
181
182            self.server_list.remove_custom_server(server_name);
183        }
184    }
185}
186
187glib::wrapper! {
188    /// A popover that lists the servers that can be explored.
189    pub struct ExploreServersPopover(ObjectSubclass<imp::ExploreServersPopover>)
190        @extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
191}
192
193impl ExploreServersPopover {
194    pub fn new(session: &Session) -> Self {
195        glib::Object::builder().property("session", session).build()
196    }
197}