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:
@@ -36,6 +36,7 @@ add_executable(sphaira
|
|||||||
source/ui/menus/appstore.cpp
|
source/ui/menus/appstore.cpp
|
||||||
source/ui/menus/file_viewer.cpp
|
source/ui/menus/file_viewer.cpp
|
||||||
source/ui/menus/filebrowser.cpp
|
source/ui/menus/filebrowser.cpp
|
||||||
|
source/ui/menus/file_picker.cpp
|
||||||
source/ui/menus/homebrew.cpp
|
source/ui/menus/homebrew.cpp
|
||||||
source/ui/menus/irs_menu.cpp
|
source/ui/menus/irs_menu.cpp
|
||||||
source/ui/menus/main_menu.cpp
|
source/ui/menus/main_menu.cpp
|
||||||
|
|||||||
216
sphaira/include/ui/menus/file_picker.hpp
Normal file
216
sphaira/include/ui/menus/file_picker.hpp
Normal 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
|
||||||
@@ -20,6 +20,10 @@ public:
|
|||||||
|
|
||||||
using Widget::Draw;
|
using Widget::Draw;
|
||||||
virtual void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left);
|
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 = {}) {
|
void Depends(const DependsCallback& callback, const std::string& depends_info, const DependsClickCallback& depends_click = {}) {
|
||||||
m_depends_callback = callback;
|
m_depends_callback = callback;
|
||||||
@@ -63,6 +67,7 @@ private:
|
|||||||
DependsCallback m_depends_callback{};
|
DependsCallback m_depends_callback{};
|
||||||
DependsClickCallback m_depends_click{};
|
DependsClickCallback m_depends_click{};
|
||||||
ScrollingText m_scolling_title{};
|
ScrollingText m_scolling_title{};
|
||||||
|
ScrollingText m_scolling_value{};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename T>
|
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, 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, 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 = "");
|
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;
|
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
|
||||||
auto OnFocusGained() noexcept -> void override;
|
|
||||||
auto OnFocusLost() noexcept -> void override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Items m_items;
|
Items m_items;
|
||||||
ListCallback m_list_callback;
|
ListCallback m_list_callback;
|
||||||
Callback m_callback;
|
Callback m_callback;
|
||||||
s64 m_index;
|
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:
|
public:
|
||||||
using Callback = std::function<void(bool&)>;
|
using Callback = std::function<void(void)>;
|
||||||
|
|
||||||
public:
|
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;
|
void Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) override;
|
||||||
|
|
||||||
auto GetText() const {
|
void SetCallback(const Callback& cb) {
|
||||||
return m_title;
|
m_callback = cb;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetText(const std::string& text) {
|
auto GetValue() const {
|
||||||
m_title = text;
|
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) {
|
void SetGuide(const std::string& guide) {
|
||||||
m_guide = guide;
|
m_guide = guide;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string m_guide;
|
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 {
|
class Sidebar final : public Widget {
|
||||||
|
|||||||
573
sphaira/source/ui/menus/file_picker.cpp
Normal file
573
sphaira/source/ui/menus/file_picker.cpp
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
#include "ui/menus/file_picker.hpp"
|
||||||
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/option_box.hpp"
|
||||||
|
#include "ui/popup_list.hpp"
|
||||||
|
#include "ui/error_box.hpp"
|
||||||
|
|
||||||
|
#include "log.hpp"
|
||||||
|
#include "app.hpp"
|
||||||
|
#include "ui/nvg_util.hpp"
|
||||||
|
#include "fs.hpp"
|
||||||
|
#include "defines.hpp"
|
||||||
|
#include "i18n.hpp"
|
||||||
|
#include "location.hpp"
|
||||||
|
#include "minizip_helper.hpp"
|
||||||
|
|
||||||
|
#include <minIni.h>
|
||||||
|
#include <minizip/unzip.h>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <ctime>
|
||||||
|
#include <span>
|
||||||
|
#include <utility>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
namespace sphaira::ui::menu::filepicker {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr FsEntry FS_ENTRY_DEFAULT{
|
||||||
|
"microSD card", "/", FsType::Sd, FsEntryFlag_Assoc,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr FsEntry FS_ENTRIES[]{
|
||||||
|
FS_ENTRY_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr std::string_view AUDIO_EXTENSIONS[] = {
|
||||||
|
"mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav",
|
||||||
|
"bfsar", "bfstm",
|
||||||
|
};
|
||||||
|
constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
||||||
|
"mp4", "mkv", "m3u", "m3u8", "hls", "vob", "avi", "dv", "flv", "m2ts",
|
||||||
|
"m2v", "m4a", "mov", "mpeg", "mpg", "mts", "swf", "ts", "vob", "wma", "wmv",
|
||||||
|
};
|
||||||
|
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||||
|
"png", "jpg", "jpeg", "bmp", "gif",
|
||||||
|
};
|
||||||
|
constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
||||||
|
"nsp", "xci", "nsz", "xcz",
|
||||||
|
};
|
||||||
|
constexpr std::string_view ZIP_EXTENSIONS[] = {
|
||||||
|
"zip",
|
||||||
|
};
|
||||||
|
|
||||||
|
// case insensitive check
|
||||||
|
auto IsSamePath(std::string_view a, std::string_view b) -> bool {
|
||||||
|
return a.length() == b.length() && !strncasecmp(a.data(), b.data(), a.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IsExtension(std::string_view ext, std::span<const std::string_view> list) -> bool {
|
||||||
|
for (auto e : list) {
|
||||||
|
if (e.length() == ext.length() && !strncasecmp(ext.data(), e.data(), ext.length())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto IsExtension(std::string_view ext1, std::string_view ext2) -> bool {
|
||||||
|
return ext1.length() == ext2.length() && !strncasecmp(ext1.data(), ext2.data(), ext1.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Menu::SetIndex(s64 index) {
|
||||||
|
m_index = index;
|
||||||
|
if (!m_index) {
|
||||||
|
m_list->SetYoff();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSd() && !m_entries_current.empty() && !GetEntry().checked_internal_extension && IsSamePath(GetEntry().extension, "zip")) {
|
||||||
|
GetEntry().checked_internal_extension = true;
|
||||||
|
|
||||||
|
if (auto zfile = unzOpen64(GetNewPathCurrent())) {
|
||||||
|
ON_SCOPE_EXIT(unzClose(zfile));
|
||||||
|
|
||||||
|
// only check first entry (i think RA does the same)
|
||||||
|
fs::FsPath filename_inzip{};
|
||||||
|
unz_file_info64 file_info{};
|
||||||
|
if (UNZ_OK == unzOpenCurrentFile(zfile)) {
|
||||||
|
ON_SCOPE_EXIT(unzCloseCurrentFile(zfile));
|
||||||
|
if (UNZ_OK == unzGetCurrentFileInfo64(zfile, &file_info, filename_inzip, sizeof(filename_inzip), NULL, 0, NULL, 0)) {
|
||||||
|
if (auto ext = std::strrchr(filename_inzip, '.')) {
|
||||||
|
GetEntry().internal_name = filename_inzip.toString();
|
||||||
|
GetEntry().internal_extension = ext+1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSubheading();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||||
|
App::SetBoostMode(true);
|
||||||
|
ON_SCOPE_EXIT(App::SetBoostMode(false));
|
||||||
|
|
||||||
|
log_write("new scan path: %s\n", new_path.s);
|
||||||
|
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||||
|
const LastFile f(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
|
||||||
|
m_previous_highlighted_file.emplace_back(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_path = new_path;
|
||||||
|
m_entries.clear();
|
||||||
|
m_index = 0;
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
SetTitleSubHeading(m_path);
|
||||||
|
|
||||||
|
fs::Dir d;
|
||||||
|
R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d));
|
||||||
|
|
||||||
|
// we won't run out of memory here (tm)
|
||||||
|
std::vector<FsDirectoryEntry> dir_entries;
|
||||||
|
R_TRY(d.ReadAll(dir_entries));
|
||||||
|
|
||||||
|
const auto count = dir_entries.size();
|
||||||
|
m_entries.reserve(count);
|
||||||
|
|
||||||
|
m_entries_index.clear();
|
||||||
|
m_entries_index_hidden.clear();
|
||||||
|
|
||||||
|
m_entries_index.reserve(count);
|
||||||
|
m_entries_index_hidden.reserve(count);
|
||||||
|
|
||||||
|
u32 i = 0;
|
||||||
|
for (const auto& e : dir_entries) {
|
||||||
|
m_entries_index_hidden.emplace_back(i);
|
||||||
|
|
||||||
|
bool hidden = false;
|
||||||
|
// check if we have a filter.
|
||||||
|
if (e.type == FsDirEntryType_File && !m_filter.empty()) {
|
||||||
|
hidden = true;
|
||||||
|
if (const auto ext = std::strrchr(e.name, '.')) {
|
||||||
|
for (const auto& filter : m_filter) {
|
||||||
|
if (IsExtension(ext, filter)) {
|
||||||
|
hidden = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hidden) {
|
||||||
|
m_entries_index.emplace_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_entries.emplace_back(e);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sort();
|
||||||
|
SetIndex(0);
|
||||||
|
|
||||||
|
// find previous entry
|
||||||
|
if (is_walk_up && !m_previous_highlighted_file.empty()) {
|
||||||
|
ON_SCOPE_EXIT(m_previous_highlighted_file.pop_back());
|
||||||
|
SetIndexFromLastFile(m_previous_highlighted_file.back());
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write("finished scan\n");
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Sort() {
|
||||||
|
// returns true if lhs should be before rhs
|
||||||
|
const auto sort = m_sort.Get();
|
||||||
|
const auto order = m_order.Get();
|
||||||
|
const auto folders_first = m_folders_first.Get();
|
||||||
|
const auto hidden_last = m_hidden_last.Get();
|
||||||
|
|
||||||
|
const auto sorter = [this, sort, order, folders_first, hidden_last](u32 _lhs, u32 _rhs) -> bool {
|
||||||
|
const auto& lhs = m_entries[_lhs];
|
||||||
|
const auto& rhs = m_entries[_rhs];
|
||||||
|
|
||||||
|
if (hidden_last) {
|
||||||
|
if (lhs.IsHidden() && !rhs.IsHidden()) {
|
||||||
|
return false;
|
||||||
|
} else if (!lhs.IsHidden() && rhs.IsHidden()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folders_first) {
|
||||||
|
if (lhs.type == FsDirEntryType_Dir && !(rhs.type == FsDirEntryType_Dir)) { // left is folder
|
||||||
|
return true;
|
||||||
|
} else if (!(lhs.type == FsDirEntryType_Dir) && rhs.type == FsDirEntryType_Dir) { // right is folder
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sort) {
|
||||||
|
case SortType_Size: {
|
||||||
|
if (lhs.file_size == rhs.file_size) {
|
||||||
|
return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0;
|
||||||
|
} else if (order == OrderType_Descending) {
|
||||||
|
return lhs.file_size > rhs.file_size;
|
||||||
|
} else {
|
||||||
|
return lhs.file_size < rhs.file_size;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case SortType_Alphabetical: {
|
||||||
|
if (order == OrderType_Descending) {
|
||||||
|
return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) < 0;
|
||||||
|
} else {
|
||||||
|
return strncasecmp(lhs.name, rhs.name, sizeof(lhs.name)) > 0;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unreachable();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (m_show_hidden.Get()) {
|
||||||
|
m_entries_current = m_entries_index_hidden;
|
||||||
|
} else {
|
||||||
|
m_entries_current = m_entries_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(m_entries_current.begin(), m_entries_current.end(), sorter);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SortAndFindLastFile(bool scan) {
|
||||||
|
std::optional<LastFile> last_file;
|
||||||
|
if (!m_path.empty() && !m_entries_current.empty()) {
|
||||||
|
last_file = LastFile(GetEntry().name, m_index, m_list->GetYoff(), m_entries_current.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scan) {
|
||||||
|
Scan(m_path);
|
||||||
|
} else {
|
||||||
|
Sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_file.has_value()) {
|
||||||
|
SetIndexFromLastFile(*last_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SetIndexFromLastFile(const LastFile& last_file) {
|
||||||
|
SetIndex(0);
|
||||||
|
|
||||||
|
s64 index = -1;
|
||||||
|
for (u64 i = 0; i < m_entries_current.size(); i++) {
|
||||||
|
if (last_file.name == GetEntry(i).name) {
|
||||||
|
index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
if (index == last_file.index && m_entries_current.size() == last_file.entries_count) {
|
||||||
|
m_list->SetYoff(last_file.offset);
|
||||||
|
log_write("index is the same as last time\n");
|
||||||
|
} else {
|
||||||
|
// file position changed!
|
||||||
|
log_write("file position changed\n");
|
||||||
|
// guesstimate where the position is
|
||||||
|
if (index >= 8) {
|
||||||
|
m_list->SetYoff(((index - 8) + 1) * m_list->GetMaxY());
|
||||||
|
} else {
|
||||||
|
m_list->SetYoff(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SetIndex(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::SetFs(const fs::FsPath& new_path, const FsEntry& new_entry) {
|
||||||
|
if (m_fs && m_fs_entry.root == new_entry.root && m_fs_entry.type == new_entry.type) {
|
||||||
|
log_write("same fs, ignoring\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// m_fs.reset();
|
||||||
|
m_path = new_path;
|
||||||
|
m_entries.clear();
|
||||||
|
m_entries_index.clear();
|
||||||
|
m_entries_index_hidden.clear();
|
||||||
|
m_entries_current = {};
|
||||||
|
m_previous_highlighted_file.clear();
|
||||||
|
m_fs_entry = new_entry;
|
||||||
|
|
||||||
|
switch (new_entry.type) {
|
||||||
|
case FsType::Sd:
|
||||||
|
m_fs = std::make_unique<fs::FsNativeSd>(m_ignore_read_only.Get());
|
||||||
|
break;
|
||||||
|
case FsType::ImageNand:
|
||||||
|
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Nand);
|
||||||
|
break;
|
||||||
|
case FsType::ImageSd:
|
||||||
|
m_fs = std::make_unique<fs::FsNativeImage>(FsImageDirectoryId_Sd);
|
||||||
|
break;
|
||||||
|
case FsType::Stdio:
|
||||||
|
m_fs = std::make_unique<fs::FsStdio>(true, new_entry.root);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HasFocus()) {
|
||||||
|
if (m_path.empty()) {
|
||||||
|
Scan(m_fs->Root());
|
||||||
|
} else {
|
||||||
|
Scan(m_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::DisplayOptions() {
|
||||||
|
auto options = std::make_unique<Sidebar>("File Options"_i18n, Sidebar::Side::RIGHT);
|
||||||
|
ON_SCOPE_EXIT(App::Push(std::move(options)));
|
||||||
|
|
||||||
|
SidebarEntryArray::Items mount_items;
|
||||||
|
std::vector<FsEntry> fs_entries;
|
||||||
|
|
||||||
|
const auto stdio_locations = location::GetStdio(false);
|
||||||
|
for (const auto& e: stdio_locations) {
|
||||||
|
u32 flags{};
|
||||||
|
if (e.write_protect) {
|
||||||
|
flags |= FsEntryFlag_ReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs_entries.emplace_back(e.name, e.mount, FsType::Stdio, flags);
|
||||||
|
mount_items.push_back(e.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& e: FS_ENTRIES) {
|
||||||
|
fs_entries.emplace_back(e);
|
||||||
|
mount_items.push_back(i18n::get(e.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
options->Add<SidebarEntryArray>("Mount"_i18n, mount_items, [this, fs_entries](s64& index_out){
|
||||||
|
App::PopToMenu();
|
||||||
|
SetFs(fs_entries[index_out].root, fs_entries[index_out]);
|
||||||
|
}, i18n::get(m_fs_entry.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::Menu(const Callback& cb, const std::vector<std::string>& filter, const fs::FsPath& path)
|
||||||
|
: MenuBase{"FilePicker"_i18n, MenuFlag_None}
|
||||||
|
, m_callback{cb}
|
||||||
|
, m_filter{filter} {
|
||||||
|
FsEntry entry = FS_ENTRY_DEFAULT;
|
||||||
|
|
||||||
|
if (!IsTab()) {
|
||||||
|
SetAction(Button::SELECT, Action{"Close"_i18n, [this](){
|
||||||
|
PromptIfShouldExit();
|
||||||
|
}});
|
||||||
|
}
|
||||||
|
|
||||||
|
this->SetActions(
|
||||||
|
std::make_pair(Button::A, Action{"Open"_i18n, [this](){
|
||||||
|
if (m_entries_current.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& entry = GetEntry();
|
||||||
|
|
||||||
|
if (entry.type == FsDirEntryType_Dir) {
|
||||||
|
// todo: add support for folder picker.
|
||||||
|
Scan(GetNewPathCurrent());
|
||||||
|
} else {
|
||||||
|
if (m_callback(GetNewPathCurrent())) {
|
||||||
|
SetPop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
|
||||||
|
std::make_pair(Button::B, Action{"Back"_i18n, [this](){
|
||||||
|
if (!IsTab() && App::GetApp()->m_controller.GotHeld(Button::R2)) {
|
||||||
|
PromptIfShouldExit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view view{m_path};
|
||||||
|
if (view != m_fs->Root()) {
|
||||||
|
const auto end = view.find_last_of('/');
|
||||||
|
assert(end != view.npos);
|
||||||
|
|
||||||
|
if (end == 0) {
|
||||||
|
Scan(m_fs->Root(), true);
|
||||||
|
} else {
|
||||||
|
Scan(view.substr(0, end), true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!IsTab()) {
|
||||||
|
PromptIfShouldExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}),
|
||||||
|
|
||||||
|
std::make_pair(Button::X, Action{"Options"_i18n, [this](){
|
||||||
|
DisplayOptions();
|
||||||
|
}})
|
||||||
|
);
|
||||||
|
|
||||||
|
const Vec4 v{75, GetY() + 1.f + 42.f, 1220.f-45.f*2, 60};
|
||||||
|
m_list = std::make_unique<List>(1, 8, m_pos, v);
|
||||||
|
|
||||||
|
auto buf = path;
|
||||||
|
if (path.empty()) {
|
||||||
|
ini_gets(INI_SECTION, "last_path", entry.root, buf, sizeof(buf), App::CONFIG_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetFs(buf, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu::~Menu() {
|
||||||
|
// don't store mount points for non-sd card paths.
|
||||||
|
if (IsSd()) {
|
||||||
|
ini_puts(INI_SECTION, "last_path", m_path, App::CONFIG_PATH);
|
||||||
|
|
||||||
|
// save last selected file.
|
||||||
|
if (!m_entries.empty()) {
|
||||||
|
ini_puts(INI_SECTION, "last_file", GetEntry().name, App::CONFIG_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||||
|
MenuBase::Update(controller, touch);
|
||||||
|
|
||||||
|
m_list->OnUpdate(controller, touch, m_index, m_entries_current.size(), [this](bool touch, auto i) {
|
||||||
|
if (touch && m_index == i) {
|
||||||
|
FireAction(Button::A);
|
||||||
|
} else {
|
||||||
|
App::PlaySoundEffect(SoundEffect_Focus);
|
||||||
|
SetIndex(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||||
|
MenuBase::Draw(vg, theme);
|
||||||
|
|
||||||
|
const auto& text_col = theme->GetColour(ThemeEntryID_TEXT);
|
||||||
|
|
||||||
|
if (m_entries_current.empty()) {
|
||||||
|
gfx::drawTextArgs(vg, GetX() + GetW() / 2.f, GetY() + GetH() / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Empty..."_i18n.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr float text_xoffset{15.f};
|
||||||
|
|
||||||
|
m_list->Draw(vg, theme, m_entries_current.size(), [this, text_col](auto* vg, auto* theme, auto v, auto i) {
|
||||||
|
const auto& [x, y, w, h] = v;
|
||||||
|
auto& e = GetEntry(i);
|
||||||
|
|
||||||
|
if (e.IsDir()) {
|
||||||
|
// NOTE: make this native only if hdd dir scan is too slow.
|
||||||
|
// if (m_fs->IsNative() && e.file_count == -1 && e.dir_count == -1) {
|
||||||
|
if (e.file_count == -1 && e.dir_count == -1) {
|
||||||
|
m_fs->DirGetEntryCount(GetNewPath(e), &e.file_count, &e.dir_count);
|
||||||
|
}
|
||||||
|
} else if (!e.checked_extension) {
|
||||||
|
e.checked_extension = true;
|
||||||
|
if (auto ext = std::strrchr(e.name, '.')) {
|
||||||
|
e.extension = ext+1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto text_id = ThemeEntryID_TEXT;
|
||||||
|
const auto selected = m_index == i;
|
||||||
|
if (selected) {
|
||||||
|
text_id = ThemeEntryID_TEXT_SELECTED;
|
||||||
|
gfx::drawRectOutline(vg, theme, 4.f, v);
|
||||||
|
} else {
|
||||||
|
if (i != m_entries_current.size() - 1) {
|
||||||
|
gfx::drawRect(vg, Vec4{x, y + h, w, 1.f}, theme->GetColour(ThemeEntryID_LINE_SEPARATOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.IsDir()) {
|
||||||
|
DrawElement(x + text_xoffset, y + 5, 50, 50, ThemeEntryID_ICON_FOLDER);
|
||||||
|
} else {
|
||||||
|
auto icon = ThemeEntryID_ICON_FILE;
|
||||||
|
const auto ext = e.GetExtension();
|
||||||
|
if (IsExtension(ext, AUDIO_EXTENSIONS)) {
|
||||||
|
icon = ThemeEntryID_ICON_AUDIO;
|
||||||
|
} else if (IsExtension(ext, VIDEO_EXTENSIONS)) {
|
||||||
|
icon = ThemeEntryID_ICON_VIDEO;
|
||||||
|
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
|
||||||
|
icon = ThemeEntryID_ICON_IMAGE;
|
||||||
|
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
|
||||||
|
// todo: maybe replace this icon with something else?
|
||||||
|
icon = ThemeEntryID_ICON_NRO;
|
||||||
|
} else if (IsExtension(ext, ZIP_EXTENSIONS)) {
|
||||||
|
icon = ThemeEntryID_ICON_ZIP;
|
||||||
|
} else if (IsExtension(ext, "nro")) {
|
||||||
|
icon = ThemeEntryID_ICON_NRO;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawElement(x + text_xoffset, y + 5, 50, 50, icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_scroll_name.Draw(vg, selected, x + text_xoffset+65, y + (h / 2.f), w-(75+text_xoffset+65+50), 20, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), e.name);
|
||||||
|
|
||||||
|
// NOTE: make this native only if i disable dir scan from above.
|
||||||
|
if (e.IsDir()) {
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%zd files"_i18n.c_str(), e.file_count);
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%zd dirs"_i18n.c_str(), e.dir_count);
|
||||||
|
} else if (e.IsFile()) {
|
||||||
|
if (!e.time_stamp.is_valid) {
|
||||||
|
const auto path = GetNewPath(e);
|
||||||
|
if (m_fs->IsNative()) {
|
||||||
|
m_fs->GetFileTimeStampRaw(path, &e.time_stamp);
|
||||||
|
} else {
|
||||||
|
m_fs->FileGetSizeAndTimestamp(path, &e.time_stamp, &e.file_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto t = (time_t)(e.time_stamp.modified);
|
||||||
|
struct tm tm{};
|
||||||
|
localtime_r(&t, &tm);
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(text_id), "%02u/%02u/%u", tm.tm_mday, tm.tm_mon + 1, tm.tm_year + 1900);
|
||||||
|
if ((double)e.file_size / 1024.0 / 1024.0 <= 0.009) {
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f KiB", (double)e.file_size / 1024.0);
|
||||||
|
} else {
|
||||||
|
gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(text_id), "%.2f MiB", (double)e.file_size / 1024.0 / 1024.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::OnFocusGained() {
|
||||||
|
MenuBase::OnFocusGained();
|
||||||
|
|
||||||
|
if (m_entries.empty()) {
|
||||||
|
if (m_path.empty()) {
|
||||||
|
Scan(m_fs->Root());
|
||||||
|
} else {
|
||||||
|
Scan(m_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSd() && !m_entries.empty()) {
|
||||||
|
LastFile last_file{};
|
||||||
|
if (ini_gets(INI_SECTION, "last_file", "", last_file.name, sizeof(last_file.name), App::CONFIG_PATH)) {
|
||||||
|
SetIndexFromLastFile(last_file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::UpdateSubheading() {
|
||||||
|
const auto index = m_entries_current.empty() ? 0 : m_index + 1;
|
||||||
|
this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries_current.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Menu::PromptIfShouldExit() {
|
||||||
|
if (IsTab()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
App::Push<ui::OptionBox>(
|
||||||
|
"Close File Picker?"_i18n,
|
||||||
|
"No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||||
|
if (op_index && *op_index) {
|
||||||
|
SetPop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace sphaira::ui::menu::filepicker
|
||||||
@@ -489,7 +489,7 @@ void Menu::DisplayOptions() {
|
|||||||
}, GetEntry().image
|
}, GetEntry().image
|
||||||
);
|
);
|
||||||
}, "Perminately delete the selected homebrew.\n\n"
|
}, "Perminately delete the selected homebrew.\n\n"
|
||||||
"Files and folders created by the homebrew will still remain. "
|
"Files and folders created by the homebrew will still remain. "
|
||||||
"Use the FileBrowser to delete them."_i18n);
|
"Use the FileBrowser to delete them."_i18n);
|
||||||
|
|
||||||
auto forwarder_entry = options->Add<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
|
auto forwarder_entry = options->Add<SidebarEntryCallback>("Install Forwarder"_i18n, [this](){
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "ui/sidebar.hpp"
|
#include "ui/sidebar.hpp"
|
||||||
|
#include "ui/menus/file_picker.hpp"
|
||||||
#include "app.hpp"
|
#include "app.hpp"
|
||||||
#include "ui/popup_list.hpp"
|
#include "ui/popup_list.hpp"
|
||||||
#include "ui/nvg_util.hpp"
|
#include "ui/nvg_util.hpp"
|
||||||
@@ -9,14 +10,6 @@
|
|||||||
namespace sphaira::ui {
|
namespace sphaira::ui {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
auto GetTextScrollSpeed() -> float {
|
|
||||||
switch (App::GetTextScrollSpeed()) {
|
|
||||||
case 0: return 0.5;
|
|
||||||
default: case 1: return 1.0;
|
|
||||||
case 2: return 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
auto DistanceBetweenY(Vec4 va, Vec4 vb) -> Vec4 {
|
||||||
return Vec4{
|
return Vec4{
|
||||||
va.x, va.y,
|
va.x, va.y,
|
||||||
@@ -79,6 +72,38 @@ void SidebarEntryBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto SidebarEntryBase::OnFocusGained() noexcept -> void {
|
||||||
|
Widget::OnFocusGained();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SidebarEntryBase::OnFocusLost() noexcept -> void {
|
||||||
|
Widget::OnFocusLost();
|
||||||
|
m_scolling_value.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SidebarEntryBase::DrawEntry(NVGcontext* vg, Theme* theme, const std::string& left, const std::string& right, bool use_selected) {
|
||||||
|
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
||||||
|
|
||||||
|
// scrolling text
|
||||||
|
float bounds[4];
|
||||||
|
nvgFontSize(vg, 20);
|
||||||
|
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
nvgTextBounds(vg, 0, 0, left.c_str(), nullptr, bounds);
|
||||||
|
const float start_x = bounds[2] + 50;
|
||||||
|
const float max_off = m_pos.w - start_x - 15.f;
|
||||||
|
|
||||||
|
nvgTextBounds(vg, 0, 0, right.c_str(), nullptr, bounds);
|
||||||
|
|
||||||
|
const Vec2 key_text_pos{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)};
|
||||||
|
gfx::drawText(vg, key_text_pos, 20.f, theme->GetColour(colour_id), left.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
|
|
||||||
|
const auto value_id = use_selected ? ThemeEntryID_TEXT_SELECTED : ThemeEntryID_TEXT;
|
||||||
|
const float xpos = m_pos.x + m_pos.w - 15.f - std::min(max_off, bounds[2]);
|
||||||
|
const float ypos = m_pos.y + (m_pos.h / 2.f);
|
||||||
|
|
||||||
|
m_scolling_value.Draw(vg, HasFocus(), xpos, ypos, max_off, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(value_id), right);
|
||||||
|
}
|
||||||
|
|
||||||
SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, Callback cb, const std::string& info, const std::string& true_str, const std::string& false_str)
|
SidebarEntryBool::SidebarEntryBool(const std::string& title, bool option, Callback cb, const std::string& info, const std::string& true_str, const std::string& false_str)
|
||||||
: SidebarEntryBase{title, info}
|
: SidebarEntryBase{title, info}
|
||||||
, m_option{option}
|
, m_option{option}
|
||||||
@@ -126,20 +151,7 @@ SidebarEntryBool::SidebarEntryBool(const std::string& title, option::OptionBool&
|
|||||||
|
|
||||||
void SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
void SidebarEntryBool::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
||||||
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
||||||
|
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_option ? m_true_str : m_false_str, m_option);
|
||||||
// if (HasFocus()) {
|
|
||||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
// } else {
|
|
||||||
// }
|
|
||||||
|
|
||||||
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(colour_id), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
|
|
||||||
if (m_option == true) {
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_true_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
|
||||||
} else { // text info
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + m_pos.w - 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT), m_false_str.c_str(), NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarEntryCallback::SidebarEntryCallback(const std::string& title, Callback cb, bool pop_on_click, const std::string& info)
|
SidebarEntryCallback::SidebarEntryCallback(const std::string& title, Callback cb, bool pop_on_click, const std::string& info)
|
||||||
@@ -167,11 +179,7 @@ void SidebarEntryCallback::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_p
|
|||||||
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
||||||
|
|
||||||
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
||||||
// if (HasFocus()) {
|
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(colour_id), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
||||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
// } else {
|
|
||||||
gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(colour_id), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& items, std::string& index, const std::string& info)
|
SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& items, std::string& index, const std::string& info)
|
||||||
@@ -227,82 +235,53 @@ SidebarEntryArray::SidebarEntryArray(const std::string& title, const Items& item
|
|||||||
|
|
||||||
void SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
void SidebarEntryArray::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
||||||
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
||||||
|
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_items[m_index], true);
|
||||||
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
|
||||||
const auto& text_entry = m_items[m_index];
|
|
||||||
|
|
||||||
// scrolling text
|
|
||||||
// todo: move below in a flexible class and use it for all text drawing.
|
|
||||||
float bounds[4];
|
|
||||||
nvgFontSize(vg, 20);
|
|
||||||
nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
nvgTextBounds(vg, 0, 0, m_title.c_str(), nullptr, bounds);
|
|
||||||
const float start_x = bounds[2] + 50;
|
|
||||||
const float max_off = m_pos.w - start_x - 15.f;
|
|
||||||
|
|
||||||
auto value_str = m_items[m_index];
|
|
||||||
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
|
||||||
|
|
||||||
if (HasFocus()) {
|
|
||||||
const auto scroll_amount = GetTextScrollSpeed();
|
|
||||||
if (bounds[2] > max_off) {
|
|
||||||
value_str += " ";
|
|
||||||
nvgTextBounds(vg, 0, 0, value_str.c_str(), nullptr, bounds);
|
|
||||||
|
|
||||||
if (!m_text_yoff) {
|
|
||||||
m_tick++;
|
|
||||||
if (m_tick >= 90) {
|
|
||||||
m_tick = 0;
|
|
||||||
m_text_yoff += scroll_amount;
|
|
||||||
}
|
|
||||||
} else if (bounds[2] > m_text_yoff) {
|
|
||||||
m_text_yoff += std::min(scroll_amount, bounds[2] - m_text_yoff);
|
|
||||||
} else {
|
|
||||||
m_text_yoff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_str += text_entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Vec2 key_text_pos{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)};
|
|
||||||
gfx::drawText(vg, key_text_pos, 20.f, theme->GetColour(colour_id), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
|
|
||||||
nvgSave(vg);
|
|
||||||
const float xpos = m_pos.x + m_pos.w - 15.f - std::min(max_off, bounds[2]);
|
|
||||||
nvgIntersectScissor(vg, xpos, GetY(), max_off, GetH());
|
|
||||||
const Vec2 value_text_pos{xpos - m_text_yoff, m_pos.y + (m_pos.h / 2.f)};
|
|
||||||
gfx::drawText(vg, value_text_pos, 20.f, theme->GetColour(ThemeEntryID_TEXT_SELECTED), value_str.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
|
||||||
nvgRestore(vg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto SidebarEntryArray::OnFocusGained() noexcept -> void {
|
SidebarEntryTextBase::SidebarEntryTextBase(const std::string& title, const std::string& value, const Callback& cb, const std::string& info)
|
||||||
Widget::OnFocusGained();
|
: SidebarEntryBase{title, info}
|
||||||
}
|
, m_value{value}
|
||||||
|
, m_callback{cb} {
|
||||||
auto SidebarEntryArray::OnFocusLost() noexcept -> void {
|
|
||||||
Widget::OnFocusLost();
|
|
||||||
m_text_yoff = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& text, const std::string& guide, const std::string& info)
|
|
||||||
: SidebarEntryBase{text, info}, m_guide{guide} {
|
|
||||||
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
SetAction(Button::A, Action{"OK"_i18n, [this](){
|
||||||
std::string out;
|
if (m_callback) {
|
||||||
if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), m_title.c_str()))) {
|
m_callback();
|
||||||
m_title = out;
|
|
||||||
}
|
}
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SidebarEntryTextInput::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
void SidebarEntryTextBase::Draw(NVGcontext* vg, Theme* theme, const Vec4& root_pos, bool left) {
|
||||||
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
SidebarEntryBase::Draw(vg, theme, root_pos, left);
|
||||||
|
SidebarEntryBase::DrawEntry(vg, theme, m_title, m_value, true);
|
||||||
|
|
||||||
const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
// const auto colour_id = IsEnabled() ? ThemeEntryID_TEXT : ThemeEntryID_TEXT_INFO;
|
||||||
const auto max_w = m_pos.w - 15.f * 2;
|
// const auto max_w = m_pos.w - 15.f * 2;
|
||||||
|
|
||||||
m_scolling_title.Draw(vg, HasFocus(), m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f), max_w, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour_id), m_title);
|
// m_scolling_title.Draw(vg, HasFocus(), m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f), max_w, 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(colour_id), m_title);
|
||||||
// gfx::drawText(vg, Vec2{m_pos.x + 15.f, m_pos.y + (m_pos.h / 2.f)}, 20.f, theme->GetColour(colour_id), m_title.c_str(), NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE);
|
}
|
||||||
|
|
||||||
|
SidebarEntryTextInput::SidebarEntryTextInput(const std::string& title, const std::string& value, const std::string& guide, const std::string& info)
|
||||||
|
: SidebarEntryTextBase{title, value, {}, info}, m_guide{guide} {
|
||||||
|
|
||||||
|
SetCallback([this](){
|
||||||
|
std::string out;
|
||||||
|
if (R_SUCCEEDED(swkbd::ShowText(out, m_guide.c_str(), GetValue().c_str()))) {
|
||||||
|
SetValue(out);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SidebarEntryFilePicker::SidebarEntryFilePicker(const std::string& title, const std::string& value, const std::vector<std::string>& filter, const std::string& info)
|
||||||
|
: SidebarEntryTextBase{title, value, {}, info}, m_filter{filter} {
|
||||||
|
|
||||||
|
SetCallback([this](){
|
||||||
|
App::Push<menu::filepicker::Menu>(
|
||||||
|
[this](const fs::FsPath& path) {
|
||||||
|
SetValue(path);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
m_filter
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Sidebar::Sidebar(const std::string& title, Side side, Items&& items)
|
Sidebar::Sidebar(const std::string& title, Side side, Items&& items)
|
||||||
|
|||||||
Reference in New Issue
Block a user