Skip to main content

fractal/components/power_level_selection/
row.rs

1use adw::{prelude::*, subclass::prelude::*};
2use gtk::{glib, glib::closure_local};
3use ruma::{Int, events::room::power_levels::UserPowerLevel, int};
4
5use super::PowerLevelSelectionPopover;
6use crate::{
7    components::{LoadingBin, RoleBadge},
8    session::Permissions,
9};
10
11mod imp {
12    use std::{
13        cell::{Cell, RefCell},
14        marker::PhantomData,
15        sync::LazyLock,
16    };
17
18    use glib::subclass::{InitializingObject, Signal};
19
20    use super::*;
21
22    #[derive(Debug, gtk::CompositeTemplate, glib::Properties)]
23    #[template(resource = "/org/gnome/Fractal/ui/components/power_level_selection/row.ui")]
24    #[properties(wrapper_type = super::PowerLevelSelectionRow)]
25    pub struct PowerLevelSelectionRow {
26        #[template_child]
27        subtitle_bin: TemplateChild<adw::Bin>,
28        #[template_child]
29        combo_selection_bin: TemplateChild<adw::Bin>,
30        #[template_child]
31        arrow_box: TemplateChild<gtk::Box>,
32        #[template_child]
33        loading_bin: TemplateChild<LoadingBin>,
34        #[template_child]
35        popover: TemplateChild<PowerLevelSelectionPopover>,
36        #[template_child]
37        selected_box: TemplateChild<gtk::Box>,
38        #[template_child]
39        selected_level_label: TemplateChild<gtk::Label>,
40        #[template_child]
41        creator_info_button: TemplateChild<gtk::MenuButton>,
42        #[template_child]
43        selected_role_badge: TemplateChild<RoleBadge>,
44        /// The permissions to watch.
45        #[property(get, set = Self::set_permissions, explicit_notify, nullable)]
46        permissions: RefCell<Option<Permissions>>,
47        /// The selected power level.
48        pub(super) selected_power_level: Cell<UserPowerLevel>,
49        /// Whether the selected power level should be displayed in the
50        /// subtitle, rather than next to the combo arrow.
51        #[property(get, set = Self::set_use_subtitle, explicit_notify)]
52        use_subtitle: Cell<bool>,
53        /// Whether the row is loading.
54        #[property(get = Self::is_loading, set = Self::set_is_loading)]
55        is_loading: PhantomData<bool>,
56        /// Whether the row is read-only.
57        #[property(get, set = Self::set_read_only, explicit_notify)]
58        read_only: Cell<bool>,
59    }
60
61    impl Default for PowerLevelSelectionRow {
62        fn default() -> Self {
63            Self {
64                subtitle_bin: Default::default(),
65                combo_selection_bin: Default::default(),
66                arrow_box: Default::default(),
67                loading_bin: Default::default(),
68                popover: Default::default(),
69                selected_box: Default::default(),
70                selected_level_label: Default::default(),
71                creator_info_button: Default::default(),
72                selected_role_badge: Default::default(),
73                permissions: Default::default(),
74                selected_power_level: Cell::new(UserPowerLevel::Int(int!(0))),
75                use_subtitle: Default::default(),
76                is_loading: PhantomData,
77                read_only: Default::default(),
78            }
79        }
80    }
81
82    #[glib::object_subclass]
83    impl ObjectSubclass for PowerLevelSelectionRow {
84        const NAME: &'static str = "PowerLevelSelectionRow";
85        type Type = super::PowerLevelSelectionRow;
86        type ParentType = adw::PreferencesRow;
87
88        fn class_init(klass: &mut Self::Class) {
89            Self::bind_template(klass);
90            Self::bind_template_callbacks(klass);
91
92            klass.set_accessible_role(gtk::AccessibleRole::ComboBox);
93
94            klass.install_action("power-level-selection-row.popup", None, |obj, _, _| {
95                if !obj.read_only() && !obj.is_loading() {
96                    obj.imp().popover.popup();
97                }
98            });
99        }
100
101        fn instance_init(obj: &InitializingObject<Self>) {
102            obj.init_template();
103        }
104    }
105
106    #[glib::derived_properties]
107    impl ObjectImpl for PowerLevelSelectionRow {
108        fn signals() -> &'static [Signal] {
109            static SIGNALS: LazyLock<Vec<Signal>> =
110                LazyLock::new(|| vec![Signal::builder("selected-power-level-changed").build()]);
111            SIGNALS.as_ref()
112        }
113
114        fn constructed(&self) {
115            self.parent_constructed();
116
117            self.update_selected_position();
118        }
119    }
120
121    impl WidgetImpl for PowerLevelSelectionRow {}
122    impl ListBoxRowImpl for PowerLevelSelectionRow {}
123    impl PreferencesRowImpl for PowerLevelSelectionRow {}
124
125    #[gtk::template_callbacks]
126    impl PowerLevelSelectionRow {
127        /// Set the permissions to watch.
128        fn set_permissions(&self, permissions: Option<Permissions>) {
129            if *self.permissions.borrow() == permissions {
130                return;
131            }
132
133            self.permissions.replace(permissions);
134            self.update_selected_label();
135            self.obj().notify_permissions();
136        }
137
138        /// Update the label of the selected power level.
139        fn update_selected_label(&self) {
140            let Some(permissions) = self.permissions.borrow().clone() else {
141                return;
142            };
143
144            let power_level = self.selected_power_level.get();
145            let role = permissions.role(power_level);
146
147            self.selected_role_badge.set_role(role);
148
149            let (creator_info_visible, accessible_desc) =
150                if let UserPowerLevel::Int(value) = power_level {
151                    self.selected_level_label.set_label(&value.to_string());
152                    self.popover.set_selected_power_level(i64::from(value));
153                    (false, format!("{value} {role}"))
154                } else {
155                    (true, role.to_string())
156                };
157
158            self.creator_info_button.set_visible(creator_info_visible);
159            self.selected_level_label.set_visible(!creator_info_visible);
160
161            self.obj()
162                .update_property(&[gtk::accessible::Property::Description(&accessible_desc)]);
163        }
164
165        /// Set the selected power level.
166        pub(super) fn set_selected_power_level(&self, power_level: UserPowerLevel) {
167            if self.selected_power_level.get() == power_level {
168                return;
169            }
170
171            self.selected_power_level.set(power_level);
172
173            self.update_selected_label();
174            self.obj()
175                .emit_by_name::<()>("selected-power-level-changed", &[]);
176        }
177
178        /// Set whether the selected power level should be displayed in the
179        /// subtitle, rather than next to the combo arrow.
180        fn set_use_subtitle(&self, use_subtitle: bool) {
181            if self.use_subtitle.get() == use_subtitle {
182                return;
183            }
184
185            self.use_subtitle.set(use_subtitle);
186
187            self.update_selected_position();
188            self.obj().notify_use_subtitle();
189        }
190
191        /// Whether the row is loading.
192        fn is_loading(&self) -> bool {
193            self.loading_bin.is_loading()
194        }
195
196        /// Set whether the row is loading.
197        fn set_is_loading(&self, loading: bool) {
198            if self.is_loading() == loading {
199                return;
200            }
201
202            self.loading_bin.set_is_loading(loading);
203            self.obj().notify_is_loading();
204        }
205
206        /// Update the position of the selected label.
207        fn update_selected_position(&self) {
208            if self.use_subtitle.get() {
209                if self
210                    .selected_box
211                    .parent()
212                    .is_none_or(|p| p != *self.subtitle_bin)
213                {
214                    if self.selected_box.parent().is_some() {
215                        self.combo_selection_bin.set_child(None::<&gtk::Widget>);
216                    }
217
218                    self.subtitle_bin.set_child(Some(&*self.selected_box));
219                }
220            } else if self
221                .selected_box
222                .parent()
223                .is_none_or(|p| p != *self.combo_selection_bin)
224            {
225                if self.selected_box.parent().is_some() {
226                    self.subtitle_bin.set_child(None::<&gtk::Widget>);
227                }
228
229                self.combo_selection_bin
230                    .set_child(Some(&*self.selected_box));
231            }
232        }
233
234        /// Set whether the row is read-only.
235        fn set_read_only(&self, read_only: bool) {
236            if self.read_only.get() == read_only {
237                return;
238            }
239            let obj = self.obj();
240
241            self.read_only.set(read_only);
242
243            obj.update_property(&[gtk::accessible::Property::ReadOnly(read_only)]);
244            obj.notify_read_only();
245        }
246
247        /// The popover's visibility changed.
248        #[template_callback]
249        fn popover_visible(&self) {
250            let obj = self.obj();
251            let is_visible = self.popover.is_visible();
252
253            if is_visible {
254                obj.add_css_class("has-open-popup");
255            } else {
256                obj.remove_css_class("has-open-popup");
257            }
258        }
259
260        /// The selected power level changed.
261        #[template_callback]
262        fn power_level_changed(&self) {
263            self.set_selected_power_level(UserPowerLevel::Int(Int::new_saturating(
264                self.popover.selected_power_level(),
265            )));
266        }
267    }
268}
269
270glib::wrapper! {
271    /// An `AdwPreferencesRow` behaving like a combo box to select a room member's power level.
272    pub struct PowerLevelSelectionRow(ObjectSubclass<imp::PowerLevelSelectionRow>)
273        @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow,
274        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
275}
276
277impl PowerLevelSelectionRow {
278    pub fn new() -> Self {
279        glib::Object::new()
280    }
281
282    /// The selected power level.
283    pub(crate) fn selected_power_level(&self) -> UserPowerLevel {
284        self.imp().selected_power_level.get()
285    }
286
287    /// Set the selected power level.
288    pub(crate) fn set_selected_power_level(&self, power_level: UserPowerLevel) {
289        self.imp().set_selected_power_level(power_level);
290    }
291
292    /// Connect to the signal emitted when the selected power level changed.
293    pub fn connect_power_level_changed<F: Fn(&Self) + 'static>(
294        &self,
295        f: F,
296    ) -> glib::SignalHandlerId {
297        self.connect_closure(
298            "selected-power-level-changed",
299            true,
300            closure_local!(move |obj: Self| {
301                f(&obj);
302            }),
303        )
304    }
305}