[fractal/fractal-next] components: Create EditableAvatar
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] components: Create EditableAvatar
- Date: Fri, 25 Feb 2022 09:48:44 +0000 (UTC)
commit 6dc9084ec79f5e6805efbc1ebd654b1859198490
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Tue Feb 8 19:09:55 2022 +0100
components: Create EditableAvatar
data/resources/resources.gresource.xml | 1 +
data/resources/ui/components-editable-avatar.ui | 81 +++++
po/POTFILES.in | 1 +
src/components/editable_avatar.rs | 417 ++++++++++++++++++++++++
src/components/mod.rs | 2 +
5 files changed, 502 insertions(+)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 813d0a753..2ac70f3fd 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -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>
diff --git a/data/resources/ui/components-editable-avatar.ui b/data/resources/ui/components-editable-avatar.ui
new file mode 100644
index 000000000..9c8e48aed
--- /dev/null
+++ b/data/resources/ui/components-editable-avatar.ui
@@ -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>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index fcb9078a5..81faf3cb5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/src/components/editable_avatar.rs b/src/components/editable_avatar.rs
new file mode 100644
index 000000000..76ca68489
--- /dev/null
+++ b/src/components/editable_avatar.rs
@@ -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()
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 86e47298a..9f127fd75 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -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,
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]