pytauri_core/plugins/dialog/
mod.rs

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            // TODO: unify this error with `tauri_plugin_fs::Error`
41            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
55// TODO: support `tauri::window::Window` also, something like:
56//
57// ```rust
58// // TODO: unify/stable this in `pytauri_core::ext_mod`
59// #[non_exhaustive]
60// #[derive(FromPyObject)]
61// enum HasWindowHandleAndHasDisplayHandle {
62//     WebviewWindow(Py<WebviewWindow>),
63//     Window(Py<Window>),
64// }
65// ```
66type HasWindowHandleAndHasDisplayHandle = Py<WebviewWindow>;
67
68/// See also: [tauri_plugin_dialog::MessageDialogButtons]
69#[pyclass(frozen)]
70#[non_exhaustive]
71pub enum MessageDialogButtons {
72    Ok(),
73    OkCancel(),
74    YesNo(),
75    // TODO, PERF: or we can use [pyo3::pybacked::PyBackedStr],
76    // so that we don't need to require GIL for `fn to_tauri`.
77    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                // TODO, PERF: once we drop py39 support, we can use [PyStringMethods::to_str] directly.
89                plugin::MessageDialogButtons::OkCustom(text.to_cow(py)?.into_owned())
90            }
91            MessageDialogButtons::OkCancelCustom(ok_text, cancel_text) => {
92                // TODO, PERF: once we drop py39 support, we can use [PyStringMethods::to_str] directly.
93                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        /// See also: [tauri_plugin_dialog::MessageDialogKind]
106        #[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/// See also: [tauri_plugin_dialog::MessageDialogBuilder]
130#[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/// See also: [tauri_plugin_dialog::MessageDialogBuilder]
186#[pyclass(frozen)]
187#[non_exhaustive]
188// [tauri_plugin_dialog::MessageDialogBuilder] is `!Sync`,
189// so we wrap [tauri::AppHandle] instead.
190pub 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        // PERF: it's short enough, so we don't release the GIL
232        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
246// TODO: unify this type with [tauri_plugin_fs::FilePath]
247/// See also: [tauri_plugin_dialog::FilePath]
248pub 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
262/// `pathlib.Path`
263impl<'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                // TODO: support Android/iOS:
272                //
273                // let pyobj: Bound<'_, PyString> = Url::from(url).into_pyobject(py)?;
274                // pyobj.into_any() // str
275                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)?; // pathlib.Path
282                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/// See also: [tauri_plugin_dialog::FileDialogBuilder]
300#[non_exhaustive]
301pub struct FileDialogBuilderArgs {
302    // TODO, PERF: avoid `Vec`, use `PyIterable` or `smallvec` instead.
303    add_filter: NotRequired<(String, Vec<PyBackedStr>)>,
304    // PERF: avoid `PathBuf`, prefer `&Path` instead.
305    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            // TODO, PERF: avoid alloc `Vec` (because `collect`) here
347            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/// See also: [tauri_plugin_dialog::FileDialogBuilder]
371#[pyclass(frozen)]
372#[non_exhaustive]
373// [tauri_plugin_dialog::FileDialogBuilder] is `!Sync`,
374// so we wrap [tauri::AppHandle] instead.
375pub 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        // PERF: it's short enough, so we don't release the GIL
398        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        // PERF: it's short enough, so we don't release the GIL
440        builder.pick_files(move |file_paths| {
441            Python::with_gil(|py| {
442                let file_paths = file_paths
443                    // TODO, PERF: avoid `Vec`, use `PyList` instead
444                    .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                // TODO, PERF: avoid `Vec`, use `PyList` instead
474                .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        // PERF: it's short enough, so we don't release the GIL
489        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        // PERF: it's short enough, so we don't release the GIL
531        builder.pick_folders(move |file_paths| {
532            Python::with_gil(|py| {
533                let file_paths = file_paths
534                    // TODO, PERF: avoid `Vec`, use `PyList` instead
535                    .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                // TODO, PERF: avoid `Vec`, use `PyList` instead
565                .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        // PERF: it's short enough, so we don't release the GIL
580        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/// See also: [tauri_plugin_dialog::DialogExt]
614#[pyclass(frozen)]
615#[non_exhaustive]
616pub struct DialogExt;
617
618/// The Implementers of [tauri_plugin_dialog::DialogExt].
619pub 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            // PERF: it's short enough, so we don't release the GIL
631            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            // PERF: it's short enough, so we don't release the GIL
640            let handle = manager.app_handle().clone();
641            FileDialogBuilder { handle }
642        })
643    }
644}
645
646/// See also: [tauri_plugin_dialog]
647#[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}