diff --git a/data/resources/style.css b/data/resources/style.css index a368d2c6..9bbbcc0c 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -447,21 +447,21 @@ sidebar-row icon-item image { min-width: 24px; /* Same width as avatars, so the text is aligned */ } -sidebar-row category { +sidebar-row sidebar-section { margin-top: 6px; font-size: 0.8em; font-weight: bold; } -sidebar-row category image.arrow { +sidebar-row sidebar-section image.arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); } -sidebar-row category:not(:checked) image.arrow:dir(ltr) { +sidebar-row sidebar-section:not(:checked) image.arrow:dir(ltr) { transform: rotate(-0.5turn); } -sidebar-row category:not(:checked) image.arrow:dir(rtl) { +sidebar-row sidebar-section:not(:checked) image.arrow:dir(rtl) { transform: rotate(0.5turn); } diff --git a/po/POTFILES.in b/po/POTFILES.in index a6b10bd0..c555aab9 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -75,7 +75,7 @@ src/session/model/room/join_rule.rs src/session/model/room/mod.rs src/session/model/room/permissions.rs src/session/model/room_list/mod.rs -src/session/model/sidebar_data/category/category_type.rs +src/session/model/sidebar_data/section/name.rs src/session/model/sidebar_data/icon_item.rs src/session/view/account_settings/general_page/change_password_subpage.rs src/session/view/account_settings/general_page/change_password_subpage.ui @@ -175,11 +175,11 @@ src/session/view/media_viewer.rs src/session/view/media_viewer.ui src/session/view/room_creation.rs src/session/view/room_creation.ui -src/session/view/sidebar/category_row.rs src/session/view/sidebar/mod.rs src/session/view/sidebar/mod.ui src/session/view/sidebar/room_row.rs src/session/view/sidebar/row.rs +src/session/view/sidebar/section_row.rs src/session_list/mod.rs src/shortcuts.ui src/user_facing_error.rs diff --git a/src/session/model/mod.rs b/src/session/model/mod.rs index fa9ba216..81c08172 100644 --- a/src/session/model/mod.rs +++ b/src/session/model/mod.rs @@ -23,8 +23,8 @@ pub use self::{ session::*, session_settings::{SessionSettings, StoredSessionSettings}, sidebar_data::{ - Category, CategoryType, Selection, SidebarIconItem, SidebarIconItemType, SidebarItemList, - SidebarListModel, + Selection, SidebarIconItem, SidebarIconItemType, SidebarItemList, SidebarListModel, + SidebarSection, SidebarSectionName, }, user::{User, UserExt}, user_sessions_list::{UserSession, UserSessionsList}, diff --git a/src/session/model/room/category.rs b/src/session/model/room/category.rs index 6af0405f..711d6da7 100644 --- a/src/session/model/room/category.rs +++ b/src/session/model/room/category.rs @@ -3,7 +3,7 @@ use std::fmt; use gtk::glib; use matrix_sdk::RoomState; -use crate::session::model::CategoryType; +use crate::session::model::SidebarSectionName; /// The category of a room. #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] @@ -73,35 +73,10 @@ impl RoomCategory { impl fmt::Display for RoomCategory { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - CategoryType::from(self).fmt(f) - } -} - -impl TryFrom for RoomCategory { - type Error = &'static str; + let Some(section_name) = SidebarSectionName::from_room_category(*self) else { + unimplemented!(); + }; - fn try_from(category_type: CategoryType) -> Result { - Self::try_from(&category_type) - } -} - -impl TryFrom<&CategoryType> for RoomCategory { - type Error = &'static str; - - fn try_from(category_type: &CategoryType) -> Result { - match category_type { - CategoryType::None => Err("CategoryType::None cannot be a RoomCategory"), - CategoryType::Invited => Ok(Self::Invited), - CategoryType::Favorite => Ok(Self::Favorite), - CategoryType::Normal => Ok(Self::Normal), - CategoryType::LowPriority => Ok(Self::LowPriority), - CategoryType::Left => Ok(Self::Left), - CategoryType::Outdated => Ok(Self::Outdated), - CategoryType::VerificationRequest => { - Err("CategoryType::VerificationRequest cannot be a RoomCategory") - } - CategoryType::Space => Ok(Self::Space), - CategoryType::Ignored => Ok(Self::Ignored), - } + section_name.fmt(f) } } diff --git a/src/session/model/session_settings.rs b/src/session/model/session_settings.rs index 86c68e6f..93677b8e 100644 --- a/src/session/model/session_settings.rs +++ b/src/session/model/session_settings.rs @@ -1,7 +1,9 @@ +use std::collections::BTreeSet; + use gtk::{glib, prelude::*, subclass::prelude::*}; use serde::{Deserialize, Serialize}; -use super::CategoryType; +use super::SidebarSectionName; use crate::Application; #[derive(Debug, Clone, Serialize, Deserialize, glib::Boxed)] @@ -32,9 +34,9 @@ pub struct StoredSessionSettings { )] typing_enabled: bool, - /// Which categories are expanded. + /// The sections that are expanded. #[serde(default)] - categories_expanded: CategoriesExpanded, + sections_expanded: SectionsExpanded, } impl Default for StoredSessionSettings { @@ -44,7 +46,7 @@ impl Default for StoredSessionSettings { notifications_enabled: true, public_read_receipts_enabled: true, typing_enabled: true, - categories_expanded: Default::default(), + sections_expanded: Default::default(), } } } @@ -200,76 +202,54 @@ impl SessionSettings { self.save(); } - /// Whether the given category is expanded. - pub fn is_category_expanded(&self, category: CategoryType) -> bool { + /// Whether the section with the given name is expanded. + pub fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { self.imp() .stored_settings .borrow() - .categories_expanded - .is_category_expanded(category) + .sections_expanded + .is_section_expanded(section_name) } - /// Set whether the given category is expanded. - pub fn set_category_expanded(&self, category: CategoryType, expanded: bool) { + /// Set whether the section with the given name is expanded. + pub fn set_section_expanded(&self, section_name: SidebarSectionName, expanded: bool) { self.imp() .stored_settings .borrow_mut() - .categories_expanded - .set_category_expanded(category, expanded); + .sections_expanded + .set_section_expanded(section_name, expanded); self.save(); } } -/// Whether the categories are expanded. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] -pub struct CategoriesExpanded { - verification_request: bool, - invited: bool, - favorite: bool, - normal: bool, - low_priority: bool, - left: bool, -} +/// The sections that are expanded. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct SectionsExpanded(BTreeSet); -impl CategoriesExpanded { - /// Whether the given category is expanded. - pub fn is_category_expanded(&self, category: CategoryType) -> bool { - match category { - CategoryType::VerificationRequest => self.verification_request, - CategoryType::Invited => self.invited, - CategoryType::Favorite => self.favorite, - CategoryType::Normal => self.normal, - CategoryType::LowPriority => self.low_priority, - CategoryType::Left => self.left, - _ => false, - } +impl SectionsExpanded { + /// Whether the section with the given name is expanded. + pub fn is_section_expanded(&self, section_name: SidebarSectionName) -> bool { + self.0.contains(§ion_name) } - /// Set whether the given category is expanded. - pub fn set_category_expanded(&mut self, category: CategoryType, expanded: bool) { - let field = match category { - CategoryType::VerificationRequest => &mut self.verification_request, - CategoryType::Invited => &mut self.invited, - CategoryType::Favorite => &mut self.favorite, - CategoryType::Normal => &mut self.normal, - CategoryType::LowPriority => &mut self.low_priority, - CategoryType::Left => &mut self.left, - _ => return, - }; - - *field = expanded; + /// Set whether the section with the given name is expanded. + pub fn set_section_expanded(&mut self, section_name: SidebarSectionName, expanded: bool) { + if expanded { + self.0.insert(section_name); + } else { + self.0.remove(§ion_name); + } } } -impl Default for CategoriesExpanded { +impl Default for SectionsExpanded { fn default() -> Self { - Self { - verification_request: true, - invited: true, - favorite: true, - normal: true, - low_priority: true, - left: false, - } + Self(BTreeSet::from([ + SidebarSectionName::VerificationRequest, + SidebarSectionName::Invited, + SidebarSectionName::Favorite, + SidebarSectionName::Normal, + SidebarSectionName::LowPriority, + ])) } } diff --git a/src/session/model/sidebar_data/category/category_type.rs b/src/session/model/sidebar_data/category/category_type.rs deleted file mode 100644 index fd0e4257..00000000 --- a/src/session/model/sidebar_data/category/category_type.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::fmt; - -use gettextrs::gettext; -use gtk::glib; - -use crate::session::model::RoomCategory; - -#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] -#[repr(i32)] -#[enum_type(name = "CategoryType")] -pub enum CategoryType { - #[default] - None = -1, - VerificationRequest = 0, - Invited = 1, - Favorite = 2, - Normal = 3, - LowPriority = 4, - Left = 5, - Outdated = 6, - Space = 7, - Ignored = 8, -} - -impl fmt::Display for CategoryType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let label = match self { - CategoryType::None => unimplemented!(), - CategoryType::VerificationRequest => gettext("Verifications"), - CategoryType::Invited => gettext("Invited"), - CategoryType::Favorite => gettext("Favorites"), - CategoryType::Normal => gettext("Rooms"), - CategoryType::LowPriority => gettext("Low Priority"), - CategoryType::Left => gettext("Historical"), - // These categories are hidden. - CategoryType::Outdated | CategoryType::Space | CategoryType::Ignored => { - unimplemented!() - } - }; - f.write_str(&label) - } -} - -impl From for CategoryType { - fn from(category: RoomCategory) -> Self { - Self::from(&category) - } -} - -impl From<&RoomCategory> for CategoryType { - fn from(category: &RoomCategory) -> Self { - match category { - RoomCategory::Invited => Self::Invited, - RoomCategory::Favorite => Self::Favorite, - RoomCategory::Normal => Self::Normal, - RoomCategory::LowPriority => Self::LowPriority, - RoomCategory::Left => Self::Left, - RoomCategory::Outdated => Self::Outdated, - RoomCategory::Space => Self::Space, - RoomCategory::Ignored => Self::Ignored, - } - } -} diff --git a/src/session/model/sidebar_data/icon_item.rs b/src/session/model/sidebar_data/icon_item.rs index 11ea03a4..eece4411 100644 --- a/src/session/model/sidebar_data/icon_item.rs +++ b/src/session/model/sidebar_data/icon_item.rs @@ -3,14 +3,16 @@ use std::fmt; use gettextrs::gettext; use gtk::{glib, prelude::*, subclass::prelude::*}; -use super::CategoryType; +use crate::session::model::RoomCategory; #[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)] #[repr(u32)] #[enum_type(name = "SidebarIconItemType")] pub enum SidebarIconItemType { + /// The explore view. #[default] Explore = 0, + /// An action to forget a room. Forget = 1, } @@ -88,12 +90,12 @@ impl SidebarIconItem { .build() } - /// Whether this item should be shown for a drag-n-drop from the given - /// category. - pub fn visible_for_category(&self, for_category: CategoryType) -> bool { + /// Whether this item should be shown for the drag-n-drop of a room with the + /// given category. + pub fn visible_for_room_category(&self, source_category: Option) -> bool { match self.item_type() { SidebarIconItemType::Explore => true, - SidebarIconItemType::Forget => for_category == CategoryType::Left, + SidebarIconItemType::Forget => source_category == Some(RoomCategory::Left), } } } diff --git a/src/session/model/sidebar_data/item.rs b/src/session/model/sidebar_data/item.rs index 5edd5305..0c45d656 100644 --- a/src/session/model/sidebar_data/item.rs +++ b/src/session/model/sidebar_data/item.rs @@ -1,7 +1,10 @@ use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; -use super::{Category, CategoryType, SidebarIconItem}; -use crate::utils::{BoundConstructOnlyObject, SingleItemListModel}; +use super::{SidebarIconItem, SidebarSection}; +use crate::{ + session::model::RoomCategory, + utils::{BoundConstructOnlyObject, SingleItemListModel}, +}; mod imp { use std::cell::{Cell, OnceCell}; @@ -19,7 +22,7 @@ mod imp { pub is_visible: Cell, /// Whether to inhibit the expanded state. /// - /// It means that all the categories will be expanded regardless of + /// It means that all the sections will be expanded regardless of /// their "is-expanded" property. #[property(get, set = Self::set_inhibit_expanded, explicit_notify)] pub inhibit_expanded: Cell, @@ -71,26 +74,26 @@ mod imp { fn set_inner_item(&self, item: glib::Object) { let mut handlers = Vec::new(); - let inner_model = if let Some(category) = item.downcast_ref::() { - // Create a list model to have an item for the category itself. - let category_model = SingleItemListModel::new(category); + let inner_model = if let Some(section) = item.downcast_ref::() { + // Create a list model to have an item for the section itself. + let section_model = SingleItemListModel::new(section); - // Filter the children depending on whether the category is expanded or not. + // Filter the children depending on whether the section is expanded or not. self.is_expanded_filter.set_filter_func(clone!( #[weak(rename_to = imp)] self, #[weak] - category, + section, #[upgrade_or] false, - move |_| imp.inhibit_expanded.get() || category.is_expanded() + move |_| imp.inhibit_expanded.get() || section.is_expanded() )); let children_model = gtk::FilterListModel::new( - Some(category.clone()), + Some(section.clone()), Some(self.is_expanded_filter.clone()), ); - let is_expanded_handler = category.connect_is_expanded_notify(clone!( + let is_expanded_handler = section.connect_is_expanded_notify(clone!( #[weak(rename_to = imp)] self, move |_| { @@ -101,7 +104,7 @@ mod imp { // Merge the models for the category and its children. let wrapper_model = gio::ListStore::new::(); - wrapper_model.append(&category_model); + wrapper_model.append(§ion_model); wrapper_model.append(&children_model); gtk::FlattenListModel::new(Some(wrapper_model)).upcast::() @@ -178,14 +181,14 @@ impl SidebarItem { .build() } - /// Update the visibility of this item for a drag-n-drop from the given - /// category. - pub fn update_visibility_for_category(&self, category_type: CategoryType) { + /// Update the visibility of this item for the drag-n-drop of a room with + /// the given category. + pub fn update_visibility_for_room_category(&self, source_category: Option) { let inner_item = self.inner_item(); - let visible = if let Some(category) = inner_item.downcast_ref::() { - category.visible_for_category(category_type) + let visible = if let Some(section) = inner_item.downcast_ref::() { + section.visible_for_room_category(source_category) } else if let Some(icon_item) = inner_item.downcast_ref::() { - icon_item.visible_for_category(category_type) + icon_item.visible_for_room_category(source_category) } else { true }; diff --git a/src/session/model/sidebar_data/item_list.rs b/src/session/model/sidebar_data/item_list.rs index 58525e37..ad7441e7 100644 --- a/src/session/model/sidebar_data/item_list.rs +++ b/src/session/model/sidebar_data/item_list.rs @@ -2,8 +2,10 @@ use std::cell::Cell; use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; -use super::{Category, CategoryType, SidebarIconItem, SidebarIconItemType, SidebarItem}; -use crate::session::model::{RoomList, VerificationList}; +use super::{ + SidebarIconItem, SidebarIconItemType, SidebarItem, SidebarSection, SidebarSectionName, +}; +use crate::session::model::{RoomCategory, RoomList, VerificationList}; /// The number of top-level items in the sidebar. const TOP_LEVEL_ITEMS_COUNT: usize = 8; @@ -20,16 +22,16 @@ mod imp { list: OnceCell<[SidebarItem; TOP_LEVEL_ITEMS_COUNT]>, /// The list of rooms. #[property(get, construct_only)] - pub room_list: OnceCell, + room_list: OnceCell, /// The list of verification requests. #[property(get, construct_only)] - pub verification_list: OnceCell, - /// The `CategoryType` to show all compatible categories for. + verification_list: OnceCell, + /// The room category to show all compatible sections and icon items + /// for. /// - /// The UI is updated to show possible actions for the list items - /// according to the `CategoryType`. - #[property(get, set = Self::set_show_all_for_category, explicit_notify, builder(CategoryType::default()))] - pub show_all_for_category: Cell, + /// The UI is updated to show possible drop actions for a room with the + /// given category. + show_all_for_room_category: Cell>, } #[glib::object_subclass] @@ -51,22 +53,28 @@ mod imp { let list = self.list.get_or_init(|| { [ SidebarItem::new(SidebarIconItem::new(SidebarIconItemType::Explore)), - SidebarItem::new(Category::new( - CategoryType::VerificationRequest, + SidebarItem::new(SidebarSection::new( + SidebarSectionName::VerificationRequest, &verification_list, )), - SidebarItem::new(Category::new(CategoryType::Invited, &room_list)), - SidebarItem::new(Category::new(CategoryType::Favorite, &room_list)), - SidebarItem::new(Category::new(CategoryType::Normal, &room_list)), - SidebarItem::new(Category::new(CategoryType::LowPriority, &room_list)), - SidebarItem::new(Category::new(CategoryType::Left, &room_list)), + SidebarItem::new(SidebarSection::new(SidebarSectionName::Invited, &room_list)), + SidebarItem::new(SidebarSection::new( + SidebarSectionName::Favorite, + &room_list, + )), + SidebarItem::new(SidebarSection::new(SidebarSectionName::Normal, &room_list)), + SidebarItem::new(SidebarSection::new( + SidebarSectionName::LowPriority, + &room_list, + )), + SidebarItem::new(SidebarSection::new(SidebarSectionName::Left, &room_list)), SidebarItem::new(SidebarIconItem::new(SidebarIconItemType::Forget)), ] }); for item in list { - if let Some(category) = item.inner_item().downcast_ref::() { - category.connect_empty_notify(clone!( + if let Some(section) = item.inner_item().downcast_ref::() { + section.connect_is_empty_notify(clone!( #[weak(rename_to = imp)] self, #[weak] @@ -101,28 +109,27 @@ mod imp { self.list.get().unwrap() } - /// Set the `CategoryType` to show all compatible categories for. - fn set_show_all_for_category(&self, category: CategoryType) { - if category == self.show_all_for_category.get() { + /// Set the room category to show all compatible sections and icon items + /// for. + pub(super) fn set_show_all_for_room_category(&self, category: Option) { + if self.show_all_for_room_category.get() == category { return; } - self.show_all_for_category.set(category); + self.show_all_for_room_category.set(category); for item in self.list() { self.update_item_visibility(item); } - - self.obj().notify_show_all_for_category(); } /// Update the visibility of the given item. fn update_item_visibility(&self, item: &SidebarItem) { - item.update_visibility_for_category(self.show_all_for_category.get()); + item.update_visibility_for_room_category(self.show_all_for_room_category.get()); } - /// Set whether to inhibit the expanded state of the categories. + /// Set whether to inhibit the expanded state of the sections. /// - /// It means that all the categories will be expanded regardless of + /// It means that all the sections will be expanded regardless of /// their "is-expanded" property. pub(super) fn inhibit_expanded(&self, inhibit: bool) { for item in self.list() { @@ -151,26 +158,35 @@ impl SidebarItemList { .build() } - /// Set whether to inhibit the expanded state of the categories. + /// Set the room category to show all compatible sections and icon items + /// for. + pub fn set_show_all_for_room_category(&self, category: Option) { + self.imp().set_show_all_for_room_category(category); + } + + /// Set whether to inhibit the expanded state of the sections. /// - /// It means that all the categories will be expanded regardless of their + /// It means that all the sections will be expanded regardless of their /// "is-expanded" property. pub fn inhibit_expanded(&self, inhibit: bool) { self.imp().inhibit_expanded(inhibit); } - /// Returns the `Category` object representing the item that a user can - /// toggle to show or hide the given room `CategoryType` - pub fn category_from_type(&self, category_type: CategoryType) -> Option { - let list = self.imp().list(); - match category_type { - CategoryType::VerificationRequest => list[1].inner_item().downcast().ok(), - CategoryType::Invited => list[2].inner_item().downcast().ok(), - CategoryType::Favorite => list[3].inner_item().downcast().ok(), - CategoryType::Normal => list[4].inner_item().downcast().ok(), - CategoryType::LowPriority => list[5].inner_item().downcast().ok(), - CategoryType::Left => list[6].inner_item().downcast().ok(), - _ => None, - } + /// Returns the [`SidebarSection`] for the given room category. + pub fn section_from_room_category(&self, category: RoomCategory) -> Option { + let index = match category { + RoomCategory::Invited => 2, + RoomCategory::Favorite => 3, + RoomCategory::Normal => 4, + RoomCategory::LowPriority => 5, + RoomCategory::Left => 6, + _ => return None, + }; + + self.imp() + .list() + .get(index) + .map(|item| item.inner_item()) + .and_downcast() } } diff --git a/src/session/model/sidebar_data/mod.rs b/src/session/model/sidebar_data/mod.rs index 7b81e254..e2fab1db 100644 --- a/src/session/model/sidebar_data/mod.rs +++ b/src/session/model/sidebar_data/mod.rs @@ -1,15 +1,15 @@ -mod category; mod icon_item; mod item; mod item_list; mod list_model; +mod section; mod selection; pub use self::{ - category::{Category, CategoryType}, icon_item::{SidebarIconItem, SidebarIconItemType}, item::SidebarItem, item_list::SidebarItemList, list_model::SidebarListModel, + section::{SidebarSection, SidebarSectionName}, selection::Selection, }; diff --git a/src/session/model/sidebar_data/category/mod.rs b/src/session/model/sidebar_data/section/mod.rs similarity index 53% rename from src/session/model/sidebar_data/category/mod.rs rename to src/session/model/sidebar_data/section/mod.rs index 27ab2356..3b279bb1 100644 --- a/src/session/model/sidebar_data/category/mod.rs +++ b/src/session/model/sidebar_data/section/mod.rs @@ -1,15 +1,10 @@ -use gtk::{ - gio, glib, - glib::{clone, closure}, - prelude::*, - subclass::prelude::*, -}; +use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; -mod category_filter; -mod category_type; +mod name; +mod room_category_filter; -use self::category_filter::CategoryFilter; -pub use self::category_type::CategoryType; +pub use self::name::SidebarSectionName; +use self::room_category_filter::RoomCategoryFilter; use crate::{ session::model::{Room, RoomCategory, RoomList, SessionSettings, VerificationList}, utils::ExpressionListModel, @@ -24,38 +19,38 @@ mod imp { use super::*; #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::Category)] - pub struct Category { - /// The source model of this category. + #[properties(wrapper_type = super::SidebarSection)] + pub struct SidebarSection { + /// The source model of this section. #[property(get, set = Self::set_model, construct_only)] pub model: OnceCell, - /// The inner model of this category. + /// The inner model of this section. inner_model: OnceCell, - /// The filter of this category. - pub filter: CategoryFilter, - /// The type of this category. - #[property(get = Self::category_type, set = Self::set_category_type, construct_only, builder(CategoryType::default()))] - pub category_type: PhantomData, - /// Whether this category is empty. - #[property(get)] - pub empty: Cell, - /// The display name of this category. + /// The filter of this section. + pub filter: RoomCategoryFilter, + /// The name of this section. + #[property(get, set = Self::set_name, construct_only, builder(SidebarSectionName::default()))] + pub name: Cell, + /// The display name of this section. #[property(get = Self::display_name)] pub display_name: PhantomData, - /// Whether this category is expanded. + /// Whether this section is empty. + #[property(get)] + pub is_empty: Cell, + /// Whether this section is expanded. #[property(get, set = Self::set_is_expanded, explicit_notify)] pub is_expanded: Cell, } #[glib::object_subclass] - impl ObjectSubclass for Category { - const NAME: &'static str = "Category"; - type Type = super::Category; + impl ObjectSubclass for SidebarSection { + const NAME: &'static str = "SidebarSection"; + type Type = super::SidebarSection; type Interfaces = (gio::ListModel,); } #[glib::derived_properties] - impl ObjectImpl for Category { + impl ObjectImpl for SidebarSection { fn constructed(&self) { self.parent_constructed(); @@ -63,12 +58,12 @@ mod imp { return; }; - let is_expanded = settings.is_category_expanded(self.category_type()); + let is_expanded = settings.is_section_expanded(self.name.get()); self.set_is_expanded(is_expanded); } } - impl ListModelImpl for Category { + impl ListModelImpl for SidebarSection { fn item_type(&self) -> glib::Type { glib::Object::static_type() } @@ -82,34 +77,29 @@ mod imp { } } - impl Category { - /// The source model of this category. + impl SidebarSection { + /// The source model of this section. fn model(&self) -> &gio::ListModel { self.model.get().unwrap() } - /// Set the source model of this category. + /// Set the source model of this section. fn set_model(&self, model: gio::ListModel) { let model = self.model.get_or_init(|| model).clone(); let obj = self.obj(); - // Special-case room lists so that they are sorted and in the right category. + // Special-case room lists so that they are sorted and in the right section. let inner_model = if model.is::() { - let room_category_type = Room::this_expression("category") - .chain_closure::(closure!( - |_: Option, category: RoomCategory| { - CategoryType::from(category) - } - )); + let room_category = Room::this_expression("category"); self.filter - .set_expression(Some(room_category_type.clone().upcast())); + .set_expression(Some(room_category.clone().upcast())); - let category_type_expr_model = ExpressionListModel::new(); - category_type_expr_model.set_expressions(vec![room_category_type.upcast()]); - category_type_expr_model.set_model(Some(model)); + let section_name_expr_model = ExpressionListModel::new(); + section_name_expr_model.set_expressions(vec![room_category.upcast()]); + section_name_expr_model.set_model(Some(model)); let filter_model = gtk::FilterListModel::new( - Some(category_type_expr_model), + Some(section_name_expr_model), Some(self.filter.clone()), ); @@ -135,45 +125,45 @@ mod imp { obj, move |model, pos, removed, added| { obj.items_changed(pos, removed, added); - obj.imp().set_empty(model.n_items() == 0); + obj.imp().set_is_empty(model.n_items() == 0); } )); - self.set_empty(inner_model.n_items() == 0); + self.set_is_empty(inner_model.n_items() == 0); self.inner_model.set(inner_model).unwrap(); } - /// The inner model of this category. + /// The inner model of this section. fn inner_model(&self) -> &gio::ListModel { self.inner_model.get().unwrap() } - /// The type of this category. - fn category_type(&self) -> CategoryType { - self.filter.category_type() + /// Set the name of this section. + fn set_name(&self, name: SidebarSectionName) { + if let Some(room_category) = name.as_room_category() { + self.filter.set_room_category(room_category); + } + + self.name.set(name); + self.obj().notify_name(); } - /// Set the type of this category. - fn set_category_type(&self, type_: CategoryType) { - self.filter.set_category_type(type_); + /// The display name of this section. + fn display_name(&self) -> String { + self.name.get().to_string() } - /// Set whether this category is empty. - fn set_empty(&self, empty: bool) { - if empty == self.empty.get() { + /// Set whether this section is empty. + fn set_is_empty(&self, is_empty: bool) { + if is_empty == self.is_empty.get() { return; } - self.empty.set(empty); - self.obj().notify_empty(); + self.is_empty.set(is_empty); + self.obj().notify_is_empty(); } - /// The display name of this category. - fn display_name(&self) -> String { - self.category_type().to_string() - } - - /// Set whether this category is expanded. + /// Set whether this section is expanded. fn set_is_expanded(&self, expanded: bool) { if self.is_expanded.get() == expanded { return; @@ -183,7 +173,7 @@ mod imp { self.obj().notify_is_expanded(); if let Some(settings) = self.session_settings() { - settings.set_category_expanded(self.category_type(), expanded); + settings.set_section_expanded(self.name.get(), expanded); } } @@ -204,34 +194,31 @@ mod imp { } glib::wrapper! { - /// A list of items in the same category. - pub struct Category(ObjectSubclass) + /// A list of items in the same section of the sidebar. + pub struct SidebarSection(ObjectSubclass) @implements gio::ListModel; } -impl Category { - /// Constructs a new `Category` with the given category type and source - /// model. - pub fn new(category_type: CategoryType, model: &impl IsA) -> Self { +impl SidebarSection { + /// Constructs a new `SidebarSection` with the given name and source model. + pub fn new(name: SidebarSectionName, model: &impl IsA) -> Self { glib::Object::builder() - .property("category-type", category_type) + .property("name", name) .property("model", model) .build() } - /// Whether this category should be shown for a drag-n-drop from the given - /// category. - pub fn visible_for_category(&self, for_category: CategoryType) -> bool { - if !self.empty() { + /// Whether this section should be shown for the drag-n-drop of a room with + /// the given category. + pub fn visible_for_room_category(&self, source_category: Option) -> bool { + if !self.is_empty() { return true; } - let room_categories = RoomCategory::try_from(for_category) - .ok() - .zip(RoomCategory::try_from(self.category_type()).ok()); - - room_categories.is_some_and(|(source_category, target_category)| { - source_category.can_change_to(target_category) - }) + source_category + .zip(self.name().as_room_category()) + .is_some_and(|(source_category, target_category)| { + source_category.can_change_to(target_category) + }) } } diff --git a/src/session/model/sidebar_data/section/name.rs b/src/session/model/sidebar_data/section/name.rs new file mode 100644 index 00000000..7439a49c --- /dev/null +++ b/src/session/model/sidebar_data/section/name.rs @@ -0,0 +1,73 @@ +use std::fmt; + +use gettextrs::gettext; +use gtk::glib; +use serde::{Deserialize, Serialize}; + +use crate::session::model::RoomCategory; + +/// The possible names of the sections in the sidebar. +#[derive( + Debug, Default, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, glib::Enum, Serialize, Deserialize, +)] +#[enum_type(name = "SidebarSectionName")] +#[serde(rename_all = "kebab-case")] +pub enum SidebarSectionName { + /// The section for verification requests. + VerificationRequest, + /// The section for room invites. + Invited, + /// The section for favorite rooms. + Favorite, + /// The section for joined rooms without a tag. + #[default] + Normal, + /// The section for low-priority rooms. + LowPriority, + /// The section for room that were left. + Left, +} + +impl SidebarSectionName { + /// Convert the given `RoomCategory` to a `SidebarSectionName`, if possible. + pub fn from_room_category(category: RoomCategory) -> Option { + let name = match category { + RoomCategory::Invited => Self::Invited, + RoomCategory::Favorite => Self::Favorite, + RoomCategory::Normal => Self::Normal, + RoomCategory::LowPriority => Self::LowPriority, + RoomCategory::Left => Self::Left, + RoomCategory::Outdated | RoomCategory::Space | RoomCategory::Ignored => return None, + }; + + Some(name) + } + + /// Convert this `SidebarSectionName` to a `RoomCategory`, if possible. + pub fn as_room_category(&self) -> Option { + let category = match self { + Self::VerificationRequest => return None, + Self::Invited => RoomCategory::Invited, + Self::Favorite => RoomCategory::Favorite, + Self::Normal => RoomCategory::Normal, + Self::LowPriority => RoomCategory::LowPriority, + Self::Left => RoomCategory::Left, + }; + + Some(category) + } +} + +impl fmt::Display for SidebarSectionName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let label = match self { + SidebarSectionName::VerificationRequest => gettext("Verifications"), + SidebarSectionName::Invited => gettext("Invited"), + SidebarSectionName::Favorite => gettext("Favorites"), + SidebarSectionName::Normal => gettext("Rooms"), + SidebarSectionName::LowPriority => gettext("Low Priority"), + SidebarSectionName::Left => gettext("Historical"), + }; + f.write_str(&label) + } +} diff --git a/src/session/model/sidebar_data/category/category_filter.rs b/src/session/model/sidebar_data/section/room_category_filter.rs similarity index 50% rename from src/session/model/sidebar_data/category/category_filter.rs rename to src/session/model/sidebar_data/section/room_category_filter.rs index 4f55e7d8..98229ce4 100644 --- a/src/session/model/sidebar_data/category/category_filter.rs +++ b/src/session/model/sidebar_data/section/room_category_filter.rs @@ -1,6 +1,6 @@ use gtk::{glib, prelude::*, subclass::prelude::*}; -use super::CategoryType; +use crate::session::model::RoomCategory; mod imp { use std::cell::{Cell, RefCell}; @@ -8,32 +8,30 @@ mod imp { use super::*; #[derive(Debug, Default, glib::Properties)] - #[properties(wrapper_type = super::CategoryFilter)] - pub struct CategoryFilter { + #[properties(wrapper_type = super::RoomCategoryFilter)] + pub struct RoomCategoryFilter { /// The expression to watch. + /// + /// This expression must return a [`RoomCategory`]. #[property(get, set = Self::set_expression, explicit_notify, nullable)] - pub expression: RefCell>, - /// The category type to filter. - #[property(get, set = Self::set_category_type, explicit_notify, builder(CategoryType::default()))] - pub category_type: Cell, + expression: RefCell>, + /// The room category to filter. + #[property(get, set = Self::set_room_category, explicit_notify, builder(RoomCategory::default()))] + room_category: Cell, } #[glib::object_subclass] - impl ObjectSubclass for CategoryFilter { - const NAME: &'static str = "CategoryFilter"; - type Type = super::CategoryFilter; + impl ObjectSubclass for RoomCategoryFilter { + const NAME: &'static str = "RoomCategoryFilter"; + type Type = super::RoomCategoryFilter; type ParentType = gtk::Filter; } #[glib::derived_properties] - impl ObjectImpl for CategoryFilter {} + impl ObjectImpl for RoomCategoryFilter {} - impl FilterImpl for CategoryFilter { + impl FilterImpl for RoomCategoryFilter { fn strictness(&self) -> gtk::FilterMatch { - if self.category_type.get() == CategoryType::None { - return gtk::FilterMatch::All; - } - if self.expression.borrow().is_none() { return gtk::FilterMatch::None; } @@ -42,29 +40,24 @@ mod imp { } fn match_(&self, item: &glib::Object) -> bool { - let category_type = self.category_type.get(); - if category_type == CategoryType::None { - return true; - } + let room_category = self.room_category.get(); - let Some(value) = self - .expression + self.expression .borrow() .as_ref() .and_then(|e| e.evaluate(Some(item))) - .map(|v| v.get::().unwrap()) - else { - return false; - }; - - value == category_type + .map(|v| { + v.get::() + .expect("expression returns a room category") + }) + .is_some_and(|item_room_category| item_room_category == room_category) } } - impl CategoryFilter { + impl RoomCategoryFilter { /// Set the expression to watch. /// - /// This expression must return a [`CategoryType`]. + /// This expression must return a [`RoomCategory`]. fn set_expression(&self, expression: Option) { let prev_expression = self.expression.borrow().clone(); @@ -73,9 +66,7 @@ mod imp { } let obj = self.obj(); - let change = if self.category_type.get() == CategoryType::None { - None - } else if prev_expression.is_none() { + let change = if prev_expression.is_none() { Some(gtk::FilterChange::LessStrict) } else if expression.is_none() { Some(gtk::FilterChange::MoreStrict) @@ -90,47 +81,43 @@ mod imp { obj.notify_expression(); } - /// Set the category type to filter. - fn set_category_type(&self, category_type: CategoryType) { - let prev_category_type = self.category_type.get(); + /// Set the room category to filter. + fn set_room_category(&self, category: RoomCategory) { + let prev_category = self.room_category.get(); - if prev_category_type == category_type { + if prev_category == category { return; } let obj = self.obj(); let change = if self.expression.borrow().is_none() { None - } else if prev_category_type == CategoryType::None { - Some(gtk::FilterChange::MoreStrict) - } else if category_type == CategoryType::None { - Some(gtk::FilterChange::LessStrict) } else { Some(gtk::FilterChange::Different) }; - self.category_type.set(category_type); + self.room_category.set(category); if let Some(change) = change { obj.changed(change) } - obj.notify_category_type(); + obj.notify_room_category(); } } } glib::wrapper! { - /// A filter by `CategoryType`. - pub struct CategoryFilter(ObjectSubclass) + /// A `GtkFilter` to filter by [`RoomCategory`]. + pub struct RoomCategoryFilter(ObjectSubclass) @extends gtk::Filter; } -impl CategoryFilter { +impl RoomCategoryFilter { pub fn new() -> Self { glib::Object::new() } } -impl Default for CategoryFilter { +impl Default for RoomCategoryFilter { fn default() -> Self { Self::new() } diff --git a/src/session/view/session_view.rs b/src/session/view/session_view.rs index 1a922de3..3ab24e43 100644 --- a/src/session/view/session_view.rs +++ b/src/session/view/session_view.rs @@ -273,22 +273,22 @@ impl SessionView { self.select_item(room.clone()); - // if we selected a room, it might not currently be visible - // we now make it visible - let Some(room) = room else { return }; - - // ensure the category of the room is expanded - let category_type = room.category().into(); - if let Some(category) = imp - .sidebar - .list_model() - .and_then(|list_model| list_model.item_list().category_from_type(category_type)) - { - category.set_is_expanded(true); + // If we selected a room, make sure it is visible in the sidebar. + let Some(room) = room else { + return; + }; + + // First, ensure that the section containing the room is expanded. + if let Some(section) = imp.sidebar.list_model().and_then(|list_model| { + list_model + .item_list() + .section_from_room_category(room.category()) + }) { + section.set_is_expanded(true); }; - // now the room should be in the sidebar, but we still need to scroll to it to - // make it visible + // Now scroll to the room to make sure that it is in the viewport, and that it + // is focused in the list for users using keyboard navigation. imp.sidebar.scroll_to_selection(); } diff --git a/src/session/view/sidebar/category_row.rs b/src/session/view/sidebar/category_row.rs deleted file mode 100644 index b6ab8e67..00000000 --- a/src/session/view/sidebar/category_row.rs +++ /dev/null @@ -1,212 +0,0 @@ -use adw::subclass::prelude::BinImpl; -use gettextrs::gettext; -use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; - -use crate::session::model::{Category, CategoryType}; - -mod imp { - use std::{ - cell::{Cell, RefCell}, - marker::PhantomData, - }; - - use glib::subclass::InitializingObject; - - use super::*; - - #[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>, - category_binding: 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] - pub display_name: TemplateChild, - } - - #[glib::object_subclass] - impl ObjectSubclass for CategoryRow { - const NAME: &'static str = "SidebarCategoryRow"; - type Type = super::CategoryRow; - type ParentType = adw::Bin; - - fn class_init(klass: &mut Self::Class) { - Self::bind_template(klass); - klass.set_css_name("category"); - } - - fn instance_init(obj: &InitializingObject) { - obj.init_template(); - } - } - - #[glib::derived_properties] - impl ObjectImpl for CategoryRow { - fn constructed(&self) { - self.parent_constructed(); - - self.obj().connect_parent_notify(|obj| { - obj.set_expanded_accessibility_state(obj.expanded()); - }); - } - - fn dispose(&self) { - if let Some(binding) = self.category_binding.take() { - binding.unbind(); - } - } - } - - 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() == category { - return; - } - - if let Some(binding) = self.category_binding.take() { - binding.unbind(); - } - let obj = self.obj(); - - if let Some(category) = &category { - let category_binding = category - .bind_property("is-expanded", &*obj, "expanded") - .sync_create() - .build(); - self.category_binding.replace(Some(category_binding)); - } - - self.category.replace(category); - - 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()?.category_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; -} - -impl CategoryRow { - pub fn new() -> Self { - glib::Object::new() - } - - /// 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(&[gtk::accessible::State::Expanded(Some(expanded))]); - } - } - - /// 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/mod.rs b/src/session/view/sidebar/mod.rs index faa3f01a..2c5ba5ac 100644 --- a/src/session/view/sidebar/mod.rs +++ b/src/session/view/sidebar/mod.rs @@ -2,19 +2,19 @@ use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; use gtk::{ gio, - glib::{self, clone}, + glib::{self, clone, closure_local}, CompositeTemplate, ListScrollFlags, }; use tracing::error; -mod category_row; mod icon_item_row; mod room_row; mod row; +mod section_row; mod verification_row; use self::{ - category_row::CategoryRow, icon_item_row::IconItemRow, room_row::RoomRow, row::Row, + icon_item_row::IconItemRow, room_row::RoomRow, row::Row, section_row::SidebarSectionRow, verification_row::VerificationRow, }; use super::{account_settings::AccountSettingsSubpage, AccountSettings}; @@ -22,19 +22,17 @@ use crate::{ account_switcher::AccountSwitcherButton, components::OfflineBanner, session::model::{ - Category, CategoryType, CryptoIdentityState, RecoveryState, RoomCategory, Selection, - SessionVerificationState, SidebarListModel, User, + CryptoIdentityState, RecoveryState, RoomCategory, Selection, SessionVerificationState, + SidebarListModel, SidebarSection, User, }, utils::expression, }; mod imp { - use std::{ - cell::{Cell, OnceCell, RefCell}, - marker::PhantomData, - }; + use std::cell::{Cell, OnceCell, RefCell}; - use glib::subclass::InitializingObject; + use glib::subclass::{InitializingObject, Signal}; + use once_cell::sync::Lazy; use super::*; @@ -62,18 +60,11 @@ mod imp { pub user: RefCell>, /// The category of the source that activated drop mode. pub drop_source_category: 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 category of the drop target that is currently hovered. pub drop_active_target_category: 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 binding: RefCell>, pub expr_watch: RefCell>, session_handlers: RefCell>, } @@ -101,6 +92,16 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for Sidebar { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("drop-source-category-changed").build(), + Signal::builder("drop-active-target-category-changed").build(), + ] + }); + SIGNALS.as_ref() + } + fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); @@ -132,8 +133,8 @@ mod imp { return; }; - if let Some(category) = item.downcast_ref::() { - category.set_is_expanded(!category.is_expanded()); + if let Some(section) = item.downcast_ref::() { + section.set_is_expanded(!section.is_expanded()); } else { model.set_selected(pos); } @@ -152,9 +153,6 @@ mod imp { } fn dispose(&self) { - if let Some(binding) = self.binding.take() { - binding.unbind(); - } if let Some(expr_watch) = self.expr_watch.take() { expr_watch.unwatch(); } @@ -239,24 +237,11 @@ mod imp { } let obj = self.obj(); - if let Some(binding) = self.binding.take() { - binding.unbind(); - } if let Some(expr_watch) = self.expr_watch.take() { expr_watch.unwatch(); } if let Some(list_model) = &list_model { - let binding = obj - .bind_property( - "drop-source-category-type", - &list_model.item_list(), - "show-all-for-category", - ) - .sync_create() - .build(); - self.binding.replace(Some(binding)); - let expr_watch = expression::normalize_string( self.room_search_entry.property_expression("text"), ) @@ -268,22 +253,6 @@ mod imp { obj.notify_list_model(); } - /// The `CategoryType` of the source that activated drop mode. - fn drop_source_category_type(&self) -> CategoryType { - self.drop_source_category - .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_category - .get() - .map(Into::into) - .unwrap_or_default() - } - /// Update the security banner. fn update_security_banner(&self) { let Some(session) = self.user.borrow().as_ref().map(|u| u.session()) else { @@ -404,7 +373,12 @@ impl Sidebar { imp.listview.remove_css_class("drop-mode"); } - self.notify_drop_source_category_type(); + let Some(item_list) = self.list_model().map(|model| model.item_list()) else { + return; + }; + + item_list.set_show_all_for_room_category(source_category); + self.emit_by_name::<()>("drop-source-category-changed", &[]); } /// The category of the drop target that is currently hovered. @@ -419,7 +393,7 @@ impl Sidebar { } self.imp().drop_active_target_category.set(target_category); - self.notify_drop_active_target_category_type(); + self.emit_by_name::<()>("drop-active-target-category-changed", &[]); } /// The shared popover for a room row in the sidebar. @@ -456,4 +430,33 @@ impl Sidebar { .scroll_to(selected, ListScrollFlags::FOCUS, None); } } + + /// Connect to the signal emitted when the drop source category changed. + pub fn connect_drop_source_category_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "drop-source-category-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } + + /// Connect to the signal emitted when the drop active target category + /// changed. + pub fn connect_drop_active_target_category_changed( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_closure( + "drop-active-target-category-changed", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } } diff --git a/src/session/view/sidebar/row.rs b/src/session/view/sidebar/row.rs index 8252be60..39e454fa 100644 --- a/src/session/view/sidebar/row.rs +++ b/src/session/view/sidebar/row.rs @@ -4,14 +4,14 @@ use gtk::{accessible::Relation, gdk, gio, glib, glib::clone}; use ruma::api::client::receipt::create_receipt::v3::ReceiptType; use tracing::error; -use super::{CategoryRow, IconItemRow, RoomRow, Sidebar, VerificationRow}; +use super::{IconItemRow, RoomRow, Sidebar, SidebarSectionRow, VerificationRow}; use crate::{ components::{ confirm_leave_room_dialog, ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl, }, session::model::{ - Category, CategoryType, IdentityVerification, ReceiptPosition, Room, RoomCategory, - SidebarIconItem, SidebarIconItemType, User, + IdentityVerification, ReceiptPosition, Room, RoomCategory, SidebarIconItem, + SidebarIconItemType, SidebarSection, User, }, spawn, spawn_tokio, toast, utils::BoundObjectWeakRef, @@ -27,10 +27,10 @@ mod imp { pub struct Row { /// The ancestor sidebar of this row. #[property(get, set = Self::set_sidebar, construct_only)] - pub sidebar: BoundObjectWeakRef, + sidebar: BoundObjectWeakRef, /// The item of this row. #[property(get, set = Self::set_item, explicit_notify, nullable)] - pub item: RefCell>, + item: RefCell>, room_handler: RefCell>, room_join_rule_handler: RefCell>, room_is_read_handler: RefCell>, @@ -114,30 +114,31 @@ mod imp { 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, + fn set_sidebar(&self, sidebar: &Sidebar) { + let drop_source_category_handler = + sidebar.connect_drop_source_category_changed(clone!( + #[weak(rename_to = imp)] + self, move |_| { - obj.update_for_drop_source_type(); + imp.update_for_drop_source_category(); } )); - let drop_active_target_type_handler = sidebar - .connect_drop_active_target_category_type_notify(clone!( - #[weak] - obj, + let drop_active_target_category_handler = sidebar + .connect_drop_active_target_category_changed(clone!( + #[weak(rename_to = imp)] + self, move |_| { - obj.update_for_drop_active_target_type(); + imp.update_for_drop_active_target_category(); } )); self.sidebar.set( - &sidebar, - vec![drop_source_type_handler, drop_active_target_type_handler], + sidebar, + vec![ + drop_source_category_handler, + drop_active_target_category_handler, + ], ); } @@ -165,16 +166,17 @@ mod imp { self.update_context_menu(); if let Some(item) = item { - if let Some(category) = item.downcast_ref::() { - let child = if let Some(child) = obj.child().and_downcast::() { + if let Some(section) = item.downcast_ref::() { + let child = if let Some(child) = obj.child().and_downcast::() + { child } else { - let child = CategoryRow::new(); + let child = SidebarSectionRow::new(); obj.set_child(Some(&child)); obj.update_relation(&[Relation::LabelledBy(&[child.labelled_by()])]); child }; - child.set_category(Some(category.clone())); + child.set_section(Some(section.clone())); } else if let Some(room) = item.downcast_ref::() { let child = if let Some(child) = obj.child().and_downcast::() { child @@ -238,7 +240,7 @@ mod imp { panic!("Wrong row item: {item:?}"); } - obj.update_for_drop_source_type(); + self.update_for_drop_source_category(); } self.update_context_menu(); @@ -250,6 +252,31 @@ mod imp { self.item.borrow().clone().and_downcast() } + /// Get the `RoomCategory` of this row, if any. + /// + /// If this does not display a room or a section containing rooms, + /// returns `None`. + pub(super) fn room_category(&self) -> Option { + let borrowed_item = self.item.borrow(); + let item = borrowed_item.as_ref()?; + + if let Some(room) = item.downcast_ref::() { + Some(room.category()) + } else { + item.downcast_ref::() + .and_then(|section| section.name().as_room_category()) + } + } + + /// Get the [`SidebarIconItemType`] of the icon item displayed by this + /// row, if any. + pub(super) fn item_type(&self) -> Option { + let borrowed_item = self.item.borrow(); + let item = borrowed_item.as_ref()?; + item.downcast_ref::() + .map(|i| i.item_type()) + } + /// Whether this has a room context menu. fn has_room_context_menu(&self) -> bool { self.room().is_some_and(|r| { @@ -481,6 +508,73 @@ mod imp { Some(action_group) } + + /// Update the disabled or empty state of this drop target. + fn update_for_drop_source_category(&self) { + let obj = self.obj(); + let source_category = self.sidebar.obj().and_then(|s| s.drop_source_category()); + + if let Some(source_category) = source_category { + if self + .room_category() + .is_some_and(|row_category| source_category.can_change_to(row_category)) + { + obj.remove_css_class("drop-disabled"); + + if self + .item + .borrow() + .as_ref() + .and_then(|o| o.downcast_ref::()) + .is_some_and(|section| section.is_empty()) + { + obj.add_css_class("drop-empty"); + } else { + obj.remove_css_class("drop-empty"); + } + } else { + let is_forget_item = self + .item_type() + .is_some_and(|item_type| item_type == SidebarIconItemType::Forget); + if is_forget_item && source_category == RoomCategory::Left { + obj.remove_css_class("drop-disabled"); + } else { + obj.add_css_class("drop-disabled"); + obj.remove_css_class("drop-empty"); + } + } + } else { + // Clear style + obj.remove_css_class("drop-disabled"); + obj.remove_css_class("drop-empty"); + obj.remove_css_class("drop-active"); + }; + + if let Some(section_row) = obj.child().and_downcast::() { + section_row.set_show_label_for_room_category(source_category); + } + } + + /// Update the active state of this drop target. + fn update_for_drop_active_target_category(&self) { + let obj = self.obj(); + + let Some(room_category) = self.room_category() else { + obj.remove_css_class("drop-active"); + return; + }; + + let target_category = self + .sidebar + .obj() + .and_then(|s| s.drop_active_target_category()); + + if target_category.is_some_and(|target_category| target_category == room_category) { + obj.add_css_class("drop-active"); + } else { + obj.remove_css_class("drop-active"); + } + } } } @@ -495,32 +589,23 @@ impl Row { glib::Object::builder().property("sidebar", sidebar).build() } - /// Get the `Room` of this item, if this is a room row. + /// Get the `Room` displayed by this row, if any. pub fn room(&self) -> Option { self.imp().room() } - /// Get the `RoomCategory` of this item. + /// Get the `RoomCategory` of this row, if any. /// - /// If this is not a `Category` containing rooms or a room, returns `None`. + /// If this does not display a room or a section containing rooms, returns + /// `None`. pub fn room_category(&self) -> Option { - let item = self.item()?; - - if let Some(room) = item.downcast_ref::() { - Some(room.category()) - } else { - item.downcast_ref::() - .and_then(|category| RoomCategory::try_from(category.category_type()).ok()) - } + self.imp().room_category() } - /// Get the [`SidebarIconItemType`] of this item. - /// - /// If this is not a [`SidebarIconItem`], returns `None`. + /// Get the [`SidebarIconItemType`] of the icon item displayed by this row, + /// if any. pub fn item_type(&self) -> Option { - self.item() - .and_downcast_ref::() - .map(|i| i.item_type()) + self.imp().item_type() } /// Handle the drag-n-drop hovering this row. @@ -659,63 +744,4 @@ impl Row { } } } - - /// Update the disabled or empty state of this drop target. - fn update_for_drop_source_type(&self) { - let source_type = self.sidebar().and_then(|s| s.drop_source_category()); - - if let Some(source_type) = source_type { - if self - .room_category() - .is_some_and(|row_type| source_type.can_change_to(row_type)) - { - self.remove_css_class("drop-disabled"); - - if self - .item() - .and_downcast::() - .is_some_and(|category| category.empty()) - { - self.add_css_class("drop-empty"); - } else { - self.remove_css_class("drop-empty"); - } - } else { - let is_forget_item = self - .item_type() - .is_some_and(|item_type| item_type == SidebarIconItemType::Forget); - if is_forget_item && source_type == RoomCategory::Left { - self.remove_css_class("drop-disabled"); - } else { - self.add_css_class("drop-disabled"); - self.remove_css_class("drop-empty"); - } - } - } else { - // Clear style - self.remove_css_class("drop-disabled"); - self.remove_css_class("drop-empty"); - self.remove_css_class("drop-active"); - }; - - if let Some(category_row) = self.child().and_downcast::() { - category_row.set_show_label_for_category( - source_type.map(CategoryType::from).unwrap_or_default(), - ); - } - } - - /// Update the active state of this drop target. - fn update_for_drop_active_target_type(&self) { - let Some(room_category) = self.room_category() else { - return; - }; - let target_category = self.sidebar().and_then(|s| s.drop_active_target_category()); - - if target_category.is_some_and(|target_category| target_category == room_category) { - self.add_css_class("drop-active"); - } else { - self.remove_css_class("drop-active"); - } - } } diff --git a/src/session/view/sidebar/section_row.rs b/src/session/view/sidebar/section_row.rs new file mode 100644 index 00000000..e0620b17 --- /dev/null +++ b/src/session/view/sidebar/section_row.rs @@ -0,0 +1,213 @@ +use adw::subclass::prelude::BinImpl; +use gettextrs::gettext; +use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; + +use crate::session::model::{RoomCategory, SidebarSection, SidebarSectionName}; + +mod imp { + use std::{ + cell::{Cell, RefCell}, + marker::PhantomData, + }; + + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/section_row.ui")] + #[properties(wrapper_type = super::SidebarSectionRow)] + pub struct SidebarSectionRow { + /// The section of this row. + #[property(get, set = Self::set_section, explicit_notify, nullable)] + section: RefCell>, + section_binding: RefCell>, + /// Whether this row is expanded. + #[property(get, set = Self::set_is_expanded, explicit_notify, construct, default = true)] + is_expanded: Cell, + /// The label to show for this row. + #[property(get = Self::label)] + label: PhantomData>, + /// The room category 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 dropping a room with the given category. + show_label_for_room_category: Cell>, + /// The label showing the category name. + #[template_child] + pub(super) display_name: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for SidebarSectionRow { + const NAME: &'static str = "SidebarSectionRow"; + type Type = super::SidebarSectionRow; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + klass.set_css_name("sidebar-section"); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for SidebarSectionRow { + fn constructed(&self) { + self.parent_constructed(); + + self.obj().connect_parent_notify(|obj| { + obj.set_expanded_accessibility_state(obj.is_expanded()); + }); + } + + fn dispose(&self) { + if let Some(binding) = self.section_binding.take() { + binding.unbind(); + } + } + } + + impl WidgetImpl for SidebarSectionRow {} + impl BinImpl for SidebarSectionRow {} + + impl SidebarSectionRow { + /// Set the section represented by this row. + fn set_section(&self, section: Option) { + if *self.section.borrow() == section { + return; + } + + if let Some(binding) = self.section_binding.take() { + binding.unbind(); + } + let obj = self.obj(); + + if let Some(section) = §ion { + let section_binding = section + .bind_property("is-expanded", &*obj, "is-expanded") + .sync_create() + .build(); + self.section_binding.replace(Some(section_binding)); + } + + self.section.replace(section); + + obj.notify_section(); + obj.notify_label(); + } + + /// The label to show for this row. + fn label(&self) -> Option { + let target_section_name = self.section.borrow().as_ref()?.name(); + let source_room_category = self.show_label_for_room_category.get(); + + let label = match source_room_category { + Some(RoomCategory::Invited) => match target_section_name { + // Translators: This is an action to join a room and put it in the "Favorites" + // section. + SidebarSectionName::Favorite => gettext("Join Room as Favorite"), + SidebarSectionName::Normal => gettext("Join Room"), + // Translators: This is an action to join a room and put it in the "Low + // Priority" section. + SidebarSectionName::LowPriority => gettext("Join Room as Low Priority"), + SidebarSectionName::Left => gettext("Reject Invite"), + _ => target_section_name.to_string(), + }, + Some(RoomCategory::Favorite) => match target_section_name { + SidebarSectionName::Normal => gettext("Move to Rooms"), + SidebarSectionName::LowPriority => gettext("Move to Low Priority"), + SidebarSectionName::Left => gettext("Leave Room"), + _ => target_section_name.to_string(), + }, + Some(RoomCategory::Normal) => match target_section_name { + SidebarSectionName::Favorite => gettext("Move to Favorites"), + SidebarSectionName::LowPriority => gettext("Move to Low Priority"), + SidebarSectionName::Left => gettext("Leave Room"), + _ => target_section_name.to_string(), + }, + Some(RoomCategory::LowPriority) => match target_section_name { + SidebarSectionName::Favorite => gettext("Move to Favorites"), + SidebarSectionName::Normal => gettext("Move to Rooms"), + SidebarSectionName::Left => gettext("Leave Room"), + _ => target_section_name.to_string(), + }, + Some(RoomCategory::Left) => match target_section_name { + // Translators: This is an action to rejoin a room and put it in the "Favorites" + // section. + SidebarSectionName::Favorite => gettext("Rejoin Room as Favorite"), + SidebarSectionName::Normal => gettext("Rejoin Room"), + // Translators: This is an action to rejoin a room and put it in the "Low + // Priority" section. + SidebarSectionName::LowPriority => gettext("Rejoin Room as Low Priority"), + _ => target_section_name.to_string(), + }, + _ => target_section_name.to_string(), + }; + + Some(label) + } + + /// Set whether this row is expanded. + fn set_is_expanded(&self, is_expanded: bool) { + if self.is_expanded.get() == is_expanded { + return; + } + let obj = self.obj(); + + if is_expanded { + obj.set_state_flags(gtk::StateFlags::CHECKED, false); + } else { + obj.unset_state_flags(gtk::StateFlags::CHECKED); + } + + self.is_expanded.set(is_expanded); + obj.set_expanded_accessibility_state(is_expanded); + obj.notify_is_expanded(); + } + + /// Set the room category to show the label for. + pub(super) fn set_show_label_for_room_category(&self, category: Option) { + if self.show_label_for_room_category.get() == category { + return; + } + + self.show_label_for_room_category.set(category); + + self.obj().notify_label(); + } + } +} + +glib::wrapper! { + /// A sidebar row representing a category. + pub struct SidebarSectionRow(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl SidebarSectionRow { + pub fn new() -> Self { + glib::Object::new() + } + + /// Set the room category to show the label for. + pub fn set_show_label_for_room_category(&self, category: Option) { + self.imp().set_show_label_for_room_category(category); + } + + /// Set the expanded state of this row for a11y. + fn set_expanded_accessibility_state(&self, is_expanded: bool) { + if let Some(row) = self.parent() { + row.update_state(&[gtk::accessible::State::Expanded(Some(is_expanded))]); + } + } + + /// 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/category_row.ui b/src/session/view/sidebar/section_row.ui similarity index 84% rename from src/session/view/sidebar/category_row.ui rename to src/session/view/sidebar/section_row.ui index 033d497a..fb769165 100644 --- a/src/session/view/sidebar/category_row.ui +++ b/src/session/view/sidebar/section_row.ui @@ -1,6 +1,6 @@ -