use super::View;
use super::button::Button;
use super::dialog::Dialog;
use super::input_line::InputLine;
use super::label::Label;
use super::listbox::ListBox;
use crate::core::command::{CM_CANCEL, CM_FILE_FOCUSED, CM_OK, CommandId};
use crate::core::event::{Event, EventType};
use crate::core::geometry::Rect;
use crate::terminal::Terminal;
use std::cell::RefCell;
use std::fs;
use std::path::PathBuf;
use std::rc::Rc;
const CMD_FILE_SELECTED: u16 = 1000;
const CHILD_LISTBOX: usize = 4; const CHILD_OK_BUTTON: usize = 5;
pub struct FileDialog {
dialog: Dialog,
current_path: PathBuf,
wildcard: String,
file_name_data: Rc<RefCell<String>>,
files: Vec<String>,
selected_file_index: usize, title: String, button_label: String, }
impl FileDialog {
pub fn new(bounds: Rect, title: &str, wildcard: &str, initial_dir: Option<PathBuf>) -> Self {
let dialog = Dialog::new(bounds, title);
let current_path = initial_dir
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
let file_name_data = Rc::new(RefCell::new(String::new()));
Self {
dialog,
current_path,
wildcard: wildcard.to_string(),
file_name_data,
files: Vec::new(),
selected_file_index: 0,
title: title.to_string(),
button_label: "~O~pen".to_string(), }
}
pub fn with_button_label(mut self, label: &str) -> Self {
self.button_label = label.to_string();
self
}
pub fn build(mut self) -> Self {
let bounds = self.dialog.bounds();
let dialog_width = bounds.width();
let content_width = dialog_width - 15;
let name_label = Label::new(Rect::new(2, 1, 12, 1), "~N~ame:");
self.dialog.add(Box::new(name_label));
let file_input = InputLine::new(
Rect::new(12, 1, content_width, 2),
255,
self.file_name_data.clone(),
);
self.dialog.add(Box::new(file_input));
let path_str = format!(" {}", self.current_path.display());
let path_label = Label::new(Rect::new(2, 3, content_width, 3), &path_str);
self.dialog.add(Box::new(path_label));
let files_label = Label::new(Rect::new(2, 5, 12, 5), "~F~iles:");
self.dialog.add(Box::new(files_label));
let mut file_list = ListBox::new(
Rect::new(2, 6, content_width, bounds.height() - 2),
CMD_FILE_SELECTED,
);
self.read_directory();
file_list.set_items(self.files.clone());
self.dialog.add(Box::new(file_list));
let button_x = dialog_width - 14; let mut button_y = 6;
let button_text = format!(" {} ", self.button_label);
let open_button = Button::new(
Rect::new(button_x, button_y, button_x + 11, button_y + 2),
&button_text,
CM_OK,
true,
);
self.dialog.add(Box::new(open_button));
button_y += 3;
let cancel_button = Button::new(
Rect::new(button_x, button_y, button_x + 11, button_y + 2),
" ~C~ancel ",
CM_CANCEL,
false,
);
self.dialog.add(Box::new(cancel_button));
self.dialog.set_initial_focus();
self
}
pub fn execute(&mut self, app: &mut crate::app::Application) -> Option<PathBuf> {
use crate::core::state::SF_MODAL;
let old_state = self.dialog.state();
self.dialog.set_state(old_state | SF_MODAL);
loop {
self.update_ok_button_state();
app.desktop.draw(&mut app.terminal);
if let Some(ref mut menu_bar) = app.menu_bar {
menu_bar.draw(&mut app.terminal);
}
if let Some(ref mut status_line) = app.status_line {
status_line.draw(&mut app.terminal);
}
self.dialog.draw(&mut app.terminal);
for widget in &mut app.overlay_widgets {
widget.draw(&mut app.terminal);
}
self.dialog.update_cursor(&mut app.terminal);
let _ = app.terminal.flush();
match app
.terminal
.poll_event(std::time::Duration::from_millis(20))
.ok()
.flatten()
{
Some(mut event) => {
if event.what == EventType::Keyboard
&& event.key_code == crate::core::event::KB_ESC_ESC
{
return None;
}
self.dialog.handle_event(&mut event);
let end_state = self.dialog.get_end_state();
if end_state == crate::core::command::CM_CANCEL
|| end_state == crate::core::command::CM_CLOSE {
return None;
}
self.sync_inputline_with_listbox();
if event.what == EventType::Command {
match event.command {
CM_OK => {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
if self.contains_wildcards(&file_name) {
self.wildcard = file_name.clone();
self.read_directory();
if CHILD_LISTBOX < self.dialog.child_count() {
let view = self.dialog.child_at_mut(CHILD_LISTBOX);
if let Some(listbox) =
view.as_any_mut().downcast_mut::<ListBox>()
{
listbox.set_items(self.files.clone());
listbox.set_list_selection(0);
}
}
*self.file_name_data.borrow_mut() = self.wildcard.clone();
self.selected_file_index = 0;
self.dialog.set_end_state(0);
app.terminal.force_full_redraw();
continue;
}
if let Some(path) =
self.handle_selection(&file_name, &mut app.terminal)
{
return Some(path);
}
self.dialog.set_end_state(0);
} else {
self.dialog.set_end_state(0);
}
}
CM_CANCEL | crate::core::command::CM_CLOSE => {
return None;
}
CMD_FILE_SELECTED => {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
if let Some(path) =
self.handle_selection(&file_name, &mut app.terminal)
{
return Some(path);
}
}
}
_ => {}
}
}
}
None => {
app.idle();
}
}
}
}
fn sync_inputline_with_listbox(&mut self) {
if CHILD_LISTBOX >= self.dialog.child_count() {
return;
}
let listbox = self.dialog.child_at(CHILD_LISTBOX);
let new_selection = listbox.get_list_selection();
if new_selection != self.selected_file_index {
self.selected_file_index = new_selection;
if self.selected_file_index < self.files.len() {
let selected = self.files[self.selected_file_index].clone();
let display_text = if selected.starts_with('[') && selected.ends_with(']') {
let dir_name = &selected[1..selected.len() - 1];
format!("{}/{}", dir_name, self.wildcard)
} else if selected == ".." {
selected.clone()
} else {
selected.clone()
};
*self.file_name_data.borrow_mut() = display_text;
let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
self.dialog.handle_event(&mut broadcast);
}
}
}
fn handle_selection(&mut self, file_name: &str, terminal: &mut Terminal) -> Option<PathBuf> {
if let Some(slash_pos) = file_name.rfind('/') {
let dir_part = &file_name[..slash_pos];
let file_part = &file_name[slash_pos + 1..];
if !dir_part.is_empty() {
self.current_path.push(dir_part);
}
if self.contains_wildcards(file_part) {
self.wildcard = file_part.to_string();
}
self.rebuild_and_redraw(terminal);
return None; }
if file_name == ".." {
if let Some(parent) = self.current_path.parent() {
self.current_path = parent.to_path_buf();
self.rebuild_and_redraw(terminal);
}
None } else if file_name.starts_with('[') && file_name.ends_with(']') {
let dir_name = &file_name[1..file_name.len() - 1];
self.current_path.push(dir_name);
self.rebuild_and_redraw(terminal);
None } else {
*self.file_name_data.borrow_mut() = file_name.to_string();
Some(self.current_path.join(file_name)) }
}
fn update_ok_button_state(&mut self) {
use crate::core::state::SF_DISABLED;
let file_name = self.file_name_data.borrow().clone();
let should_disable = file_name.is_empty()
|| file_name == ".."
|| file_name.starts_with('[') && file_name.ends_with(']')
|| file_name.contains('/');
if CHILD_OK_BUTTON < self.dialog.child_count() {
let ok_button = self.dialog.child_at_mut(CHILD_OK_BUTTON);
ok_button.set_state_flag(SF_DISABLED, should_disable);
}
}
fn rebuild_and_redraw(&mut self, _terminal: &mut Terminal) {
let old_bounds = self.dialog.bounds();
let old_title = self.title.clone();
let old_button_label = self.button_label.clone();
*self = Self::new(
old_bounds,
&old_title,
&self.wildcard.clone(),
Some(self.current_path.clone()),
)
.with_button_label(&old_button_label)
.build();
if CHILD_LISTBOX < self.dialog.child_count() {
self.dialog.set_focus_to_child(CHILD_LISTBOX);
self.dialog
.child_at_mut(CHILD_LISTBOX)
.set_list_selection(0);
}
self.selected_file_index = 0;
if !self.files.is_empty() {
let first_item = self.files[0].clone();
let display_text = if first_item.starts_with('[') && first_item.ends_with(']') {
let dir_name = &first_item[1..first_item.len() - 1];
format!("{}/{}", dir_name, self.wildcard)
} else if first_item == ".." {
first_item.clone()
} else {
if self.wildcard.contains('*') || self.wildcard.contains('?') {
self.wildcard.clone()
} else {
first_item.clone()
}
};
*self.file_name_data.borrow_mut() = display_text;
let mut broadcast = Event::broadcast(CM_FILE_FOCUSED);
self.dialog.handle_event(&mut broadcast);
} else {
if self.wildcard.contains('*') || self.wildcard.contains('?') {
*self.file_name_data.borrow_mut() = self.wildcard.clone();
} else {
*self.file_name_data.borrow_mut() = String::new();
}
}
}
fn read_directory(&mut self) {
self.files.clear();
if self.current_path.parent().is_some() {
self.files.push("..".to_string());
}
if let Ok(entries) = fs::read_dir(&self.current_path) {
let mut dirs = Vec::new();
let mut regular_files = Vec::new();
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
let name = entry.file_name().to_string_lossy().to_string();
if metadata.is_dir() {
dirs.push(format!("[{}]", name));
} else if self.matches_wildcard(&name) {
regular_files.push(name);
}
}
}
dirs.sort();
regular_files.sort();
self.files.extend(dirs);
self.files.extend(regular_files);
}
}
fn contains_wildcards(&self, name: &str) -> bool {
name.contains('*') || name.contains('?')
}
fn matches_wildcard(&self, name: &str) -> bool {
if self.wildcard == "*.*" || self.wildcard == "*" || self.wildcard.is_empty() {
return true;
}
if let Some(ext) = self.wildcard.strip_prefix("*.") {
name.ends_with(&format!(".{}", ext))
} else {
name.contains(&self.wildcard)
}
}
pub fn get_selected_file(&self) -> Option<PathBuf> {
let file_name = self.file_name_data.borrow().clone();
if !file_name.is_empty() {
Some(self.current_path.join(file_name))
} else {
None
}
}
pub fn get_current_directory(&self) -> PathBuf {
self.current_path.clone()
}
pub fn get_end_state(&self) -> CommandId {
self.dialog.get_end_state()
}
}
impl View for FileDialog {
fn bounds(&self) -> Rect {
self.dialog.bounds()
}
fn set_bounds(&mut self, bounds: Rect) {
self.dialog.set_bounds(bounds);
}
fn draw(&mut self, terminal: &mut Terminal) {
self.dialog.draw(terminal);
}
fn handle_event(&mut self, event: &mut Event) {
self.dialog.handle_event(event);
}
fn get_palette(&self) -> Option<crate::core::palette::Palette> {
self.dialog.get_palette()
}
}
pub struct FileDialogBuilder {
bounds: Option<Rect>,
title: Option<String>,
wildcard: String,
initial_dir: Option<PathBuf>,
button_label: String,
}
impl FileDialogBuilder {
pub fn new() -> Self {
Self {
bounds: None,
title: None,
wildcard: "*".to_string(),
initial_dir: None,
button_label: "~O~pen".to_string(),
}
}
#[must_use]
pub fn bounds(mut self, bounds: Rect) -> Self {
self.bounds = Some(bounds);
self
}
#[must_use]
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn wildcard(mut self, wildcard: impl Into<String>) -> Self {
self.wildcard = wildcard.into();
self
}
#[must_use]
pub fn initial_dir(mut self, dir: PathBuf) -> Self {
self.initial_dir = Some(dir);
self
}
#[must_use]
pub fn button_label(mut self, label: impl Into<String>) -> Self {
self.button_label = label.into();
self
}
pub fn build(self) -> FileDialog {
let bounds = self.bounds.expect("FileDialog bounds must be set");
let title = self.title.expect("FileDialog title must be set");
FileDialog::new(bounds, &title, &self.wildcard, self.initial_dir)
.with_button_label(&self.button_label)
.build()
}
pub fn build_boxed(self) -> Box<FileDialog> {
Box::new(self.build())
}
}
impl Default for FileDialogBuilder {
fn default() -> Self {
Self::new()
}
}