From fbb4d1b360cccb7bcad71d1476091581afaf021c Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Mon, 9 Mar 2026 22:27:24 -0500 Subject: [PATCH 1/9] new: open project sets directory to last project opened, tracks a list of recently opened projects --- CMakeLists.txt | 2 ++ src/editor/globalActions.cpp | 3 +++ src/editor/pages/launcher.cpp | 5 +++- src/editor/recentProjects.cpp | 49 +++++++++++++++++++++++++++++++++++ src/editor/recentProjects.h | 16 ++++++++++++ src/utils/filePicker.cpp | 8 +++++- src/utils/filePicker.h | 1 + src/utils/json.h | 1 + 8 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/editor/recentProjects.cpp create mode 100644 src/editor/recentProjects.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bbb5019..0da27c69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -128,6 +128,8 @@ add_executable(pyrite64 src/main.cpp src/renderer/texture.cpp src/renderer/texture.h src/project/project.cpp + src/editor/recentProjects.h + src/editor/recentProjects.cpp src/editor/actions.h src/editor/actions.cpp src/editor/undoRedo.h diff --git a/src/editor/globalActions.cpp b/src/editor/globalActions.cpp index d4d8765a..943dbf01 100644 --- a/src/editor/globalActions.cpp +++ b/src/editor/globalActions.cpp @@ -14,6 +14,7 @@ #include "../utils/json.h" #include "../utils/proc.h" #include "undoRedo.h" +#include "recentProjects.h" #include "pages/editorScene.h" //#include @@ -27,6 +28,7 @@ namespace Editor::Actions UndoRedo::getHistory().clear(); try { ctx.project = new Project::Project(path); + Editor::RecentProjects::setMostRecentPath(path); if(ctx.project && !ctx.project->getScenes().getEntries().empty()) { ctx.project->getScenes().loadScene(ctx.project->conf.sceneIdLastOpened); } @@ -96,6 +98,7 @@ namespace Editor::Actions configJSON["name"] = args["name"]; configJSON["romName"] = args["rom"]; Utils::FS::saveTextFile(configPath, configJSON.dump(2)); + Editor::RecentProjects::setMostRecentPath(configPath); return true; }); diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index cdc61ca2..cc6d89fe 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -12,6 +12,7 @@ #include "../imgui/theme.h" #include "../actions.h" #include "../../utils/filePicker.h" +#include "../recentProjects.h" #include "../../context.h" #include "backends/imgui_impl_sdlgpu3.h" #include "parts/createProjectOverlay.h" @@ -159,6 +160,7 @@ void Editor::Launcher::draw() } if (renderButton(texBtnOpen, "Open Project", isHoverLast, posX)) { + std::string mostRecent = Editor::RecentProjects::getMostRecentPath(); Utils::FilePicker::open([](const std::string &path) { if (path.empty()) return; @@ -174,7 +176,8 @@ void Editor::Launcher::draw() }, { .title="Choose Project File (.p64proj)", .isDirectory = false, - .customFilters = {{"Pyrite64 Project", "p64proj"}} + .customFilters = {{"Pyrite64 Project", "p64proj"}}, + .defaultPath = mostRecent }); } } diff --git a/src/editor/recentProjects.cpp b/src/editor/recentProjects.cpp new file mode 100644 index 00000000..4edc6a0f --- /dev/null +++ b/src/editor/recentProjects.cpp @@ -0,0 +1,49 @@ +/** +* @copyright 2026 - Nolan Baker +* @license MIT +*/ + +#include "recentProjects.h" +#include +#include "json.hpp" +#include "../utils/fs.h" +#include "../utils/proc.h" +#include "../utils/json.h" + +namespace Editor::RecentProjects { + std::vector recentPaths = {}; + + std::string getJsonPath() { + auto path = Utils::Proc::getAppDataPath() / "recent.json"; + return path.string(); + } + + std::string getMostRecentPath() { + if (!recentPaths.empty()) return recentPaths.front(); + + nlohmann::json json; + try { + json = Utils::JSON::loadFile(getJsonPath()); + if (!json.is_array()) return ""; + recentPaths = json.get>(); + } catch (const std::exception& e) { + fprintf(stderr, "Error loading recent.json: %s\n", e.what()); + return ""; + } + + if (recentPaths.empty()) return ""; + return recentPaths.front(); + } + + void setMostRecentPath(const std::string &path) { + recentPaths.erase(std::remove(recentPaths.begin(), recentPaths.end(), path), recentPaths.end()); + recentPaths.insert(recentPaths.begin(), path); + + try { + nlohmann::json json = recentPaths; + Utils::FS::saveTextFile(getJsonPath(), json.dump(2)); + } catch (const std::exception& e) { + fprintf(stderr, "Error saving recent.json: %s\n", e.what()); + } + } +} diff --git a/src/editor/recentProjects.h b/src/editor/recentProjects.h new file mode 100644 index 00000000..e84ed471 --- /dev/null +++ b/src/editor/recentProjects.h @@ -0,0 +1,16 @@ +/** +* @copyright 2026 - Nolan Baker +* @license MIT +*/ +#pragma once + +#include +#include + +namespace Editor::RecentProjects { + extern std::vector recentPaths; + + std::string getJsonPath(); + std::string getMostRecentPath(); + void setMostRecentPath(const std::string &path); +} diff --git a/src/utils/filePicker.cpp b/src/utils/filePicker.cpp index 06ed1a1e..1ab0a2d8 100644 --- a/src/utils/filePicker.cpp +++ b/src/utils/filePicker.cpp @@ -7,6 +7,9 @@ #include #include #include +#include + +namespace fs = std::filesystem; #include "../context.h" @@ -34,7 +37,10 @@ bool Utils::FilePicker::open(std::function cb, con resultUserCb = cb; SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, ctx.window); - //SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location); + if (!options.defaultPath.empty()) { + auto dir = fs::path(options.defaultPath).parent_path(); + SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, dir.c_str()); + } SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false); if(!options.title.empty()) { SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, options.title.c_str()); diff --git a/src/utils/filePicker.h b/src/utils/filePicker.h index 741f1e07..adc802cf 100644 --- a/src/utils/filePicker.h +++ b/src/utils/filePicker.h @@ -20,6 +20,7 @@ namespace Utils::FilePicker std::string title{}; bool isDirectory{false}; std::vector customFilters{}; + std::string defaultPath{}; }; /** diff --git a/src/utils/json.h b/src/utils/json.h index 16ec970f..d817a82b 100644 --- a/src/utils/json.h +++ b/src/utils/json.h @@ -4,6 +4,7 @@ */ #pragma once #include "fs.h" +#include "prop.h" #include "glm/vec3.hpp" #include "glm/gtc/quaternion.hpp" From 85eb436fb890c5d832a6ca9a8b73516186d76753 Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Tue, 10 Mar 2026 22:43:45 -0500 Subject: [PATCH 2/9] chg: only passes default path when opening proj on non-Windows platforms --- src/editor/pages/launcher.cpp | 5 +++-- src/utils/filePicker.cpp | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index cc6d89fe..0982b65c 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -160,7 +160,6 @@ void Editor::Launcher::draw() } if (renderButton(texBtnOpen, "Open Project", isHoverLast, posX)) { - std::string mostRecent = Editor::RecentProjects::getMostRecentPath(); Utils::FilePicker::open([](const std::string &path) { if (path.empty()) return; @@ -177,7 +176,9 @@ void Editor::Launcher::draw() .title="Choose Project File (.p64proj)", .isDirectory = false, .customFilters = {{"Pyrite64 Project", "p64proj"}}, - .defaultPath = mostRecent +#ifndef _WIN32 //this bit is already handled on Windows + .defaultPath = Editor::RecentProjects::getMostRecentPath() +#endif }); } } diff --git a/src/utils/filePicker.cpp b/src/utils/filePicker.cpp index 1ab0a2d8..77011991 100644 --- a/src/utils/filePicker.cpp +++ b/src/utils/filePicker.cpp @@ -38,7 +38,7 @@ bool Utils::FilePicker::open(std::function cb, con SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, ctx.window); if (!options.defaultPath.empty()) { - auto dir = fs::path(options.defaultPath).parent_path(); + auto dir = fs::path(options.defaultPath).parent_path().string(); SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, dir.c_str()); } SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false); From ee58043deb90c7758ad63244130c82c56b9624a5 Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Fri, 13 Mar 2026 02:16:13 -0500 Subject: [PATCH 3/9] new: stubbs out recent projects table --- src/editor/pages/launcher.cpp | 100 +++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 13 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 0982b65c..266787ce 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include "imgui.h" #include "../imgui/theme.h" @@ -51,6 +53,7 @@ Editor::Launcher::Launcher(SDL_GPUDevice* device) texBG{device, "data/img/splashBG.png"} { ctx.toolchain.scan(); + Editor::RecentProjects::getMostRecentPath(); } Editor::Launcher::~Launcher() { @@ -58,7 +61,7 @@ Editor::Launcher::~Launcher() { void Editor::Launcher::draw() { - float BTN_SPACING = 300_px; + float BTN_SPACING = 160_px; const auto &toolState = ctx.toolchain.getState(); auto &io = ImGui::GetIO(); @@ -75,7 +78,7 @@ void Editor::Launcher::draw() // BG ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ImplSDLGPU3_SetSamplerRepeat, nullptr); - float topBgHeight = 7_px; + float topBgHeight = 5_px; float bottomBgHeight = 3_px; float bgRepeatsX = io.DisplaySize.x / texBG.getWidth(); ImGui::SetCursorPos({0,0}); @@ -104,21 +107,18 @@ void Editor::Launcher::draw() ImGui::SetMouseCursor(ImGuiMouseCursor_Arrow); } - auto logoSize = texTitle.getSize(0.65 * ImGui::Theme::zoomFactor); - ImGui::SetCursorPos({ - centerPos.x - (logoSize.x/2) + 16_px, - 28_px - }); + auto logoSize = texTitle.getSize(0.4 * ImGui::Theme::zoomFactor); + ImGui::SetCursorPos({32_px, 24_px}); ImGui::Image(ImTextureID(texTitle.getGPUTex()),logoSize); auto renderButton = [&](Renderer::Texture &img, const char* text, bool& hover, int &posX) -> bool { - auto btnSizeAdd = img.getSize(hover ? 0.85f : 0.8f); + auto btnSizeAdd = img.getSize(hover ? 0.45f : 0.4f); btnSizeAdd *= ImGui::Theme::zoomFactor; ImVec2 btnPos{ posX - (btnSizeAdd.x/2), - midBgPointY - (btnSizeAdd.y/2), + 72_px - (btnSizeAdd.y/2), }; ImGui::SetCursorPos(btnPos); @@ -131,7 +131,7 @@ void Editor::Launcher::draw() renderSubText( btnPos.x + (btnSizeAdd.x / 2), - btnSizeAdd, midBgPointY, text + btnSizeAdd, 72_px, text ); posX += BTN_SPACING; @@ -146,10 +146,9 @@ void Editor::Launcher::draw() bool validToolchain = toolState.hasToolchain && toolState.hasLibdragon && toolState.hasTiny3d; int buttonCount = (validToolchain && toolState.upToDateLibs) ? 3 : 1; - // screen center - int posX = (int)centerPos.x - 6_px; + int posX = (int)io.DisplaySize.x - BTN_SPACING + 48_px; if(buttonCount == 3) { - posX -= (BTN_SPACING); + posX -= (BTN_SPACING * 2); } if(buttonCount == 3) @@ -210,6 +209,81 @@ void Editor::Launcher::draw() ImGui::PopStyleColor(3); + // recent files + ImGui::SetCursorPos({8_px, (float)texBG.getHeight() * topBgHeight - 40_px}); + ImGui::PushStyleColor(ImGuiCol_TableHeaderBg, ImVec4(0,0,0,0)); + if (ImGui::BeginTable("RecentProjects", 5, ImGuiTableFlags_NoBordersInBody)) { + ImGui::PushFont(nullptr, 20_px); + ImGui::TableSetupColumn(ICON_MDI_CHEVRON_RIGHT, ImGuiTableColumnFlags_WidthFixed, 32_px); + ImGui::TableSetupColumn("Project\nName"); + ImGui::TableSetupColumn("Last\nEdit", ImGuiTableColumnFlags_WidthFixed, 120_px); + ImGui::TableSetupColumn("Editor\nVersion", ImGuiTableColumnFlags_WidthFixed, 80_px); + ImGui::TableSetupColumn(ICON_MDI_COG, ImGuiTableColumnFlags_WidthFixed, 32_px); + ImGui::PopFont(); + ImGui::TableNextRow(ImGuiTableRowFlags_Headers, 50_px); + for (int column = 0; column < 5; column++) { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); + if (column == 0 || column == 4) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8_px); + ImGui::TextUnformatted(column_name); + } + ImGui::PopStyleColor(); + + auto paths = Editor::RecentProjects::recentPaths; + int index = 0; + for(auto path : paths) { + //expand arrow + ImGui::TableNextRow(ImGuiTableRowFlags_None, 48_px);//TODO make this number bigger when expanded + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(ICON_MDI_CHEVRON_RIGHT); + + //project name + ImGui::TableSetColumnIndex(1); + ImGui::AlignTextToFramePadding(); + ImGui::BeginGroup(); + ImGui::PushFont(nullptr, 16_px); + ImGui::TextUnformatted("My Project Name"); + ImGui::PopFont(); + ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); + ImGui::TextUnformatted(path.c_str()); + ImGui::PopStyleColor(); + ImGui::PopFont(); + ImGui::EndGroup(); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + Editor::Actions::call(Editor::Actions::Type::PROJECT_OPEN, path); + } + + //last modified + ImGui::TableSetColumnIndex(2); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("Three Days Ago"); + + //editor ersion + ImGui::TableSetColumnIndex(3); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted("0.6.0"); + + //open context menu + ImGui::TableSetColumnIndex(4); + ImGui::AlignTextToFramePadding(); + ImGui::PushID(index); // Unique ID for each row's popup + if (ImGui::Button(ICON_MDI_DOTS_HORIZONTAL)) { + ImGui::OpenPopup("ProjectContextMenu"); + } + + if (ImGui::BeginPopup("ProjectContextMenu")) { + if (ImGui::MenuItem("Reveal in Explorer")) { /* ... */ } + if (ImGui::MenuItem("Delete Reference", NULL, false, true)) { /* ... */ } + ImGui::EndPopup(); + } + ImGui::PopID(); + index++; + } + ImGui::EndTable(); + } + // version + credits { float PADDING = 24_px; From f21db174a50a938c6db46a1aa0dfa9f2dcd7f685 Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Fri, 13 Mar 2026 03:00:29 -0500 Subject: [PATCH 4/9] chg: adds editor version info to project conf, reads conf info in table --- src/editor/pages/launcher.cpp | 18 +++++++++++++++--- src/project/project.cpp | 2 ++ src/project/project.h | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 266787ce..37c1a043 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -9,6 +9,8 @@ #include #include #include +#include +#include #include "imgui.h" #include "../imgui/theme.h" @@ -22,6 +24,8 @@ #include "SDL3/SDL_dialog.h" #include "../imgui/notification.h" +namespace fs = std::filesystem; + void ImDrawCallback_ImplSDLGPU3_SetSamplerRepeat(const ImDrawList* parent_list, const ImDrawCmd* cmd); namespace @@ -232,6 +236,14 @@ void Editor::Launcher::draw() auto paths = Editor::RecentProjects::recentPaths; int index = 0; for(auto path : paths) { + auto json = Utils::JSON::loadFile(path); + if (json.empty()) continue; + std::string projectName = json.value("name", ""); + std::string editorVersion = json.value("editorVersion", PYRITE_VERSION); + fs::path projPath{path}; + auto writeTime = fs::last_write_time(projPath); + std::string writeTimeString = std::format("{:%Y-%m-%d}", writeTime); + //expand arrow ImGui::TableNextRow(ImGuiTableRowFlags_None, 48_px);//TODO make this number bigger when expanded ImGui::TableSetColumnIndex(0); @@ -243,7 +255,7 @@ void Editor::Launcher::draw() ImGui::AlignTextToFramePadding(); ImGui::BeginGroup(); ImGui::PushFont(nullptr, 16_px); - ImGui::TextUnformatted("My Project Name"); + ImGui::TextUnformatted(projectName.c_str()); ImGui::PopFont(); ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); @@ -258,12 +270,12 @@ void Editor::Launcher::draw() //last modified ImGui::TableSetColumnIndex(2); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("Three Days Ago"); + ImGui::TextUnformatted(writeTimeString.c_str()); //editor ersion ImGui::TableSetColumnIndex(3); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted("0.6.0"); + ImGui::TextUnformatted(editorVersion.c_str()); //open context menu ImGui::TableSetColumnIndex(4); diff --git a/src/project/project.cpp b/src/project/project.cpp index 2bd1ad0b..49a0588c 100644 --- a/src/project/project.cpp +++ b/src/project/project.cpp @@ -63,6 +63,7 @@ std::string Project::ProjectConf::serialize() const { .set("romName", romName) .set("pathEmu", pathEmu) .set("pathN64Inst", pathN64Inst) + .set("editorVersion", PYRITE_VERSION) .set("sceneIdOnBoot", sceneIdOnBoot) .set("sceneIdOnReset", sceneIdOnReset) .set("sceneIdLastOpened", sceneIdLastOpened) @@ -82,6 +83,7 @@ void Project::Project::deserialize(const nlohmann::json &doc) { conf.romName = doc.value("romName", "pyrite64"); conf.pathEmu = doc.value("pathEmu", "ares"); conf.pathN64Inst = doc.value("pathN64Inst", ""); + conf.editorVersion = doc.value("editorVersion", PYRITE_VERSION); conf.sceneIdOnBoot = doc.value("sceneIdOnBoot", 1); conf.sceneIdOnReset = doc.value("sceneIdOnReset", 1); conf.sceneIdLastOpened = doc.value("sceneIdLastOpened", 1); diff --git a/src/project/project.h b/src/project/project.h index 9db7f75e..853dd0ce 100644 --- a/src/project/project.h +++ b/src/project/project.h @@ -16,6 +16,7 @@ namespace Project std::string romName{}; std::string pathEmu{}; std::string pathN64Inst{}; + std::string editorVersion{}; uint32_t sceneIdOnBoot{1}; uint32_t sceneIdOnReset{1}; From 5c256a4443dc9a7d2b7e8d023af40ea9a174d82d Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Fri, 13 Mar 2026 03:40:25 -0500 Subject: [PATCH 5/9] fix: caches project data so I'm not thrashing the disk --- src/editor/pages/launcher.cpp | 76 ++++++++++++++++++++++++++--------- src/editor/pages/launcher.h | 11 +++++ 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 37c1a043..1f40e2ae 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -58,6 +58,19 @@ Editor::Launcher::Launcher(SDL_GPUDevice* device) { ctx.toolchain.scan(); Editor::RecentProjects::getMostRecentPath(); + projectEntries = {}; + for(auto path : Editor::RecentProjects::recentPaths) { + auto json = Utils::JSON::loadFile(path); + if (json.empty()) continue; + Editor::ProjectEntry entry; + entry.name = json.value("name", ""); + entry.path = path; + entry.editorVersion = json.value("editorVersion", PYRITE_VERSION); + fs::path projPath{path}; + auto writeTime = fs::last_write_time(projPath); + entry.lastModified = std::format("{:%Y-%m-%d}", writeTime); + projectEntries.push_back(entry); + } } Editor::Launcher::~Launcher() { @@ -233,21 +246,20 @@ void Editor::Launcher::draw() } ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); auto paths = Editor::RecentProjects::recentPaths; int index = 0; - for(auto path : paths) { - auto json = Utils::JSON::loadFile(path); - if (json.empty()) continue; - std::string projectName = json.value("name", ""); - std::string editorVersion = json.value("editorVersion", PYRITE_VERSION); - fs::path projPath{path}; - auto writeTime = fs::last_write_time(projPath); - std::string writeTimeString = std::format("{:%Y-%m-%d}", writeTime); - + for(auto entry : projectEntries) { //expand arrow + ImGui::PushID(index); ImGui::TableNextRow(ImGuiTableRowFlags_None, 48_px);//TODO make this number bigger when expanded ImGui::TableSetColumnIndex(0); - ImGui::AlignTextToFramePadding(); + float y = ImGui::GetCursorPosY(); + ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap; + if (ImGui::Selectable("##rowSelectable", false, selectableFlags, ImVec2(0, 48_px))) { + Editor::Actions::call(Editor::Actions::Type::PROJECT_OPEN, entry.path); + } + ImGui::SetCursorPosY(y + 8_px); ImGui::TextUnformatted(ICON_MDI_CHEVRON_RIGHT); //project name @@ -255,45 +267,41 @@ void Editor::Launcher::draw() ImGui::AlignTextToFramePadding(); ImGui::BeginGroup(); ImGui::PushFont(nullptr, 16_px); - ImGui::TextUnformatted(projectName.c_str()); + ImGui::TextUnformatted(entry.name.c_str()); ImGui::PopFont(); ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); - ImGui::TextUnformatted(path.c_str()); + ImGui::TextUnformatted(entry.path.c_str()); ImGui::PopStyleColor(); ImGui::PopFont(); ImGui::EndGroup(); - if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { - Editor::Actions::call(Editor::Actions::Type::PROJECT_OPEN, path); - } //last modified ImGui::TableSetColumnIndex(2); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(writeTimeString.c_str()); + ImGui::TextUnformatted(entry.lastModified.c_str()); //editor ersion ImGui::TableSetColumnIndex(3); ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(editorVersion.c_str()); + ImGui::TextUnformatted(entry.editorVersion.c_str()); //open context menu ImGui::TableSetColumnIndex(4); ImGui::AlignTextToFramePadding(); - ImGui::PushID(index); // Unique ID for each row's popup if (ImGui::Button(ICON_MDI_DOTS_HORIZONTAL)) { ImGui::OpenPopup("ProjectContextMenu"); } if (ImGui::BeginPopup("ProjectContextMenu")) { - if (ImGui::MenuItem("Reveal in Explorer")) { /* ... */ } - if (ImGui::MenuItem("Delete Reference", NULL, false, true)) { /* ... */ } + Editor::Launcher::showContextMenu(entry.path); ImGui::EndPopup(); } ImGui::PopID(); index++; } ImGui::EndTable(); + ImGui::PopStyleColor(); } // version + credits @@ -319,3 +327,31 @@ void Editor::Launcher::draw() ImGui::End(); } + + +void Editor::Launcher::showContextMenu(const std::string& path) { +#if defined(_WIN32) + std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in Explorer"; +#elif defined(__APPLE__) + std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in Finder"; +#else + std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in File Manager"; +#endif + if(ImGui::MenuItem(showPrompt.c_str())) { + if (!Utils::Proc::openInFileBrowser(path)) { + Editor::Noti::add(Editor::Noti::Type::ERROR, "Failed to open File Explorer. This may be due to WSL path conversion failure."); + } + } + + if(ImGui::MenuItem(ICON_MDI_CONTENT_COPY " Copy Path")) { + SDL_SetClipboardText(path.c_str()); + } + + if(ImGui::MenuItem(ICON_MDI_RENAME " Rename Project")) { + //TODO + } + + if(ImGui::MenuItem(ICON_MDI_DELETE " Remove from List")) { + //TODO + } +} \ No newline at end of file diff --git a/src/editor/pages/launcher.h b/src/editor/pages/launcher.h index 37e7eb85..2251d8fd 100644 --- a/src/editor/pages/launcher.h +++ b/src/editor/pages/launcher.h @@ -3,11 +3,19 @@ * @license MIT */ #pragma once +#include #include "SDL3/SDL_gpu.h" #include "../../renderer/texture.h" namespace Editor { + struct ProjectEntry { + std::string name; + std::string path; + std::string editorVersion; + std::string lastModified; + }; + class Launcher { private: @@ -16,11 +24,14 @@ namespace Editor Renderer::Texture texBtnOpen; Renderer::Texture texBtnTool; Renderer::Texture texBG; + std::vector projectEntries; public: Launcher(SDL_GPUDevice* device); ~Launcher(); void draw(); + void showContextMenu(const std::string& path); + }; } From 6d0f34319ad96ff7e004a87158b69e5d9d6b4169 Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Fri, 13 Mar 2026 23:58:42 -0500 Subject: [PATCH 6/9] new: remove project from list, refresh list, expand entry --- src/editor/pages/launcher.cpp | 101 +++++++++++++++++++++++----------- src/editor/pages/launcher.h | 6 +- src/editor/recentProjects.cpp | 31 +++++++---- src/editor/recentProjects.h | 3 + 4 files changed, 97 insertions(+), 44 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 1f40e2ae..5826ae15 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -57,7 +57,14 @@ Editor::Launcher::Launcher(SDL_GPUDevice* device) texBG{device, "data/img/splashBG.png"} { ctx.toolchain.scan(); - Editor::RecentProjects::getMostRecentPath(); + updateProjectEntries(); +} + +Editor::Launcher::~Launcher() { +} + +void Editor::Launcher::updateProjectEntries() { + Editor::RecentProjects::load(); projectEntries = {}; for(auto path : Editor::RecentProjects::recentPaths) { auto json = Utils::JSON::loadFile(path); @@ -69,13 +76,11 @@ Editor::Launcher::Launcher(SDL_GPUDevice* device) fs::path projPath{path}; auto writeTime = fs::last_write_time(projPath); entry.lastModified = std::format("{:%Y-%m-%d}", writeTime); + entry.expand = false; projectEntries.push_back(entry); } } -Editor::Launcher::~Launcher() { -} - void Editor::Launcher::draw() { float BTN_SPACING = 160_px; @@ -95,8 +100,8 @@ void Editor::Launcher::draw() // BG ImGui::GetWindowDrawList()->AddCallback(ImDrawCallback_ImplSDLGPU3_SetSamplerRepeat, nullptr); - float topBgHeight = 5_px; - float bottomBgHeight = 3_px; + float topBgHeight = 4.5_px; + float bottomBgHeight = 2.5_px; float bgRepeatsX = io.DisplaySize.x / texBG.getWidth(); ImGui::SetCursorPos({0,0}); ImGui::Image(ImTextureID(texBG.getGPUTex()), @@ -227,40 +232,66 @@ void Editor::Launcher::draw() ImGui::PopStyleColor(3); // recent files - ImGui::SetCursorPos({8_px, (float)texBG.getHeight() * topBgHeight - 40_px}); - ImGui::PushStyleColor(ImGuiCol_TableHeaderBg, ImVec4(0,0,0,0)); + ImGui::SetCursorPos({8_px, (float)texBG.getHeight() * topBgHeight + 8_px}); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); if (ImGui::BeginTable("RecentProjects", 5, ImGuiTableFlags_NoBordersInBody)) { - ImGui::PushFont(nullptr, 20_px); - ImGui::TableSetupColumn(ICON_MDI_CHEVRON_RIGHT, ImGuiTableColumnFlags_WidthFixed, 32_px); + + //header + ImGui::PushStyleColor(ImGuiCol_TableHeaderBg, ImVec4(0,0,0,0)); + const char* expandLabel = expandAll ? ICON_MDI_CHEVRON_DOWN : ICON_MDI_CHEVRON_RIGHT; + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 32_px); ImGui::TableSetupColumn("Project\nName"); - ImGui::TableSetupColumn("Last\nEdit", ImGuiTableColumnFlags_WidthFixed, 120_px); + ImGui::TableSetupColumn("Last\nModified", ImGuiTableColumnFlags_WidthFixed, 120_px); ImGui::TableSetupColumn("Editor\nVersion", ImGuiTableColumnFlags_WidthFixed, 80_px); - ImGui::TableSetupColumn(ICON_MDI_COG, ImGuiTableColumnFlags_WidthFixed, 32_px); - ImGui::PopFont(); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 32_px); + ImGui::PushFont(nullptr, 16_px); ImGui::TableNextRow(ImGuiTableRowFlags_Headers, 50_px); for (int column = 0; column < 5; column++) { ImGui::TableSetColumnIndex(column); - const char* column_name = ImGui::TableGetColumnName(column); - if (column == 0 || column == 4) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8_px); - ImGui::TextUnformatted(column_name); + //ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 8_px); + const char* columnName = ImGui::TableGetColumnName(column); + if (column == 0 && ImGui::Button(expandLabel, ImVec2(32_px, 32_px))) { + expandAll = !expandAll; + for(auto &entry : projectEntries) entry.expand = expandAll; + } else if (column == 4 && ImGui::Button(ICON_MDI_COG, ImVec2(32_px, 32_px))) { + ImGui::OpenPopup("HeaderContextMenu"); + } else ImGui::TextUnformatted(columnName); } + ImGui::PopFont(); ImGui::PopStyleColor(); + if (ImGui::BeginPopup("HeaderContextMenu")) { + Editor::Launcher::showHeaderContextMenu(); + ImGui::EndPopup(); + } - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); + //separator + float y = ImGui::GetCursorScreenPos().y; + y -= 16_px; + ImGui::SetCursorPosY(y); + ImGui::GetWindowDrawList()->AddLine( + ImVec2(8_px, y), + ImVec2(io.DisplaySize.x - 8_px, y), + ImGui::GetColorU32(ImGuiCol_Separator), + 1_px + ); + auto paths = Editor::RecentProjects::recentPaths; int index = 0; - for(auto entry : projectEntries) { + for (auto& entry : projectEntries) { //expand arrow ImGui::PushID(index); - ImGui::TableNextRow(ImGuiTableRowFlags_None, 48_px);//TODO make this number bigger when expanded + float rowHeight = entry.expand ? 48_px : 32_px; + ImGui::TableNextRow(ImGuiTableRowFlags_None, rowHeight); ImGui::TableSetColumnIndex(0); float y = ImGui::GetCursorPosY(); ImGuiSelectableFlags selectableFlags = ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap; - if (ImGui::Selectable("##rowSelectable", false, selectableFlags, ImVec2(0, 48_px))) { + if (ImGui::Selectable("##SelectableRow", false, selectableFlags, ImVec2(0, rowHeight))) { Editor::Actions::call(Editor::Actions::Type::PROJECT_OPEN, entry.path); } - ImGui::SetCursorPosY(y + 8_px); - ImGui::TextUnformatted(ICON_MDI_CHEVRON_RIGHT); + ImGui::SetCursorPosY(y); + if (ImGui::Button(entry.expand ? ICON_MDI_CHEVRON_DOWN : ICON_MDI_CHEVRON_RIGHT)) { + entry.expand = !entry.expand; + } //project name ImGui::TableSetColumnIndex(1); @@ -269,11 +300,13 @@ void Editor::Launcher::draw() ImGui::PushFont(nullptr, 16_px); ImGui::TextUnformatted(entry.name.c_str()); ImGui::PopFont(); - ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px); - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); - ImGui::TextUnformatted(entry.path.c_str()); - ImGui::PopStyleColor(); - ImGui::PopFont(); + if (entry.expand) { + ImGui::PushFont(ImGui::Theme::getFontMono(), 16_px); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); + ImGui::TextUnformatted(entry.path.c_str()); + ImGui::PopStyleColor(); + ImGui::PopFont(); + } ImGui::EndGroup(); //last modified @@ -294,15 +327,15 @@ void Editor::Launcher::draw() } if (ImGui::BeginPopup("ProjectContextMenu")) { - Editor::Launcher::showContextMenu(entry.path); + Editor::Launcher::showProjectContextMenu(entry.path); ImGui::EndPopup(); } ImGui::PopID(); index++; } ImGui::EndTable(); - ImGui::PopStyleColor(); } + ImGui::PopStyleColor(); // version + credits { @@ -328,8 +361,13 @@ void Editor::Launcher::draw() ImGui::End(); } +void Editor::Launcher::showHeaderContextMenu() { + if(ImGui::MenuItem(ICON_MDI_RELOAD " Reload List")) { + updateProjectEntries(); + } +} -void Editor::Launcher::showContextMenu(const std::string& path) { +void Editor::Launcher::showProjectContextMenu(const std::string& path) { #if defined(_WIN32) std::string showPrompt = ICON_MDI_FOLDER_OPEN " Show in Explorer"; #elif defined(__APPLE__) @@ -352,6 +390,7 @@ void Editor::Launcher::showContextMenu(const std::string& path) { } if(ImGui::MenuItem(ICON_MDI_DELETE " Remove from List")) { - //TODO + Editor::RecentProjects::removePath(path); + updateProjectEntries(); } } \ No newline at end of file diff --git a/src/editor/pages/launcher.h b/src/editor/pages/launcher.h index 2251d8fd..1782d9b6 100644 --- a/src/editor/pages/launcher.h +++ b/src/editor/pages/launcher.h @@ -14,6 +14,7 @@ namespace Editor std::string path; std::string editorVersion; std::string lastModified; + bool expand; }; class Launcher @@ -25,13 +26,16 @@ namespace Editor Renderer::Texture texBtnTool; Renderer::Texture texBG; std::vector projectEntries; + bool expandAll; public: Launcher(SDL_GPUDevice* device); ~Launcher(); void draw(); - void showContextMenu(const std::string& path); + void updateProjectEntries(); + void showHeaderContextMenu(); + void showProjectContextMenu(const std::string& path); }; } diff --git a/src/editor/recentProjects.cpp b/src/editor/recentProjects.cpp index 4edc6a0f..fa2382ff 100644 --- a/src/editor/recentProjects.cpp +++ b/src/editor/recentProjects.cpp @@ -19,18 +19,7 @@ namespace Editor::RecentProjects { } std::string getMostRecentPath() { - if (!recentPaths.empty()) return recentPaths.front(); - - nlohmann::json json; - try { - json = Utils::JSON::loadFile(getJsonPath()); - if (!json.is_array()) return ""; - recentPaths = json.get>(); - } catch (const std::exception& e) { - fprintf(stderr, "Error loading recent.json: %s\n", e.what()); - return ""; - } - + if (recentPaths.empty()) load(); if (recentPaths.empty()) return ""; return recentPaths.front(); } @@ -38,7 +27,15 @@ namespace Editor::RecentProjects { void setMostRecentPath(const std::string &path) { recentPaths.erase(std::remove(recentPaths.begin(), recentPaths.end(), path), recentPaths.end()); recentPaths.insert(recentPaths.begin(), path); + save(); + } + void removePath(const std::string &path) { + recentPaths.erase(std::remove(recentPaths.begin(), recentPaths.end(), path), recentPaths.end()); + save(); + } + + void save() { try { nlohmann::json json = recentPaths; Utils::FS::saveTextFile(getJsonPath(), json.dump(2)); @@ -46,4 +43,14 @@ namespace Editor::RecentProjects { fprintf(stderr, "Error saving recent.json: %s\n", e.what()); } } + + void load() { + nlohmann::json json; + try { + json = Utils::JSON::loadFile(getJsonPath()); + if (json.is_array()) recentPaths = json.get>(); + } catch (const std::exception& e) { + fprintf(stderr, "Error loading recent.json: %s\n", e.what()); + } + } } diff --git a/src/editor/recentProjects.h b/src/editor/recentProjects.h index e84ed471..de30f01a 100644 --- a/src/editor/recentProjects.h +++ b/src/editor/recentProjects.h @@ -13,4 +13,7 @@ namespace Editor::RecentProjects { std::string getJsonPath(); std::string getMostRecentPath(); void setMostRecentPath(const std::string &path); + void removePath(const std::string &path); + void save(); + void load(); } From 8e5fd14d05027c147ff2303d9230198045a6f6dc Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Sat, 14 Mar 2026 00:02:36 -0500 Subject: [PATCH 7/9] chg: removes non-functional rename project context menu item --- src/editor/pages/launcher.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 5826ae15..982de1db 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -385,10 +385,6 @@ void Editor::Launcher::showProjectContextMenu(const std::string& path) { SDL_SetClipboardText(path.c_str()); } - if(ImGui::MenuItem(ICON_MDI_RENAME " Rename Project")) { - //TODO - } - if(ImGui::MenuItem(ICON_MDI_DELETE " Remove from List")) { Editor::RecentProjects::removePath(path); updateProjectEntries(); From b1fe510d03952ed58bffb5adbcd89000ad7909ac Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Sat, 14 Mar 2026 21:39:58 -0500 Subject: [PATCH 8/9] chg: reverts changes to file picker as unnecessary --- src/editor/pages/launcher.cpp | 5 +---- src/utils/filePicker.cpp | 8 +------- src/utils/filePicker.h | 1 - 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 982de1db..9e47062c 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -196,10 +196,7 @@ void Editor::Launcher::draw() }, { .title="Choose Project File (.p64proj)", .isDirectory = false, - .customFilters = {{"Pyrite64 Project", "p64proj"}}, -#ifndef _WIN32 //this bit is already handled on Windows - .defaultPath = Editor::RecentProjects::getMostRecentPath() -#endif + .customFilters = {{"Pyrite64 Project", "p64proj"}} }); } } diff --git a/src/utils/filePicker.cpp b/src/utils/filePicker.cpp index 77011991..06ed1a1e 100644 --- a/src/utils/filePicker.cpp +++ b/src/utils/filePicker.cpp @@ -7,9 +7,6 @@ #include #include #include -#include - -namespace fs = std::filesystem; #include "../context.h" @@ -37,10 +34,7 @@ bool Utils::FilePicker::open(std::function cb, con resultUserCb = cb; SDL_PropertiesID props = SDL_CreateProperties(); SDL_SetPointerProperty(props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, ctx.window); - if (!options.defaultPath.empty()) { - auto dir = fs::path(options.defaultPath).parent_path().string(); - SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, dir.c_str()); - } + //SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, default_location); SDL_SetBooleanProperty(props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, false); if(!options.title.empty()) { SDL_SetStringProperty(props, SDL_PROP_FILE_DIALOG_TITLE_STRING, options.title.c_str()); diff --git a/src/utils/filePicker.h b/src/utils/filePicker.h index adc802cf..741f1e07 100644 --- a/src/utils/filePicker.h +++ b/src/utils/filePicker.h @@ -20,7 +20,6 @@ namespace Utils::FilePicker std::string title{}; bool isDirectory{false}; std::vector customFilters{}; - std::string defaultPath{}; }; /** From 0b784a612a762ddcc418af7b672a84e7c8aa2757 Mon Sep 17 00:00:00 2001 From: Nolan Baker Date: Fri, 8 May 2026 13:20:07 -0500 Subject: [PATCH 9/9] chg: remove project button uses close icon instead of delete icon --- src/editor/pages/launcher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/pages/launcher.cpp b/src/editor/pages/launcher.cpp index 9e47062c..9504bc9f 100644 --- a/src/editor/pages/launcher.cpp +++ b/src/editor/pages/launcher.cpp @@ -382,7 +382,7 @@ void Editor::Launcher::showProjectContextMenu(const std::string& path) { SDL_SetClipboardText(path.c_str()); } - if(ImGui::MenuItem(ICON_MDI_DELETE " Remove from List")) { + if(ImGui::MenuItem(ICON_MDI_CLOSE_CIRCLE " Remove from List")) { Editor::RecentProjects::removePath(path); updateProjectEntries(); }