From 2bb654c272e93ad240a2f1a5eacc15aa79da6084 Mon Sep 17 00:00:00 2001 From: Jannis Date: Wed, 3 Jul 2024 23:17:02 +0200 Subject: [PATCH] Release v0.6.0 (#137) * Fix path edit size and focus (#102) * Fix path edit size * Fix path edit focus when pressing checkmark * Update CHANGELOG.md * Ability to pin folders to the left sidebar (#100) * Add show_pinned_folders option * Implement pin and unpin option * Update CHANGELOG.md * Deprecate FileDialog::overwrite_config (#103) * Deprecate FileDialog::overwrite_config * Update CHANGELOG.md * Implement persistent storage (#104) * Add serde dependency * Implement serialize and deserialize for FileDialogStorage * Add feature persistence * Update CHANGELOG.md * Move storage to config and add builder methods (#105) * Move storage to config, add builder methods * Update CHANGELOG.md * Add option to overwrite a file (#106) * Implement modal actions * Finish modal UI * Add option to allow overwriting a file * Update CHANGELOG.md * Persistence example (#107) * Add example * Update CHANGELOG.md * Restructure config module (#109) * Restructure config module * Update CHANGELOG.md * Fix new 1.78 clippy errors * Keyboard navigation (#110) * Implement keybindings * Improve reset method * Set cursor to end of text fields * Update changelog * Move FileDialogStorage to config module * Fix submit not working when saving a file * Hidden files and folders (#111) * Implement show hidden files and folders option * Implement config options * Update changelog * Implement keep focus option (#112) * Implement keep focus option * Update changelog.md * Improve navigation (#113) * Close path edit on submit * Exec selection keybinding if any widget is focused * Select first item when updating search value * Submit dialog when exiting search using enter * Update changelog * Improve submit and cancel keybindings * Submit directory when no other item is selected * Rename persistence feature to serde (#114) * Document breaking changes in changelog (#115) * Implement modal option (#118) * Implement modal * Update changelog * Make DirectoryEntry publicly reachable (#119) * Update changelog (#120) * Fix modal (#121) * Remove keep_focus option * Fix modal * Implement file filters (#124) * Implement file filters * Update changelog * Home directory shortcut (#125) * Cleanup edit_search_on_text_input * Implement home_edit_path keybinding * Fix clippy error * Implement multi selection (#127) * Implement multi selection * Update changelog * Implement keybinding select_all * Improve ID handling (#128) * Improve ID handling * Update changelog * Add multi_selection example (#129) * Add multi_selection example * Update changelog * File Dialog doesnt implement the Send Bound (#131) * modified FileDialog to be Send * Update changelog (#132) * Update to egui 0.28 (#133) * Update to egui 0.28 * Also update dependencies Use workspace dependencies to make updating easier * Ok-wrap the App struct in the examples The new API expects a Result * Add serde and ron features to sandbox example * Add eframe persistence feature (#134) * Prepare README.md for v0.6.0 release (#108) * Add table of contents * Fix table of contents * Remove description from table of contents * Add persistence section * Update CHANGELOG.md * Document keybindings * Fix arrow icons * Move keybindings up * Update README.md * Add show hidden feature to readme * Update readme * Rename persistence feature * Add description to keybindings * Update feature list * Fix incorrect feature in persistent section * Add file filter feature to readme * Add home_edit_path to keybinding list * Update examples * Update demo image * Reduce size of demo screenshot * Update eframe version in README.md * Use take_selected in README example * Update changelog * Update crate documentation (#135) * Update crate documentation in lib.rs * Update changelog * Prepare release v0.6.0 (#136) * Increase crate version * Update changelog --------- Co-authored-by: crumblingstatue Co-authored-by: nat3 <121395237+nat3Github@users.noreply.github.com> --- CHANGELOG.md | 38 + Cargo.toml | 14 +- README.md | 135 +- examples/multi_selection/Cargo.toml | 10 + examples/multi_selection/README.md | 7 + examples/multi_selection/screenshot.png | Bin 0 -> 39523 bytes examples/multi_selection/src/main.rs | 52 + examples/multilingual/Cargo.toml | 2 +- examples/multilingual/src/main.rs | 17 +- examples/multiple_actions/Cargo.toml | 2 +- examples/multiple_actions/src/main.rs | 2 +- examples/persistence/Cargo.toml | 10 + examples/persistence/README.md | 6 + examples/persistence/src/main.rs | 67 + examples/sandbox/Cargo.toml | 4 +- examples/sandbox/src/main.rs | 67 +- examples/save_file/Cargo.toml | 2 +- examples/save_file/src/main.rs | 2 +- examples/select_directory/Cargo.toml | 2 +- examples/select_directory/src/main.rs | 2 +- examples/select_file/Cargo.toml | 2 +- examples/select_file/src/main.rs | 2 +- media/customization_demo.png | Bin 42481 -> 43104 bytes media/demo.png | Bin 42491 -> 45532 bytes src/config/keybindings.rs | 170 +++ src/config/labels.rs | 166 +++ src/{config.rs => config/mod.rs} | 356 ++--- src/create_directory_dialog.rs | 45 +- src/data/directory_content.rs | 113 +- src/file_dialog.rs | 1635 ++++++++++++++++++----- src/lib.rs | 104 +- src/modals/mod.rs | 33 + src/modals/overwrite_file_modal.rs | 109 ++ 33 files changed, 2606 insertions(+), 570 deletions(-) create mode 100644 examples/multi_selection/Cargo.toml create mode 100644 examples/multi_selection/README.md create mode 100644 examples/multi_selection/screenshot.png create mode 100644 examples/multi_selection/src/main.rs create mode 100644 examples/persistence/Cargo.toml create mode 100644 examples/persistence/README.md create mode 100644 examples/persistence/src/main.rs create mode 100644 src/config/keybindings.rs create mode 100644 src/config/labels.rs rename src/{config.rs => config/mod.rs} (65%) create mode 100644 src/modals/mod.rs create mode 100644 src/modals/overwrite_file_modal.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index aa3eb089..0f358e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # egui-file-dialog changelog +## 2024-07-03 - v0.6.0 - Keyboard navigation, multi selection, pinable folders and more +### 🚨 Breaking Changes +- Updated `egui` from version `0.27.1` to version `0.28.0` [#133](https://github.com/fluxxcode/egui-file-dialog/pull/133) (thanks [@crumblingstatue](https://github.com/crumblingstatue)!) +- Added `DialogMode::SelectMultiple` and `DialogState::SelectedMultiple` [#127](https://github.com/fluxxcode/egui-file-dialog/pull/127) +- Added new labels to `FileDialogLabels` [#100](https://github.com/fluxxcode/egui-file-dialog/pull/100), [#111](https://github.com/fluxxcode/egui-file-dialog/pull/111), [#127](https://github.com/fluxxcode/egui-file-dialog/pull/127) +- Added new configuration values to `FileDialogConfig` [#100](https://github.com/fluxxcode/egui-file-dialog/pull/100), [#104](https://github.com/fluxxcode/egui-file-dialog/pull/104), [#106](https://github.com/fluxxcode/egui-file-dialog/pull/106), [#110](https://github.com/fluxxcode/egui-file-dialog/pull/110), [#111](https://github.com/fluxxcode/egui-file-dialog/pull/111), [#118](https://github.com/fluxxcode/egui-file-dialog/pull/118) + +### ✨ Features +- Added the ability to pin folders to the left sidebar and enable or disable the feature with `FileDialog::show_pinned_folders` [#100](https://github.com/fluxxcode/egui-file-dialog/pull/100) +- Added `FileDialogConfig::storage`, `FileDialog::storage` and `FileDialog::storage_mut` to be able to save and load persistent data [#104](https://github.com/fluxxcode/egui-file-dialog/pull/104) and [#105](https://github.com/fluxxcode/egui-file-dialog/pull/105) +- Added new modal and option `FileDialog::allow_file_overwrite` to allow overwriting an already existing file when the dialog is in `DialogMode::SaveFile` mode [#106](https://github.com/fluxxcode/egui-file-dialog/pull/106) +- Implemented customizable keyboard navigation using `FileDialogKeybindings` and `FileDialog::keybindings` [#110](https://github.com/fluxxcode/egui-file-dialog/pull/110) +- Implemented show hidden files and folders option [#111](https://github.com/fluxxcode/egui-file-dialog/pull/111) +- The dialog is now displayed as a modal window by default. This can be disabled with `FileDialog::as_modal`. The color of the modal overlay can be adjusted using `FileDialog::modal_overlay_color`. [#118](https://github.com/fluxxcode/egui-file-dialog/pull/118) +- Added `FileDialog::add_file_filter` and `FileDialog::default_file_filter` to add file filters that can be selected by the user from a drop-down menu at the bottom [#124](https://github.com/fluxxcode/egui-file-dialog/pull/124) +- Implemented selection of multiple files and folders at once, using `FileDialog::select_multiple`, `FileDialog::selected_multiple` and `FileDialog::take_selected_multiple` [#127](https://github.com/fluxxcode/egui-file-dialog/pull/127) + +### ☢️ Deprecated +- Deprecated `FileDialog::overwrite_config`. Use `FileDialog::with_config` and `FileDialog::config_mut` instead [#103](https://github.com/fluxxcode/egui-file-dialog/pull/103) + +### 🐛 Bug Fixes +- Fixed the size of the path edit input box and fixed an issue where the path edit would not close when clicking the apply button [#102](https://github.com/fluxxcode/egui-file-dialog/pull/102) + +### 🔧 Changes +- Restructured `config` module and fixed new `1.78` clippy warnings [#109](https://github.com/fluxxcode/egui-file-dialog/pull/109) +- The reload button has been changed to a menu button. This menu contains the reload button and the “Show hidden" option [#111](https://github.com/fluxxcode/egui-file-dialog/pull/111) +- Minor navigation improvements [#113](https://github.com/fluxxcode/egui-file-dialog/pull/113) +- Made `DirectoryEntry` public reachable [#119](https://github.com/fluxxcode/egui-file-dialog/pull/119) (thanks [@crumblingstatue](https://github.com/crumblingstatue)!) +- Improved handling of internal IDs [#128](https://github.com/fluxxcode/egui-file-dialog/pull/128) +- Made file dialog `Send` [#131](https://github.com/fluxxcode/egui-file-dialog/pull/131) (thanks [@nat3](https://github.com/nat3Github)!) + +### 📚 Documentation +- Added `persistence` example showing how to save the persistent data of the file dialog [#107](https://github.com/fluxxcode/egui-file-dialog/pull/107) +- Reworked `README.md` [#108](https://github.com/fluxxcode/egui-file-dialog/pull/108https://github.com/fluxxcode/egui-file-dialog/pull/108) +- Added `multi_selection` example showing how to select multiple files and folders at once [#129](https://github.com/fluxxcode/egui-file-dialog/pull/129) +- Updated crate documentation in `lib.rs` [#135](https://github.com/fluxxcode/egui-file-dialog/pull/135) +- Use workspace dependencies in examples [#133](https://github.com/fluxxcode/egui-file-dialog/pull/133) (thanks [@crumblingstatue](https://github.com/crumblingstatue)!) + ## 2024-03-30 - v0.5.0 - egui update and QoL changes ### 🚨 Breaking Changes - Updated `egui` from version `0.26.0` to version `0.27.1` [#97](https://github.com/fluxxcode/egui-file-dialog/pull/97) diff --git a/Cargo.toml b/Cargo.toml index 2891b907..1adff052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,15 @@ [workspace] members = [ "examples/*" + ] +[workspace.dependencies] +eframe = { version = "0.28.0", default-features = false, features = ["glow", "persistence"] } [package] name = "egui-file-dialog" description = "An easy-to-use file dialog for egui" -version = "0.5.0" +version = "0.6.0" edition = "2021" authors = ["fluxxcode"] repository = "https://github.com/fluxxcode/egui-file-dialog" @@ -17,10 +20,17 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -egui = "0.27.1" +egui = "0.28.0" # Used to fetch user folders directories = "5.0" # Used to fetch disks sysinfo = { version = "0.30.5", default-features = false } + +# Used for persistent storage +serde = { version = "1", features = ["derive"], optional = true} + +[features] +default = ["serde"] +serde = ["dep:serde"] diff --git a/README.md b/README.md index 49873492..75fb2f16 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,24 @@ [![Total lines of code ](https://sloc.xyz/github/fluxxcode/egui-file-dialog/)](https://github.com/fluxxcode/egui-file-dialog/) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fluxxcode/egui-file-dialog/blob/master/LICENSE) +
+Table of contents + +1. [Features](#features) +1. [Example](#example) +1. [Keybindings](#keybindings) +1. [Customization](#customization) +1. [Multilingual support](#multilingual-support) +1. [Persistent data](#persistent-data) + +
+ This repository provides an easy-to-use and customizable file dialog (a.k.a. file explorer, file picker) for [egui](https://github.com/emilk/egui). The file dialog is intended for use by desktop applications, allowing the use of a file dialog directly within the egui application without relying on the operating system's file explorer. This also ensures that the file dialog looks the same and provides the same functionality on all platforms. -The project is currently in a very early version. Some planned features are still missing and some improvements still need to be made. See the [Planned features](#Planned-features) section for some of the features to be implemented in the future. - The latest changes included in the next release can be found in the [CHANGELOG.md](https://github.com/fluxxcode/egui-file-dialog/blob/develop/CHANGELOG.md) file on the develop branch. **Currently only tested on Linux and Windows!** @@ -21,25 +31,24 @@ The latest changes included in the next release can be found in the [CHANGELOG.m ## Features - Select a file or a directory - Save a file (Prompt user for a destination path) + - Dialog to ask the user if the existing file should be overwritten +- Select multiple files and folders at once (ctrl/shift + click) +- Open the dialog in a normal or modal window - Create a new folder +- Keyboard navigation +- Option to show or hide hidden files and folders - Navigation buttons to open the parent or previous directories - Search for items in a directory +- Add file filters the user can select from a dropdown - Shortcut for user directories (Home, Documents, ...) and system disks +- Pin folders to the left sidebar - Manually edit the path via text - Customization highlights: - Customize which areas and functions of the dialog are visible - Customize the text labels used by the dialog to enable multilingual support - Customize file and folder icons - Add custom quick access sections to the left sidebar - -## Planned features -The following lists some of the features that are currently missing but are planned for the future! -- Selection of multiple directory items at once -- Pinnable folders for quick access [#42](https://github.com/fluxxcode/egui-file-dialog/issues/42) -- Only show files with a specific file extension (The user can already filter files by file extension using the search, but there is currently no backend method for this or a dropdown to be able to select from predefined file extensions.) -- Keyboard input [#70](https://github.com/fluxxcode/egui-file-dialog/issues/70) -- Context menus, for example for renaming, deleting or copying files or directories -- Option to show or hide hidden files and folders + - Customize keybindings used by the file dialog ## Example Detailed examples that can be run can be found in the [examples](https://github.com/fluxxcode/egui-file-dialog/tree/master/examples) folder. @@ -49,8 +58,8 @@ The following example shows the basic use of the file dialog with [eframe](https Cargo.toml: ```toml [dependencies] -eframe = "0.27.1" -egui-file-dialog = "0.5.0" +eframe = "0.28.0" +egui-file-dialog = "0.6.0" ``` main.rs: @@ -85,8 +94,11 @@ impl eframe::App for MyApp { ui.label(format!("Selected file: {:?}", self.selected_file)); - // Update the dialog and check if the user selected a file - if let Some(path) = self.file_dialog.update(ctx).selected() { + // Update the dialog + self.file_dialog.update(ctx); + + // Check if the user selected a file. + if let Some(path) = self.file_dialog.take_selected() { self.selected_file = Some(path.to_path_buf()); } }); @@ -102,6 +114,24 @@ fn main() -> eframe::Result<()> { } ``` +## Keybindings +Keybindings can be used in the file dialog for easier navigation. All keybindings can be configured from the backend with `FileDialogKeyBindings` and `FileDialog::keybindings`. \ +The following table lists all available keybindings and their default values. +| Name | Description | Default | +| --- | --- | --- | +| submit | Submit the current action or open the currently selected folder | `Enter` | +| cancel | Cancel the current action | `Escape` | +| parent | Open the parent directory | `ALT` + `↑` | +| back | Go back | `Mouse button 1`
`ALT` + `←`
`Backspace` | +| forward | Go forward | `Mouse button 2`
`ALT` + `→` | +| reload | Reload the file dialog data and the currently open directory | `F5` | +| new_folder | Open the dialog to create a new folder | `CTRL` + `N` | +| edit_path | Text edit the current path | `/` | +| home_edit_path | Open the home directory and start text editing the path | `~` | +| selection_up | Move the selection one item up | `↑` | +| selection_down | Move the selection one item down | `↓` | +| select_all | Select every item in the directory when using the file dialog to select multiple files and folders | `CTRL` + `A` | + ## Customization Many things can be customized so that the dialog can be used in different situations. \ A few highlights of the customization are listed below. For all possible customization options, see the documentation on [docs.rs](https://docs.rs/egui-file-dialog/latest/egui_file_dialog/struct.FileDialog.html). @@ -109,6 +139,7 @@ A few highlights of the customization are listed below. For all possible customi - Set which areas and functions of the dialog are visible using `FileDialog::show_*` methods - Update the text labels that the dialog uses. See [Multilingual support](#multilingual-support) - Customize file and folder icons using `FileDialog::set_file_icon` (Currently only unicode is supported) +- Customize keybindings used by the file dialog using `FileDialog::keybindings`. See [Keybindings](#keybindings) Since the dialog uses the egui style to look like the rest of the application, the appearance can be customized with `egui::Style` and `egui::Context::set_style`. @@ -134,26 +165,24 @@ FileDialog::new() s.add_path("📷 Media", "media"); s.add_path("📂 Source", "src"); }) - // Markdown and text files should use the "document with text (U+1F5B9)" icon + // Markdown files should use the "document with text (U+1F5B9)" icon .set_file_icon( "🖹", - Arc::new(|path| { - match path - .extension() - .unwrap_or_default() - .to_str() - .unwrap_or_default() - { - "md" => true, - "txt" => true, - _ => false, - } - }), + Arc::new(|path| path.extension().unwrap_or_default() == "md"), ) // .gitignore files should use the "web-github (U+E624)" icon .set_file_icon( "", Arc::new(|path| path.file_name().unwrap_or_default() == ".gitignore"), + ) + // Add file filters the user can select in the bottom right + .add_file_filter( + "PNG files", + Arc::new(|p| p.extension().unwrap_or_default() == "png"), + ) + .add_file_filter( + "Rust source files", + Arc::new(|p| p.extension().unwrap_or_default() == "rs"), ); ``` With the options the dialog then looks like this: @@ -206,3 +235,53 @@ fn update_labels(language: &Language, file_dialog: &mut FileDialog) { }; } ``` + +## Persistent data +The file dialog currently requires the following persistent data to be stored across multiple file dialog objects: + +- Folders the user pinned to the left sidebar (`FileDialog::show_pinned_folders`) +- If hidden files and folders should be visible (`FileDialog::show_hidden_option`) + +If one of the above feature is activated, the data should be saved by the application. Otherwise, frustrating situations could arise for the user and the features would not offer much added value. + +All data that needs to be stored permanently is contained in the `FileDialogStorage` struct. This struct can be accessed using `FileDialog::storage` or `FileDialog::storage_mut` to save or load the persistent data. \ +By default the feature `serde` is enabled, which implements `serde::Serialize` and `serde::Deserialize` for the objects to be saved. However, the objects can also be accessed without the feature enabled. + +The following example shows how the data can be saved with [eframe](https://github.com/emilk/egui/tree/master/crates/eframe) and the `serde` feature enabled. \ +Checkout `examples/persistence` for the full example. + +```rust +use egui_file_dialog::FileDialog; + +struct MyApp { + file_dialog: FileDialog, +} + +impl MyApp { + pub fn new(cc: &eframe::CreationContext) -> Self { + let mut file_dialog = FileDialog::default(); + + // Load the persistent data of the file dialog. + // Alternatively, you can also use the `FileDialog::storage` builder method. + if let Some(storage) = cc.storage { + *file_dialog.storage_mut() = + eframe::get_value(storage, "file_dialog_storage").unwrap_or_default() + } + + Self { + file_dialog, + } + } +} + +impl eframe::App for MyApp { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + // Save the persistent data of the file dialog + eframe::set_value( + storage, + "file_dialog_storage", + self.file_dialog.storage_mut(), + ); + } +} +``` diff --git a/examples/multi_selection/Cargo.toml b/examples/multi_selection/Cargo.toml new file mode 100644 index 00000000..208ab854 --- /dev/null +++ b/examples/multi_selection/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "multi_selection" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eframe = { workspace = true } +egui-file-dialog = { path = "../../"} diff --git a/examples/multi_selection/README.md b/examples/multi_selection/README.md new file mode 100644 index 00000000..acec7f0f --- /dev/null +++ b/examples/multi_selection/README.md @@ -0,0 +1,7 @@ +Example showing how to select multiple files and folders at once. + +``` +cargo run -p multi_selection +``` + +![](screenshot.png) diff --git a/examples/multi_selection/screenshot.png b/examples/multi_selection/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..1a1179aa73139122ee0c02e405b654fc465c2517 GIT binary patch literal 39523 zcma&O2{@MT8ZY`O$&|5>43RV;Q&OhP5<-$B8A=jD=1iG~qEUuqDnk-76EY=A$efu9 zNl3;F=l9lH`<%Vc-q$&wth3Hu!Uhmb+jEB#W#oR$BTG`i$7ds;y)a{%jMA!(>I?H zc5;PqmR=LP5Kb4r(*pJSQ=VsfdR7yE#P*%(X*)B&AXZKL=f;!5AGs4}Dm?o~-Hbyj zO(=bW@w4~1S}hqbev)cy$R85Fxkt^cNBly4zlL@Q@fYdB^1F#Ycv|(JKL|DAU^tbo zr=6~M=lzKOv+ciJrR|Q!O}k1D6R%6Us@2P-$DcZTGEw_jx?X6t5`H?L7E$fL-x^Tq z%#S~b*?L$$?m&Bc`>?idsLK=GD`uK3*W`%5+j`clJ5tTml&2%>Ld2FG5+db^d*xpT42lZ)Nu7yf>9dmeM!ty{DL=L%>A z1qCJDe(}Y`#tu0iWuzppIX$XpVUZ#!K2=kf*KKWSY3b85Nf*Y<$jI2z8$JI_`AmXeZqE%Ob@H<11Q{oCZ? zMRJE?j)xB)l6-P$9zEZf5vkA@)Fh_YY?&!*l0`7XFZtzJC2` zX=}?;Ab8+_qN-}$gBr0axHVqt z!;@TXEiF2h;_P2WN5jIht=jiLefreI&W=I-nXcN;8ngCu#SXjk3$Hv7KSZQ+1&Fu&YAG}jWM#j45 z_ix8{7hh$wF)T$T_E=oLY;J8$TU}kPy?1NT&yoq={mw4;U%uF>+%>eahRrUAxkdZcS+*ma8b_dV>dVfA<(k&J9Huv=Obgb!OYy28t z8~v|8t7G54GX-r*{3;QUTOrpGQuP`_aJ* zEZ=L~D-tIah~08OB`#sZ8!U2QV4#UOyN;33t?%Eh34J8d(T^L&Mn(B(rP74e{@-tF@gRZXc6fM|ZJ{~# zAKQ){#Ir2EK3daWwdhfvHkW5;Q}tf5miI~aHjW$A8j-c<>`hWGik|Jt@#&g+L>!`& z7-xy7d-p`Y<_}EOb#<}n>gwVGs~c)nR8^BZc5L4+udYt5!GFJqk}2e3L(b)fsfAs; zb}4c^;q4dM%@oosmZ7~DFG&3z`qsUIxK;6Xto7`#UnZi_gATRifs`}Xa`)I)-OeSLee9p&Y3-nmF|l2e@5&QoIt5PJ6EL5IdfA|C{nfJ6s)ZFTTApj z*qxFx>*XmMpKZiS9MF7g6{`K3&(qT@Lzwr}3~e{{RVNnZ%4c)!@q@mFv&43^{8^)^ zU1ppmspQ?!-Mx>lk*z11__b~?dHYuf5x1DjToIQWB#r+TiqpMkSA-s2ZTP}X9Im7# zrozpCtDhy0$p2F98eZ>CJAL}}aI^1zLBZRLlBd!g`};+4QH+f@{a@u{%`p9p0Nr%juq)#1)?L~1!AFn`+k z__%OvW0AvG>W`0)_`QF0@Zh@_Yiecjxt9uiaIORd#>Khob`qD^9a8$?0p6)~+`rb0 zjD&+s6Y=Fi{l&ck0{(`Xfvw|8s;Zl%xlWFKv!6KrOm`~_i$ZeCP6o<^tSox$Ei8Ci zS{g~%?L&Eax&Ok*jB8SV!^smTs;8%ged=veuHPCSHcM-_QhTN=$w5cEWlN>CgkqvL zQHnA%w|G@>lkUXF)8k2#P%_>Q4(<#M4Q=l0+mhN!MXX?`fCCGwnTH1l`)!Ki;^I@b z6KC}Gd3W#Lq^PK|pG@4Qr0R0bW7 zChDg9ztYCSRIQ&}U^{#EEDj)Xm3Zm?{rd-Z1CnqJhu;o8NNyGsq9?7wtJFPcr@JA#zm7KgNH~KM-IXo^hlHYnq zqht8c>=FYdIga5*6Ne(AhJOD1nMATJ+z}8EKq5WUrON6IOH546UU2&L<@nJ0-Y#aC zYgcr}Y7Nr_TjCTI>4pB)F5wNSt@~d893Q`_m0DfmG$AZ1>N`I_pZzqBC_hOj_R4Qz z=j04P-NL&H%gWLPRo|px6$;J>m|61BnYfbwIn7+T)@)K04Tt}JNOddA1e;l9PRd>5 z(mq?G*RG6Lu3UNd{(W`mwgcK{&n{(ak!Sk9EEPc_^)DanP=Vjik*ThMoZ;hvnF z3@{)Ztuf3?8mbE~#gOSClQ_Lh{50;d$J2^S&OdR2$ zb98Y@&db}nY11auJK}L--{T{)9lol&mKi5{<*kxm?qS`wjpyJ&dhA=P{G&f@=Egb9 zk7CE7rsAQYb_z>N)0P=WG&HD-i;GicP%;ozMvuv_lEGy#1;%n#j=1&U3<=5rlFw$H|8H@Bow5du_aykZ;iQO&+SA_x_OMm61EA*qsVn8sfg{( zm;1?BjD&WpA%|k(5SRARnfz*rx5{T1jiwcPL=zVmSJnFPqUd&N8k$7Z9Mwp7=_k$S z|6-*>e2H9oB+DyTZffu=>ge1pq9kdbKD{YZA6uOVm`LyRX)2kQ7P;H;i8%bIJGj(M zPe0Ww{!bMF#fY*`kSL?)&6%{c$p(LYegE#=JD(Oy+OQqBRd9~m+S?ORyMW{DBstzS zHf|E`-q_RG8B|^_YgFP$rT&Zw|G^pBw{KrsR+g%_x17-V67t(B8>2N?3cRt}yd}#ODij7ct6h2twDsv`($9~YyNNgS7`RIL)MI(En2Fg`x ztEq>F^hKJ#@1n19Kz@_Y&!4tWpFQId6FU{`R%SdD>3IhjuXAEc*rrVz$kh0lP<&80 zjf(8#jRJ_r@=~X@q?Wtw^XE{s#G$#dvAufxAKFb^c&5vPTe3Yk*3}iI{TD1yM#K7aoFUO~Ztx{;d-iEjWOmY0{6&IRW7Opg8hsdDygbaRgt+Cy6# z-}B?dQ|_cjJ6?SMrseECH9oZyC;S>M{CXx?SnD3d#86iZ&}X#o7b5N?>FT@3{Qve} z?xAFnfB!TaK((KE47<1e`&SS2Qe?s|U=6Z571Q;!&i;8o+|Ay1Qw!CT&ec}=RZ|OL zVaYKaeCZ!PXk5H>3H7ORWX50SNa)|cdw{d7kmv*W_vQ^zS!+-8s!PfX?%nI7n_dG@ zjZL+cdee=6)Plca;Gp!k{*jr46xy`k-yxd=&9~L zyzI5{!L8H*(+Q~t!xqt~ltgpj=Eip4#!Bo>#@#g&bG&GFwyKwZ=#t#axUiQ+qcx0( zMN$$M#LBvt8FSDvAC^D8{~=B=Anh=*FuX}uX8x?APYv5e6;VCc+poNnA-HGT3F2iB zf?eId=5+!UFoE=Ry>=6Om77}~xN*rtmP5vUtBs9~clzsN1Yf~(<~Zu8X5`p@Hf^Cq zyN-v~3@{}(NjKf@ydAEV@$Vj~{b9h>S$1CXrD4j!H?j z^!E!r-yVxgofql+;&24$NmyK*Pbk!K@^k*bYNz^PF1hpN`7j&=5Rl+7v(Vqh=5ZsD zg69%CCuUkyxkKs5Yf@0KU85*Z@@>OL&3scUtG0h zoE88#+iex={@2@q&WFE;S-g)FEE)>jvGdWM`5hHWNgFyZczb?W24^$Kx7z05;4s|3 ztgiIpIxYEzR`(uh%BFddMvRY4WDAt12D}uynXZL z&1^}Z;KKCl*RSVTcZ&kw{9ah7EA!;q-(vGf@Ek>MH!W%g;97V%-H;KuTEqMI0VUu& zj7@!(;tITX2`YB!(xtSF4C1(F=I4_A1mE6;nEL_(1yGijI(#MP)h`&49p zBRS3-*MqluX;i$y#Nm|!2rU2yfDN!ao~HlHmqMmJu1q$kcnu8;t__5*{#gjCHr0P& zaP#e36?<3xs05~#Ti{dGW;w?)+NotyQIHjuIx^3JMgbuaaL2qZoQxD27B-Wg<*Av* z`o&HX$cWj!O3lVbP+eVJSVV*@zA5PI`$x%TWt`qCBi!hFHSN`WB{rIxThJ=dtWf+D z^!B6j`S+`$d~Z9TPdRt+e92Z3`Sx)~P)A9ZpBz75&uZzNITJLO)NoPs!Gi|`7^Ltc zBPgf#f&KeQ8k*W5Wpu?xT!-avJBL4eM!@=QOl`5YB~;4F%IJJPsA4$!&rfGG_wFkg$r?xbhRnfl33pYmDUD>193mY(l2cb#oKJSu+t5)O_S@BMnawH3F# zf`ZVnY^MqRFJHb4eyP8o_WU^$t7>~&n?J~2ulqzMs(*y!*|b-$n84O0oxX34iHQL& z(>68^kBMOiBTstvO!;-l&K8>k2M*}qx7SBM4h`3aC6#FaBh}vAxH;u_U?{`{zz0;} z|KN+cxj9q_5`SG^!&e87x%M*zjon3eKPp|RNoW5<0_$>nOy6WUARtf^xMfGSX*C6h zw5#w_=OU$^2f>kC09=O$JeVV7<_&82oXYQ=^11&s*fQLv`<1eqR%~o+ZLLV$k?REK z@ioq>E3oOw#v{!>*GAIT*XQSxtu%i3?VH7|TelJt5_Z0LX^_+t`RGw{MTMM|!%%F}t5&VDGdSBF2iW|Cq z-^7k=>he=px}KJu%>euf5S5mhsf=yoGktD-25=}LF_8>d0UfCJ?d=_R@89=3SiZR7 z%@>=iI3-RK9RW53uh!3~HZu#c&1kp6N#xWu|Mo!rS+m~}Ny*^;eq-#E!B5XlfK>V! zX1=WrV}pa883lTTbE#_w*bPqPXK4%hd8|_s`>#V`EYGzlb|NE?Eq6 z`(;EGVp)Fz&8%*ODwgzh#x>Bab{~ zxdDz11RiMcpRnvPkx6acu74TKe7lEiyd96NrR5u<;^Ca4<|dVv?ttPz0yo8}suF4& z4x3k?Cf@*2M5j|$Gfy+ba-4=gePHRM3pQWJ**&Rdl~*rL$J9Xu`*ur-R~{`wQphRg zW4v6%t5>4vgo!U-(gFMfYe;%7|IX|9^?S*K%4TAA38#K|erR8^&bw>LF0Ap!K} zs%s}_{$ji(CcisrI~BITix)M(P61KsQyTsGXS5D<_P5xI#*ifR-^rSmlGLQ;>R z01N6#SoMa&o-J`&kF+aPgY0?OV;meg1f4C}`C7VEe^JLXW`#R4pv{zc{#k z2+_LRKjHXMBAU7XN*!{QP`jHd+$@jeRH7)vJW;(4?`UO>>8t z`wGg-xd7=dtS@&4%(hsHSCu?PJ8zztdk3xnWC330`IZN0XK%a^|&Fje*eb5|7l`<7j~mm``d#+*xWEVev)zscm)BT+DsQs>F4 zYu{~#4M9@lRN1KQnDVi$(tKv*+xkYIccW|8||s7ZhtUj(~}ZByrJLla#ggw znlFdcfQA7+h|*lEOycJzfkp8=R5#5xBn~j2tr;K#pgU+hIRIa3uUX{#ulWBA?j*(9 z9trfyN=tE+OOFQTObvc!0?-^i%I=)uB}*P}c`4se{7#jNyZ1OyH>2J4M^4@J_l&I2Ff#{9j;ilF z?>cAz!irx8waLEHnvxwNdsjn4W3X(YpE>BtU2^8O=78mp;BTiaec89hT>Zr&$JiK+ z)h&t`05=O29~X7K!EJ)Zm^-xI-CGjlZ<5reUSA_qzB*)w4S%(<~Q<|T& z4qg=!`%AIH)IHwE|AWwilJ>gn%Q64?Kh*a3*#D!cvuIl0V_)(9FTh572Uyk7J?G1W*nN362!U(a-8UKzESpwa_VEmhV!sQ&i6@O?;pP28|LDM6DTPP%4Q0 z?0m#WK>e={A=Rh&s*mu!zicWR4R!>)P{s0fH?LvChMOcj8lZ6~HcTZ#do~(B+NVRa z#2Md7eh8Wmw434S$Hz~e4CF4eBa^TxCyR zPLbxK$IcpD_*1LA!T8Y?ex}v%CW<}BTYPoW^|HIIhlLqiPS;uF%0JT$sWx>y+V^^Q z>YFbkd`?H>Bsl^*FU6!Qm5=0#te?N3_Yo%1COJ3jco#o$f)2Ar-9>J|3B%V-XUJ7q|yD}NQH*_gRAO#q%#j?4j zMI>27Wc&8*_6NUp@D8i!JQA)A2xsS(MECB&@#uyu&+C!`ij9kp7pcF zo;;Wj!HVK1hi?a^j^9_~Gbr~s@@sY$D6<)qx1d16KEH^iq%iVZWaq^BY;vdZf$Pav zs}xrK5}pMO+_ua4Y~KCqQqe;HjhQa!v6cCvxsG$DIR4_>U76v4WkX^Rac`Nw!+qdESCuJ*D zxjp?+$zgE}fv%nZRe1Hx3j-cH!Q$8qfBNqUHi!yWONOa1E# zy6JkZD2+3{*PDiN>bRI$FSLV<&U9an4-huI2wER?SqCG+p_qi*kPg1Wu%&3?(uw7wNkI=LLV40 z%p*y!6-jg(GSayMYY29s2Kqpn0H>s50FA$X{US6_pmE5Hlz+nc4jtMC9m&(vb2!N# zG#>JRDLg9B-n!L=fr!bT!Vy1uS(5OP6V9{W8qsd5ly5)_ee-3qu`%}WE|6r|czWWm zA09Z9ZPO$1W1!-KbuyYz1;Eei-;D1V)1!#Lrwn~6%o<}*2nqlts_GWsey zp}Cj0LXV;bpv+W$YU<|Hlkwq(avjAXP%Q{jjdg~IWqQEW|EG6riYC;ofPtxq&o6Ui zCoFA52Xg1dL`A1Ny;nQ;?%hiwdCb(ZCb7?sluW!-baF!3RE8i_Q}CFkySrOwj|A}3 zP{+Az;7GH)SISQ_g^=Hv|4~VBkQ)oO_A|a`%RQD}3;I2P6R{yg$+x%H=)uE>Az`c< zDk^VZpLZ-NDQWKNq6D;vlX9W<{-B>_%$fh<^=l4Nwr$^0J$?OQa3kn@$1RKF0V1sj z$~hMXyyc!}WL#cltAJu2{DAMr#>QZ- z2I8dlpVW49E6y&d1lDQk?he7OC!j3k3sA#j*RPAAAlvoW6jJr{xeR!MJ1Q$GZWL$N z?5c<}m>fqj0cQj_H4uorQaEAV={*7kw&0VrF2YqX1#(r$hJ&{vk;aa|-&V>sRsA$%o^Y#sA{xXPn%9DsbiAU=CE&8(m z7jXtkC5^FBMMFb&YHI4?wMVcA`W-()%##;-q{@+!3tm)@-Ur~M z4Mtyna#JO_5{lZ2mv`H%op7(g&Q3fM+Wq>0-iZ@5hK7cOMGQ5Tz;1A_tGjbA@0zEs zY3=CXr(Gx1P%AadTX_v%&BF+8yH*|;f)bvWy_)=kakq+{~(m48neyZZ`3 zwr_6cI)>s+$9a?-qJ-GxkCaa4`7?=%K~>+CuSWFt_6EmqV(=1rQ17Z1RWf0~jqJlA=F^{28dBE?1H>Py;Y6e5t+}XnX{KgW^loQDuXc%{yi(exCRwoGEj8xHCHevjjM-}JN=oT zgcv~`279n_$Zl}3($92e334Crnu3uv3OG{;-*BzhOS}smU_Pf${h=M)ZeJKlZ$OCc ze|@QMet!M!R|l$y&&*+Z&fnYi5m0EVzpTh(Ni>|}2vcFufiXObD*Uz1sZ)L+p%7XJ zQL2Wm>@QusNXhyIhCa^_4Ldu#0u~J77i6}}bKP^lfBW|dac~52qOS&`x|IKou`etQu^_9?EJz4O`%P#S-yRm!KA4~Ec2QG%X7iuxNCNjZ#DYd zM&lG>z0g-%mlJnP^g`t1E*Kbe+21iDezTst&!yKsNSH?*W?S`9Uo+1Rx*+oo8&S^v z52*n|{!+aAGoF9uO<|%-+70@o&94v#+|)EtQo7b-Pa2l5`e$G1%)4C?35A8MD7p|G zp%{BGz{3K8ZPk8RS)|eXI|h!x%8C| z+q|C1BFFJhzN^V{*T3CrzCJg3&3niCQv7;!GxxV{NF_Q!OiKG9p}YQ>EQC!P{Q0xt zkM7*fQxLce;?&SlI$oUhd;MA(@qr&bh4j$qhpnj2=370S4uXZFbyh_cZYZ?wNoUm` zf^&aXsRk%m)=Q_CJgN$~A~u7ix=v&kR()4uPM7hs`1JC6cM5d?@Au{4oX?VaFXrEU z_;73StkNrOqJf-(7T99R>vfW8b#3LRO9gWf6U#(zjhq)Qfq8z{{um@*-dIbFc(09m zL#GSK$xg)T>Sr9Xiv18&c4I+-|Nc;Zr*X9+y^OzN5S?jb%ppnF>7C)6Qv5Gv4?a-8 zq`Q5JVEd-||4W9H~W=7Y{zTCtG__PJ|ZgJ3$et1orN4+g+Pa2Tx zV(mdM9=d~}gNtd<1Nj_vqE6zsAf-{iRHOLw3kwOBiw8HHv=3%%w*82Tqm$E6a)c}s zztIOJoN?&8GuUn$NdOrL=Y-w6$7EFOunAf`V4lcZmQI5uB<}+`{QbAtcU`niQRf=Y;-TaTUU%q{#KA`%cePK_e!6?7;*M@CF zvsbLFLYMsvlUo2Oc)ux%V|{ZzUwkPu{bplTA75-^VEo$eR6JZ@pQ5yHD@^Nb!Tc@m zxkGdAI#5H2dsu`o1Mo$JPR^G)iyVJ;BzKI;>Xp4D2TV3z^%Ez|jnziO{)K?hQ8@Bd zukaX^d$<`}OgD8vtJl`mB^bV6<(+1m_e5{X%hMRYt>KTFyDLK4_t;KSu@N0Ev~ipX@O$>>&U@U^bL;rZ*Q4R@)Tz@qh5LwX&+pF|Q+T8BbT!;LPbn zZH+%kveMGGAiEBSOa1ievklVQU;E~b&mHNyPk4jP+#U?D8i1Vue_E|NyYu!5cORG^ zOOB`-vOAX5nVgYvz`FNJnyU@R)(jbJog^=ouy&T5kDaJ@K-emH;y)|ay&#*y`Ws@q zKC>Ef5e=093|@bF22^AY=b)8xNIBCIxDCDss`uT^vTW58@*r8SPuqny`i9hHG#}S8%A0zIjLASH+Ez;o#Ps^V>Xzs@PiF<8& z^N-K@Gz5y>zh4109S^3aJvc?vOqh*#y?0d%8113@q2wGnm1KM4%d8vjvlbYPMk1JV zt>Ts6W39^{>b~y=!>ztdJjRZWj$kpD)M{&qY7=1-OUu2(Gs}Peq^^!2$FTdm*$_U4 z#LpaRN%*4F)WZ0=WCkISk-r~~0*!sKQ8>y6R)Us}PG!VdV%fhrT>1jwoD zGu-Qpvjd*el~;|$XI{R)@aJ=<_Ni05=uX1Jq2iGBFyNqIs6dl2=Y^P)?fHkeV?gin z;3f0=aYvQ&FK^w8H_W7af7xPrRfPREkS}yLs<0rsAyfMOf=5`B_R2auP(xSyeh!zC*0VkTQs#J3Pjf|9(zRPSHqSNJ<#g8^I#W!r9d7amS?e z^nKsE+Mm6Ia0!NqE+XmtgAI|wQSt5PLcf)2qIh}DWn9>um&eT5c#uEMu|2xpRb)m` zftr=_gY}CGdCuET+Qozf7zByliw@rs(ZSO4PhgklC{Nk_nZMO$BEfs@MkZ-`r6#Jh3s@wWZyQ^4y6RW-TCEgeM6SvA^#bvZo zufL6FY^)fp=CZt^e(`v5aTS-Ip&jyE!_ALj;fVb_LgsVxCK>X4{+;h%>$^%n*}gkf z9_(zkTx&!jy0PR96blH_XEe|8WpsO}bUqJoXQLH%6F}0Zu&2zV?rAY{GE4=L^B!5p zrJ=l7-wRNxm3pf{>k#2`%k}GZo_>+x{8l5i-)!|On`=gA8^`n|Y9l|yFDaOx(-yTa z?Ssckvs{Lt((&1295Ka9YyBT41idoa*{_c$cdLKV5AT`OlC+OOR%XY-M)kn!YJp9# zm~&n31;3;cjTW%2lNZhInIv4LppaoiG2-U$g|~`i$)u6M4WzCW<_+(@B#$078_Bl& zb5A7gXm(dTa%F+)&tf_ciZkkn62e7Rm_)4Lx1(ve+^%J$C}Ab-1kXcoMeGT=S|?qA z0vS*Y6q8G5ONzy(sP^A6_$4~Pq&fWUl<4DXGcT5rkrCuuX8V&hl=7k=LI5wI8rCSA zLIQ?6594*zL;8Z9gE?>K%9AHta3!M}zqw7QC1wOQjlz$wM$Ij<)Dl$lRVUZzR|kW&7Ma2UrLU1aA82Uftj)m1gQE1A!r`Y`BaP00El-reI$8@261={tSb)+@4uT&Nrz1)3ch98FZQeC+R;cWk(W4sM7_>XUg zd-_*hq{LDTYgZA*5Pptd(w9lgt-lq*`|tQ7J~~XiS&7R3Z~5{67-M-gM)e(RyoTrR z8wNIhKdu|{x%m1X7^FOR)!=Rt_VB4xEvVvE))HYE|3W9$^6uK#9UQW~*S!eN1m8>X z_;D%-Mf@*Sl)UHLPMr)<#D#a>+BA-=27bV4ZaKuJ$Vi0oN4Q|eb}|$kOnR1( zovqD5EQW=EfiJuI{)fB_l)=>moi#V#g~T6$ZXo&P6&9K={Om)`K-@PFk%*p23gIJ; zZxD7!Pp3jh>B=$R3{^|U$a{4T2<;6J$DTC3{jSnnEqBMPlH09js=3z~5LgI4Ut~wd z$;ru+?~|+=QDu=EP>W}_w-_I9B@qkj9HCIcHwTEYf0%9j5riN9I}zs<6x?#{^$f*| zSB17Ql8JtmKCtmQNudA11dL4ALm#O`nJX?nV!flp{p;vxa(eo44izmet*#053-J5E zTCn)}AKe>syKf=j<1t;inYdLbKdAlF5WsL)%}3tehDXy?;Vp-Nun?eK)#&Wd%F?W) z`|rJ^L8x&zyR8F}@bdPS`}jmA4A7g6mI4HM2$B%1m++>E{adMNX=zOc%01aQIk(6h zBDFNplbnBa-Gs9SU_n4VNqX$+XV&sedgx`4imI_czB&xOKUh%%LR`~Mh($#18(Al0 z5K$0dLix5^#oe=vX|#Y?Ie1rhPp2ff!9yva1^ z&K^PP96?v-pS>Z-B(`;SCKnX2crR9S56-v}E-?`oB@Fo%;IAFmUNh3mtqAvf%xaP- zGtR-Bp7L5KU99t7QHAEw+#2TYb*|91cFW;jpOdmngsLuQuSmz>{U{>mXCfRxb2 z9TL`ry70h0P#&R(JMX_PE_YT42_i?zQY(pOzktBjw^J#J9V4k>_l0{srF3&}Fn9OWq`Y2?1~0+Fp8kdUI_0uDmPEaIKKwFhx-3(bc3v+w8k?G`!#3w&p*PI6*n+J^2%iuXh|@y^2N2ASxUe$J3ZURR9I~e- zmMrQA&z3k^2v~~hsk-|gp86?TBUN$6*liQ?^+efu;_>VF%CAQiThE{h8#=J?$*&fA z|2b^0HvO}7iWjLJ5CVeE!qlLF12QyZin#Gc5|M|+wL@X%#!CD7LGD1ULGTSnnJnt{ z>gTpLa@3g0N_kT`$2oOJi*iYM8<_5p$it!M56yetL7ol8e;N`@0%THf8*yY5D3KP& z+yyX!zIO>s8c2H_berLp5M;8J^@4wUh?AH6Bcb!YP;xxk6o+^!h=@nb7?Ac21vC@3fZ(Rb3dp5EU=ep;FW_4}=)K z{ikb0oRzLO!sQ;a)3_n<6}osZw4%`JQy!392%VzVlyv`o9OyATfEwjf|9tCRaM*y@ zsv=&DFLpP11$r+Ac^^YFgr8NlcKbLV6TprC>SCy)7t#U%EyxVA{1iaIb$9OI!19wP zPqK@Yc$eOzqN}PSBS&^Q|NdAl5bw(+lB--{gVOV^Nz4kV@Nj3~=2K$Fm zXbMN{DG8(Be!z1fT76i1F6A@O$_d45k%MGkD=^X=y`o*+pM zn3mfuK=&6FFlAdt96ev=euy~Cl_tXeweSm{xDHdTEO@UU#%^WSmpFxr4+Nm%JR$A|Ebfs)eZ1e1dWX8-NOcD!DC^>-_>m>WrL9Ub5iREUl% zf)Nsz2VgQ(6+i>dT7`ll@MDx{GT0|*GC2R!HOwa|FDaSf6L&&!qbC7DC+T9xhg!6_ zYv;p{Ax9<*I~Zxc7cT_t?Umqh%B{^rRGY!60~6XzLi<8|f8^OCW8bFHjU?$mKXSXu zJ*E2HMmeEGBcw=4fafsSP+Ke3SEtB`bT9%6O!MZV(bHgZ8%SDlM+4$lzU(1}pCmbU zGQEc=i?soSnqHmq<~~^Vn@NBrtcV@Pb%=nWFFQt)Zlc>ER#9z{TWMQZJ!HrCIAIxH z+3!y$OmpB3u!5$MeDLrg(o@#&BOzu*Gh(cmUr+!XLY=jlg2KNObhX(ke{qQ3d+&qy zUmY;$MBtI&mSIXSiT^zi`gRTunX&h(6Mrh;9~DaS99DJ_0>*R|&B0@T2NE#`JJ)#bp`sGIrOW8HqW=DeU4W;! zb7blm=gm{0__7Kakx0Exoqtoh5NwuaN9}))=_GPtIRuB<+SH)$1R|Y=RcjF8BtFNg{dv*)WMAo-*W-_GD|*3QKmiUT zL>>t6!tA#}5=cH1FH5!34g;7$(@6qbmvZ^3bo%r|Z57;qSeM^X1phFB@>{p0?`O7g zV=;T@5$s;5utP*|g3E85rRwP^^H(cmKZVFW9fzckDwi8MiW!_FR8Cj)%9_(x7?Bt%`6dS9l3)f7NGJ??)AmGDo!6FV!^eP(9G2efcC;5X9Lo5JcocvU)qF4CIp6m zCHIM6Ul}#cVxDvHUT_&^0MISV?u_?dJ3_}Tdn+`Q8nR))?M)tQOQVWbffYZ|;%E-O z-uYVAZjeM=5#AO%H7`G(>QY`2nt>yXcWLF<+_GYvK}Ilh(2`)pCJHd2j9>GdI7j#y zI~kB=s2dwgBerGBA)6Mawt#Q5OW$5RdX4}fN0PEdVNY#h@Nj$VxFjK+18f8Jz>(aA zhE@UP(FJLP8JGxnT~D+j~WXs4~4?eg#1%Km z{-M2Av05N0ZIkns=Zv1-F{Fo(W-~W8A2$1+dITFgI}_33ORP4ncPzjbs8yUgCMy5z zDe68k?i3lQ#H1JgzYIM6ADLSf`JtLqCuKGjKR!`S1Xu)9HZ{F{;lzh_u;fvT;ju$!+RwaQ&kHrey^0 z;Psxe+(e$kxwKZ<6wwh$afqBGBOz8ylh}WG$4FE_*RjN)>CacQ$cXI9|IBErl7(FU zpJanhHG*qP-yz$wIenlTVw0*JKVFF=N=5<-scK19#jOx-D=HK*9ra+Pcl0#AM0tvFGf5?qkh7TUl9Yvf{m#ufTeKEbGY*ljx-M3^~aJ^J7HZ+HPgh z9wP*R8z;IbNJPw`t*tEqxk4hQNSK`S^Uj3HNqXY;Z96`E;TRE^)qVey%!Y|jf5T%XKyn-my*0Znb+@R?!SC4h}(-2b7c@z z`$}C#2w%^(Q&kt94JuIfmjY~cfCHZVwIn37mzI`>g&PwGFJ}UGcYz5a2*apL1LisQ zI6{X|LW=VTsh*=S9IjZ>#OaCdWy;FT+~zKGUWgU)?#!?E2g9s1-M>COqphQJlOS>c zr!@z?mrkGnA$`D2>{dW9c!R_gacq@sA?D)z30e#R*+4)oUGFXD^%J@G(KCp2o@+xy z;D4zz?`_p`)N5jLEzAisYYn84PU4}+IA2$PSxhnRD0?DyT1hANERz-){L zh{H_2;b!*NxF61?)9WEclLb9?AbNwh5BHNu5E2KGn?Xo#HypsK1MA)390+t15R58^ zi0lHS6Xep^bM7>TL=~V^?Ao;vt%T%LQgXD{zK%2Pfp;!n9Htp!6JMDB*+&zt!B2Nm2SUS?=hTg=i9y#d|0t;;TZ9L)fg|t(0C=nZ zPELz7f>>St_!x7p>TTS7=uP5MG*1$|*8dF9-&k!pkVWbJ1KnWBe|_vmneoctW;v)U zK2S+&%_`4bE8G1$d~SYz_|46Yu3zq|Vn-sPhye!Nx9diN@FI+WG#YddyPv(q*`D08 z9&Ch&O(1BZ?|_Zg4XiW{ys^k7Mu@U1){f#Z24P>ZVO9iQ6xtP`;|?{Y}mk%&Og*|XdK;dv5is$yLhAax@~ zqrpU=p>H5P@m>=u_~P>4EP$J7PvL;qxy5zjzA;t_a0D@lf<#m#NsmQgSa8kl?U>Hx zZF4yq=eqRuk&dpeFZ4$tk$e&whXTD15j)9BzOBk~VDH{H@7}2?3}^yM#>6RWY6Ewl ze+}+4Y*&@EJH?D}JQ-jQr)VUEJS#0d#{ul9=;>J|Noyj?NG)_qdG=rn56`eWe8^$f zi#daQM~OG`KGS|QtsYc?)i_CPwwB7O8N1MKdQhH-8;6ldSZTGBC+ncH)Dm$?@w%?v z;`F}vpIHv^#UzB*CA{4c!O=D_5P=bhk=TJ1e*EBB6S;p6Xb8j4iPxOa&>nxjQ%gEI z$1oF+VtWLKBR+tDv54IUA*+1(a%vGZael}!2o+V*eEB~uG(`4)w9pVj*v$L=-#&;T z>*;ll|EQh+uL-FC1~mVT8AA@$c@+Jb7&{RaC20sHe4JDK%L4!~u6h6IG03&Joh|H+ zU@-fR9slNcy3r>CFh@_qewIgA1-q~COZ>yNHyh~(<6Qa~k;g>C-yY@=w%#+aCIQI> z*8L3bpQ2)^@fv`pNap3ZM+nh39v-Nf$f_Vl77{VwK#a}g8D{((o@K8;iN; zsv@bn_C0Hj8u%GH)0z*0J%JBBN2LaCc1~;}A>-shoVx?8JJ{HK-hWkaaXE~9f$!yb zPZoNa-wcHNM7sZ{tL(;M&=<_+2I65PgDsOVlaqb5QwY`+L5$zs-38sX`tVwOnG}8a zD|J@6wMpwZ1}3#s#hnLE2;U>U3OMGd&A=NNHx_)$M6+I*j4{*R;wgidk*%9xom7WU zr&LF9)tH7zGH=lv5*4{316B8yEoa{_ShC;ar#d_SRBCD}La+aL#aYVW<29qFlu!4M zK3;g;NUJY*Wa|1yw_z&iPtRmWLiRivL}f6l@QOn;f|-piI4EfInJnYo2M&Z46-jz{ zdKP1z8nR3xXcwu>xCxAaik7yselnn&wda)_I_y^ zY@Uyg4;}kv(q8P}`;n0yzlQu76exLqD#*zC9d-Y0`SL=A7Q`s9xs4{U>x)WDN9-ht zWXMEk)*0rqbN1+c5Uf&+v(DxQHaBZy%mH4r+1&SbS`JADpZ1J1%p4pVu=haDdyDKh z#%a|dXqVbgFYNroKw3tIEEv;u+1Yo1PZbmth|x);OYi_h9mljIh0Z_kFN6$9OR*7Z z#NzVuD+Jo}1%-sdF}#gWnwg$9ff$F2+KAjhvE9%HFqMR)BwsK~D5BLEo`>yFYmu8i zLdk>s>b>pmL6M)BNKC4nDtmJ1>fimq?fIJ#)%0rXY)72TjYzN6Wip0s+muaAcE(EB zK&v2SWA6=*j2!V?7WbprVg~hB>k!nAm*1N>b9yFW5VDcNYOCPxcbB>xY}VZ@<-O*S z@U@%5@8<0thd0D zI@;S&b)T)t)Wo%)bE+ttGs3keWlhYG=L=rAaN*mBXhmQiTybgU+13IIdaf6dl>R|M zr*w3N9<)U_T43Mtj2T5;M(>URboYs z8r^BfS7?NtSa{9_?KU(?@_ZreKBF@Chp79Cx1FBhqguOr;1?Iz_NiaI$VWv*^#Va8 zo1TIzrK>-jB3CwV+H~Z6P+*_~y#G`KRi_Hni%T`ZjL02>xVDuHK!RdoXAdncl>%Ub zp-zr{*IRs@iiT!m!mC&JHg2W^`f0f&=%CuP4clGmAYYoXvGMmu4n5o7aes$&&cn|i zh_bT5Zu$2WF3;aONy*8?^Y`-dy2XAd6tGa&$mj!dAPr+J$@NW5XX>=K1^^CWfJjwK zE1bRJG{Arc`?gqGAC5HypTh|Tn0R{1D31u$9o78R1By9ZSh1RU z>P7xfR_x&o1Z4fx+B!2oZ_)qSO;1k`?VrH(gfJ};dmHC(qu{e$TwGiiEi5(&uCstr zq^G5s;N;<)`Qck(-iC9&$2>dn#UpLw*8^~@TWF}hSNd&k@65)_$SEkS+#MtOY)2O= z)@4GI27mt40%gZ(xO-H}BH!=`A~wj^J;N7OaC38`WFtJbd9XQAapn7&iX>?Kaay=l zx(l9j+dv;K)!*Aq5SgN)BAA2MisxI+a_&5MKoBUTIXN+@lH2`)zv;Y4v<3mfv4QLn zOaZbE4iA58kqaHD&C@(UjFt6~t&K>>(Hq^71wUC0)yWLeMeUHZgp&bV6uw59jrrSK zrqhF!$eBiCe2~QDF)cs%E&mGb4{SYz64)eXgxeDmg!$w^He9mde4JJHc- z(+D_a^ypAshLP}lYT&{S+tkEF`5jiOsjbAACQ4{v(%!|zMQDHNlN5OmXkn?PIz;Ff z8iom=as{)E(wh4OOb=1wT7`dSW5<;744h2QryCAl-*}xp-K0 zqGmOe`E7$1`R_iDQWG^xIg39{p?6=Ornb~``Sy-gE=fBdhWi#ROO>npQ_e7KjIE@&i$c$=SHIn{1Z21AJ>{-1Zx^UKP|ujp&dw$$ zCJsd|xSxM@kqR;0&cL1{4qqq%hvo8zoSiC;#)ZSC7#<(52hJ^(dEZ^(?TujkI~>t= z$iZ zM{n_Lq>DGvAHC>G?k6-H@Z#}G0Gnjw;JB5vml&Ev=W4J{!(Ys7RP)_mL;v9JU3nFi zK#9N4u&1r;x(sm+Q7(DzB@j!b5G91gbmoY7HSY|lypdk`@ z62JWf&c5GZGsO=d zzUsc_ic8N=7!%p3haJyhLYV6y`QhqTX1p$}B;m^t_A%se&4cROJzH zjjNq6zk*p_gdRdFDk*toQn>+pF8%rQVU!Sr4!dBW@0kI@K(Et_|K z`xy5gCE}2aQxn{??Gn~e?q&FH1YB_ahYwHF(?ft`#Owx-fjOf4pPDdE)rhVtvLCUt zzp@m63DH*ZLx;#1SXotZN5p7`t7|rpN^f6Z`{lgvKhBbXY49~I*U_!MHO8vy={;~i zX=f*lCW}+Pva-@V?x+zj8;2E#T_%r-1&P?q$z-J+crG8}q#h}UvE=^|^ICDWacMd6 zwA>K=%H?+h?f1o*5jPBlK`ABCvWo}f3|xm6zp}cTo|T1f9r13t}F9m_!vU$#t+s80~0#T&0t+yl`ET?BEY)gI)CZ< z5E~_Ix&i0J#-+m(?Ht)7{{Vb3%V@k*e*QL2PECCG1a>m|3BFS%-gACC>p|l$0j$O* z7^c@+z5F2xbC83rC*_<%<<=HQ_J)2ET7%e4cpDf9Z#PgD6Em!WGsfnu*o943v1Ws~rM(1mKz9Fb6<5QIqOd zO5Eq;c<0)&bxM-YFtuU2PUq~|5KJQ@$3P_TaJwm5aScGSbzli(sG=>s-7-K0`W0#L z648h4)Td#3A;R^ezbqCq%`m!wh6XKO@9*zFI5h<`y|)@q;plnm_U7heCMF4|d_TA6 z;xGc(nRjKI?hQ@q_#7Az;I#bs{{_qB(#5{txQDDF#+F~B(P_;9a^<>A88b06lO5I2 z#qQR?CP#+*v9ueLvQF^r+s`25`($SeH*yM&Qo?*F2+RNYZqJ@Q84DX}IgdU_ec~Pp zFYb9rVruH1%F0Ts_^vJk7D2-r1Sr7CJp8m$cS%S@68#qN66Q*2tH|@rue*47cyPE2 zPH1DUy8*ld4RHtpFQB?8*45a3d^a+^Ye8rjgWsB1u%7O^S$t_A$tWIv1R=9bU*!B(vD@@@9^_L$%x6Kwia9@;rEN6?0#>Pl4cVvIIyKjOeQ;4!-;G)eeU z!&Y1jyz42)Z?AwE`yvg_%N&rEeF7fR?>-@j9z_re5_COy!>Kn6F+@)c)}!?wS5q^= z80nY+gZN~EzLW8DghCkB|9nAEhi_>UYpdz(|*HCvM&lSakQbR*z8wT(~dpFLX- zR(uwd;23B40w0XLWZ{QCum-=dFau8BOXD(i%vIsuv3SUS6OwIm7A3-J6-2Eg~dI!f3WQiX0zC zP%WdsvLCYA3Sde`i14>KrC0#$fR`ay?AgDcI=ST?;aF1y{haq9f}K3n~+ zi=rYXZ%A)%ZEqcCIfv)Ow`Y%->$Hi@{h|=)e*9XG-yx!}vo+w(9fs6-9>u^xY|00Z z`(KKfzr7O^qi$#zQ!<#9S+P2K&DGug6*`3tGmgdYiO!~d@+*lY%caQnr}ZTFea{^k z9>xR%pOjQA-V5IjA&+zh9xJgwA+kL8l*-5=wou8Z17c}_^#TUjBo`N#x%ba=a_#~S zj|>e};yd`3rmN|t;>xVw+ZN6ekql)WI~SMKl9Ct5kb#? zKV+mXV&k1BPd>hOo5cmx4B1U%I_GXw)H@v9qaKs}j2Z0>(BIv#;!>U+cX$*H@YRDz zWnzaF=S#doLWn^l{7FWlVG%HCc(9Ln&{o{aI$il!S>c6Pm!9oK zlX6)+0>TWr3%%6H$mlT(9cJF)<*qts&7E_ab14NI-bg`9!Y(U-oEq~R{~D=t!g#wG zSr&|uGy1%?a%cU2+v^#s96&LaB+g7Rkd{Y4;8w<&to~0QzWxU}Pfxi^XFhna6@L+O z>(&>1*@0Ex`3!>1oxj%qx_g{`@ZlL_`x4vO?3I}aZ@#^IDaTgMvV;W)o+}V`_@V}; zU+l5;%CLg?wiK9gDrf_?wzjaO^SwkcM1>edxAjlEa|dSrulCM7tmm}-`(N9PeHlWQ zj9qr3WJz|Blv0$+h*T=ttueNYElScRLMw$fwAm^oON&&hDN2hnrKl*+>#DiuH}`%2 z?q~b+IiBOVkHbAueZJS{y3X@`zR&mhz8+BNys3@EzC@1l+xqCpQKM$2nymDk_8X($ zA+zK-ggplitT=N)m>RgaxXjLR3yErnzz^L9VYR?yqk2<31--o>I2Fh}-NuP(f`@ z^|hEgnynk)-K%)3gSWa=lC4HAKl=w=0!=a=DFzHe#yf`8+v1D8sDgLa2Fk0_*(Zy$ z*O%mC&xga)3Bb$r&*a$a8z289W|_7%HQG_#FTSL4&E4M0>K95Tv%UM!&)9#=&-*18 z+Iq9n*AH@z4Jn2dnu)_vrEQ^UDW=%K`Lo&6*?T36|kt&pk&+1h)Jizm=z3|Ka>v**#5~>{f*6&)xR~QFbAu%u zwZgM~gn7n9Q5g)~Y2d4(uD@?9^Yk1`dgH*-%3qbKi~j7>khm5kOA9&_}V zF-OSPy&;M$`nA**)x#^~I+Y`Xs;;SNyB;QScEdZ@yz_~*pFSCpT2(n&Si+cjZO!I$ zW}}v(B+eQ2w{JbtYq*Gt8>bd-o6)gj$89s`#*Ty1pcX#}KQP6nM0I*qQIU`iW&4j7 zia~)T1(*bltc|gskkDKgCV=@@CSro-#!w4vuiijekqo5Et9(APdYO3#Y1S|~A# zHN2<#$E9)w8``vKBT72zC;_c56r0z2{dVja6%NytlDqpmbU8P5Q|AK5uh;wo!U2*- z(*{tqbwO8PqBv=thIfxwP&&gVPd>!}AuA_Y!&2b>w43=4wLObIdbC2~E0-@nl$DW| zRxvjp|I07Gh)#!^g~g&JOL`9(;*3BiARPyvXCNivDif){*EX*y@v2;5VIi<clqTS_qD{#* z`pSqZt|oo{SP<4c+o5XR;^X~)B~XqY?GzC)ZMw|=lK1y}Mn36H>~=r_Ew7?J{aEkDnS@=<4>|3fI(C#;hMm0~vWtP|6;zuX1!U3@U$2y* zj4=jR(2&oO6#=H!E^g`pPZyW_)H3*@pn|rBvyN=8)y8Ah2BD9II9$9?ep`=!YKoZ{e*{4L#1Pl5INg%as=qPvgPR?C_v@ETwrgO- z4h>T)s)_I{u9{p0thxis*^(4@sjg+{~gb%r@TC5*-hK0*R1C=^Zn18O;u z^t)UHr6dt=jvPP!Fg3Ls6yU4D@ikAzm2ZdZLmTsxv~x& zJa=(SXObp^nZlq6{1OD8MZ+1MD}+us2I|JHwGdp6ZV!;EpA z#tKdSC|8w%%^HL)yM|@)c0cY5oiXU7%-2}I_q;yp55Y;^y0s=(m8u!HRbkEmbQ64o zML+h46iMLY^^NcB%{*f6yoc=au^frHuA*&e&xCKqlfb6uRQUCNnVE-Ew}D9vLM1cNv>g6fJH(AL zx`y({VP}tsGdKT@opx*70caKM@A9+b3n^x`-nKkfz^A&p%9=bwklN0;`1CuohE|Q3 zJ9jRvkUP8U@=i<38BhM@e*1vRixE_Ii0ef^FlW?-;`K)Y1IP4PbTKe+sn2wr#o4pHDf=W7o_FconK2KC9V@pKh~UU9 zje(+;vY@C|Un{7y_>$ms_T#j=%_{1P46S22mGe7ykTBq6GUbENh57ET5k7M{{2}^yp^{QwEB{>fwY_tH1FlJL7Ng;bD z^A7cdt|uIAk%g<-=}3)~e`o>f|0ratlLEt%{^UKtU4Li-At6BQUpiH>16XhWpI~Z_ zy7TBj62F*~dFna+7`;Szr<}^(ihTH@q9X4NgJoN&Ns&t7pJ`xslYpZjYuio;`${P! z*f_!#R97{XA4%%5-1FJ(5Z<>DRD+N#u~Szi??rCTWVa+M%y2V8jDv>{7tu69Y6~Kq z7nT)Haoe_CJG%D5(E=j7?Sz27^e--q=?uAvn`+S8R?hcX-S+*RCtDRQq)IQ%3L8R^ z%N%9j@oSW6zz|?F?HS?G(W#3#k6dBpifObQPSBw>Hipd(vXpF%&dt0u5<5b8L$xn$ zDCEz=zFPyhg+m09*eTWcEp=P@YX!?UyxjwYG0{0?#CPtLJ&m^fRx!Im!|S!Vyq#+G z+drzS3-A3n_Df{bN!}Gnb4vTt3Nfd~rC5rn+@RQJjl*TnovIllD zu8=NOkfU8oEd&E*T%Hlc8=V!oWXTdas8x=^QX89aO|O;?=i0cc@*}DyWxg&heg;bx zEu!xuBv%sXqarclosx14FuF=dXNk7<*+Ev>3urRlC8!f~t&T3ieQDspjrsMjP7+-l+_d40!j9GWr6e&G(y zsDyvzu$@Qlt^b*p*TIAB>C`O1jX?7&aB2qmfS#AO5!pg#@A<#e>X%;icF=OanqDTz zb}*@gcx}zWCedxPv)e@0Vi8uYUM;3y7~d2-@VlOAFNb9Ij_qj71msh8H@nW)mC&_Sx+Gfva+2aiHZs#U^b+@n1utC&fX zkr@vkQpwb<`Lq4}{3N1{DrTY(70`8-+9%j>Y3bicVvt?v5(r@u5hO^j9J(}!8~~Q| zjBOVhy|u5^y=)a3v@LDSM}C3M^YOE1qv)?-AizA4I393^o66FzSU9zy#n`G9lk(>s z^$QFsn?;fE@zL?;tWHZpZp;i5&^I8RdAkQXhI_ChDMGVxIW+Vn-5MM+yPuSq^TcK3 zaxq{8HAs+g&|P45W6jcK%Zd=5`XQCX8a~_Hn4TjIlOv#iI%_{1nAf+o){k)$t)>Qk zo_TuO2D;-kT}>aTA24}e%c=q!BaXDrsn4DeLa3Q&O24iJn zIn@Hv<%8HX(2H4zeZGB;PRKm0g`V|jBO?-jYq%Qd*WSb%hAhQF^T#K99~P;6ZWK)I zr%#^_M`R}-U3u;rWDg%3h}JeHFLdGy0Y<^JOx8iP!#O@vII47szBz@b#hNSWWx`Mo z(g75X7ay*X8TRc!{Cz?)a|#FU6Z{Lf&9QKbS8DqX9U{vzZ|L3r^7UhaL*rMtXS;?k zy-9?Pe&i||jk(vNzTc2NI-iLQ`QzElyYu@7U146Rbs(P8Lzmxd1e^k)L2 za>}n#mlmH`wO4uo708_nEA@u#3Ew4tddv#v=NCG^%o3f6|CZ|^&WrdVZ~l58le{L= z>~$Si>FK3lJ1OA3&}6}=k2$*kLE&AqNNc2N2z8iL?gr`wvol8ejJdzx$zz_>o+p*hE_{}99kpWEj2ZnG zX;86`R8WY1u)0YgvFC12c|{Kp;NdL!LZsl1R$VqqCH!YTsqEzdE#NE1tI;g@@)x^pzI0re%(aX z1zxzYq_>ty_Q#hbbb#+4NKEofotN(0lybJ0jLb8^8|R%*wv0xll|FHv4>d0p21BME z2$F5jdAGu(_-o0%wa5rOc;qjmsh=;Wl$(Yq255N=zxDqszyE-C_GTZ>|g$ouc@ z;E(AQb>r9FI(7H1GW3w)?&=O~Ugea>bN-b9Xrrq(5>Iz!jXv0%s4QQuncSxZ3rB|~ z_fwfJFK@3qFz1P7bo%SpugkicL4YDHf0&pUbVJM{!bJmGe!MHRZBf^d!u|gHdaU9Y zQLJxh2zvaH{6S?c#vfQ{L^ki&~sVU9{E4RnzSx5hX8u#Zf6`plWjWgUQr zNhZ_K=JuU_8a+Ma0%;JbJpLlRgdDz_If26B;zZ7E$F5yBLSC3j--jLoDg}UxS{u1J zZexz*PoXBF^|?$%VJnCPXq;|LxOrpB!B}VJ8{2{Sh|1&)&jA{a!~|cIid(zf!zvu3L9C3e-H=D5+~R0}+zRP&_CRoY6agJ8_VY z|5mdc{xGZTmN+VT!R!+H2OM(l>|wc0y{v3(7^DhH(fKAxJ~>#-h277}az`QnM5P&= zV+j*wRhcs#XN$-ckY7l3QW2#GC&8R`!Au3T&oGzo-KWn(ruKCHSr@0UA~4Z8 zB{S2lrp8*#vikIl2mDR^M`~0^cwPheTabG@5urk*PGh@g;u- z`Phc>KWopSmF7atSY(;`c6uPY(wH(Y=ejHXEIB3^)V8SW5exQr>OE@m?)q(XWg}80 zq?8}J_P+}Wlru8+zaXGu;>9Zm1)U8?)o#MBTPItEc7c0nuh54zMj>XVT07xifnm-> zh3Oj~^+b&6?UJrnhvKdq#e3h0>jEu`XlHblo_Y{Did`3Z@s#hcCT~`ECZBOUi9om< z`jfx)=LfmbEJXZwG~2v{lbvJC9W~=<$r^!wpPrwCYn&R9i33T42Gp{hnNH#0fUmH; zS2nB$oCTHAO+pxnuCcD(E|Z?^eccToBbW>L+}UlQbYeKx21rSDjRSq)A%)&4E@r^6*s zjh;@v;ce?f0MRYFdQlh6d`^GE0eSkh$|m~t0rKtdjth(RYu^2D-8%cpva-K{&&V@K zDAbObi`F^hJX?llK+xCVs_bwlY!nGdaLF8xIvJ}kLJyJSd3899bSzfp`te66k?kl- z9zI+MuUwIF7BLT84mB4|-F>3*!z{t~idU@%+PIj-*V|1?tjV0;_GRq!kuo8)0z<0Gf;}jJ=pl;wXBUdOd zS2>!v;THm3c~A>w@_dD}V>?=;O7*|DvsiOPDHXn#9+HvsoFocrN_q(@W5|{_mKO_B zch@`~EiPR1k*fRh=Mj-KC1e|d)f{eHc3lh*j;+x;uvu7707LmX^Z-g?Y{_;yN`-9d$ zVpDj*ytLMJnJ*M^#nV=_Fjxk82^vR+FqPBV4-5P7XFn|Fns3fB6j6x9nY}Ubx`W@~ zv41dFN6^?>xaUr#PCwjz@G{6p$Pk?hpH`UoSL>@^a!S|WTjK=5EvxAtIIzY6I(%Do z(ISibvy9~+a1849eLW3TpmU2v?csBmrx%VY729uC7tp0T{e_RTR<_i-7VfB4_Ag55 zzqD_t^c~hI_&>v))2^2v>fcLFb`oqbDgcY#>?hP{TORoJhUdfT z;2aWI3s(VfhSPp?I3$9KUAYzmI^IQ~K@js#efuUc2VRx&ib>Sfc-Wx60mq4E1Coi~ z>Dg>AyW;)_ZTXpnMXgC{X`CZ;9Bi7Xtxn8Kc}d$; zUrf@a|9u>hhc)E#g_25cqG$X#pVNdp0&ih5ttKm>S!!Qv>Nx55?W@-MV~KvvAKt%D7J4*0yOHd2x*Cxd ze-db@7Z3LsqQ-V2nQW%XNEO(oXWzby=@*ii-VwjbIhQpGiQnnecE>U$Z8#a=xjltc3W*#v#a8Wf(AklWJ^pG}2SiW)!agme}~2ceZ8U zNQ_6Zb0b31j9@VM>}cQJE{uh=gF;HAmqjTN<=Lm5;Oh|cqe{>0Z`-t1cY;CeIiT9? z*|Tr$tQ(7W2@tCg%mi(R(|iwNsswf2WD0p9bH8>i@xg-ya-L!_!#AEjb1MW-7dePE zWGP4d#*lqXVj^rpI_3OR3(?FgSCMHVibKsXoCO+K(L|asLPvL8X3hXkB}IMi$iTc_ zDEhd|1oDobBp7z0KLZ7uXw#0@zoS(dgGP|D^)VNRnMD@8fB#-<{?7uQO#|OML`H>s zFqO8!tM6vyX?N*v!)#5Z;~(EpGB7eVG4W7cMOH`_C>{5>A*2l9`Qat*c#*&a-aR9$ zSr4h5*{n;)=Zay1V|gS1h>4foMEu!MZ{HY9-;68=z8os-SU^BPPISogw6h4hMOOBu zo7>X2ywca6mX`AJVdgCj%40Lf4V0EH`}0X7d~Nramz2D!I=1$R9!2vlVy~qCOxrQ= zo+r}xUt#w!ZC=;iFyv1KMsF#p4iT}A&L>V(PMs%oPDm>jX&`Q&V^dn?nR2*th4 zv#~iYM7%m`^k^`ME9@2_%3CASL2>S+8QEbX%M@(%H5Bw7lwVY0HF_R%Hl{>8rXkvO|HF_8s|T#fo?qi~f25z+7Yt;nt6 zL&Q`C84#2~Lvrx3z7sa^;>ED0$=Y*2_3P(E)Q6LPPb-CRjHhD{+!pVH|Ms2D4mxtA zBPF>Sj3tZNPB%)Lk3vyjv~5N`?k5>KUV~3`K~b|@UA^Z~Ka6KoVbW8aq~>lB9UGW8 zRiBw|g8BV3nKhEAev*>9QnfNi`!5v;i2vEgz2A2sn1QyW$X04B9WH!sl$DkD-6-{7ZxckUaC7tw8|p<%(qf^bBOepwY{&J20G zT;@tsXwaFMn`;@-44?h&j|al4>289;xVc>8gB5o2g9ir~rzLI;0uncfHAmu!Gp~@a z;&|%(z25n|`jzb#>k8Iy&%wU-9aX8PmpN1XvOGLGpt4~8yWytt^OgeYjA zlOlv6w_N{HntvS?mTasZi62a0d=aEoG9?PtuVg}qw=dJqA_5}eA~N42RB#T7cZm6H zb1#bHNq*YM>r^{7=gxp=s$zs3!XtF;&({5fJV?`DwX7yH7L6j@Mb012Rmp0qdQGR^ z^*LquLIf!3xU&};DNMuUq4}z7Yu~{ouFA!ATbd>dkeTE-71199JM$78n{q57lt2hz z)tr%@`X{`2dl%*%5JTAFNa51C&9hKy?gYpfVMG$$Dt7n1V6a?p+L<@SB~M7h8D~{t|*}*fpm)yFQ!5uxO+snODO@ z>LbxHEZn&9@*SOxWFc{{=b=NUJEIVQZbmTM70D2Tsn1L`Iz`0hV9~?n1wUFUO^k>g z@a)MGydF)w_(@{cr}VV67J|~=D`&Tfh}{r|SQ^phobRy=j)7cBUqKU3n&vI;tou{y zE4MF$Ge2=ZwTp+xV?>*(cuCN*Vy^uYEi>jI0ov6CBG3fVB}mk1AB<$n6u4^UenAs=5t4kco((ul37T2^{h0X%}OZLM5YzU=gE`$`nS%v zch&_*d5NF&@VPcEdnCvb1Y5X9t01O8FRFTewg-Z);7SFCRL}(p>X|NRk(%>I8jxXO zlYmb$@KyXQIluvRb!cS^qO1tB07=6@^vWq0dxQJpDt2|UsXdOgg%({dpfw;}in1|c~!bM@d!KmU9oubz5mID>7xT*mQ3 zUM1VtJ#C#bQ=}V0A|5_=?6SepnQZ8-x<3gh)3Mqn{jFRe^j{C=Muid=W2^Y)#x26z zd>?&ZkhJ7DFcm&2Dtaaovki|<4Hcuz5VUAxQA8c{oGpxW$8@H;gRG|?*il%mZLf^X zf~hPLe3h4cH@b0rER-LEP-XF;0V@M&g5dYV-%20~s%kwX3?4?n(ESeMlDKhd&<)X| zhc;;@=;)m2)DkBQma3zeX{$`sg5{ugYR7CPx_v$|>ddD>hMOI$j~PNqs4tqE}+ zQ*>|NF4(l`N{$k;I5C-~L}s#_bXTCAgY~ydtJ`OqOlvDfPJZY%;PSI`X59u%TRPBs zDwDPdv8kxV_)}Izr2f9M79mK&pWv8x(W0&Hvd+SGn0aePZT6>kHLa+pGh&{1$BkhN z^uqkfDX_Mg07LL$=GqO2$Ga1%|s69bC-Y;|KlQ0*_I^R!aSE9{V9$M5;NhHOnEFA3(jXHR942=E!? zDVQ_#LmVKwMVOg&x=VK`2+BpH58faJh4nzDVMyR9NH`(0X2qmzJ`d0Iud`^>e`3HF z?I2}I7s(=x`sd%jt!&L2U3|jAJ|+ZooYj*7Y=ba|;EUgeBHNzNUN#crA-xQxXP&sOE$2+i*& z&j+QK^=HI^(*_+b9gs)%SwW=q{FUV~Xo5m>rRht(q{ZikxoSR>sG7iKWHvM82 z>xNih(;jur$z zOZxc8XQzK__E<~yV1$M+w|Jm-H|E&z6;mEqrA`41;K-Rwv==VR1LHUOoG3W0x5F4w zBmE|E?@G(scB~G6HLjDHU|#_=Vt(dW;(V|%Qb%LDOlDtGdDBD-*xsWWCp5UH(fAab;Hjq<}Yk_t@j0$ zu%$VHQrA3F3|8xFEh=BMby!`f*`%Wn7u{KC`oYo#fawA4#fygL7jnv3|^~!Ab?sZrw!40W{zi9SdfV@6Z*-kA$6XOnbOUqDh zE*Avp3Ys)CxBqcLw0H1aRapi1kG8n&vv+B*pWl3WuN9!r#{4hw0TjKJZ*HfXeEjg? zOm5{XRBP_*r1D@`g9*-D85Rd?NMOO0+m(lceVyo)I!dMSUJu6$usnM}qBYxRH*&SW zIF%=iS@3jR5PH0tMvnW5@0jJ5p_A6Z+k+dEH-2zFG5vjg{Vf7R7?;wbaWiGVxLl6T z-??1aOilSwB9RQs`y`cta94K96b&xb{Zs@WxI(Ib=eJbKJ>gnaCEgz1v z6}&?QL`M$!4((AbdW`;sH*VfsRk&@9n2G~2IX0&IE^|?8c-OnBs9yq3o@s=iS|0AP zW%Rf{or72r*zL9Q%}9?fJ$@`&PT0h%xDY^fyfs~JY#2M?U=4F#A-8X52ev^bb!V51 zXikq*v5eMZ_OuGFY#~P&OyanTxIW^kqJ_Md z!H!~qm1GBXA2)L3BKA>apWTmw&CK3jn=+)=xXZ`her<-z1^4HJ&qAAS%*`!qR6TYVB%B^XxqX)FX^GL*5_UZz36hlN#{fJ zQ<4mAIZ<>b=F_E|@?Le>GMDG?UAfUHuwCn~?qJ6jnxeC0*REY?E!m%zmQ$IUSdQ^{ zQe_?HZE?WonYN{;Fud8(;lR925S5meYGV6CK3$<)-C}LMiUYwn;JOhwA_xaJ-kCU8 z-d_>ujxs9iRDGLxJEV;UES(hGT zX$~_&x)S|DFAH(He>iZ5M*70M@j3N;3CWxpaQ=9S4p#r7{PKq18=edqLPdwL*;ZD5YQ!GzR;!~QLn@@8e zIbl$ZuebMAe)6V5E!|t)5?t{AvN82=OZr$MaH~u zQ%BqS4x>f4V~ueUqY5~0ym@o#3g2TfbCg(%4dS2d&CJh__jh|S%Pu;Z)2(d&<(sS{ zU;Mdm+p+vH{?I;tnq>LV3vDHmyQ?||@*mgx{Qocgzp@%#Ncrg|iR2GS zb;ItGG|yh@zbIN+eOuF3a{c`2_UjcL6AVe7B~!^!qhBBRKiv|q+-P9mTX!cwc-2|t zL_A(sQrw=+HBULQT?Jg`3(-*%{z*F-Dp{?kH!fz*XJ=b+CT;a_6*RRei=uy#!hJg# zSlig4bh*@=@T`sG?tzJ)x7ytGBSvcxmgv@(fw>z|xxf3=(5Jgos{ULZRI=0Dn>TYS zPotOtRsQ+svZ|-zTMvFcy8UMnZAkV+8<)%Z`35+REpuThwDj~s=xywXGZ4lQusdbN zA)jq#3<|z>4Z`E>{EMf=fR>%T{a7U>1G<~8Be-d)l;bufnK_tr8X}N!<2$tdRcdQ7 z#8a2c@LVR)@8aNZ-KxWtRbciY zE;nk}9e?P1e~F|@PFuZNxxB1Q!`RqgX4i))+2&D4qL3l%m@R{Y>R4G>6?BWvCWd`N z3g~U3?ix?T4+;oa5*{Aza#?-MW!l(fbf9a#Hf@m!;K`JHSE3b$h%P(Lp&{Y5mU6RL-6g}YvES2-n{Xq0l8X3<01*xXg1Fn zU7Dh}FOk(Ohj$GQ3c@PREhjhEj(K}-<$CnCx)gbgPfI8lw<@1Gb!z=VRTZ9n?TrH3 zf=EXEDQwLg9rJH3b|Eb@2flu{frU47n5>T~6Ob;K;q>0-xV?Ech|lHe>zjq@cEkkp zey;(aK+fSRp5ESC78ch=$iiT;cdcMROV-pjtCL8?_%t zs3>gBKmL00WF;lb-Ih?U{3~HWTn1ki%w^;qD>*S8+DP??#g+QR@#CdJEW2{0u#~N3 z0yqE7g!>;pd^p-9%mB8{cv>8|;e_ky(-)B#*fc~XdOk3>3Nwv({;RSw4l_C$8mHNc zwc?t0(KdCj<#P;pecu@j`AY$Cku92~-4hlPYJmvI&s_?CPFfBqHTtYQ}R>Wq_F6`A5|oQ(~%*%IDOWv zan8wNN1|p73Jp~QY@K97rMgQb+YDdNmRb3C#V>zO{BEIEZXzA+qMI%3lE#-C;K ziSLr_-!(WPp~SnR89&Hd6d?cD^!Tf8Uj!>R{FNvhDx=rTg=mKq(8TG&qxd zoDzS#(LFmpdjDy{F8zqLyLZPDoi;{?$1BX572@F`ZPQ@0fFHJgFpK1xTPg3Jb%nii z=)o^1oSnVAhTY1m%H3&K*UuxRcohpfx&JhUJrnCk^9&U|3#YlH6i;;@hB4IrAeD7* z^7P)1XwC9f2DLn&VShj@R(IiK2Y-M6jQspiQE@z&QgF_)69ShsuDeX#w5GrKNLL?~ z*#`4kSZ8jI<)uG){P-oh9|0B_lUwPSP_Wyh@S>AU8l8zVE-u5oQvJCM%4eqCGQ8tU zLLF9cm-p>jTnSP1bsPB3cBWK!r|DCsTqJs%*xSE)Q`e;hh-lZMrV61P097|)Z?}H- z(%N^fE6unXFo$#B3}jrqXAMe}Wi+891gb_WJZ4aLOnk-~UuYUepEwKCcRzG2m*i*8 z45q9b8F!i2hx}qu##x?Pg8^o|K&H)@5y(OktW4+&bLx)ns622|U?IOJ&!1;8z(G&$ z3Y<25K6TEfdsfA2KqoIJ0%bB4Pe?#O^6uLN9XtFB_aKT`T#1GzDN77Xu>V9%1A?Oq zQE`c{aSIdB6&)urnW&NV`t`ZXhkmqS9QYa&2NZewvD-Bei!m3Z5c+9n9A;9^h#fXP zwVOojdR$TGnUW%-$`$HIcUUVECGfAwEGU>{T4)kLM-gM0WfW>Wi|YtzsVBwOlvO0P zWdeftQt$300-$wF9>)?>BllkB{VEVL6IDqvFKcUS@4x#Z-9x2~WM3e8wom6nwe(&q z7z!5e7@hkWPtUO`fYgj{-(tB8b>p2?Tyk|Lz3h0$0ApYSD+@wCEd2WDa;l`c$JdS| zF$f=KMn0+FrC#aqk82~GwW6U;e+_VK>2BMa+i3IWmprr=T>Tym64{_Rv3PtSyXtHkIWcrDO~!A z5oPEJ8WA5&3P|6f)5F#9w>(S3^j@krEMEp(zPvC`ey9p;!eWWrWk7uUY_4izWG#pr zX6O_V2+T}OPL1GWm;yJr69}vTL>Mq=kQVk}l)kGqH80_jwc2pawn)TW&Bn%OJUmRV z_VHZV{ZUGaE4BERusGw4LEW7gk;=@=i@W?ju2R5DMK{x?240O^_kd zg-p-LIHPucwk&}GB}4`Om;UoFsy|#-ijIF`Vsdf_3&Cw7CB}^1jZsk0*&0`OmdnC~ zdA3cz1v3W8e^yx>(_xN+LI78sfDxX)u{C_y*?D%SZ@aBSFQQ4lXa1?V7+ zbIF1t8xBzZd40SV3LTe;jRX3K`k3HoM{j@*kv+fox$M@30l&;QsOgKq?zMd#KPPujco-gBYTQw>9plC@sD9_T%bl z?4SYC(o@`rb6Lu{W52zWIeJIujb*erY1!GPmb$%m35X!GzQk?mxV2paO5Cb4TR?-% z`j^@#uB|tn5$EGnI6{=;6{{ll9IePU>oNYCT|(u~sdMLUohaSxzHMwTe!?1eZU%n3 z@5cG3si~$LHr!u#a`N1{D+3LO_3s*5-!$1tGdg!qZja*XQ}9I0S3bls+r_z%qe3eE4Krx^G@{H~7<&4TsWm<5E{DAG-2z<}iZK z8dt^Ro^uOASAV&{#7%Z(h&_LKcjm_KmtHDO$~(X|(b+S`BJ-W%^ywo5(oH7{vdHIH z-^DtOu|SE?JHdC%l)JTW%WpuGHdR63q`_Q(*DJbO=c_lmZC))rpwV#??QVfCwx3E? za1-K97tYJRjwP8d+>~bdsPQ+UWvMz_ItNnVfYP;q7!X_8uHs)!XP|@x!|Oj!vSg2? z>Yu%_WA^#!2?K`0YqBw6O(Ay%MS_*~Ng;!Q-67EeAq|NcB@+){d7qN6t*kE!!0t}N zi!u$587>JC&$(yq?si$aHNU6r+qvZZg^6=sE9mw=q0I-ByxO07nLqw#{K=V^pVZxD jdh@R&SKSEB-`kAeR@v*2QD8suS1OCt7KYAWzyE&#l=L^f literal 0 HcmV?d00001 diff --git a/examples/multi_selection/src/main.rs b/examples/multi_selection/src/main.rs new file mode 100644 index 00000000..a4ffa199 --- /dev/null +++ b/examples/multi_selection/src/main.rs @@ -0,0 +1,52 @@ +use std::path::PathBuf; + +use eframe::egui; +use egui_file_dialog::FileDialog; + +struct MyApp { + file_dialog: FileDialog, + selected_items: Option>, +} + +impl MyApp { + pub fn new(_cc: &eframe::CreationContext) -> Self { + Self { + file_dialog: FileDialog::new(), + selected_items: None, + } + } +} + +impl eframe::App for MyApp { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Select multiple").clicked() { + self.file_dialog.select_multiple(); + } + + ui.label("Selected items:"); + + if let Some(items) = &self.selected_items { + for item in items { + ui.label(format!("{:?}", item)); + } + } else { + ui.label("None"); + } + + self.file_dialog.update(ctx); + + if let Some(items) = self.file_dialog.take_selected_multiple() { + self.selected_items = Some(items); + } + }); + } +} + +fn main() -> eframe::Result<()> { + eframe::run_native( + "File dialog example", + eframe::NativeOptions::default(), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), + ) +} diff --git a/examples/multilingual/Cargo.toml b/examples/multilingual/Cargo.toml index f2529439..a5b2b394 100644 --- a/examples/multilingual/Cargo.toml +++ b/examples/multilingual/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } +eframe = { workspace = true } egui-file-dialog = { path = "../../"} diff --git a/examples/multilingual/src/main.rs b/examples/multilingual/src/main.rs index 1a3fe6d9..961d2097 100644 --- a/examples/multilingual/src/main.rs +++ b/examples/multilingual/src/main.rs @@ -13,8 +13,16 @@ fn get_labels_german() -> FileDialogLabels { FileDialogLabels { title_select_directory: "📁 Ordner Öffnen".to_string(), title_select_file: "📂 Datei Öffnen".to_string(), + title_select_multiple: "🗐 Mehrere Öffnen".to_string(), title_save_file: "📥 Datei Speichern".to_string(), + cancel: "Abbrechen".to_string(), + overwrite: "Überschreiben".to_string(), + + reload: "⟲ Neu laden".to_string(), + show_hidden: " Versteckte Dateien anzeigen".to_string(), + + heading_pinned: "Angeheftet".to_string(), heading_places: "Orte".to_string(), heading_devices: "Medien".to_string(), heading_removable_devices: "Wechselmedien".to_string(), @@ -27,14 +35,21 @@ fn get_labels_german() -> FileDialogLabels { pictures_dir: "🖼 Fotos".to_string(), videos_dir: "🎞 Videos".to_string(), + pin_folder: "📌 Ordner anheften".to_string(), + unpin_folder: "✖ Ordner loslösen".to_string(), + selected_directory: "Ausgewählter Ordner:".to_string(), selected_file: "Ausgewählte Datei:".to_string(), + selected_items: "Ausgewählte Elemente:".to_string(), file_name: "Dateiname:".to_string(), + file_filter_all_files: "Alle Dateien".to_string(), open_button: "🗀 Öffnen".to_string(), save_button: "📥 Speichern".to_string(), cancel_button: "🚫 Abbrechen".to_string(), + overwrite_file_modal_text: "existiert bereits. Möchtest du es überschreiben?".to_string(), + err_empty_folder_name: "Der Ordnername darf nicht leer sein".to_string(), err_empty_file_name: "Der Dateiname darf nicht leer sein".to_string(), err_directory_exists: "Ein Ordner mit diesem Namen existiert bereits".to_string(), @@ -106,6 +121,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "My egui application", options, - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/examples/multiple_actions/Cargo.toml b/examples/multiple_actions/Cargo.toml index a6aae49e..1219f1b0 100644 --- a/examples/multiple_actions/Cargo.toml +++ b/examples/multiple_actions/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } +eframe = { workspace = true } egui-file-dialog = { path = "../../"} diff --git a/examples/multiple_actions/src/main.rs b/examples/multiple_actions/src/main.rs index 44c620cd..a6dca507 100644 --- a/examples/multiple_actions/src/main.rs +++ b/examples/multiple_actions/src/main.rs @@ -63,6 +63,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "My egui application", options, - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/examples/persistence/Cargo.toml b/examples/persistence/Cargo.toml new file mode 100644 index 00000000..548ab152 --- /dev/null +++ b/examples/persistence/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "persistence" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eframe = { workspace = true } +egui-file-dialog = { path = "../../" } diff --git a/examples/persistence/README.md b/examples/persistence/README.md new file mode 100644 index 00000000..5bfdc199 --- /dev/null +++ b/examples/persistence/README.md @@ -0,0 +1,6 @@ +This example uses eframe to show how the persistent data of the file dialog can be saved. \ +The example uses the `serde` feature to serialize the required data. + +``` +cargo run -p persistence +``` diff --git a/examples/persistence/src/main.rs b/examples/persistence/src/main.rs new file mode 100644 index 00000000..3d180711 --- /dev/null +++ b/examples/persistence/src/main.rs @@ -0,0 +1,67 @@ +use std::path::PathBuf; + +use eframe::egui; +use egui_file_dialog::FileDialog; + +struct MyApp { + file_dialog: FileDialog, + selected_file: Option, +} + +impl MyApp { + pub fn new(cc: &eframe::CreationContext) -> Self { + let mut file_dialog = FileDialog::default(); + + // Load the persistent data of the file dialog. + // Alternatively, you can also use the `FileDialog::storage` builder method. + if let Some(storage) = cc.storage { + *file_dialog.storage_mut() = + eframe::get_value(storage, "file_dialog_storage").unwrap_or_default() + } + + Self { + file_dialog, + selected_file: None, + } + } +} + +impl eframe::App for MyApp { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + // Save the persistent data of the file dialog + eframe::set_value( + storage, + "file_dialog_storage", + self.file_dialog.storage_mut(), + ); + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + if ui.button("Select file").clicked() { + self.file_dialog.select_file(); + } + + ui.label(format!("Selected file: {:?}", self.selected_file)); + + self.file_dialog.update(ctx); + + if let Some(path) = self.file_dialog.take_selected() { + self.selected_file = Some(path); + } + }); + } +} + +fn main() -> eframe::Result<()> { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([1080.0, 720.0]), + ..Default::default() + }; + + eframe::run_native( + "My egui application", + options, + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), + ) +} diff --git a/examples/sandbox/Cargo.toml b/examples/sandbox/Cargo.toml index 129a4031..b4ae62e2 100644 --- a/examples/sandbox/Cargo.toml +++ b/examples/sandbox/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } -egui-file-dialog = { path = "../../"} +eframe = { workspace = true, features = ["serde", "ron"] } +egui-file-dialog = { path = "../../" } diff --git a/examples/sandbox/src/main.rs b/examples/sandbox/src/main.rs index a29a8405..752b4470 100644 --- a/examples/sandbox/src/main.rs +++ b/examples/sandbox/src/main.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use eframe::egui; use egui_file_dialog::{DialogMode, FileDialog}; @@ -8,28 +8,57 @@ struct MyApp { selected_directory: Option, selected_file: Option, + selected_multiple: Option>, saved_file: Option, } impl MyApp { - pub fn new(_cc: &eframe::CreationContext) -> Self { + pub fn new(cc: &eframe::CreationContext) -> Self { + let mut file_dialog = FileDialog::new() + .add_quick_access("Project", |s| { + s.add_path("☆ Examples", "examples"); + s.add_path("📷 Media", "media"); + s.add_path("📂 Source", "src"); + }) + .add_file_filter( + "PNG files", + Arc::new(|p| p.extension().unwrap_or_default() == "png"), + ) + .add_file_filter( + "RS files", + Arc::new(|p| p.extension().unwrap_or_default() == "rs"), + ) + .add_file_filter( + "TOML files", + Arc::new(|p| p.extension().unwrap_or_default() == "toml"), + ) + .id("egui_file_dialog"); + + if let Some(storage) = cc.storage { + *file_dialog.storage_mut() = + eframe::get_value(storage, "file_dialog_storage").unwrap_or_default() + } + Self { - file_dialog: FileDialog::new() - .add_quick_access("Project", |s| { - s.add_path("☆ Examples", "examples"); - s.add_path("📷 Media", "media"); - s.add_path("📂 Source", "src"); - }) - .id("egui_file_dialog"), + file_dialog, selected_directory: None, selected_file: None, + selected_multiple: None, saved_file: None, } } } impl eframe::App for MyApp { + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value( + storage, + "file_dialog_storage", + self.file_dialog.storage_mut(), + ); + } + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ctx, |ui| { ui.heading("My egui application"); @@ -49,6 +78,19 @@ impl eframe::App for MyApp { } ui.label(format!("Selected file: {:?}", self.selected_file)); + if ui.button("Select multiple").clicked() { + self.file_dialog.select_multiple(); + } + ui.label("Selected multiple:"); + + if let Some(items) = &self.selected_multiple { + for item in items { + ui.label(format!("{:?}", item)); + } + } else { + ui.label("None"); + } + ui.add_space(5.0); if ui.button("Save file").clicked() { @@ -63,8 +105,13 @@ impl eframe::App for MyApp { DialogMode::SelectDirectory => self.selected_directory = Some(path), DialogMode::SelectFile => self.selected_file = Some(path), DialogMode::SaveFile => self.saved_file = Some(path), + _ => {} } } + + if let Some(items) = self.file_dialog.take_selected_multiple() { + self.selected_multiple = Some(items); + } }); } } @@ -78,6 +125,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "My egui application", options, - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/examples/save_file/Cargo.toml b/examples/save_file/Cargo.toml index f5a85a5c..c0b4eae2 100644 --- a/examples/save_file/Cargo.toml +++ b/examples/save_file/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } +eframe = { workspace = true } egui-file-dialog = { path = "../../"} diff --git a/examples/save_file/src/main.rs b/examples/save_file/src/main.rs index 0990ed15..6dff005d 100644 --- a/examples/save_file/src/main.rs +++ b/examples/save_file/src/main.rs @@ -37,6 +37,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "File dialog example", eframe::NativeOptions::default(), - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/examples/select_directory/Cargo.toml b/examples/select_directory/Cargo.toml index eccb6324..6114747c 100644 --- a/examples/select_directory/Cargo.toml +++ b/examples/select_directory/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } +eframe = { workspace = true } egui-file-dialog = { path = "../../"} diff --git a/examples/select_directory/src/main.rs b/examples/select_directory/src/main.rs index e4331fb5..1276f061 100644 --- a/examples/select_directory/src/main.rs +++ b/examples/select_directory/src/main.rs @@ -37,6 +37,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "File dialog example", eframe::NativeOptions::default(), - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/examples/select_file/Cargo.toml b/examples/select_file/Cargo.toml index c3d53e15..62583583 100644 --- a/examples/select_file/Cargo.toml +++ b/examples/select_file/Cargo.toml @@ -6,5 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -eframe = { version = "0.27.1", default-features = false, features = ["glow"] } +eframe = { workspace = true } egui-file-dialog = { path = "../../"} diff --git a/examples/select_file/src/main.rs b/examples/select_file/src/main.rs index d330471e..b8d62ec3 100644 --- a/examples/select_file/src/main.rs +++ b/examples/select_file/src/main.rs @@ -37,6 +37,6 @@ fn main() -> eframe::Result<()> { eframe::run_native( "File dialog example", eframe::NativeOptions::default(), - Box::new(|ctx| Box::new(MyApp::new(ctx))), + Box::new(|ctx| Ok(Box::new(MyApp::new(ctx)))), ) } diff --git a/media/customization_demo.png b/media/customization_demo.png index fa78fa8c75da945e23f17a266b6336623d5e9862..ec8f19684d2ab00c0e493359ca590b83e710a187 100644 GIT binary patch literal 43104 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 UWW~*668<-Tw$7};nJa($UwL9)IsgCw diff --git a/media/demo.png b/media/demo.png index 8c0ea1c5f11a116944bee595d0f47a80dca16043..0d23d687e2d7f39fc2126fe4471f074fb7d26efa 100644 GIT binary patch literal 45532 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 diff --git a/src/config/keybindings.rs b/src/config/keybindings.rs new file mode 100644 index 00000000..4d2378b6 --- /dev/null +++ b/src/config/keybindings.rs @@ -0,0 +1,170 @@ +/// Defines a keybinding used for a specific action inside the file dialog. +#[derive(Debug, Clone)] +pub enum KeyBinding { + /// If a single key should be used as a keybinding + Key(egui::Key), + /// If a keyboard shortcut should be used as a keybinding + KeyboardShortcut(egui::KeyboardShortcut), + /// If a pointer button should be used as the keybinding + PointerButton(egui::PointerButton), + /// If a text event should be used as the keybinding. + Text(String), +} + +impl KeyBinding { + /// Creates a new keybinding where a single key is used. + pub fn key(key: egui::Key) -> Self { + Self::Key(key) + } + + /// Creates a new keybinding where a keyboard shortcut is used. + pub fn keyboard_shortcut(modifiers: egui::Modifiers, logical_key: egui::Key) -> Self { + Self::KeyboardShortcut(egui::KeyboardShortcut { + modifiers, + logical_key, + }) + } + + /// Creates a new keybinding where a pointer button is used. + pub fn pointer_button(pointer_button: egui::PointerButton) -> Self { + Self::PointerButton(pointer_button) + } + + /// Creates a new keybinding where a text event is used. + pub fn text(text: String) -> Self { + Self::Text(text) + } + + /// Checks if the keybinding was pressed by the user. + /// + /// # Arguments + /// + /// * `ignore_if_any_focused` - Determines whether keyboard shortcuts pressed while another + /// widget is currently in focus should be ignored. + /// In most cases, this should be enabled so that no shortcuts are executed if, + /// for example, the search text field is currently in focus. With the selection + /// keybindings, however, it is desired that when they are pressed, the text fields + /// lose focus and the keybinding is executed. + pub fn pressed(&self, ctx: &egui::Context, ignore_if_any_focused: bool) -> bool { + let any_focused = ctx.memory(|r| r.focused()).is_some(); + + // We want to suppress keyboard input when any other widget like + // text fields have focus. + if ignore_if_any_focused && any_focused { + return false; + } + + match self { + KeyBinding::Key(k) => ctx.input(|i| i.key_pressed(*k)), + KeyBinding::KeyboardShortcut(s) => ctx.input_mut(|i| i.consume_shortcut(s)), + KeyBinding::PointerButton(b) => ctx.input(|i| i.pointer.button_clicked(*b)), + KeyBinding::Text(s) => ctx.input_mut(|i| { + // We force to suppress the text events when any other widget has focus + if any_focused { + return false; + } + + let mut found_item: Option = None; + + for (i, text) in i + .events + .iter() + .filter_map(|ev| match ev { + egui::Event::Text(t) => Some(t), + _ => None, + }) + .enumerate() + { + if text == s { + found_item = Some(i); + break; + } + } + + if let Some(index) = found_item { + i.events.remove(index); + return true; + } + + false + }), + } + } +} + +/// Stores the keybindings used for the file dialog. +#[derive(Debug, Clone)] +pub struct FileDialogKeyBindings { + /// Shortcut to submit the current action or enter the currently selected directory + pub submit: Vec, + /// Shortcut to cancel the current action + pub cancel: Vec, + /// Shortcut to open the parent directory + pub parent: Vec, + /// Shortcut to go back + pub back: Vec, + /// Shortcut to go forward + pub forward: Vec, + /// Shortcut to reload the file dialog + pub reload: Vec, + /// Shortcut to open the dialog to create a new folder + pub new_folder: Vec, + /// Shortcut to text edit the current path + pub edit_path: Vec, + /// Shortcut to switch to the home directory and text edit the current path + pub home_edit_path: Vec, + /// Shortcut to move the selection one item up + pub selection_up: Vec, + /// Shortcut to move the selection one item down + pub selection_down: Vec, + /// Shortcut to select every item when the dialog is in `DialogMode::SelectMultiple` mode + pub select_all: Vec, +} + +impl FileDialogKeyBindings { + /// Checks wether any of the given keybindings is pressed. + pub fn any_pressed( + ctx: &egui::Context, + keybindings: &Vec, + suppress_if_any_focused: bool, + ) -> bool { + for keybinding in keybindings { + if keybinding.pressed(ctx, suppress_if_any_focused) { + return true; + } + } + + false + } +} + +impl Default for FileDialogKeyBindings { + fn default() -> Self { + use egui::{Key, Modifiers, PointerButton}; + + Self { + submit: vec![KeyBinding::key(Key::Enter)], + cancel: vec![KeyBinding::key(Key::Escape)], + parent: vec![KeyBinding::keyboard_shortcut(Modifiers::ALT, Key::ArrowUp)], + back: vec![ + KeyBinding::pointer_button(PointerButton::Extra1), + KeyBinding::keyboard_shortcut(Modifiers::ALT, Key::ArrowLeft), + KeyBinding::key(Key::Backspace), + ], + forward: vec![ + KeyBinding::pointer_button(PointerButton::Extra2), + KeyBinding::keyboard_shortcut(Modifiers::ALT, Key::ArrowRight), + ], + reload: vec![KeyBinding::key(egui::Key::F5)], + new_folder: vec![KeyBinding::keyboard_shortcut(Modifiers::CTRL, Key::N)], + edit_path: vec![KeyBinding::key(Key::Slash)], + home_edit_path: vec![ + KeyBinding::keyboard_shortcut(Modifiers::SHIFT, egui::Key::Backtick), + KeyBinding::text("~".to_string()), + ], + selection_up: vec![KeyBinding::key(Key::ArrowUp)], + selection_down: vec![KeyBinding::key(Key::ArrowDown)], + select_all: vec![KeyBinding::keyboard_shortcut(Modifiers::CTRL, Key::A)], + } + } +} diff --git a/src/config/labels.rs b/src/config/labels.rs new file mode 100644 index 00000000..f00762a7 --- /dev/null +++ b/src/config/labels.rs @@ -0,0 +1,166 @@ +/// Contains the text labels that the file dialog uses. +/// +/// This is used to enable multiple language support. +/// +/// # Example +/// +/// The following example shows how the default title of the dialog can be displayed +/// in German instead of English. +/// +/// ``` +/// use egui_file_dialog::{FileDialog, FileDialogLabels}; +/// +/// let labels_german = FileDialogLabels { +/// title_select_directory: "📁 Ordner Öffnen".to_string(), +/// title_select_file: "📂 Datei Öffnen".to_string(), +/// title_save_file: "📥 Datei Speichern".to_string(), +/// ..Default::default() +/// }; +/// +/// let file_dialog = FileDialog::new().labels(labels_german); +/// ``` +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct FileDialogLabels { + // ------------------------------------------------------------------------ + // General: + /// The default window title used when the dialog is in `DialogMode::SelectDirectory` mode. + pub title_select_directory: String, + /// The default window title used when the dialog is in `DialogMode::SelectFile` mode. + pub title_select_file: String, + /// The default window title used when the dialog is in `DialogMode::SelectMultiple` mode. + pub title_select_multiple: String, + /// The default window title used when the dialog is in `DialogMode::SaveFile` mode. + pub title_save_file: String, + + /// Text displayed in the buttons to cancel the current action. + pub cancel: String, + /// Text displayed in the buttons to overwrite something, such as a file. + pub overwrite: String, + + // ------------------------------------------------------------------------ + // Top panel: + /// Text used for the option to reload the file dialog. + pub reload: String, + /// Text used for the option to show or hide hidden files and folders. + pub show_hidden: String, + + // ------------------------------------------------------------------------ + // Left panel: + /// Heading of the "Pinned" sections in the left panel + pub heading_pinned: String, + /// Heading of the "Places" section in the left panel + pub heading_places: String, + /// Heading of the "Devices" section in the left panel + pub heading_devices: String, + /// Heading of the "Removable Devices" section in the left panel + pub heading_removable_devices: String, + + /// Name of the home directory + pub home_dir: String, + /// Name of the desktop directory + pub desktop_dir: String, + /// Name of the documents directory + pub documents_dir: String, + /// Name of the downloads directory + pub downloads_dir: String, + /// Name of the audio directory + pub audio_dir: String, + /// Name of the pictures directory + pub pictures_dir: String, + /// Name of the videos directory + pub videos_dir: String, + + // ------------------------------------------------------------------------ + // Central panel: + /// Text used for the option to pin a folder. + pub pin_folder: String, + /// Text used for the option to unpin a folder. + pub unpin_folder: String, + + // ------------------------------------------------------------------------ + // Bottom panel: + /// Text that appears in front of the selected folder preview in the bottom panel. + pub selected_directory: String, + /// Text that appears in front of the selected file preview in the bottom panel. + pub selected_file: String, + /// Text that appears in front of the selected items preview in the bottom panel. + pub selected_items: String, + /// Text that appears in front of the file name input in the bottom panel. + pub file_name: String, + /// Text displayed in the file filter dropdown for the "All Files" option. + pub file_filter_all_files: String, + + /// Button text to open the selected item. + pub open_button: String, + /// Button text to save the file. + pub save_button: String, + /// Button text to cancel the dialog. + pub cancel_button: String, + + // ------------------------------------------------------------------------ + // Modal windows: + /// Text displayed after the path within the modal to overwrite the selected file. + pub overwrite_file_modal_text: String, + + // ------------------------------------------------------------------------ + // Error message: + /// Error if no folder name was specified. + pub err_empty_folder_name: String, + /// Error if no file name was specified. + pub err_empty_file_name: String, + /// Error if the directory already exists. + pub err_directory_exists: String, + /// Error if the file already exists. + pub err_file_exists: String, +} + +impl Default for FileDialogLabels { + /// Creates a new object with the default english labels. + fn default() -> Self { + Self { + title_select_directory: "📁 Select Folder".to_string(), + title_select_file: "📂 Open File".to_string(), + title_select_multiple: "🗐 Select Multiple".to_string(), + title_save_file: "📥 Save File".to_string(), + + cancel: "Cancel".to_string(), + overwrite: "Overwrite".to_string(), + + reload: "⟲ Reload".to_string(), + show_hidden: " Show hidden".to_string(), + + heading_pinned: "Pinned".to_string(), + heading_places: "Places".to_string(), + heading_devices: "Devices".to_string(), + heading_removable_devices: "Removable Devices".to_string(), + + home_dir: "🏠 Home".to_string(), + desktop_dir: "🖵 Desktop".to_string(), + documents_dir: "🗐 Documents".to_string(), + downloads_dir: "📥 Downloads".to_string(), + audio_dir: "🎵 Audio".to_string(), + pictures_dir: "🖼 Pictures".to_string(), + videos_dir: "🎞 Videos".to_string(), + + pin_folder: "📌 Pin folder".to_string(), + unpin_folder: "✖ Unpin folder".to_string(), + + selected_directory: "Selected directory:".to_string(), + selected_file: "Selected file:".to_string(), + selected_items: "Selected items:".to_string(), + file_name: "File name:".to_string(), + file_filter_all_files: "All Files".to_string(), + + open_button: "🗀 Open".to_string(), + save_button: "📥 Save".to_string(), + cancel_button: "🚫 Cancel".to_string(), + + overwrite_file_modal_text: "already exists. Do you want to overwrite it?".to_string(), + + err_empty_folder_name: "Name of the folder cannot be empty".to_string(), + err_empty_file_name: "The file name cannot be empty".to_string(), + err_directory_exists: "A directory with the name already exists".to_string(), + err_file_exists: "A file with the name already exists".to_string(), + } + } +} diff --git a/src/config.rs b/src/config/mod.rs similarity index 65% rename from src/config.rs rename to src/config/mod.rs index bda426e3..4882769d 100644 --- a/src/config.rs +++ b/src/config/mod.rs @@ -1,68 +1,38 @@ +mod labels; +pub use labels::FileDialogLabels; + +mod keybindings; +pub use keybindings::{FileDialogKeyBindings, KeyBinding}; + use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; -/// Function that returns true if the specific item matches the filter. -pub type Filter = Arc bool>; - -/// Sets a specific icon for directory entries. -#[derive(Clone)] -pub struct IconFilter { - /// The icon that should be used. - pub icon: String, - /// Sets a filter function that checks whether a given Path matches the criteria for this icon. - pub filter: Filter, -} - -impl std::fmt::Debug for IconFilter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("IconFilter") - .field("icon", &self.icon) - .finish() - } -} - -/// Stores the display name and the actual path of a quick access link. -#[derive(Debug, Clone)] -pub struct QuickAccessPath { - pub display_name: String, - pub path: PathBuf, -} +use crate::data::DirectoryEntry; -/// Stores a custom quick access section of the file dialog. +/// Contains data of the FileDialog that should be stored persistently. #[derive(Debug, Clone)] -pub struct QuickAccess { - pub canonicalize_paths: bool, - pub heading: String, - pub paths: Vec, +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct FileDialogStorage { + /// The folders the user pinned to the left sidebar. + pub pinned_folders: Vec, + /// If hidden files and folders should be listed inside the directory view. + pub show_hidden: bool, } -impl QuickAccess { - /// Adds a new path to the quick access. - /// - /// Since `fs::canonicalize` is used, both absolute paths and relative paths are allowed. - /// See `FileDialog::canonicalize_paths` for more information. - /// - /// See `FileDialogConfig::add_quick_access` for an example. - pub fn add_path(&mut self, display_name: &str, path: impl Into) { - let path = path.into(); - - let canonicalized_path = match self.canonicalize_paths { - true => fs::canonicalize(&path).unwrap_or(path), - false => path, - }; - - self.paths.push(QuickAccessPath { - display_name: display_name.to_string(), - path: canonicalized_path, - }); +impl Default for FileDialogStorage { + /// Creates a new object with default values + fn default() -> Self { + Self { + pinned_folders: Vec::new(), + show_hidden: false, + } } } /// Contains configuration values of a file dialog. /// -/// The configuration of a file dialog can be set using -/// `FileDialog::with_config` or `FileDialog::overwrite_config`. +/// The configuration of a file dialog can be set using `FileDialog::with_config`. /// /// If you only need to configure a single file dialog, you don't need to /// manually use a `FileDialogConfig` object. `FileDialog` provides setter methods for @@ -92,13 +62,28 @@ impl QuickAccess { #[derive(Debug, Clone)] pub struct FileDialogConfig { // ------------------------------------------------------------------------ - // General options: + // Core: + /// Persistent data of the file dialog. + pub storage: FileDialogStorage, /// The labels that the dialog uses. pub labels: FileDialogLabels, + /// Keybindings used by the file dialog. + pub keybindings: FileDialogKeyBindings, + + // ------------------------------------------------------------------------ + // General options: + /// If the file dialog should be visible as a modal window. + /// This means that the input outside the window is not registered. + pub as_modal: bool, + /// Color of the overlay that is displayed under the modal to prevent user interaction. + pub modal_overlay_color: egui::Color32, /// The first directory that will be opened when the dialog opens. pub initial_directory: PathBuf, /// The default filename when opening the dialog in `DialogMode::SaveFile` mode. pub default_file_name: String, + /// If the user is allowed to select an already existing file when the dialog is + /// in `DialogMode::SaveFile` mode. + pub allow_file_overwrite: bool, /// Sets the separator of the directories when displaying a path. /// Currently only used when the current path is displayed in the top panel. pub directory_separator: String, @@ -107,15 +92,23 @@ pub struct FileDialogConfig { /// The icon that is used to display error messages. pub err_icon: String, + /// The icon that is used to display warning messages. + pub warn_icon: String, /// The default icon used to display files. pub default_file_icon: String, /// The default icon used to display folders. pub default_folder_icon: String, + /// The icon used to display pinned paths in the left panel. + pub pinned_icon: String, /// The icon used to display devices in the left panel. pub device_icon: String, /// The icon used to display removable devices in the left panel. pub removable_device_icon: String, + /// File filters presented to the user in a dropdown. + pub file_filters: Vec, + /// Name of the file filter to be selected by default. + pub default_file_filter: Option, /// Sets custom icons for different files or folders. /// Use `FileDialogConfig::set_file_icon` to add a new icon to this list. pub file_icon_filters: Vec, @@ -167,14 +160,21 @@ pub struct FileDialogConfig { pub show_current_path: bool, /// If the button to text edit the current path should be visible. pub show_path_edit_button: bool, - /// If the reload button in the top panel should be visible. + /// If the menu button containing the reload button and other options should be visible. + pub show_menu_button: bool, + /// If the reload button inside the top panel menu should be visible. pub show_reload_button: bool, + /// If the show hidden files and folders option inside the top panel menu should be visible. + pub show_hidden_option: bool, /// If the search input in the top panel should be visible. pub show_search: bool, /// If the sidebar with the shortcut directories such as /// “Home”, “Documents” etc. should be visible. pub show_left_panel: bool, + /// If pinned folders should be listed in the left sidebar. + /// Disabling this will also disable the functionality to pin a folder. + pub show_pinned_folders: bool, /// If the Places section in the left sidebar should be visible. pub show_places: bool, /// If the Devices section in the left sidebar should be visible. @@ -187,18 +187,28 @@ impl Default for FileDialogConfig { /// Creates a new configuration with default values fn default() -> Self { Self { + storage: FileDialogStorage::default(), labels: FileDialogLabels::default(), + keybindings: FileDialogKeyBindings::default(), + + as_modal: true, + modal_overlay_color: egui::Color32::from_rgba_premultiplied(0, 0, 0, 120), initial_directory: std::env::current_dir().unwrap_or_default(), default_file_name: String::new(), + allow_file_overwrite: true, directory_separator: String::from(">"), canonicalize_paths: true, err_icon: String::from("⚠"), + warn_icon: String::from("⚠"), default_file_icon: String::from("🗋"), default_folder_icon: String::from("🗀"), + pinned_icon: String::from("📌"), device_icon: String::from("🖴"), removable_device_icon: String::from("💾"), + file_filters: Vec::new(), + default_file_filter: None, file_icon_filters: Vec::new(), quick_accesses: Vec::new(), @@ -222,10 +232,13 @@ impl Default for FileDialogConfig { show_new_folder_button: true, show_current_path: true, show_path_edit_button: true, + show_menu_button: true, show_reload_button: true, + show_hidden_option: true, show_search: true, show_left_panel: true, + show_pinned_folders: true, show_places: true, show_devices: true, show_removable_devices: true, @@ -234,6 +247,56 @@ impl Default for FileDialogConfig { } impl FileDialogConfig { + /// Sets the storage used by the file dialog. + /// Storage includes all data that is persistently stored between multiple + /// file dialog instances. + pub fn storage(mut self, storage: FileDialogStorage) -> Self { + self.storage = storage; + self + } + + /// Adds a new file filter the user can select from a dropdown widget. + /// + /// NOTE: The name must be unique. If a filter with the same name already exists, + /// it will be overwritten. + /// + /// # Arguments + /// + /// * `name` - Display name of the filter + /// * `filter` - Sets a filter function that checks whether a given + /// Path matches the criteria for this filter. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// use egui_file_dialog::FileDialogConfig; + /// + /// let config = FileDialogConfig::default() + /// .add_file_filter( + /// "PNG files", + /// Arc::new(|path| path.extension().unwrap_or_default() == "png")) + /// .add_file_filter( + /// "JPG files", + /// Arc::new(|path| path.extension().unwrap_or_default() == "jpg")); + /// ``` + pub fn add_file_filter(mut self, name: &str, filter: Filter) -> Self { + let id = egui::Id::new(name); + + if let Some(item) = self.file_filters.iter_mut().find(|p| p.id == id) { + item.filter = filter.clone(); + return self; + } + + self.file_filters.push(FileFilter { + id, + name: name.to_string(), + filter, + }); + + self + } + /// Sets a new icon for specific files or folders. /// /// # Arguments @@ -293,122 +356,83 @@ impl FileDialogConfig { } } -/// Contains the text labels that the file dialog uses. -/// -/// This is used to enable multiple language support. -/// -/// # Example -/// -/// The following example shows how the default title of the dialog can be displayed -/// in German instead of English. -/// -/// ``` -/// use egui_file_dialog::{FileDialog, FileDialogLabels}; -/// -/// let labels_german = FileDialogLabels { -/// title_select_directory: "📁 Ordner Öffnen".to_string(), -/// title_select_file: "📂 Datei Öffnen".to_string(), -/// title_save_file: "📥 Datei Speichern".to_string(), -/// ..Default::default() -/// }; -/// -/// let file_dialog = FileDialog::new().labels(labels_german); -/// ``` -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct FileDialogLabels { - // ------------------------------------------------------------------------ - // General: - /// The default window title used when the dialog is in `DialogMode::SelectDirectory` mode. - pub title_select_directory: String, - /// The default window title used when the dialog is in `DialogMode::SelectFile` mode. - pub title_select_file: String, - /// The default window title used when the dialog is in `DialogMode::SaveFile` mode. - pub title_save_file: String, +/// Function that returns true if the specific item matches the filter. +pub type Filter = Arc bool + Send + Sync>; - // ------------------------------------------------------------------------ - // Left panel: - /// Heading of the "Places" section in the left panel - pub heading_places: String, - /// Heading of the "Devices" section in the left panel - pub heading_devices: String, - /// Heading of the "Removable Devices" section in the left panel - pub heading_removable_devices: String, - - /// Name of the home directory - pub home_dir: String, - /// Name of the desktop directory - pub desktop_dir: String, - /// Name of the documents directory - pub documents_dir: String, - /// Name of the downloads directory - pub downloads_dir: String, - /// Name of the audio directory - pub audio_dir: String, - /// Name of the pictures directory - pub pictures_dir: String, - /// Name of the videos directory - pub videos_dir: String, +/// Defines a specific file filter that the user can select from a dropdown. +#[derive(Clone)] +pub struct FileFilter { + /// The ID of the file filter, used internally for identification. + pub id: egui::Id, + /// The display name of the file filter + pub name: String, + /// Sets a filter function that checks whether a given Path matches the criteria for this file. + pub filter: Filter, +} - // ------------------------------------------------------------------------ - // Bottom panel: - /// Text that appears in front of the selected folder preview in the bottom panel. - pub selected_directory: String, - /// Text that appears in front of the selected file preview in the bottom panel. - pub selected_file: String, - /// Text that appears in front of the file name input in the bottom panel. - pub file_name: String, - - /// Button text to open the selected item. - pub open_button: String, - /// Button text to save the file. - pub save_button: String, - /// Button text to cancel the dialog. - pub cancel_button: String, +impl std::fmt::Debug for FileFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FileFilter") + .field("name", &self.name) + .finish() + } +} - // ------------------------------------------------------------------------ - // Error message: - /// Error if no folder name was specified. - pub err_empty_folder_name: String, - /// Error if no file name was specified. - pub err_empty_file_name: String, - /// Error if the directory already exists. - pub err_directory_exists: String, - /// Error if the file already exists. - pub err_file_exists: String, +/// Sets a specific icon for directory entries. +#[derive(Clone)] +pub struct IconFilter { + /// The icon that should be used. + pub icon: String, + /// Sets a filter function that checks whether a given Path matches the criteria for this icon. + pub filter: Filter, } -impl Default for FileDialogLabels { - /// Creates a new object with the default english labels. - fn default() -> Self { - Self { - title_select_directory: "📁 Select Folder".to_string(), - title_select_file: "📂 Open File".to_string(), - title_save_file: "📥 Save File".to_string(), - - heading_places: "Places".to_string(), - heading_devices: "Devices".to_string(), - heading_removable_devices: "Removable Devices".to_string(), - - home_dir: "🏠 Home".to_string(), - desktop_dir: "🖵 Desktop".to_string(), - documents_dir: "🗐 Documents".to_string(), - downloads_dir: "📥 Downloads".to_string(), - audio_dir: "🎵 Audio".to_string(), - pictures_dir: "🖼 Pictures".to_string(), - videos_dir: "🎞 Videos".to_string(), - - selected_directory: "Selected directory:".to_string(), - selected_file: "Selected file:".to_string(), - file_name: "File name:".to_string(), - - open_button: "🗀 Open".to_string(), - save_button: "📥 Save".to_string(), - cancel_button: "🚫 Cancel".to_string(), - - err_empty_folder_name: "Name of the folder cannot be empty".to_string(), - err_empty_file_name: "The file name cannot be empty".to_string(), - err_directory_exists: "A directory with the name already exists".to_string(), - err_file_exists: "A file with the name already exists".to_string(), - } +impl std::fmt::Debug for IconFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("IconFilter") + .field("icon", &self.icon) + .finish() + } +} + +/// Stores the display name and the actual path of a quick access link. +#[derive(Debug, Clone)] +pub struct QuickAccessPath { + /// Name of the path that is shown inside the left panel. + pub display_name: String, + /// Absolute or relative path to the folder. + pub path: PathBuf, +} + +/// Stores a custom quick access section of the file dialog. +#[derive(Debug, Clone)] +pub struct QuickAccess { + /// If the path's inside the quick access section should be canonicalized. + canonicalize_paths: bool, + /// Name of the quick access section displayed inside the left panel. + pub heading: String, + /// Path's contained inside the quick access section. + pub paths: Vec, +} + +impl QuickAccess { + /// Adds a new path to the quick access. + /// + /// Since `fs::canonicalize` is used, both absolute paths and relative paths are allowed. + /// See `FileDialog::canonicalize_paths` for more information. + /// + /// See `FileDialogConfig::add_quick_access` for an example. + pub fn add_path(&mut self, display_name: &str, path: impl Into) { + let path = path.into(); + + let canonicalized_path = match self.canonicalize_paths { + true => fs::canonicalize(&path).unwrap_or(path), + false => path, + }; + + self.paths.push(QuickAccessPath { + display_name: display_name.to_string(), + path: canonicalized_path, + }); } } diff --git a/src/create_directory_dialog.rs b/src/create_directory_dialog.rs index 6bceab20..9917f6df 100644 --- a/src/create_directory_dialog.rs +++ b/src/create_directory_dialog.rs @@ -44,6 +44,8 @@ pub struct CreateDirectoryDialog { error: Option, /// If we should scroll to the error in the next frame scroll_to_error: bool, + /// If the text input should request focus in the next frame + request_focus: bool, } impl CreateDirectoryDialog { @@ -57,6 +59,7 @@ impl CreateDirectoryDialog { input: String::new(), error: None, scroll_to_error: false, + request_focus: true, } } @@ -69,11 +72,23 @@ impl CreateDirectoryDialog { self.directory = Some(directory); } - /// Closes and resets the dialog. + /// Closes and resets the dialog without creating the directory. pub fn close(&mut self) { self.reset(); } + /// Tries to create the given folder. + pub fn submit(&mut self) -> CreateDirectoryResponse { + // Only necessary in the event of an error + self.request_focus = true; + + if self.error.is_none() { + return self.create_directory(); + } + + CreateDirectoryResponse::new_empty() + } + /// Main update function of the dialog. Should be called in every frame /// in which the dialog is to be displayed. pub fn update( @@ -90,28 +105,36 @@ impl CreateDirectoryDialog { ui.horizontal(|ui| { ui.label(&config.default_folder_icon); - let response = ui.text_edit_singleline(&mut self.input); + let text_edit_response = ui.text_edit_singleline(&mut self.input); if self.init { - response.scroll_to_me(Some(egui::Align::Center)); - response.request_focus(); + text_edit_response.scroll_to_me(Some(egui::Align::Center)); + text_edit_response.request_focus(); self.error = self.validate_input(&config.labels); self.init = false; + self.request_focus = false; + } + + if self.request_focus { + text_edit_response.request_focus(); + self.request_focus = false; } - if response.changed() { + if text_edit_response.changed() { self.error = self.validate_input(&config.labels); } - if ui - .add_enabled(self.error.is_none(), egui::Button::new("✔")) - .clicked() - { - result = self.create_directory(); + let apply_button_response = + ui.add_enabled(self.error.is_none(), egui::Button::new("✔")); + + if apply_button_response.clicked() { + result = self.submit(); } - if ui.button("✖").clicked() { + if ui.button("✖").clicked() + || (text_edit_response.lost_focus() && !apply_button_response.contains_pointer()) + { self.close(); } }); diff --git a/src/data/directory_content.rs b/src/data/directory_content.rs index da05dc77..9d947f8d 100644 --- a/src/data/directory_content.rs +++ b/src/data/directory_content.rs @@ -1,17 +1,20 @@ use std::path::{Path, PathBuf}; use std::{fs, io}; -use crate::FileDialogConfig; +use crate::config::{FileDialogConfig, FileFilter}; /// Contains the metadata of a directory item. /// This struct is mainly there so that the metadata can be loaded once and not that /// a request has to be sent to the OS every frame using, for example, `path.is_file()`. -#[derive(Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct DirectoryEntry { path: PathBuf, is_directory: bool, is_system_file: bool, icon: String, + /// If the item is marked as selected as part of a multi selection. + pub selected: bool, } impl DirectoryEntry { @@ -22,9 +25,15 @@ impl DirectoryEntry { is_directory: path.is_dir(), is_system_file: !path.is_dir() && !path.is_file(), icon: gen_path_icon(config, path), + selected: false, } } + /// Checks if the path of the current directory entry matches the other directory entry. + pub fn path_eq(&self, other: &DirectoryEntry) -> bool { + other.as_path() == self.as_path() + } + /// Returns true if the item is a directory. /// False is returned if the item is a file or the path did not exist when the /// DirectoryEntry object was created. @@ -50,6 +59,11 @@ impl DirectoryEntry { } /// Returns the path of the directory item. + pub fn as_path(&self) -> &Path { + &self.path + } + + /// Clones the path of the directory item. pub fn to_path_buf(&self) -> PathBuf { self.path.clone() } @@ -88,6 +102,11 @@ impl DirectoryEntry { "" }) } + + /// Returns whether the path this DirectoryEntry points to is considered hidden. + pub fn is_hidden(&self) -> bool { + is_path_hidden(self) + } } /// Contains the content of a directory. @@ -114,16 +133,77 @@ impl DirectoryContent { }) } - /// Very simple wrapper methods of the contents .iter() method. - /// No trait is implemented since this is currently only used internal. - pub fn iter(&self) -> std::slice::Iter<'_, DirectoryEntry> { - self.content.iter() + /// Checks if the given directory entry is visible with the applied filters. + fn is_entry_visible( + dir_entry: &DirectoryEntry, + show_hidden: bool, + search_value: &str, + file_filter: Option<&FileFilter>, + ) -> bool { + if !search_value.is_empty() + && !dir_entry + .file_name() + .to_lowercase() + .contains(&search_value.to_lowercase()) + { + return false; + } + + if !show_hidden && dir_entry.is_hidden() { + return false; + } + + if let Some(file_filter) = file_filter { + if dir_entry.is_file() && !(file_filter.filter)(dir_entry.as_path()) { + return false; + } + } + + true + } + + pub fn filtered_iter<'s>( + &'s self, + show_hidden: bool, + search_value: &'s str, + file_filter: Option<&'s FileFilter>, + ) -> impl Iterator + 's { + self.content + .iter() + .filter(move |p| Self::is_entry_visible(p, show_hidden, search_value, file_filter)) + } + + pub fn filtered_iter_mut<'s>( + &'s mut self, + show_hidden: bool, + search_value: &'s str, + file_filter: Option<&'s FileFilter>, + ) -> impl Iterator + 's { + self.content + .iter_mut() + .filter(move |p| Self::is_entry_visible(p, show_hidden, search_value, file_filter)) + } + + pub fn reset_multi_selection(&mut self) { + for item in self.content.iter_mut() { + item.selected = false; + } + } + + /// Returns the number of elements inside the directory. + pub fn len(&self) -> usize { + self.content.len() } /// Pushes a new item to the content. pub fn push(&mut self, item: DirectoryEntry) { self.content.push(item); } + + /// Clears the items inside the directory. + pub fn clear(&mut self) { + self.content.clear(); + } } /// Loads the contents of the given directory. @@ -162,11 +242,28 @@ fn load_directory( }, }); - // TODO: Implement "Show hidden files and folders" option - Ok(result) } +#[cfg(windows)] +fn is_path_hidden(item: &DirectoryEntry) -> bool { + use std::os::windows::fs::MetadataExt; + + match fs::metadata(item.as_path()) { + Ok(metadata) => metadata.file_attributes() & 0x2 > 0, + Err(_) => false, + } +} + +#[cfg(not(windows))] +fn is_path_hidden(item: &DirectoryEntry) -> bool { + if item.file_name().bytes().next() == Some(b'.') { + return true; + } + + false +} + /// Generates the icon for the specific path. /// The default icon configuration is taken into account, as well as any configured file icon filters. fn gen_path_icon(config: &FileDialogConfig, path: &Path) -> String { diff --git a/src/file_dialog.rs b/src/file_dialog.rs index 02ca31d4..f324472b 100644 --- a/src/file_dialog.rs +++ b/src/file_dialog.rs @@ -3,20 +3,27 @@ use std::{fs, io}; use egui::text::{CCursor, CCursorRange}; -use crate::config::{FileDialogConfig, FileDialogLabels, Filter, QuickAccess}; +use crate::config::{ + FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, FileFilter, + Filter, QuickAccess, +}; use crate::create_directory_dialog::CreateDirectoryDialog; use crate::data::{DirectoryContent, DirectoryEntry, Disk, Disks, UserDirectories}; +use crate::modals::{FileDialogModal, ModalAction, ModalState, OverwriteFileModal}; /// Represents the mode the file dialog is currently in. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum DialogMode { - /// When the dialog is currently used to select a file + /// When the dialog is currently used to select a single file. SelectFile, - /// When the dialog is currently used to select a directory + /// When the dialog is currently used to select a single directory. SelectDirectory, - /// When the dialog is currently used to save a file + /// When the dialog is currently used to select multiple files and directories. + SelectMultiple, + + /// When the dialog is currently used to save a file. SaveFile, } @@ -32,6 +39,9 @@ pub enum DialogState { /// The user has selected a folder or file or specified a destination path for saving a file. Selected(PathBuf), + /// The user has finished selecting multiple files and folders. + SelectedMultiple(Vec), + /// The user cancelled the dialog and didn't select anything. Cancelled, } @@ -65,6 +75,10 @@ pub struct FileDialog { /// The configuration of the file dialog config: FileDialogConfig, + /// Stack of modal windows to be displayed. + /// The top element is what is currently being rendered. + modals: Vec>, + /// The mode the dialog is currently in mode: DialogMode, /// The state the dialog is currently in @@ -79,6 +93,9 @@ pub struct FileDialog { /// This ID is not used internally. operation_id: Option, + /// The currently used window ID. + window_id: egui::Id, + /// The user directories like Home or Documents. /// These are loaded once when the dialog is created or when the refresh() method is called. user_directories: Option, @@ -107,6 +124,9 @@ pub struct FileDialog { path_edit_visible: bool, /// Buffer holding the text when the user edits the current path. path_edit_value: String, + /// If the path edit should be initialized. Unlike `path_edit_request_focus`, + /// this also sets the cursor to the end of the text input field. + path_edit_activate: bool, /// If the text edit of the path should request focus in the next frame. path_edit_request_focus: bool, @@ -118,11 +138,30 @@ pub struct FileDialog { /// This variables contains the error message if the file_name_input is invalid. /// This can be the case, for example, if a file or folder with the name already exists. file_name_input_error: Option, + /// If the file name input text field should request focus in the next frame. + file_name_input_request_focus: bool, + /// The file filter the user selected + selected_file_filter: Option, /// If we should scroll to the item selected by the user in the next frame. scroll_to_selection: bool, /// Buffer containing the value of the search input. search_value: String, + /// If the search should be initialized in the next frame. + init_search: bool, + + /// If any widget was focused in the last frame. + /// This is used to prevent the dialog from closing when pressing the escape key + /// inside a text input. + any_focused_last_frame: bool, +} + +/// this tests if file dialog is send. +#[cfg(test)] +fn test_prop() {} +#[test] +fn test() { + test_prop::() } impl Default for FileDialog { @@ -141,15 +180,19 @@ impl FileDialog { Self { config: FileDialogConfig::default(), + modals: Vec::new(), + mode: DialogMode::SelectDirectory, state: DialogState::Closed, show_files: true, operation_id: None, + window_id: egui::Id::new("file_dialog"), + user_directories: UserDirectories::new(true), system_disks: Disks::new_with_refreshed_list(true), - directory_stack: vec![], + directory_stack: Vec::new(), directory_offset: 0, directory_content: DirectoryContent::new(), directory_error: None, @@ -158,20 +201,28 @@ impl FileDialog { path_edit_visible: false, path_edit_value: String::new(), + path_edit_activate: false, path_edit_request_focus: false, selected_item: None, file_name_input: String::new(), file_name_input_error: None, + file_name_input_request_focus: true, + selected_file_filter: None, scroll_to_selection: false, search_value: String::new(), + init_search: false, + + any_focused_last_frame: false, } } /// Creates a new file dialog object and initializes it with the specified configuration. pub fn with_config(config: FileDialogConfig) -> Self { - Self::new().overwrite_config(config) + let mut obj = Self::new(); + *obj.config_mut() = config; + obj } // ------------------------------------------------- @@ -248,7 +299,17 @@ impl FileDialog { } if mode == DialogMode::SaveFile { - self.file_name_input = self.config.default_file_name.clone(); + self.file_name_input + .clone_from(&self.config.default_file_name); + } + + // Select the default file filter + if let Some(name) = &self.config.default_file_filter { + for filter in &self.config.file_filters { + if filter.name == name.as_str() { + self.selected_file_filter = Some(filter.id); + } + } } self.mode = mode; @@ -256,6 +317,11 @@ impl FileDialog { self.show_files = show_files; self.operation_id = operation_id.map(String::from); + self.window_id = match self.config.id { + Some(id) => id, + None => egui::Id::new(self.get_window_title()), + }; + self.load_directory(&self.gen_initial_directory(&self.config.initial_directory)) } @@ -276,7 +342,17 @@ impl FileDialog { /// /// The function ignores the result of the initial directory loading operation. pub fn select_file(&mut self) { - let _ = self.open(DialogMode::SelectFile, false, None); + let _ = self.open(DialogMode::SelectFile, true, None); + } + + /// Shortcut function to open the file dialog to prompt the user to select multiple + /// files and folders. + /// This function resets the file dialog. Configuration variables such as `initial_directory` + /// are retained. + /// + /// The function ignores the result of the initial directory loading operation. + pub fn select_multiple(&mut self) { + let _ = self.open(DialogMode::SelectMultiple, true, None); } /// Shortcut function to open the file dialog to prompt the user to save a file. @@ -296,6 +372,7 @@ impl FileDialog { return self; } + self.update_keybindings(ctx); self.update_ui(ctx); self @@ -303,7 +380,6 @@ impl FileDialog { // ------------------------------------------------- // Setter: - /// Overwrites the configuration of the file dialog. /// /// This is useful when you want to configure multiple `FileDialog` objects with the @@ -362,6 +438,10 @@ impl FileDialog { /// } /// } /// ``` + #[deprecated( + since = "0.6.0", + note = "use `FileDialog::with_config` and `FileDialog::config_mut` instead" + )] pub fn overwrite_config(mut self, config: FileDialogConfig) -> Self { self.config = config; self @@ -372,6 +452,25 @@ impl FileDialog { &mut self.config } + /// Sets the storage used by the file dialog. + /// Storage includes all data that is persistently stored between multiple + /// file dialog instances. + pub fn storage(mut self, storage: FileDialogStorage) -> Self { + self.config.storage = storage; + self + } + + /// Mutably borrow internal storage. + pub fn storage_mut(&mut self) -> &mut FileDialogStorage { + &mut self.config.storage + } + + /// Sets the keybindings used by the file dialog. + pub fn keybindings(mut self, keybindings: FileDialogKeyBindings) -> Self { + self.config.keybindings = keybindings; + self + } + /// Sets the labels the file dialog uses. /// /// Used to enable multiple language support. @@ -387,6 +486,21 @@ impl FileDialog { &mut self.config.labels } + /// If the file dialog window should be displayed as a modal. + /// + /// If the window is displayed as modal, the area outside the dialog can no longer be + /// interacted with and an overlay is displayed. + pub fn as_modal(mut self, as_modal: bool) -> Self { + self.config.as_modal = as_modal; + self + } + + /// Sets the color of the overlay when the dialog is displayed as a modal window. + pub fn modal_overlay_color(mut self, modal_overlay_color: egui::Color32) -> Self { + self.config.modal_overlay_color = modal_overlay_color; + self + } + /// Sets the first loaded directory when the dialog opens. /// If the path is a file, the file's parent directory is used. If the path then has no /// parent directory or cannot be loaded, the user will receive an error. @@ -396,7 +510,7 @@ impl FileDialog { /// Since `fs::canonicalize` is used, both absolute paths and relative paths are allowed. /// See `FileDialog::canonicalize_paths` for more information. pub fn initial_directory(mut self, directory: PathBuf) -> Self { - self.config.initial_directory = directory.clone(); + self.config.initial_directory.clone_from(&directory); self } @@ -406,6 +520,16 @@ impl FileDialog { self } + /// Sets if the user is allowed to select an already existing file when the dialog is in + /// `DialogMode::SaveFile` mode. + /// + /// If this is enabled, the user will receive a modal asking whether the user really + /// wants to overwrite an existing file. + pub fn allow_file_overwrite(mut self, allow_file_overwrite: bool) -> Self { + self.config.allow_file_overwrite = allow_file_overwrite; + self + } + /// Sets the separator of the directories when displaying a path. /// Currently only used when the current path is displayed in the top panel. pub fn directory_separator(mut self, separator: &str) -> Self { @@ -464,6 +588,44 @@ impl FileDialog { self } + /// Adds a new file filter the user can select from a dropdown widget. + /// + /// NOTE: The name must be unique. If a filter with the same name already exists, + /// it will be overwritten. + /// + /// # Arguments + /// + /// * `name` - Display name of the filter + /// * `filter` - Sets a filter function that checks whether a given + /// Path matches the criteria for this filter. + /// + /// # Examples + /// + /// ``` + /// use std::sync::Arc; + /// use egui_file_dialog::FileDialog; + /// + /// FileDialog::new() + /// .add_file_filter( + /// "PNG files", + /// Arc::new(|path| path.extension().unwrap_or_default() == "png")) + /// .add_file_filter( + /// "JPG files", + /// Arc::new(|path| path.extension().unwrap_or_default() == "jpg")); + /// ``` + pub fn add_file_filter(mut self, name: &str, filter: Filter) -> Self { + self.config = self.config.add_file_filter(name, filter); + self + } + + /// Name of the file filter to be selected by default. + /// + /// No file filter is selected if there is no file filter with that name. + pub fn default_file_filter(mut self, name: &str) -> Self { + self.config.default_file_filter = Some(name.to_string()); + self + } + /// Sets a new icon for specific files or folders. /// /// # Arguments @@ -641,14 +803,34 @@ impl FileDialog { self } - /// Sets whether the reload button should be visible in the top panel. + /// Sets whether the menu with the reload button and other options should be visible + /// inside the top panel. /// /// Has no effect when `FileDialog::show_top_panel` is disabled. + pub fn show_menu_button(mut self, show_menu_button: bool) -> Self { + self.config.show_menu_button = show_menu_button; + self + } + + /// Sets whether the reload button inside the top panel menu should be visible. + /// + /// Has no effect when `FileDialog::show_top_panel` or + /// `FileDialog::show_menu_button` is disabled. pub fn show_reload_button(mut self, show_reload_button: bool) -> Self { self.config.show_reload_button = show_reload_button; self } + /// Sets whether the show hidden files and folders option inside the top panel + /// menu should be visible. + /// + /// Has no effect when `FileDialog::show_top_panel` or + /// `FileDialog::show_menu_button` is disabled. + pub fn show_hidden_option(mut self, show_hidden_option: bool) -> Self { + self.config.show_hidden_option = show_hidden_option; + self + } + /// Sets whether the search input should be visible in the top panel. /// /// Has no effect when `FileDialog::show_top_panel` is disabled. @@ -664,6 +846,13 @@ impl FileDialog { self } + /// Sets if pinned folders should be listed in the left sidebar. + /// Disabling this will also disable the functionality to pin a folder. + pub fn show_pinned_folders(mut self, show_pinned_folders: bool) -> Self { + self.config.show_pinned_folders = show_pinned_folders; + self + } + /// Sets if the "Places" section should be visible in the left sidebar. /// The Places section contains the user directories such as Home or Documents. /// @@ -722,6 +911,36 @@ impl FileDialog { } } + /// Returns a list of the files and folders the user selected, when the dialog is in + /// `DialogMode::SelectMultiple` mode. + /// + /// None is returned when the user has not yet selected an item. + pub fn selected_multiple(&self) -> Option> { + match &self.state { + DialogState::SelectedMultiple(items) => { + Some(items.iter().map(|f| f.as_path()).collect()) + } + _ => None, + } + } + + /// Returns a list of the files and folders the user selected, when the dialog is in + /// `DialogMode::SelectMultiple` mode. + /// Unlike `FileDialog::selected_multiple`, this method returns the selected paths only once + /// and sets the dialog's state to `DialogState::Closed`. + /// + /// None is returned when the user has not yet selected an item. + pub fn take_selected_multiple(&mut self) -> Option> { + match &mut self.state { + DialogState::SelectedMultiple(items) => { + let items = std::mem::take(items); + self.state = DialogState::Closed; + Some(items) + } + _ => None, + } + } + /// Returns the ID of the operation for which the dialog is currently being used. /// /// See `FileDialog::open` for more information. @@ -746,9 +965,19 @@ impl FileDialog { fn update_ui(&mut self, ctx: &egui::Context) { let mut is_open = true; - self.create_window(&mut is_open).show(ctx, |ui| { + if self.config.as_modal { + let re = self.ui_update_modal_background(ctx); + ctx.move_to_top(re.response.layer_id); + } + + let re = self.create_window(&mut is_open).show(ctx, |ui| { + if !self.modals.is_empty() { + self.ui_update_modals(ui); + return; + } + if self.config.show_top_panel { - egui::TopBottomPanel::top("fe_top_panel") + egui::TopBottomPanel::top(self.window_id.with("top_panel")) .resizable(false) .show_inside(ui, |ui| { self.ui_update_top_panel(ui); @@ -756,7 +985,7 @@ impl FileDialog { } if self.config.show_left_panel { - egui::SidePanel::left("fe_left_panel") + egui::SidePanel::left(self.window_id.with("left_panel")) .resizable(true) .default_width(150.0) .width_range(90.0..=250.0) @@ -765,7 +994,7 @@ impl FileDialog { }); } - egui::TopBottomPanel::bottom("fe_bottom_panel") + egui::TopBottomPanel::bottom(self.window_id.with("bottom_panel")) .resizable(false) .show_inside(ui, |ui| { self.ui_update_bottom_panel(ui); @@ -776,24 +1005,68 @@ impl FileDialog { }); }); + if self.config.as_modal { + if let Some(inner_response) = re { + ctx.move_to_top(inner_response.response.layer_id); + } + } + + self.any_focused_last_frame = ctx.memory(|r| r.focused()).is_some(); + // User closed the window without finishing the dialog if !is_open { self.cancel(); } } + /// Updates the main modal background of the file dialog window. + fn ui_update_modal_background(&self, ctx: &egui::Context) -> egui::InnerResponse<()> { + egui::Area::new(self.window_id.with("modal_overlay")) + .interactable(true) + .fixed_pos(egui::Pos2::ZERO) + .show(ctx, |ui| { + let screen_rect = ctx.input(|i| i.screen_rect); + + ui.allocate_response(screen_rect.size(), egui::Sense::click()); + + ui.painter().rect_filled( + screen_rect, + egui::Rounding::ZERO, + self.config.modal_overlay_color, + ); + }) + } + + fn ui_update_modals(&mut self, ui: &mut egui::Ui) { + // Currently, a rendering error occurs when only a single central panel is rendered + // inside a window. Therefore, when rendering a modal, we render an invisible bottom panel, + // which prevents the error. + // This is currently a bit hacky and should be adjusted again in the future. + egui::TopBottomPanel::bottom(self.window_id.with("modal_bottom_panel")) + .resizable(false) + .show_separator_line(false) + .show_inside(ui, |_| {}); + + // We need to use a central panel for the modals so that the + // window doesn't resize to the size of the modal. + egui::CentralPanel::default().show_inside(ui, |ui| { + if let Some(modal) = self.modals.last_mut() { + #[allow(clippy::single_match)] + match modal.update(&self.config, ui) { + ModalState::Close(action) => { + self.exec_modal_action(action); + self.modals.pop(); + } + _ => {} + } + } + }); + } + /// Creates a new egui window with the configured options. fn create_window<'a>(&self, is_open: &'a mut bool) -> egui::Window<'a> { - let window_title = match &self.config.title { - Some(title) => title, - None => match &self.mode { - DialogMode::SelectDirectory => &self.config.labels.title_select_directory, - DialogMode::SelectFile => &self.config.labels.title_select_file, - DialogMode::SaveFile => &self.config.labels.title_save_file, - }, - }; - - let mut window = egui::Window::new(window_title) + let mut window = egui::Window::new(self.get_window_title()) + .id(self.window_id) .open(is_open) .default_size(self.config.default_size) .min_size(self.config.min_size) @@ -802,10 +1075,6 @@ impl FileDialog { .title_bar(self.config.title_bar) .collapsible(false); - if let Some(id) = self.config.id { - window = window.id(id); - } - if let Some(pos) = self.config.default_pos { window = window.default_pos(pos); } @@ -825,6 +1094,20 @@ impl FileDialog { window } + /// Gets the window title to use. + /// This is either one of the default window titles or the configured window title. + fn get_window_title(&self) -> &String { + match &self.config.title { + Some(title) => title, + None => match &self.mode { + DialogMode::SelectDirectory => &self.config.labels.title_select_directory, + DialogMode::SelectFile => &self.config.labels.title_select_file, + DialogMode::SelectMultiple => &self.config.labels.title_select_multiple, + DialogMode::SaveFile => &self.config.labels.title_save_file, + }, + } + } + /// Updates the top panel of the dialog. Including the navigation buttons, /// the current path display, the reload button and the search field. fn ui_update_top_panel(&mut self, ui: &mut egui::Ui) { @@ -835,7 +1118,7 @@ impl FileDialog { let mut path_display_width = ui.available_width(); - // Leave some area for the reload button and search input + // Leave some area for the menu button and search input if self.config.show_reload_button { path_display_width -= BUTTON_SIZE.x + ui.style().spacing.item_spacing.x * 2.5; } @@ -848,11 +1131,35 @@ impl FileDialog { self.ui_update_current_path(ui, path_display_width); } - // Reload button - if self.config.show_reload_button - && ui.add_sized(BUTTON_SIZE, egui::Button::new("⟲")).clicked() + // Menu button containing reload button and different options + if self.config.show_menu_button + && (self.config.show_reload_button || self.config.show_hidden_option) { - self.refresh(); + ui.allocate_ui_with_layout( + BUTTON_SIZE, + egui::Layout::centered_and_justified(egui::Direction::LeftToRight), + |ui| { + ui.menu_button("☰", |ui| { + if self.config.show_reload_button + && ui.button(&self.config.labels.reload).clicked() + { + self.refresh(); + ui.close_menu(); + } + + if self.config.show_hidden_option + && ui + .checkbox( + &mut self.config.storage.show_hidden, + &self.config.labels.show_hidden, + ) + .clicked() + { + ui.close_menu(); + } + }); + }, + ); } if self.config.show_search { @@ -902,9 +1209,7 @@ impl FileDialog { None, ) { - if let Some(x) = self.current_directory() { - self.create_directory_dialog.open(x.to_path_buf()); - } + self.open_new_folder_dialog(); } } @@ -1017,30 +1322,32 @@ impl FileDialog { /// Updates the view when the user currently wants to text edit the current path. fn ui_update_path_edit(&mut self, ui: &mut egui::Ui, width: f32, edit_button_size: egui::Vec2) { let desired_width: f32 = - width - edit_button_size.x - ui.style().spacing.item_spacing.x * 2.0; + width - edit_button_size.x - ui.style().spacing.item_spacing.x * 3.0; let response = egui::TextEdit::singleline(&mut self.path_edit_value) .desired_width(desired_width) .show(ui) .response; + if self.path_edit_activate { + response.request_focus(); + Self::set_cursor_to_end(&response, &self.path_edit_value); + self.path_edit_activate = false; + } + if self.path_edit_request_focus { response.request_focus(); self.path_edit_request_focus = false; } - if response.lost_focus() && ui.ctx().input(|input| input.key_pressed(egui::Key::Enter)) { - self.path_edit_request_focus = true; - self.load_path_edit_directory(false); - } else if !response.has_focus() { - self.path_edit_visible = false; + let btn_response = ui.add_sized(edit_button_size, egui::Button::new("✔")); + + if btn_response.clicked() { + self.submit_path_edit(); } - if ui - .add_sized(edit_button_size, egui::Button::new("✔")) - .clicked() - { - self.load_path_edit_directory(true); + if !response.has_focus() && !btn_response.contains_pointer() { + self.path_edit_visible = false; } } @@ -1061,29 +1368,42 @@ impl FileDialog { egui::Vec2::new(ui.available_width(), 0.0), egui::TextEdit::singleline(&mut self.search_value), ); - self.edit_filter_on_text_input(ui, re); + + self.edit_search_on_text_input(ui); + + if re.changed() || self.init_search { + self.selected_item = None; + self.select_first_visible_item(); + } + + if self.init_search { + re.request_focus(); + Self::set_cursor_to_end(&re, &self.search_value); + self.directory_content.reset_multi_selection(); + + self.init_search = false; + } }); }); } - /// Focuses and types into the filter input, if text input without + /// Focuses and types into the search input, if text input without /// shortcut modifiers is detected, and no other inputs are focused. /// /// # Arguments /// /// - `re`: The [`egui::Response`] returned by the filter text edit widget - fn edit_filter_on_text_input(&mut self, ui: &mut egui::Ui, re: egui::Response) { - let any_focused = ui.memory(|mem| mem.focused().is_some()); - if any_focused { + fn edit_search_on_text_input(&mut self, ui: &mut egui::Ui) { + if ui.memory(|mem| mem.focused().is_some()) { return; } - // Whether to activate the text input widget - let mut activate = false; + ui.input(|inp| { // We stop if any modifier is active besides only shift if inp.modifiers.any() && !inp.modifiers.shift_only() { return; } + // If we find any text input event, we append it to the filter string // and allow proceeding to activating the filter input widget. for text in inp.events.iter().filter_map(|ev| match ev { @@ -1091,22 +1411,9 @@ impl FileDialog { _ => None, }) { self.search_value.push_str(text); - activate = true; + self.init_search = true; } }); - if activate { - // Focus the filter input widget - re.request_focus(); - // Set the cursor to the end of the filter input string - if let Some(mut state) = egui::TextEdit::load_state(ui.ctx(), re.id) { - state - .cursor - .set_char_range(Some(CCursorRange::one(CCursor::new( - self.search_value.len(), - )))); - state.store(ui.ctx(), re.id); - } - } } /// Updates the left panel of the dialog. Including the list of the user directories (Places) @@ -1116,35 +1423,44 @@ impl FileDialog { egui::containers::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { + // Spacing for the first section in the left sidebar let mut spacing = ui.ctx().style().spacing.item_spacing.y * 2.0; + // Spacing multiplier used between sections in the left sidebar + const SPACING_MULTIPLIER: f32 = 4.0; + + // Update paths pinned to the left sidebar by the user + if self.config.show_pinned_folders && self.ui_update_pinned_paths(ui, spacing) { + spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER; + } + // Update custom quick access sections let quick_accesses = std::mem::take(&mut self.config.quick_accesses); for quick_access in &quick_accesses { ui.add_space(spacing); self.ui_update_quick_access(ui, quick_access); - spacing = ui.ctx().style().spacing.item_spacing.y * 4.0; + spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER; } self.config.quick_accesses = quick_accesses; // Update native quick access sections if self.config.show_places && self.ui_update_user_directories(ui, spacing) { - spacing = ui.ctx().style().spacing.item_spacing.y * 4.0; + spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER; } let disks = std::mem::take(&mut self.system_disks); if self.config.show_devices && self.ui_update_devices(ui, spacing, &disks) { - spacing = ui.ctx().style().spacing.item_spacing.y * 4.0; + spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER; } if self.config.show_removable_devices && self.ui_update_removable_devices(ui, spacing, &disks) { // Add this when we add a new section after removable devices - // spacing = ui.ctx().style().spacing.item_spacing.y * 4.0; + // spacing = ui.ctx().style().spacing.item_spacing.y * SPACING_MULTIPLIER; } self.system_disks = disks; @@ -1152,118 +1468,117 @@ impl FileDialog { }); } + /// Updates a path entry in the left panel. + /// + /// Returns the response of the selectable label. + fn ui_update_left_panel_entry( + &mut self, + ui: &mut egui::Ui, + display_name: &str, + path: &Path, + ) -> egui::Response { + let response = ui.selectable_label(self.current_directory() == Some(path), display_name); + + if response.clicked() { + let _ = self.load_directory(path); + } + + response + } + /// Updates a custom quick access section added to the left panel. fn ui_update_quick_access(&mut self, ui: &mut egui::Ui, quick_access: &QuickAccess) { ui.label(&quick_access.heading); for entry in &quick_access.paths { - if ui - .selectable_label( - self.current_directory() == Some(&entry.path), - &entry.display_name, - ) - .clicked() - { - let _ = self.load_directory(&entry.path); + self.ui_update_left_panel_entry(ui, &entry.display_name, &entry.path); + } + } + + /// Updates the list of pinned folders. + /// + /// Returns true if at least one directory item was included in the list and the + /// heading is visible. If no item was listed, false is returned. + fn ui_update_pinned_paths(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool { + let mut visible = false; + + for (i, path) in self + .config + .storage + .pinned_folders + .clone() + .iter() + .enumerate() + { + if i == 0 { + ui.add_space(spacing); + ui.label(self.config.labels.heading_pinned.as_str()); + + visible = true; } + + let response = self.ui_update_left_panel_entry( + ui, + &format!("{} {}", self.config.pinned_icon, path.file_name()), + path.as_path(), + ); + + self.ui_update_path_context_menu(&response, path); } + + visible } - /// Updates the list of the user directories (Places). + /// Updates the list of user directories (Places). /// /// Returns true if at least one directory was included in the list and the /// heading is visible. If no directory was listed, false is returned. fn ui_update_user_directories(&mut self, ui: &mut egui::Ui, spacing: f32) -> bool { - if let Some(dirs) = self.user_directories.clone() { + // Take temporary ownership of the user directories and configuration. + // This is done so that we don't have to clone the user directories and + // configured display names. + let user_directories = std::mem::take(&mut self.user_directories); + let config = std::mem::take(&mut self.config); + + let mut visible = false; + + if let Some(dirs) = &user_directories { ui.add_space(spacing); ui.label(self.config.labels.heading_places.as_str()); if let Some(path) = dirs.home_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.home_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.home_dir, path); } if let Some(path) = dirs.desktop_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.desktop_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.desktop_dir, path); } if let Some(path) = dirs.document_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.documents_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.documents_dir, path); } if let Some(path) = dirs.download_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.downloads_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.downloads_dir, path); } if let Some(path) = dirs.audio_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.audio_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.audio_dir, path); } if let Some(path) = dirs.picture_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.pictures_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.pictures_dir, path); } if let Some(path) = dirs.video_dir() { - if ui - .selectable_label( - self.current_directory() == Some(path), - self.config.labels.videos_dir.as_str(), - ) - .clicked() - { - let _ = self.load_directory(path); - } + self.ui_update_left_panel_entry(ui, &config.labels.videos_dir, path); } - return true; + visible = true; } - false + self.user_directories = user_directories; + self.config = config; + + visible } - /// Updates the list of devices like system disks + /// Updates the list of devices like system disks. /// /// Returns true if at least one device was included in the list and the /// heading is visible. If no device was listed, false is returned. @@ -1284,7 +1599,7 @@ impl FileDialog { visible } - /// Updates the list of removable devices like USB drives + /// Updates the list of removable devices like USB drives. /// /// Returns true if at least one device was included in the list and the /// heading is visible. If no device was listed, false is returned. @@ -1321,53 +1636,88 @@ impl FileDialog { false => format!("{} {}", self.config.device_icon, device.display_name()), }; - if ui.selectable_label(false, label).clicked() { - let _ = self.load_directory(device.mount_point()); - } + self.ui_update_left_panel_entry(ui, &label, device.mount_point()); } /// Updates the bottom panel showing the selected item and main action buttons. fn ui_update_bottom_panel(&mut self, ui: &mut egui::Ui) { ui.add_space(5.0); - self.ui_update_selection_preview(ui); + const BUTTON_HEIGHT: f32 = 20.0; + + // Calculate the width of the action buttons + let label_submit_width = match self.mode { + DialogMode::SelectDirectory | DialogMode::SelectFile | DialogMode::SelectMultiple => { + Self::calc_text_width(ui, &self.config.labels.open_button) + } + DialogMode::SaveFile => Self::calc_text_width(ui, &self.config.labels.save_button), + }; + + let mut btn_width = Self::calc_text_width(ui, &self.config.labels.cancel_button); + if label_submit_width > btn_width { + btn_width = label_submit_width; + } + + btn_width += ui.spacing().button_padding.x * 4.0; + + // The size of the action buttons "cancel" and "open"/"save" + let button_size: egui::Vec2 = egui::Vec2::new(btn_width, BUTTON_HEIGHT); + + self.ui_update_selection_preview(ui, button_size); if self.mode == DialogMode::SaveFile { ui.add_space(ui.style().spacing.item_spacing.y * 2.0) } - self.ui_update_action_buttons(ui); + self.ui_update_action_buttons(ui, button_size); } /// Updates the selection preview like "Selected directory: X" - fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui) { + fn ui_update_selection_preview(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) { + const SELECTION_PREVIEW_MIN_WIDTH: f32 = 50.0; + let item_spacing = ui.style().spacing.item_spacing; + + let render_filter_selection = !self.config.file_filters.is_empty() + && (self.mode == DialogMode::SelectFile || self.mode == DialogMode::SelectMultiple); + + let filter_selection_width = button_size.x * 2.0 + item_spacing.x; + let mut filter_selection_separate_line = false; + ui.horizontal(|ui| { match &self.mode { - DialogMode::SelectDirectory => { - ui.label(self.config.labels.selected_directory.as_str()) - } - DialogMode::SelectFile => ui.label(self.config.labels.selected_file.as_str()), - DialogMode::SaveFile => ui.label(self.config.labels.file_name.as_str()), + DialogMode::SelectDirectory => ui.label(&self.config.labels.selected_directory), + DialogMode::SelectFile => ui.label(&self.config.labels.selected_file), + DialogMode::SelectMultiple => ui.label(&self.config.labels.selected_items), + DialogMode::SaveFile => ui.label(&self.config.labels.file_name), }; + // Make sure there is enough width for the selection preview. If the available + // width is not enough, render the drop-down menu to select a file filter on + // a separate line and give the selection preview the entire available width. + let mut scroll_bar_width: f32 = + ui.available_width() - filter_selection_width - item_spacing.x; + + if scroll_bar_width < SELECTION_PREVIEW_MIN_WIDTH || !render_filter_selection { + filter_selection_separate_line = true; + scroll_bar_width = ui.available_width(); + } + match &self.mode { - DialogMode::SelectDirectory | DialogMode::SelectFile => { - if self.is_selection_valid() { - if let Some(x) = &self.selected_item { - use egui::containers::scroll_area::ScrollBarVisibility; - - egui::containers::ScrollArea::horizontal() - .auto_shrink([false, false]) - .stick_to_right(true) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .show(ui, |ui| { - ui.colored_label( - ui.style().visuals.selection.bg_fill, - x.file_name(), - ); - }); - } - } + DialogMode::SelectDirectory + | DialogMode::SelectFile + | DialogMode::SelectMultiple => { + use egui::containers::scroll_area::ScrollBarVisibility; + + let text = self.get_selection_preview_text(); + + egui::containers::ScrollArea::horizontal() + .auto_shrink([false, false]) + .max_width(scroll_bar_width) + .stick_to_right(true) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .show(ui, |ui| { + ui.colored_label(ui.style().visuals.selection.bg_fill, text); + }); } DialogMode::SaveFile => { let response = ui.add( @@ -1375,64 +1725,135 @@ impl FileDialog { .desired_width(f32::INFINITY), ); + if self.file_name_input_request_focus { + response.request_focus(); + self.file_name_input_request_focus = false; + } + if response.changed() { self.file_name_input_error = self.validate_file_name_input(); } - } + + if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + self.submit(); + } + } }; + + if !filter_selection_separate_line && render_filter_selection { + self.ui_update_file_filter_selection(ui, filter_selection_width); + } }); + + if filter_selection_separate_line && render_filter_selection { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { + self.ui_update_file_filter_selection(ui, filter_selection_width); + }); + } } - /// Updates the action buttons like save, open and cancel - fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui) { - const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(78.0, 20.0); + fn get_selection_preview_text(&self) -> String { + if self.is_selection_valid() { + match &self.mode { + DialogMode::SelectDirectory | DialogMode::SelectFile => { + if let Some(item) = &self.selected_item { + item.file_name().to_string() + } else { + String::new() + } + } + DialogMode::SelectMultiple => { + let mut result = String::new(); + + for (i, item) in self + .get_dir_content_filtered_iter() + .filter(|p| p.selected) + .enumerate() + { + if i == 0 { + result += item.file_name(); + continue; + } + result += format!(", {}", item.file_name()).as_str(); + } + + result + } + _ => String::new(), + } + } else { + String::new() + } + } + + fn ui_update_file_filter_selection(&mut self, ui: &mut egui::Ui, width: f32) { + let selected_filter = self.get_selected_file_filter(); + let selected_text = match selected_filter { + Some(f) => &f.name, + None => &self.config.labels.file_filter_all_files, + }; + + // The item that the user selected inside the drop down. + // If none, no item was selected by the user. + let mut select_filter: Option> = None; + + egui::containers::ComboBox::from_id_source(self.window_id.with("file_filter_selection")) + .width(width) + .selected_text(selected_text) + .show_ui(ui, |ui| { + for filter in self.config.file_filters.iter() { + let selected = match selected_filter { + Some(f) => f.id == filter.id, + None => false, + }; + + if ui.selectable_label(selected, &filter.name).clicked() { + select_filter = Some(Some(filter.id)); + } + } + + if ui + .selectable_label( + selected_filter.is_none(), + &self.config.labels.file_filter_all_files, + ) + .clicked() + { + select_filter = Some(None); + } + }); + + if let Some(i) = select_filter { + self.selected_file_filter = i; + self.selected_item = None; + self.directory_content.reset_multi_selection(); + } + } + + /// Updates the action buttons like save, open and cancel + fn ui_update_action_buttons(&mut self, ui: &mut egui::Ui, button_size: egui::Vec2) { ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| { let label = match &self.mode { - DialogMode::SelectDirectory | DialogMode::SelectFile => { - self.config.labels.open_button.as_str() - } + DialogMode::SelectDirectory + | DialogMode::SelectFile + | DialogMode::SelectMultiple => self.config.labels.open_button.as_str(), DialogMode::SaveFile => self.config.labels.save_button.as_str(), }; if self.ui_button_sized( ui, self.is_selection_valid(), - BUTTON_SIZE, + button_size, label, self.file_name_input_error.as_deref(), ) { - match &self.mode { - DialogMode::SelectDirectory | DialogMode::SelectFile => { - // self.selected_item should always contain a value, - // since self.is_selection_valid() validates the selection and - // returns false if the selection is none. - if let Some(selection) = self.selected_item.clone() { - self.finish(selection.to_path_buf()); - } - } - DialogMode::SaveFile => { - // self.current_directory should always contain a value, - // since self.is_selection_valid() makes sure there is no - // file_name_input_error. The file_name_input_error - // gets validated every time something changes - // by the validate_file_name_input, which sets an error - // if we are currently not in a directory. - if let Some(path) = self.current_directory() { - let mut full_path = path.to_path_buf(); - full_path.push(&self.file_name_input); - - self.finish(full_path); - } - } - } + self.submit(); } - ui.add_space(ui.ctx().style().spacing.item_spacing.y); - if ui .add_sized( - BUTTON_SIZE, + button_size, egui::Button::new(self.config.labels.cancel_button.as_str()), ) .clicked() @@ -1458,78 +1879,212 @@ impl FileDialog { egui::containers::ScrollArea::vertical() .auto_shrink([false, false]) .show(ui, |ui| { - // Temporarily take ownership of the directory contents to be able to - // update it in the for loop using load_directory. - // Otherwise we would get an error that `*self` cannot be borrowed as mutable - // more than once at a time. - // Make sure to return the function after updating the directory_content, - // otherwise the change will be overwritten with the last statement - // of the function. - let data = std::mem::take(&mut self.directory_content); - - for path in data.iter() { - let file_name = path.file_name(); - - if !self.search_value.is_empty() - && !file_name - .to_lowercase() - .contains(&self.search_value.to_lowercase()) - { - continue; + let mut data = std::mem::take(&mut self.directory_content); + let file_filter = self.get_selected_file_filter().cloned(); + + // If the multi selection should be reset, excluding the currently + // selected primary item + let mut reset_multi_selection = false; + // The item the user wants to make a batch selection from. + // The primary selected item is used for item a. + let mut batch_select_item_b: Option = None; + + for item in data.filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value.clone(), + file_filter.as_ref(), + ) { + let file_name = item.file_name(); + + let mut primary_selected = false; + if let Some(x) = &self.selected_item { + primary_selected = x.path_eq(item); } - let mut selected = false; - if let Some(x) = &self.selected_item { - selected = x == path; + let pinned = self.is_pinned(item); + let label = match pinned { + true => { + format!("{} {} {}", item.icon(), self.config.pinned_icon, file_name) + } + false => format!("{} {}", item.icon(), file_name), + }; + + let re = ui.selectable_label(primary_selected || item.selected, label); + + if item.is_dir() { + self.ui_update_path_context_menu(&re, item); + + if re.context_menu_opened() { + self.select_item(item); + } } - let response = - ui.selectable_label(selected, format!("{} {}", path.icon(), file_name)); + if primary_selected && self.scroll_to_selection { + re.scroll_to_me(Some(egui::Align::Center)); + self.scroll_to_selection = false; + } + + // The user wants to select the item as the primary selected item + if re.clicked() + && !ui.input(|i| i.modifiers.ctrl) + && !ui.input(|i| i.modifiers.shift_only()) + { + self.select_item(item); - if selected && self.scroll_to_selection { - response.scroll_to_me(Some(egui::Align::Center)); + // Mark the item as part of the multi selection + if self.mode == DialogMode::SelectMultiple { + reset_multi_selection = true; + } } - if response.clicked() { - self.select_item(path); + // The user wants to select or unselect the item as part of a + // multi selection + if self.mode == DialogMode::SelectMultiple + && re.clicked() + && ui.input(|i| i.modifiers.ctrl) + { + if primary_selected { + // If the clicked item is the primary selected item, + // deselect it and remove it from the multi selection + item.selected = false; + self.selected_item = None; + } else { + item.selected = !item.selected; + + // If the item was selected, make it the primary selected item + if item.selected { + self.select_item(item); + } + } } - if response.double_clicked() { - if path.is_dir() { - let _ = self.load_directory(&path.to_path_buf()); + // The user wants to select every item between the last selected item + // and the current item + if self.mode == DialogMode::SelectMultiple + && re.clicked() + && ui.input(|i| i.modifiers.shift_only()) + { + if let Some(selected_item) = self.selected_item.clone() { + // We perform a batch selection from the item that was + // primarily selected before the user clicked on this item. + batch_select_item_b = Some(selected_item); + + // And now make this item the primary selected item + item.selected = true; + self.select_item(item); + } + } + + // The user double clicked on the directory entry. + // Either open the directory of submit the dialog. + if re.double_clicked() && !ui.input(|i| i.modifiers.ctrl) { + if item.is_dir() { + let _ = self.load_directory(&item.to_path_buf()); return; } - self.select_item(path); + self.select_item(item); - if self.is_selection_valid() { - // self.selected_item should always contain a value - // since self.is_selection_valid() validates the selection - // and returns false if the selection is none. - if let Some(selection) = self.selected_item.clone() { - self.finish(selection.to_path_buf()); + self.submit(); + } + } + + // Reset the multi selection except the currently selected primary item + if reset_multi_selection { + for item in data.filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value.clone(), + file_filter.as_ref(), + ) { + if let Some(selected_item) = &self.selected_item { + if selected_item.path_eq(item) { + continue; } } + + item.selected = false; + } + } + + // Check if we should perform a batch selection + if let Some(item_b) = batch_select_item_b { + if let Some(item_a) = &self.selected_item { + self.batch_select_between(&mut data, item_a, &item_b); } } - self.scroll_to_selection = false; self.directory_content = data; + self.scroll_to_selection = false; if let Some(path) = self .create_directory_dialog .update(ui, &self.config) .directory() { - let entry = DirectoryEntry::from_path(&self.config, &path); - - self.directory_content.push(entry.clone()); - self.select_item(&entry); + self.process_new_folder(&path); } }); }); } + /// Selects every item inside the directory_content between item_a and item_b, + /// excluding both given items. + fn batch_select_between( + &self, + directory_content: &mut DirectoryContent, + item_a: &DirectoryEntry, + item_b: &DirectoryEntry, + ) { + // Get the position of item a and item b + let pos_a = directory_content + .filtered_iter( + self.config.storage.show_hidden, + &self.search_value, + self.get_selected_file_filter(), + ) + .position(|p| p.path_eq(item_a)); + let pos_b = directory_content + .filtered_iter( + self.config.storage.show_hidden, + &self.search_value, + self.get_selected_file_filter(), + ) + .position(|p| p.path_eq(item_b)); + + // If both items where found inside the directory entry, mark every item between + // them as selected + if let Some(pos_a) = pos_a { + if let Some(pos_b) = pos_b { + if pos_a == pos_b { + return; + } + + // Get the min and max of both positions. + // We will iterate from min to max. + let mut min = pos_a; + let mut max = pos_b; + + if min > max { + min = pos_b; + max = pos_a; + } + + for item in directory_content + .filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value, + self.get_selected_file_filter(), + ) + .enumerate() + .filter(|(i, _)| i > &min && i < &max) + .map(|(_, p)| p) + { + item.selected = true; + } + } + } + } + /// Helper function to add a sized button that can be enabled or disabled fn ui_button_sized( &self, @@ -1563,10 +2118,297 @@ impl FileDialog { clicked } + + /// Updates the context menu of a path. + /// + /// # Arguments + /// + /// * `item_response` - The response of the egui item for which the context menu should + /// be opened. + /// * `path` - The path for which the context menu should be opened. + fn ui_update_path_context_menu( + &mut self, + item_response: &egui::Response, + path: &DirectoryEntry, + ) { + // Path context menus are currently only used for pinned folders. + if !self.config.show_pinned_folders { + return; + } + + item_response.context_menu(|ui| { + let pinned = self.is_pinned(path); + + if pinned { + if ui.button(&self.config.labels.unpin_folder).clicked() { + self.unpin_path(path); + ui.close_menu(); + } + } else if ui.button(&self.config.labels.pin_folder).clicked() { + self.pin_path(path.clone()); + ui.close_menu(); + } + }); + } + + /// Sets the cursor position to the end of a text input field. + /// + /// # Arguments + /// + /// * `re` - response of the text input widget + /// * `data` - buffer holding the text of the input widget + fn set_cursor_to_end(re: &egui::Response, data: &str) { + // Set the cursor to the end of the filter input string + if let Some(mut state) = egui::TextEdit::load_state(&re.ctx, re.id) { + state + .cursor + .set_char_range(Some(CCursorRange::one(CCursor::new(data.len())))); + state.store(&re.ctx, re.id); + } + } + + /// Calculate the width of the specified text using the current font configuration. + fn calc_text_width(ui: &egui::Ui, text: &str) -> f32 { + let mut width = 0.0; + for char in text.chars() { + width += ui.fonts(|f| f.glyph_width(&egui::TextStyle::Body.resolve(ui.style()), char)); + } + + width + } +} + +/// Keybindings +impl FileDialog { + /// Checks whether certain keybindings have been pressed and executes the corresponding actions. + fn update_keybindings(&mut self, ctx: &egui::Context) { + // We don't want to execute keybindings if a modal is currently open. + // The modals implement the keybindings themselves. + if let Some(modal) = self.modals.last_mut() { + modal.update_keybindings(&self.config, ctx); + return; + } + + let keybindings = std::mem::take(&mut self.config.keybindings); + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.submit, false) { + self.exec_keybinding_submit(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.cancel, false) { + self.exec_keybinding_cancel(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.parent, true) { + let _ = self.load_parent_directory(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.back, true) { + let _ = self.load_previous_directory(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.forward, true) { + let _ = self.load_next_directory(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.reload, true) { + self.refresh(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.new_folder, true) { + self.open_new_folder_dialog(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.edit_path, true) { + self.open_path_edit(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.home_edit_path, true) { + if let Some(dirs) = &self.user_directories { + if let Some(home) = dirs.home_dir() { + let _ = self.load_directory(home.to_path_buf().as_path()); + self.open_path_edit(); + } + } + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_up, false) { + self.exec_keybinding_selection_up(); + + // We want to break out of input fields like search when pressing selection keys + if let Some(id) = ctx.memory(|r| r.focused()) { + ctx.memory_mut(|w| w.surrender_focus(id)); + } + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.selection_down, false) { + self.exec_keybinding_selection_down(); + + // We want to break out of input fields like search when pressing selection keys + if let Some(id) = ctx.memory(|r| r.focused()) { + ctx.memory_mut(|w| w.surrender_focus(id)); + } + } + + if FileDialogKeyBindings::any_pressed(ctx, &keybindings.select_all, true) { + for item in self.directory_content.filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value, + self.get_selected_file_filter().cloned().as_ref(), + ) { + item.selected = true; + } + } + + self.config.keybindings = keybindings; + } + + /// Executes the action when the keybinding `submit` is pressed. + fn exec_keybinding_submit(&mut self) { + if self.path_edit_visible { + self.submit_path_edit(); + return; + } + + if self.create_directory_dialog.is_open() { + if let Some(dir) = self.create_directory_dialog.submit().directory() { + self.process_new_folder(&dir); + } + return; + } + + // Check if there is a directory selected we can open + if let Some(item) = &self.selected_item { + // Make sure the selected item is visible inside the directory view. + let is_visible = self + .get_dir_content_filtered_iter() + .any(|p| p.path_eq(item)); + + if is_visible && item.is_dir() { + let _ = self.load_directory(&item.to_path_buf()); + return; + } + } + + self.submit(); + } + + /// Executes the action when the keybinding `cancel` is pressed. + fn exec_keybinding_cancel(&mut self) { + // We have to check if the `create_directory_dialog` and `path_edit_visible` is open, + // because egui does not consume pressing the escape key inside a text input. + // So when pressing the escape key inside a text input, the text input is closed + // but the keybindings still register the press on the escape key. + // (Although the keybindings are updated before the UI and they check whether another + // widget is currently in focus!) + // + // This is practical for us because we can close the path edit and + // the create directory dialog. + // However, this causes problems when the user presses escape in other text + // inputs for which we have no status saved. This would then close the entire file dialog. + // To fix this, we check if any item was focused in the last frame. + // + // Note that this only happens with the escape key and not when the enter key is + // used to close a text input. This is why we don't have to check for the + // dialogs in `exec_keybinding_submit`. + + if self.create_directory_dialog.is_open() { + self.create_directory_dialog.close(); + } else if self.path_edit_visible { + self.close_path_edit() + } else if !self.any_focused_last_frame { + self.cancel(); + return; + } + } + + /// Executes the action when the keybinding `selection_up` is pressed. + fn exec_keybinding_selection_up(&mut self) { + if self.directory_content.len() == 0 { + return; + } + + self.directory_content.reset_multi_selection(); + + if let Some(item) = &self.selected_item { + if self.select_next_visible_item_before(&item.clone()) { + return; + } + } + + // No item is selected or no more items left. + // Select the last item from the directory content. + self.select_last_visible_item(); + } + + /// Executes the action when the keybinding `selection_down` is pressed. + fn exec_keybinding_selection_down(&mut self) { + if self.directory_content.len() == 0 { + return; + } + + self.directory_content.reset_multi_selection(); + + if let Some(item) = &self.selected_item { + if self.select_next_visible_item_after(&item.clone()) { + return; + } + } + + // No item is selected or no more items left. + // Select the last item from the directory content. + self.select_first_visible_item(); + } } /// Implementation impl FileDialog { + /// Get the file filter the user currently selected. + fn get_selected_file_filter(&self) -> Option<&FileFilter> { + match self.selected_file_filter { + Some(id) => self.config.file_filters.iter().find(|p| p.id == id), + None => None, + } + } + + /// Gets a filtered iterator of the directory content of this object. + fn get_dir_content_filtered_iter(&self) -> impl Iterator { + self.directory_content.filtered_iter( + self.config.storage.show_hidden, + &self.search_value, + self.get_selected_file_filter(), + ) + } + + /// Opens the dialog to create a new folder. + fn open_new_folder_dialog(&mut self) { + if let Some(x) = self.current_directory() { + self.create_directory_dialog.open(x.to_path_buf()); + } + } + + /// Function that processes a newly created folder. + fn process_new_folder(&mut self, created_dir: &Path) { + let mut entry = DirectoryEntry::from_path(&self.config, created_dir); + + self.directory_content.push(entry.clone()); + + self.select_item(&mut entry); + } + + /// Opens a new modal window. + fn open_modal(&mut self, modal: Box) { + self.modals.push(modal); + } + + /// Executes the given modal action. + fn exec_modal_action(&mut self, action: ModalAction) { + match action { + ModalAction::None => {} + ModalAction::SaveFile(path) => self.state = DialogState::Selected(path), + }; + } + /// Canonicalizes the specified path if canonicalization is enabled. /// Returns the input path if an error occurs or canonicalization is disabled. fn canonicalize_path(&self, path: &Path) -> PathBuf { @@ -1576,28 +2418,33 @@ impl FileDialog { } } - /// Resets the dialog to use default values. - /// Configuration variables such as `initial_directory` are retained. - fn reset(&mut self) { - self.state = DialogState::Closed; - self.show_files = true; - self.operation_id = None; - - self.user_directories = UserDirectories::new(self.config.canonicalize_paths); - self.system_disks = Disks::new_with_refreshed_list(self.config.canonicalize_paths); - - self.directory_stack = vec![]; - self.directory_offset = 0; - self.directory_content = DirectoryContent::new(); - self.directory_error = None; + /// Pins a path to the left sidebar. + fn pin_path(&mut self, path: DirectoryEntry) { + self.config.storage.pinned_folders.push(path); + } - self.create_directory_dialog = CreateDirectoryDialog::new(); + /// Unpins a path from the left sidebar. + fn unpin_path(&mut self, path: &DirectoryEntry) { + self.config + .storage + .pinned_folders + .retain(|p| !p.path_eq(path)); + } - self.selected_item = None; - self.file_name_input = String::new(); + /// Checks if the path is pinned to the left sidebar. + fn is_pinned(&self, path: &DirectoryEntry) -> bool { + self.config + .storage + .pinned_folders + .iter() + .any(|p| path.path_eq(p)) + } - self.scroll_to_selection = false; - self.search_value = String::new(); + /// Resets the dialog to use default values. + /// Configuration variables are retained. + fn reset(&mut self) { + let config = self.config.clone(); + *self = FileDialog::with_config(config); } /// Refreshes the dialog. @@ -1609,10 +2456,47 @@ impl FileDialog { let _ = self.reload_directory(); } - /// Finishes the dialog. - /// `selected_item`` is the item that was selected by the user. - fn finish(&mut self, selected_item: PathBuf) { - self.state = DialogState::Selected(selected_item); + /// Submits the current selection and tries to finish the dialog, if the selection is valid. + fn submit(&mut self) { + // Make sure the selected item or entered file name is valid. + if !self.is_selection_valid() { + return; + } + + match &self.mode { + DialogMode::SelectDirectory | DialogMode::SelectFile => { + // Should always contain a value since `is_selection_valid` is used to + // validate the selection. + if let Some(item) = self.selected_item.clone() { + self.state = DialogState::Selected(item.to_path_buf()); + } + } + DialogMode::SelectMultiple => { + let result: Vec = self + .get_dir_content_filtered_iter() + .filter(|p| p.selected) + .map(|p| p.to_path_buf()) + .collect(); + + self.state = DialogState::SelectedMultiple(result); + } + DialogMode::SaveFile => { + // Should always contain a value since `is_selection_valid` is used to + // validate the selection. + if let Some(path) = self.current_directory() { + let mut full_path = path.to_path_buf(); + full_path.push(&self.file_name_input); + + if full_path.exists() { + self.open_modal(Box::new(OverwriteFileModal::new(full_path))); + + return; + } + + self.state = DialogState::Selected(full_path); + } + } + } } /// Cancels the dialog. @@ -1648,19 +2532,24 @@ impl FileDialog { /// Checks whether the selection or the file name entered is valid. /// What is checked depends on the mode the dialog is currently in. fn is_selection_valid(&self) -> bool { - if let Some(selection) = &self.selected_item { - return match &self.mode { - DialogMode::SelectDirectory => selection.is_dir(), - DialogMode::SelectFile => selection.is_file(), - DialogMode::SaveFile => self.file_name_input_error.is_none(), - }; - } - - if self.mode == DialogMode::SaveFile && self.file_name_input_error.is_none() { - return true; + match &self.mode { + DialogMode::SelectDirectory => { + if let Some(item) = &self.selected_item { + item.is_dir() + } else { + false + } + } + DialogMode::SelectFile => { + if let Some(item) = &self.selected_item { + item.is_file() + } else { + false + } + } + DialogMode::SelectMultiple => self.get_dir_content_filtered_iter().any(|p| p.selected), + DialogMode::SaveFile => self.file_name_input_error.is_none(), } - - false } /// Validates the file name entered by the user. @@ -1678,7 +2567,8 @@ impl FileDialog { if full_path.is_dir() { return Some(self.config.labels.err_directory_exists.clone()); } - if full_path.is_file() { + + if !self.config.allow_file_overwrite && full_path.is_file() { return Some(self.config.labels.err_file_exists.clone()); } } else { @@ -1691,15 +2581,146 @@ impl FileDialog { /// Marks the given item as the selected directory item. /// Also updates the file_name_input to the name of the selected item. - fn select_item(&mut self, dir_entry: &DirectoryEntry) { - self.selected_item = Some(dir_entry.clone()); + fn select_item(&mut self, item: &mut DirectoryEntry) { + if self.mode == DialogMode::SelectMultiple { + item.selected = true; + } + self.selected_item = Some(item.clone()); - if self.mode == DialogMode::SaveFile && dir_entry.is_file() { - self.file_name_input = dir_entry.file_name().to_string(); + if self.mode == DialogMode::SaveFile && item.is_file() { + self.file_name_input = item.file_name().to_string(); self.file_name_input_error = self.validate_file_name_input(); } } + /// Attempts to select the last visible item in `directory_content` before the specified item. + /// + /// Returns true if an item is found and selected. + /// Returns false if no visible item is found before the specified item. + fn select_next_visible_item_before(&mut self, item: &DirectoryEntry) -> bool { + let mut return_val = false; + + self.directory_content.reset_multi_selection(); + + let mut directory_content = std::mem::take(&mut self.directory_content); + let search_value = std::mem::take(&mut self.search_value); + let file_filter = self.get_selected_file_filter().cloned(); + + let index = directory_content + .filtered_iter( + self.config.storage.show_hidden, + &search_value, + file_filter.as_ref(), + ) + .position(|p| p.path_eq(item)); + + if let Some(index) = index { + if index != 0 { + if let Some(item) = directory_content + .filtered_iter_mut( + self.config.storage.show_hidden, + &search_value.clone(), + file_filter.as_ref(), + ) + .nth(index.saturating_sub(1)) + { + self.select_item(item); + self.scroll_to_selection = true; + return_val = true; + } + } + } + + self.directory_content = directory_content; + self.search_value = search_value; + + return_val + } + + /// Attempts to select the last visible item in `directory_content` after the specified item. + /// + /// Returns true if an item is found and selected. + /// Returns false if no visible item is found after the specified item. + fn select_next_visible_item_after(&mut self, item: &DirectoryEntry) -> bool { + let mut return_val = false; + + self.directory_content.reset_multi_selection(); + + let mut directory_content = std::mem::take(&mut self.directory_content); + let search_value = std::mem::take(&mut self.search_value); + let file_filter = self.get_selected_file_filter().cloned(); + + let index = directory_content + .filtered_iter( + self.config.storage.show_hidden, + &search_value, + file_filter.as_ref(), + ) + .position(|p| p.path_eq(item)); + + if let Some(index) = index { + if let Some(item) = directory_content + .filtered_iter_mut( + self.config.storage.show_hidden, + &search_value.clone(), + file_filter.as_ref(), + ) + .nth(index.saturating_add(1)) + { + self.select_item(item); + self.scroll_to_selection = true; + return_val = true; + } + } + + self.directory_content = directory_content; + self.search_value = search_value; + + return_val + } + + /// Tries to select the first visible item inside `directory_content`. + fn select_first_visible_item(&mut self) { + self.directory_content.reset_multi_selection(); + + let mut directory_content = std::mem::take(&mut self.directory_content); + + if let Some(item) = directory_content + .filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value.clone(), + self.get_selected_file_filter().cloned().as_ref(), + ) + .next() + { + self.select_item(item); + self.scroll_to_selection = true; + } + + self.directory_content = directory_content; + } + + /// Tries to select the last visible item inside `directory_content`. + fn select_last_visible_item(&mut self) { + self.directory_content.reset_multi_selection(); + + let mut directory_content = std::mem::take(&mut self.directory_content); + + if let Some(item) = directory_content + .filtered_iter_mut( + self.config.storage.show_hidden, + &self.search_value.clone(), + self.get_selected_file_filter().cloned().as_ref(), + ) + .last() + { + self.select_item(item); + self.scroll_to_selection = true; + } + + self.directory_content = directory_content; + } + /// Opens the text field in the top panel to text edit the current path. fn open_path_edit(&mut self) { let path = match self.current_directory() { @@ -1708,19 +2729,22 @@ impl FileDialog { }; self.path_edit_value = path; - self.path_edit_request_focus = true; + self.path_edit_activate = true; self.path_edit_visible = true; } /// Loads the directory from the path text edit. - fn load_path_edit_directory(&mut self, close_text_edit: bool) { - if close_text_edit { - self.path_edit_visible = false; - } - + fn submit_path_edit(&mut self) { + self.close_path_edit(); let _ = self.load_directory(&self.canonicalize_path(&PathBuf::from(&self.path_edit_value))); } + /// Closes the text field at the top to edit the current path without loading + /// the entered directory. + fn close_path_edit(&mut self) { + self.path_edit_visible = false; + } + /// Loads the next directory in the directory_stack. /// If directory_offset is 0 and there is no other directory to load, Ok() is returned and /// nothing changes. @@ -1776,6 +2800,9 @@ impl FileDialog { /// Reloads the currently open directory. /// If no directory is currently open, Ok() will be returned. /// Otherwise, the result of the directory loading operation is returned. + /// + /// In most cases, this function should not be called directly. + /// Instead, `refresh` should be used to reload all other data like system disks too. fn reload_directory(&mut self) -> io::Result<()> { if let Some(x) = self.current_directory() { return self.load_directory_content(x.to_path_buf().as_path()); @@ -1808,8 +2835,8 @@ impl FileDialog { self.load_directory_content(path)?; - let dir_entry = DirectoryEntry::from_path(&self.config, path); - self.select_item(&dir_entry); + let mut dir_entry = DirectoryEntry::from_path(&self.config, path); + self.select_item(&mut dir_entry); // Clear the entry filter buffer. // It's unlikely the user wants to keep the current filter when entering a new directory. @@ -1826,6 +2853,8 @@ impl FileDialog { match DirectoryContent::from_path(&self.config, path, self.show_files) { Ok(content) => content, Err(err) => { + self.directory_content.clear(); + self.selected_item = None; self.directory_error = Some(err.to_string()); return Err(err); } diff --git a/src/lib.rs b/src/lib.rs index 8b35c1cd..32d965d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,6 @@ //! An easy-to-use and customizable file dialog (a.k.a. file explorer, file picker) for //! [egui](https://github.com/emilk/egui). //! -//! The project is currently in a very early version. Some planned features are still missing -//! and some improvements still need to be made. -//! //! **Currently only tested on Linux and Windows!** //! //! Read more about the project: @@ -13,14 +10,24 @@ //! ### Features //! - Select a file or a directory //! - Save a file (Prompt user for a destination path) +//! - Dialog to ask the user if the existing file should be overwritten +//! - Select multiple files and folders at once (ctrl/shift + click) +//! - Open the dialog in a normal or modal window //! - Create a new folder +//! - Keyboard navigation +//! - Option to show or hide hidden files and folders //! - Navigation buttons to open the parent or previous directories //! - Search for items in a directory +//! - Add file filters the user can select from a dropdown //! - Shortcut for user directories (Home, Documents, ...) and system disks +//! - Pin folders to the left sidebar +//! - Manually edit the path via text //! - Customization highlights: //! - Customize which areas and functions of the dialog are visible -//! - Multilingual support: Customize the text labels that the dialog uses +//! - Customize the text labels used by the dialog to enable multilingual support //! - Customize file and folder icons +//! - Add custom quick access sections to the left sidebar +//! - Customize keybindings used by the file dialog //! //! ### A simple example //! @@ -59,25 +66,39 @@ //! } //! ``` //! -//! ### Customization +//! ### Keybindings +//! Keybindings can be used in the file dialog for easier navigation. All keybindings can be configured from the backend with `FileDialogKeyBindings` and `FileDialog::keybindings`. \ +//! The following table lists all available keybindings and their default values. +//! +//! | Name | Description | Default | +//! | --- | --- | --- | +//! | submit | Submit the current action or open the currently selected folder | `Enter` | +//! | cancel | Cancel the current action | `Escape` | +//! | parent | Open the parent directory | `ALT` + `↑` | +//! | back | Go back | `Mouse button 1`
`ALT` + `←`
`Backspace` | +//! | forward | Go forward | `Mouse button 2`
`ALT` + `→` | +//! | reload | Reload the file dialog data and the currently open directory | `F5` | +//! | new_folder | Open the dialog to create a new folder | `CTRL` + `N` | +//! | edit_path | Text edit the current path | `/` | +//! | home_edit_path | Open the home directory and start text editing the path | `~` | +//! | selection_up | Move the selection one item up | `↑` | +//! | selection_down | Move the selection one item down | `↓` | +//! | select_all | Select every item in the directory when using the file dialog to select multiple files and folders | `CTRL` + `A` | //! +//! ### Customization //! Many things can be customized so that the dialog can be used in different situations. \ -//! A few highlights of the customization are listed below. -//! (More customization will be implemented in the future!) +//! A few highlights of the customization are listed below. For all possible customization options, see the documentation on [docs.rs](https://docs.rs/egui-file-dialog/latest/egui_file_dialog/struct.FileDialog.html). //! //! - Set which areas and functions of the dialog are visible using `FileDialog::show_*` methods //! - Update the text labels that the dialog uses. See [Multilingual support](#multilingual-support) -//! - Customize file and folder icons using `FileDialog::set_file_icon` -//! (Currently only unicode is supported) +//! - Customize file and folder icons using `FileDialog::set_file_icon` (Currently only unicode is supported) +//! - Customize keybindings used by the file dialog using `FileDialog::keybindings`. See [Keybindings](#keybindings) //! -//! Since the dialog uses the egui style to look like the rest of the application, -//! the appearance can be customized with `egui::Style`. +//! Since the dialog uses the egui style to look like the rest of the application, the appearance can be customized with `egui::Style` and `egui::Context::set_style`. //! -//! The following example shows how a file dialog can be customized. If you need to -//! configure multiple file dialog objects with the same or almost the same options, -//! it is a good idea to use `FileDialogConfig` and `FileDialog::with_config` -//! -//! ``` +//! The following example shows how a single file dialog can be customized. \ +//! If you need to configure multiple file dialog objects with the same or almost the same options, it is a good idea to use `FileDialogConfig` and `FileDialog::with_config` (See `FileDialogConfig` on [docs.rs](https://docs.rs/egui-file-dialog/latest/egui_file_dialog/struct.FileDialogConfig.html)). +//! ```rust //! use std::path::PathBuf; //! use std::sync::Arc; //! @@ -90,26 +111,31 @@ //! .resizable(false) //! .show_new_folder_button(false) //! .show_search(false) -//! // Markdown and text files should use the "document with text (U+1F5B9)" icon +//! .show_path_edit_button(false) +//! // Add a new quick access section to the left sidebar +//! .add_quick_access("Project", |s| { +//! s.add_path("☆ Examples", "examples"); +//! s.add_path("📷 Media", "media"); +//! s.add_path("📂 Source", "src"); +//! }) +//! // Markdown files should use the "document with text (U+1F5B9)" icon //! .set_file_icon( //! "🖹", -//! Arc::new(|path| { -//! match path -//! .extension() -//! .unwrap_or_default() -//! .to_str() -//! .unwrap_or_default() -//! { -//! "md" => true, -//! "txt" => true, -//! _ => false, -//! } -//! }), +//! Arc::new(|path| path.extension().unwrap_or_default() == "md"), //! ) //! // .gitignore files should use the "web-github (U+E624)" icon //! .set_file_icon( //! "", //! Arc::new(|path| path.file_name().unwrap_or_default() == ".gitignore"), +//! ) +//! // Add file filters the user can select in the bottom right +//! .add_file_filter( +//! "PNG files", +//! Arc::new(|p| p.extension().unwrap_or_default() == "png"), +//! ) +//! .add_file_filter( +//! "Rust source files", +//! Arc::new(|p| p.extension().unwrap_or_default() == "rs"), //! ); //! ``` //! @@ -152,6 +178,19 @@ //! }; //! } //! ``` +//! +//! ### Persistent data +//! The file dialog currently requires the following persistent data to be stored across multiple file dialog objects: +//! +//! - Folders the user pinned to the left sidebar (`FileDialog::show_pinned_folders`) +//! - If hidden files and folders should be visible (`FileDialog::show_hidden_option`) +//! +//! If one of the above feature is activated, the data should be saved by the application. Otherwise, frustrating situations could arise for the user and the features would not offer much added value. +//! +//! All data that needs to be stored permanently is contained in the `FileDialogStorage` struct. This struct can be accessed using `FileDialog::storage` or `FileDialog::storage_mut` to save or load the persistent data. \ +//! By default the feature `serde` is enabled, which implements `serde::Serialize` and `serde::Deserialize` for the objects to be saved. However, the objects can also be accessed without the feature enabled. +//! +//! Checkout `examples/persistence` for an example. #![warn(missing_docs)] // Let's keep the public API well documented! @@ -159,6 +198,11 @@ mod config; mod create_directory_dialog; mod data; mod file_dialog; +mod modals; -pub use config::{FileDialogConfig, FileDialogLabels}; +pub use config::{ + FileDialogConfig, FileDialogKeyBindings, FileDialogLabels, FileDialogStorage, IconFilter, + KeyBinding, QuickAccess, QuickAccessPath, +}; +pub use data::DirectoryEntry; pub use file_dialog::{DialogMode, DialogState, FileDialog}; diff --git a/src/modals/mod.rs b/src/modals/mod.rs new file mode 100644 index 00000000..b794f894 --- /dev/null +++ b/src/modals/mod.rs @@ -0,0 +1,33 @@ +use std::path::PathBuf; + +use crate::FileDialogConfig; + +mod overwrite_file_modal; +pub use overwrite_file_modal::OverwriteFileModal; + +/// Contains actions that are executed by the file dialog when closing a modal. +#[derive(Clone)] +pub enum ModalAction { + /// If no action should be executed. + None, + /// If the file dialog should save the specified path. + /// Should only be used if the FileDialog is in `FileDialogMode::SaveFile` mode. + SaveFile(PathBuf), +} + +#[derive(Clone)] +pub enum ModalState { + /// If the modal is currently open and still waiting for user input + Pending, + /// If the modal should be closed in the next frame + Close(ModalAction), +} + +pub trait FileDialogModal { + /// Main update method of the modal. + /// Should be called every time the modal should be visible. + fn update(&mut self, config: &FileDialogConfig, ui: &mut egui::Ui) -> ModalState; + + /// Updates the configured keybindings for the modal window. + fn update_keybindings(&mut self, _config: &FileDialogConfig, _ctx: &egui::Context) {} +} diff --git a/src/modals/overwrite_file_modal.rs b/src/modals/overwrite_file_modal.rs new file mode 100644 index 00000000..c42c47fb --- /dev/null +++ b/src/modals/overwrite_file_modal.rs @@ -0,0 +1,109 @@ +use std::path::PathBuf; + +use super::{FileDialogModal, ModalAction, ModalState}; +use crate::config::{FileDialogConfig, FileDialogKeyBindings}; + +/// The modal that is used to ask the user if the selected path should be +/// overwritten. +pub struct OverwriteFileModal { + /// The current state of the modal. + state: ModalState, + /// The path selected for overwriting. + path: PathBuf, +} + +impl OverwriteFileModal { + /// Creates a new modal object. + /// + /// # Arguments + /// + /// * `path` - The path selected for overwriting. + pub fn new(path: PathBuf) -> Self { + Self { + state: ModalState::Pending, + path, + } + } +} + +impl OverwriteFileModal { + /// Submits the modal and triggers the action to save the file. + fn submit(&mut self) { + self.state = ModalState::Close(ModalAction::SaveFile(self.path.to_path_buf())); + } + + /// Closes the modal without overwriting the file. + fn cancel(&mut self) { + self.state = ModalState::Close(ModalAction::None); + } +} + +impl FileDialogModal for OverwriteFileModal { + fn update(&mut self, config: &FileDialogConfig, ui: &mut egui::Ui) -> ModalState { + const SECTION_SPACING: f32 = 15.0; + const BUTTON_SIZE: egui::Vec2 = egui::Vec2::new(90.0, 20.0); + + ui.vertical_centered(|ui| { + let warn_icon = egui::RichText::new(&config.warn_icon) + .color(ui.visuals().warn_fg_color) + .heading(); + + ui.add_space(SECTION_SPACING); + + ui.label(warn_icon); + + ui.add_space(SECTION_SPACING); + + // Used to wrap the path on a single line. + let mut job = egui::text::LayoutJob::single_section( + format!("'{}'", self.path.to_str().unwrap_or_default()), + egui::TextFormat::default(), + ); + + job.wrap = egui::text::TextWrapping { + max_rows: 1, + ..Default::default() + }; + + ui.label(job); + ui.label(&config.labels.overwrite_file_modal_text); + + ui.add_space(SECTION_SPACING); + + ui.horizontal(|ui| { + let required_width = BUTTON_SIZE.x * 2.0 + ui.style().spacing.item_spacing.x; + let padding = (ui.available_width() - required_width) / 2.0; + + ui.add_space(padding); + + if ui + .add_sized(BUTTON_SIZE, egui::Button::new(&config.labels.cancel)) + .clicked() + { + self.cancel() + } + + ui.add_space(ui.style().spacing.item_spacing.x); + + if ui + .add_sized(BUTTON_SIZE, egui::Button::new(&config.labels.overwrite)) + .clicked() + { + self.submit(); + } + }); + }); + + self.state.clone() + } + + fn update_keybindings(&mut self, config: &FileDialogConfig, ctx: &egui::Context) { + if FileDialogKeyBindings::any_pressed(ctx, &config.keybindings.submit, true) { + self.submit(); + } + + if FileDialogKeyBindings::any_pressed(ctx, &config.keybindings.cancel, true) { + self.cancel(); + } + } +}