Step 84: native file dialogs
This commit is contained in:
@@ -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. |
|
||||
|
||||
@@ -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)
|
||||
|
||||
48
editor/src/FileDialog.cpp
Normal file
48
editor/src/FileDialog.cpp
Normal file
@@ -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<std::string>& filters, std::vector<const char*>& 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<const char*> 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<const char*> 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();
|
||||
}
|
||||
67
editor/src/FileDialog.h
Normal file
67
editor/src/FileDialog.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
class FileDialog {
|
||||
public:
|
||||
struct OpenRequest {
|
||||
std::string title;
|
||||
std::vector<std::string> filters;
|
||||
std::string defaultPath;
|
||||
};
|
||||
|
||||
struct SaveRequest {
|
||||
std::string title;
|
||||
std::vector<std::string> filters;
|
||||
std::string defaultPath;
|
||||
};
|
||||
|
||||
struct FolderRequest {
|
||||
std::string title;
|
||||
std::string defaultPath;
|
||||
};
|
||||
|
||||
using OpenFn = std::function<std::string(const OpenRequest&)>;
|
||||
using SaveFn = std::function<std::string(const SaveRequest&)>;
|
||||
using FolderFn = std::function<std::string(const FolderRequest&)>;
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "KeybindingManager.h"
|
||||
#include "CodeEditorWidget.h"
|
||||
#include "EditorMode.h"
|
||||
#include "FileDialog.h"
|
||||
#include "ast/Generator.h"
|
||||
|
||||
#include <cstdio>
|
||||
@@ -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)
|
||||
|
||||
64
editor/tests/step84_test.cpp
Normal file
64
editor/tests/step84_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <iostream>
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user