From c1951280346e65105267d8529db7bf0a5d5e9c2f Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 9 Feb 2026 09:26:54 -0700 Subject: [PATCH] Step 84: native file dialogs --- PROGRESS.md | 5 +++ editor/CMakeLists.txt | 23 ++++++++++++- editor/src/FileDialog.cpp | 48 ++++++++++++++++++++++++++ editor/src/FileDialog.h | 67 ++++++++++++++++++++++++++++++++++++ editor/src/main.cpp | 36 ++++++++++--------- editor/tests/step84_test.cpp | 64 ++++++++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 18 deletions(-) create mode 100644 editor/src/FileDialog.cpp create mode 100644 editor/src/FileDialog.h create mode 100644 editor/tests/step84_test.cpp diff --git a/PROGRESS.md b/PROGRESS.md index 03c8bf0..e70773e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -180,6 +180,9 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1 - [x] Step 82: **IMPLEMENTED** — Minimap: right-side overview with viewport indicator and click-to-scroll (1/1 tests pass) - [x] Step 83: **IMPLEMENTED** — Additional tree-sitter grammars (JS/TS/Java/Rust/Go), new EditorMode configs, syntax highlighting for 8 languages (2/2 tests pass) +### Phase 4b: File Management (Steps 84–89) — In Progress +- [x] Step 84: **IMPLEMENTED** — Native file dialogs via tinyfiledialogs, FileDialog wrapper with test provider injection (2/2 tests pass) + --- ## Build Infrastructure @@ -241,6 +244,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi **Step 81:** Compile and pass (2/2) **Step 82:** Compile and pass (1/1) **Step 83:** Compile and pass (2/2) +**Step 84:** Compile and pass (2/2) --- @@ -332,3 +336,4 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e | 2026-02-09 | Codex | Step 81: Code folding with tree-sitter fold regions and gutter toggles. 2/2 tests pass. | | 2026-02-09 | Codex | Step 82: Minimap overview with viewport indicator and click-to-scroll. 1/1 tests pass. | | 2026-02-09 | Codex | Step 83: Added tree-sitter grammars for JS/TS/Java/Rust/Go and editor modes; syntax highlighting extended to 8 languages. 2/2 tests pass. | +| 2026-02-09 | Codex | Step 84: Native file dialogs via tinyfiledialogs; FileDialog wrapper with injectable provider. 2/2 tests pass. | diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 675dd9e..516cb1a 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -71,6 +71,13 @@ FetchContent_Declare( GIT_SHALLOW TRUE ) +FetchContent_Declare( + tinyfiledialogs + GIT_REPOSITORY https://github.com/native-toolkit/tinyfiledialogs.git + GIT_TAG master + GIT_SHALLOW TRUE +) + # Populate without add_subdirectory (grammars have their own CMakeLists with conflicting targets) FetchContent_GetProperties(tree-sitter-python) if(NOT tree-sitter-python_POPULATED) @@ -112,6 +119,11 @@ if(NOT tree-sitter-go_POPULATED) FetchContent_Populate(tree-sitter-go) endif() +FetchContent_GetProperties(tinyfiledialogs) +if(NOT tinyfiledialogs_POPULATED) + FetchContent_Populate(tinyfiledialogs) +endif() + # Build tree-sitter-python grammar as a static C library add_library(tree_sitter_python STATIC ${tree-sitter-python_SOURCE_DIR}/src/parser.c @@ -187,6 +199,10 @@ add_library(tree_sitter_go STATIC ${_ts_go_src}) target_include_directories(tree_sitter_go PUBLIC ${tree-sitter-go_SOURCE_DIR}/src) target_link_libraries(tree_sitter_go PUBLIC unofficial::tree-sitter::tree-sitter) +# tinyfiledialogs (single C file) +add_library(tinyfiledialogs STATIC ${tinyfiledialogs_SOURCE_DIR}/tinyfiledialogs.c) +target_include_directories(tinyfiledialogs PUBLIC ${tinyfiledialogs_SOURCE_DIR}) + add_executable(step1_test tests/step1_test.cpp) target_include_directories(step1_test PRIVATE src) @@ -466,6 +482,10 @@ target_link_libraries(step83_test PRIVATE unofficial::tree-sitter::tree-sitter tree_sitter_rust tree_sitter_go) +add_executable(step84_test tests/step84_test.cpp src/FileDialog.cpp) +target_include_directories(step84_test PRIVATE src) +target_link_libraries(step84_test PRIVATE tinyfiledialogs) + find_package(SDL2 CONFIG REQUIRED) find_package(OpenGL REQUIRED) find_package(glad CONFIG REQUIRED) @@ -500,7 +520,7 @@ else() endif() endif() -add_executable(whetstone_editor src/main.cpp ${IMGUI_SDL2_BACKEND}) +add_executable(whetstone_editor src/main.cpp src/FileDialog.cpp ${IMGUI_SDL2_BACKEND}) target_include_directories(whetstone_editor PRIVATE src ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include/imgui @@ -526,6 +546,7 @@ target_link_libraries(whetstone_editor PRIVATE tree_sitter_java tree_sitter_rust tree_sitter_go) +target_link_libraries(whetstone_editor PRIVATE tinyfiledialogs) add_executable(orchestrator src/orchestrator_main.cpp) target_include_directories(orchestrator PRIVATE src) diff --git a/editor/src/FileDialog.cpp b/editor/src/FileDialog.cpp new file mode 100644 index 0000000..541a6d4 --- /dev/null +++ b/editor/src/FileDialog.cpp @@ -0,0 +1,48 @@ +#include "FileDialog.h" + +#ifdef _WIN32 +#define TINYFD_WIN_CONSOLE +#endif + +#include "tinyfiledialogs.h" + +static const char* const* buildFilterArray(const std::vector& filters, std::vector& out) { + out.clear(); + for (const auto& f : filters) out.push_back(f.c_str()); + return out.empty() ? nullptr : out.data(); +} + +std::string FileDialog::openFileNative(const OpenRequest& req) { + std::vector filters; + const char* const* filter = buildFilterArray(req.filters, filters); + const char* path = tinyfd_openFileDialog( + req.title.empty() ? "Open File" : req.title.c_str(), + req.defaultPath.empty() ? nullptr : req.defaultPath.c_str(), + (int)req.filters.size(), + filter, + nullptr, + 0 + ); + return path ? std::string(path) : std::string(); +} + +std::string FileDialog::saveFileNative(const SaveRequest& req) { + std::vector filters; + const char* const* filter = buildFilterArray(req.filters, filters); + const char* path = tinyfd_saveFileDialog( + req.title.empty() ? "Save File" : req.title.c_str(), + req.defaultPath.empty() ? nullptr : req.defaultPath.c_str(), + (int)req.filters.size(), + filter, + nullptr + ); + return path ? std::string(path) : std::string(); +} + +std::string FileDialog::openFolderNative(const FolderRequest& req) { + const char* path = tinyfd_selectFolderDialog( + req.title.empty() ? "Open Folder" : req.title.c_str(), + req.defaultPath.empty() ? nullptr : req.defaultPath.c_str() + ); + return path ? std::string(path) : std::string(); +} diff --git a/editor/src/FileDialog.h b/editor/src/FileDialog.h new file mode 100644 index 0000000..f5b9c2d --- /dev/null +++ b/editor/src/FileDialog.h @@ -0,0 +1,67 @@ +#pragma once +// Step 84: Native file dialogs wrapper +// +// Provides a small abstraction over native file dialogs (tinyfiledialogs) +// with an injectable provider for tests. + +#include +#include +#include + +class FileDialog { +public: + struct OpenRequest { + std::string title; + std::vector filters; + std::string defaultPath; + }; + + struct SaveRequest { + std::string title; + std::vector filters; + std::string defaultPath; + }; + + struct FolderRequest { + std::string title; + std::string defaultPath; + }; + + using OpenFn = std::function; + using SaveFn = std::function; + using FolderFn = std::function; + + struct Provider { + OpenFn openFile; + SaveFn saveFile; + FolderFn openFolder; + }; + + static void setProvider(const Provider& provider) { provider_() = provider; } + static void clearProvider() { provider_() = Provider{}; } + + static std::string openFile(const OpenRequest& req) { + if (provider_().openFile) return provider_().openFile(req); + return openFileNative(req); + } + + static std::string saveFile(const SaveRequest& req) { + if (provider_().saveFile) return provider_().saveFile(req); + return saveFileNative(req); + } + + static std::string openFolder(const FolderRequest& req) { + if (provider_().openFolder) return provider_().openFolder(req); + return openFolderNative(req); + } + +private: + static Provider& provider_() { + static Provider p{}; + return p; + } + + static std::string openFileNative(const OpenRequest& req); + static std::string saveFileNative(const SaveRequest& req); + static std::string openFolderNative(const FolderRequest& req); +}; diff --git a/editor/src/main.cpp b/editor/src/main.cpp index 3d6a672..85500c4 100644 --- a/editor/src/main.cpp +++ b/editor/src/main.cpp @@ -19,6 +19,7 @@ #include "KeybindingManager.h" #include "CodeEditorWidget.h" #include "EditorMode.h" +#include "FileDialog.h" #include "ast/Generator.h" #include @@ -462,8 +463,8 @@ int main(int, char**) { EditorState state; state.init(); - // File open dialog state - static char openPathBuf[512] = {}; + // File dialog defaults + std::string lastDialogPath; bool done = false; while (!done) { @@ -550,11 +551,24 @@ int main(int, char**) { } if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str())) { - ImGui::OpenPopup("OpenFilePopup"); + auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"}, lastDialogPath}); + if (!path.empty()) { + lastDialogPath = path; + state.doOpen(path); + } } if (ImGui::MenuItem("Save", state.keys.getBinding("file.save").toString().c_str())) { - state.doSave(); + if (state.filePath == "(untitled)") { + auto path = FileDialog::saveFile({"Save File", {"*.*"}, lastDialogPath}); + if (!path.empty()) { + state.filePath = path; + lastDialogPath = path; + state.doSave(); + } + } else { + state.doSave(); + } } ImGui::Separator(); if (ImGui::MenuItem("Exit")) done = true; @@ -607,19 +621,7 @@ int main(int, char**) { ImGui::EndMainMenuBar(); } - // Open file popup - if (ImGui::BeginPopupModal("OpenFilePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Enter file path:"); - ImGui::InputText("##path", openPathBuf, sizeof(openPathBuf)); - if (ImGui::Button("Open", ImVec2(120, 0))) { - state.doOpen(openPathBuf); - ImGui::CloseCurrentPopup(); - } - ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(120, 0))) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } + // Native dialogs replace manual path popup (Step 84) // --------------------------------------------------------------- // File Explorer (left panel) diff --git a/editor/tests/step84_test.cpp b/editor/tests/step84_test.cpp new file mode 100644 index 0000000..294d355 --- /dev/null +++ b/editor/tests/step84_test.cpp @@ -0,0 +1,64 @@ +// Step 84 TDD Test: Native file dialogs wrapper +// +// Tests: +// 1. Provider override returns expected open/save paths +// 2. Clearing provider falls back to empty (non-interactive) + +#include +#include +#include "FileDialog.h" + +static std::string g_open; +static std::string g_save; +static std::string g_folder; + +static std::string openProvider(const FileDialog::OpenRequest& req) { + (void)req; + return g_open; +} + +static std::string saveProvider(const FileDialog::SaveRequest& req) { + (void)req; + return g_save; +} + +static std::string folderProvider(const FileDialog::FolderRequest& req) { + (void)req; + return g_folder; +} + +int main() { + int passed = 0; + int failed = 0; + + // --- Test 1: Provider override --- + { + g_open = "C:/tmp/foo.py"; + g_save = "C:/tmp/out.py"; + g_folder = "C:/tmp/project"; + FileDialog::Provider provider{openProvider, saveProvider, folderProvider}; + FileDialog::setProvider(provider); + + auto open = FileDialog::openFile({"Open", {"*.py"}, ""}); + auto save = FileDialog::saveFile({"Save", {"*.py"}, ""}); + auto folder = FileDialog::openFolder({"Folder", ""}); + + assert(open == g_open); + assert(save == g_save); + assert(folder == g_folder); + std::cout << "Test 1 PASS: Provider override" << std::endl; + ++passed; + } + + // --- Test 2: Clear provider --- + { + FileDialog::clearProvider(); + auto open = FileDialog::openFile({"Open", {"*.py"}, ""}); + assert(open.empty()); + std::cout << "Test 2 PASS: Clear provider" << std::endl; + ++passed; + } + + std::cout << "\n=== Step 84 Results: " << passed << " passed, " << failed << " failed ===" << std::endl; + return failed > 0 ? 1 : 0; +}