Skip to main content

fyrox_ui/file_browser/
selector.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21use crate::button::Button;
22use crate::dropdown_list::DropdownList;
23use crate::file_browser::FileBrowser;
24use crate::messagebox::MessageBox;
25use crate::text_box::TextBox;
26use crate::{
27    border::BorderBuilder,
28    button::{ButtonBuilder, ButtonMessage},
29    core::{
30        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
31        uuid_provider, visitor::prelude::*,
32    },
33    draw::DrawingContext,
34    dropdown_list::{DropdownListBuilder, DropdownListMessage},
35    file_browser::{FileBrowserBuilder, FileBrowserMessage, PathFilter},
36    grid::{Column, GridBuilder, Row},
37    message::{MessageData, OsEvent, UiMessage},
38    messagebox::{MessageBoxBuilder, MessageBoxButtons, MessageBoxMessage, MessageBoxResult},
39    stack_panel::StackPanelBuilder,
40    style::{resource::StyleResourceExt, Style},
41    text::{TextBuilder, TextMessage},
42    text_box::{TextBoxBuilder, TextCommitMode},
43    utils::make_dropdown_list_option,
44    widget::{Widget, WidgetBuilder, WidgetMessage},
45    window::{Window, WindowBuilder, WindowMessage, WindowTitle},
46    BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
47    VerticalAlignment,
48};
49use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
50use std::{
51    cell::Cell,
52    ops::{Deref, DerefMut},
53    path::{Path, PathBuf},
54};
55
56#[derive(Default, Clone, PartialEq, Eq, Hash, Debug, Visit, Reflect)]
57pub enum FileSelectorMode {
58    #[default]
59    Open,
60    Save {
61        default_file_name: PathBuf,
62    },
63}
64
65#[derive(Debug, Clone, PartialEq)]
66pub enum FileSelectorMessage {
67    Root(Option<PathBuf>),
68    Path(PathBuf),
69    Commit(PathBuf),
70    FocusCurrentPath,
71    Cancel,
72    FileTypes(PathFilter),
73}
74impl MessageData for FileSelectorMessage {}
75
76/// File selector is a modal window that allows you to select a file (or directory) and commit or
77/// cancel selection.
78#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
79#[reflect(derived_type = "UiNode")]
80pub struct FileSelector {
81    #[component(include)]
82    pub window: Window,
83    pub browser: Handle<FileBrowser>,
84    pub ok: Handle<Button>,
85    pub cancel: Handle<Button>,
86    pub selected_folder: PathBuf,
87    pub mode: FileSelectorMode,
88    pub file_name: Handle<TextBox>,
89    pub file_name_value: PathBuf,
90    pub filter: PathFilter,
91    pub file_type_selector: Handle<DropdownList>,
92    pub selected_file_type: Option<usize>,
93    pub overwrite_message_box: Cell<Handle<MessageBox>>,
94}
95
96impl ConstructorProvider<UiNode, UserInterface> for FileSelector {
97    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
98        GraphNodeConstructor::new::<Self>()
99            .with_variant("File Selector", |ui| {
100                FileSelectorBuilder::new(WindowBuilder::new(
101                    WidgetBuilder::new().with_name("File Selector"),
102                ))
103                .build(&mut ui.build_ctx())
104                .to_base()
105                .into()
106            })
107            .with_group("File System")
108    }
109}
110
111impl Deref for FileSelector {
112    type Target = Widget;
113
114    fn deref(&self) -> &Self::Target {
115        &self.window
116    }
117}
118
119impl DerefMut for FileSelector {
120    fn deref_mut(&mut self) -> &mut Self::Target {
121        &mut self.window
122    }
123}
124
125uuid_provider!(FileSelector = "878b2220-03e6-4a50-a97d-3a8e5397b6cb");
126
127fn extract_folder_path(path: &Path) -> Option<&Path> {
128    if path.is_file() {
129        path.parent()
130    } else if path.is_dir() {
131        Some(path)
132    } else {
133        None
134    }
135}
136
137fn extract_folder_path_buf(path: &Path) -> Option<PathBuf> {
138    extract_folder_path(path).map(|p| p.to_path_buf())
139}
140
141impl FileSelector {
142    fn on_ok_clicked(&self, ui: &mut UserInterface) {
143        let final_path = self.final_path();
144
145        if final_path.exists() && matches!(self.mode, FileSelectorMode::Save { .. }) {
146            self.overwrite_message_box.set(
147                MessageBoxBuilder::new(
148                    WindowBuilder::new(WidgetBuilder::new().with_width(350.0).with_height(100.0))
149                        .with_title(WindowTitle::text("Confirm Action"))
150                        .open(false),
151                )
152                .with_text(
153                    format!(
154                        "The file {} already exist. Do you want to overwrite it?",
155                        final_path.display()
156                    )
157                    .as_str(),
158                )
159                .with_buttons(MessageBoxButtons::YesNo)
160                .build(&mut ui.build_ctx()),
161            );
162
163            ui.send(
164                self.overwrite_message_box.get(),
165                MessageBoxMessage::Open {
166                    title: None,
167                    text: None,
168                },
169            );
170        } else {
171            ui.send(self.handle, FileSelectorMessage::Commit(self.final_path()));
172        }
173    }
174
175    fn on_path_selected(&mut self, path: &Path, ui: &UserInterface) {
176        if path.is_file() {
177            ui.send(
178                self.file_name,
179                TextMessage::Text(
180                    path.file_name()
181                        .map(|f| f.to_string_lossy().to_string())
182                        .unwrap_or_default(),
183                ),
184            );
185            self.selected_folder = extract_folder_path_buf(path).unwrap_or_default();
186        } else {
187            self.selected_folder = path.to_path_buf();
188        }
189
190        self.validate_selection(ui);
191    }
192
193    fn on_file_selector_message(&mut self, msg: &FileSelectorMessage, ui: &UserInterface) {
194        match msg {
195            FileSelectorMessage::Commit(_) | FileSelectorMessage::Cancel => {
196                ui.send(self.handle, WindowMessage::Close)
197            }
198            FileSelectorMessage::Path(path) => {
199                ui.send(self.browser, FileBrowserMessage::Path(path.clone()))
200            }
201            FileSelectorMessage::Root(root) => {
202                ui.send(self.browser, FileBrowserMessage::Root(root.clone()));
203            }
204            FileSelectorMessage::FileTypes(filter) => {
205                ui.send(self.browser, FileBrowserMessage::Filter(filter.clone()));
206            }
207            FileSelectorMessage::FocusCurrentPath => {
208                ui.send(self.browser, FileBrowserMessage::FocusCurrentPath);
209            }
210        }
211    }
212
213    fn final_path(&self) -> PathBuf {
214        let mut final_path = self.selected_folder.join(&self.file_name_value);
215        if let Some(file_type) = self.selected_file_type.and_then(|i| self.filter.get(i)) {
216            final_path.set_extension(&file_type.extension);
217        }
218        final_path
219    }
220
221    fn validate_selection(&self, ui: &UserInterface) {
222        let final_path = self.final_path();
223        let passed = self
224            .filter
225            .supports_specific_type(&final_path, self.selected_file_type)
226            && match self.mode {
227                FileSelectorMode::Open => final_path.exists(),
228                FileSelectorMode::Save { .. } => true,
229            };
230        ui.send(self.ok, WidgetMessage::Enabled(passed))
231    }
232
233    fn on_file_type_selected(&mut self, selection: Option<usize>, ui: &UserInterface) {
234        // Minus one here because there's "All supported" option in the beginning of the file
235        // type selector.
236        let selection = selection.and_then(|i| i.checked_sub(1));
237        self.selected_file_type = selection;
238        self.validate_selection(ui);
239    }
240
241    fn on_file_name_changed(&mut self, file_name: &str, ui: &UserInterface) {
242        self.file_name_value = file_name.into();
243        self.validate_selection(ui);
244    }
245}
246
247// File selector extends Window widget so it delegates most of the calls
248// to inner window.
249impl Control for FileSelector {
250    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
251        self.window.measure_override(ui, available_size)
252    }
253
254    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
255        self.window.arrange_override(ui, final_size)
256    }
257
258    fn draw(&self, drawing_context: &mut DrawingContext) {
259        self.window.draw(drawing_context)
260    }
261
262    fn update(&mut self, dt: f32, ui: &mut UserInterface) {
263        self.window.update(dt, ui);
264    }
265
266    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
267        self.window.handle_routed_message(ui, message);
268
269        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
270            if message.destination() == self.ok {
271                self.on_ok_clicked(ui)
272            } else if message.destination() == self.cancel {
273                ui.send(self.handle, FileSelectorMessage::Cancel)
274            }
275        } else if let Some(msg) = message.data_for::<FileSelectorMessage>(self.handle) {
276            self.on_file_selector_message(msg, ui)
277        } else if let Some(FileBrowserMessage::Path(path)) = message.data_from(self.browser) {
278            self.on_path_selected(path, ui)
279        } else if let Some(TextMessage::Text(file_name)) = message.data_from(self.file_name) {
280            self.on_file_name_changed(file_name, ui)
281        } else if let Some(DropdownListMessage::Selection(selection)) =
282            message.data_from(self.file_type_selector)
283        {
284            self.on_file_type_selected(*selection, ui)
285        }
286    }
287
288    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
289        self.window.preview_message(ui, message);
290
291        if let Some(MessageBoxMessage::Close(result)) = message.data() {
292            if message.destination() == self.overwrite_message_box.get() {
293                if let MessageBoxResult::Yes = *result {
294                    ui.send(self.handle, FileSelectorMessage::Commit(self.final_path()));
295                }
296
297                ui.send(self.overwrite_message_box.get(), WidgetMessage::Remove);
298
299                self.overwrite_message_box.set(Handle::NONE);
300            }
301        }
302    }
303
304    fn handle_os_event(
305        &mut self,
306        self_handle: Handle<UiNode>,
307        ui: &mut UserInterface,
308        event: &OsEvent,
309    ) {
310        self.window.handle_os_event(self_handle, ui, event);
311    }
312}
313
314pub struct FileSelectorBuilder {
315    window_builder: WindowBuilder,
316    filter: PathFilter,
317    mode: FileSelectorMode,
318    path: PathBuf,
319    root: Option<PathBuf>,
320    selected_file_type: Option<usize>,
321}
322
323impl FileSelectorBuilder {
324    pub fn new(window_builder: WindowBuilder) -> Self {
325        Self {
326            window_builder,
327            mode: FileSelectorMode::Open,
328            path: "./".into(),
329            root: None,
330            filter: Default::default(),
331            selected_file_type: None,
332        }
333    }
334
335    pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
336        path.as_ref().clone_into(&mut self.path);
337        self
338    }
339
340    pub fn with_mode(mut self, mode: FileSelectorMode) -> Self {
341        self.mode = mode;
342        self
343    }
344
345    pub fn with_root(mut self, root: PathBuf) -> Self {
346        self.root = Some(root);
347        self
348    }
349
350    pub fn with_filter(mut self, file_types: PathFilter) -> Self {
351        self.filter = file_types;
352        self
353    }
354
355    pub fn with_selected_file_type(mut self, selected: usize) -> Self {
356        self.selected_file_type = Some(selected);
357        self
358    }
359
360    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<FileSelector> {
361        let browser;
362        let ok;
363        let cancel;
364
365        if self.window_builder.title.is_none() {
366            self.window_builder.title = Some(WindowTitle::text("Select File"));
367        }
368
369        let file_name;
370        let name_grid = GridBuilder::new(
371            WidgetBuilder::new()
372                .with_visibility(!self.filter.folders_only)
373                .with_margin(Thickness::uniform(1.0))
374                .on_row(1)
375                .on_column(0)
376                .with_child(
377                    TextBuilder::new(
378                        WidgetBuilder::new()
379                            .on_row(0)
380                            .on_column(0)
381                            .with_vertical_alignment(VerticalAlignment::Center),
382                    )
383                    .with_text("File Name:")
384                    .build(ctx),
385                )
386                .with_child({
387                    file_name = TextBoxBuilder::new(
388                        WidgetBuilder::new()
389                            .on_row(0)
390                            .on_column(1)
391                            .with_height(25.0)
392                            .with_margin(Thickness::uniform(1.0)),
393                    )
394                    .with_text_commit_mode(TextCommitMode::Immediate)
395                    .with_vertical_text_alignment(VerticalAlignment::Center)
396                    .with_text(match self.mode {
397                        FileSelectorMode::Open => Default::default(),
398                        FileSelectorMode::Save {
399                            default_file_name: ref default_file_name_no_extension,
400                        } => default_file_name_no_extension.to_string_lossy().to_string(),
401                    })
402                    .build(ctx);
403                    file_name
404                }),
405        )
406        .add_row(Row::auto())
407        .add_column(Column::strict(80.0))
408        .add_column(Column::stretch())
409        .build(ctx);
410
411        let mut filter_items = self
412            .filter
413            .iter()
414            .map(|file_type| make_dropdown_list_option(ctx, &file_type.to_string()))
415            .collect::<Vec<_>>();
416
417        filter_items.insert(0, make_dropdown_list_option(ctx, "All Supported"));
418
419        let extension_selector;
420        let extension_grid = GridBuilder::new(
421            WidgetBuilder::new()
422                .with_visibility(!self.filter.folders_only)
423                .with_margin(Thickness::uniform(1.0))
424                .on_row(2)
425                .on_column(0)
426                .with_child(
427                    TextBuilder::new(
428                        WidgetBuilder::new()
429                            .on_row(0)
430                            .on_column(0)
431                            .with_vertical_alignment(VerticalAlignment::Center),
432                    )
433                    .with_text("File Type:")
434                    .build(ctx),
435                )
436                .with_child({
437                    extension_selector = DropdownListBuilder::new(
438                        WidgetBuilder::new()
439                            .with_height(25.0)
440                            .on_column(1)
441                            .with_margin(Thickness::uniform(1.0)),
442                    )
443                    .with_items(filter_items)
444                    .with_close_on_selection(true)
445                    .with_selected(0)
446                    .build(ctx);
447                    extension_selector
448                }),
449        )
450        .add_row(Row::auto())
451        .add_column(Column::strict(80.0))
452        .add_column(Column::stretch())
453        .build(ctx);
454
455        let browser_container = BorderBuilder::new(
456            WidgetBuilder::new()
457                .on_row(0)
458                .on_column(0)
459                .with_background(ctx.style.property(Style::BRUSH_LIGHT))
460                .with_child({
461                    browser = FileBrowserBuilder::new(
462                        WidgetBuilder::new()
463                            .with_margin(Thickness::uniform(1.0))
464                            .with_tab_index(Some(0)),
465                    )
466                    .with_filter(self.filter.clone())
467                    .with_path(self.path.clone())
468                    .with_opt_root(self.root)
469                    .build(ctx);
470                    browser
471                }),
472        )
473        .build(ctx);
474
475        let ok_enabled = match self.mode {
476            FileSelectorMode::Open => {
477                let passed = self
478                    .filter
479                    .supports_specific_type(&self.path, self.selected_file_type);
480                self.path.exists() && passed
481            }
482            FileSelectorMode::Save { .. } => true,
483        };
484
485        let buttons = StackPanelBuilder::new(
486            WidgetBuilder::new()
487                .with_margin(Thickness::uniform(1.0))
488                .with_horizontal_alignment(HorizontalAlignment::Right)
489                .on_row(3)
490                .on_column(0)
491                .with_child({
492                    ok = ButtonBuilder::new(
493                        WidgetBuilder::new()
494                            .with_tab_index(Some(1))
495                            .with_margin(Thickness::uniform(1.0))
496                            .with_width(100.0)
497                            .with_height(25.0)
498                            .with_enabled(ok_enabled),
499                    )
500                    .with_ok_back(ctx)
501                    .with_text(match &self.mode {
502                        FileSelectorMode::Open => "Open",
503                        FileSelectorMode::Save { .. } => "Save",
504                    })
505                    .build(ctx);
506                    ok
507                })
508                .with_child({
509                    cancel = ButtonBuilder::new(
510                        WidgetBuilder::new()
511                            .with_tab_index(Some(2))
512                            .with_margin(Thickness::uniform(1.0))
513                            .with_width(100.0)
514                            .with_height(25.0),
515                    )
516                    .with_cancel_back(ctx)
517                    .with_text("Cancel")
518                    .build(ctx);
519                    cancel
520                }),
521        )
522        .with_orientation(Orientation::Horizontal)
523        .build(ctx);
524
525        self.window_builder.widget_builder.preview_messages = true;
526
527        let window = self
528            .window_builder
529            .with_content(
530                GridBuilder::new(
531                    WidgetBuilder::new()
532                        .with_child(browser_container)
533                        .with_child(buttons)
534                        .with_child(name_grid)
535                        .with_child(extension_grid),
536                )
537                .add_column(Column::stretch())
538                .add_row(Row::stretch())
539                .add_row(Row::auto())
540                .add_row(Row::auto())
541                .add_row(Row::auto())
542                .build(ctx),
543            )
544            .build_window(ctx);
545
546        let file_selector = FileSelector {
547            window,
548            browser,
549            ok,
550            cancel,
551            selected_folder: extract_folder_path_buf(&self.path).unwrap_or_default(),
552            file_name_value: match self.mode {
553                FileSelectorMode::Open => Default::default(),
554                FileSelectorMode::Save {
555                    ref default_file_name,
556                } => default_file_name.clone(),
557            },
558            filter: self.filter,
559            file_type_selector: extension_selector,
560            mode: self.mode,
561            file_name,
562            selected_file_type: self.selected_file_type,
563            overwrite_message_box: Default::default(),
564        };
565
566        ctx.add(file_selector)
567    }
568}
569
570#[cfg(test)]
571mod test {
572    use crate::file_browser::FileSelectorBuilder;
573    use crate::window::WindowBuilder;
574    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
575
576    #[test]
577    fn test_deletion() {
578        test_widget_deletion(|ctx| {
579            FileSelectorBuilder::new(WindowBuilder::new(WidgetBuilder::new())).build(ctx)
580        });
581    }
582}