[studio] Add popup to warn about UUID duplication

This commit is contained in:
Gary Talent 2025-05-06 22:22:26 -05:00
parent d4329981e7
commit ff1e8f260b
4 changed files with 152 additions and 34 deletions

View File

@ -17,6 +17,12 @@
#include "font.hpp" #include "font.hpp"
#include "studioui.hpp" #include "studioui.hpp"
#ifdef OX_OS_Darwin
#define STUDIO_CTRL "Cmd"
#else
#define STUDIO_CTRL "Ctrl"
#endif
namespace studio { namespace studio {
static bool shutdownHandler(turbine::Context &ctx) { static bool shutdownHandler(turbine::Context &ctx) {
@ -174,45 +180,64 @@ bool StudioUI::handleShutdown() noexcept {
void StudioUI::drawMenu() noexcept { void StudioUI::drawMenu() noexcept {
if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) { if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New...", "Ctrl+N", false, m_project)) { if (ImGui::MenuItem("New...", STUDIO_CTRL "+N", false, m_project)) {
m_newMenu.open(); m_newMenu.open();
} }
if (ImGui::MenuItem("New Project...", "Ctrl+Shift+N")) { if (ImGui::MenuItem("New Project...", STUDIO_CTRL "+Shift+N")) {
m_newProject.open(); m_newProject.open();
} }
if (ImGui::MenuItem("Open Project...", "Ctrl+O")) { if (ImGui::MenuItem("Open Project...", STUDIO_CTRL "+O")) {
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath)); m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
} }
if (ImGui::MenuItem("Save", "Ctrl+S", false, m_activeEditor && m_activeEditor->unsavedChanges())) { if (ImGui::MenuItem(
"Save",
STUDIO_CTRL "+S",
false,
m_activeEditor && m_activeEditor->unsavedChanges())) {
m_activeEditor->save(); m_activeEditor->save();
} }
if (ImGui::MenuItem("Quit", "Ctrl+Q")) { if (ImGui::MenuItem("Quit", STUDIO_CTRL "+Q")) {
turbine::requestShutdown(m_tctx); turbine::requestShutdown(m_tctx);
} }
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("Edit")) { if (ImGui::BeginMenu("Edit")) {
auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr; auto undoStack = m_activeEditor ? m_activeEditor->undoStack() : nullptr;
if (ImGui::MenuItem("Undo", "Ctrl+Z", false, undoStack && undoStack->canUndo())) { if (ImGui::MenuItem(
"Undo", STUDIO_CTRL "+Z", false, undoStack && undoStack->canUndo())) {
oxLogError(undoStack->undo()); oxLogError(undoStack->undo());
} }
if (ImGui::MenuItem("Redo", "Ctrl+Y", false, undoStack && undoStack->canRedo())) { if (ImGui::MenuItem(
"Redo", STUDIO_CTRL "+Y", false, undoStack && undoStack->canRedo())) {
oxLogError(undoStack->redo()); oxLogError(undoStack->redo());
} }
ImGui::Separator(); ImGui::Separator();
if (ImGui::MenuItem("Copy", "Ctrl+C", false, m_activeEditor && m_activeEditor->copyEnabled())) { if (ImGui::MenuItem(
"Copy",
STUDIO_CTRL "+C",
false,
m_activeEditor && m_activeEditor->copyEnabled())) {
m_activeEditor->copy(); m_activeEditor->copy();
} }
if (ImGui::MenuItem("Cut", "Ctrl+X", false, m_activeEditor && m_activeEditor->cutEnabled())) { if (ImGui::MenuItem(
"Cut",
STUDIO_CTRL "+X",
false,
m_activeEditor && m_activeEditor->cutEnabled())) {
m_activeEditor->cut(); m_activeEditor->cut();
} }
if (ImGui::MenuItem("Paste", "Ctrl+V", false, m_activeEditor && m_activeEditor->pasteEnabled())) { if (ImGui::MenuItem(
"Paste",
STUDIO_CTRL "+V",
false,
m_activeEditor && m_activeEditor->pasteEnabled())) {
m_activeEditor->paste(); m_activeEditor->paste();
} }
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("View")) { if (ImGui::BeginMenu("View")) {
if (ImGui::MenuItem("Project Explorer", "Ctrl+Shift+1", m_showProjectExplorer)) { if (ImGui::MenuItem(
"Project Explorer", STUDIO_CTRL "+Shift+1", m_showProjectExplorer)) {
toggleProjectExplorer(); toggleProjectExplorer();
} }
ImGui::EndMenu(); ImGui::EndMenu();
@ -278,12 +303,8 @@ void StudioUI::drawTabs() noexcept {
if (m_activeEditor == (*it).get()) { if (m_activeEditor == (*it).get()) {
m_activeEditor = nullptr; m_activeEditor = nullptr;
} }
try { if (auto const err = m_editors.erase(it).moveTo(it)) {
OX_THROW_ERROR(m_editors.erase(it).moveTo(it)); oxErrf("Editor tab deletion failed: {} ({}:{})\n", toStr(err), err.src.file_name(), err.src.line());
} catch (ox::Exception const&ex) {
oxErrf("Editor tab deletion failed: {} ({}:{})\n", ex.what(), ex.src.file_name(), ex.src.line());
} catch (std::exception const&ex) {
oxErrf("Editor tab deletion failed: {}\n", ex.what());
} }
} }
} else { } else {
@ -478,12 +499,25 @@ ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept {
ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept { ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
OX_REQUIRE_M(fs, keel::loadRomFs(path.view())); OX_REQUIRE_M(fs, keel::loadRomFs(path.view()));
OX_RETURN_ERROR(keel::setRomFs(keelCtx(m_tctx), std::move(fs))); keel::DuplicateSet ds;
OX_RETURN_ERROR(keel::setRomFs(keelCtx(m_tctx), std::move(fs), ds));
if (ds.size()) {
ox::String msg;
msg += "Multiple files have the same UUID:\n";
for (auto const &k : ds.keys()) {
msg += ox::sfmt("\n\t{}:\n", k.toString());
for (auto const &v : ds[k]) {
msg += ox::sfmt("\t\t - {}\n", v);
}
}
m_messagePopup.show(msg);
}
OX_RETURN_ERROR( OX_RETURN_ERROR(
ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir) ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir)
.moveTo(m_project)); .moveTo(m_project));
m_sctx.project = m_project.get(); m_sctx.project = m_project.get();
turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); turbine::setWindowTitle(
m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath()));
m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem); m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem);
m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir); m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);

View File

@ -52,11 +52,12 @@ class StudioUI: public ox::SignalHandler {
"Close Application?", "Close Application?",
"There are files with unsaved changes. Close?" "There are files with unsaved changes. Close?"
}; };
ig::MessagePopup m_messagePopup{"Message", ""};
MakeCopyPopup m_copyFilePopup; MakeCopyPopup m_copyFilePopup;
RenameFile m_renameFile; RenameFile m_renameFile;
NewProject m_newProject; NewProject m_newProject;
AboutPopup m_aboutPopup; AboutPopup m_aboutPopup;
ox::Array<Widget*, 9> const m_widgets { ox::Array<Widget*, 10> const m_widgets {
&m_closeFileConfirm, &m_closeFileConfirm,
&m_closeAppConfirm, &m_closeAppConfirm,
&m_copyFilePopup, &m_copyFilePopup,
@ -66,6 +67,7 @@ class StudioUI: public ox::SignalHandler {
&m_deleteConfirmation, &m_deleteConfirmation,
&m_newDirDialog, &m_newDirDialog,
&m_renameFile, &m_renameFile,
&m_messagePopup,
}; };
bool m_showProjectExplorer = true; bool m_showProjectExplorer = true;
struct NavAction { struct NavAction {

View File

@ -169,7 +169,9 @@ TextInput<ox::IString<MaxChars>> InputText(
out.changed = ImGui::InputText( out.changed = ImGui::InputText(
label.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data); label.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data);
if (out.changed) { if (out.changed) {
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str())); std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str()));
OX_ALLOW_UNSAFE_BUFFERS_END
} }
return out; return out;
} }
@ -186,7 +188,9 @@ TextInput<ox::IString<MaxChars>> InputTextWithHint(
out.changed = ImGui::InputTextWithHint( out.changed = ImGui::InputTextWithHint(
label.c_str(), hint.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data); label.c_str(), hint.c_str(), out.text.data(), MaxChars + 1, flags, callback, user_data);
if (out.changed) { if (out.changed) {
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str())); std::ignore = out.text.unsafeResize(ox::strlen(out.text.c_str()));
OX_ALLOW_UNSAFE_BUFFERS_END
} }
return out; return out;
} }
@ -201,7 +205,9 @@ bool InputText(
auto const out = ImGui::InputText( auto const out = ImGui::InputText(
label.c_str(), text.data(), StrCap + 1, flags, callback, user_data); label.c_str(), text.data(), StrCap + 1, flags, callback, user_data);
if (out) { if (out) {
OX_ALLOW_UNSAFE_BUFFERS_BEGIN
std::ignore = text.unsafeResize(ox::strlen(text.c_str())); std::ignore = text.unsafeResize(ox::strlen(text.c_str()));
OX_ALLOW_UNSAFE_BUFFERS_END
} }
return out; return out;
} }
@ -223,6 +229,10 @@ PopupResponse PopupControlsOkCancel(
ox::CStringViewCR ok = "OK", ox::CStringViewCR ok = "OK",
ox::CStringViewCR cancel = "Cancel"); ox::CStringViewCR cancel = "Cancel");
PopupResponse PopupControlsOk(
bool &popupOpen,
ox::CStringViewCR ok);
[[nodiscard]] [[nodiscard]]
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0}); bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0});
@ -250,7 +260,7 @@ bool ComboBox(ox::CStringView lbl, ox::Span<const ox::String> list, size_t &sele
/** /**
* *
* @param lbl * @param lbl
* @param callback * @param f callback function
* @param selectedIdx * @param selectedIdx
* @return true if new value selected, false otherwise * @return true if new value selected, false otherwise
*/ */
@ -285,7 +295,7 @@ bool ListBox(ox::CStringViewCR name, ox::SpanView<ox::String> const&list, size_t
class FilePicker { class FilePicker {
private: private:
bool m_show{}; bool m_show{};
studio::StudioContext &m_sctx; StudioContext &m_sctx;
ox::String const m_title; ox::String const m_title;
ox::String const m_fileExt; ox::String const m_fileExt;
ImVec2 const m_size; ImVec2 const m_size;
@ -304,8 +314,8 @@ class FilePicker {
}; };
class QuestionPopup: public Widget { class Popup: public Widget {
private: protected:
enum class Stage { enum class Stage {
Closed, Closed,
Opening, Opening,
@ -314,12 +324,11 @@ class QuestionPopup: public Widget {
Stage m_stage = Stage::Closed; Stage m_stage = Stage::Closed;
bool m_open{}; bool m_open{};
ox::String m_title; ox::String m_title;
ox::String m_question;
public: public:
ox::Signal<ox::Error(PopupResponse)> response; ox::Signal<ox::Error(PopupResponse)> response;
QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept; explicit Popup(ox::StringParam title) noexcept;
void open() noexcept; void open() noexcept;
@ -328,7 +337,33 @@ class QuestionPopup: public Widget {
[[nodiscard]] [[nodiscard]]
bool isOpen() const noexcept; bool isOpen() const noexcept;
void draw(StudioContext &ctx) noexcept; };
class QuestionPopup: public Popup {
private:
ox::String m_question;
public:
ox::Signal<ox::Error(PopupResponse)> response;
QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept;
void draw(StudioContext &ctx) noexcept override;
};
class MessagePopup: public Popup {
private:
ox::String m_msg;
public:
ox::Signal<ox::Error(PopupResponse)> response;
MessagePopup(ox::StringParam title, ox::StringParam msg) noexcept;
void show(ox::StringParam msg) noexcept;
void draw(StudioContext &ctx) noexcept override;
}; };

View File

@ -88,7 +88,7 @@ PopupResponse PopupControlsOk(
auto out = PopupResponse::None; auto out = PopupResponse::None;
constexpr auto btnSz = ImVec2{50, BtnSz.y}; constexpr auto btnSz = ImVec2{50, BtnSz.y};
ImGui::Separator(); ImGui::Separator();
ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - 101); ImGui::SetCursorPosX(ImGui::GetContentRegionAvail().x - 42);
if (ImGui::Button(ok.c_str(), btnSz)) { if (ImGui::Button(ok.c_str(), btnSz)) {
popupOpen = false; popupOpen = false;
out = PopupResponse::OK; out = PopupResponse::OK;
@ -245,24 +245,28 @@ void FilePicker::show() noexcept {
} }
QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept: Popup::Popup(ox::StringParam title) noexcept: m_title{std::move(title)} {
m_title{std::move(title)},
m_question{std::move(question)} {
} }
void QuestionPopup::open() noexcept { void Popup::open() noexcept {
m_stage = Stage::Opening; m_stage = Stage::Opening;
} }
void QuestionPopup::close() noexcept { void Popup::close() noexcept {
m_stage = Stage::Closed; m_stage = Stage::Closed;
m_open = false; m_open = false;
} }
bool QuestionPopup::isOpen() const noexcept { bool Popup::isOpen() const noexcept {
return m_open; return m_open;
} }
QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept:
Popup{std::move(title)},
m_question{std::move(question)} {
}
void QuestionPopup::draw(StudioContext &ctx) noexcept { void QuestionPopup::draw(StudioContext &ctx) noexcept {
switch (m_stage) { switch (m_stage) {
case Stage::Closed: case Stage::Closed:
@ -298,6 +302,49 @@ void QuestionPopup::draw(StudioContext &ctx) noexcept {
} }
MessagePopup::MessagePopup(ox::StringParam title, ox::StringParam msg) noexcept:
Popup{std::move(title)},
m_msg{std::move(msg)} {
}
void MessagePopup::show(ox::StringParam msg) noexcept {
m_msg = std::move(msg);
open();
}
void MessagePopup::draw(StudioContext &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:
break;
case Stage::Opening:
ImGui::OpenPopup(m_title.c_str());
m_stage = Stage::Open;
m_open = true;
[[fallthrough]];
case Stage::Open:
centerNextWindow(ctx.tctx);
ImGui::SetNextWindowSize({});
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) {
ImGui::Text("%s", m_msg.c_str());
auto const r = PopupControlsOk(m_open, "OK");
switch (r) {
case PopupResponse::None:
break;
case PopupResponse::OK:
response.emit(r);
close();
break;
case PopupResponse::Cancel:
break;
}
ImGui::EndPopup();
}
break;
}
}
bool s_mainWinHasFocus{}; bool s_mainWinHasFocus{};
bool mainWinHasFocus() noexcept { bool mainWinHasFocus() noexcept {
return s_mainWinHasFocus; return s_mainWinHasFocus;