1use 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#[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 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
247impl 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}