avatar-data: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-12 20:09:41 +01:00
parent cd93b65ebe
commit deaee7393d
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
12 changed files with 287 additions and 411 deletions

View File

@ -1,145 +0,0 @@
use gtk::{gdk, glib, prelude::*, subclass::prelude::*};
use tracing::warn;
use super::AvatarImage;
use crate::{
application::Application,
utils::notifications::{paintable_as_notification_icon, string_as_notification_icon},
};
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct AvatarData {
/// The data of the user-defined image.
pub image: RefCell<Option<AvatarImage>>,
/// The display name used as a fallback for this avatar.
pub display_name: RefCell<Option<String>>,
}
#[glib::object_subclass]
impl ObjectSubclass for AvatarData {
const NAME: &'static str = "AvatarData";
type Type = super::AvatarData;
}
impl ObjectImpl for AvatarData {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<AvatarImage>("image")
.explicit_notify()
.build(),
glib::ParamSpecString::builder("display-name")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"image" => obj.set_image(value.get().unwrap()),
"display-name" => obj.set_display_name(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"image" => obj.image().to_value(),
"display-name" => obj.display_name().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
/// Data about a Users or Rooms avatar.
pub struct AvatarData(ObjectSubclass<imp::AvatarData>);
}
impl AvatarData {
/// Construct a new empty `AvatarData`.
pub fn new() -> Self {
glib::Object::new()
}
/// Constructs an `AvatarData` with the given image data.
pub fn with_image(image: AvatarImage) -> Self {
glib::Object::builder().property("image", image).build()
}
/// The data of the user-defined image.
pub fn image(&self) -> Option<AvatarImage> {
self.imp().image.borrow().clone()
}
/// Set the data of the user-defined image.
pub fn set_image(&self, image: Option<AvatarImage>) {
let imp = self.imp();
if imp.image.borrow().as_ref() == image.as_ref() {
return;
}
imp.image.replace(image);
self.notify("image");
}
/// Set the display name used as a fallback for this avatar.
pub fn set_display_name(&self, display_name: Option<String>) {
let imp = self.imp();
if imp.display_name.borrow().as_ref() == display_name.as_ref() {
return;
}
imp.display_name.replace(display_name);
self.notify("display-name");
}
/// The display name used as a fallback for this avatar.
pub fn display_name(&self) -> Option<String> {
self.imp().display_name.borrow().clone()
}
/// Get this avatar as a notification icon.
///
/// Returns `None` if an error occurred while generating the icon.
pub fn as_notification_icon(&self) -> Option<gdk::Texture> {
let window = Application::default().active_window()?.upcast();
let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) {
paintable_as_notification_icon(paintable.upcast_ref(), &window)
} else {
string_as_notification_icon(&self.display_name().unwrap_or_default(), &window)
};
match icon {
Ok(icon) => Some(icon),
Err(error) => {
warn!("Failed to generate icon for notification: {error}");
None
}
}
}
}
impl Default for AvatarData {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,237 +0,0 @@
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*};
use matrix_sdk::{
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri,
OwnedMxcUri,
},
};
use tracing::error;
use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio};
/// The source of an avatar's URI.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "AvatarUriSource")]
pub enum AvatarUriSource {
/// The URI comes from a Matrix user.
#[default]
User = 0,
/// The URI comes from a Matrix room.
Room = 1,
}
mod imp {
use std::cell::{Cell, RefCell};
use once_cell::sync::{Lazy, OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct AvatarImage {
pub paintable: RefCell<Option<gdk::Paintable>>,
pub needed_size: Cell<u32>,
pub uri: RefCell<Option<OwnedMxcUri>>,
/// The source of the avatar's URI.
pub uri_source: Cell<AvatarUriSource>,
pub session: OnceCell<Session>,
}
#[glib::object_subclass]
impl ObjectSubclass for AvatarImage {
const NAME: &'static str = "AvatarImage";
type Type = super::AvatarImage;
}
impl ObjectImpl for AvatarImage {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<gdk::Paintable>("paintable")
.read_only()
.build(),
glib::ParamSpecUInt::builder("needed-size")
.minimum(0)
.explicit_notify()
.build(),
glib::ParamSpecString::builder("uri")
.explicit_notify()
.build(),
glib::ParamSpecEnum::builder::<AvatarUriSource>("uri-source")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<Session>("session")
.construct_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"needed-size" => obj.set_needed_size(value.get().unwrap()),
"uri" => obj.set_uri(value.get::<&str>().ok().map(Into::into)),
"uri-source" => obj.set_uri_source(value.get().unwrap()),
"session" => {
if let Some(session) = value.get().unwrap() {
if self.session.set(session).is_err() {
error!("Trying to set a session while it is already set");
}
}
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"paintable" => obj.paintable().to_value(),
"needed-size" => obj.needed_size().to_value(),
"uri" => obj.uri().map_or_else(
|| {
let none: Option<&str> = None;
none.to_value()
},
|url| url.as_str().to_value(),
),
"uri-source" => obj.uri_source().to_value(),
"session" => obj.session().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
/// The image data for an avatar.
pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>);
}
impl AvatarImage {
/// Construct a new `AvatarImage` with the given session and Matrix URI.
pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self {
glib::Object::builder()
.property("session", session)
.property("uri", uri.map(|uri| uri.to_string()))
.property("uri-source", uri_source)
.build()
}
/// The current session.
fn session(&self) -> &Session {
self.imp().session.get().unwrap()
}
/// The image content as a paintable, if any.
pub fn paintable(&self) -> Option<gdk::Paintable> {
self.imp().paintable.borrow().clone()
}
/// Set the content of the image.
fn set_image_data(&self, data: Option<Vec<u8>>) {
let paintable = data
.and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok())
.map(|texture| texture.upcast());
self.imp().paintable.replace(paintable);
self.notify("paintable");
}
fn load(&self) {
// Don't do anything here if we don't need the avatar.
if self.needed_size() == 0 {
return;
}
let Some(uri) = self.uri() else {
return;
};
let client = self.session().client();
let needed_size = self.needed_size();
let request = MediaRequest {
source: MediaSource::Plain(uri),
format: MediaFormat::Thumbnail(MediaThumbnailSize {
width: needed_size.into(),
height: needed_size.into(),
method: Method::Scale,
}),
};
let handle =
spawn_tokio!(async move { client.media().get_media_content(&request, true).await });
spawn!(
glib::Priority::LOW,
clone!(@weak self as obj => async move {
match handle.await.unwrap() {
Ok(data) => obj.set_image_data(Some(data)),
Err(error) => error!("Could not fetch avatar: {error}"),
};
})
);
}
/// Set the needed size of the user-defined image.
///
/// Only the biggest size will be stored.
pub fn set_needed_size(&self, size: u32) {
let imp = self.imp();
if imp.needed_size.get() < size {
imp.needed_size.set(size);
self.load();
}
self.notify("needed-size");
}
/// Get the biggest needed size of the user-defined image.
///
/// If this is `0`, no image will be loaded.
pub fn needed_size(&self) -> u32 {
self.imp().needed_size.get()
}
/// Set the Matrix URI of the `AvatarImage`.
pub fn set_uri(&self, uri: Option<OwnedMxcUri>) {
let imp = self.imp();
if imp.uri.borrow().as_ref() == uri.as_ref() {
return;
}
let has_uri = uri.is_some();
imp.uri.replace(uri);
if has_uri {
self.load();
} else {
self.set_image_data(None);
}
self.notify("uri");
}
/// The Matrix URI of the `AvatarImage`.
pub fn uri(&self) -> Option<OwnedMxcUri> {
self.imp().uri.borrow().to_owned()
}
/// The source of the avatar's URI.
pub fn uri_source(&self) -> AvatarUriSource {
self.imp().uri_source.get()
}
/// Set the source of the avatar's URI.
fn set_uri_source(&self, uri_source: AvatarUriSource) {
self.imp().uri_source.set(uri_source);
}
}

View File

@ -1,7 +0,0 @@
mod data;
mod image;
pub use self::{
data::AvatarData,
image::{AvatarImage, AvatarUriSource},
};

View File

@ -0,0 +1,161 @@
use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*};
use matrix_sdk::{
media::{MediaFormat, MediaRequest, MediaThumbnailSize},
ruma::{
api::client::media::get_content_thumbnail::v3::Method, events::room::MediaSource, MxcUri,
OwnedMxcUri,
},
};
use tracing::error;
use crate::{components::ImagePaintable, session::model::Session, spawn, spawn_tokio};
/// The source of an avatar's URI.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "AvatarUriSource")]
pub enum AvatarUriSource {
/// The URI comes from a Matrix user.
#[default]
User = 0,
/// The URI comes from a Matrix room.
Room = 1,
}
mod imp {
use std::cell::{Cell, OnceCell, RefCell};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::AvatarImage)]
pub struct AvatarImage {
/// The image content as a paintable, if any.
#[property(get)]
pub paintable: RefCell<Option<gdk::Paintable>>,
/// The biggest needed size of the user-defined image.
///
/// If this is `0`, no image will be loaded.
#[property(get, set = Self::set_needed_size, explicit_notify, minimum = 0)]
pub needed_size: Cell<u32>,
/// The Matrix URI of the `AvatarImage`.
#[property(get = Self::uri, set = Self::set_uri, explicit_notify, nullable, type = Option<String>)]
pub uri: RefCell<Option<OwnedMxcUri>>,
/// The source of the avatar's URI.
#[property(get, construct_only, builder(AvatarUriSource::default()))]
pub uri_source: Cell<AvatarUriSource>,
/// The current session.
#[property(get, construct_only)]
pub session: OnceCell<Session>,
}
#[glib::object_subclass]
impl ObjectSubclass for AvatarImage {
const NAME: &'static str = "AvatarImage";
type Type = super::AvatarImage;
}
#[glib::derived_properties]
impl ObjectImpl for AvatarImage {}
impl AvatarImage {
/// Set the needed size of the user-defined image.
///
/// Only the biggest size will be stored.
fn set_needed_size(&self, size: u32) {
if self.needed_size.get() >= size {
return;
}
let obj = self.obj();
self.needed_size.set(size);
obj.load();
obj.notify_needed_size();
}
/// The Matrix URI of the `AvatarImage`.
fn uri(&self) -> Option<String> {
self.uri.borrow().as_ref().map(ToString::to_string)
}
/// Set the Matrix URI of the `AvatarImage`.
fn set_uri(&self, uri: Option<String>) {
let uri = uri.map(OwnedMxcUri::from);
if self.uri.borrow().as_ref() == uri.as_ref() {
return;
}
let obj = self.obj();
let has_uri = uri.is_some();
self.uri.replace(uri);
if has_uri {
obj.load();
} else {
obj.set_image_data(None);
}
obj.notify_uri();
}
}
}
glib::wrapper! {
/// The image data for an avatar.
pub struct AvatarImage(ObjectSubclass<imp::AvatarImage>);
}
impl AvatarImage {
/// Construct a new `AvatarImage` with the given session and Matrix URI.
pub fn new(session: &Session, uri: Option<&MxcUri>, uri_source: AvatarUriSource) -> Self {
glib::Object::builder()
.property("session", session)
.property("uri", uri.map(|uri| uri.to_string()))
.property("uri-source", uri_source)
.build()
}
/// Set the content of the image.
fn set_image_data(&self, data: Option<Vec<u8>>) {
let paintable = data
.and_then(|data| ImagePaintable::from_bytes(&glib::Bytes::from(&data), None).ok())
.map(|texture| texture.upcast());
self.imp().paintable.replace(paintable);
self.notify("paintable");
}
fn load(&self) {
// Don't do anything here if we don't need the avatar.
if self.needed_size() == 0 {
return;
}
let Some(uri) = self.imp().uri.borrow().clone() else {
return;
};
let client = self.session().client();
let needed_size = self.needed_size();
let request = MediaRequest {
source: MediaSource::Plain(uri),
format: MediaFormat::Thumbnail(MediaThumbnailSize {
width: needed_size.into(),
height: needed_size.into(),
method: Method::Scale,
}),
};
let handle =
spawn_tokio!(async move { client.media().get_media_content(&request, true).await });
spawn!(
glib::Priority::LOW,
clone!(@weak self as obj => async move {
match handle.await.unwrap() {
Ok(data) => obj.set_image_data(Some(data)),
Err(error) => error!("Could not fetch avatar: {error}"),
};
})
);
}
}

View File

@ -0,0 +1,102 @@
use gtk::{gdk, glib, prelude::*, subclass::prelude::*};
use tracing::warn;
mod avatar_image;
pub use self::avatar_image::{AvatarImage, AvatarUriSource};
use crate::{
application::Application,
utils::notifications::{paintable_as_notification_icon, string_as_notification_icon},
};
mod imp {
use std::cell::RefCell;
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::AvatarData)]
pub struct AvatarData {
/// The data of the user-defined image.
#[property(get, set = Self::set_image, explicit_notify, nullable)]
pub image: RefCell<Option<AvatarImage>>,
/// The display name used as a fallback for this avatar.
#[property(get, set = Self::set_display_name, explicit_notify, nullable)]
pub display_name: RefCell<Option<String>>,
}
#[glib::object_subclass]
impl ObjectSubclass for AvatarData {
const NAME: &'static str = "AvatarData";
type Type = super::AvatarData;
}
#[glib::derived_properties]
impl ObjectImpl for AvatarData {}
impl AvatarData {
/// Set the data of the user-defined image.
fn set_image(&self, image: Option<AvatarImage>) {
if self.image.borrow().as_ref() == image.as_ref() {
return;
}
self.image.replace(image);
self.obj().notify_image();
}
/// Set the display name used as a fallback for this avatar.
fn set_display_name(&self, display_name: Option<String>) {
if self.display_name.borrow().as_ref() == display_name.as_ref() {
return;
}
self.display_name.replace(display_name);
self.obj().notify_display_name();
}
}
}
glib::wrapper! {
/// Data about a Users or Rooms avatar.
pub struct AvatarData(ObjectSubclass<imp::AvatarData>);
}
impl AvatarData {
/// Construct a new empty `AvatarData`.
pub fn new() -> Self {
glib::Object::new()
}
/// Constructs an `AvatarData` with the given image data.
pub fn with_image(image: AvatarImage) -> Self {
glib::Object::builder().property("image", image).build()
}
/// Get this avatar as a notification icon.
///
/// Returns `None` if an error occurred while generating the icon.
pub fn as_notification_icon(&self) -> Option<gdk::Texture> {
let window = Application::default().active_window()?.upcast();
let icon = if let Some(paintable) = self.image().and_then(|i| i.paintable()) {
paintable_as_notification_icon(paintable.upcast_ref(), &window)
} else {
string_as_notification_icon(&self.display_name().unwrap_or_default(), &window)
};
match icon {
Ok(icon) => Some(icon),
Err(error) => {
warn!("Failed to generate icon for notification: {error}");
None
}
}
}
}
impl Default for AvatarData {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,4 +1,4 @@
mod avatar;
mod avatar_data;
mod notifications;
mod room;
mod room_list;
@ -9,7 +9,7 @@ mod user;
mod verification;
pub use self::{
avatar::{AvatarData, AvatarImage, AvatarUriSource},
avatar_data::{AvatarData, AvatarImage, AvatarUriSource},
notifications::Notifications,
room::{
Event, EventKey, HighlightFlags, Member, MemberList, MemberRole, Membership, MessageState,

View File

@ -199,7 +199,7 @@ impl Member {
self.avatar_data()
.image()
.unwrap()
.set_uri(member.avatar_url().map(std::borrow::ToOwned::to_owned));
.set_uri(member.avatar_url().map(ToString::to_string));
self.set_power_level(member.power_level());
self.set_membership(member.membership().into());
}
@ -215,7 +215,7 @@ impl Member {
self.avatar_data()
.image()
.unwrap()
.set_uri(event.avatar_url());
.set_uri(event.avatar_url().map(String::from));
self.set_membership((&event.content().membership).into());
let session = self.session();

View File

@ -1651,7 +1651,10 @@ impl Room {
}
}
self.avatar_data().image().unwrap().set_uri(avatar_url);
self.avatar_data()
.image()
.unwrap()
.set_uri(avatar_url.map(String::from));
}
/// Whether anyone can join this room.

View File

@ -229,7 +229,10 @@ pub trait UserExt: IsA<User> {
/// Set the avatar URL of this user.
fn set_avatar_url(&self, uri: Option<OwnedMxcUri>) {
self.avatar_data().image().unwrap().set_uri(uri);
self.avatar_data()
.image()
.unwrap()
.set_uri(uri.map(String::from));
}
/// The actions the currently logged-in user is allowed to perform on this

View File

@ -5,7 +5,7 @@ use gtk::{
glib::{self, clone},
CompositeTemplate,
};
use matrix_sdk::ruma::{api::client::discovery::get_capabilities, OwnedMxcUri};
use matrix_sdk::ruma::api::client::discovery::get_capabilities;
use tracing::error;
mod change_password_subpage;
@ -57,7 +57,7 @@ mod imp {
pub deactivate_account_subpage: TemplateChild<DeactivateAccountSubpage>,
#[template_child]
pub log_out_subpage: TemplateChild<LogOutSubpage>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<String>>>,
pub changing_display_name: RefCell<Option<OngoingAsyncAction<String>>>,
}
@ -167,12 +167,9 @@ impl GeneralPage {
.avatar_data()
.image()
.unwrap()
.connect_notify_local(
Some("uri"),
clone!(@weak self as obj => move |avatar_image, _| {
obj.avatar_changed(avatar_image.uri());
}),
);
.connect_uri_notify(clone!(@weak self as obj => move |avatar_image| {
obj.avatar_changed(avatar_image.uri());
}));
self.user().connect_notify_local(
Some("display-name"),
clone!(@weak self as obj => move |user, _| {
@ -224,7 +221,7 @@ impl GeneralPage {
}));
}
fn avatar_changed(&self, uri: Option<OwnedMxcUri>) {
fn avatar_changed(&self, uri: Option<String>) {
let imp = self.imp();
if let Some(action) = imp.changing_avatar.borrow().as_ref() {
@ -278,7 +275,7 @@ impl GeneralPage {
}
};
let (action, weak_action) = OngoingAsyncAction::set(uri.clone());
let (action, weak_action) = OngoingAsyncAction::set(uri.to_string());
imp.changing_avatar.replace(Some(action));
let uri_clone = uri.clone();

View File

@ -164,12 +164,12 @@ impl PublicRoom {
pub fn set_matrix_public_room(&self, room: PublicRoomsChunk) {
let imp = self.imp();
let display_name = room.name.clone().map(Into::into);
let display_name = room.name.clone();
self.avatar_data().set_display_name(display_name);
self.avatar_data()
.image()
.unwrap()
.set_uri(room.avatar_url.clone());
.set_uri(room.avatar_url.clone().map(String::from));
if let Some(room) = self.room_list().get(&room.room_id) {
self.set_room(room);

View File

@ -14,7 +14,6 @@ use ruma::{
room::{avatar::ImageInfo, power_levels::PowerLevelAction},
StateEventType,
},
OwnedMxcUri,
};
use tracing::error;
@ -63,7 +62,7 @@ mod imp {
pub members_count: TemplateChild<gtk::Label>,
/// Whether edit mode is enabled.
pub edit_mode_enabled: Cell<bool>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<OwnedMxcUri>>>,
pub changing_avatar: RefCell<Option<OngoingAsyncAction<String>>>,
pub changing_name: RefCell<Option<OngoingAsyncAction<String>>>,
pub changing_topic: RefCell<Option<OngoingAsyncAction<String>>>,
pub expr_watches: RefCell<Vec<gtk::ExpressionWatch>>,
@ -232,7 +231,7 @@ impl GeneralPage {
self.imp().expr_watches.borrow_mut().push(expr_watch);
}
fn avatar_changed(&self, uri: Option<OwnedMxcUri>) {
fn avatar_changed(&self, uri: Option<String>) {
let imp = self.imp();
if let Some(action) = imp.changing_avatar.borrow().as_ref() {
@ -302,7 +301,7 @@ impl GeneralPage {
}
};
let (action, weak_action) = OngoingAsyncAction::set(uri.clone());
let (action, weak_action) = OngoingAsyncAction::set(uri.to_string());
imp.changing_avatar.replace(Some(action));
let handle =