From 7c202b948829032a56229f4e5870a17ae0302ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Dom=C3=ADnguez?= Date: Mon, 14 Jun 2021 20:16:07 +0200 Subject: [PATCH] Add account switcher --- data/resources/resources.gresource.xml | 3 + data/resources/style.css | 22 +++ data/resources/ui/add-account-row.ui | 23 +++ data/resources/ui/sidebar-account-switcher.ui | 11 ++ data/resources/ui/sidebar.ui | 19 ++- data/resources/ui/user-entry-row.ui | 62 ++++++++ po/POTFILES.in | 6 + src/application.rs | 8 + src/components/avatar.rs | 2 +- src/meson.build | 4 + src/session/mod.rs | 7 +- .../sidebar/account_switcher/add_account.rs | 43 +++++ src/session/sidebar/account_switcher/item.rs | 148 ++++++++++++++++++ src/session/sidebar/account_switcher/mod.rs | 124 +++++++++++++++ .../sidebar/account_switcher/user_entry.rs | 91 +++++++++++ src/session/sidebar/mod.rs | 12 +- src/window.rs | 6 + 17 files changed, 584 insertions(+), 7 deletions(-) create mode 100644 data/resources/ui/add-account-row.ui create mode 100644 data/resources/ui/sidebar-account-switcher.ui create mode 100644 data/resources/ui/user-entry-row.ui create mode 100644 src/session/sidebar/account_switcher/add_account.rs create mode 100644 src/session/sidebar/account_switcher/item.rs create mode 100644 src/session/sidebar/account_switcher/mod.rs create mode 100644 src/session/sidebar/account_switcher/user_entry.rs diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index 58479c3c..ca7c77b3 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -1,6 +1,8 @@ + ui/add-account-row.ui + ui/user-entry-row.ui ui/shortcuts.ui ui/content.ui ui/content-room-history.ui @@ -19,6 +21,7 @@ ui/login.ui ui/session.ui ui/sidebar.ui + ui/sidebar-account-switcher.ui ui/sidebar-item.ui ui/sidebar-category-row.ui ui/sidebar-entry-row.ui diff --git a/data/resources/style.css b/data/resources/style.css index b291ab32..08e2583f 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -78,6 +78,28 @@ headerbar.flat { border: none; } +/* Account switcher */ +#account-switcher row { + border-radius: 10px; + margin-top: 2px; + margin-bottom: 2px; + padding-top: 7px; + padding-bottom: 7px; +} + +#account-switcher .user-id { + font-size: 12px; +} + +#new-login-icon { + /* + * 2 * padding + pixel-size = size (of avatar) + */ + padding: 10px; + background-color: lightgrey; + border-radius: 9999px; +} + /* Sidebar */ .sidebar row { padding-left: 10px; diff --git a/data/resources/ui/add-account-row.ui b/data/resources/ui/add-account-row.ui new file mode 100644 index 00000000..367fe5ca --- /dev/null +++ b/data/resources/ui/add-account-row.ui @@ -0,0 +1,23 @@ + + + + diff --git a/data/resources/ui/sidebar-account-switcher.ui b/data/resources/ui/sidebar-account-switcher.ui new file mode 100644 index 00000000..74dace8d --- /dev/null +++ b/data/resources/ui/sidebar-account-switcher.ui @@ -0,0 +1,11 @@ + + + + diff --git a/data/resources/ui/sidebar.ui b/data/resources/ui/sidebar.ui index c56907c3..06b3c2f9 100644 --- a/data/resources/ui/sidebar.ui +++ b/data/resources/ui/sidebar.ui @@ -25,12 +25,16 @@ vertical + + + - - system-search-symbolic - - win.toggle-room-search + + system-users-symbolic + + + @@ -39,6 +43,13 @@ primary_menu + + + system-search-symbolic + + win.toggle-room-search + + diff --git a/data/resources/ui/user-entry-row.ui b/data/resources/ui/user-entry-row.ui new file mode 100644 index 00000000..611fc607 --- /dev/null +++ b/data/resources/ui/user-entry-row.ui @@ -0,0 +1,62 @@ + + + + diff --git a/po/POTFILES.in b/po/POTFILES.in index 1c55e8f1..222b35dd 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -5,6 +5,7 @@ data/org.gnome.FractalNext.gschema.xml.in data/org.gnome.FractalNext.metainfo.xml.in.in # UI files +data/resources/ui/add_account.ui data/resources/ui/components-avatar.ui data/resources/ui/content-divider-row.ui data/resources/ui/content-item-row-menu.ui @@ -29,6 +30,7 @@ data/resources/ui/sidebar-room-row.ui data/resources/ui/sidebar.ui data/resources/ui/spinner-button.ui data/resources/ui/pill.ui +data/resources/ui/user-entry-row.ui data/resources/ui/window.ui # Rust files @@ -64,6 +66,10 @@ src/session/room/highlight_flags.rs src/session/room/item.rs src/session/room/mod.rs src/session/room/timeline.rs +src/session/sidebar/account_switcher/add_account.rs +src/session/sidebar/account_switcher/item.rs +src/session/sidebar/account_switcher/mod.rs +src/session/sidebar/account_switcher/user_entry.rs src/session/sidebar/category_row.rs src/session/sidebar/entry.rs src/session/sidebar/mod.rs diff --git a/src/application.rs b/src/application.rs index 82ec6f89..0f86a436 100644 --- a/src/application.rs +++ b/src/application.rs @@ -120,6 +120,14 @@ impl Application { app.show_about_dialog(); }) ); + + action!( + self, + "new-login", + clone!(@weak self as app => move |_, _| { + app.get_main_window().switch_to_login_page(); + }) + ); } /// Sets up keyboard shortcuts for application and window actions. diff --git a/src/components/avatar.rs b/src/components/avatar.rs index e3cbc25b..4745dd82 100644 --- a/src/components/avatar.rs +++ b/src/components/avatar.rs @@ -6,6 +6,7 @@ use crate::session::Avatar as AvatarItem; mod imp { use super::*; use glib::subclass::InitializingObject; + use once_cell::sync::Lazy; use std::cell::RefCell; #[derive(Debug, Default, CompositeTemplate)] @@ -34,7 +35,6 @@ mod imp { impl ObjectImpl for Avatar { fn properties() -> &'static [glib::ParamSpec] { - use once_cell::sync::Lazy; static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpec::new_object( diff --git a/src/meson.build b/src/meson.build index c0d57ba0..b72383c2 100644 --- a/src/meson.build +++ b/src/meson.build @@ -69,6 +69,10 @@ sources = files( 'session/sidebar/row.rs', 'session/sidebar/room_row.rs', 'session/sidebar/selection.rs', + 'session/sidebar/account_switcher/add_account.rs', + 'session/sidebar/account_switcher/item.rs', + 'session/sidebar/account_switcher/mod.rs', + 'session/sidebar/account_switcher/user_entry.rs', ) custom_target( diff --git a/src/session/mod.rs b/src/session/mod.rs index 3084f7cd..436fa2f6 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -25,7 +25,7 @@ use crate::session::content::ContentType; use adw::subclass::prelude::BinImpl; use gtk::subclass::prelude::*; use gtk::{self, prelude::*}; -use gtk::{gio, glib, glib::clone, glib::SyncSender, CompositeTemplate}; +use gtk::{gio, glib, glib::clone, glib::SyncSender, CompositeTemplate, SelectionModel}; use gtk_macros::send; use log::error; use matrix_sdk::ruma::{ @@ -449,6 +449,11 @@ impl Session { fn handle_sync_response(&self, response: SyncResponse) { self.room_list().handle_response_rooms(response.rooms); } + + pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { + let priv_ = &imp::Session::from_instance(self); + priv_.sidebar.set_logged_in_users(sessions_stack_pages); + } } impl Default for Session { diff --git a/src/session/sidebar/account_switcher/add_account.rs b/src/session/sidebar/account_switcher/add_account.rs new file mode 100644 index 00000000..abf9ccff --- /dev/null +++ b/src/session/sidebar/account_switcher/add_account.rs @@ -0,0 +1,43 @@ +use adw::subclass::prelude::BinImpl; +use gtk::subclass::prelude::*; +use gtk::{self, prelude::*}; +use gtk::{glib, CompositeTemplate}; + +mod imp { + use super::*; + use glib::subclass::InitializingObject; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/add-account-row.ui")] + pub struct AddAccountRow; + + #[glib::object_subclass] + impl ObjectSubclass for AddAccountRow { + const NAME: &'static str = "AddAccountRow"; + type Type = super::AddAccountRow; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AddAccountRow {} + impl WidgetImpl for AddAccountRow {} + impl BinImpl for AddAccountRow {} +} + +glib::wrapper! { + pub struct AddAccountRow(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl AddAccountRow { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create AddAccountRow") + } +} diff --git a/src/session/sidebar/account_switcher/item.rs b/src/session/sidebar/account_switcher/item.rs new file mode 100644 index 00000000..332fa3e8 --- /dev/null +++ b/src/session/sidebar/account_switcher/item.rs @@ -0,0 +1,148 @@ +use super::add_account::AddAccountRow; +use super::user_entry::UserEntryRow; +use gtk::{gio::ListStore, glib, prelude::*, subclass::prelude::*}; +use std::convert::TryFrom; + +mod imp { + use super::*; + use once_cell::sync::Lazy; + use std::cell::Cell; + + #[derive(Debug, Default)] + pub struct ExtraItemObj(pub Cell); + + #[glib::object_subclass] + impl ObjectSubclass for ExtraItemObj { + const NAME: &'static str = "ExtraItemObj"; + type Type = super::ExtraItemObj; + type ParentType = glib::Object; + } + + impl ObjectImpl for ExtraItemObj { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_enum( + "inner", + "Inner", + "Inner value of ExtraItem", + super::ExtraItem::static_type(), + 0, + glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY, + )] + }); + + PROPERTIES.as_ref() + } + + fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "inner" => obj.get().to_value(), + _ => unimplemented!(), + } + } + + fn set_property( + &self, + _obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "inner" => self.0.set(value.get().unwrap()), + _ => unimplemented!(), + } + } + } +} + +#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)] +#[repr(u32)] +#[genum(type_name = "ExtraItem")] +pub enum ExtraItem { + Separator = 0, + AddAccount = 1, +} + +impl ExtraItem { + const VALUES: [Self; 2] = [Self::Separator, Self::AddAccount]; +} + +impl Default for ExtraItem { + fn default() -> Self { + Self::Separator + } +} + +glib::wrapper! { + pub struct ExtraItemObj(ObjectSubclass); +} + +impl From<&ExtraItem> for ExtraItemObj { + fn from(item: &ExtraItem) -> Self { + glib::Object::new(&[("inner", item)]).expect("Failed to create ExtraItem") + } +} + +impl ExtraItemObj { + pub fn list_store() -> ListStore { + ExtraItem::VALUES.iter().map(ExtraItemObj::from).fold( + ListStore::new(ExtraItemObj::static_type()), + |list_items, item| { + list_items.append(&item); + list_items + }, + ) + } + + pub fn get(&self) -> ExtraItem { + let priv_ = imp::ExtraItemObj::from_instance(self); + + priv_.0.get() + } + + pub fn is_separator(&self) -> bool { + self.get() == ExtraItem::Separator + } + + pub fn is_add_account(&self) -> bool { + self.get() == ExtraItem::AddAccount + } +} + +#[derive(Debug, Clone)] +pub enum Item { + User(gtk::StackPage), + Separator, + AddAccount, +} + +impl From for Item { + fn from(extra_item: ExtraItem) -> Self { + match extra_item { + ExtraItem::Separator => Self::Separator, + ExtraItem::AddAccount => Self::AddAccount, + } + } +} + +impl TryFrom for Item { + type Error = glib::Object; + + fn try_from(object: glib::Object) -> Result { + object + .downcast::() + .map(Self::User) + .or_else(|object| object.downcast::().map(|it| it.get().into())) + } +} + +impl Item { + pub fn build_widget(&self) -> gtk::Widget { + match self { + Self::User(ref session_page) => UserEntryRow::new(session_page).upcast(), + Self::Separator => gtk::Separator::new(gtk::Orientation::Vertical).upcast(), + Self::AddAccount => AddAccountRow::new().upcast(), + } + } +} diff --git a/src/session/sidebar/account_switcher/mod.rs b/src/session/sidebar/account_switcher/mod.rs new file mode 100644 index 00000000..e1e5db0b --- /dev/null +++ b/src/session/sidebar/account_switcher/mod.rs @@ -0,0 +1,124 @@ +use gtk::{ + gio::{self, ListModel, ListStore}, + glib, + prelude::*, + subclass::prelude::*, + CompositeTemplate, SelectionModel, +}; +use std::convert::TryFrom; + +use super::account_switcher::item::{ExtraItemObj, Item as AccountSwitcherItem}; + +pub mod add_account; +pub mod item; +pub mod user_entry; + +mod imp { + use super::*; + use glib::subclass::InitializingObject; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/sidebar-account-switcher.ui")] + pub struct AccountSwitcher { + #[template_child] + pub entries: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AccountSwitcher { + const NAME: &'static str = "AccountSwitcher"; + type Type = super::AccountSwitcher; + type ParentType = gtk::Popover; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for AccountSwitcher { + fn constructed(&self, obj: &Self::Type) { + self.parent_constructed(obj); + + self.entries.connect_activate(|list_view, index| { + list_view + .model() + .and_then(|model| model.item(index)) + .map(AccountSwitcherItem::try_from) + .and_then(Result::ok) + .map(|item| match item { + AccountSwitcherItem::User(session_page) => { + let session_widget = session_page.child(); + session_widget + .parent() + .unwrap() + .downcast::() + .unwrap() + .set_visible_child(&session_widget); + } + AccountSwitcherItem::AddAccount => { + list_view.activate_action("app.new-login", None); + } + _ => {} + }); + }); + + // There is no permanent stuff to take care of, + // so only bind and unbind are connected. + let ref factory = gtk::SignalListItemFactory::new(); + factory.connect_bind(|_, list_item| { + list_item.set_selectable(false); + let child = list_item + .item() + .map(AccountSwitcherItem::try_from) + .and_then(Result::ok) + .as_ref() + .map(|item| { + match item { + AccountSwitcherItem::Separator => { + list_item.set_activatable(false); + } + _ => {} + } + + item + }) + .map(AccountSwitcherItem::build_widget); + + list_item.set_child(child.as_ref()); + }); + + factory.connect_unbind(|_, list_item| { + list_item.set_child(gtk::NONE_WIDGET); + }); + + self.entries.set_factory(Some(factory)); + } + } + + impl WidgetImpl for AccountSwitcher {} + impl PopoverImpl for AccountSwitcher {} +} + +glib::wrapper! { + pub struct AccountSwitcher(ObjectSubclass) + @extends gtk::Widget, gtk::Popover, @implements gtk::Accessible, gio::ListModel; +} + +impl AccountSwitcher { + pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { + let entries = imp::AccountSwitcher::from_instance(self).entries.get(); + + let ref end_items = ExtraItemObj::list_store(); + let ref items_split = ListStore::new(ListModel::static_type()); + items_split.append(sessions_stack_pages); + items_split.append(end_items); + let ref items = gtk::FlattenListModel::new(Some(items_split)); + let ref selectable_items = gtk::NoSelection::new(Some(items)); + + entries.set_model(Some(selectable_items)); + } +} diff --git a/src/session/sidebar/account_switcher/user_entry.rs b/src/session/sidebar/account_switcher/user_entry.rs new file mode 100644 index 00000000..6edeaf14 --- /dev/null +++ b/src/session/sidebar/account_switcher/user_entry.rs @@ -0,0 +1,91 @@ +use crate::components::Avatar; +use adw::subclass::prelude::BinImpl; +use gtk::{self, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; + +mod imp { + use super::*; + use glib::subclass::InitializingObject; + use once_cell::sync::Lazy; + use std::cell::RefCell; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/user-entry-row.ui")] + pub struct UserEntryRow { + #[template_child] + pub avatar_component: TemplateChild, + #[template_child] + pub display_name: TemplateChild, + #[template_child] + pub user_id: TemplateChild, + pub session_page: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for UserEntryRow { + const NAME: &'static str = "UserEntryRow"; + type Type = super::UserEntryRow; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + Avatar::static_type(); + Self::bind_template(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for UserEntryRow { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![glib::ParamSpec::new_object( + "session-page", + "Session StackPage", + "The stack page of the session that this entry represents", + gtk::StackPage::static_type(), + glib::ParamFlags::READWRITE, + )] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + _obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "session-page" => { + let session_page = value.get().unwrap(); + self.session_page.replace(Some(session_page)); + } + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "session-page" => self.session_page.borrow().to_value(), + _ => unimplemented!(), + } + } + } + + impl WidgetImpl for UserEntryRow {} + impl BinImpl for UserEntryRow {} +} + +glib::wrapper! { + pub struct UserEntryRow(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl UserEntryRow { + pub fn new(session_page: >k::StackPage) -> Self { + glib::Object::new(&[("session-page", session_page)]).expect("Failed to create UserEntryRow") + } +} diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs index 56cbef1e..324a0d47 100644 --- a/src/session/sidebar/mod.rs +++ b/src/session/sidebar/mod.rs @@ -1,3 +1,4 @@ +mod account_switcher; mod category; mod category_row; mod entry; @@ -17,11 +18,12 @@ use self::row::Row; use self::selection::Selection; use adw::subclass::prelude::BinImpl; -use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; +use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate, SelectionModel}; use crate::session::content::ContentType; use crate::session::room::Room; use crate::session::RoomList; +use account_switcher::AccountSwitcher; mod imp { use super::*; @@ -38,6 +40,8 @@ mod imp { #[template_child] pub headerbar: TemplateChild, #[template_child] + pub account_switcher: TemplateChild, + #[template_child] pub listview: TemplateChild, #[template_child] pub room_search_entry: TemplateChild, @@ -263,6 +267,12 @@ impl Sidebar { priv_.selected_room.replace(selected_room); self.notify("selected-room"); } + + pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { + imp::Sidebar::from_instance(self) + .account_switcher + .set_logged_in_users(sessions_stack_pages); + } } impl Default for Sidebar { diff --git a/src/window.rs b/src/window.rs index b15df917..be2c512d 100644 --- a/src/window.rs +++ b/src/window.rs @@ -99,6 +99,7 @@ impl Window { fn add_session(&self, session: &Session) { let priv_ = &imp::Window::from_instance(self); + session.set_logged_in_users(&priv_.sessions.pages()); priv_.sessions.add_child(session); priv_.sessions.set_visible_child(session); self.install_session_actions(session); @@ -179,4 +180,9 @@ impl Window { let priv_ = imp::Window::from_instance(self); priv_.main_stack.set_visible_child(&priv_.sessions.get()); } + + pub fn switch_to_login_page(&self) { + let priv_ = imp::Window::from_instance(self); + priv_.main_stack.set_visible_child(&priv_.login.get()); + } }