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
55type HasWindowHandleAndHasDisplayHandle = Py<WebviewWindow>;
67
68#[pyfunction]
70pub fn init() -> Plugin {
71 Plugin::new(Box::new(|| Box::new(plugin::init::<Runtime>())))
72}
73
74#[pyclass(frozen)]
76#[non_exhaustive]
77pub enum MessageDialogButtons {
78 Ok(),
79 OkCancel(),
80 YesNo(),
81 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 plugin::MessageDialogButtons::OkCustom(text.to_cow(py)?.into_owned())
96 }
97 MessageDialogButtons::OkCancelCustom(ok_text, cancel_text) => {
98 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 #[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#[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#[pyclass(frozen)]
193#[non_exhaustive]
194pub 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 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
252pub 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
268impl<'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 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)?; 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#[non_exhaustive]
307pub struct FileDialogBuilderArgs {
308 add_filter: NotRequired<(String, Vec<PyBackedStr>)>,
310 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 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#[pyclass(frozen)]
378#[non_exhaustive]
379pub 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 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 builder.pick_files(move |file_paths| {
447 Python::with_gil(|py| {
448 let file_paths = file_paths
449 .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 .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 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 builder.pick_folders(move |file_paths| {
538 Python::with_gil(|py| {
539 let file_paths = file_paths
540 .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 .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 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#[pyclass(frozen)]
621#[non_exhaustive]
622pub struct DialogExt;
623
624pub 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 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 let handle = manager.app_handle().clone();
647 FileDialogBuilder { handle }
648 })
649 }
650}
651
652#[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}