diff --git a/po/POTFILES.in b/po/POTFILES.in index 061cce42..c5ba7528 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -4,6 +4,7 @@ data/org.gnome.Fractal.desktop.in.in data/org.gnome.Fractal.gschema.xml.in data/org.gnome.Fractal.metainfo.xml.in.in +src/account_switcher/account_switcher_button.ui src/account_switcher/account_switcher_popover.ui src/account_switcher/session_item.ui src/application.rs diff --git a/src/account_switcher/account_switcher_button.rs b/src/account_switcher/account_switcher_button.rs new file mode 100644 index 00000000..444bb687 --- /dev/null +++ b/src/account_switcher/account_switcher_button.rs @@ -0,0 +1,121 @@ +use gtk::{ + glib::{self, clone}, + prelude::*, + subclass::prelude::*, + CompositeTemplate, +}; + +use super::AccountSwitcherPopover; +use crate::{components::Avatar, session_list::SessionInfo, utils::BoundObjectWeakRef, Window}; + +mod imp { + use glib::subclass::InitializingObject; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/Fractal/ui/account_switcher/account_switcher_button.ui")] + pub struct AccountSwitcherButton { + pub popover: BoundObjectWeakRef, + } + + #[glib::object_subclass] + impl ObjectSubclass for AccountSwitcherButton { + const NAME: &'static str = "AccountSwitcherButton"; + type Type = super::AccountSwitcherButton; + type ParentType = gtk::ToggleButton; + + fn class_init(klass: &mut Self::Class) { + Avatar::static_type(); + SessionInfo::static_type(); + + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AccountSwitcherButton { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + + obj.connect_toggled(|obj| { + obj.handle_toggled(); + }); + } + } + + impl WidgetImpl for AccountSwitcherButton {} + impl ButtonImpl for AccountSwitcherButton {} + impl ToggleButtonImpl for AccountSwitcherButton {} +} + +glib::wrapper! { + /// A button showing the currently selected account and opening the account switcher popover. + pub struct AccountSwitcherButton(ObjectSubclass) + @extends gtk::Widget, gtk::Button, gtk::ToggleButton, @implements gtk::Accessible; +} + +#[gtk::template_callbacks] +impl AccountSwitcherButton { + pub fn new() -> Self { + glib::Object::new() + } + + pub fn popover(&self) -> Option { + self.imp().popover.obj() + } + + pub fn set_popover(&self, popover: Option<&AccountSwitcherPopover>) { + let old_popover = self.popover(); + + if old_popover.as_ref() == popover { + return; + } + + let imp = self.imp(); + + // Reset the state. + if let Some(popover) = old_popover { + popover.unparent(); + } + imp.popover.disconnect_signals(); + self.set_active(false); + + if let Some(popover) = popover { + // We need to remove the popover from the previous button, if any. + if let Some(parent) = popover.parent().and_downcast::() { + parent.set_popover(None); + } + + popover.set_parent(self); + imp.popover.set(popover, vec![]); + } + } + + fn handle_toggled(&self) { + if self.is_active() { + let Some(window) = self.root().and_downcast::() else { + return; + }; + let popover = window.account_switcher(); + + self.set_popover(Some(popover)); + popover.connect_closed(clone!(@weak self as obj => move |_| { + obj.set_active(false); + })); + popover.popup(); + } else if let Some(popover) = self.popover() { + popover.popdown(); + } + } +} + +impl Default for AccountSwitcherButton { + fn default() -> Self { + Self::new() + } +} diff --git a/src/account_switcher/account_switcher_button.ui b/src/account_switcher/account_switcher_button.ui new file mode 100644 index 00000000..c56712f5 --- /dev/null +++ b/src/account_switcher/account_switcher_button.ui @@ -0,0 +1,27 @@ + + + + diff --git a/src/account_switcher/mod.rs b/src/account_switcher/mod.rs index b8cdde27..fd9aac96 100644 --- a/src/account_switcher/mod.rs +++ b/src/account_switcher/mod.rs @@ -1,5 +1,9 @@ +mod account_switcher_button; mod account_switcher_popover; mod avatar_with_selection; mod session_item; -pub use account_switcher_popover::AccountSwitcherPopover; +pub use self::{ + account_switcher_button::AccountSwitcherButton, + account_switcher_popover::AccountSwitcherPopover, +}; diff --git a/src/session/model/session.rs b/src/session/model/session.rs index 1c9733fe..23707fb3 100644 --- a/src/session/model/session.rs +++ b/src/session/model/session.rs @@ -29,7 +29,8 @@ use tracing::{debug, error}; use url::Url; use super::{ - ItemList, Notifications, RoomList, SessionSettings, SidebarListModel, User, VerificationList, + AvatarData, ItemList, Notifications, RoomList, SessionSettings, SidebarListModel, User, + VerificationList, }; use crate::{ prelude::*, @@ -148,7 +149,11 @@ mod imp { } } - impl SessionInfoImpl for Session {} + impl SessionInfoImpl for Session { + fn avatar_data(&self) -> AvatarData { + self.obj().user().unwrap().avatar_data().clone() + } + } } glib::wrapper! { diff --git a/src/session/view/sidebar/mod.rs b/src/session/view/sidebar/mod.rs index 2df69b64..31b72644 100644 --- a/src/session/view/sidebar/mod.rs +++ b/src/session/view/sidebar/mod.rs @@ -13,13 +13,12 @@ use self::{ verification_row::VerificationRow, }; use crate::{ - components::Avatar, + account_switcher::AccountSwitcherButton, prelude::*, session::model::{ Category, CategoryType, IconItem, IdentityVerification, Room, RoomType, Selection, SidebarListModel, User, }, - Window, }; mod imp { @@ -42,8 +41,6 @@ mod imp { #[template_child] pub room_search: TemplateChild, #[template_child] - pub account_switcher_button: TemplateChild, - #[template_child] pub room_row_menu: TemplateChild, #[template_child] pub offline_banner: TemplateChild, @@ -66,9 +63,8 @@ mod imp { type ParentType = adw::NavigationPage; fn class_init(klass: &mut Self::Class) { - RoomRow::static_type(); - Row::static_type(); - Avatar::static_type(); + AccountSwitcherButton::static_type(); + Self::bind_template(klass); klass.set_css_name("sidebar"); } @@ -168,21 +164,6 @@ mod imp { } }); - self.account_switcher_button.set_create_popup_func(clone!(@weak obj => move |btn| { - if let Some(window) = obj.parent_window() { - let account_switcher = window.account_switcher(); - // We need to remove the popover from the previous MenuButton, if any - if let Some(prev_parent) = account_switcher.parent().and_downcast::() { - if &prev_parent == btn { - return; - } else { - prev_parent.set_popover(gtk::Widget::NONE); - } - } - btn.set_popover(Some(account_switcher)); - } - })); - // FIXME: Remove this hack once https://gitlab.gnome.org/GNOME/gtk/-/issues/4938 is resolved self.scrolled_window .vscrollbar() @@ -358,9 +339,4 @@ impl Sidebar { .build() }) } - - /// Returns the parent `Window` containing the `Sidebar` - fn parent_window(&self) -> Option { - self.root().and_downcast() - } } diff --git a/src/session/view/sidebar/mod.ui b/src/session/view/sidebar/mod.ui index 55951293..7ad6dff0 100644 --- a/src/session/view/sidebar/mod.ui +++ b/src/session/view/sidebar/mod.ui @@ -82,26 +82,7 @@ False - - Switch Accounts - - Switch Accounts - - - - - 24 - - - Sidebar - - - presentation - - - + diff --git a/src/session_list/failed_session.rs b/src/session_list/failed_session.rs index b426712e..2debdd48 100644 --- a/src/session_list/failed_session.rs +++ b/src/session_list/failed_session.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use gtk::{glib, prelude::*, subclass::prelude::*}; use super::{BoxedStoredSession, SessionInfo, SessionInfoImpl}; -use crate::{secret::StoredSession, utils::matrix::ClientSetupError}; +use crate::{ + prelude::*, secret::StoredSession, session::model::AvatarData, utils::matrix::ClientSetupError, +}; #[derive(Clone, Debug, glib::Boxed)] #[boxed_type(name = "BoxedClientSetupError")] @@ -18,6 +20,8 @@ mod imp { pub struct FailedSession { /// The error encountered when initializing the session. pub error: OnceCell>, + /// The data for the avatar representation for this session. + pub avatar_data: OnceCell, } #[glib::object_subclass] @@ -52,7 +56,17 @@ mod imp { } } - impl SessionInfoImpl for FailedSession {} + impl SessionInfoImpl for FailedSession { + fn avatar_data(&self) -> AvatarData { + self.avatar_data + .get_or_init(|| { + let avatar_data = AvatarData::new(); + avatar_data.set_display_name(Some(self.obj().user_id().to_string())); + avatar_data + }) + .clone() + } + } } glib::wrapper! { diff --git a/src/session_list/new_session.rs b/src/session_list/new_session.rs index 5504751c..7b65ee47 100644 --- a/src/session_list/new_session.rs +++ b/src/session_list/new_session.rs @@ -1,13 +1,18 @@ use gtk::{glib, subclass::prelude::*}; use super::{BoxedStoredSession, SessionInfo, SessionInfoImpl}; -use crate::secret::StoredSession; +use crate::{prelude::*, secret::StoredSession, session::model::AvatarData}; mod imp { + use std::cell::OnceCell; + use super::*; #[derive(Debug, Default)] - pub struct NewSession {} + pub struct NewSession { + /// The data for the avatar representation for this session. + pub avatar_data: OnceCell, + } #[glib::object_subclass] impl ObjectSubclass for NewSession { @@ -17,7 +22,18 @@ mod imp { } impl ObjectImpl for NewSession {} - impl SessionInfoImpl for NewSession {} + + impl SessionInfoImpl for NewSession { + fn avatar_data(&self) -> AvatarData { + self.avatar_data + .get_or_init(|| { + let avatar_data = AvatarData::new(); + avatar_data.set_display_name(Some(self.obj().user_id().to_string())); + avatar_data + }) + .clone() + } + } } glib::wrapper! { diff --git a/src/session_list/session_info.rs b/src/session_list/session_info.rs index 4090a40d..0bd3fcc0 100644 --- a/src/session_list/session_info.rs +++ b/src/session_list/session_info.rs @@ -2,7 +2,7 @@ use gtk::{glib, prelude::*, subclass::prelude::*}; use ruma::{DeviceId, UserId}; use url::Url; -use crate::secret::StoredSession; +use crate::{secret::StoredSession, session::model::AvatarData}; #[derive(Clone, Debug, glib::Boxed)] #[boxed_type(name = "BoxedStoredSession")] @@ -18,12 +18,18 @@ mod imp { #[repr(C)] pub struct SessionInfoClass { pub parent_class: glib::object::ObjectClass, + pub avatar_data: fn(&super::SessionInfo) -> AvatarData, } unsafe impl ClassStruct for SessionInfoClass { type Type = SessionInfo; } + pub(super) fn session_info_avatar_data(this: &super::SessionInfo) -> AvatarData { + let klass = this.class(); + (klass.as_ref().avatar_data)(this) + } + #[derive(Debug, Default)] pub struct SessionInfo { /// The Matrix session's info. @@ -58,6 +64,9 @@ mod imp { glib::ParamSpecString::builder("session-id") .read_only() .build(), + glib::ParamSpecObject::builder::("avatar-data") + .read_only() + .build(), ] }); @@ -82,6 +91,7 @@ mod imp { "homeserver" => obj.homeserver().as_str().to_value(), "device-id" => obj.device_id().as_str().to_value(), "session-id" => obj.session_id().to_value(), + "avatar-data" => obj.avatar_data().to_value(), _ => unimplemented!(), } } @@ -123,12 +133,19 @@ pub trait SessionInfoExt: 'static { fn session_id(&self) -> &str { self.info().id() } + + /// The avatar data to represent this session. + fn avatar_data(&self) -> AvatarData; } impl> SessionInfoExt for O { fn info(&self) -> &StoredSession { self.upcast_ref().imp().info.get().unwrap() } + + fn avatar_data(&self) -> AvatarData { + imp::session_info_avatar_data(self.upcast_ref()) + } } /// Public trait that must be implemented for everything that derives from @@ -136,7 +153,9 @@ impl> SessionInfoExt for O { /// /// Overriding a method from this Trait overrides also its behavior in /// `SessionInfoExt`. -pub trait SessionInfoImpl: ObjectImpl {} +pub trait SessionInfoImpl: ObjectImpl { + fn avatar_data(&self) -> AvatarData; +} // Make `SessionInfo` subclassable. unsafe impl IsSubclassable for SessionInfo @@ -146,5 +165,18 @@ where { fn class_init(class: &mut glib::Class) { Self::parent_class_init::(class.upcast_ref_mut()); + let klass = class.as_mut(); + + klass.avatar_data = avatar_data_trampoline::; } } + +// Virtual method implementation trampolines. +fn avatar_data_trampoline(this: &SessionInfo) -> AvatarData +where + T: ObjectSubclass + SessionInfoImpl, + T::Type: IsA, +{ + let this = this.downcast_ref::().unwrap(); + this.imp().avatar_data() +} diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 5af045c4..cd637f6c 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -1,6 +1,7 @@ + account_switcher/account_switcher_button.ui account_switcher/account_switcher_popover.ui account_switcher/avatar_with_selection.ui account_switcher/session_item.ui