Account switcher: Show hints in account entry for visible session
This commit is contained in:
parent
5a170f90f7
commit
5369d720cf
12 changed files with 272 additions and 53 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
24
data/resources/ui/avatar-with-selection.ui
Normal file
24
data/resources/ui/avatar-with-selection.ui
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
131
src/session/sidebar/account_switcher/avatar_with_selection.rs
Normal file
131
src/session/sidebar/account_switcher/avatar_with_selection.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
"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 { &[] });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue