diff --git a/.changes/dialog-3-buttons.md b/.changes/dialog-3-buttons.md new file mode 100644 index 0000000000..e2b7645465 --- /dev/null +++ b/.changes/dialog-3-buttons.md @@ -0,0 +1,6 @@ +--- +"dialog": "minor" +"dialog-js": "minor" +--- + +Add support for showing a message dialog with 3 buttons. \ No newline at end of file diff --git a/examples/api/src/views/Dialog.svelte b/examples/api/src/views/Dialog.svelte index 462eecff7e..5aadad5a94 100644 --- a/examples/api/src/views/Dialog.svelte +++ b/examples/api/src/views/Dialog.svelte @@ -44,6 +44,13 @@ await message("Tauri is awesome!"); } + async function msgCustom(result) { + const buttons = { yes: "awesome", no: "amazing", cancel: "stunning" }; + await message(`Tauri is: `, { buttons }) + .then((res) => onMessage(`Tauri is ${res}`)) + .catch(onMessage); + } + function openDialog() { open({ title: "My wonderful open dialog", @@ -136,12 +143,17 @@
- - - - - + +
+ + + + + + + +
\ No newline at end of file diff --git a/plugins/dialog/android/src/main/java/DialogPlugin.kt b/plugins/dialog/android/src/main/java/DialogPlugin.kt index af0467d894..a815e1baa2 100644 --- a/plugins/dialog/android/src/main/java/DialogPlugin.kt +++ b/plugins/dialog/android/src/main/java/DialogPlugin.kt @@ -38,6 +38,7 @@ class MessageOptions { var title: String? = null lateinit var message: String var okButtonLabel: String? = null + var noButtonLabel: String? = null var cancelButtonLabel: String? = null } @@ -169,9 +170,8 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) { return } - val handler = { cancelled: Boolean, value: Boolean -> + val handler = { value: String -> val ret = JSObject() - ret.put("cancelled", cancelled) ret.put("value", value) invoke.resolve(ret) } @@ -183,24 +183,34 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) { if (args.title != null) { builder.setTitle(args.title) } + + val okButtonLabel = args.okButtonLabel ?: "Ok" + builder .setMessage(args.message) - .setPositiveButton( - args.okButtonLabel ?: "OK" - ) { dialog, _ -> + .setPositiveButton(okButtonLabel) { dialog, _ -> dialog.dismiss() - handler(false, true) + handler(okButtonLabel) } .setOnCancelListener { dialog -> dialog.dismiss() - handler(true, false) + handler(args.cancelButtonLabel ?: "Cancel") + } + + if (args.noButtonLabel != null) { + builder.setNeutralButton(args.noButtonLabel) { dialog, _ -> + dialog.dismiss() + handler(args.noButtonLabel!!) } + } + if (args.cancelButtonLabel != null) { builder.setNegativeButton( args.cancelButtonLabel) { dialog, _ -> dialog.dismiss() - handler(false, false) + handler(args.cancelButtonLabel!!) } } + val dialog = builder.create() dialog.show() } diff --git a/plugins/dialog/api-iife.js b/plugins/dialog/api-iife.js index c2e0870c82..a357f2c0bd 100644 --- a/plugins/dialog/api-iife.js +++ b/plugins/dialog/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,yesButtonLabel:i?.okLabel?.toString(),noButtonLabel:i?.cancelLabel?.toString()})},t.confirm=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),cancelButtonLabel:i?.cancelLabel?.toString()})},t.message=async function(t,e){const i="string"==typeof e?{title:e}:e;await n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()})},t.open=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|open",{options:t})},t.save=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|save",{options:t})},t}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}function e(t){if(void 0!==t)return"string"==typeof t?t:"ok"in t&&"cancel"in t?{OkCancelCustom:[t.ok,t.cancel]}:"yes"in t&&"no"in t&&"cancel"in t?{YesNoCancelCustom:[t.yes,t.no,t.cancel]}:"ok"in t?{OkCustom:t.ok}:void 0}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,yesButtonLabel:o?.okLabel?.toString(),noButtonLabel:o?.cancelLabel?.toString()})},t.confirm=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,okButtonLabel:o?.okLabel?.toString(),cancelButtonLabel:o?.cancelLabel?.toString()})},t.message=async function(t,o){const i="string"==typeof o?{title:o}:o;return n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),buttons:e(i?.buttons)})},t.open=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|open",{options:t})},t.save=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|save",{options:t})},t}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})} diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 150be95a00..2130ec8987 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -77,6 +77,72 @@ interface SaveDialogOptions { canCreateDirectories?: boolean } +/** + * Default buttons for a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogDefaultButtons = + | 'Ok' + | 'OkCancel' + | 'YesNo' + | 'YesNoCancel' + +/** + * The Yes, No and Cancel buttons of a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogButtonsYesNoCancel = { + /** The Yes button. */ + yes: string + /** The No button. */ + no: string + /** The Cancel button. */ + cancel: string +} + +/** + * The Ok and Cancel buttons of a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogButtonsOkCancel = { + /** The Ok button. */ + ok: string + /** The Cancel button. */ + cancel: string +} + +/** + * The Ok button of a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogButtonsOk = { + /** The Ok button. */ + ok: string +} + +/** + * Custom buttons for a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogCustomButtons = + | MessageDialogButtonsYesNoCancel + | MessageDialogButtonsOkCancel + | MessageDialogButtonsOk + +/** + * The buttons of a message dialog. + * + * @since 2.3.0 + */ +export type MessageDialogButtons = + | MessageDialogDefaultButtons + | MessageDialogCustomButtons + /** * @since 2.0.0 */ @@ -85,8 +151,41 @@ interface MessageDialogOptions { title?: string /** The kind of the dialog. Defaults to `info`. */ kind?: 'info' | 'warning' | 'error' - /** The label of the confirm button. */ + /** + * The label of the Ok button. + * + * @deprecated Use {@linkcode MessageDialogOptions.buttons} instead. + */ okLabel?: string + /** + * The buttons of the dialog. + * + * @since 2.3.0 + */ + buttons?: MessageDialogButtons +} + +/** + * Internal function to convert the buttons to the Rust type. + */ +function buttonsToRust(buttons: MessageDialogButtons | undefined) { + if (buttons === undefined) { + return undefined + } + + if (typeof buttons === 'string') { + return buttons + } else if ('ok' in buttons && 'cancel' in buttons) { + return { OkCancelCustom: [buttons.ok, buttons.cancel] } + } else if ('yes' in buttons && 'no' in buttons && 'cancel' in buttons) { + return { + YesNoCancelCustom: [buttons.yes, buttons.no, buttons.cancel] + } + } else if ('ok' in buttons) { + return { OkCustom: buttons.ok } + } + + return undefined } interface ConfirmDialogOptions { @@ -202,6 +301,16 @@ async function save(options: SaveDialogOptions = {}): Promise { return await invoke('plugin:dialog|save', { options }) } +/** + * The result of a message dialog. + * + * The result is a string if the dialog has custom buttons, + * otherwise it is one of the default buttons. + * + * @since 2.3.0 + */ +export type MessageDialogResult = 'Yes' | 'No' | 'Ok' | 'Cancel' | (string & {}) + /** * Shows a message dialog with an `Ok` button. * @example @@ -222,13 +331,15 @@ async function save(options: SaveDialogOptions = {}): Promise { async function message( message: string, options?: string | MessageDialogOptions -): Promise { +): Promise { const opts = typeof options === 'string' ? { title: options } : options - await invoke('plugin:dialog|message', { + + return invoke('plugin:dialog|message', { message: message.toString(), title: opts?.title?.toString(), kind: opts?.kind, - okButtonLabel: opts?.okLabel?.toString() + okButtonLabel: opts?.okLabel?.toString(), + buttons: buttonsToRust(opts?.buttons) }) } diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index b3f7e7da6e..710fd0bb25 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -20,6 +20,7 @@ struct MessageDialogOptions: Decodable { var title: String? let message: String var okButtonLabel: String? + var noButtonLabel: String? var cancelButtonLabel: String? } @@ -200,36 +201,38 @@ class DialogPlugin: Plugin { let alert = UIAlertController( title: args.title, message: args.message, preferredStyle: UIAlertController.Style.alert) - let cancelButtonLabel = args.cancelButtonLabel ?? "" - if !cancelButtonLabel.isEmpty { + if let cancelButtonLabel = args.cancelButtonLabel { alert.addAction( UIAlertAction( title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("cancel") - - invoke.resolve([ - "value": false, - "cancelled": false, - ]) - })) + invoke.resolve(["value": cancelButtonLabel]) + } + ) + ) } - let okButtonLabel = args.okButtonLabel ?? (cancelButtonLabel.isEmpty ? "OK" : "") - if !okButtonLabel.isEmpty { + if let noButtonLabel = args.noButtonLabel { alert.addAction( UIAlertAction( - title: okButtonLabel, style: UIAlertAction.Style.default, + title: noButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("ok") - - invoke.resolve([ - "value": true, - "cancelled": false, - ]) - })) + invoke.resolve(["value": noButtonLabel]) + } + ) + ) } + let okButtonLabel = args.okButtonLabel ?? "Ok" + alert.addAction( + UIAlertAction( + title: okButtonLabel, style: UIAlertAction.Style.default, + handler: { (_) -> Void in + invoke.resolve(["value": okButtonLabel]) + } + ) + ) + manager.viewController?.present(alert, animated: true, completion: nil) } } diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index c3caf027fb..5298de9d07 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -9,8 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window}; use tauri_plugin_fs::FsExt; use crate::{ - Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL, - NO, OK, YES, + Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons, + MessageDialogKind, MessageDialogResult, Result, CANCEL, NO, OK, YES, }; #[derive(Serialize)] @@ -248,7 +248,7 @@ fn message_dialog( message: String, kind: Option, buttons: MessageDialogButtons, -) -> bool { +) -> MessageDialogBuilder { let mut builder = dialog.message(message); builder = builder.buttons(buttons); @@ -266,7 +266,7 @@ fn message_dialog( builder = builder.kind(kind); } - builder.blocking_show() + builder } #[command] @@ -277,19 +277,15 @@ pub(crate) async fn message( message: String, kind: Option, ok_button_label: Option, -) -> Result { - Ok(message_dialog( - window, - dialog, - title, - message, - kind, - if let Some(ok_button_label) = ok_button_label { - MessageDialogButtons::OkCustom(ok_button_label) - } else { - MessageDialogButtons::Ok - }, - )) + buttons: Option, +) -> Result { + let buttons = buttons.unwrap_or(if let Some(ok_button_label) = ok_button_label { + MessageDialogButtons::OkCustom(ok_button_label) + } else { + MessageDialogButtons::Ok + }); + + Ok(message_dialog(window, dialog, title, message, kind, buttons).blocking_show_with_result()) } #[command] @@ -302,7 +298,7 @@ pub(crate) async fn ask( yes_button_label: Option, no_button_label: Option, ) -> Result { - Ok(message_dialog( + let dialog = message_dialog( window, dialog, title, @@ -318,7 +314,9 @@ pub(crate) async fn ask( } else { MessageDialogButtons::YesNo }, - )) + ); + + Ok(dialog.blocking_show()) } #[command] @@ -331,7 +329,7 @@ pub(crate) async fn confirm( ok_button_label: Option, cancel_button_label: Option, ) -> Result { - Ok(message_dialog( + let dialog = message_dialog( window, dialog, title, @@ -347,5 +345,7 @@ pub(crate) async fn confirm( } else { MessageDialogButtons::OkCancel }, - )) + ); + + Ok(dialog.blocking_show()) } diff --git a/plugins/dialog/src/desktop.rs b/plugins/dialog/src/desktop.rs index d1a3e8b21a..732e81ec8a 100644 --- a/plugins/dialog/src/desktop.rs +++ b/plugins/dialog/src/desktop.rs @@ -13,7 +13,7 @@ use rfd::{AsyncFileDialog, AsyncMessageDialog}; use serde::de::DeserializeOwned; use tauri::{plugin::PluginApi, AppHandle, Runtime}; -use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, OK}; +use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder}; pub fn init( app: &AppHandle, @@ -115,6 +115,10 @@ impl From for rfd::MessageButtons { MessageDialogButtons::YesNo => Self::YesNo, MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok), MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel), + MessageDialogButtons::YesNoCancel => Self::YesNoCancel, + MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => { + Self::YesNoCancelCustom(yes, no, cancel) + } } } } @@ -208,24 +212,11 @@ pub fn save_file) + Send + 'static>( } /// Shows a message dialog -pub fn show_message_dialog( +pub fn show_message_dialog( dialog: MessageDialogBuilder, - f: F, + callback: F, ) { - use rfd::MessageDialogResult; - - let ok_label = match &dialog.buttons { - MessageDialogButtons::OkCustom(ok) => Some(ok.clone()), - MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()), - _ => None, - }; - let f = move |res| { - f(match res { - MessageDialogResult::Ok | MessageDialogResult::Yes => true, - MessageDialogResult::Custom(s) => ok_label.map_or(s == OK, |ok_label| ok_label == s), - _ => false, - }); - }; + let f = move |res: rfd::MessageDialogResult| callback(res.into()); let handle = dialog.dialog.app_handle().to_owned(); let _ = handle.run_on_main_thread(move || { diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 2ef1c1eade..17d9a829d4 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -216,6 +216,7 @@ pub(crate) struct MessageDialogPayload<'a> { message: &'a String, kind: &'a MessageDialogKind, ok_button_label: Option<&'a str>, + no_button_label: Option<&'a str>, cancel_button_label: Option<&'a str>, } @@ -238,13 +239,17 @@ impl MessageDialogBuilder { #[cfg(mobile)] pub(crate) fn payload(&self) -> MessageDialogPayload<'_> { - let (ok_button_label, cancel_button_label) = match &self.buttons { - MessageDialogButtons::Ok => (Some(OK), None), - MessageDialogButtons::OkCancel => (Some(OK), Some(CANCEL)), - MessageDialogButtons::YesNo => (Some(YES), Some(NO)), - MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), Some(CANCEL)), + let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons { + MessageDialogButtons::Ok => (Some(OK), None, None), + MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)), + MessageDialogButtons::YesNo => (Some(YES), Some(NO), None), + MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)), + MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None), MessageDialogButtons::OkCancelCustom(ok, cancel) => { - (Some(ok.as_str()), Some(cancel.as_str())) + (Some(ok.as_str()), None, Some(cancel.as_str())) + } + MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => { + (Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str())) } }; MessageDialogPayload { @@ -252,6 +257,7 @@ impl MessageDialogBuilder { message: &self.message, kind: &self.kind, ok_button_label, + no_button_label, cancel_button_label, } } @@ -295,16 +301,55 @@ impl MessageDialogBuilder { } /// Shows a message dialog + /// + /// Returns `true` if the user pressed the OK/Yes button, pub fn show(self, f: F) { + let ok_label = match &self.buttons { + MessageDialogButtons::OkCustom(ok) => Some(ok.clone()), + MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()), + MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()), + _ => None, + }; + + show_message_dialog(self, move |res| { + let sucess = match res { + MessageDialogResult::Ok | MessageDialogResult::Yes => true, + MessageDialogResult::Custom(s) => { + ok_label.map_or(s == OK, |ok_label| ok_label == s) + } + _ => false, + }; + + f(sucess) + }) + } + + /// Shows a message dialog and returns the button that was pressed. + /// + /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed. + pub fn show_with_result(self, f: F) { show_message_dialog(self, f) } /// Shows a message dialog. + /// + /// Returns `true` if the user pressed the OK/Yes button, + /// /// This is a blocking operation, /// and should *NOT* be used when running on the main thread context. pub fn blocking_show(self) -> bool { blocking_fn!(self, show) } + + /// Shows a message dialog and returns the button that was pressed. + /// + /// Returns a [`MessageDialogResult`] enum that indicates which button was pressed. + /// + /// This is a blocking operation, + /// and should *NOT* be used when running on the main thread context. + pub fn blocking_show_with_result(self) -> MessageDialogResult { + blocking_fn!(self, show_with_result) + } } #[derive(Debug, Serialize)] pub(crate) struct Filter { diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index b73def4f98..46ea3a2769 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -8,7 +8,7 @@ use tauri::{ AppHandle, Runtime, }; -use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder}; +use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogResult}; #[cfg(target_os = "android")] const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog"; @@ -107,13 +107,11 @@ pub fn save_file) + Send + 'static>( #[derive(Debug, Deserialize)] struct ShowMessageDialogResponse { - #[allow(dead_code)] - cancelled: bool, - value: bool, + value: String, } /// Shows a message dialog -pub fn show_message_dialog( +pub fn show_message_dialog( dialog: MessageDialogBuilder, f: F, ) { @@ -122,6 +120,8 @@ pub fn show_message_dialog( .dialog .0 .run_mobile_plugin::("showMessageDialog", dialog.payload()); - f(res.map(|r| r.value).unwrap_or_default()) + + let res = res.map(|res| res.value.into()); + f(res.unwrap_or_default()) }); } diff --git a/plugins/dialog/src/models.rs b/plugins/dialog/src/models.rs index d6452bce7d..0b2de2c9a3 100644 --- a/plugins/dialog/src/models.rs +++ b/plugins/dialog/src/models.rs @@ -52,7 +52,7 @@ impl Serialize for MessageDialogKind { /// Set of button that will be displayed on the dialog #[non_exhaustive] -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum MessageDialogButtons { #[default] /// A single `Ok` button with OS default dialog text @@ -61,8 +61,49 @@ pub enum MessageDialogButtons { OkCancel, /// 2 buttons `Yes` and `No` with OS default dialog texts YesNo, + /// 3 buttons `Yes`, `No` and `Cancel` with OS default dialog texts + YesNoCancel, /// A single `Ok` button with custom text OkCustom(String), /// 2 buttons `Ok` and `Cancel` with custom texts OkCancelCustom(String, String), + /// 3 buttons `Yes`, `No` and `Cancel` with custom texts + YesNoCancelCustom(String, String, String), +} + +/// Result of a message dialog +#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum MessageDialogResult { + Yes, + No, + Ok, + #[default] + Cancel, + #[serde(untagged)] + Custom(String), +} + +#[cfg(desktop)] +impl From for MessageDialogResult { + fn from(result: rfd::MessageDialogResult) -> Self { + match result { + rfd::MessageDialogResult::Yes => Self::Yes, + rfd::MessageDialogResult::No => Self::No, + rfd::MessageDialogResult::Ok => Self::Ok, + rfd::MessageDialogResult::Cancel => Self::Cancel, + rfd::MessageDialogResult::Custom(s) => Self::Custom(s), + } + } +} + +impl From for MessageDialogResult { + fn from(value: String) -> Self { + match value.as_str() { + "Yes" => Self::Yes, + "No" => Self::No, + "Ok" => Self::Ok, + "Cancel" => Self::Cancel, + _ => Self::Custom(value), + } + } }