account-switcher: Create AccountSwitcherButton

That is compatible with any SessionInfo
This commit is contained in:
Kévin Commaille 2023-11-28 15:40:58 +01:00
parent 741f1dc5e0
commit bed14c3b99
No known key found for this signature in database
GPG key ID: 29A48C1F03620416
11 changed files with 235 additions and 57 deletions

View file

@ -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

View file

@ -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<AccountSwitcherPopover>,
}
#[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<Self>) {
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<imp::AccountSwitcherButton>)
@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<AccountSwitcherPopover> {
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::<AccountSwitcherButton>() {
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::<Window>() 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()
}
}

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="AccountSwitcherButton" parent="GtkToggleButton">
<property name="tooltip-text" translatable="yes">Switch Accounts</property>
<accessibility>
<property name="label" translatable="yes">Switch Accounts</property>
</accessibility>
<style>
<class name="image-button"/>
</style>
<property name="child">
<object class="ComponentsAvatar">
<property name="size">24</property>
<binding name="data">
<lookup name="avatar-data" type="SessionInfo">
<lookup name="selected-item" type="GtkSingleSelection">
<lookup name="session-selection" type="Window">
<lookup name="root">AccountSwitcherButton</lookup>
</lookup>
</lookup>
</lookup>
</binding>
<property name="accessible-role">presentation</property>
</object>
</property>
</template>
</interface>

View file

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

View file

@ -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! {

View file

@ -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<gtk::SearchBar>,
#[template_child]
pub account_switcher_button: TemplateChild<gtk::MenuButton>,
#[template_child]
pub room_row_menu: TemplateChild<gio::MenuModel>,
#[template_child]
pub offline_banner: TemplateChild<adw::Banner>,
@ -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::<gtk::MenuButton>() {
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<Window> {
self.root().and_downcast()
}
}

View file

@ -82,26 +82,7 @@
<object class="AdwHeaderBar">
<property name="show-title">False</property>
<child type="start">
<object class="GtkMenuButton" id="account_switcher_button">
<property name="tooltip-text" translatable="yes">Switch Accounts</property>
<accessibility>
<property name="label" translatable="yes">Switch Accounts</property>
</accessibility>
<style>
<class name="image-button"/>
</style>
<property name="child">
<object class="ComponentsAvatar">
<property name="size">24</property>
<binding name="data">
<lookup name="avatar-data" type="User">
<lookup name="user">Sidebar</lookup>
</lookup>
</binding>
<property name="accessible-role">presentation</property>
</object>
</property>
</object>
<object class="AccountSwitcherButton"/>
</child>
<child type="end">
<object class="GtkMenuButton" id="appmenu_button">

View file

@ -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<Arc<ClientSetupError>>,
/// The data for the avatar representation for this session.
pub avatar_data: OnceCell<AvatarData>,
}
#[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! {

View file

@ -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<AvatarData>,
}
#[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! {

View file

@ -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::<AvatarData>("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<O: IsA<SessionInfo>> 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<O: IsA<SessionInfo>> 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<T> IsSubclassable<T> for SessionInfo
@ -146,5 +165,18 @@ where
{
fn class_init(class: &mut glib::Class<Self>) {
Self::parent_class_init::<T>(class.upcast_ref_mut());
let klass = class.as_mut();
klass.avatar_data = avatar_data_trampoline::<T>;
}
}
// Virtual method implementation trampolines.
fn avatar_data_trampoline<T>(this: &SessionInfo) -> AvatarData
where
T: ObjectSubclass + SessionInfoImpl,
T::Type: IsA<SessionInfo>,
{
let this = this.downcast_ref::<T::Type>().unwrap();
this.imp().avatar_data()
}

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/Fractal/ui/">
<file compressed="true" preprocess="xml-stripblanks">account_switcher/account_switcher_button.ui</file>
<file compressed="true" preprocess="xml-stripblanks">account_switcher/account_switcher_popover.ui</file>
<file compressed="true" preprocess="xml-stripblanks">account_switcher/avatar_with_selection.ui</file>
<file compressed="true" preprocess="xml-stripblanks">account_switcher/session_item.ui</file>