use crate::button::Button;
use crate::dropdown_list::DropdownList;
use crate::file_browser::FileBrowser;
use crate::messagebox::MessageBox;
use crate::text_box::TextBox;
use crate::{
border::BorderBuilder,
button::{ButtonBuilder, ButtonMessage},
core::{
algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
uuid_provider, visitor::prelude::*,
},
draw::DrawingContext,
dropdown_list::{DropdownListBuilder, DropdownListMessage},
file_browser::{FileBrowserBuilder, FileBrowserMessage, PathFilter},
grid::{Column, GridBuilder, Row},
message::{MessageData, OsEvent, UiMessage},
messagebox::{MessageBoxBuilder, MessageBoxButtons, MessageBoxMessage, MessageBoxResult},
stack_panel::StackPanelBuilder,
style::{resource::StyleResourceExt, Style},
text::{TextBuilder, TextMessage},
text_box::{TextBoxBuilder, TextCommitMode},
utils::make_dropdown_list_option,
widget::{Widget, WidgetBuilder, WidgetMessage},
window::{Window, WindowBuilder, WindowMessage, WindowTitle},
BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
VerticalAlignment,
};
use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
use std::{
cell::Cell,
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
#[derive(Default, Clone, PartialEq, Eq, Hash, Debug, Visit, Reflect)]
pub enum FileSelectorMode {
#[default]
Open,
Save {
default_file_name: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum FileSelectorMessage {
Root(Option<PathBuf>),
Path(PathBuf),
Commit(PathBuf),
FocusCurrentPath,
Cancel,
FileTypes(PathFilter),
}
impl MessageData for FileSelectorMessage {}
#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
#[reflect(derived_type = "UiNode")]
pub struct FileSelector {
#[component(include)]
pub window: Window,
pub browser: Handle<FileBrowser>,
pub ok: Handle<Button>,
pub cancel: Handle<Button>,
pub selected_folder: PathBuf,
pub mode: FileSelectorMode,
pub file_name: Handle<TextBox>,
pub file_name_value: PathBuf,
pub filter: PathFilter,
pub file_type_selector: Handle<DropdownList>,
pub selected_file_type: Option<usize>,
pub overwrite_message_box: Cell<Handle<MessageBox>>,
}
impl ConstructorProvider<UiNode, UserInterface> for FileSelector {
fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
GraphNodeConstructor::new::<Self>()
.with_variant("File Selector", |ui| {
FileSelectorBuilder::new(WindowBuilder::new(
WidgetBuilder::new().with_name("File Selector"),
))
.build(&mut ui.build_ctx())
.to_base()
.into()
})
.with_group("File System")
}
}
impl Deref for FileSelector {
type Target = Widget;
fn deref(&self) -> &Self::Target {
&self.window
}
}
impl DerefMut for FileSelector {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.window
}
}
uuid_provider!(FileSelector = "878b2220-03e6-4a50-a97d-3a8e5397b6cb");
fn extract_folder_path(path: &Path) -> Option<&Path> {
if path.is_file() {
path.parent()
} else if path.is_dir() {
Some(path)
} else {
None
}
}
fn extract_folder_path_buf(path: &Path) -> Option<PathBuf> {
extract_folder_path(path).map(|p| p.to_path_buf())
}
impl FileSelector {
fn on_ok_clicked(&self, ui: &mut UserInterface) {
let final_path = self.final_path();
if final_path.exists() && matches!(self.mode, FileSelectorMode::Save { .. }) {
self.overwrite_message_box.set(
MessageBoxBuilder::new(
WindowBuilder::new(WidgetBuilder::new().with_width(350.0).with_height(100.0))
.with_title(WindowTitle::text("Confirm Action"))
.open(false),
)
.with_text(
format!(
"The file {} already exist. Do you want to overwrite it?",
final_path.display()
)
.as_str(),
)
.with_buttons(MessageBoxButtons::YesNo)
.build(&mut ui.build_ctx()),
);
ui.send(
self.overwrite_message_box.get(),
MessageBoxMessage::Open {
title: None,
text: None,
},
);
} else {
ui.send(self.handle, FileSelectorMessage::Commit(self.final_path()));
}
}
fn on_path_selected(&mut self, path: &Path, ui: &UserInterface) {
if path.is_file() {
ui.send(
self.file_name,
TextMessage::Text(
path.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_default(),
),
);
self.selected_folder = extract_folder_path_buf(path).unwrap_or_default();
} else {
self.selected_folder = path.to_path_buf();
}
self.validate_selection(ui);
}
fn on_file_selector_message(&mut self, msg: &FileSelectorMessage, ui: &UserInterface) {
match msg {
FileSelectorMessage::Commit(_) | FileSelectorMessage::Cancel => {
ui.send(self.handle, WindowMessage::Close)
}
FileSelectorMessage::Path(path) => {
ui.send(self.browser, FileBrowserMessage::Path(path.clone()))
}
FileSelectorMessage::Root(root) => {
ui.send(self.browser, FileBrowserMessage::Root(root.clone()));
}
FileSelectorMessage::FileTypes(filter) => {
ui.send(self.browser, FileBrowserMessage::Filter(filter.clone()));
}
FileSelectorMessage::FocusCurrentPath => {
ui.send(self.browser, FileBrowserMessage::FocusCurrentPath);
}
}
}
fn final_path(&self) -> PathBuf {
let mut final_path = self.selected_folder.join(&self.file_name_value);
if let Some(file_type) = self.selected_file_type.and_then(|i| self.filter.get(i)) {
final_path.set_extension(&file_type.extension);
}
final_path
}
fn validate_selection(&self, ui: &UserInterface) {
let final_path = self.final_path();
let passed = self
.filter
.supports_specific_type(&final_path, self.selected_file_type)
&& match self.mode {
FileSelectorMode::Open => final_path.exists(),
FileSelectorMode::Save { .. } => true,
};
ui.send(self.ok, WidgetMessage::Enabled(passed))
}
fn on_file_type_selected(&mut self, selection: Option<usize>, ui: &UserInterface) {
let selection = selection.and_then(|i| i.checked_sub(1));
self.selected_file_type = selection;
self.validate_selection(ui);
}
fn on_file_name_changed(&mut self, file_name: &str, ui: &UserInterface) {
self.file_name_value = file_name.into();
self.validate_selection(ui);
}
}
impl Control for FileSelector {
fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
self.window.measure_override(ui, available_size)
}
fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
self.window.arrange_override(ui, final_size)
}
fn draw(&self, drawing_context: &mut DrawingContext) {
self.window.draw(drawing_context)
}
fn update(&mut self, dt: f32, ui: &mut UserInterface) {
self.window.update(dt, ui);
}
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.window.handle_routed_message(ui, message);
if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
if message.destination() == self.ok {
self.on_ok_clicked(ui)
} else if message.destination() == self.cancel {
ui.send(self.handle, FileSelectorMessage::Cancel)
}
} else if let Some(msg) = message.data_for::<FileSelectorMessage>(self.handle) {
self.on_file_selector_message(msg, ui)
} else if let Some(FileBrowserMessage::Path(path)) = message.data_from(self.browser) {
self.on_path_selected(path, ui)
} else if let Some(TextMessage::Text(file_name)) = message.data_from(self.file_name) {
self.on_file_name_changed(file_name, ui)
} else if let Some(DropdownListMessage::Selection(selection)) =
message.data_from(self.file_type_selector)
{
self.on_file_type_selected(*selection, ui)
}
}
fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
self.window.preview_message(ui, message);
if let Some(MessageBoxMessage::Close(result)) =
message.data_from(self.overwrite_message_box.get())
{
if let MessageBoxResult::Yes = *result {
ui.send(self.handle, FileSelectorMessage::Commit(self.final_path()));
}
ui.send(self.overwrite_message_box.get(), WidgetMessage::Remove);
self.overwrite_message_box.set(Handle::NONE);
}
}
fn handle_os_event(
&mut self,
self_handle: Handle<UiNode>,
ui: &mut UserInterface,
event: &OsEvent,
) {
self.window.handle_os_event(self_handle, ui, event);
}
}
pub struct FileSelectorBuilder {
window_builder: WindowBuilder,
filter: PathFilter,
mode: FileSelectorMode,
path: PathBuf,
root: Option<PathBuf>,
selected_file_type: Option<usize>,
}
impl FileSelectorBuilder {
pub fn new(window_builder: WindowBuilder) -> Self {
Self {
window_builder,
mode: FileSelectorMode::Open,
path: "./".into(),
root: None,
filter: Default::default(),
selected_file_type: None,
}
}
pub fn with_path<P: AsRef<Path>>(mut self, path: P) -> Self {
path.as_ref().clone_into(&mut self.path);
self
}
pub fn with_mode(mut self, mode: FileSelectorMode) -> Self {
self.mode = mode;
self
}
pub fn with_root(mut self, root: PathBuf) -> Self {
self.root = Some(root);
self
}
pub fn with_filter(mut self, file_types: PathFilter) -> Self {
self.filter = file_types;
self
}
pub fn with_selected_file_type(mut self, selected: usize) -> Self {
self.selected_file_type = Some(selected);
self
}
pub fn build(mut self, ctx: &mut BuildContext) -> Handle<FileSelector> {
let browser;
let ok;
let cancel;
if self.window_builder.title.is_none() {
self.window_builder.title = Some(WindowTitle::text("Select File"));
}
let file_name;
let name_grid = GridBuilder::new(
WidgetBuilder::new()
.with_visibility(!self.filter.folders_only)
.with_margin(Thickness::uniform(1.0))
.on_row(1)
.on_column(0)
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(0)
.with_vertical_alignment(VerticalAlignment::Center),
)
.with_text("File Name:")
.build(ctx),
)
.with_child({
file_name = TextBoxBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(1)
.with_height(25.0)
.with_margin(Thickness::uniform(1.0)),
)
.with_text_commit_mode(TextCommitMode::Immediate)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(match self.mode {
FileSelectorMode::Open => Default::default(),
FileSelectorMode::Save {
default_file_name: ref default_file_name_no_extension,
} => default_file_name_no_extension.to_string_lossy().to_string(),
})
.build(ctx);
file_name
}),
)
.add_row(Row::auto())
.add_column(Column::strict(80.0))
.add_column(Column::stretch())
.build(ctx);
let mut filter_items = self
.filter
.iter()
.map(|file_type| make_dropdown_list_option(ctx, &file_type.to_string()))
.collect::<Vec<_>>();
filter_items.insert(0, make_dropdown_list_option(ctx, "All Supported"));
let extension_selector;
let extension_grid = GridBuilder::new(
WidgetBuilder::new()
.with_visibility(!self.filter.folders_only)
.with_margin(Thickness::uniform(1.0))
.on_row(2)
.on_column(0)
.with_child(
TextBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(0)
.with_vertical_alignment(VerticalAlignment::Center),
)
.with_text("File Type:")
.build(ctx),
)
.with_child({
extension_selector = DropdownListBuilder::new(
WidgetBuilder::new()
.with_height(25.0)
.on_column(1)
.with_margin(Thickness::uniform(1.0)),
)
.with_items(filter_items)
.with_close_on_selection(true)
.with_selected(0)
.build(ctx);
extension_selector
}),
)
.add_row(Row::auto())
.add_column(Column::strict(80.0))
.add_column(Column::stretch())
.build(ctx);
let browser_container = BorderBuilder::new(
WidgetBuilder::new()
.on_row(0)
.on_column(0)
.with_background(ctx.style.property(Style::BRUSH_LIGHT))
.with_child({
browser = FileBrowserBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_tab_index(Some(0)),
)
.with_filter(self.filter.clone())
.with_path(self.path.clone())
.with_opt_root(self.root)
.build(ctx);
browser
}),
)
.build(ctx);
let ok_enabled = match self.mode {
FileSelectorMode::Open => {
let passed = self
.filter
.supports_specific_type(&self.path, self.selected_file_type);
self.path.exists() && passed
}
FileSelectorMode::Save { .. } => true,
};
let buttons = StackPanelBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(1.0))
.with_horizontal_alignment(HorizontalAlignment::Right)
.on_row(3)
.on_column(0)
.with_child({
ok = ButtonBuilder::new(
WidgetBuilder::new()
.with_tab_index(Some(1))
.with_margin(Thickness::uniform(1.0))
.with_width(100.0)
.with_height(25.0)
.with_enabled(ok_enabled),
)
.with_ok_back(ctx)
.with_text(match &self.mode {
FileSelectorMode::Open => "Open",
FileSelectorMode::Save { .. } => "Save",
})
.build(ctx);
ok
})
.with_child({
cancel = ButtonBuilder::new(
WidgetBuilder::new()
.with_tab_index(Some(2))
.with_margin(Thickness::uniform(1.0))
.with_width(100.0)
.with_height(25.0),
)
.with_cancel_back(ctx)
.with_text("Cancel")
.build(ctx);
cancel
}),
)
.with_orientation(Orientation::Horizontal)
.build(ctx);
self.window_builder.widget_builder.preview_messages = true;
let window = self
.window_builder
.with_content(
GridBuilder::new(
WidgetBuilder::new()
.with_child(browser_container)
.with_child(buttons)
.with_child(name_grid)
.with_child(extension_grid),
)
.add_column(Column::stretch())
.add_row(Row::stretch())
.add_row(Row::auto())
.add_row(Row::auto())
.add_row(Row::auto())
.build(ctx),
)
.build_window(ctx);
let file_selector = FileSelector {
window,
browser,
ok,
cancel,
selected_folder: extract_folder_path_buf(&self.path).unwrap_or_default(),
file_name_value: match self.mode {
FileSelectorMode::Open => Default::default(),
FileSelectorMode::Save {
ref default_file_name,
} => default_file_name.clone(),
},
filter: self.filter,
file_type_selector: extension_selector,
mode: self.mode,
file_name,
selected_file_type: self.selected_file_type,
overwrite_message_box: Default::default(),
};
ctx.add(file_selector)
}
}
#[cfg(test)]
mod test {
use crate::file_browser::FileSelectorBuilder;
use crate::window::WindowBuilder;
use crate::{test::test_widget_deletion, widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| {
FileSelectorBuilder::new(WindowBuilder::new(WidgetBuilder::new())).build(ctx)
});
}
}