dialog.rs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. // Copyright 2019-2023 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. #![allow(unused_imports)]
  5. use super::{InvokeContext, InvokeResponse};
  6. use crate::Runtime;
  7. #[cfg(any(dialog_open, dialog_save))]
  8. use crate::{api::dialog::blocking::FileDialogBuilder, Manager, Scopes};
  9. use serde::{Deserialize, Deserializer};
  10. use tauri_macros::{command_enum, module_command_handler, CommandModule};
  11. use std::path::PathBuf;
  12. macro_rules! message_dialog {
  13. ($fn_name: ident, $allowlist: ident, $button_labels_type: ty, $buttons: expr) => {
  14. #[module_command_handler($allowlist)]
  15. fn $fn_name<R: Runtime>(
  16. context: InvokeContext<R>,
  17. title: Option<String>,
  18. message: String,
  19. level: Option<MessageDialogType>,
  20. button_labels: $button_labels_type,
  21. ) -> super::Result<bool> {
  22. let determine_button = $buttons;
  23. let mut builder = crate::api::dialog::blocking::MessageDialogBuilder::new(
  24. title.unwrap_or_else(|| context.window.app_handle.package_info().name.clone()),
  25. message,
  26. )
  27. .buttons(determine_button(button_labels));
  28. #[cfg(any(windows, target_os = "macos"))]
  29. {
  30. builder = builder.parent(&context.window);
  31. }
  32. if let Some(level) = level {
  33. builder = builder.kind(level.into());
  34. }
  35. Ok(builder.show())
  36. }
  37. };
  38. }
  39. #[allow(dead_code)]
  40. #[derive(Debug, Clone, Deserialize)]
  41. #[serde(rename_all = "camelCase")]
  42. pub struct DialogFilter {
  43. name: String,
  44. extensions: Vec<String>,
  45. }
  46. /// The options for the open dialog API.
  47. #[derive(Debug, Clone, Deserialize)]
  48. #[serde(rename_all = "camelCase")]
  49. pub struct OpenDialogOptions {
  50. /// The title of the dialog window.
  51. pub title: Option<String>,
  52. /// The filters of the dialog.
  53. #[serde(default)]
  54. pub filters: Vec<DialogFilter>,
  55. /// Whether the dialog allows multiple selection or not.
  56. #[serde(default)]
  57. pub multiple: bool,
  58. /// Whether the dialog is a directory selection (`true` value) or file selection (`false` value).
  59. #[serde(default)]
  60. pub directory: bool,
  61. /// The initial path of the dialog.
  62. pub default_path: Option<PathBuf>,
  63. /// If [`Self::directory`] is true, indicates that it will be read recursively later.
  64. /// Defines whether subdirectories will be allowed on the scope or not.
  65. #[serde(default)]
  66. pub recursive: bool,
  67. }
  68. /// The options for the save dialog API.
  69. #[derive(Debug, Clone, Deserialize)]
  70. #[serde(rename_all = "camelCase")]
  71. pub struct SaveDialogOptions {
  72. /// The title of the dialog window.
  73. pub title: Option<String>,
  74. /// The filters of the dialog.
  75. #[serde(default)]
  76. pub filters: Vec<DialogFilter>,
  77. /// The initial path of the dialog.
  78. pub default_path: Option<PathBuf>,
  79. }
  80. /// Types of message, ask and confirm dialogs.
  81. #[non_exhaustive]
  82. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
  83. pub enum MessageDialogType {
  84. /// Information dialog.
  85. Info,
  86. /// Warning dialog.
  87. Warning,
  88. /// Error dialog.
  89. Error,
  90. }
  91. impl<'de> Deserialize<'de> for MessageDialogType {
  92. fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  93. where
  94. D: Deserializer<'de>,
  95. {
  96. let s = String::deserialize(deserializer)?;
  97. Ok(match s.to_lowercase().as_str() {
  98. "info" => MessageDialogType::Info,
  99. "warning" => MessageDialogType::Warning,
  100. "error" => MessageDialogType::Error,
  101. _ => MessageDialogType::Info,
  102. })
  103. }
  104. }
  105. #[cfg(any(dialog_message, dialog_ask, dialog_confirm))]
  106. impl From<MessageDialogType> for crate::api::dialog::MessageDialogKind {
  107. fn from(kind: MessageDialogType) -> Self {
  108. match kind {
  109. MessageDialogType::Info => Self::Info,
  110. MessageDialogType::Warning => Self::Warning,
  111. MessageDialogType::Error => Self::Error,
  112. }
  113. }
  114. }
  115. /// The API descriptor.
  116. #[command_enum]
  117. #[derive(Deserialize, CommandModule)]
  118. #[serde(tag = "cmd", rename_all = "camelCase")]
  119. #[allow(clippy::enum_variant_names)]
  120. pub enum Cmd {
  121. /// The open dialog API.
  122. #[cmd(dialog_open, "dialog > open")]
  123. OpenDialog { options: OpenDialogOptions },
  124. /// The save dialog API.
  125. #[cmd(dialog_save, "dialog > save")]
  126. SaveDialog { options: SaveDialogOptions },
  127. #[cmd(dialog_message, "dialog > message")]
  128. MessageDialog {
  129. title: Option<String>,
  130. message: String,
  131. #[serde(rename = "type")]
  132. level: Option<MessageDialogType>,
  133. #[serde(rename = "buttonLabel")]
  134. button_label: Option<String>,
  135. },
  136. #[cmd(dialog_ask, "dialog > ask")]
  137. AskDialog {
  138. title: Option<String>,
  139. message: String,
  140. #[serde(rename = "type")]
  141. level: Option<MessageDialogType>,
  142. #[serde(rename = "buttonLabels")]
  143. button_label: Option<(String, String)>,
  144. },
  145. #[cmd(dialog_confirm, "dialog > confirm")]
  146. ConfirmDialog {
  147. title: Option<String>,
  148. message: String,
  149. #[serde(rename = "type")]
  150. level: Option<MessageDialogType>,
  151. #[serde(rename = "buttonLabels")]
  152. button_labels: Option<(String, String)>,
  153. },
  154. }
  155. impl Cmd {
  156. #[module_command_handler(dialog_open)]
  157. #[allow(unused_variables)]
  158. fn open_dialog<R: Runtime>(
  159. context: InvokeContext<R>,
  160. options: OpenDialogOptions,
  161. ) -> super::Result<InvokeResponse> {
  162. let mut dialog_builder = FileDialogBuilder::new();
  163. #[cfg(any(windows, target_os = "macos"))]
  164. {
  165. dialog_builder = dialog_builder.set_parent(&context.window);
  166. }
  167. if let Some(title) = options.title {
  168. dialog_builder = dialog_builder.set_title(&title);
  169. }
  170. if let Some(default_path) = options.default_path {
  171. dialog_builder = set_default_path(dialog_builder, default_path);
  172. }
  173. for filter in options.filters {
  174. let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
  175. dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
  176. }
  177. let scopes = context.window.state::<Scopes>();
  178. let res = if options.directory {
  179. if options.multiple {
  180. let folders = dialog_builder.pick_folders();
  181. if let Some(folders) = &folders {
  182. for folder in folders {
  183. scopes
  184. .allow_directory(folder, options.recursive)
  185. .map_err(crate::error::into_anyhow)?;
  186. }
  187. }
  188. folders.into()
  189. } else {
  190. let folder = dialog_builder.pick_folder();
  191. if let Some(path) = &folder {
  192. scopes
  193. .allow_directory(path, options.recursive)
  194. .map_err(crate::error::into_anyhow)?;
  195. }
  196. folder.into()
  197. }
  198. } else if options.multiple {
  199. let files = dialog_builder.pick_files();
  200. if let Some(files) = &files {
  201. for file in files {
  202. scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
  203. }
  204. }
  205. files.into()
  206. } else {
  207. let file = dialog_builder.pick_file();
  208. if let Some(file) = &file {
  209. scopes.allow_file(file).map_err(crate::error::into_anyhow)?;
  210. }
  211. file.into()
  212. };
  213. Ok(res)
  214. }
  215. #[module_command_handler(dialog_save)]
  216. #[allow(unused_variables)]
  217. fn save_dialog<R: Runtime>(
  218. context: InvokeContext<R>,
  219. options: SaveDialogOptions,
  220. ) -> super::Result<Option<PathBuf>> {
  221. let mut dialog_builder = FileDialogBuilder::new();
  222. #[cfg(any(windows, target_os = "macos"))]
  223. {
  224. dialog_builder = dialog_builder.set_parent(&context.window);
  225. }
  226. if let Some(title) = options.title {
  227. dialog_builder = dialog_builder.set_title(&title);
  228. }
  229. if let Some(default_path) = options.default_path {
  230. dialog_builder = set_default_path(dialog_builder, default_path);
  231. }
  232. for filter in options.filters {
  233. let extensions: Vec<&str> = filter.extensions.iter().map(|s| &**s).collect();
  234. dialog_builder = dialog_builder.add_filter(filter.name, &extensions);
  235. }
  236. let scopes = context.window.state::<Scopes>();
  237. let path = dialog_builder.save_file();
  238. if let Some(p) = &path {
  239. scopes.allow_file(p).map_err(crate::error::into_anyhow)?;
  240. }
  241. Ok(path)
  242. }
  243. message_dialog!(
  244. message_dialog,
  245. dialog_message,
  246. Option<String>,
  247. |label: Option<String>| {
  248. label
  249. .map(crate::api::dialog::MessageDialogButtons::OkWithLabel)
  250. .unwrap_or(crate::api::dialog::MessageDialogButtons::Ok)
  251. }
  252. );
  253. message_dialog!(
  254. ask_dialog,
  255. dialog_ask,
  256. Option<(String, String)>,
  257. |labels: Option<(String, String)>| {
  258. labels
  259. .map(|(yes, no)| crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(yes, no))
  260. .unwrap_or(crate::api::dialog::MessageDialogButtons::YesNo)
  261. }
  262. );
  263. message_dialog!(
  264. confirm_dialog,
  265. dialog_confirm,
  266. Option<(String, String)>,
  267. |labels: Option<(String, String)>| {
  268. labels
  269. .map(|(ok, cancel)| {
  270. crate::api::dialog::MessageDialogButtons::OkCancelWithLabels(ok, cancel)
  271. })
  272. .unwrap_or(crate::api::dialog::MessageDialogButtons::OkCancel)
  273. }
  274. );
  275. }
  276. #[cfg(any(dialog_open, dialog_save))]
  277. fn set_default_path(
  278. mut dialog_builder: FileDialogBuilder,
  279. default_path: PathBuf,
  280. ) -> FileDialogBuilder {
  281. // we need to adjust the separator on Windows: https://github.com/tauri-apps/tauri/issues/8074
  282. let default_path: PathBuf = default_path.components().collect();
  283. if default_path.is_file() || !default_path.exists() {
  284. if let (Some(parent), Some(file_name)) = (default_path.parent(), default_path.file_name()) {
  285. if parent.components().count() > 0 {
  286. dialog_builder = dialog_builder.set_directory(parent);
  287. }
  288. dialog_builder = dialog_builder.set_file_name(&file_name.to_string_lossy());
  289. } else {
  290. dialog_builder = dialog_builder.set_directory(default_path);
  291. }
  292. dialog_builder
  293. } else {
  294. dialog_builder.set_directory(default_path)
  295. }
  296. }
  297. #[cfg(test)]
  298. mod tests {
  299. use super::{OpenDialogOptions, SaveDialogOptions};
  300. use quickcheck::{Arbitrary, Gen};
  301. impl Arbitrary for OpenDialogOptions {
  302. fn arbitrary(g: &mut Gen) -> Self {
  303. Self {
  304. filters: Vec::new(),
  305. multiple: bool::arbitrary(g),
  306. directory: bool::arbitrary(g),
  307. default_path: Option::arbitrary(g),
  308. title: Option::arbitrary(g),
  309. recursive: bool::arbitrary(g),
  310. }
  311. }
  312. }
  313. impl Arbitrary for SaveDialogOptions {
  314. fn arbitrary(g: &mut Gen) -> Self {
  315. Self {
  316. filters: Vec::new(),
  317. default_path: Option::arbitrary(g),
  318. title: Option::arbitrary(g),
  319. }
  320. }
  321. }
  322. #[tauri_macros::module_command_test(dialog_open, "dialog > open")]
  323. #[quickcheck_macros::quickcheck]
  324. fn open_dialog(_options: OpenDialogOptions) {}
  325. #[tauri_macros::module_command_test(dialog_save, "dialog > save")]
  326. #[quickcheck_macros::quickcheck]
  327. fn save_dialog(_options: SaveDialogOptions) {}
  328. }