diff --git a/src/session/view/sidebar/category_row.rs b/src/session/view/sidebar/category_row.rs index e0fce202..5f5eaa56 100644 --- a/src/session/view/sidebar/category_row.rs +++ b/src/session/view/sidebar/category_row.rs @@ -1,25 +1,39 @@ use adw::subclass::prelude::BinImpl; use gettextrs::gettext; -use gtk::{self, accessible, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; +use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; use crate::session::model::{Category, CategoryType}; mod imp { - use std::cell::{Cell, RefCell}; + use std::{ + cell::{Cell, RefCell}, + marker::PhantomData, + }; use glib::subclass::InitializingObject; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/category_row.ui")] + #[properties(wrapper_type = super::CategoryRow)] pub struct CategoryRow { /// The category of this row. + #[property(get, set = Self::set_category, explicit_notify, nullable)] pub category: RefCell>, /// The expanded state of this row. + #[property(get, set = Self::set_expanded, explicit_notify, construct, default = true)] pub expanded: Cell, + /// The label to show for this row. + #[property(get = Self::label)] + pub label: PhantomData>, /// The `CategoryType` to show a label for during a drag-and-drop /// operation. + /// + /// This will change the label according to the action that can be + /// performed when changing from the `CategoryType` to this + /// row's `Category`. + #[property(get, set = Self::set_show_label_for_category, explicit_notify, builder(CategoryType::default()))] pub show_label_for_category: Cell, /// The label showing the category name. #[template_child] @@ -42,58 +56,112 @@ mod imp { } } - impl ObjectImpl for CategoryRow { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("category") - .explicit_notify() - .build(), - glib::ParamSpecBoolean::builder("expanded") - .default_value(true) - .explicit_notify() - .construct() - .build(), - glib::ParamSpecString::builder("label").read_only().build(), - glib::ParamSpecEnum::builder::("show-label-for-category") - .explicit_notify() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "category" => obj.set_category(value.get().unwrap()), - "expanded" => obj.set_expanded(value.get().unwrap()), - "show-label-for-category" => obj.set_show_label_for_category(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "category" => obj.category().to_value(), - "expanded" => obj.expanded().to_value(), - "label" => obj.label().to_value(), - "show-label-for-category" => obj.show_label_for_category().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for CategoryRow {} impl WidgetImpl for CategoryRow {} impl BinImpl for CategoryRow {} + + impl CategoryRow { + /// Set the category represented by this row. + fn set_category(&self, category: Option) { + if self.category.borrow().clone() == category { + return; + } + + self.category.replace(category); + + let obj = self.obj(); + obj.notify_category(); + obj.notify_label(); + } + + /// The label to show for this row. + fn label(&self) -> Option { + let to_type = self.category.borrow().as_ref()?.r#type(); + let from_type = self.show_label_for_category.get(); + + let label = match from_type { + CategoryType::Invited => match to_type { + // Translators: This is an action to join a room and put it in the "Favorites" + // section. + CategoryType::Favorite => gettext("Join Room as Favorite"), + CategoryType::Normal => gettext("Join Room"), + // Translators: This is an action to join a room and put it in the "Low + // Priority" section. + CategoryType::LowPriority => gettext("Join Room as Low Priority"), + CategoryType::Left => gettext("Reject Invite"), + _ => to_type.to_string(), + }, + CategoryType::Favorite => match to_type { + CategoryType::Normal => gettext("Move to Rooms"), + CategoryType::LowPriority => gettext("Move to Low Priority"), + CategoryType::Left => gettext("Leave Room"), + _ => to_type.to_string(), + }, + CategoryType::Normal => match to_type { + CategoryType::Favorite => gettext("Move to Favorites"), + CategoryType::LowPriority => gettext("Move to Low Priority"), + CategoryType::Left => gettext("Leave Room"), + _ => to_type.to_string(), + }, + CategoryType::LowPriority => match to_type { + CategoryType::Favorite => gettext("Move to Favorites"), + CategoryType::Normal => gettext("Move to Rooms"), + CategoryType::Left => gettext("Leave Room"), + _ => to_type.to_string(), + }, + CategoryType::Left => match to_type { + // Translators: This is an action to rejoin a room and put it in the "Favorites" + // section. + CategoryType::Favorite => gettext("Rejoin Room as Favorite"), + CategoryType::Normal => gettext("Rejoin Room"), + // Translators: This is an action to rejoin a room and put it in the "Low + // Priority" section. + CategoryType::LowPriority => gettext("Rejoin Room as Low Priority"), + _ => to_type.to_string(), + }, + _ => to_type.to_string(), + }; + + Some(label) + } + + /// Set the expanded state of this row. + fn set_expanded(&self, expanded: bool) { + if self.expanded.get() == expanded { + return; + } + let obj = self.obj(); + + if expanded { + obj.set_state_flags(gtk::StateFlags::CHECKED, false); + } else { + obj.unset_state_flags(gtk::StateFlags::CHECKED); + } + + self.expanded.set(expanded); + obj.set_expanded_accessibility_state(expanded); + obj.notify_expanded(); + } + + /// Set the `CategoryType` to show a label for. + fn set_show_label_for_category(&self, category: CategoryType) { + if category == self.show_label_for_category.get() { + return; + } + + self.show_label_for_category.set(category); + + let obj = self.obj(); + obj.notify_show_label_for_category(); + obj.notify_label(); + } + } } glib::wrapper! { + /// A sidebar row representing a category. pub struct CategoryRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -103,123 +171,15 @@ impl CategoryRow { glib::Object::new() } - /// The category represented by this row. - pub fn category(&self) -> Option { - self.imp().category.borrow().clone() - } - - /// Set the category represented by this row. - pub fn set_category(&self, category: Option) { - if self.category() == category { - return; - } - - self.imp().category.replace(category); - self.notify("category"); - self.notify("label"); - } - - /// The expanded state of this row. - fn expanded(&self) -> bool { - self.imp().expanded.get() - } - - /// Set the expanded state of this row. - fn set_expanded(&self, expanded: bool) { - if self.expanded() == expanded { - return; - } - - if expanded { - self.set_state_flags(gtk::StateFlags::CHECKED, false); - } else { - self.unset_state_flags(gtk::StateFlags::CHECKED); - } - - self.set_expanded_accessibility_state(expanded); - self.imp().expanded.set(expanded); - self.notify("expanded"); - } - + /// Set the expanded state of this row for a11y. fn set_expanded_accessibility_state(&self, expanded: bool) { if let Some(row) = self.parent() { - row.update_state(&[accessible::State::Expanded(Some(expanded))]); + row.update_state(&[gtk::accessible::State::Expanded(Some(expanded))]); } } - /// The label to show for this row. - pub fn label(&self) -> Option { - let to_type = self.category()?.r#type(); - let from_type = self.show_label_for_category(); - - let label = match from_type { - CategoryType::Invited => match to_type { - // Translators: This is an action to join a room and put it in the "Favorites" - // section. - CategoryType::Favorite => gettext("Join Room as Favorite"), - CategoryType::Normal => gettext("Join Room"), - // Translators: This is an action to join a room and put it in the "Low Priority" - // section. - CategoryType::LowPriority => gettext("Join Room as Low Priority"), - CategoryType::Left => gettext("Reject Invite"), - _ => to_type.to_string(), - }, - CategoryType::Favorite => match to_type { - CategoryType::Normal => gettext("Move to Rooms"), - CategoryType::LowPriority => gettext("Move to Low Priority"), - CategoryType::Left => gettext("Leave Room"), - _ => to_type.to_string(), - }, - CategoryType::Normal => match to_type { - CategoryType::Favorite => gettext("Move to Favorites"), - CategoryType::LowPriority => gettext("Move to Low Priority"), - CategoryType::Left => gettext("Leave Room"), - _ => to_type.to_string(), - }, - CategoryType::LowPriority => match to_type { - CategoryType::Favorite => gettext("Move to Favorites"), - CategoryType::Normal => gettext("Move to Rooms"), - CategoryType::Left => gettext("Leave Room"), - _ => to_type.to_string(), - }, - CategoryType::Left => match to_type { - // Translators: This is an action to rejoin a room and put it in the "Favorites" - // section. - CategoryType::Favorite => gettext("Rejoin Room as Favorite"), - CategoryType::Normal => gettext("Rejoin Room"), - // Translators: This is an action to rejoin a room and put it in the "Low Priority" - // section. - CategoryType::LowPriority => gettext("Rejoin Room as Low Priority"), - _ => to_type.to_string(), - }, - _ => to_type.to_string(), - }; - - Some(label) - } - - /// The `CategoryType` to show a label for. - /// - /// This will change the label according to the action that can be performed - /// when changing from the `CategoryType` to this row's `Category`. - pub fn show_label_for_category(&self) -> CategoryType { - self.imp().show_label_for_category.get() - } - - /// Set the `CategoryType` to show a label for. - pub fn set_show_label_for_category(&self, category: CategoryType) { - if category == self.show_label_for_category() { - return; - } - - self.imp().show_label_for_category.set(category); - - self.notify("show-label-for-category"); - self.notify("label"); - } - - /// Returns the display name widget. - pub fn display_name(&self) -> gtk::Label { - self.imp().display_name.clone() + /// The descendant that labels this row for a11y. + pub fn labelled_by(&self) -> >k::Accessible { + self.imp().display_name.upcast_ref() } } diff --git a/src/session/view/sidebar/icon_item_row.rs b/src/session/view/sidebar/icon_item_row.rs index d74653f2..3c71bb1f 100644 --- a/src/session/view/sidebar/icon_item_row.rs +++ b/src/session/view/sidebar/icon_item_row.rs @@ -10,9 +10,12 @@ mod imp { use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/icon_item_row.ui")] + #[properties(wrapper_type = super::IconItemRow)] pub struct IconItemRow { + /// The [`IconItem`] of this row. + #[property(get, set = Self::set_icon_item, explicit_notify, nullable)] pub icon_item: RefCell>, } @@ -32,35 +35,33 @@ mod imp { } } - impl ObjectImpl for IconItemRow { - fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("icon-item") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "icon-item" => self.obj().set_icon_item(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "icon-item" => self.obj().icon_item().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for IconItemRow {} impl WidgetImpl for IconItemRow {} impl BinImpl for IconItemRow {} + + impl IconItemRow { + /// Set the [`IconItem`] of this row. + fn set_icon_item(&self, icon_item: Option) { + if self.icon_item.borrow().clone() == icon_item { + return; + } + let obj = self.obj(); + + if icon_item + .as_ref() + .is_some_and(|i| i.r#type() == ItemType::Forget) + { + obj.add_css_class("forget"); + } else { + obj.remove_css_class("forget"); + } + + self.icon_item.replace(icon_item); + obj.notify_icon_item(); + } + } } glib::wrapper! { @@ -72,28 +73,4 @@ impl IconItemRow { pub fn new() -> Self { glib::Object::new() } - - /// The [`IconItem`] of this row. - pub fn icon_item(&self) -> Option { - self.imp().icon_item.borrow().clone() - } - - /// Set the [`IconItem`] of this row. - pub fn set_icon_item(&self, icon_item: Option) { - if self.icon_item() == icon_item { - return; - } - - if icon_item - .as_ref() - .is_some_and(|i| i.r#type() == ItemType::Forget) - { - self.add_css_class("forget"); - } else { - self.remove_css_class("forget"); - } - - self.imp().icon_item.replace(icon_item); - self.notify("icon-item"); - } } diff --git a/src/session/view/sidebar/mod.rs b/src/session/view/sidebar/mod.rs index 7b94da49..ae0db47b 100644 --- a/src/session/view/sidebar/mod.rs +++ b/src/session/view/sidebar/mod.rs @@ -21,15 +21,18 @@ use crate::{ }; mod imp { - use std::cell::{Cell, RefCell}; + use std::{ + cell::{Cell, OnceCell, RefCell}, + marker::PhantomData, + }; - use glib::{signal::SignalHandlerId, subclass::InitializingObject}; - use once_cell::{sync::Lazy, unsync::OnceCell}; + use glib::subclass::InitializingObject; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/mod.ui")] + #[properties(wrapper_type = super::Sidebar)] pub struct Sidebar { #[template_child] pub scrolled_window: TemplateChild, @@ -44,15 +47,24 @@ mod imp { #[template_child] pub offline_banner: TemplateChild, pub room_row_popover: OnceCell, + /// The logged-in user. + #[property(get, set = Self::set_user, explicit_notify, nullable)] pub user: RefCell>, /// The type of the source that activated drop mode. pub drop_source_type: Cell>, + /// The `CategoryType` of the source that activated drop mode. + #[property(get = Self::drop_source_category_type, builder(CategoryType::default()))] + pub drop_source_category_type: PhantomData, /// The type of the drop target that is currently hovered. pub drop_active_target_type: Cell>, + /// The `CategoryType` of the drop target that is currently hovered. + #[property(get = Self::drop_active_target_category_type, builder(CategoryType::default()))] + pub drop_active_target_category_type: PhantomData, /// The list model of this sidebar. + #[property(get, set = Self::set_list_model, explicit_notify, nullable)] pub list_model: glib::WeakRef, pub bindings: RefCell>, - pub offline_handler_id: RefCell>, + pub offline_handler_id: RefCell>, } #[glib::object_subclass] @@ -73,58 +85,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for Sidebar { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("user") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("list-model") - .explicit_notify() - .build(), - glib::ParamSpecEnum::builder::("drop-source-type") - .read_only() - .build(), - glib::ParamSpecEnum::builder::("drop-active-target-type") - .read_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "user" => obj.set_user(value.get().unwrap()), - "list-model" => obj.set_list_model(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "user" => obj.user().to_value(), - "list-model" => obj.list_model().to_value(), - "drop-source-type" => obj - .drop_source_type() - .map(CategoryType::from) - .unwrap_or_default() - .to_value(), - "drop-active-target-type" => obj - .drop_active_target_type() - .map(CategoryType::from) - .unwrap_or_default() - .to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -195,6 +157,89 @@ mod imp { } impl NavigationPageImpl for Sidebar {} + + impl Sidebar { + /// Set the logged-in user. + fn set_user(&self, user: Option) { + let prev_user = self.user.borrow().clone(); + if prev_user == user { + return; + } + + if let Some(prev_user) = prev_user { + if let Some(handler_id) = self.offline_handler_id.take() { + prev_user.session().disconnect(handler_id); + } + } + + if let Some(user) = &user { + let session = user.session(); + let handler_id = + session.connect_offline_notify(clone!(@weak self as imp => move |session| { + imp.offline_banner.set_revealed(session.offline()); + })); + self.offline_banner.set_revealed(session.offline()); + + self.offline_handler_id.replace(Some(handler_id)); + } + + self.user.replace(user); + self.obj().notify_user(); + } + + /// Set the list model of the sidebar. + fn set_list_model(&self, list_model: Option) { + if self.list_model.upgrade() == list_model { + return; + } + let obj = self.obj(); + + for binding in self.bindings.take() { + binding.unbind(); + } + + if let Some(list_model) = &list_model { + let bindings = vec![ + obj.bind_property( + "drop-source-category-type", + &list_model.item_list(), + "show-all-for-category", + ) + .sync_create() + .build(), + list_model + .string_filter() + .bind_property("search", &*self.room_search_entry, "text") + .sync_create() + .bidirectional() + .build(), + ]; + + self.bindings.replace(bindings); + } + + self.listview + .set_model(list_model.as_ref().map(|m| m.selection_model()).as_ref()); + self.list_model.set(list_model.as_ref()); + obj.notify_list_model(); + } + + /// The `CategoryType` of the source that activated drop mode. + fn drop_source_category_type(&self) -> CategoryType { + self.drop_source_type + .get() + .map(Into::into) + .unwrap_or_default() + } + + /// The `CategoryType` of the drop target that is currently hovered. + fn drop_active_target_category_type(&self) -> CategoryType { + self.drop_active_target_type + .get() + .map(Into::into) + .unwrap_or_default() + } + } } glib::wrapper! { @@ -211,84 +256,6 @@ impl Sidebar { self.imp().room_search.clone() } - /// The list model of this sidebar. - pub fn list_model(&self) -> Option { - self.imp().list_model.upgrade() - } - - /// Set the list model of the sidebar. - pub fn set_list_model(&self, list_model: Option) { - if self.list_model() == list_model { - return; - } - - let imp = self.imp(); - - for binding in imp.bindings.take() { - binding.unbind(); - } - - if let Some(list_model) = &list_model { - let bindings = vec![ - self.bind_property( - "drop-source-type", - &list_model.item_list(), - "show-all-for-category", - ) - .sync_create() - .build(), - list_model - .string_filter() - .bind_property("search", &*imp.room_search_entry, "text") - .sync_create() - .bidirectional() - .build(), - ]; - - imp.bindings.replace(bindings); - } - - imp.listview - .set_model(list_model.as_ref().map(|m| m.selection_model()).as_ref()); - imp.list_model.set(list_model.as_ref()); - self.notify("list-model"); - } - - /// The logged-in user. - pub fn user(&self) -> Option { - self.imp().user.borrow().clone() - } - - /// Set the logged-in user. - fn set_user(&self, user: Option) { - let prev_user = self.user(); - if prev_user == user { - return; - } - - if let Some(prev_user) = prev_user { - if let Some(handler_id) = self.imp().offline_handler_id.take() { - prev_user.session().disconnect(handler_id); - } - } - - if let Some(user) = user.as_ref() { - let session = user.session(); - let handler_id = session.connect_notify_local( - Some("offline"), - clone!(@weak self as obj => move |session, _| { - obj.imp().offline_banner.set_revealed(session.offline()); - }), - ); - self.imp().offline_banner.set_revealed(session.offline()); - - self.imp().offline_handler_id.replace(Some(handler_id)); - } - - self.imp().user.replace(user); - self.notify("user"); - } - /// The type of the source that activated drop mode. pub fn drop_source_type(&self) -> Option { self.imp().drop_source_type.get() @@ -310,7 +277,7 @@ impl Sidebar { imp.listview.remove_css_class("drop-mode"); } - self.notify("drop-source-type"); + self.notify_drop_source_category_type(); } /// The type of the drop target that is currently hovered. @@ -325,7 +292,7 @@ impl Sidebar { } self.imp().drop_active_target_type.set(target_type); - self.notify("drop-active-target-type"); + self.notify_drop_active_target_category_type(); } pub fn room_row_popover(&self) -> >k::PopoverMenu { diff --git a/src/session/view/sidebar/room_row.rs b/src/session/view/sidebar/room_row.rs index 141e1b86..bec947f3 100644 --- a/src/session/view/sidebar/room_row.rs +++ b/src/session/view/sidebar/room_row.rs @@ -1,6 +1,6 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; -use gtk::{accessible::Property, gdk, glib, glib::clone, CompositeTemplate}; +use gtk::{gdk, glib, glib::clone, CompositeTemplate}; use super::Row; use crate::{ @@ -15,13 +15,15 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/room_row.ui")] + #[properties(wrapper_type = super::RoomRow)] pub struct RoomRow { + /// The room represented by this row. + #[property(get, set = Self::set_room, explicit_notify, nullable)] pub room: BoundObject, pub binding: RefCell>, #[template_child] @@ -94,31 +96,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for RoomRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::("room") - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "room" => self.obj().set_room(value.get().unwrap()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "room" => self.obj().room().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -158,16 +137,70 @@ mod imp { if let Some(sidebar) = obj .parent() .and_downcast_ref::() - .map(|row| row.sidebar()) + .and_then(|row| row.sidebar()) { let popover = sidebar.room_row_popover(); obj.set_popover(Some(popover.to_owned())); } } } + + impl RoomRow { + /// Set the room represented by this row. + pub fn set_room(&self, room: Option) { + if self.room.obj() == room { + return; + } + let obj = self.obj(); + + self.room.disconnect_signals(); + if let Some(binding) = self.binding.take() { + binding.unbind(); + } + self.display_name.remove_css_class("dim-label"); + + if let Some(room) = room { + self.binding.replace(Some( + room.bind_property( + "notification-count", + &self.notification_count.get(), + "visible", + ) + .sync_create() + .transform_from(|_, count: u64| Some(count > 0)) + .build(), + )); + + let highlight_handler = + room.connect_highlight_notify(clone!(@weak obj => move |_| { + obj.update_highlight(); + })); + let direct_handler = room.connect_is_direct_notify(clone!(@weak obj => move |_| { + obj.update_direct_icon(); + })); + let name_handler = room.connect_display_name_notify(clone!(@weak obj => move |_| { + obj.update_accessibility_label(); + })); + if room.category() == RoomType::Left { + self.display_name.add_css_class("dim-label"); + } + + self.room + .set(room, vec![highlight_handler, direct_handler, name_handler]); + + obj.update_accessibility_label(); + } + + obj.update_highlight(); + obj.update_direct_icon(); + obj.update_actions(); + obj.notify_room(); + } + } } glib::wrapper! { + /// A sidebar row representing a room. pub struct RoomRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, ContextMenuBin, @implements gtk::Accessible; } @@ -177,69 +210,6 @@ impl RoomRow { glib::Object::new() } - /// The room represented by this row. - pub fn room(&self) -> Option { - self.imp().room.obj() - } - - /// Set the room represented by this row. - pub fn set_room(&self, room: Option) { - let imp = self.imp(); - - if self.room() == room { - return; - } - - imp.room.disconnect_signals(); - if let Some(binding) = imp.binding.take() { - binding.unbind(); - } - imp.display_name.remove_css_class("dim-label"); - - if let Some(room) = room { - imp.binding.replace(Some( - room.bind_property( - "notification-count", - &imp.notification_count.get(), - "visible", - ) - .sync_create() - .transform_from(|_, count: u64| Some(count > 0)) - .build(), - )); - - let highlight_handler = room.connect_notify_local( - Some("highlight"), - clone!(@weak self as obj => move |_, _| { - obj.update_highlight(); - }), - ); - let direct_handler = room.connect_notify_local( - Some("is-direct"), - clone!(@weak self as obj => move |_, _| { - obj.update_direct_icon(); - }), - ); - let name_handler = - room.connect_display_name_notify(clone!(@weak self as obj => move |_| { - obj.update_accessibility_label(); - })); - if room.category() == RoomType::Left { - imp.display_name.add_css_class("dim-label"); - } - - imp.room - .set(room, vec![highlight_handler, direct_handler, name_handler]); - - self.update_accessibility_label(); - } - - self.update_highlight(); - self.update_direct_icon(); - self.update_actions(); - self.notify("room"); - } - fn update_highlight(&self) { let imp = self.imp(); if let Some(room) = self.room() { @@ -336,24 +306,41 @@ impl RoomRow { } fn drag_prepare(&self, drag: >k::DragSource, x: f64, y: f64) -> Option { - let paintable = gtk::WidgetPaintable::new(Some(&self.parent().unwrap())); - // FIXME: The hotspot coordinates don't work. - // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341 - drag.set_icon(Some(&paintable), x as i32, y as i32); - self.room() - .map(|room| gdk::ContentProvider::for_value(&room.to_value())) + let room = self.room()?; + + if let Some(parent) = self.parent() { + let paintable = gtk::WidgetPaintable::new(Some(&parent)); + // FIXME: The hotspot coordinates don't work. + // See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341 + drag.set_icon(Some(&paintable), x as i32, y as i32); + } + + Some(gdk::ContentProvider::for_value(&room.to_value())) } fn drag_begin(&self) { - let row = self.parent().and_downcast::().unwrap(); + let Some(room) = self.room() else { + return; + }; + let Some(row) = self.parent().and_downcast::() else { + return; + }; + let Some(sidebar) = row.sidebar() else { + return; + }; row.add_css_class("drag"); - row.sidebar() - .set_drop_source_type(Some(self.room().unwrap().category())); + + sidebar.set_drop_source_type(Some(room.category())); } fn drag_end(&self) { - let row = self.parent().and_downcast::().unwrap(); - row.sidebar().set_drop_source_type(None); + let Some(row) = self.parent().and_downcast::() else { + return; + }; + let Some(sidebar) = row.sidebar() else { + return; + }; + sidebar.set_drop_source_type(None); row.remove_css_class("drag"); } @@ -422,15 +409,17 @@ impl RoomRow { } fn update_accessibility_label(&self) { - self.parent() - .unwrap() - .update_property(&[Property::Label(&self.accessible_label())]); + let Some(parent) = self.parent() else { + return; + }; + parent.update_property(&[gtk::accessible::Property::Label(&self.accessible_label())]); } fn accessible_label(&self) -> String { let Some(room) = self.room() else { return String::new(); }; + if room.is_direct() { gettext_f( // Translators: Do NOT translate the content between '{' and '}', this is a diff --git a/src/session/view/sidebar/row.rs b/src/session/view/sidebar/row.rs index 42e75c15..aaf79f18 100644 --- a/src/session/view/sidebar/row.rs +++ b/src/session/view/sidebar/row.rs @@ -13,16 +13,22 @@ use crate::{ }; mod imp { - use std::cell::RefCell; - - use once_cell::sync::Lazy; + use std::{cell::RefCell, marker::PhantomData}; use super::*; - #[derive(Debug, Default)] + #[derive(Debug, Default, glib::Properties)] + #[properties(wrapper_type = super::Row)] pub struct Row { + /// The ancestor sidebar of this row. + #[property(get, set = Self::set_sidebar, construct_only)] pub sidebar: BoundObjectWeakRef, + /// The list row to track for expander state. + #[property(get, set = Self::set_list_row, explicit_notify, nullable)] pub list_row: RefCell>, + /// The sidebar item of this row. + #[property(get = Self::item)] + pub item: PhantomData>, pub bindings: RefCell>, } @@ -38,46 +44,8 @@ mod imp { } } + #[glib::derived_properties] impl ObjectImpl for Row { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![ - glib::ParamSpecObject::builder::("item") - .read_only() - .build(), - glib::ParamSpecObject::builder::("list-row") - .explicit_notify() - .build(), - glib::ParamSpecObject::builder::("sidebar") - .construct_only() - .build(), - ] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - let obj = self.obj(); - - match pspec.name() { - "list-row" => obj.set_list_row(value.get().unwrap()), - "sidebar" => obj.set_sidebar(value.get().ok().as_ref()), - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let obj = self.obj(); - - match pspec.name() { - "item" => obj.item().to_value(), - "list-row" => obj.list_row().to_value(), - "sidebar" => obj.sidebar().to_value(), - _ => unimplemented!(), - } - } - fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -104,9 +72,115 @@ mod imp { impl WidgetImpl for Row {} impl BinImpl for Row {} + + impl Row { + /// Set the ancestor sidebar of this row. + fn set_sidebar(&self, sidebar: Sidebar) { + let obj = self.obj(); + + let drop_source_type_handler = + sidebar.connect_drop_source_category_type_notify(clone!(@weak obj => move |_| { + obj.update_for_drop_source_type(); + })); + + let drop_active_target_type_handler = sidebar + .connect_drop_active_target_category_type_notify(clone!(@weak obj => move |_| { + obj.update_for_drop_active_target_type(); + })); + + self.sidebar.set( + &sidebar, + vec![drop_source_type_handler, drop_active_target_type_handler], + ); + } + + /// Set the list row to track for expander state. + fn set_list_row(&self, list_row: Option) { + if self.list_row.borrow().clone() == list_row { + return; + } + let obj = self.obj(); + + for binding in self.bindings.take() { + binding.unbind(); + } + + self.list_row.replace(list_row.clone()); + + let mut bindings = vec![]; + if let Some((row, item)) = list_row.zip(self.item()) { + if let Some(category) = item.downcast_ref::() { + let child = if let Some(child) = obj.child().and_downcast::() { + child + } else { + let child = CategoryRow::new(); + obj.set_child(Some(&child)); + obj.update_relation(&[Relation::LabelledBy(&[child.labelled_by()])]); + child + }; + child.set_category(Some(category.clone())); + + bindings.push( + row.bind_property("expanded", &child, "expanded") + .sync_create() + .build(), + ); + } else if let Some(room) = item.downcast_ref::() { + let child = if let Some(child) = obj.child().and_downcast::() { + child + } else { + let child = RoomRow::new(); + obj.set_child(Some(&child)); + child + }; + + child.set_room(Some(room.clone())); + } else if let Some(icon_item) = item.downcast_ref::() { + let child = if let Some(child) = obj.child().and_downcast::() { + child + } else { + let child = IconItemRow::new(); + obj.set_child(Some(&child)); + child + }; + + child.set_icon_item(Some(icon_item.clone())); + } else if let Some(verification) = item.downcast_ref::() { + let child = if let Some(child) = obj.child().and_downcast::() { + child + } else { + let child = VerificationRow::new(); + obj.set_child(Some(&child)); + child + }; + + child.set_identity_verification(Some(verification.clone())); + } else { + panic!("Wrong row item: {item:?}"); + } + + obj.update_for_drop_source_type(); + } + + self.bindings.replace(bindings); + + obj.notify_item(); + obj.notify_list_row(); + } + + /// The sidebar item of this row. + fn item(&self) -> Option { + self.list_row + .borrow() + .as_ref() + .and_then(|r| r.item()) + .and_downcast() + } + } } glib::wrapper! { + /// A row of the sidebar. pub struct Row(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -119,129 +193,6 @@ impl Row { .build() } - /// The ancestor sidebar of this row. - pub fn sidebar(&self) -> Sidebar { - self.imp().sidebar.obj().unwrap() - } - - /// Set the ancestor sidebar of this row. - fn set_sidebar(&self, sidebar: Option<&Sidebar>) { - let Some(sidebar) = sidebar else { - return; - }; - - let drop_source_type_handler = sidebar.connect_notify_local( - Some("drop-source-type"), - clone!(@weak self as obj => move |_, _| { - obj.update_for_drop_source_type(); - }), - ); - - let drop_active_target_type_handler = sidebar.connect_notify_local( - Some("drop-active-target-type"), - clone!(@weak self as obj => move |_, _| { - obj.update_for_drop_active_target_type(); - }), - ); - - self.imp().sidebar.set( - sidebar, - vec![drop_source_type_handler, drop_active_target_type_handler], - ); - } - - /// The sidebar item of this row. - pub fn item(&self) -> Option { - self.list_row().and_then(|r| r.item()).and_downcast() - } - - /// The list row to track for expander state. - pub fn list_row(&self) -> Option { - self.imp().list_row.borrow().clone() - } - - /// Set the list row to track for expander state. - pub fn set_list_row(&self, list_row: Option) { - let imp = self.imp(); - - if self.list_row() == list_row { - return; - } - - for binding in imp.bindings.take() { - binding.unbind(); - } - - let row = if let Some(row) = list_row.clone() { - imp.list_row.replace(list_row); - row - } else { - return; - }; - - let mut bindings = vec![]; - if let Some(item) = self.item() { - if let Some(category) = item.downcast_ref::() { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = CategoryRow::new(); - self.set_child(Some(&child)); - self.update_relation(&[Relation::LabelledBy(&[&child - .display_name() - .upcast()])]); - child - }; - child.set_category(Some(category.clone())); - - bindings.push( - row.bind_property("expanded", &child, "expanded") - .sync_create() - .build(), - ); - } else if let Some(room) = item.downcast_ref::() { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = RoomRow::new(); - self.set_child(Some(&child)); - child - }; - - child.set_room(Some(room.clone())); - } else if let Some(icon_item) = item.downcast_ref::() { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = IconItemRow::new(); - self.set_child(Some(&child)); - child - }; - - child.set_icon_item(Some(icon_item.clone())); - } else if let Some(verification) = item.downcast_ref::() { - let child = if let Some(child) = self.child().and_downcast::() { - child - } else { - let child = VerificationRow::new(); - self.set_child(Some(&child)); - child - }; - - child.set_identity_verification(Some(verification.clone())); - } else { - panic!("Wrong row item: {item:?}"); - } - - self.update_for_drop_source_type(); - } - - imp.bindings.replace(bindings); - - self.notify("item"); - self.notify("list-row"); - } - /// Get the `RoomType` of this item. /// /// If this is not a `Category` or one of its children, returns `None`. @@ -267,6 +218,10 @@ impl Row { /// Handle the drag-n-drop hovering this row. fn drop_accept(&self, drop: &gdk::Drop) -> bool { + let Some(sidebar) = self.sidebar() else { + return false; + }; + let room = drop .drag() .map(|drag| drag.content()) @@ -275,14 +230,13 @@ impl Row { if let Some(room) = room { if let Some(target_type) = self.room_type() { if room.category().can_change_to(target_type) { - self.sidebar() - .set_drop_active_target_type(Some(target_type)); + sidebar.set_drop_active_target_type(Some(target_type)); return true; } } else if let Some(item_type) = self.item_type() { if room.category() == RoomType::Left && item_type == ItemType::Forget { self.add_css_class("drop-active"); - self.sidebar().set_drop_active_target_type(None); + sidebar.set_drop_active_target_type(None); return true; } } @@ -293,7 +247,9 @@ impl Row { /// Handle the drag-n-drop leaving this row. fn drop_leave(&self) { self.remove_css_class("drop-active"); - self.sidebar().set_drop_active_target_type(None); + if let Some(sidebar) = self.sidebar() { + sidebar.set_drop_active_target_type(None); + } } /// Handle the drop on this row. @@ -316,7 +272,9 @@ impl Row { } } } - self.sidebar().set_drop_source_type(None); + if let Some(sidebar) = self.sidebar() { + sidebar.set_drop_source_type(None); + } ret } @@ -359,7 +317,7 @@ impl Row { /// Update the disabled or empty state of this drop target. fn update_for_drop_source_type(&self) { - let source_type = self.sidebar().drop_source_type(); + let source_type = self.sidebar().and_then(|s| s.drop_source_type()); if let Some(source_type) = source_type { if self @@ -407,9 +365,9 @@ impl Row { let Some(room_type) = self.room_type() else { return; }; - let target_type = self.sidebar().drop_active_target_type(); + let target_type = self.sidebar().and_then(|s| s.drop_active_target_type()); - if target_type.map_or(false, |target_type| target_type == room_type) { + if target_type.is_some_and(|target_type| target_type == room_type) { self.add_css_class("drop-active"); } else { self.remove_css_class("drop-active"); diff --git a/src/session/view/sidebar/verification_row.rs b/src/session/view/sidebar/verification_row.rs index 693ff7bc..94453340 100644 --- a/src/session/view/sidebar/verification_row.rs +++ b/src/session/view/sidebar/verification_row.rs @@ -7,14 +7,16 @@ mod imp { use std::cell::RefCell; use glib::subclass::InitializingObject; - use once_cell::sync::Lazy; use super::*; - #[derive(Debug, Default, CompositeTemplate)] + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/verification_row.ui")] + #[properties(wrapper_type = super::VerificationRow)] pub struct VerificationRow { - pub verification: RefCell>, + /// The identity verification represented by this row. + #[property(get, set = Self::set_identity_verification, explicit_notify, nullable)] + pub identity_verification: RefCell>, } #[glib::object_subclass] @@ -32,41 +34,27 @@ mod imp { } } - impl ObjectImpl for VerificationRow { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::builder::( - "identity-verification", - ) - .explicit_notify() - .build()] - }); - - PROPERTIES.as_ref() - } - - fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { - match pspec.name() { - "identity-verification" => { - self.obj().set_identity_verification(value.get().unwrap()) - } - _ => unimplemented!(), - } - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - match pspec.name() { - "identity-verification" => self.obj().identity_verification().to_value(), - _ => unimplemented!(), - } - } - } + #[glib::derived_properties] + impl ObjectImpl for VerificationRow {} impl WidgetImpl for VerificationRow {} impl BinImpl for VerificationRow {} + + impl VerificationRow { + /// Set the identity verification represented by this row. + fn set_identity_verification(&self, verification: Option) { + if self.identity_verification.borrow().clone() == verification { + return; + } + + self.identity_verification.replace(verification); + self.obj().notify_identity_verification(); + } + } } glib::wrapper! { + /// A sidebar row representing an identity verification. pub struct VerificationRow(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } @@ -75,19 +63,4 @@ impl VerificationRow { pub fn new() -> Self { glib::Object::new() } - - /// The identity verification represented by this row. - pub fn identity_verification(&self) -> Option { - self.imp().verification.borrow().clone() - } - - /// Set the identity verification represented by this row. - pub fn set_identity_verification(&self, verification: Option) { - if self.identity_verification() == verification { - return; - } - - self.imp().verification.replace(verification); - self.notify("identity-verification"); - } }