Account switcher: Show hints in account entry for visible session

This commit is contained in:
Alejandro Domínguez 2021-08-16 05:41:05 +02:00 committed by Julian Sparber
parent 5a170f90f7
commit 5369d720cf
12 changed files with 272 additions and 53 deletions

View file

@ -33,6 +33,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="spinner-button.ui">ui/spinner-button.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="in-app-notification.ui">ui/in-app-notification.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-avatar.ui">ui/components-avatar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="avatar-with-selection.ui">ui/avatar-with-selection.ui</file>
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>

View file

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

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="AvatarWithSelection" parent="AdwBin">
<property name="child">
<object class="GtkOverlay">
<child>
<object class="ComponentsAvatar" id="child_avatar"></object>
</child>
<child type="overlay">
<object class="GtkImage" id="checkmark">
<style>
<class name="blue-checkmark" />
</style>
<property name="visible">false</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="icon-name">emblem-default-symbolic</property>
<property name="pixel-size">14</property>
</object>
</child>
</object>
</property>
</template>
</interface>

View file

@ -5,7 +5,7 @@
<object class="GtkBox">
<property name="spacing">10</property>
<child>
<object class="ComponentsAvatar" id="avatar_component">
<object class="AvatarWithSelection" id="account_avatar">
<property name="size">40</property>
<binding name="item">
<lookup name="avatar" type="User">

View file

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

View file

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

View file

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

View file

@ -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<Avatar>,
#[template_child]
pub checkmark: TemplateChild<gtk::Image>,
}
#[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<Self>) {
obj.init_template();
}
}
impl ObjectImpl for AvatarWithSelection {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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<imp::AvatarWithSelection>)
@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()
}
}

View file

@ -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<glib::Object> for Item {
fn try_from(object: glib::Object) -> Result<Self, Self::Error> {
object
.downcast::<gtk::StackPage>()
.map(Self::User)
.map(|sp| Self::User(sp, false))
.or_else(|object| object.downcast::<ExtraItemObj>().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(),
}

View file

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

View file

@ -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<Avatar>,
pub account_avatar: TemplateChild<AvatarWithSelection>,
#[template_child]
pub display_name: TemplateChild<gtk::Label>,
#[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<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpec::new_object(
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: &gtk::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 { &[] });
}
}

View file

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