menu: add filepicker, sidebar: add file picker entry.

the filepicker is a stripped down version of the file browser. only file picking is supported atm, no "select folder" yet.
This commit is contained in:
ITotalJustice
2025-08-02 22:19:48 +01:00
parent 1a00db9d55
commit 6554b68efa
6 changed files with 904 additions and 107 deletions

View File

@@ -0,0 +1,216 @@
#pragma once
#include "ui/menus/menu_base.hpp"
#include "ui/scrolling_text.hpp"
#include "ui/list.hpp"
#include "fs.hpp"
#include "option.hpp"
#include <span>
namespace sphaira::ui::menu::filepicker {
enum FsEntryFlag {
FsEntryFlag_None,
// write protected.
FsEntryFlag_ReadOnly = 1 << 0,
// supports file assoc.
FsEntryFlag_Assoc = 1 << 1,
};
enum class FsType {
Sd,
ImageNand,
ImageSd,
Stdio,
};
enum SortType {
SortType_Size,
SortType_Alphabetical,
};
enum OrderType {
OrderType_Descending,
OrderType_Ascending,
};
struct FsEntry {
fs::FsPath name{};
fs::FsPath root{};
FsType type{};
u32 flags{FsEntryFlag_None};
auto IsReadOnly() const -> bool {
return flags & FsEntryFlag_ReadOnly;
}
auto IsAssoc() const -> bool {
return flags & FsEntryFlag_Assoc;
}
auto IsSame(const FsEntry& e) const {
return root == e.root && type == e.type;
}
};
// roughly 1kib in size per entry
struct FileEntry : FsDirectoryEntry {
std::string extension{}; // if any
std::string internal_name{}; // if any
std::string internal_extension{}; // if any
s64 file_count{-1}; // number of files in a folder, non-recursive
s64 dir_count{-1}; // number folders in a folder, non-recursive
FsTimeStampRaw time_stamp{};
bool checked_extension{}; // did we already search for an ext?
bool checked_internal_extension{}; // did we already search for an ext?
auto IsFile() const -> bool {
return type == FsDirEntryType_File;
}
auto IsDir() const -> bool {
return !IsFile();
}
auto IsHidden() const -> bool {
return name[0] == '.';
}
auto GetName() const -> std::string {
return name;
}
auto GetExtension() const -> std::string {
return extension;
}
auto GetInternalName() const -> std::string {
if (!internal_name.empty()) {
return internal_name;
}
return GetName();
}
auto GetInternalExtension() const -> std::string {
if (!internal_extension.empty()) {
return internal_extension;
}
return GetExtension();
}
};
struct LastFile {
fs::FsPath name{};
s64 index{};
float offset{};
s64 entries_count{};
};
using Callback = std::function<bool(const fs::FsPath& path)>;
struct Menu final : MenuBase {
explicit Menu(const Callback& cb, const std::vector<std::string>& filter = {}, const fs::FsPath& path = {});
~Menu();
auto GetShortTitle() const -> const char* override { return "Picker"; };
void Update(Controller* controller, TouchInfo* touch) override;
void Draw(NVGcontext* vg, Theme* theme) override;
void OnFocusGained() override;
static auto GetNewPath(const fs::FsPath& root_path, const fs::FsPath& file_path) -> fs::FsPath {
return fs::AppendPath(root_path, file_path);
}
private:
auto GetFs() {
return m_fs.get();
}
auto& GetFsEntry() const {
return m_fs_entry;
}
void SetIndex(s64 index);
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
auto GetNewPath(const FileEntry& entry) const -> fs::FsPath {
return GetNewPath(m_path, entry.name);
}
auto GetNewPath(s64 index) const -> fs::FsPath {
return GetNewPath(m_path, GetEntry(index).name);
}
auto GetNewPathCurrent() const -> fs::FsPath {
return GetNewPath(m_index);
}
auto GetEntry(u32 index) -> FileEntry& {
return m_entries[m_entries_current[index]];
}
auto GetEntry(u32 index) const -> const FileEntry& {
return m_entries[m_entries_current[index]];
}
auto GetEntry() -> FileEntry& {
return GetEntry(m_index);
}
auto GetEntry() const -> const FileEntry& {
return GetEntry(m_index);
}
auto IsSd() const -> bool {
return m_fs_entry.type == FsType::Sd;
}
void Sort();
void SortAndFindLastFile(bool scan = false);
void SetIndexFromLastFile(const LastFile& last_file);
void SetFs(const fs::FsPath& new_path, const FsEntry& new_entry);
auto GetNative() -> fs::FsNative* {
return (fs::FsNative*)m_fs.get();
}
void DisplayOptions();
void UpdateSubheading();
void PromptIfShouldExit();
private:
static constexpr inline const char* INI_SECTION = "filepicker";
Callback m_callback;
std::vector<std::string> m_filter;
std::unique_ptr<fs::Fs> m_fs{};
FsEntry m_fs_entry{};
fs::FsPath m_path{};
std::vector<FileEntry> m_entries{};
std::vector<u32> m_entries_index{}; // files not including hidden
std::vector<u32> m_entries_index_hidden{}; // includes hidden files
std::span<u32> m_entries_current{};
std::unique_ptr<List> m_list{};
// this keeps track of the highlighted file before opening a folder
// if the user presses B to go back to the previous dir
// this vector is popped, then, that entry is checked if it still exists
// if it does, the index becomes that file.
std::vector<LastFile> m_previous_highlighted_file{};
s64 m_index{};
ScrollingText m_scroll_name{};
option::OptionLong m_sort{INI_SECTION, "sort", SortType::SortType_Alphabetical, false};
option::OptionLong m_order{INI_SECTION, "order", OrderType::OrderType_Descending, false};
option::OptionBool m_show_hidden{INI_SECTION, "show_hidden", false, false};
option::OptionBool m_folders_first{INI_SECTION, "folders_first", true, false};
option::OptionBool m_hidden_last{INI_SECTION, "hidden_last", false, false};
option::OptionBool m_ignore_read_only{INI_SECTION, "ignore_read_only", false, false};
};
} // namespace sphaira::ui::menu::filepicker

View File

@@ -20,6 +20,10 @@ public:
using Widget::Draw;
virtual void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left);
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
void DrawEntry(NVGcontext* vg, Theme* theme, const std::string& left, const std::string& right, bool use_selected);
void Depends(const DependsCallback& callback, const std::string& depends_info, const DependsClickCallback& depends_click = {}) {
m_depends_callback = callback;
@@ -63,6 +67,7 @@ private:
DependsCallback m_depends_callback{};
DependsClickCallback m_depends_click{};
ScrollingText m_scolling_title{};
ScrollingText m_scolling_value{};
};
template<typename T>
@@ -110,44 +115,67 @@ public:
explicit SidebarEntryArray(const std::string& title, const Items& items, Callback cb, s64 index = 0, const std::string& info = "");
explicit SidebarEntryArray(const std::string& title, const Items& items, Callback cb, const std::string& index, const std::string& info = "");
explicit SidebarEntryArray(const std::string& title, const Items& items, std::string& index, const std::string& info = "");
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
auto OnFocusGained() noexcept -> void override;
auto OnFocusLost() noexcept -> void override;
private:
Items m_items;
ListCallback m_list_callback;
Callback m_callback;
s64 m_index;
s64 m_tick{};
float m_text_yoff{};
};
class SidebarEntryTextInput final : public SidebarEntryBase {
// single text entry.
// the callback is called when the entry is clicked.
// usually, the within the callback the text will be changed, use SetText().
class SidebarEntryTextBase : public SidebarEntryBase {
public:
using Callback = std::function<void(bool&)>;
using Callback = std::function<void(void)>;
public:
explicit SidebarEntryTextInput(const std::string& text, const std::string& guide = {}, const std::string& info = "");
explicit SidebarEntryTextBase(const std::string& title, const std::string& value, const Callback& cb, const std::string& info = "");
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
auto GetText() const {
return m_title;
void SetCallback(const Callback& cb) {
m_callback = cb;
}
void SetText(const std::string& text) {
m_title = text;
auto GetValue() const {
return m_value;
}
void SetValue(const std::string& value) {
m_value = value;
}
private:
std::string m_value;
Callback m_callback;
};
class SidebarEntryTextInput final : public SidebarEntryTextBase {
public:
explicit SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide = {}, const std::string& info = "");
void SetGuide(const std::string& guide) {
m_guide = guide;
}
private:
std::string m_guide;
ScrollingText m_scolling_title{};
};
class SidebarEntryFilePicker final : public SidebarEntryTextBase {
public:
explicit SidebarEntryFilePicker(const std::string& title, const std::string& value, const std::vector<std::string>& filter, const std::string& info = "");
// extension filter.
void SetFilter(const std::vector<std::string>& filter) {
m_filter = filter;
}
private:
std::vector<std::string> m_filter{};
};
class Sidebar final : public Widget {