Skip to main content

fractal/session_view/sidebar/
section_row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{glib, glib::clone};
4
5use crate::{
6    session::{HighlightFlags, RoomCategory, SidebarSection, SidebarSectionName},
7    utils::{BoundObject, TemplateCallbacks},
8};
9
10mod imp {
11    use std::{
12        cell::{Cell, RefCell},
13        marker::PhantomData,
14    };
15
16    use glib::subclass::InitializingObject;
17
18    use super::*;
19
20    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
21    #[template(resource = "/org/gnome/Fractal/ui/session_view/sidebar/section_row.ui")]
22    #[properties(wrapper_type = super::SidebarSectionRow)]
23    pub struct SidebarSectionRow {
24        #[template_child]
25        pub(super) display_name: TemplateChild<gtk::Label>,
26        #[template_child]
27        notification_count: TemplateChild<gtk::Label>,
28        /// The section of this row.
29        #[property(get, set = Self::set_section, explicit_notify, nullable)]
30        section: BoundObject<SidebarSection>,
31        section_binding: RefCell<Option<glib::Binding>>,
32        /// Whether this row is expanded.
33        #[property(get, set = Self::set_is_expanded, explicit_notify, construct, default = true)]
34        is_expanded: Cell<bool>,
35        /// The label to show for this row.
36        #[property(get = Self::label)]
37        label: PhantomData<Option<String>>,
38        /// The room category to show a label for during a drag-and-drop
39        /// operation.
40        ///
41        /// This will change the label according to the action that can be
42        /// performed when dropping a room with the given category.
43        show_label_for_room_category: Cell<Option<RoomCategory>>,
44    }
45
46    #[glib::object_subclass]
47    impl ObjectSubclass for SidebarSectionRow {
48        const NAME: &'static str = "SidebarSectionRow";
49        type Type = super::SidebarSectionRow;
50        type ParentType = adw::Bin;
51
52        fn class_init(klass: &mut Self::Class) {
53            Self::bind_template(klass);
54            Self::bind_template_callbacks(klass);
55            TemplateCallbacks::bind_template_callbacks(klass);
56
57            klass.set_css_name("sidebar-section");
58        }
59
60        fn instance_init(obj: &InitializingObject<Self>) {
61            obj.init_template();
62        }
63    }
64
65    #[glib::derived_properties]
66    impl ObjectImpl for SidebarSectionRow {
67        fn dispose(&self) {
68            if let Some(binding) = self.section_binding.take() {
69                binding.unbind();
70            }
71        }
72    }
73
74    impl WidgetImpl for SidebarSectionRow {}
75    impl BinImpl for SidebarSectionRow {}
76
77    #[gtk::template_callbacks]
78    impl SidebarSectionRow {
79        /// Set the section represented by this row.
80        fn set_section(&self, section: Option<SidebarSection>) {
81            if self.section.obj() == section {
82                return;
83            }
84
85            if let Some(binding) = self.section_binding.take() {
86                binding.unbind();
87            }
88            self.section.disconnect_signals();
89
90            let obj = self.obj();
91
92            if let Some(section) = section {
93                let section_binding = section
94                    .bind_property("is-expanded", &*obj, "is-expanded")
95                    .sync_create()
96                    .build();
97                self.section_binding.replace(Some(section_binding));
98
99                let highlight_handler = section.connect_highlight_notify(clone!(
100                    #[weak(rename_to = imp)]
101                    self,
102                    move |_| {
103                        imp.update_highlight();
104                    }
105                ));
106
107                self.section.set(section, vec![highlight_handler]);
108            }
109
110            self.update_highlight();
111            obj.notify_section();
112            obj.notify_label();
113        }
114
115        /// The label to show for this row.
116        fn label(&self) -> Option<String> {
117            let target_section_name = self.section.obj().as_ref()?.name();
118            let source_room_category = self.show_label_for_room_category.get();
119
120            let label = match source_room_category {
121                Some(RoomCategory::Invited) => match target_section_name {
122                    // Translators: This is an action to join a room and put it in the "Favorites"
123                    // section.
124                    SidebarSectionName::Favorite => gettext("Join Room as Favorite"),
125                    SidebarSectionName::Normal => gettext("Join Room"),
126                    // Translators: This is an action to join a room and put it in the "Low
127                    // Priority" section.
128                    SidebarSectionName::LowPriority => gettext("Join Room as Low Priority"),
129                    SidebarSectionName::Left => gettext("Reject Invite"),
130                    _ => target_section_name.to_string(),
131                },
132                Some(RoomCategory::Favorite) => match target_section_name {
133                    SidebarSectionName::Normal => gettext("Move to Rooms"),
134                    SidebarSectionName::LowPriority => gettext("Move to Low Priority"),
135                    SidebarSectionName::Left => gettext("Leave Room"),
136                    _ => target_section_name.to_string(),
137                },
138                Some(RoomCategory::Normal) => match target_section_name {
139                    SidebarSectionName::Favorite => gettext("Move to Favorites"),
140                    SidebarSectionName::LowPriority => gettext("Move to Low Priority"),
141                    SidebarSectionName::Left => gettext("Leave Room"),
142                    _ => target_section_name.to_string(),
143                },
144                Some(RoomCategory::LowPriority) => match target_section_name {
145                    SidebarSectionName::Favorite => gettext("Move to Favorites"),
146                    SidebarSectionName::Normal => gettext("Move to Rooms"),
147                    SidebarSectionName::Left => gettext("Leave Room"),
148                    _ => target_section_name.to_string(),
149                },
150                Some(RoomCategory::Left) => match target_section_name {
151                    // Translators: This is an action to rejoin a room and put it in the "Favorites"
152                    // section.
153                    SidebarSectionName::Favorite => gettext("Rejoin Room as Favorite"),
154                    SidebarSectionName::Normal => gettext("Rejoin Room"),
155                    // Translators: This is an action to rejoin a room and put it in the "Low
156                    // Priority" section.
157                    SidebarSectionName::LowPriority => gettext("Rejoin Room as Low Priority"),
158                    _ => target_section_name.to_string(),
159                },
160                _ => target_section_name.to_string(),
161            };
162
163            Some(label)
164        }
165
166        /// Set whether this row is expanded.
167        fn set_is_expanded(&self, is_expanded: bool) {
168            if self.is_expanded.get() == is_expanded {
169                return;
170            }
171            let obj = self.obj();
172
173            if is_expanded {
174                obj.set_state_flags(gtk::StateFlags::CHECKED, false);
175            } else {
176                obj.unset_state_flags(gtk::StateFlags::CHECKED);
177            }
178
179            self.is_expanded.set(is_expanded);
180            self.update_expanded_accessibility_state();
181            self.update_highlight();
182            obj.notify_is_expanded();
183        }
184
185        /// Update how this row is highlighted according to the current state of
186        /// rooms in this section.
187        fn update_highlight(&self) {
188            let highlight = self
189                .section
190                .obj()
191                .as_ref()
192                .map(SidebarSection::highlight)
193                .unwrap_or_default();
194
195            if !self.is_expanded.get() && highlight.contains(HighlightFlags::HIGHLIGHT) {
196                self.notification_count.add_css_class("highlight");
197            } else {
198                self.notification_count.remove_css_class("highlight");
199            }
200        }
201
202        /// Update the expanded state of this row for a11y.
203        #[template_callback]
204        fn update_expanded_accessibility_state(&self) {
205            if let Some(row) = self.obj().parent() {
206                row.update_state(&[gtk::accessible::State::Expanded(Some(
207                    self.is_expanded.get(),
208                ))]);
209            }
210        }
211
212        /// Set the room category to show the label for.
213        pub(super) fn set_show_label_for_room_category(&self, category: Option<RoomCategory>) {
214            if self.show_label_for_room_category.get() == category {
215                return;
216            }
217
218            self.show_label_for_room_category.set(category);
219
220            self.obj().notify_label();
221        }
222    }
223}
224
225glib::wrapper! {
226    /// A sidebar row representing a category.
227    pub struct SidebarSectionRow(ObjectSubclass<imp::SidebarSectionRow>)
228        @extends gtk::Widget, adw::Bin,
229        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
230}
231
232impl SidebarSectionRow {
233    pub fn new() -> Self {
234        glib::Object::new()
235    }
236
237    /// Set the room category to show the label for.
238    pub(crate) fn set_show_label_for_room_category(&self, category: Option<RoomCategory>) {
239        self.imp().set_show_label_for_room_category(category);
240    }
241
242    /// The descendant that labels this row for a11y.
243    pub(crate) fn labelled_by(&self) -> &gtk::Accessible {
244        self.imp().display_name.upcast_ref()
245    }
246}