Browse Source

sidebar: Rename Category to SidebarSection

Rename CategoryType to SidebarSectionName and remove unused variants.
Rename CategoryFilter to RoomCategoryFilter.
Use Option<RoomCategory> instead of CategoryType for drag-n-drop
operations, preventing us from using GObject properties.
fractal-9
Kévin Commaille 1 year ago
parent
commit
1a144e4b58
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 8
      data/resources/style.css
  2. 4
      po/POTFILES.in
  3. 4
      src/session/model/mod.rs
  4. 35
      src/session/model/room/category.rs
  5. 92
      src/session/model/session_settings.rs
  6. 63
      src/session/model/sidebar_data/category/category_type.rs
  7. 12
      src/session/model/sidebar_data/icon_item.rs
  8. 39
      src/session/model/sidebar_data/item.rs
  9. 100
      src/session/model/sidebar_data/item_list.rs
  10. 4
      src/session/model/sidebar_data/mod.rs
  11. 155
      src/session/model/sidebar_data/section/mod.rs
  12. 73
      src/session/model/sidebar_data/section/name.rs
  13. 81
      src/session/model/sidebar_data/section/room_category_filter.rs
  14. 28
      src/session/view/session_view.rs
  15. 212
      src/session/view/sidebar/category_row.rs
  16. 109
      src/session/view/sidebar/mod.rs
  17. 228
      src/session/view/sidebar/row.rs
  18. 213
      src/session/view/sidebar/section_row.rs
  19. 4
      src/session/view/sidebar/section_row.ui
  20. 2
      src/ui-resources.gresource.xml

8
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);
}

4
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

4
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},

35
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<CategoryType> 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, Self::Error> {
Self::try_from(&category_type)
}
}
impl TryFrom<&CategoryType> for RoomCategory {
type Error = &'static str;
fn try_from(category_type: &CategoryType) -> Result<Self, Self::Error> {
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)
}
}

92
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<SidebarSectionName>);
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(&section_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(&section_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,
]))
}
}

63
src/session/model/sidebar_data/category/category_type.rs

@ -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<RoomCategory> 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,
}
}
}

12
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<RoomCategory>) -> bool {
match self.item_type() {
SidebarIconItemType::Explore => true,
SidebarIconItemType::Forget => for_category == CategoryType::Left,
SidebarIconItemType::Forget => source_category == Some(RoomCategory::Left),
}
}
}

39
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<bool>,
/// 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<bool>,
@ -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::<Category>() {
// 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::<SidebarSection>() {
// 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::<glib::Object>();
wrapper_model.append(&category_model);
wrapper_model.append(&section_model);
wrapper_model.append(&children_model);
gtk::FlattenListModel::new(Some(wrapper_model)).upcast::<gio::ListModel>()
@ -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<RoomCategory>) {
let inner_item = self.inner_item();
let visible = if let Some(category) = inner_item.downcast_ref::<Category>() {
category.visible_for_category(category_type)
let visible = if let Some(section) = inner_item.downcast_ref::<SidebarSection>() {
section.visible_for_room_category(source_category)
} else if let Some(icon_item) = inner_item.downcast_ref::<SidebarIconItem>() {
icon_item.visible_for_category(category_type)
icon_item.visible_for_room_category(source_category)
} else {
true
};

100
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<RoomList>,
room_list: OnceCell<RoomList>,
/// The list of verification requests.
#[property(get, construct_only)]
pub verification_list: OnceCell<VerificationList>,
/// The `CategoryType` to show all compatible categories for.
verification_list: OnceCell<VerificationList>,
/// 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<CategoryType>,
/// The UI is updated to show possible drop actions for a room with the
/// given category.
show_all_for_room_category: Cell<Option<RoomCategory>>,
}
#[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>() {
category.connect_empty_notify(clone!(
if let Some(section) = item.inner_item().downcast_ref::<SidebarSection>() {
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<RoomCategory>) {
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<RoomCategory>) {
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<Category> {
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<SidebarSection> {
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()
}
}

4
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,
};

155
src/session/model/sidebar_data/category/mod.rs → 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<gio::ListModel>,
/// The inner model of this category.
/// The inner model of this section.
inner_model: OnceCell<gio::ListModel>,
/// 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<CategoryType>,
/// Whether this category is empty.
#[property(get)]
pub empty: Cell<bool>,
/// 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<SidebarSectionName>,
/// The display name of this section.
#[property(get = Self::display_name)]
pub display_name: PhantomData<String>,
/// Whether this category is expanded.
/// Whether this section is empty.
#[property(get)]
pub is_empty: Cell<bool>,
/// Whether this section is expanded.
#[property(get, set = Self::set_is_expanded, explicit_notify)]
pub is_expanded: Cell<bool>,
}
#[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::<RoomList>() {
let room_category_type = Room::this_expression("category")
.chain_closure::<CategoryType>(closure!(
|_: Option<glib::Object>, 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<imp::Category>)
/// A list of items in the same section of the sidebar.
pub struct SidebarSection(ObjectSubclass<imp::SidebarSection>)
@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<gio::ListModel>) -> Self {
impl SidebarSection {
/// Constructs a new `SidebarSection` with the given name and source model.
pub fn new(name: SidebarSectionName, model: &impl IsA<gio::ListModel>) -> 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<RoomCategory>) -> 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)
})
}
}

73
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<Self> {
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<RoomCategory> {
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)
}
}

81
src/session/model/sidebar_data/category/category_filter.rs → 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<Option<gtk::Expression>>,
/// The category type to filter.
#[property(get, set = Self::set_category_type, explicit_notify, builder(CategoryType::default()))]
pub category_type: Cell<CategoryType>,
expression: RefCell<Option<gtk::Expression>>,
/// The room category to filter.
#[property(get, set = Self::set_room_category, explicit_notify, builder(RoomCategory::default()))]
room_category: Cell<RoomCategory>,
}
#[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::<CategoryType>().unwrap())
else {
return false;
};
value == category_type
.map(|v| {
v.get::<RoomCategory>()
.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<gtk::Expression>) {
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<imp::CategoryFilter>)
/// A `GtkFilter` to filter by [`RoomCategory`].
pub struct RoomCategoryFilter(ObjectSubclass<imp::RoomCategoryFilter>)
@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()
}

28
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();
}

212
src/session/view/sidebar/category_row.rs

@ -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<Option<Category>>,
category_binding: RefCell<Option<glib::Binding>>,
/// The expanded state of this row.
#[property(get, set = Self::set_expanded, explicit_notify, construct, default = true)]
pub expanded: Cell<bool>,
/// The label to show for this row.
#[property(get = Self::label)]
pub label: PhantomData<Option<String>>,
/// 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<CategoryType>,
/// The label showing the category name.
#[template_child]
pub display_name: TemplateChild<gtk::Label>,
}
#[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<Self>) {
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<Category>) {
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<String> {
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<imp::CategoryRow>)
@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) -> &gtk::Accessible {
self.imp().display_name.upcast_ref()
}
}

109
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<Option<User>>,
/// The category of the source that activated drop mode.
pub drop_source_category: Cell<Option<RoomCategory>>,
/// 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<CategoryType>,
/// The category of the drop target that is currently hovered.
pub drop_active_target_category: Cell<Option<RoomCategory>>,
/// 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<CategoryType>,
/// The list model of this sidebar.
#[property(get, set = Self::set_list_model, explicit_notify, nullable)]
pub list_model: glib::WeakRef<SidebarListModel>,
pub binding: RefCell<Option<glib::Binding>>,
pub expr_watch: RefCell<Option<gtk::ExpressionWatch>>,
session_handlers: RefCell<Vec<glib::SignalHandlerId>>,
}
@ -101,6 +92,16 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for Sidebar {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = 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>() {
category.set_is_expanded(!category.is_expanded());
if let Some(section) = item.downcast_ref::<SidebarSection>() {
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<F: Fn(&Self) + 'static>(
&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<F: Fn(&Self) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"drop-active-target-category-changed",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}

228
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>,
sidebar: BoundObjectWeakRef<Sidebar>,
/// The item of this row.
#[property(get, set = Self::set_item, explicit_notify, nullable)]
pub item: RefCell<Option<glib::Object>>,
item: RefCell<Option<glib::Object>>,
room_handler: RefCell<Option<glib::SignalHandlerId>>,
room_join_rule_handler: RefCell<Option<glib::SignalHandlerId>>,
room_is_read_handler: RefCell<Option<glib::SignalHandlerId>>,
@ -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::<Category>() {
let child = if let Some(child) = obj.child().and_downcast::<CategoryRow>() {
if let Some(section) = item.downcast_ref::<SidebarSection>() {
let child = if let Some(child) = obj.child().and_downcast::<SidebarSectionRow>()
{
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::<Room>() {
let child = if let Some(child) = obj.child().and_downcast::<RoomRow>() {
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<RoomCategory> {
let borrowed_item = self.item.borrow();
let item = borrowed_item.as_ref()?;
if let Some(room) = item.downcast_ref::<Room>() {
Some(room.category())
} else {
item.downcast_ref::<SidebarSection>()
.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<SidebarIconItemType> {
let borrowed_item = self.item.borrow();
let item = borrowed_item.as_ref()?;
item.downcast_ref::<SidebarIconItem>()
.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::<SidebarSection>())
.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::<SidebarSectionRow>() {
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<Room> {
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<RoomCategory> {
let item = self.item()?;
if let Some(room) = item.downcast_ref::<Room>() {
Some(room.category())
} else {
item.downcast_ref::<Category>()
.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<SidebarIconItemType> {
self.item()
.and_downcast_ref::<SidebarIconItem>()
.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::<Category>()
.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::<CategoryRow>() {
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");
}
}
}

213
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<Option<SidebarSection>>,
section_binding: RefCell<Option<glib::Binding>>,
/// Whether this row is expanded.
#[property(get, set = Self::set_is_expanded, explicit_notify, construct, default = true)]
is_expanded: Cell<bool>,
/// The label to show for this row.
#[property(get = Self::label)]
label: PhantomData<Option<String>>,
/// 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<Option<RoomCategory>>,
/// The label showing the category name.
#[template_child]
pub(super) display_name: TemplateChild<gtk::Label>,
}
#[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<Self>) {
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<SidebarSection>) {
if *self.section.borrow() == section {
return;
}
if let Some(binding) = self.section_binding.take() {
binding.unbind();
}
let obj = self.obj();
if let Some(section) = &section {
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<String> {
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<RoomCategory>) {
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<imp::SidebarSectionRow>)
@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<RoomCategory>) {
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) -> &gtk::Accessible {
self.imp().display_name.upcast_ref()
}
}

4
src/session/view/sidebar/category_row.ui → src/session/view/sidebar/section_row.ui

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="SidebarCategoryRow" parent="AdwBin">
<template class="SidebarSectionRow" parent="AdwBin">
<child>
<object class="GtkBox">
<property name="spacing">12</property>
@ -9,7 +9,7 @@
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="ellipsize">end</property>
<property name="label" bind-source="SidebarCategoryRow" bind-property="label" bind-flags="sync-create"/>
<property name="label" bind-source="SidebarSectionRow" bind-property="label" bind-flags="sync-create"/>
<style>
<class name="dim-label"/>
</style>

2
src/ui-resources.gresource.xml

@ -134,10 +134,10 @@
<file compressed="true" preprocess="xml-stripblanks">session/view/media_viewer.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/room_creation.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/session_view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/category_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/icon_item_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/room_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/section_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">session/view/sidebar/verification_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>

Loading…
Cancel
Save