[fractal] utils: Add a macro to create toasts
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal] utils: Add a macro to create toasts
- Date: Tue, 31 May 2022 13:59:57 +0000 (UTC)
commit 3cea24d36d4d2af1fbb8492b7aad13359d8d3f93
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Tue May 31 13:43:55 2022 +0200
utils: Add a macro to create toasts
src/components/label_with_widgets.rs | 2 +-
src/components/mod.rs | 2 +-
src/components/toast.rs | 6 +-
src/i18n.rs | 10 +--
src/session/room/mod.rs | 25 +++---
src/session/user.rs | 7 ++
src/utils.rs | 158 +++++++++++++++++++++++++++++++++++
7 files changed, 186 insertions(+), 24 deletions(-)
---
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
index 737de3cd1..ae3dea646 100644
--- a/src/components/label_with_widgets.rs
+++ b/src/components/label_with_widgets.rs
@@ -2,7 +2,7 @@ use std::cmp::max;
use gtk::{glib, glib::clone, pango, prelude::*, subclass::prelude::*};
-const DEFAULT_PLACEHOLDER: &str = "<widget>";
+pub const DEFAULT_PLACEHOLDER: &str = "<widget>";
const PANGO_SCALE: i32 = 1024;
const OBJECT_REPLACEMENT_CHARACTER: &str = "\u{FFFC}";
fn pango_pixels(d: i32) -> i32 {
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 4c151b617..ffa03ef06 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -36,7 +36,7 @@ pub use self::{
editable_avatar::EditableAvatar,
entry_row::EntryRow,
in_app_notification::InAppNotification,
- label_with_widgets::LabelWithWidgets,
+ label_with_widgets::{LabelWithWidgets, DEFAULT_PLACEHOLDER},
loading_listbox_row::LoadingListBoxRow,
location_viewer::LocationViewer,
media_content_viewer::{ContentType, MediaContentViewer},
diff --git a/src/components/toast.rs b/src/components/toast.rs
index 6581445a2..e7b2c6223 100644
--- a/src/components/toast.rs
+++ b/src/components/toast.rs
@@ -131,12 +131,12 @@ impl ToastBuilder {
Self::default()
}
- pub fn title(mut self, title: &str) -> Self {
- self.title = Some(title.to_owned());
+ pub fn title(mut self, title: String) -> Self {
+ self.title = Some(title);
self
}
- pub fn widgets(mut self, widgets: &[&impl IsA<gtk::Widget>]) -> Self {
+ pub fn widgets(mut self, widgets: &[impl IsA<gtk::Widget>]) -> Self {
self.widgets = Some(widgets.iter().map(|w| w.upcast_ref().clone()).collect());
self
}
diff --git a/src/i18n.rs b/src/i18n.rs
index ea704f949..2daef5f01 100644
--- a/src/i18n.rs
+++ b/src/i18n.rs
@@ -1,14 +1,6 @@
use gettextrs::{gettext, ngettext};
-fn freplace(s: String, args: &[(&str, &str)]) -> String {
- let mut s = s;
-
- for (k, v) in args {
- s = s.replace(&format!("{{{}}}", k), v);
- }
-
- s
-}
+use crate::utils::freplace;
/// Like `gettext`, but replaces named variables with the given dictionary.
///
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index 412af38df..0b1b507eb 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -463,8 +463,8 @@ impl Room {
let room_pill = Pill::for_room(&obj);
let error = Toast::builder()
// Translators: Do NOT translate the content between '{' and '}', this is a
variable name.
- .title(&gettext_f("Failed to forget {room}.", &[("room", "<widget>")]))
- .widgets(&[&room_pill])
+ .title(gettext_f("Failed to forget {room}.", &[("room", "<widget>")]))
+ .widgets(&[room_pill])
.build();
if let Some(window) = obj.session().parent_window() {
@@ -703,12 +703,12 @@ impl Room {
let room_pill = Pill::for_room(&obj);
let error = Toast::builder()
- .title(&gettext_f(
+ .title(gettext_f(
// Translators: Do NOT translate the content between '{' and '}',
this is a variable name.
"Failed to move {room} from {previous_category} to {new_category}.",
&[("room", "<widget>"),("previous_category",
&previous_category.to_string()), ("new_category", &category.to_string())],
))
- .widgets(&[&room_pill])
+ .widgets(&[room_pill])
.build();
if let Some(window) = obj.session().parent_window() {
@@ -1438,13 +1438,13 @@ impl Room {
let room_pill = Pill::for_room(self);
let error = Toast::builder()
- .title(&gettext_f(
+ .title(gettext_f(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Failed to accept invitation for {room}. Try again later.",
&[("room", "<widget>")],
))
- .widgets(&[&room_pill])
+ .widgets(&[room_pill])
.build();
if let Some(window) = self.session().parent_window() {
@@ -1472,13 +1472,13 @@ impl Room {
let room_pill = Pill::for_room(self);
let error = Toast::builder()
- .title(&gettext_f(
+ .title(gettext_f(
// Translators: Do NOT translate the content between '{' and '}', this
// is a variable name.
"Failed to reject invitation for {room}. Try again later.",
&[("room", "<widget>")],
))
- .widgets(&[&room_pill])
+ .widgets(&[room_pill])
.build();
if let Some(window) = self.session().parent_window() {
@@ -1662,8 +1662,8 @@ impl Room {
let user_pill = Pill::for_user(first_failed);
let room_pill = Pill::for_room(self);
let error = Toast::builder()
- .title(&error_message)
- .widgets(&[&user_pill, &room_pill])
+ .title(error_message)
+ .widgets(&[user_pill, room_pill])
.build();
if let Some(window) = self.session().parent_window() {
@@ -1769,6 +1769,11 @@ impl Room {
None
})
}
+
+ /// Get a `Pill` representing this `Room`.
+ pub fn to_pill(&self) -> Pill {
+ Pill::for_room(self)
+ }
}
/// Whether the given event can count as an unread message.
diff --git a/src/session/user.rs b/src/session/user.rs
index b94eb6436..0c62129f8 100644
--- a/src/session/user.rs
+++ b/src/session/user.rs
@@ -6,6 +6,7 @@ use matrix_sdk::{
};
use crate::{
+ components::Pill,
session::{
verification::{IdentityVerification, VerificationState},
Avatar, Session,
@@ -265,6 +266,12 @@ pub trait UserExt: IsA<User> {
UserActions::NONE
}
}
+
+ /// Get a `Pill` representing this `User`.
+ fn to_pill(&self) -> Pill {
+ let user = self.upcast_ref();
+ Pill::for_user(user)
+ }
}
impl<T: IsA<User>> UserExt for T {}
diff --git a/src/utils.rs b/src/utils.rs
index b5505b5ba..745aa3ad2 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -26,6 +26,150 @@ macro_rules! spawn_tokio {
};
}
+/// Show a toast with the given message on the ancestor window of `widget`.
+///
+/// The simplest way to use this macros is for displaying a simple message. It
+/// can be anything that implements `AsRef<str>`.
+///
+/// ```ignore
+/// toast!(widget, gettext("Something happened"));
+/// ```
+///
+/// This macro also supports replacing named variables with their value. It
+/// supports both the `var` and the `var = expr` syntax. In this case the
+/// message and the variables must be `String`s.
+///
+/// ```ignore
+/// toast!(
+/// widget,
+/// gettext("Error number {n}: {msg}"),
+/// n = error_nb.to_string(),
+/// msg,
+/// );
+/// ```
+///
+/// To add `Pill`s to the toast, you can precede a [`Room`] or [`User`] with
+/// `@`.
+///
+/// ```ignore
+/// let room = Room::new(session, room_id);
+/// let member = Member::new(room, user_id);
+///
+/// toast!(
+/// widget,
+/// gettext("Could not contact {user} in {room}",
+/// @user = member,
+/// @room,
+/// );
+/// ```
+///
+/// For this macro to work, the ancestor window be a [`Window`](crate::Window)
+/// or an [`adw::PreferencesWindow`].
+///
+/// [`Room`]: crate::session::room::Room
+/// [`User`]: crate::session::user::User
+#[macro_export]
+macro_rules! toast {
+ ($widget:expr, $message:expr) => {
+ {
+ let message = $message;
+ if let Some(root) = $widget.root() {
+ if let Some(window) = root.downcast_ref::<$crate::Window>() {
+ window.add_toast(&$crate::components::Toast::new(message.as_ref()));
+ } else if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
+ use adw::prelude::PreferencesWindowExt;
+ window.add_toast(&adw::Toast::new(message.as_ref()));
+ } else {
+ log::error!("Trying to display a toast when the parent doesn't support it");
+ }
+ } else {
+ log::warn!("Could not display toast with message: {message}");
+ }
+ }
+ };
+ ($widget:expr, $message:expr, $($tail:tt)+) => {
+ {
+ let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+);
+ let string_dict: Vec<_> = string_vars
+ .iter()
+ .map(|(key, val): &(&str, String)| (key.as_ref(), val.as_ref()))
+ .collect();
+ let message = $crate::utils::freplace($message.into(), &*string_dict);
+
+ if let Some(root) = $widget.root() {
+ if pill_vars.is_empty() {
+ if let Some(window) = root.downcast_ref::<$crate::Window>() {
+ window.add_toast(&$crate::components::Toast::new(&message));
+ } else if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
+ use adw::prelude::PreferencesWindowExt;
+ window.add_toast(&adw::Toast::new(&message));
+ } else {
+ log::error!("Trying to display a toast when the parent doesn't support it");
+ }
+ } else if let Some(window) = root.downcast_ref::<$crate::Window>() {
+ let pill_vars = std::collections::HashMap::<&str,
$crate::components::Pill>::from(pill_vars);
+ let mut swapped_label = String::new();
+ let mut widgets = Vec::with_capacity(pill_vars.len());
+ let mut last_end = 0;
+
+ let mut matches = pill_vars
+ .keys()
+ .map(|key: &&str| {
+ message
+ .match_indices(&format!("{{{key}}}"))
+ .map(|(start, _)| (start, key))
+ .collect::<Vec<_>>()
+ })
+ .flatten()
+ .collect::<Vec<_>>();
+ matches.sort_unstable();
+
+ for (start, key) in matches {
+ swapped_label.push_str(&message[last_end..start]);
+ swapped_label.push_str($crate::components::DEFAULT_PLACEHOLDER);
+ last_end = start + key.len() + 2;
+ widgets.push(pill_vars.get(key).unwrap().clone())
+ }
+ swapped_label.push_str(&message[last_end..message.len()]);
+
+ let toast = $crate::components::Toast::builder()
+ .title(swapped_label)
+ .widgets(&widgets)
+ .build();
+ window.add_toast(&toast);
+ } else {
+ log::error!("Trying to display a toast with pills when the parent doesn't support it");
+ }
+ } else {
+ log::warn!("Could not display toast with message: {message}");
+ }
+ }
+ };
+}
+#[doc(hidden)]
+#[macro_export]
+macro_rules! _toast_accum {
+ ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident, $($tail:tt)*) => {
+ $crate::_toast_accum!([$($string_vars)* (stringify!($var), $var),], [$($pill_vars)*], $($tail)*)
+ };
+ ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => {
+ $crate::_toast_accum!([$($string_vars)* (stringify!($var), $val),], [$($pill_vars)*], $($tail)*)
+ };
+ ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident, $($tail:tt)*) => {
+ {
+ let pill: $crate::components::Pill = $var.to_pill();
+ $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
+ }
+ };
+ ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
+ {
+ let pill: $crate::components::Pill = $val.to_pill();
+ $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
+ }
+ };
+ ([$($string_vars:tt)*], [$($pill_vars:tt)*],) => { ([$($string_vars)*], [$($pill_vars)*]) };
+}
+
use std::{convert::TryInto, path::PathBuf, str::FromStr};
use gettextrs::gettext;
@@ -284,3 +428,17 @@ pub fn validate_password(password: &str) -> PasswordValidity {
validity
}
+
+/// Replace variables in the given string with the given dictionary.
+///
+/// The expected format to replace is `{name}`, where `name` is the first string
+/// in the dictionary entry tuple.
+pub fn freplace(s: String, args: &[(&str, &str)]) -> String {
+ let mut s = s;
+
+ for (k, v) in args {
+ s = s.replace(&format!("{{{}}}", k), v);
+ }
+
+ s
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]