Step 84: native file dialogs

This commit is contained in:
Bill
2026-02-09 09:26:54 -07:00
parent c9553590e7
commit c195128034
6 changed files with 225 additions and 18 deletions

48
editor/src/FileDialog.cpp Normal file
View 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
View 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);
};

View File

@@ -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)