1use std::{
2 error::Error,
3 fmt::{Debug, Display, Formatter},
4 ops::Deref,
5 path::PathBuf,
6};
7
8use pyo3::{
9 exceptions::{PyNotImplementedError, PyRuntimeError},
10 prelude::*,
11 pybacked::PyBackedStr,
12 types::{PyDict, PyString},
13};
14use pyo3_utils::from_py_dict::{derive_from_py_dict, FromPyDict as _, NotRequired};
15use tauri::Manager as _;
16use tauri_plugin_dialog::{self as plugin, DialogExt as _};
17
18use crate::{
19 ext_mod::{manager_method_impl, webview::WebviewWindow, ImplManager},
20 tauri_runtime::Runtime,
21 utils::{PyResultExt as _, TauriError},
22};
23
24#[derive(Debug)]
25struct PluginError(plugin::Error);
26
27impl Display for PluginError {
28 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29 Display::fmt(&self.0, f)
30 }
31}
32
33impl Error for PluginError {}
34
35impl From<PluginError> for PyErr {
36 fn from(value: PluginError) -> Self {
37 match value.0 {
38 plugin::Error::Io(e) => e.into(),
39 plugin::Error::Tauri(e) => TauriError::from(e).into(),
40 plugin::Error::Fs(e) => PyRuntimeError::new_err(e.to_string()),
42 non_exhaustive => PyRuntimeError::new_err(format!(
43 "Unimplemented plugin error, please report this to the pytauri developers: {non_exhaustive}"
44 )),
45 }
46 }
47}
48
49impl From<plugin::Error> for PluginError {
50 fn from(value: plugin::Error) -> Self {
51 Self(value)
52 }
53}
54
55type HasWindowHandleAndHasDisplayHandle = Py<WebviewWindow>;
67
68#[pyclass(frozen)]
70#[non_exhaustive]
71pub enum MessageDialogButtons {
72 Ok(),
73 OkCancel(),
74 YesNo(),
75 OkCustom(Py<PyString>),
78 OkCancelCustom(Py<PyString>, Py<PyString>),
79}
80
81impl MessageDialogButtons {
82 fn to_tauri(&self, py: Python<'_>) -> PyResult<plugin::MessageDialogButtons> {
83 let ret = match self {
84 MessageDialogButtons::Ok() => plugin::MessageDialogButtons::Ok,
85 MessageDialogButtons::OkCancel() => plugin::MessageDialogButtons::OkCancel,
86 MessageDialogButtons::YesNo() => plugin::MessageDialogButtons::YesNo,
87 MessageDialogButtons::OkCustom(text) => {
88 plugin::MessageDialogButtons::OkCustom(text.to_cow(py)?.into_owned())
90 }
91 MessageDialogButtons::OkCancelCustom(ok_text, cancel_text) => {
92 plugin::MessageDialogButtons::OkCancelCustom(
94 ok_text.to_cow(py)?.into_owned(),
95 cancel_text.to_cow(py)?.into_owned(),
96 )
97 }
98 };
99 Ok(ret)
100 }
101}
102
103macro_rules! message_dialog_kind_impl {
104 ($ident:ident => : $($variant:ident),*) => {
105 #[pyclass(frozen, eq, eq_int)]
107 #[derive(PartialEq, Clone, Copy)]
108 pub enum $ident {
109 $($variant,)*
110 }
111
112 impl From<$ident> for tauri_plugin_dialog::MessageDialogKind {
113 fn from(val: $ident) -> Self {
114 match val {
115 $($ident::$variant => tauri_plugin_dialog::MessageDialogKind::$variant,)*
116 }
117 }
118 }
119 };
120}
121
122message_dialog_kind_impl!(
123 MessageDialogKind => :
124 Info,
125 Warning,
126 Error
127);
128
129#[non_exhaustive]
131pub struct MessageDialogBuilderArgs {
132 title: NotRequired<String>,
133 parent: NotRequired<HasWindowHandleAndHasDisplayHandle>,
134 buttons: NotRequired<Py<MessageDialogButtons>>,
135 kind: NotRequired<Py<MessageDialogKind>>,
136}
137
138derive_from_py_dict!(MessageDialogBuilderArgs {
139 #[default]
140 title,
141 #[default]
142 parent,
143 #[default]
144 buttons,
145 #[default]
146 kind,
147});
148
149impl MessageDialogBuilderArgs {
150 fn from_kwargs(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Option<Self>> {
151 kwargs
152 .map(MessageDialogBuilderArgs::from_py_dict)
153 .transpose()
154 }
155
156 fn apply_to_builder(
157 self,
158 py: Python<'_>,
159 mut builder: plugin::MessageDialogBuilder<Runtime>,
160 ) -> PyResult<plugin::MessageDialogBuilder<Runtime>> {
161 let Self {
162 title,
163 parent,
164 buttons,
165 kind,
166 } = self;
167
168 if let Some(title) = title.0 {
169 builder = builder.title(title);
170 }
171 if let Some(parent) = parent.0 {
172 builder = builder.parent(&*parent.get().0.inner_ref());
173 }
174 if let Some(buttons) = buttons.0 {
175 builder = builder.buttons(buttons.get().to_tauri(py)?);
176 }
177 if let Some(kind) = kind.0 {
178 builder = builder.kind((*kind.get()).into());
179 }
180
181 Ok(builder)
182 }
183}
184
185#[pyclass(frozen)]
187#[non_exhaustive]
188pub struct MessageDialogBuilder {
191 handle: tauri::AppHandle<Runtime>,
192 message: PyBackedStr,
193}
194
195impl MessageDialogBuilder {
196 fn to_tauri(&self) -> plugin::MessageDialogBuilder<Runtime> {
197 let Self { handle, message } = self;
198 handle.dialog().message(message.deref())
199 }
200}
201
202#[pymethods]
203impl MessageDialogBuilder {
204 #[pyo3(signature = (**kwargs))]
205 fn blocking_show(&self, py: Python<'_>, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<bool> {
206 let args = MessageDialogBuilderArgs::from_kwargs(kwargs)?;
207
208 let mut builder = self.to_tauri();
209 if let Some(args) = args {
210 builder = args.apply_to_builder(py, builder)?;
211 }
212
213 let ret = py.allow_threads(|| builder.blocking_show());
214 Ok(ret)
215 }
216
217 #[pyo3(signature = (handler, /, **kwargs))]
218 fn show(
219 &self,
220 py: Python<'_>,
221 handler: PyObject,
222 kwargs: Option<&Bound<'_, PyDict>>,
223 ) -> PyResult<()> {
224 let args = MessageDialogBuilderArgs::from_kwargs(kwargs)?;
225
226 let mut builder = self.to_tauri();
227 if let Some(args) = args {
228 builder = args.apply_to_builder(py, builder)?;
229 }
230
231 builder.show(move |is_ok| {
233 Python::with_gil(|py| {
234 let handler = handler.bind(py);
235 let result = handler.call1((is_ok,));
236 result.unwrap_unraisable_py_result(py, Some(handler), || {
237 "Python exception occurred in `MessageDialogBuilder::show` handler"
238 });
239 })
240 });
241
242 Ok(())
243 }
244}
245
246pub struct FilePath(plugin::FilePath);
249
250impl From<plugin::FilePath> for FilePath {
251 fn from(value: plugin::FilePath) -> Self {
252 Self(value)
253 }
254}
255
256impl From<FilePath> for plugin::FilePath {
257 fn from(value: FilePath) -> Self {
258 value.0
259 }
260}
261
262impl<'py> IntoPyObject<'py> for &FilePath {
264 type Target = PyAny;
265 type Output = Bound<'py, Self::Target>;
266 type Error = PyErr;
267
268 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
269 let ret = match &self.0 {
270 plugin::FilePath::Url(_) => {
271 return Err(PyNotImplementedError::new_err(
276 "[FilePath::Url] type is only used on Android/iOS, report this to the pytauri developers"
277 ));
278 }
279 plugin::FilePath::Path(path) => {
280 let path: &PathBuf = path;
281 let pyobj: Bound<'_, PyAny> = path.into_pyobject(py)?; pyobj
283 }
284 };
285 Ok(ret)
286 }
287}
288
289impl<'py> IntoPyObject<'py> for FilePath {
290 type Target = PyAny;
291 type Output = Bound<'py, Self::Target>;
292 type Error = PyErr;
293
294 fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
295 (&self).into_pyobject(py)
296 }
297}
298
299#[non_exhaustive]
301pub struct FileDialogBuilderArgs {
302 add_filter: NotRequired<(String, Vec<PyBackedStr>)>,
304 set_directory: NotRequired<PathBuf>,
306 set_file_name: NotRequired<String>,
307 set_parent: NotRequired<HasWindowHandleAndHasDisplayHandle>,
308 set_title: NotRequired<String>,
309 set_can_create_directories: NotRequired<bool>,
310}
311
312derive_from_py_dict!(FileDialogBuilderArgs {
313 #[default]
314 add_filter,
315 #[default]
316 set_directory,
317 #[default]
318 set_file_name,
319 #[default]
320 set_parent,
321 #[default]
322 set_title,
323 #[default]
324 set_can_create_directories,
325});
326
327impl FileDialogBuilderArgs {
328 fn from_kwargs(kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<Option<Self>> {
329 kwargs.map(FileDialogBuilderArgs::from_py_dict).transpose()
330 }
331
332 fn apply_to_builder(
333 self,
334 mut builder: plugin::FileDialogBuilder<Runtime>,
335 ) -> plugin::FileDialogBuilder<Runtime> {
336 let Self {
337 add_filter,
338 set_directory,
339 set_file_name,
340 set_parent,
341 set_title,
342 set_can_create_directories,
343 } = self;
344
345 if let Some((name, extensions)) = add_filter.0 {
346 let extensions = extensions.iter().map(|s| s.deref()).collect::<Vec<_>>();
348 builder = builder.add_filter(name, &extensions);
349 }
350 if let Some(directory) = set_directory.0 {
351 builder = builder.set_directory(directory);
352 }
353 if let Some(file_name) = set_file_name.0 {
354 builder = builder.set_file_name(file_name);
355 }
356 if let Some(parent) = set_parent.0 {
357 builder = builder.set_parent(&*parent.get().0.inner_ref());
358 }
359 if let Some(title) = set_title.0 {
360 builder = builder.set_title(title);
361 }
362 if let Some(can_create_directories) = set_can_create_directories.0 {
363 builder = builder.set_can_create_directories(can_create_directories);
364 }
365
366 builder
367 }
368}
369
370#[pyclass(frozen)]
372#[non_exhaustive]
373pub struct FileDialogBuilder {
376 handle: tauri::AppHandle<Runtime>,
377}
378
379impl FileDialogBuilder {
380 fn to_tauri(&self) -> plugin::FileDialogBuilder<Runtime> {
381 let Self { handle } = self;
382 handle.dialog().file()
383 }
384}
385
386#[pymethods]
387impl FileDialogBuilder {
388 #[pyo3(signature = (handler, /, **kwargs))]
389 fn pick_file(&self, handler: PyObject, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
390 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
391
392 let mut builder = self.to_tauri();
393 if let Some(args) = args {
394 builder = args.apply_to_builder(builder);
395 }
396
397 builder.pick_file(move |file_path| {
399 Python::with_gil(|py| {
400 let file_path = file_path.map(FilePath::from);
401
402 let handler = handler.bind(py);
403 let result = handler.call1((file_path,));
404 result.unwrap_unraisable_py_result(py, Some(handler), || {
405 "Python exception occurred in `FileDialogBuilder::pick_file` handler"
406 });
407 })
408 });
409
410 Ok(())
411 }
412
413 #[pyo3(signature = (**kwargs))]
414 fn blocking_pick_file(
415 &self,
416 py: Python<'_>,
417 kwargs: Option<&Bound<'_, PyDict>>,
418 ) -> PyResult<Option<FilePath>> {
419 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
420
421 let mut builder = self.to_tauri();
422 if let Some(args) = args {
423 builder = args.apply_to_builder(builder);
424 }
425
426 let ret = py.allow_threads(|| builder.blocking_pick_file().map(Into::into));
427 Ok(ret)
428 }
429
430 #[pyo3(signature = (handler, / ,**kwargs))]
431 fn pick_files(&self, handler: PyObject, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
432 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
433
434 let mut builder = self.to_tauri();
435 if let Some(args) = args {
436 builder = args.apply_to_builder(builder);
437 }
438
439 builder.pick_files(move |file_paths| {
441 Python::with_gil(|py| {
442 let file_paths = file_paths
443 .map(|files| files.into_iter().map(FilePath::from).collect::<Vec<_>>());
445
446 let handler = handler.bind(py);
447 let result = handler.call1((file_paths,));
448 result.unwrap_unraisable_py_result(py, Some(handler), || {
449 "Python exception occurred in `FileDialogBuilder::pick_files` handler"
450 });
451 })
452 });
453
454 Ok(())
455 }
456
457 #[pyo3(signature = (**kwargs))]
458 fn blocking_pick_files(
459 &self,
460 py: Python<'_>,
461 kwargs: Option<&Bound<'_, PyDict>>,
462 ) -> PyResult<Option<Vec<FilePath>>> {
463 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
464
465 let mut builder = self.to_tauri();
466 if let Some(args) = args {
467 builder = args.apply_to_builder(builder);
468 }
469
470 let ret = py.allow_threads(|| {
471 builder
472 .blocking_pick_files()
473 .map(|files| files.into_iter().map(Into::into).collect::<Vec<_>>())
475 });
476 Ok(ret)
477 }
478
479 #[pyo3(signature = (handler, / ,**kwargs))]
480 fn pick_folder(&self, handler: PyObject, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
481 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
482
483 let mut builder = self.to_tauri();
484 if let Some(args) = args {
485 builder = args.apply_to_builder(builder);
486 }
487
488 builder.pick_folder(move |file_path| {
490 Python::with_gil(|py| {
491 let file_path = file_path.map(FilePath::from);
492
493 let handler = handler.bind(py);
494 let result = handler.call1((file_path,));
495 result.unwrap_unraisable_py_result(py, Some(handler), || {
496 "Python exception occurred in `FileDialogBuilder::pick_folder` handler"
497 });
498 })
499 });
500
501 Ok(())
502 }
503
504 #[pyo3(signature = (**kwargs))]
505 fn blocking_pick_folder(
506 &self,
507 py: Python<'_>,
508 kwargs: Option<&Bound<'_, PyDict>>,
509 ) -> PyResult<Option<FilePath>> {
510 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
511
512 let mut builder = self.to_tauri();
513 if let Some(args) = args {
514 builder = args.apply_to_builder(builder);
515 }
516
517 let ret = py.allow_threads(|| builder.blocking_pick_folder().map(Into::into));
518 Ok(ret)
519 }
520
521 #[pyo3(signature = (handler, / ,**kwargs))]
522 fn pick_folders(&self, handler: PyObject, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
523 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
524
525 let mut builder = self.to_tauri();
526 if let Some(args) = args {
527 builder = args.apply_to_builder(builder);
528 }
529
530 builder.pick_folders(move |file_paths| {
532 Python::with_gil(|py| {
533 let file_paths = file_paths
534 .map(|files| files.into_iter().map(FilePath::from).collect::<Vec<_>>());
536
537 let handler = handler.bind(py);
538 let result = handler.call1((file_paths,));
539 result.unwrap_unraisable_py_result(py, Some(handler), || {
540 "Python exception occurred in `FileDialogBuilder::pick_folders` handler"
541 });
542 })
543 });
544
545 Ok(())
546 }
547
548 #[pyo3(signature = (**kwargs))]
549 fn blocking_pick_folders(
550 &self,
551 py: Python<'_>,
552 kwargs: Option<&Bound<'_, PyDict>>,
553 ) -> PyResult<Option<Vec<FilePath>>> {
554 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
555
556 let mut builder = self.to_tauri();
557 if let Some(args) = args {
558 builder = args.apply_to_builder(builder);
559 }
560
561 let ret = py.allow_threads(|| {
562 builder
563 .blocking_pick_folders()
564 .map(|files| files.into_iter().map(Into::into).collect::<Vec<_>>())
566 });
567 Ok(ret)
568 }
569
570 #[pyo3(signature = (handler, / ,**kwargs))]
571 fn save_file(&self, handler: PyObject, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult<()> {
572 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
573
574 let mut builder = self.to_tauri();
575 if let Some(args) = args {
576 builder = args.apply_to_builder(builder);
577 }
578
579 builder.save_file(move |file_path| {
581 Python::with_gil(|py| {
582 let file_path = file_path.map(FilePath::from);
583
584 let handler = handler.bind(py);
585 let result = handler.call1((file_path,));
586 result.unwrap_unraisable_py_result(py, Some(handler), || {
587 "Python exception occurred in `FileDialogBuilder::save_file` handler"
588 });
589 })
590 });
591
592 Ok(())
593 }
594
595 #[pyo3(signature = (**kwargs))]
596 fn blocking_save_file(
597 &self,
598 py: Python<'_>,
599 kwargs: Option<&Bound<'_, PyDict>>,
600 ) -> PyResult<Option<FilePath>> {
601 let args = FileDialogBuilderArgs::from_kwargs(kwargs)?;
602
603 let mut builder = self.to_tauri();
604 if let Some(args) = args {
605 builder = args.apply_to_builder(builder);
606 }
607
608 let ret = py.allow_threads(|| builder.blocking_save_file().map(Into::into));
609 Ok(ret)
610 }
611}
612
613#[pyclass(frozen)]
615#[non_exhaustive]
616pub struct DialogExt;
617
618pub type ImplDialogExt = ImplManager;
620
621#[pymethods]
622impl DialogExt {
623 #[staticmethod]
624 fn message(
625 slf: ImplDialogExt,
626 py: Python<'_>,
627 message: PyBackedStr,
628 ) -> PyResult<MessageDialogBuilder> {
629 manager_method_impl!(py, &slf, |_py, manager| {
630 let handle = manager.app_handle().clone();
632 MessageDialogBuilder { handle, message }
633 })
634 }
635
636 #[staticmethod]
637 fn file(slf: ImplDialogExt, py: Python<'_>) -> PyResult<FileDialogBuilder> {
638 manager_method_impl!(py, &slf, |_py, manager| {
639 let handle = manager.app_handle().clone();
641 FileDialogBuilder { handle }
642 })
643 }
644}
645
646#[pymodule(submodule, gil_used = false)]
648pub mod dialog {
649 #[pymodule_export]
650 pub use super::{
651 DialogExt, FileDialogBuilder, MessageDialogBuilder, MessageDialogButtons, MessageDialogKind,
652 };
653
654 pub use super::{FileDialogBuilderArgs, FilePath, ImplDialogExt, MessageDialogBuilderArgs};
655}