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