diff --git a/firmware/application/apps/ui_fileman.cpp b/firmware/application/apps/ui_fileman.cpp index 1787b70a..207f0261 100644 --- a/firmware/application/apps/ui_fileman.cpp +++ b/firmware/application/apps/ui_fileman.cpp @@ -22,7 +22,6 @@ /* TODO: * - Paging menu items - * - UI with empty SD card * - Copy/Move */ @@ -101,6 +100,9 @@ void insert_sorted(std::vector& entries, fileman_entry&& entry) { // Returns the partner file path or an empty path if no partner is found. fs::path get_partner_file(fs::path path) { + if (fs::is_directory(path)) + return { }; + const fs::path txt_path{ u".TXT" }; const fs::path c16_path{ u".C16" }; auto ext = path.extension(); @@ -113,12 +115,13 @@ fs::path get_partner_file(fs::path path) { return { }; path.replace_extension(ext); - return file_exists(path) ? path : fs::path{ }; + return fs::file_exists(path) && !fs::is_directory(path) ? path : fs::path{ }; } // Modal prompt to update the partner file if it exists. // Runs continuation on_partner_action to update the partner file. // Returns true is a partner is found, otherwise false. +// Path must be the full path to the file. bool partner_file_prompt( NavigationView& nav, const fs::path& path, @@ -147,6 +150,8 @@ bool partner_file_prompt( namespace ui { +/* FileManBaseView ***********************************************************/ + void FileManBaseView::load_directory_contents(const fs::path& dir_path) { current_path = dir_path; entry_list.clear(); @@ -180,6 +185,7 @@ fs::path FileManBaseView::get_selected_full_path() const { } const fileman_entry& FileManBaseView::get_selected_entry() const { + // TODO: return reference to an "empty" entry on OOB? return entry_list[menu_view.highlighted_index()]; } @@ -212,8 +218,7 @@ FileManBaseView::FileManBaseView( text_current.set("EMPTY SD CARD!"); } else { menu_view.on_left = [this]() { - current_path = current_path.parent_path(); - reload_current(); + pop_dir(); }; } } @@ -226,10 +231,32 @@ void FileManBaseView::focus() { } } +void FileManBaseView::push_dir(const fs::path& path) { + if (path == parent_dir_path) { + pop_dir(); + } else { + current_path /= path; + saved_index_stack.push_back(menu_view.highlighted_index()); + menu_view.set_highlighted(0); + reload_current(); + } +} + +void FileManBaseView::pop_dir() { + if (saved_index_stack.empty()) + return; + + current_path = current_path.parent_path(); + reload_current(); + menu_view.set_highlighted(saved_index_stack.back()); + saved_index_stack.pop_back(); +} + void FileManBaseView::refresh_list() { if (on_refresh_widgets) on_refresh_widgets(false); + auto prev_highlight = menu_view.highlighted_index(); menu_view.clear(); for (const auto& entry : entry_list) { @@ -262,7 +289,7 @@ void FileManBaseView::refresh_list() { } } - menu_view.set_highlighted(0); // Refresh + menu_view.set_highlighted(prev_highlight); } void FileManBaseView::reload_current() { @@ -305,6 +332,8 @@ FileSaveView::FileSaveView( }; }*/ +/* FileLoadView **************************************************************/ + void FileLoadView::refresh_widgets(const bool) { set_dirty(); } @@ -329,35 +358,32 @@ FileLoadView::FileLoadView( on_select_entry = [this](KeyEvent) { if (get_selected_entry().is_directory) { - current_path = get_selected_full_path(); - reload_current(); + push_dir(get_selected_entry().path); } else { - nav_.pop(); if (on_changed) on_changed(get_selected_full_path()); + nav_.pop(); } }; } +/* FileManagerView ***********************************************************/ + void FileManagerView::on_rename() { auto& entry = get_selected_entry(); - - // Don't rename ".." - if (entry.path == parent_dir_path) - return; - name_buffer = entry.path.filename().string(); - uint32_t cursor_pos = (uint32_t)name_buffer.length(); - if (auto pos = name_buffer.find_last_of("."); pos != name_buffer.npos) + + if (auto pos = name_buffer.find_last_of("."); + pos != name_buffer.npos && !entry.is_directory) cursor_pos = pos; text_prompt(nav_, name_buffer, cursor_pos, max_filename_length, - [this, &entry](std::string& renamed) { + [this](std::string& renamed) { auto renamed_path = fs::path{ renamed }; rename_file(get_selected_full_path(), current_path / renamed_path); - auto has_partner = partner_file_prompt(nav_, entry.path, "Rename", + auto has_partner = partner_file_prompt(nav_, get_selected_full_path(), "Rename", [this, renamed_path](const fs::path& partner, bool should_rename) mutable { if (should_rename) { auto new_name = renamed_path.replace_extension(partner.extension()); @@ -373,20 +399,14 @@ void FileManagerView::on_rename() { } void FileManagerView::on_delete() { - auto& entry = get_selected_entry(); - - // Don't delete ".." - if (entry.path == parent_dir_path) - return; - - auto name = entry.path.filename().string(); + auto name = get_selected_entry().path.filename().string(); nav_.push("Delete", "Delete " + name + "\nAre you sure?", YESNO, - [this, &entry](bool choice) { + [this](bool choice) { if (choice) { delete_file(get_selected_full_path()); auto has_partner = partner_file_prompt( - nav_, entry.path, "Delete", + nav_, get_selected_full_path(), "Delete", [this](const fs::path& partner, bool should_delete) { if (should_delete) delete_file(current_path / partner); @@ -409,6 +429,11 @@ void FileManagerView::on_new_dir() { }); } +bool FileManagerView::selected_is_valid() const { + return !entry_list.empty() && + get_selected_entry().path != parent_dir_path; +} + void FileManagerView::refresh_widgets(const bool v) { button_rename.hidden(v); button_delete.hidden(v); @@ -438,26 +463,31 @@ FileManagerView::FileManagerView( }); menu_view.on_highlight = [this]() { - text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path()))); + // TODO: enable/disable buttons. + if (selected_is_valid()) + text_date.set(to_string_FAT_timestamp(file_created_date(get_selected_full_path()))); + else + text_date.set(""); }; refresh_list(); - + on_select_entry = [this](KeyEvent key) { if (key == KeyEvent::Select && get_selected_entry().is_directory) { - load_directory_contents(get_selected_full_path()); - refresh_list(); + push_dir(get_selected_entry().path); } else { button_rename.focus(); } }; button_rename.on_select = [this](Button&) { - on_rename(); + if (selected_is_valid()) + on_rename(); }; button_delete.on_select = [this](Button&) { - on_delete(); + if (selected_is_valid()) + on_delete(); }; button_new_dir.on_select = [this](Button&) { diff --git a/firmware/application/apps/ui_fileman.hpp b/firmware/application/apps/ui_fileman.hpp index 87cc8d11..3033cdc5 100644 --- a/firmware/application/apps/ui_fileman.hpp +++ b/firmware/application/apps/ui_fileman.hpp @@ -69,6 +69,8 @@ protected: std::filesystem::path get_selected_full_path() const; const fileman_entry& get_selected_entry() const; + void push_dir(const std::filesystem::path& path); + void pop_dir(); void refresh_list(); void reload_current(); void load_directory_contents(const std::filesystem::path& dir_path); @@ -85,6 +87,7 @@ protected: std::filesystem::path extension_filter { u"" }; std::vector entry_list { }; + std::vector saved_index_stack { }; Labels labels { { { 0, 0 }, "Path:", Color::light_grey() } @@ -152,6 +155,9 @@ private: void on_rename(); void on_delete(); void on_new_dir(); + + // True if the selected entry is a real file item. + bool selected_is_valid() const; Labels labels { { { 0, 26 * 8 }, "Created ", Color::light_grey() } diff --git a/firmware/application/file.cpp b/firmware/application/file.cpp index 6dfc2582..c63dc157 100644 --- a/firmware/application/file.cpp +++ b/firmware/application/file.cpp @@ -197,13 +197,6 @@ std::vector scan_root_directories(const std::filesystem:: return directory_list; } -bool file_exists(const std::filesystem::path& file_path) { - FILINFO filinfo; - auto fr = f_stat(reinterpret_cast(file_path.c_str()), &filinfo); - - return fr == FR_OK; -} - uint32_t delete_file(const std::filesystem::path& file_path) { return f_unlink(reinterpret_cast(file_path.c_str())); } @@ -316,6 +309,10 @@ bool operator==(const path& lhs, const path& rhs) { return lhs.native() == rhs.native(); } +bool operator!=(const path& lhs, const path& rhs) { + return !(lhs == rhs); +} + bool operator<(const path& lhs, const path& rhs) { return lhs.native() < rhs.native(); } @@ -343,7 +340,7 @@ directory_iterator::directory_iterator( { impl = std::make_shared(); const auto result = f_findfirst(&impl->dir, &impl->filinfo, reinterpret_cast(path.c_str()), reinterpret_cast(pattern.c_str())); - if( result != FR_OK ) { + if( result != FR_OK || impl->filinfo.fname[0] == (TCHAR)'\0') { impl.reset(); // TODO: Throw exception if/when I enable exceptions... } @@ -365,6 +362,20 @@ bool is_regular_file(const file_status s) { return !(s & AM_DIR); } +bool file_exists(const path& file_path) { + FILINFO filinfo; + auto fr = f_stat(reinterpret_cast(file_path.c_str()), &filinfo); + + return fr == FR_OK; +} + +bool is_directory(const path& file_path) { + FILINFO filinfo; + auto fr = f_stat(reinterpret_cast(file_path.c_str()), &filinfo); + + return fr == FR_OK && is_directory(static_cast(filinfo.fattrib)); +} + space_info space(const path& p) { DWORD free_clusters { 0 }; FATFS* fs; diff --git a/firmware/application/file.hpp b/firmware/application/file.hpp index a023c7b3..6e267848 100644 --- a/firmware/application/file.hpp +++ b/firmware/application/file.hpp @@ -166,6 +166,7 @@ private: }; bool operator==(const path& lhs, const path& rhs); +bool operator!=(const path& lhs, const path& rhs); bool operator<(const path& lhs, const path& rhs); bool operator>(const path& lhs, const path& rhs); path operator+(const path& lhs, const path& rhs); @@ -238,6 +239,8 @@ inline bool operator!=(const directory_iterator& lhs, const directory_iterator& bool is_directory(const file_status s); bool is_regular_file(const file_status s); +bool file_exists(const path& file_path); +bool is_directory(const path& file_path); space_info space(const path& p); @@ -249,7 +252,6 @@ struct FATTimestamp { uint16_t FAT_time; }; -bool file_exists(const std::filesystem::path& file_path); uint32_t delete_file(const std::filesystem::path& file_path); uint32_t rename_file(const std::filesystem::path& file_path, const std::filesystem::path& new_name); FATTimestamp file_created_date(const std::filesystem::path& file_path); diff --git a/firmware/application/ui/ui_menu.cpp b/firmware/application/ui/ui_menu.cpp index eb6063f0..4b548aa6 100644 --- a/firmware/application/ui/ui_menu.cpp +++ b/firmware/application/ui/ui_menu.cpp @@ -163,6 +163,8 @@ void MenuView::clear() { } menu_items.clear(); + highlighted_item = 0; + offset = 0; } void MenuView::add_item(MenuItem new_item) { @@ -209,7 +211,7 @@ MenuItemView* MenuView::item_view(size_t index) const { bool MenuView::set_highlighted(int32_t new_value) { int32_t item_count = (int32_t)menu_items.size(); - if (new_value < 0) + if (new_value < 0 || item_count == 0) return false; if (new_value >= item_count)