components: Create EditableAvatar

This commit is contained in:
Kévin Commaille 2022-02-08 19:09:55 +01:00
parent c7de7eb431
commit 6dc9084ec7
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
5 changed files with 502 additions and 0 deletions

View file

@ -20,6 +20,7 @@
<file compressed="true" preprocess="xml-stripblanks" alias="components-audio-player.ui">ui/components-audio-player.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-auth-dialog.ui">ui/components-auth-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-avatar.ui">ui/components-avatar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-editable-avatar.ui">ui/components-editable-avatar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-loading-listbox-row.ui">ui/components-loading-listbox-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-reaction-chooser.ui">ui/components-reaction-chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="components-video-player.ui">ui/components-video-player.ui</file>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="ComponentsEditableAvatar" parent="AdwBin">
<child>
<object class="GtkOverlay">
<property name="halign">center</property>
<child>
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">default</property>
<property name="child">
<object class="ComponentsAvatar">
<property name="size">128</property>
<property name="item" bind-source="ComponentsEditableAvatar" bind-property="avatar" bind-flags="sync-create"/>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">temp</property>
<property name="child">
<object class="AdwAvatar">
<property name="size">128</property>
<property name="show-initials">true</property>
<binding name="text">
<lookup name="display-name">
<lookup name="avatar">
ComponentsEditableAvatar
</lookup>
</lookup>
</binding>
<property name="custom-image" bind-source="ComponentsEditableAvatar" bind-property="temp-image" bind-flags="sync-create"/>
</object>
</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="AdwBin">
<style>
<class name="cutout-button" />
</style>
<property name="visible" bind-source="ComponentsEditableAvatar" bind-property="removable" bind-flags="sync-create"/>
<property name="halign">end</property>
<property name="valign">start</property>
<child>
<object class="ComponentsActionButton" id="button_remove">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">editable-avatar.remove-avatar</property>
<property name="state" bind-source="ComponentsEditableAvatar" bind-property="remove-state" bind-flags="sync-create"/>
<property name="sensitive" bind-source="ComponentsEditableAvatar" bind-property="remove-sensitive" bind-flags="sync-create"/>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="AdwBin">
<style>
<class name="cutout-button" />
</style>
<property name="visible" bind-source="ComponentsEditableAvatar" bind-property="editable" bind-flags="sync-create"/>
<property name="halign">end</property>
<property name="valign">end</property>
<child>
<object class="ComponentsActionButton">
<property name="icon-name">document-edit-symbolic</property>
<property name="action-name">editable-avatar.edit-avatar</property>
<property name="state" bind-source="ComponentsEditableAvatar" bind-property="edit-state" bind-flags="sync-create"/>
<property name="sensitive" bind-source="ComponentsEditableAvatar" bind-property="edit-sensitive" bind-flags="sync-create"/>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>

View file

@ -37,6 +37,7 @@ data/resources/ui/qr-code-scanner.ui
# Rust files
src/application.rs
src/components/editable_avatar.rs
src/login.rs
src/secret.rs
src/session/account_settings/devices_page/device_list.rs

View file

@ -0,0 +1,417 @@
use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::{
gdk, gio, glib,
glib::{clone, closure_local},
prelude::*,
subclass::prelude::*,
CompositeTemplate,
};
use log::error;
use super::{ActionButton, ActionState};
use crate::{session::Avatar, spawn};
mod imp {
use std::cell::{Cell, RefCell};
use glib::subclass::{InitializingObject, Signal};
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/components-editable-avatar.ui")]
pub struct EditableAvatar {
/// The avatar to display.
pub avatar: RefCell<Option<Avatar>>,
/// Whether this avatar is changeable.
pub editable: Cell<bool>,
/// The state of the avatar edit.
pub edit_state: Cell<ActionState>,
/// Whether the edit button is sensitive.
pub edit_sensitive: Cell<bool>,
/// Whether this avatar is removable.
pub removable: Cell<bool>,
/// The state of the avatar removal.
pub remove_state: Cell<ActionState>,
/// Whether the remove button is sensitive.
pub remove_sensitive: Cell<bool>,
/// A temporary image to show instead of the avatar.
pub temp_image: RefCell<Option<gdk::Paintable>>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub button_remove: TemplateChild<ActionButton>,
}
#[glib::object_subclass]
impl ObjectSubclass for EditableAvatar {
const NAME: &'static str = "ComponentsEditableAvatar";
type Type = super::EditableAvatar;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
ActionButton::static_type();
Self::bind_template(klass);
klass.install_action("editable-avatar.edit-avatar", None, |obj, _, _| {
spawn!(clone!(@weak obj => async move {
obj.choose_avatar().await;
}));
});
klass.install_action("editable-avatar.remove-avatar", None, |obj, _, _| {
obj.emit_by_name::<()>("remove-avatar", &[]);
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for EditableAvatar {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder(
"edit-avatar",
&[gio::File::static_type().into()],
<()>::static_type().into(),
)
.build(),
Signal::builder("remove-avatar", &[], <()>::static_type().into()).build(),
]
});
SIGNALS.as_ref()
}
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::new(
"avatar",
"Avatar",
"The Avatar to display",
Avatar::static_type(),
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecBoolean::new(
"editable",
"Editable",
"Whether this avatar is editable",
false,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecEnum::new(
"edit-state",
"Edit State",
"The state of the avatar edit",
ActionState::static_type(),
ActionState::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecBoolean::new(
"edit-sensitive",
"Edit Sensitive",
"Whether the edit button is sensitive",
true,
glib::ParamFlags::READWRITE
| glib::ParamFlags::EXPLICIT_NOTIFY
| glib::ParamFlags::CONSTRUCT,
),
glib::ParamSpecBoolean::new(
"removable",
"Removable",
"Whether this avatar is removable",
false,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecEnum::new(
"remove-state",
"Remove State",
"The state of the avatar removal",
ActionState::static_type(),
ActionState::default() as i32,
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
),
glib::ParamSpecBoolean::new(
"remove-sensitive",
"Remove Sensitive",
"Whether the remove button is sensitive",
true,
glib::ParamFlags::READWRITE
| glib::ParamFlags::EXPLICIT_NOTIFY
| glib::ParamFlags::CONSTRUCT,
),
glib::ParamSpecObject::new(
"temp-image",
"Temp Image",
"A temporary image to show instead of the avatar",
gdk::Paintable::static_type(),
glib::ParamFlags::READABLE,
),
]
});
PROPERTIES.as_ref()
}
fn set_property(
&self,
obj: &Self::Type,
_id: usize,
value: &glib::Value,
pspec: &glib::ParamSpec,
) {
match pspec.name() {
"avatar" => obj.set_avatar(value.get().unwrap()),
"editable" => obj.set_editable(value.get().unwrap()),
"edit-state" => obj.set_edit_state(value.get().unwrap()),
"edit-sensitive" => obj.set_edit_sensitive(value.get().unwrap()),
"removable" => obj.set_removable(value.get().unwrap()),
"remove-state" => obj.set_remove_state(value.get().unwrap()),
"remove-sensitive" => obj.set_remove_sensitive(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"avatar" => obj.avatar().to_value(),
"editable" => obj.editable().to_value(),
"edit-state" => obj.edit_state().to_value(),
"edit-sensitive" => obj.edit_sensitive().to_value(),
"removable" => obj.removable().to_value(),
"remove-state" => obj.remove_state().to_value(),
"remove-sensitive" => obj.remove_sensitive().to_value(),
"temp-image" => obj.temp_image().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
self.button_remove.set_extra_classes(&["error"]);
}
}
impl WidgetImpl for EditableAvatar {}
impl BinImpl for EditableAvatar {}
}
glib::wrapper! {
/// An `Avatar` that can be edited.
pub struct EditableAvatar(ObjectSubclass<imp::EditableAvatar>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl EditableAvatar {
pub fn new() -> Self {
glib::Object::new(&[]).expect("Failed to create EditableAvatar")
}
pub fn avatar(&self) -> Option<Avatar> {
self.imp().avatar.borrow().to_owned()
}
pub fn set_avatar(&self, avatar: Option<Avatar>) {
if self.avatar() == avatar {
return;
}
self.imp().avatar.replace(avatar);
self.notify("avatar");
}
pub fn editable(&self) -> bool {
self.imp().editable.get()
}
pub fn set_editable(&self, editable: bool) {
if self.editable() == editable {
return;
}
self.imp().editable.set(editable);
self.notify("editable");
}
pub fn edit_state(&self) -> ActionState {
self.imp().edit_state.get()
}
pub fn set_edit_state(&self, state: ActionState) {
if self.edit_state() == state {
return;
}
self.imp().edit_state.set(state);
self.notify("edit-state");
}
pub fn edit_sensitive(&self) -> bool {
self.imp().edit_sensitive.get()
}
pub fn set_edit_sensitive(&self, sensitive: bool) {
if self.edit_sensitive() == sensitive {
return;
}
self.imp().edit_sensitive.set(sensitive);
self.notify("edit-sensitive");
}
pub fn removable(&self) -> bool {
self.imp().removable.get()
}
pub fn set_removable(&self, removable: bool) {
if self.removable() == removable {
return;
}
self.imp().removable.set(removable);
self.notify("removable");
}
pub fn remove_state(&self) -> ActionState {
self.imp().remove_state.get()
}
pub fn set_remove_state(&self, state: ActionState) {
if self.remove_state() == state {
return;
}
self.imp().remove_state.set(state);
self.notify("remove-state");
}
pub fn remove_sensitive(&self) -> bool {
self.imp().remove_sensitive.get()
}
pub fn set_remove_sensitive(&self, sensitive: bool) {
if self.remove_sensitive() == sensitive {
return;
}
self.imp().remove_sensitive.set(sensitive);
self.notify("remove-sensitive");
}
pub fn temp_image(&self) -> Option<gdk::Paintable> {
self.imp().temp_image.borrow().clone()
}
pub fn set_temp_image_from_file(&self, file: Option<&gio::File>) {
self.imp().temp_image.replace(
file.and_then(|file| gdk::Texture::from_file(file).ok())
.map(|texture| texture.upcast()),
);
self.notify("temp-image");
}
/// Show an avatar with `temp_image` instead of `avatar`.
pub fn show_temp_image(&self, show_temp: bool) {
let stack = &self.imp().stack;
if show_temp {
stack.set_visible_child_name("temp");
} else {
stack.set_visible_child_name("default");
}
}
async fn choose_avatar(&self) {
let image_filter = gtk::FileFilter::new();
image_filter.add_mime_type("image/*");
let dialog = gtk::FileChooserNative::builder()
.title(&gettext("Choose Avatar"))
.modal(true)
.transient_for(
self.root()
.as_ref()
.and_then(|root| root.downcast_ref::<gtk::Window>())
.unwrap(),
)
.action(gtk::FileChooserAction::Open)
.accept_label(&gettext("Choose"))
.cancel_label(&gettext("Cancel"))
.filter(&image_filter)
.build();
if dialog.run_future().await == gtk::ResponseType::Accept {
if let Some(file) = dialog.file() {
if let Some(content_type) = file
.query_info_future(
&gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FileQueryInfoFlags::NONE,
glib::PRIORITY_LOW,
)
.await
.ok()
.and_then(|info| info.content_type())
{
if gio::content_type_is_a(&content_type, "image/*") {
self.emit_by_name::<()>("edit-avatar", &[&file]);
} else {
error!("The chosen file is not an image");
let _ = self.activate_action(
"win.message",
Some(&gettext("The chosen file is not an image").to_variant()),
);
}
} else {
error!("Could not get the content type of the file");
let _ = self.activate_action(
"win.message",
Some(
&gettext("Could not determine the type of the chosen file")
.to_variant(),
),
);
}
} else {
error!("No file chosen");
let _ = self.activate_action(
"win.message",
Some(&gettext("No file was chosen").to_variant()),
);
}
}
}
pub fn connect_edit_avatar<F: Fn(&Self, gio::File) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"edit-avatar",
true,
closure_local!(|obj: Self, file: gio::File| {
f(&obj, file);
}),
)
}
pub fn connect_remove_avatar<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"remove-avatar",
true,
closure_local!(|obj: Self| {
f(&obj);
}),
)
}
}
impl Default for EditableAvatar {
fn default() -> Self {
Self::new()
}
}

View file

@ -5,6 +5,7 @@ mod avatar;
mod badge;
mod context_menu_bin;
mod custom_entry;
mod editable_avatar;
mod in_app_notification;
mod label_with_widgets;
mod loading_listbox_row;
@ -24,6 +25,7 @@ pub use self::{
badge::Badge,
context_menu_bin::{ContextMenuBin, ContextMenuBinExt, ContextMenuBinImpl},
custom_entry::CustomEntry,
editable_avatar::EditableAvatar,
in_app_notification::InAppNotification,
label_with_widgets::LabelWithWidgets,
loading_listbox_row::LoadingListBoxRow,