components: Create EditableAvatar
This commit is contained in:
parent
c7de7eb431
commit
6dc9084ec7
5 changed files with 502 additions and 0 deletions
|
@ -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>
|
||||
|
|
81
data/resources/ui/components-editable-avatar.ui
Normal file
81
data/resources/ui/components-editable-avatar.ui
Normal 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>
|
|
@ -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
|
||||
|
|
417
src/components/editable_avatar.rs
Normal file
417
src/components/editable_avatar.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue