Browse Source

utils: Refactor toast macro

Simplify the rules for using the macro:

- The message must always implement `AsRef<str>`,
- The variables values must implement `ToString`,
- Trailing commas are always optional.

Use functions for code that doesn't actually need to be in the macro,
it allows to have linting of the code.
af/unable-to-decryt-styling
Kévin Commaille 12 months ago
parent
commit
c46404cbbf
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 2
      po/POTFILES.skip
  2. 8
      src/components/crypto/identity_setup_view.rs
  3. 3
      src/components/dialogs/room_preview.rs
  4. 20
      src/components/user_page.rs
  5. 4
      src/i18n.rs
  6. 3
      src/login/homeserver_page.rs
  7. 5
      src/login/in_browser_page.rs
  8. 3
      src/login/method_page.rs
  9. 18
      src/login/mod.rs
  10. 3
      src/session/view/account_settings/general_page/log_out_subpage.rs
  11. 18
      src/session/view/account_settings/general_page/mod.rs
  12. 11
      src/session/view/account_settings/notifications_page.rs
  13. 3
      src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs
  14. 14
      src/session/view/account_settings/security_page/import_export_keys_subpage.rs
  15. 15
      src/session/view/content/room_details/addresses_subpage/mod.rs
  16. 12
      src/session/view/content/room_details/edit_details_subpage.rs
  17. 15
      src/session/view/content/room_details/general_page.rs
  18. 5
      src/session/view/content/room_details/invite_subpage/mod.rs
  19. 2
      src/session/view/content/room_details/mod.rs
  20. 3
      src/session/view/content/room_details/permissions/permissions_subpage.rs
  21. 6
      src/session/view/content/room_history/item_row.rs
  22. 18
      src/session/view/content/room_history/message_toolbar/mod.rs
  23. 16
      src/session/view/content/room_history/sender_avatar/mod.rs
  24. 3
      src/session/view/create_room_dialog.rs
  25. 7
      src/session/view/sidebar/row.rs
  26. 186
      src/utils/macros.rs
  27. 8
      src/utils/mod.rs
  28. 241
      src/utils/toast.rs

2
po/POTFILES.skip

@ -1,4 +1,4 @@
# These are files that we don't want to translate
# Please keep this file sorted alphabetically.
src/i18n.rs
src/utils/macros.rs
src/utils/toast.rs

8
src/components/crypto/identity_setup_view.rs

@ -380,8 +380,10 @@ mod imp {
self.send_request_btn.set_is_loading(true);
if let Err(()) = session.verification_list().create(None).await {
let obj = self.obj();
toast!(obj, gettext("Could not send a new verification request"));
toast!(
self.obj(),
gettext("Could not send a new verification request")
);
}
// On success, the verification should be shown automatically.
@ -432,7 +434,7 @@ mod imp {
}
Err(error) => {
error!("Could not bootstrap cross-signing: {error:?}");
toast!(obj, gettext("Could not create the crypto identity",));
toast!(obj, gettext("Could not create the crypto identity"));
}
}

3
src/components/dialogs/room_preview.rs

@ -334,8 +334,7 @@ mod imp {
obj.close();
}
Err(error) => {
let obj = self.obj();
toast!(obj, error);
toast!(self.obj(), error);
self.join_btn.set_is_loading(false);
self.go_back_btn.set_sensitive(true);

20
src/components/user_page.rs

@ -1,5 +1,5 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::{gettext, pgettext};
use gettextrs::{gettext, ngettext, pgettext};
use gtk::{
glib,
glib::{clone, closure_local},
@ -13,8 +13,7 @@ use crate::{
confirm_mute_room_member_dialog, confirm_room_member_destructive_action_dialog,
confirm_set_room_member_power_level_same_as_own_dialog, RoomMemberDestructiveAction,
},
i18n::gettext_f,
ngettext_f,
gettext_f,
prelude::*,
session::model::{Member, Membership, Permissions, Room, User},
toast,
@ -496,8 +495,7 @@ mod imp {
let user_id = member.user_id().clone();
if room.invite(&[user_id]).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not invite user"));
toast!(self.obj(), gettext("Could not invite user"));
}
self.reset_room();
@ -610,8 +608,7 @@ mod imp {
let user_id = member.user_id().clone();
if room.unban(&[(user_id, None)]).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not unban user"));
toast!(self.obj(), gettext("Could not unban user"));
}
self.reset_room();
@ -653,18 +650,17 @@ mod imp {
) {
if let Err(events) = room.redact(&events, reason).await {
let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
let obj = self.obj();
toast!(
obj,
ngettext_f(
self.obj(),
ngettext(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Could not remove 1 message sent by the user",
"Could not remove {n} messages sent by the user",
n,
&[("n", &n.to_string())]
)
),
n,
);
}
}

4
src/i18n.rs

@ -8,7 +8,7 @@ use crate::utils::freplace;
/// in the dictionary entry tuple.
pub fn gettext_f(msgid: &str, args: &[(&str, &str)]) -> String {
let s = gettext(msgid);
freplace(s, args)
freplace(&s, args).into_owned()
}
/// Like `ngettext`, but replaces named variables with the given dictionary.
@ -17,7 +17,7 @@ pub fn gettext_f(msgid: &str, args: &[(&str, &str)]) -> String {
/// in the dictionary entry tuple.
pub fn ngettext_f(msgid: &str, msgid_plural: &str, n: u32, args: &[(&str, &str)]) -> String {
let s = ngettext(msgid, msgid_plural, n);
freplace(s, args)
freplace(&s, args).into_owned()
}
#[cfg(test)]

3
src/login/homeserver_page.rs

@ -264,8 +264,7 @@ mod imp {
/// Show the given error and abort the current login.
fn abort_on_error(&self, error: &str) {
let obj = self.obj();
toast!(obj, error);
toast!(self.obj(), error);
// Drop the client because it is bound to the homeserver.
if let Some(login) = self.login.obj() {

5
src/login/in_browser_page.rs

@ -97,8 +97,7 @@ mod imp {
.await
{
error!("Could not launch URI: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not open URL"));
toast!(self.obj(), gettext("Could not open URL"));
return;
}
@ -225,7 +224,7 @@ mod imp {
// We need to restart the server if the user wants to try again, so let's go
// back to the previous screen.
let _ = self.obj().activate_action("navigation.pop", None);
let _ = obj.activate_action("navigation.pop", None);
}
/// Reset this page.

3
src/login/method_page.rs

@ -196,8 +196,7 @@ mod imp {
}
Err(error) => {
warn!("Could not log in: {error}");
let obj = self.obj();
toast!(obj, error.to_user_facing());
toast!(self.obj(), error.to_user_facing());
}
}

18
src/login/mod.rs

@ -297,8 +297,7 @@ mod imp {
Ok(authorization_data) => authorization_data,
Err(error) => {
warn!("Could not construct OAuth 2.0 authorization URL: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not set up login"));
toast!(self.obj(), gettext("Could not set up login"));
return;
}
};
@ -322,8 +321,7 @@ mod imp {
Ok(response) => response.flows,
Err(error) => {
warn!("Could not get available Matrix login types: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not set up login"));
toast!(self.obj(), gettext("Could not set up login"));
return;
}
};
@ -369,8 +367,7 @@ mod imp {
}
Err(error) => {
warn!("Could not build Matrix SSO URL: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not set up login"));
toast!(self.obj(), gettext("Could not set up login"));
}
}
}
@ -387,8 +384,7 @@ mod imp {
.expect("task was not aborted")
.map_err(|error| {
warn!("Could not spawn local server: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not set up login"));
toast!(self.obj(), gettext("Could not set up login"));
})
}
@ -424,8 +420,7 @@ mod imp {
}
Err(error) => {
warn!("Could not create session: {error}");
let obj = self.obj();
toast!(obj, error.to_user_facing());
toast!(self.obj(), error.to_user_facing());
self.navigation.pop();
}
@ -458,8 +453,7 @@ mod imp {
let session_info = session.info().clone();
if Secret::store_session(session_info).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not store session"));
toast!(self.obj(), gettext("Could not store session"));
}
session.prepare().await;

3
src/session/view/account_settings/general_page/log_out_subpage.rs

@ -139,8 +139,7 @@ mod imp {
if is_logout_page {
self.stack.set_visible_child_name("failed");
} else {
let obj = self.obj();
toast!(obj, error);
toast!(self.obj(), error);
}
}

18
src/session/view/account_settings/general_page/mod.rs

@ -287,8 +287,7 @@ mod imp {
Ok(info) => info,
Err(error) => {
error!("Could not load user avatar file info: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not load file"));
toast!(self.obj(), gettext("Could not load file"));
avatar.reset();
return;
}
@ -298,8 +297,7 @@ mod imp {
Ok((data, _)) => data,
Err(error) => {
error!("Could not load user avatar file: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not load file"));
toast!(self.obj(), gettext("Could not load file"));
avatar.reset();
return;
}
@ -318,8 +316,7 @@ mod imp {
Ok(res) => res.content_uri,
Err(error) => {
error!("Could not upload user avatar: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not upload avatar"));
toast!(self.obj(), gettext("Could not upload avatar"));
avatar.reset();
return;
}
@ -350,8 +347,7 @@ mod imp {
if weak_action.is_ongoing() {
self.changing_avatar.take();
error!("Could not change user avatar: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change avatar"));
toast!(self.obj(), gettext("Could not change avatar"));
avatar.reset();
}
}
@ -456,8 +452,7 @@ mod imp {
entry.set_sensitive(true);
button.set_visible(false);
button.set_state(ActionState::Confirm);
let obj = self.obj();
toast!(obj, gettext("Name changed successfully"));
toast!(self.obj(), gettext("Name changed successfully"));
}
/// Change the display name of the user.
@ -507,8 +502,7 @@ mod imp {
if weak_action.is_ongoing() {
self.changing_display_name.take();
error!("Could not change user display name: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change display name"));
toast!(self.obj(), gettext("Could not change display name"));
button.set_state(ActionState::Retry);
entry.add_css_class("error");
entry.set_sensitive(true);

11
src/session/view/account_settings/notifications_page.rs

@ -271,8 +271,7 @@ mod imp {
} else {
gettext("Could not disable account notifications")
};
let obj = self.obj();
toast!(obj, msg);
toast!(self.obj(), msg);
}
self.set_account_loading(false);
@ -317,10 +316,9 @@ mod imp {
self.set_global_loading(true, setting);
if settings.set_global_setting(setting).await.is_err() {
let obj = self.obj();
toast!(
obj,
gettext("Could not change global notifications setting")
self.obj(),
gettext("Could not change global notifications setting"),
);
}
@ -438,8 +436,7 @@ mod imp {
let keyword = self.keywords_add_row.text().into();
if settings.add_keyword(keyword).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not add notification keyword"));
toast!(self.obj(), gettext("Could not add notification keyword"));
} else {
// Adding the keyword was successful, reset the entry.
self.keywords_add_row.set_text("");

3
src/session/view/account_settings/security_page/ignored_users_subpage/ignored_user_row.rs

@ -82,8 +82,7 @@ mod imp {
self.stop_ignoring_button.set_is_loading(true);
if ignored_users.remove(&user_id).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not stop ignoring user"));
toast!(self.obj(), gettext("Could not stop ignoring user"));
self.stop_ignoring_button.set_is_loading(false);
}
}

14
src/session/view/account_settings/security_page/import_export_keys_subpage.rs

@ -1,12 +1,10 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gettextrs::{gettext, ngettext};
use gtk::{gio, glib, CompositeTemplate};
use matrix_sdk::encryption::{KeyExportError, RoomKeyImportError};
use tracing::{debug, error};
use crate::{
components::LoadingButtonRow, ngettext_f, session::model::Session, spawn_tokio, toast,
};
use crate::{components::LoadingButtonRow, session::model::Session, spawn_tokio, toast};
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
@ -325,12 +323,12 @@ mod imp {
let n = nb.try_into().unwrap_or(u32::MAX);
toast!(
obj,
ngettext_f(
ngettext(
"Imported 1 room encryption key",
"Imported {n} room encryption keys",
n,
&[("n", &n.to_string())]
)
),
n,
);
}
@ -355,7 +353,7 @@ mod imp {
obj,
gettext(
"The passphrase doesn't match the one used to export the keys."
)
),
);
} else {
error!("Could not import the keys: {error}");

15
src/session/view/content/room_details/addresses_subpage/mod.rs

@ -501,8 +501,7 @@ mod imp {
};
if result.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not remove public address"));
toast!(self.obj(), gettext("Could not remove public address"));
self.public_addresses_list.set_sensitive(true);
row.set_is_loading(false);
}
@ -527,8 +526,7 @@ mod imp {
button.set_is_loading(true);
if aliases.set_canonical_alias(alias).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not set main public address"));
toast!(self.obj(), gettext("Could not set main public address"));
self.public_addresses_list.set_sensitive(true);
button.set_is_loading(false);
}
@ -576,8 +574,7 @@ mod imp {
row.set_text("");
}
Err(error) => {
let obj = self.obj();
toast!(obj, gettext("Could not add public address"));
toast!(self.obj(), gettext("Could not add public address"));
let label = match error {
AddAltAliasError::NotRegistered => {
@ -679,8 +676,7 @@ mod imp {
row.set_is_loading(true);
if aliases.unregister_local_alias(alias).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not unregister local address"));
toast!(self.obj(), gettext("Could not unregister local address"));
}
self.update_local_addresses().await;
@ -750,8 +746,7 @@ mod imp {
row.set_text("");
}
Err(error) => {
let obj = self.obj();
toast!(obj, gettext("Could not register local address"));
toast!(self.obj(), gettext("Could not register local address"));
if let RegisterLocalAliasError::AlreadyInUse = error {
self.local_addresses_error

12
src/session/view/content/room_details/edit_details_subpage.rs

@ -322,8 +322,7 @@ mod imp {
return;
}
let obj = self.obj();
toast!(obj, gettext("Room name saved successfully"));
toast!(self.obj(), gettext("Room name saved successfully"));
// Reset state.
self.changing_name.take();
@ -394,8 +393,7 @@ mod imp {
if weak_action.is_ongoing() {
self.changing_name.take();
error!("Could not change room name: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change room name"));
toast!(self.obj(), gettext("Could not change room name"));
self.name_entry_row.set_sensitive(true);
self.name_button.set_state(ActionState::Retry);
}
@ -433,8 +431,7 @@ mod imp {
return;
}
let obj = self.obj();
toast!(obj, gettext("Room description saved successfully"));
toast!(self.obj(), gettext("Room description saved successfully"));
// Reset state.
self.changing_topic.take();
@ -510,8 +507,7 @@ mod imp {
if weak_action.is_ongoing() {
self.changing_topic.take();
error!("Could not change room description: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change room description"));
toast!(self.obj(), gettext("Could not change room description"));
self.topic_text_view.set_sensitive(true);
self.save_topic_button.set_is_loading(false);
}

15
src/session/view/content/room_details/general_page.rs

@ -626,8 +626,7 @@ mod imp {
.await
.is_err()
{
let obj = imp.obj();
toast!(obj, gettext("Could not change notifications setting"));
toast!(imp.obj(), gettext("Could not change notifications setting"));
}
imp.set_notifications_loading(false, setting);
@ -802,8 +801,7 @@ mod imp {
row.set_read_only(true);
if join_rule.set_value(value).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not change who can join"));
toast!(self.obj(), gettext("Could not change who can join"));
self.update_join_rule();
}
}
@ -851,8 +849,7 @@ mod imp {
if let Err(error) = handle.await.unwrap() {
error!("Could not change guest access: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change guest access"));
toast!(self.obj(), gettext("Could not change guest access"));
self.update_guest_access();
}
}
@ -948,8 +945,7 @@ mod imp {
} else {
gettext("Could not unpublish room from directory")
};
let obj = self.obj();
toast!(obj, text);
toast!(self.obj(), text);
}
self.update_publish().await;
@ -1024,8 +1020,7 @@ mod imp {
if let Err(error) = handle.await.unwrap() {
error!("Could not change room history visibility: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not change who can read history"));
toast!(self.obj(), gettext("Could not change who can read history"));
self.update_history_visibility();
}

5
src/session/view/content/room_details/invite_subpage/mod.rs

@ -220,9 +220,8 @@ mod imp {
let first_failed =
invite_list.first_invitee().map(|item| item.user()).unwrap();
let obj = self.obj();
toast!(
obj,
self.obj(),
ngettext(
// Translators: Do NOT translate the content between '{' and '}', these
// are variable names.
@ -232,7 +231,7 @@ mod imp {
),
@user = first_failed,
@room,
n = n.to_string(),
n,
);
}
}

2
src/session/view/content/room_details/mod.rs

@ -129,7 +129,7 @@ mod imp {
obj.pop_subpage();
toast!(
obj,
gettext("The user is not in the room members list anymore")
gettext("The user is not in the room members list anymore"),
);
}
));

3
src/session/view/content/room_details/permissions/permissions_subpage.rs

@ -593,8 +593,7 @@ mod imp {
};
if permissions.set_power_levels(power_levels).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not save permissions"));
toast!(self.obj(), gettext("Could not save permissions"));
self.save_button.set_is_loading(false);
}
}

6
src/session/view/content/room_history/item_row.rs

@ -936,8 +936,7 @@ mod imp {
};
if event.room().toggle_reaction(key, &event).await.is_err() {
let obj = self.obj();
toast!(obj, gettext("Could not toggle reaction"));
toast!(self.obj(), gettext("Could not toggle reaction"));
}
}
@ -1011,8 +1010,7 @@ mod imp {
if let Err(error) = handle.await.unwrap() {
error!("Could not discard local event: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not discard message"));
toast!(self.obj(), gettext("Could not discard message"));
}
}
}

18
src/session/view/content/room_history/message_toolbar/mod.rs

@ -520,8 +520,7 @@ mod imp {
if let Err(error) = handle.await.expect("task was not aborted") {
error!("Could not send reply: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not send reply"));
toast!(self.obj(), gettext("Could not send reply"));
}
}
Some(RelationInfo::Edit(event_id)) => {
@ -537,8 +536,7 @@ mod imp {
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send edit: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not send edit"));
toast!(self.obj(), gettext("Could not send edit"));
}
}
_ => {
@ -549,8 +547,7 @@ mod imp {
});
if let Err(error) = handle.await.unwrap() {
error!("Could not send message: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not send message"));
toast!(self.obj(), gettext("Could not send message"));
}
}
}
@ -679,8 +676,7 @@ mod imp {
if let Err(error) = handle.await.unwrap() {
error!("Could not send location: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not send location"));
toast!(self.obj(), gettext("Could not send location"));
}
}
@ -692,8 +688,7 @@ mod imp {
LocationError::Other => gettext("Could not retrieve current location"),
};
let obj = self.obj();
toast!(obj, msg);
toast!(self.obj(), msg);
}
/// Send the attachment with the given data.
@ -721,8 +716,7 @@ mod imp {
if let Err(error) = handle.await.unwrap() {
error!("Could not send file: {error}");
let obj = self.obj();
toast!(obj, gettext("Could not send file"));
toast!(self.obj(), gettext("Could not send file"));
}
}

16
src/session/view/content/room_history/sender_avatar/mod.rs

@ -1,5 +1,5 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gettextrs::{gettext, ngettext};
use gtk::{gdk, glib, glib::clone, CompositeTemplate};
use ruma::{events::room::power_levels::PowerLevelUserAction, OwnedEventId};
@ -8,7 +8,7 @@ use crate::{
confirm_mute_room_member_dialog, confirm_room_member_destructive_action_dialog, Avatar,
RoomMemberDestructiveAction, UserProfileDialog,
},
gettext_f, ngettext_f,
gettext_f,
prelude::*,
session::{
model::{Member, MemberRole, Membership, User},
@ -753,14 +753,14 @@ mod imp {
let n = u32::try_from(events.len()).unwrap_or(u32::MAX);
toast!(
obj,
ngettext_f(
ngettext(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Removing 1 message sent by the user…",
"Removing {n} messages sent by the user…",
n,
&[("n", &n.to_string())]
)
),
n,
);
let room = sender.room();
@ -769,14 +769,14 @@ mod imp {
let n = u32::try_from(failed_events.len()).unwrap_or(u32::MAX);
toast!(
obj,
ngettext_f(
ngettext(
// Translators: Do NOT translate the content between '{' and '}',
// this is a variable name.
"Could not remove 1 message sent by the user",
"Could not remove {n} messages sent by the user",
n,
&[("n", &n.to_string())]
)
),
n,
);
}
}

3
src/session/view/create_room_dialog.rs

@ -238,8 +238,7 @@ mod imp {
}
}
let obj = self.obj();
toast!(obj, error.to_user_facing());
toast!(self.obj(), error.to_user_facing());
}
}
}

7
src/session/view/sidebar/row.rs

@ -766,9 +766,8 @@ mod imp {
/// Forget the given room.
async fn forget_room(&self, room: &Room) {
if room.forget().await.is_err() {
let obj = self.obj();
toast!(
obj,
self.obj(),
// Translators: Do NOT translate the content between '{' and '}', this is a variable name.
gettext("Could not forget {room}"),
@room,
@ -788,12 +787,12 @@ mod imp {
error!("Could not mark room as direct chat: {error}");
// Translators: Do NOT translate the content between '{' and '}', this is a
// variable name.
toast!(obj, gettext("Could not mark {room} as direct chat"), @room,);
toast!(obj, gettext("Could not mark {room} as direct chat"), @room);
} else {
error!("Could not unmark room as direct chat: {error}");
// Translators: Do NOT translate the content between '{' and '}', this is a
// variable name.
toast!(obj, gettext("Could not unmark {room} as direct chat"), @room,);
toast!(obj, gettext("Could not unmark {room} as direct chat"), @room);
}
}
}

186
src/utils/macros.rs

@ -1,11 +1,10 @@
//! Collection of macros.
/// Spawn a future on the default `MainContext`
/// Spawn a local future on the default `GMainContext`.
///
/// This was taken from `gtk-macros`
/// but allows setting optionally the priority
/// A custom [`glib::Priority`] can be set as the first argument.
///
/// FIXME: this should maybe be upstreamed
/// [`glib::Priority`]: gtk::glib::Priority
#[macro_export]
macro_rules! spawn {
($future:expr) => {
@ -18,187 +17,10 @@ macro_rules! spawn {
};
}
/// Spawn a future on the tokio runtime
/// Spawn a future on the tokio runtime.
#[macro_export]
macro_rules! spawn_tokio {
($future:expr) => {
$crate::RUNTIME.spawn($future)
};
}
/// 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>`.
///
/// ```no_run
/// use gettextts::gettext;
///
/// use crate::toast;
///
/// # let widget = unimplemented!();
/// 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.
///
/// ```no_run
/// use gettextts::gettext;
///
/// use crate::toast;
///
/// # let widget = unimplemented!();
/// # let error_nb = 0;
/// 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
/// `@`.
///
/// ```no_run
/// use gettextts::gettext;
/// use crate::toast;
/// use crate::session::model::{Room, User};
///
/// # let session = unimplemented!();
/// # let room_id = unimplemented!();
/// # let user_id = unimplemented!();
/// 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::model::Room
/// [`User`]: crate::session::model::User
#[macro_export]
macro_rules! toast {
($widget:expr, $message:expr) => {
{
$crate::_add_toast!($widget, adw::Toast::new($message.as_ref()));
}
};
($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);
let toast = if pill_vars.is_empty() {
adw::Toast::new($message.as_ref())
} else {
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::LabelWithWidgets::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 widget = $crate::components::LabelWithWidgets::new();
widget.set_label_and_widgets(
swapped_label,
widgets,
);
adw::Toast::builder()
.custom_title(&widget)
.build()
};
$crate::_add_toast!($widget, toast);
}
};
}
#[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)*) => {
{
use $crate::components::PillSourceExt;
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)*) => {
{
use $crate::components::PillSourceExt;
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)*]) };
}
#[doc(hidden)]
#[macro_export]
macro_rules! _add_toast {
($widget:expr, $toast:expr) => {{
use gtk::prelude::WidgetExt;
if let Some(dialog) = $widget
.ancestor($crate::components::ToastableDialog::static_type())
.and_downcast::<$crate::components::ToastableDialog>()
{
use $crate::prelude::ToastableDialogExt;
dialog.add_toast($toast);
} else if let Some(dialog) = $widget
.ancestor(adw::PreferencesDialog::static_type())
.and_downcast::<adw::PreferencesDialog>()
{
use adw::prelude::PreferencesDialogExt;
dialog.add_toast($toast);
} else if let Some(root) = $widget.root() {
// FIXME: AdwPreferencesWindow is deprecated but RoomDetails uses it.
#[allow(deprecated)]
if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
use adw::prelude::PreferencesWindowExt;
window.add_toast($toast);
} else if let Some(window) = root.downcast_ref::<$crate::Window>() {
window.add_toast($toast);
} else {
panic!("Trying to display a toast when the parent doesn't support it");
}
}
}};
}

8
src/utils/mod.rs

@ -1,6 +1,7 @@
//! Collection of common methods and types.
use std::{
borrow::Cow,
cell::{Cell, OnceCell, RefCell},
fmt, fs,
io::{self, Write},
@ -30,6 +31,7 @@ mod single_item_list_model;
pub(crate) mod sourceview;
pub(crate) mod string;
mod template_callbacks;
pub(crate) mod toast;
pub(crate) use self::{
dummy_object::DummyObject,
@ -86,11 +88,11 @@ pub(crate) async fn timeout_future<T>(
///
/// The expected format to replace is `{name}`, where `name` is the first string
/// in the dictionary entry tuple.
pub(crate) fn freplace(s: String, args: &[(&str, &str)]) -> String {
let mut s = s;
pub(crate) fn freplace<'a>(s: &'a str, args: &[(&str, &str)]) -> Cow<'a, str> {
let mut s = Cow::Borrowed(s);
for (k, v) in args {
s = s.replace(&format!("{{{k}}}"), v);
s = Cow::Owned(s.replace(&format!("{{{k}}}"), v));
}
s

241
src/utils/toast.rs

@ -0,0 +1,241 @@
//! Macros and methods to display toasts in the interface.
use std::collections::HashMap;
use adw::prelude::*;
use super::freplace;
use crate::{
components::{LabelWithWidgets, Pill, ToastableDialog},
prelude::*,
Window,
};
/// Show a toast with the given message on an ancestor of `widget`.
///
/// The simplest way to use this macro is for displaying a simple message. It
/// can be anything that implements `AsRef<str>`.
///
/// ```no_run
/// use gettextts::gettext;
///
/// use crate::toast;
///
/// # let widget = unimplemented!();
/// 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. The variable value must
/// implement `ToString`.
///
/// ```no_run
/// use gettextts::gettext;
///
/// use crate::toast;
///
/// # let widget = unimplemented!();
/// # let error_nb = 0;
/// toast!(
/// widget,
/// gettext("Error number {n}: {msg}"),
/// n = error_nb.to_string(),
/// msg,
/// );
/// ```
///
/// To add [`Pill`]s to the toast, you can precede a type that implements
/// [`PillSource`] with `@`.
///
/// ```no_run
/// use gettextts::gettext;
/// use crate::toast;
/// use crate::session::model::{Room, User};
///
/// # let session = unimplemented!();
/// # let room_id = unimplemented!();
/// # let user_id = unimplemented!();
/// 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 widget must have one of these ancestors that can
/// show toasts:
///
/// - `ToastableDialog`
/// - `AdwPreferencesDialog`
/// - `AdwPreferencesWindow`
/// - `Window`
///
/// [`PillSource`]: crate::components::PillSource
#[macro_export]
macro_rules! toast {
// Without vars, with or without a trailing comma.
($widget:expr, $message:expr $(,)?) => {
{
$crate::utils::toast::add_toast(
$widget.upcast_ref(),
adw::Toast::new($message.as_ref())
);
}
};
// With vars.
($widget:expr, $message:expr, $($tail:tt)+) => {
{
let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+);
$crate::utils::toast::add_toast_with_vars(
$widget.upcast_ref(),
$message.as_ref(),
&string_vars,
&pill_vars.into()
);
}
};
}
/// Macro to accumulate the variables passed to `toast!`.
///
/// Returns a `([(&str, String)],[(&str, Pill)])` tuple. The items in the first
/// array are `(var_name, var_value)` tuples, and the ones in the second array
/// are `(var_name, pill)` tuples.
#[doc(hidden)]
#[macro_export]
macro_rules! _toast_accum {
// `var = val` syntax, without anything after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr) => {
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], $var = $val,)
};
// `var = val` syntax, with a trailing comma or other vars after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => {
$crate::_toast_accum!([$($string_vars)* (stringify!($var), $val.to_string()),], [$($pill_vars)*], $($tail)*)
};
// `var` syntax, with or without a trailing comma and other vars after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident $($tail:tt)*) => {
$crate::_toast_accum!([$($string_vars)* (stringify!($var), $var.to_string()),], [$($pill_vars)*] $($tail)*)
};
// `@var = val` syntax, without anything after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr) => {
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], @$var = $val,)
};
// `@var = val` syntax, with a trailing comma or other vars after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
{
use $crate::components::PillSourceExt;
let pill: $crate::components::Pill = $val.to_pill();
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
}
};
// `@var` syntax, with or without a trailing comma and other vars after.
([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident $($tail:tt)*) => {
{
use $crate::components::PillSourceExt;
let pill: $crate::components::Pill = $var.to_pill();
$crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),] $($tail)*)
}
};
// No more vars, with or without trailing comma.
([$($string_vars:tt)*], [$($pill_vars:tt)*] $(,)?) => { ([$($string_vars)*], [$($pill_vars)*]) };
}
/// Add the given `AdwToast` to the ancestor of the given widget.
///
/// The widget must have one of these ancestors that can show toasts:
///
/// - `ToastableDialog`
/// - `AdwPreferencesDialog`
/// - `AdwPreferencesWindow`
/// - `Window`
pub(crate) fn add_toast(widget: &gtk::Widget, toast: adw::Toast) {
if let Some(dialog) = widget
.ancestor(ToastableDialog::static_type())
.and_downcast::<ToastableDialog>()
{
dialog.add_toast(toast);
} else if let Some(dialog) = widget
.ancestor(adw::PreferencesDialog::static_type())
.and_downcast::<adw::PreferencesDialog>()
{
dialog.add_toast(toast);
} else if let Some(root) = widget.root() {
// FIXME: AdwPreferencesWindow is deprecated but RoomDetails uses it.
#[allow(deprecated)]
if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
use adw::prelude::PreferencesWindowExt;
window.add_toast(toast);
} else if let Some(window) = root.downcast_ref::<Window>() {
window.add_toast(toast);
} else {
panic!("Trying to display a toast when the parent doesn't support it");
}
}
}
/// Add a toast with the given message and variables to the ancestor of the
/// given widget.
///
/// The widget must have one of these ancestors that can show toasts:
///
/// - `ToastableDialog`
/// - `AdwPreferencesDialog`
/// - `AdwPreferencesWindow`
/// - `Window`
pub(crate) fn add_toast_with_vars(
widget: &gtk::Widget,
message: &str,
string_vars: &[(&str, String)],
pill_vars: &HashMap<&str, Pill>,
) {
let string_dict: Vec<_> = string_vars
.iter()
.map(|(key, val)| (*key, val.as_ref()))
.collect();
let message = freplace(message, &string_dict);
let toast = if pill_vars.is_empty() {
adw::Toast::new(&message)
} else {
let mut swapped_label = String::new();
let mut widgets = Vec::with_capacity(pill_vars.len());
let mut last_end = 0;
// Find the locations of the pills in the message.
let mut matches = pill_vars
.keys()
.flat_map(|key| {
message
.match_indices(&format!("{{{key}}}"))
.map(|(start, _)| (start, *key))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
// Sort the locations, so we can insert the pills in the right order.
matches.sort_unstable();
for (start, key) in matches {
swapped_label.push_str(&message[last_end..start]);
swapped_label.push_str(LabelWithWidgets::PLACEHOLDER);
last_end = start + key.len() + 2;
widgets.push(
pill_vars
.get(key)
.expect("match key should be in map")
.clone(),
);
}
swapped_label.push_str(&message[last_end..message.len()]);
let widget = LabelWithWidgets::new();
widget.set_label_and_widgets(swapped_label, widgets);
adw::Toast::builder().custom_title(&widget).build()
};
add_toast(widget, toast);
}
Loading…
Cancel
Save