account-switcher: Create AccountSwitcherButton
That is compatible with any SessionInfo
This commit is contained in:
parent
741f1dc5e0
commit
bed14c3b99
11 changed files with 235 additions and 57 deletions
|
@ -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
|
||||
|
|
121
src/account_switcher/account_switcher_button.rs
Normal file
121
src/account_switcher/account_switcher_button.rs
Normal 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()
|
||||
}
|
||||
}
|
27
src/account_switcher/account_switcher_button.ui
Normal file
27
src/account_switcher/account_switcher_button.ui
Normal 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>
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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! {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue