Choose the amiibo folder with the host's folder dialog

The Amiibo tab's folder field took no typing: the menu never passes typed characters to Dear ImGui, so "Open" only
re-read "./amiibo". A missing folder said so at the bottom of the right pane, below its scroll.

"Open" now shows the shell's folder dialog on Windows (IFileOpenDialog on a thread of its own, owned by the game
window) and SDL_ShowOpenFolderDialog on the SDL window thread elsewhere; the tab shows the folder as text and names a
missing one inside the empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-02 17:36:09 +03:00
1 parent 275a752bec
commit 4e90c61c52
7 files changed
+188 -12

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
typedef void (*FolderPickDone)(const char *path, void *ctx);
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
// The caller keeps one dialog open at a time.
void folder_pick(const char *start, FolderPickDone done, void *ctx);
#ifdef __cplusplus
}
#endif
+42
View File
@@ -0,0 +1,42 @@
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
// thread, which runs SDL's event loop that delivers the portal's answer.
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "folder_pick.h"
#include "host.h"
#include "input_sdl.h"
typedef struct {
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
FolderPickDone done;
void *ctx;
} PickReq;
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
(void)filter;
PickReq *r = userdata;
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
r->done(list && list[0] ? list[0] : NULL, r->ctx);
free(r);
}
static void show(void *arg) {
PickReq *r = arg;
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
}
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = calloc(1, sizeof *r);
if (!r) {
done(NULL, ctx);
return;
}
r->done = done;
r->ctx = ctx;
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
gfx_window_sdl_run(show, r);
}
+84
View File
@@ -0,0 +1,84 @@
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
// game window, on a thread of its own so the present thread that draws the menu keeps running.
#include <windows.h>
#include <shobjidl.h>
#include <stdlib.h>
#include <string>
extern "C" {
#include "folder_pick.h"
#include "host.h"
#include "input_win.h"
#include "platform/platform.h"
}
struct PickReq {
std::string start;
FolderPickDone done;
void *ctx;
};
static std::wstring wide(const std::string &utf8) {
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
std::wstring w(n > 0 ? n : 1, L'\0');
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
w.resize(wcslen(w.c_str()));
return w;
}
static std::string utf8(const wchar_t *w) {
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
std::string s(n > 0 ? n : 1, '\0');
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
s.resize(strlen(s.c_str()));
return s;
}
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
static void set_start(IFileOpenDialog *d, const std::string &start) {
wchar_t full[MAX_PATH * 2];
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
IShellItem *item = NULL;
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
d->SetFolder(item);
item->Release();
}
}
static void pick_main(void *arg) {
PickReq *r = (PickReq *)arg;
std::string chosen;
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
IFileOpenDialog *d = NULL;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
if (SUCCEEDED(hr)) {
FILEOPENDIALOGOPTIONS opt = 0;
d->GetOptions(&opt);
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
set_start(d, r->start);
hr = d->Show(gfx_window_handle());
IShellItem *item = NULL;
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
PWSTR path = NULL;
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
chosen = utf8(path);
CoTaskMemFree(path);
}
item->Release();
}
d->Release();
}
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
if (SUCCEEDED(co)) CoUninitialize();
delete r;
}
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = new PickReq{start ? start : "", done, ctx};
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
rt_log("folder_pick: cannot start the dialog thread");
done(NULL, ctx);
delete r;
}
}
+2
View File
@@ -64,6 +64,8 @@ double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, _
SDL_Window *gfx_window_sdl(void) { return g_win; }
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
// ---- settings: size and fullscreen
static char g_applied_size[32];
static int g_applied_fs;
+1
View File
@@ -11,6 +11,7 @@ extern "C" {
// gfx_window_sdl.c
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
// input_sdl.c
void input_sdl_window(SDL_Window *w); // the game window was created
+39 -10
View File
@@ -17,6 +17,7 @@
extern "C" {
#include "amiibo.h"
#include "cfg.h"
#include "folder_pick.h"
#include "platform/platform.h"
#include "ui_text.h"
}
@@ -30,7 +31,6 @@ struct FileEntry {
static const char kDefaultAmiiboDir[] = "./amiibo";
static std::string g_current_dir = kDefaultAmiiboDir;
static char g_dir_input[512] = "./amiibo";
static std::vector<FileEntry> g_entries;
static int g_selected_entry = -1;
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
@@ -61,10 +61,24 @@ static bool iequals(std::string_view a, std::string_view b) {
static void set_current_dir(const std::string &path) {
g_current_dir = path;
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
cfg_set("TOTK_AMIIBO_DIR", g_current_dir.c_str());
}
// The host's folder dialog (folder_pick.h) answers on its own thread; the page takes the answer when it draws.
static PlatMutex g_pick_lock = PLAT_MUTEX_INIT;
static bool g_pick_open;
static bool g_pick_ready;
static std::string g_pick_path;
static void on_folder_picked(const char *path, void *ctx) {
(void)ctx;
plat_mutex_lock(&g_pick_lock);
g_pick_ready = path != NULL;
if (path) g_pick_path = path;
g_pick_open = false;
plat_mutex_unlock(&g_pick_lock);
}
static void refresh_dir(void) {
g_entries.clear();
g_selected_entry = -1;
@@ -122,11 +136,21 @@ void tab_amiibo(float scale) {
} else {
g_current_dir = kDefaultAmiiboDir;
}
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
refresh_dir();
s_inited = true;
}
plat_mutex_lock(&g_pick_lock);
bool picking = g_pick_open;
std::string picked;
if (g_pick_ready) picked.swap(g_pick_path);
g_pick_ready = false;
plat_mutex_unlock(&g_pick_lock);
if (!picked.empty()) {
set_current_dir(picked);
refresh_dir();
}
// Two panes: Left (Directory Browser & Tag List), Right (Amiibo Tag Details & Controls)
const float content_h = ImGui::GetContentRegionAvail().y - opt_bottom(); // Continue and Quit stay visible
const float left_w = ImGui::GetContentRegionAvail().x * 0.58f;
@@ -136,17 +160,17 @@ void tab_amiibo(float scale) {
ImGui::BeginChild("##amiibo_explorer", ImVec2(left_w, content_h), true);
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_DIR));
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::InputText("##amiibo_dir_input", g_dir_input, sizeof(g_dir_input), ImGuiInputTextFlags_EnterReturnsTrue)) {
set_current_dir(g_dir_input);
refresh_dir();
}
ImGui::TextWrapped("%s", g_current_dir.c_str());
ImGui::Spacing();
ImGui::BeginDisabled(picking);
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_OPEN))) {
set_current_dir(g_dir_input);
refresh_dir();
plat_mutex_lock(&g_pick_lock);
g_pick_open = true;
plat_mutex_unlock(&g_pick_lock);
folder_pick(g_current_dir.c_str(), on_folder_picked, NULL);
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_DEFAULT_DIR))) {
set_current_dir(kDefaultAmiiboDir);
@@ -161,6 +185,11 @@ void tab_amiibo(float scale) {
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_PICK));
ImGui::BeginChild("##file_list", ImVec2(0, 0), true);
if (g_status_id == UI_MENU_AMIIBO_NO_DIR) {
char missing[512];
snprintf(missing, sizeof missing, menu_text(UI_MENU_AMIIBO_NO_DIR), g_current_dir.c_str());
ImGui::TextWrapped("%s", missing);
}
for (size_t i = 0; i < g_entries.size(); i++) {
const auto &e = g_entries[i];
std::string label = e.is_dir ? e.name + "/" : e.name;
+2 -2
View File
@@ -115,7 +115,7 @@ if WIN:
RT_FLAGS.append('-D_WIN32_WINNT=0x0A00')
# -Xlinker passes the map path as one argument; -Wl, would split it at any comma in the build dir path
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '-Xlinker', '"--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
VMA_INCLUDE = UCRT.parent / 'include' / 'vma' # vk_mem_alloc.h; MSYS2 package vulkan-memory-allocator
else:
# -no-pie: the lifted modules' function tables (hundreds of thousands of pointers) need no load-time relocation.
@@ -151,7 +151,7 @@ def vma_include():
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lvulkan-1', '-lSDL3', '-lopus',
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lvulkan-1', '-lSDL3', '-lopus',
'-l:libvpx.a', '-l:libzstd.a', '-l:libc++.a', '-l:libc++abi.a', '-l:libunwind.a', '-l:libwinpthread.a']
DEFAULT_DEPS = ROOT / 'toolchain' / 'deps-mingw'
# Runtime sources compiled per install, not in the library: romfs.c includes romfs_config.h, patches.c and ov_zstd.c