Browse Source

components: Add Removable row, an action row with a remove button

merge-requests/1461/merge
Kévin Commaille 2 years ago
parent
commit
dac1d7d549
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
  1. 2
      src/components/mod.rs
  2. 123
      src/components/removable_row.rs
  3. 17
      src/components/removable_row.ui
  4. 52
      src/session/view/account_settings/notifications_page.rs
  5. 1
      src/ui-resources.gresource.xml

2
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,

123
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<SpinnerButton>,
/// 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<Option<glib::GString>>,
/// Whether this row is loading.
#[property(get = Self::is_loading, set = Self::set_is_loading, explicit_notify)]
pub is_loading: PhantomData<bool>,
}
#[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<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for RemovableRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
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<glib::GString> {
self.remove_button.tooltip_text()
}
/// Set the tooltip text of the remove button.
fn set_remove_button_tooltip_text(&self, tooltip_text: Option<glib::GString>) {
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<imp::RemovableRow>)
@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<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_closure(
"remove",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}

17
src/components/removable_row.ui

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="RemovableRow" parent="AdwActionRow">
<property name="selectable">False</property>
<child>
<object class="SpinnerButton" id="remove_button">
<property name="content-icon-name">close-symbolic</property>
<property name="valign">center</property>
<property name="halign">center</property>
<signal name="clicked" handler="remove" swapped="true"/>
<style>
<class name="flat"/>
</style>
</object>
</child>
</template>
</interface>

52
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::<gtk::StringObject>() {
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);
})
);
}

1
src/ui-resources.gresource.xml

@ -25,6 +25,7 @@
<file compressed="true" preprocess="xml-stripblanks">components/pill/mod.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/pill/source_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/reaction_chooser.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/removable_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/room_title.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/switch_loading_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">components/toastable_window.ui</file>

Loading…
Cancel
Save