diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs index d918ec0a..9688e597 100644 --- a/src/session/room/mod.rs +++ b/src/session/room/mod.rs @@ -62,7 +62,10 @@ use crate::{ gettext_f, ngettext_f, prelude::*, session::{ - avatar::update_room_avatar_from_file, room::member_list::MemberList, Avatar, Session, User, + avatar::update_room_avatar_from_file, + room::member_list::MemberList, + sidebar::{SidebarItem, SidebarItemImpl}, + Avatar, Session, User, }, spawn, spawn_tokio, utils::pending_event_ids, @@ -107,6 +110,7 @@ mod imp { impl ObjectSubclass for Room { const NAME: &'static str = "Room"; type Type = super::Room; + type ParentType = SidebarItem; } impl ObjectImpl for Room { @@ -335,13 +339,15 @@ mod imp { } } } + + impl SidebarItemImpl for Room {} } glib::wrapper! { /// GObject representation of a Matrix room. /// /// Handles populating the Timeline. - pub struct Room(ObjectSubclass); + pub struct Room(ObjectSubclass) @extends SidebarItem; } impl Room { diff --git a/src/session/sidebar/category.rs b/src/session/sidebar/category.rs index 69dd90f2..6eecccdc 100644 --- a/src/session/sidebar/category.rs +++ b/src/session/sidebar/category.rs @@ -1,9 +1,13 @@ use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; -use crate::session::{room::Room, room_list::RoomList, sidebar::CategoryType}; +use super::{CategoryType, SidebarItem, SidebarItemExt, SidebarItemImpl}; +use crate::session::{ + room::{Room, RoomType}, + room_list::RoomList, +}; mod imp { - use std::cell::Cell; + use std::{cell::Cell, convert::TryFrom}; use once_cell::unsync::OnceCell; @@ -20,6 +24,7 @@ mod imp { impl ObjectSubclass for Category { const NAME: &'static str = "Category"; type Type = super::Category; + type ParentType = SidebarItem; type Interfaces = (gio::ListModel,); } @@ -96,15 +101,33 @@ mod imp { impl ListModelImpl for Category { fn item_type(&self, _list_model: &Self::Type) -> glib::Type { - glib::Object::static_type() + SidebarItem::static_type() } + fn n_items(&self, _list_model: &Self::Type) -> u32 { self.model.get().unwrap().n_items() } + fn item(&self, _list_model: &Self::Type, position: u32) -> Option { self.model.get().unwrap().item(position) } } + + impl SidebarItemImpl for Category { + fn update_visibility(&self, obj: &Self::Type, for_category: CategoryType) { + obj.set_visible( + !obj.is_empty() + || RoomType::try_from(for_category) + .ok() + .and_then(|room_type| { + RoomType::try_from(obj.type_()) + .ok() + .filter(|category| room_type.can_change_to(category)) + }) + .is_some(), + ) + } + } } glib::wrapper! { @@ -112,6 +135,7 @@ glib::wrapper! { /// /// This struct is used in ItemList for the sidebar. pub struct Category(ObjectSubclass) + @extends SidebarItem, @implements gio::ListModel; } diff --git a/src/session/sidebar/entry.rs b/src/session/sidebar/entry.rs index 79c3ffbb..c8f58dad 100644 --- a/src/session/sidebar/entry.rs +++ b/src/session/sidebar/entry.rs @@ -1,6 +1,6 @@ use gtk::{glib, prelude::*, subclass::prelude::*}; -use crate::session::sidebar::EntryType; +use super::{CategoryType, EntryType, SidebarItem, SidebarItemExt, SidebarItemImpl}; mod imp { use std::cell::{Cell, RefCell}; @@ -17,6 +17,7 @@ mod imp { impl ObjectSubclass for Entry { const NAME: &'static str = "Entry"; type Type = super::Entry; + type ParentType = SidebarItem; } impl ObjectImpl for Entry { @@ -79,6 +80,15 @@ mod imp { } } } + + impl SidebarItemImpl for Entry { + fn update_visibility(&self, obj: &Self::Type, for_category: CategoryType) { + match obj.type_() { + EntryType::Explore => obj.set_visible(true), + EntryType::Forget => obj.set_visible(for_category == CategoryType::Left), + } + } + } } glib::wrapper! { @@ -86,7 +96,7 @@ glib::wrapper! { /// /// Entry is supposed to be used in a TreeListModel, but as it does not have /// any children, implementing the ListModel interface is not required. - pub struct Entry(ObjectSubclass); + pub struct Entry(ObjectSubclass) @extends SidebarItem; } impl Entry { diff --git a/src/session/sidebar/item_list.rs b/src/session/sidebar/item_list.rs index bee93a34..5294d071 100644 --- a/src/session/sidebar/item_list.rs +++ b/src/session/sidebar/item_list.rs @@ -1,13 +1,7 @@ -use std::convert::TryFrom; - use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*}; -use crate::session::{ - room::RoomType, - room_list::RoomList, - sidebar::{Category, CategoryType, Entry, EntryType}, - verification::VerificationList, -}; +use super::{Category, CategoryType, Entry, EntryType, SidebarItem, SidebarItemExt}; +use crate::session::{room_list::RoomList, verification::VerificationList}; mod imp { use std::cell::Cell; @@ -18,7 +12,7 @@ mod imp { #[derive(Debug, Default)] pub struct ItemList { - pub list: OnceCell<[(glib::Object, Cell); 8]>, + pub list: OnceCell<[SidebarItem; 8]>, pub room_list: OnceCell, pub verification_list: OnceCell, /// The `CategoryType` to show all compatible categories for. @@ -96,72 +90,48 @@ mod imp { let room_list = obj.room_list(); let verification_list = obj.verification_list(); - let list = [ - Entry::new(EntryType::Explore).upcast::(), - Category::new(CategoryType::VerificationRequest, verification_list) - .upcast::(), - Category::new(CategoryType::Invited, room_list).upcast::(), - Category::new(CategoryType::Favorite, room_list).upcast::(), - Category::new(CategoryType::Normal, room_list).upcast::(), - Category::new(CategoryType::LowPriority, room_list).upcast::(), - Category::new(CategoryType::Left, room_list).upcast::(), - Entry::new(EntryType::Forget).upcast::(), + let list: [SidebarItem; 8] = [ + Entry::new(EntryType::Explore).upcast(), + Category::new(CategoryType::VerificationRequest, verification_list).upcast(), + Category::new(CategoryType::Invited, room_list).upcast(), + Category::new(CategoryType::Favorite, room_list).upcast(), + Category::new(CategoryType::Normal, room_list).upcast(), + Category::new(CategoryType::LowPriority, room_list).upcast(), + Category::new(CategoryType::Left, room_list).upcast(), + Entry::new(EntryType::Forget).upcast(), ]; - for (index, item) in list.iter().enumerate() { + for item in list.iter() { if let Some(category) = item.downcast_ref::() { category.connect_notify_local( Some("empty"), - clone!(@weak obj => move |_, _| { - obj.update_item(index); + clone!(@weak obj => move |category, _| { + category.update_visibility(obj.show_all_for_category()); }), ); } + item.update_visibility(obj.show_all_for_category()); } - let list = list.map(|item| { - let visible = if let Some(category) = item.downcast_ref::() { - !category.is_empty() - } else { - item.downcast_ref::() - .filter(|entry| entry.type_() == EntryType::Forget) - .is_none() - }; - (item, Cell::new(visible)) - }); - self.list.set(list).unwrap(); } } impl ListModelImpl for ItemList { fn item_type(&self, _list_model: &Self::Type) -> glib::Type { - glib::Object::static_type() + SidebarItem::static_type() } + fn n_items(&self, _list_model: &Self::Type) -> u32 { - self.list - .get() - .unwrap() - .iter() - .filter(|(_, visible)| visible.get()) - .count() as u32 + self.list.get().unwrap().len() as u32 } + fn item(&self, _list_model: &Self::Type, position: u32) -> Option { self.list .get() .unwrap() - .iter() - .filter_map( - |(item, visible)| { - if visible.get() { - Some(item) - } else { - None - } - }, - ) - .nth(position as usize) - .cloned() + .get(position as usize) + .map(|item| item.to_owned().upcast()) } } } @@ -184,48 +154,6 @@ impl ItemList { .expect("Failed to create ItemList") } - fn update_item(&self, position: usize) { - let priv_ = self.imp(); - let (item, old_visible) = priv_.list.get().unwrap().get(position).unwrap(); - - let visible = if let Some(category) = item.downcast_ref::() { - !category.is_empty() - || RoomType::try_from(self.show_all_for_category()) - .ok() - .and_then(|room_type| { - RoomType::try_from(category.type_()) - .ok() - .filter(|category| room_type.can_change_to(category)) - }) - .is_some() - } else if item - .downcast_ref::() - .filter(|entry| entry.type_() == EntryType::Forget) - .is_some() - { - self.show_all_for_category() == CategoryType::Left - } else { - return; - }; - - if visible != old_visible.get() { - old_visible.set(visible); - let hidden_before_position = priv_ - .list - .get() - .unwrap() - .iter() - .take(position) - .filter(|(_, visible)| !visible.get()) - .count(); - let real_position = position - hidden_before_position; - - let (removed, added) = if visible { (0, 1) } else { (1, 0) }; - - self.items_changed(real_position as u32, removed, added); - } - } - pub fn show_all_for_category(&self) -> CategoryType { self.imp().show_all_for_category.get() } @@ -238,8 +166,8 @@ impl ItemList { } priv_.show_all_for_category.set(category); - for i in 0..priv_.list.get().unwrap().len() { - self.update_item(i); + for item in priv_.list.get().unwrap().iter() { + item.update_visibility(category); } self.notify("show-all-for-category"); diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs index c43aa817..5ffb3583 100644 --- a/src/session/sidebar/mod.rs +++ b/src/session/sidebar/mod.rs @@ -9,6 +9,7 @@ mod item_list; mod room_row; mod row; mod selection; +mod sidebar_item; mod verification_row; use account_switcher::AccountSwitcher; @@ -16,8 +17,12 @@ use adw::{prelude::*, subclass::prelude::*}; use gtk::{gio, glib, glib::closure, subclass::prelude::*, CompositeTemplate, SelectionModel}; pub use self::{ - category::Category, category_type::CategoryType, entry::Entry, entry_type::EntryType, + category::Category, + category_type::CategoryType, + entry::Entry, + entry_type::EntryType, item_list::ItemList, + sidebar_item::{SidebarItem, SidebarItemExt, SidebarItemImpl}, }; use self::{ category_row::CategoryRow, entry_row::EntryRow, room_row::RoomRow, row::Row, diff --git a/src/session/sidebar/row.rs b/src/session/sidebar/row.rs index 0a17f6e4..eda20b99 100644 --- a/src/session/sidebar/row.rs +++ b/src/session/sidebar/row.rs @@ -6,7 +6,7 @@ use gtk::{gdk, glib, glib::clone, subclass::prelude::*}; use super::EntryType; use crate::session::{ room::{Room, RoomType}, - sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow, VerificationRow}, + sidebar::{Category, CategoryRow, Entry, EntryRow, RoomRow, SidebarItem, VerificationRow}, verification::IdentityVerification, }; @@ -20,7 +20,7 @@ mod imp { #[derive(Debug, Default)] pub struct Row { pub list_row: RefCell>, - pub binding: RefCell>, + pub bindings: RefCell>, } #[glib::object_subclass] @@ -119,8 +119,10 @@ impl Row { glib::Object::new(&[]).expect("Failed to create Row") } - pub fn item(&self) -> Option { - self.list_row().and_then(|r| r.item()) + pub fn item(&self) -> Option { + self.list_row() + .and_then(|r| r.item()) + .and_then(|obj| obj.downcast().ok()) } pub fn list_row(&self) -> Option { @@ -134,7 +136,7 @@ impl Row { return; } - if let Some(binding) = priv_.binding.take() { + for binding in priv_.bindings.take() { binding.unbind(); } @@ -145,7 +147,16 @@ impl Row { return; }; + let mut bindings = vec![]; if let Some(item) = self.item() { + if let Some(list_item) = self.parent() { + bindings.push( + item.bind_property("visible", &list_item, "visible") + .flags(glib::BindingFlags::SYNC_CREATE) + .build(), + ); + } + if let Some(category) = item.downcast_ref::() { let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::()) { @@ -157,12 +168,11 @@ impl Row { }; child.set_category(Some(category.clone())); - let binding = row - .bind_property("expanded", &child, "expanded") - .flags(glib::BindingFlags::SYNC_CREATE) - .build(); - - priv_.binding.replace(Some(binding)); + bindings.push( + row.bind_property("expanded", &child, "expanded") + .flags(glib::BindingFlags::SYNC_CREATE) + .build(), + ); if let Some(list_item) = self.parent() { list_item.set_css_classes(&["category"]); @@ -223,6 +233,8 @@ impl Row { .unwrap(); } + priv_.bindings.replace(bindings); + self.notify("item"); self.notify("list-row"); } diff --git a/src/session/sidebar/sidebar_item.rs b/src/session/sidebar/sidebar_item.rs new file mode 100644 index 00000000..339ed663 --- /dev/null +++ b/src/session/sidebar/sidebar_item.rs @@ -0,0 +1,163 @@ +use gtk::{glib, prelude::*, subclass::prelude::*}; + +use super::CategoryType; + +mod imp { + use std::cell::Cell; + + use once_cell::sync::Lazy; + + use super::*; + + #[repr(C)] + pub struct SidebarItemClass { + pub parent_class: glib::object::ObjectClass, + pub update_visibility: fn(&super::SidebarItem, for_category: CategoryType), + } + + unsafe impl ClassStruct for SidebarItemClass { + type Type = SidebarItem; + } + + pub(super) fn sidebar_item_update_visibility( + this: &super::SidebarItem, + for_category: CategoryType, + ) { + let klass = this.class(); + (klass.as_ref().update_visibility)(this, for_category) + } + + #[derive(Debug)] + pub struct SidebarItem { + /// Whether this item is visible. + pub visible: Cell, + } + + impl Default for SidebarItem { + fn default() -> Self { + Self { + visible: Cell::new(true), + } + } + } + + #[glib::object_subclass] + unsafe impl ObjectSubclass for SidebarItem { + const NAME: &'static str = "SidebarItem"; + const ABSTRACT: bool = true; + type Type = super::SidebarItem; + type Class = SidebarItemClass; + } + + impl ObjectImpl for SidebarItem { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpecBoolean::new( + "visible", + "Visible", + "Whether this item is visible.", + true, + glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, + )] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "visible" => obj.set_visible(value.get().unwrap()), + _ => unimplemented!(), + } + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "visible" => obj.visible().to_value(), + _ => unimplemented!(), + } + } + } +} + +glib::wrapper! { + /// Parent class of items inside the `Sidebar`. + pub struct SidebarItem(ObjectSubclass); +} + +/// Public trait containing implemented methods for everything that derives from +/// `SidebarItem`. +/// +/// To override the behavior of these methods, override the corresponding method +/// of `SidebarItemImpl`. +pub trait SidebarItemExt: 'static { + /// Whether this `SidebarItem` is visible. + /// + /// Defaults to `true`. + fn visible(&self) -> bool; + + /// Set the visibility of the `SidebarItem`. + fn set_visible(&self, visible: bool); + + /// Update the visibility of the `SidebarItem` for the given `CategoryType`. + fn update_visibility(&self, for_category: CategoryType); +} + +impl> SidebarItemExt for O { + fn visible(&self) -> bool { + self.upcast_ref().imp().visible.get() + } + + fn set_visible(&self, visible: bool) { + if self.visible() == visible { + return; + } + + self.upcast_ref().imp().visible.set(visible); + self.notify("visible"); + } + + fn update_visibility(&self, for_category: CategoryType) { + imp::sidebar_item_update_visibility(self.upcast_ref(), for_category) + } +} + +/// Public trait that must be implemented for everything that derives from +/// `SidebarItem`. +/// +/// Overriding a method from this Trait overrides also its behavior in +/// `SidebarItemExt`. +pub trait SidebarItemImpl: ObjectImpl { + fn update_visibility(&self, _obj: &Self::Type, _for_category: CategoryType) {} +} + +// Make `SidebarItem` subclassable. +unsafe impl IsSubclassable for SidebarItem +where + T: SidebarItemImpl, + T::Type: IsA, +{ + fn class_init(class: &mut glib::Class) { + Self::parent_class_init::(class.upcast_ref_mut()); + + let klass = class.as_mut(); + + klass.update_visibility = update_visibility_trampoline::; + } +} + +// Virtual method implementation trampolines. +fn update_visibility_trampoline(this: &SidebarItem, for_category: CategoryType) +where + T: ObjectSubclass + SidebarItemImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().update_visibility(this, for_category) +} diff --git a/src/session/verification/identity_verification.rs b/src/session/verification/identity_verification.rs index e7082e90..b0281efc 100644 --- a/src/session/verification/identity_verification.rs +++ b/src/session/verification/identity_verification.rs @@ -24,7 +24,11 @@ use super::{VERIFICATION_CREATION_TIMEOUT, VERIFICATION_RECEIVE_TIMEOUT}; use crate::{ components::Toast, contrib::Camera, - session::{user::UserExt, Session, User}, + session::{ + sidebar::{SidebarItem, SidebarItemImpl}, + user::UserExt, + Session, User, + }, spawn, spawn_tokio, }; @@ -188,6 +192,7 @@ mod imp { impl ObjectSubclass for IdentityVerification { const NAME: &'static str = "IdentityVerification"; type Type = super::IdentityVerification; + type ParentType = SidebarItem; } impl ObjectImpl for IdentityVerification { @@ -354,10 +359,13 @@ mod imp { obj.cancel(true); } } + + impl SidebarItemImpl for IdentityVerification {} } glib::wrapper! { - pub struct IdentityVerification(ObjectSubclass); + pub struct IdentityVerification(ObjectSubclass) + @extends SidebarItem; } impl IdentityVerification {