diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml index ca7c77b3..088f12e9 100644 --- a/data/resources/resources.gresource.xml +++ b/data/resources/resources.gresource.xml @@ -33,6 +33,7 @@ ui/spinner-button.ui ui/in-app-notification.ui ui/components-avatar.ui + ui/avatar-with-selection.ui style.css icons/scalable/actions/send-symbolic.svg icons/scalable/status/welcome.svg diff --git a/data/resources/style.css b/data/resources/style.css index 08e2583f..f9486b67 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -55,6 +55,16 @@ background-color: alpha(@theme_bg_color, 0.2); } +.selected-avatar avatar { + border: 2px solid @accent_bg_color; +} + +.blue-checkmark { + color: @accent_bg_color; + border-radius: 9999px; + background-color: white; +} + /* Login */ .login { min-width: 250px; diff --git a/data/resources/ui/avatar-with-selection.ui b/data/resources/ui/avatar-with-selection.ui new file mode 100644 index 00000000..aa9cae12 --- /dev/null +++ b/data/resources/ui/avatar-with-selection.ui @@ -0,0 +1,24 @@ + + + + diff --git a/data/resources/ui/user-entry-row.ui b/data/resources/ui/user-entry-row.ui index 611fc607..c1c558fc 100644 --- a/data/resources/ui/user-entry-row.ui +++ b/data/resources/ui/user-entry-row.ui @@ -5,7 +5,7 @@ 10 - + 40 diff --git a/po/POTFILES.in b/po/POTFILES.in index 222b35dd..2086295f 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -7,6 +7,7 @@ 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/avatar-with-selection.ui data/resources/ui/content-divider-row.ui data/resources/ui/content-item-row-menu.ui data/resources/ui/content-item.ui @@ -67,6 +68,7 @@ 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/avatar_with_selection.rs src/session/sidebar/account_switcher/item.rs src/session/sidebar/account_switcher/mod.rs src/session/sidebar/account_switcher/user_entry.rs diff --git a/src/meson.build b/src/meson.build index b72383c2..94e8dd32 100644 --- a/src/meson.build +++ b/src/meson.build @@ -70,6 +70,7 @@ sources = files( 'session/sidebar/room_row.rs', 'session/sidebar/selection.rs', 'session/sidebar/account_switcher/add_account.rs', + 'session/sidebar/account_switcher/avatar_with_selection.rs', 'session/sidebar/account_switcher/item.rs', 'session/sidebar/account_switcher/mod.rs', 'session/sidebar/account_switcher/user_entry.rs', diff --git a/src/session/mod.rs b/src/session/mod.rs index 436fa2f6..b888ccf0 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -452,7 +452,9 @@ impl Session { 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); + priv_ + .sidebar + .set_logged_in_users(sessions_stack_pages, self); } } diff --git a/src/session/sidebar/account_switcher/avatar_with_selection.rs b/src/session/sidebar/account_switcher/avatar_with_selection.rs new file mode 100644 index 00000000..827d5e66 --- /dev/null +++ b/src/session/sidebar/account_switcher/avatar_with_selection.rs @@ -0,0 +1,131 @@ +use adw::subclass::prelude::*; +use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate}; + +use crate::components::Avatar; +use crate::session::Avatar as AvatarItem; + +mod imp { + use super::*; + use glib::subclass::InitializingObject; + use once_cell::sync::Lazy; + + #[derive(Debug, Default, CompositeTemplate)] + #[template(resource = "/org/gnome/FractalNext/avatar-with-selection.ui")] + pub struct AvatarWithSelection { + #[template_child] + pub child_avatar: TemplateChild, + #[template_child] + pub checkmark: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for AvatarWithSelection { + const NAME: &'static str = "AvatarWithSelection"; + type Type = super::AvatarWithSelection; + 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 AvatarWithSelection { + fn properties() -> &'static [glib::ParamSpec] { + static PROPERTIES: Lazy> = Lazy::new(|| { + vec![ + glib::ParamSpec::new_object( + "item", + "Item", + "The Avatar item displayed by this widget", + AvatarItem::static_type(), + glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, + ), + glib::ParamSpec::new_int( + "size", + "Size", + "The size of the Avatar", + -1, + i32::MAX, + -1, + glib::ParamFlags::READWRITE, + ), + glib::ParamSpec::new_boolean( + "selected", + "Selected", + "Style helper for the inner Avatar", + false, + glib::ParamFlags::WRITABLE, + ), + ] + }); + + PROPERTIES.as_ref() + } + + fn set_property( + &self, + obj: &Self::Type, + _id: usize, + value: &glib::Value, + pspec: &glib::ParamSpec, + ) { + match pspec.name() { + "item" => self.child_avatar.set_item(value.get().unwrap()), + "size" => self.child_avatar.set_size(value.get().unwrap()), + "selected" => obj.set_selected(value.get().unwrap()), + _ => unimplemented!(), + } + } + + fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { + match pspec.name() { + "item" => self.child_avatar.item().to_value(), + "size" => self.child_avatar.size().to_value(), + _ => unimplemented!(), + } + } + } + + impl WidgetImpl for AvatarWithSelection {} + impl BinImpl for AvatarWithSelection {} +} + +glib::wrapper! { + /// A widget displaying an `Avatar` for a `Room` or `User`. + pub struct AvatarWithSelection(ObjectSubclass) + @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; +} + +impl AvatarWithSelection { + pub fn new() -> Self { + glib::Object::new(&[]).expect("Failed to create AvatarWithSelection") + } + + pub fn set_selected(&self, selected: bool) { + let priv_ = imp::AvatarWithSelection::from_instance(self); + + priv_.checkmark.set_visible(selected); + + if selected { + priv_.child_avatar.add_css_class("selected-avatar"); + } else { + priv_.child_avatar.remove_css_class("selected-avatar"); + } + } + + pub fn avatar(&self) -> &Avatar { + let priv_ = imp::AvatarWithSelection::from_instance(self); + &priv_.child_avatar + } +} + +impl Default for AvatarWithSelection { + fn default() -> Self { + Self::new() + } +} diff --git a/src/session/sidebar/account_switcher/item.rs b/src/session/sidebar/account_switcher/item.rs index 332fa3e8..987fa82e 100644 --- a/src/session/sidebar/account_switcher/item.rs +++ b/src/session/sidebar/account_switcher/item.rs @@ -1,5 +1,6 @@ use super::add_account::AddAccountRow; use super::user_entry::UserEntryRow; +use crate::session::Session; use gtk::{gio::ListStore, glib, prelude::*, subclass::prelude::*}; use std::convert::TryFrom; @@ -110,9 +111,9 @@ impl ExtraItemObj { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Item { - User(gtk::StackPage), + User(gtk::StackPage, bool), Separator, AddAccount, } @@ -132,15 +133,29 @@ impl TryFrom for Item { fn try_from(object: glib::Object) -> Result { object .downcast::() - .map(Self::User) + .map(|sp| Self::User(sp, false)) .or_else(|object| object.downcast::().map(|it| it.get().into())) } } impl Item { + pub fn set_hint(self, session_root: Session) -> Self { + match self { + Self::User(session_page, _) => { + let hinted = session_root == session_page.child(); + Self::User(session_page, hinted) + } + other => other, + } + } + pub fn build_widget(&self) -> gtk::Widget { match self { - Self::User(ref session_page) => UserEntryRow::new(session_page).upcast(), + Self::User(ref session_page, hinted) => { + let user_entry = UserEntryRow::new(session_page); + user_entry.set_hint(hinted.clone()); + user_entry.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 index e1e5db0b..d4f92ee2 100644 --- a/src/session/sidebar/account_switcher/mod.rs +++ b/src/session/sidebar/account_switcher/mod.rs @@ -1,6 +1,6 @@ use gtk::{ gio::{self, ListModel, ListStore}, - glib, + glib::{self, clone}, prelude::*, subclass::prelude::*, CompositeTemplate, SelectionModel, @@ -8,8 +8,10 @@ use gtk::{ use std::convert::TryFrom; use super::account_switcher::item::{ExtraItemObj, Item as AccountSwitcherItem}; +use crate::session::Session; pub mod add_account; +pub mod avatar_with_selection; pub mod item; pub mod user_entry; @@ -50,7 +52,7 @@ mod imp { .map(AccountSwitcherItem::try_from) .and_then(Result::ok) .map(|item| match item { - AccountSwitcherItem::User(session_page) => { + AccountSwitcherItem::User(session_page, _) => { let session_widget = session_page.child(); session_widget .parent() @@ -65,37 +67,6 @@ mod imp { _ => {} }); }); - - // 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)); } } @@ -109,9 +80,47 @@ glib::wrapper! { } impl AccountSwitcher { - pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { + pub fn set_logged_in_users( + &self, + sessions_stack_pages: &SelectionModel, + session_root: &Session, + ) { let entries = imp::AccountSwitcher::from_instance(self).entries.get(); + // 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(clone!(@weak session_root => move |_, list_item| { + list_item.set_selectable(false); + let child = list_item + .item() + .map(AccountSwitcherItem::try_from) + .and_then(Result::ok) + .map(|item| { + // Given that all the account switchers are built per-session widget + // there is no need for callbacks or data bindings; just set the hint + // when building the entries and they will show correctly marked in + // each session widget. + let item = item.set_hint(session_root); + + if item == AccountSwitcherItem::Separator { + list_item.set_activatable(false); + } + + item + }) + .as_ref() + .map(AccountSwitcherItem::build_widget); + + list_item.set_child(child.as_ref()); + })); + + factory.connect_unbind(|_, list_item| { + list_item.set_child(gtk::NONE_WIDGET); + }); + + entries.set_factory(Some(factory)); + let ref end_items = ExtraItemObj::list_store(); let ref items_split = ListStore::new(ListModel::static_type()); items_split.append(sessions_stack_pages); diff --git a/src/session/sidebar/account_switcher/user_entry.rs b/src/session/sidebar/account_switcher/user_entry.rs index 6edeaf14..613df8c8 100644 --- a/src/session/sidebar/account_switcher/user_entry.rs +++ b/src/session/sidebar/account_switcher/user_entry.rs @@ -1,4 +1,4 @@ -use crate::components::Avatar; +use super::avatar_with_selection::AvatarWithSelection; use adw::subclass::prelude::BinImpl; use gtk::{self, glib, prelude::*, subclass::prelude::*, CompositeTemplate}; @@ -12,7 +12,7 @@ mod imp { #[template(resource = "/org/gnome/FractalNext/user-entry-row.ui")] pub struct UserEntryRow { #[template_child] - pub avatar_component: TemplateChild, + pub account_avatar: TemplateChild, #[template_child] pub display_name: TemplateChild, #[template_child] @@ -27,7 +27,7 @@ mod imp { type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { - Avatar::static_type(); + AvatarWithSelection::static_type(); Self::bind_template(klass); } @@ -39,13 +39,22 @@ mod imp { 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, - )] + 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, + ), + glib::ParamSpec::new_boolean( + "hint", + "Selection hint", + "The hint of the session that owns the account switcher which this entry belongs to", + false, + glib::ParamFlags::WRITABLE | glib::ParamFlags::CONSTRUCT_ONLY, + ), + ] }); PROPERTIES.as_ref() @@ -53,7 +62,7 @@ mod imp { fn set_property( &self, - _obj: &Self::Type, + obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec, @@ -63,6 +72,7 @@ mod imp { let session_page = value.get().unwrap(); self.session_page.replace(Some(session_page)); } + "hint" => obj.set_hint(value.get().unwrap()), _ => unimplemented!(), } } @@ -88,4 +98,13 @@ impl UserEntryRow { pub fn new(session_page: >k::StackPage) -> Self { glib::Object::new(&[("session-page", session_page)]).expect("Failed to create UserEntryRow") } + + pub fn set_hint(&self, hinted: bool) { + let priv_ = imp::UserEntryRow::from_instance(self); + + priv_.account_avatar.set_selected(hinted); + priv_ + .display_name + .set_css_classes(if hinted { &["bold"] } else { &[] }); + } } diff --git a/src/session/sidebar/mod.rs b/src/session/sidebar/mod.rs index 324a0d47..76262b61 100644 --- a/src/session/sidebar/mod.rs +++ b/src/session/sidebar/mod.rs @@ -23,6 +23,7 @@ use gtk::{gio, glib, prelude::*, subclass::prelude::*, CompositeTemplate, Select use crate::session::content::ContentType; use crate::session::room::Room; use crate::session::RoomList; +use crate::session::Session; use account_switcher::AccountSwitcher; mod imp { @@ -268,10 +269,14 @@ impl Sidebar { self.notify("selected-room"); } - pub fn set_logged_in_users(&self, sessions_stack_pages: &SelectionModel) { + pub fn set_logged_in_users( + &self, + sessions_stack_pages: &SelectionModel, + session_root: &Session, + ) { imp::Sidebar::from_instance(self) .account_switcher - .set_logged_in_users(sessions_stack_pages); + .set_logged_in_users(sessions_stack_pages, session_root); } }