utils: Add a macro to create toasts

This commit is contained in:
Kévin Commaille 2022-05-31 13:43:55 +02:00
parent a6f293ccba
commit 3cea24d36d
No known key found for this signature in database
GPG Key ID: DD507DAE96E8245C
7 changed files with 186 additions and 24 deletions

View File

@ -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 {

View File

@ -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},

View File

@ -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
}

View File

@ -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.
///

View File

@ -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.

View File

@ -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 {}

View File

@ -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
}