diff --git a/src/components/mod.rs b/src/components/mod.rs index a370c501..eba1383e 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -21,6 +21,7 @@ mod overlapping_avatars; mod pill; mod power_level_badge; mod reaction_chooser; +mod removable_row; mod room_title; mod scale_revealer; mod spinner; @@ -56,6 +57,7 @@ pub use self::{ pill::{Pill, PillSource, PillSourceExt, PillSourceImpl, PillSourceRow}, power_level_badge::PowerLevelBadge, reaction_chooser::ReactionChooser, + removable_row::RemovableRow, room_title::RoomTitle, scale_revealer::ScaleRevealer, spinner::Spinner, diff --git a/src/components/removable_row.rs b/src/components/removable_row.rs new file mode 100644 index 00000000..f64026da --- /dev/null +++ b/src/components/removable_row.rs @@ -0,0 +1,123 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{glib, glib::closure_local, CompositeTemplate}; + +use super::SpinnerButton; + +mod imp { + use std::marker::PhantomData; + + use glib::subclass::{InitializingObject, Signal}; + use once_cell::sync::Lazy; + + use super::*; + + #[derive(Debug, Default, CompositeTemplate, glib::Properties)] + #[template(resource = "/org/gnome/Fractal/ui/components/removable_row.ui")] + #[properties(wrapper_type = super::RemovableRow)] + pub struct RemovableRow { + #[template_child] + pub remove_button: TemplateChild, + /// The tooltip text of the remove button. + #[property(get = Self::remove_button_tooltip_text, set = Self::set_remove_button_tooltip_text, explicit_notify, nullable)] + pub remove_button_tooltip_text: PhantomData>, + /// Whether this row is loading. + #[property(get = Self::is_loading, set = Self::set_is_loading, explicit_notify)] + pub is_loading: PhantomData, + } + + #[glib::object_subclass] + impl ObjectSubclass for RemovableRow { + const NAME: &'static str = "RemovableRow"; + type Type = super::RemovableRow; + type ParentType = adw::ActionRow; + + fn class_init(klass: &mut Self::Class) { + Self::bind_template(klass); + Self::Type::bind_template_callbacks(klass); + } + + fn instance_init(obj: &InitializingObject) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for RemovableRow { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("remove").build()]); + SIGNALS.as_ref() + } + } + + impl WidgetImpl for RemovableRow {} + impl ListBoxRowImpl for RemovableRow {} + impl PreferencesRowImpl for RemovableRow {} + impl ActionRowImpl for RemovableRow {} + + impl RemovableRow { + /// The tooltip text of the remove button. + fn remove_button_tooltip_text(&self) -> Option { + self.remove_button.tooltip_text() + } + + /// Set the tooltip text of the remove button. + fn set_remove_button_tooltip_text(&self, tooltip_text: Option) { + if self.remove_button_tooltip_text() == tooltip_text { + return; + } + + self.remove_button.set_tooltip_text(tooltip_text.as_deref()); + self.obj().notify_remove_button_tooltip_text(); + } + + /// Whether this row is loading. + fn is_loading(&self) -> bool { + self.remove_button.loading() + } + + /// Set whether this row is loading. + fn set_is_loading(&self, is_loading: bool) { + if self.is_loading() == is_loading { + return; + } + + self.remove_button.set_loading(is_loading); + + let obj = self.obj(); + obj.set_sensitive(!is_loading); + obj.notify_is_loading(); + } + } +} + +glib::wrapper! { + /// An `AdwActionRow` with a "remove" button. + pub struct RemovableRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow, + @implements gtk::Actionable, gtk::Accessible; +} + +#[gtk::template_callbacks] +impl RemovableRow { + pub fn new() -> Self { + glib::Object::new() + } + + /// Emit the `remove` signal. + #[template_callback] + fn remove(&self) { + self.emit_by_name::<()>("remove", &[]); + } + + /// Connect to the `remove` signal. + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { + self.connect_closure( + "remove", + true, + closure_local!(move |obj: Self| { + f(&obj); + }), + ) + } +} diff --git a/src/components/removable_row.ui b/src/components/removable_row.ui new file mode 100644 index 00000000..1ba9ee4a --- /dev/null +++ b/src/components/removable_row.ui @@ -0,0 +1,17 @@ + + + + diff --git a/src/session/view/account_settings/notifications_page.rs b/src/session/view/account_settings/notifications_page.rs index f8c39f4b..0b916bc9 100644 --- a/src/session/view/account_settings/notifications_page.rs +++ b/src/session/view/account_settings/notifications_page.rs @@ -4,7 +4,9 @@ use gtk::{gio, glib, glib::clone, CompositeTemplate}; use tracing::error; use crate::{ - components::{CheckLoadingRow, EntryAddRow, LoadingBin, Spinner, SwitchLoadingRow}, + components::{ + CheckLoadingRow, EntryAddRow, LoadingBin, RemovableRow, Spinner, SwitchLoadingRow, + }, i18n::gettext_f, session::model::{NotificationsGlobalSetting, NotificationsSettings}, spawn, toast, @@ -351,27 +353,16 @@ impl NotificationsPage { if let Some(string_obj) = item.downcast_ref::() { let keyword = string_obj.string(); - let row = adw::ActionRow::builder() - .title(keyword.clone()) - .selectable(false) - .build(); - - let suffix = LoadingBin::new(); - let remove_button = gtk::Button::builder() - .icon_name("close-symbolic") - .valign(gtk::Align::Center) - .halign(gtk::Align::Center) - .css_classes(["flat"]) - .tooltip_text(gettext_f("Remove “{keyword}”", &[("keyword", &keyword)])) - .build(); - remove_button.connect_clicked(clone!(@weak self as obj, @weak row => move |_| { - obj.remove_keyword(row.title()); + let row = RemovableRow::new(); + row.set_title(&keyword); + row.set_remove_button_tooltip_text(Some(gettext_f( + "Remove “{keyword}”", + &[("keyword", &keyword)], + ))); + + row.connect_remove(clone!(@weak self as obj => move |row| { + obj.remove_keyword(row); })); - suffix.set_child(Some(remove_button)); - - row.add_suffix(&suffix); - // We need to keep track of suffixes to change their loading state. - imp.keywords_suffixes.borrow_mut().insert(keyword, suffix); row.upcast() } else { @@ -380,31 +371,24 @@ impl NotificationsPage { } } - /// Remove the given keyword. - fn remove_keyword(&self, keyword: glib::GString) { + /// Remove the keyword from the given row. + fn remove_keyword(&self, row: &RemovableRow) { let Some(settings) = self.notifications_settings() else { return; }; - let Some(suffix) = self.imp().keywords_suffixes.borrow().get(&keyword).cloned() else { - return; - }; - - suffix.set_is_loading(true); + row.set_is_loading(true); spawn!( - clone!(@weak self as obj, @weak settings, @weak suffix => async move { - if settings.remove_keyword(keyword.to_string()).await.is_err() { + clone!(@weak self as obj, @weak settings, @weak row => async move { + if settings.remove_keyword(row.title().into()).await.is_err() { toast!( obj, gettext("Could not remove notification keyword") ); - } else { - // The row should be removed. - obj.imp().keywords_suffixes.borrow_mut().remove(&keyword); } - suffix.set_is_loading(false); + row.set_is_loading(false); }) ); } diff --git a/src/ui-resources.gresource.xml b/src/ui-resources.gresource.xml index 24fbe732..c496ad2e 100644 --- a/src/ui-resources.gresource.xml +++ b/src/ui-resources.gresource.xml @@ -25,6 +25,7 @@ components/pill/mod.ui components/pill/source_row.ui components/reaction_chooser.ui + components/removable_row.ui components/room_title.ui components/switch_loading_row.ui components/toastable_window.ui