Merge commit 'a6b9657268eb3fe139b0c22df27c2cb2efc0013c'

This commit is contained in:
2025-02-19 00:34:26 -06:00
43 changed files with 817 additions and 72 deletions

View File

@ -5,6 +5,7 @@ add_library(
deleteconfirmation.cpp
filedialogmanager.cpp
main.cpp
makecopypopup.cpp
newdir.cpp
newmenu.cpp
newproject.cpp

View File

@ -0,0 +1,81 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include "makecopypopup.hpp"
namespace studio {
ox::Error MakeCopyPopup::open(ox::StringViewCR path) noexcept {
m_stage = Stage::Opening;
OX_REQUIRE(idx, ox::findIdx(path.rbegin(), path.rend(), '/'));
m_srcPath = path;
m_dirPath = substr(path, 0, idx + 1);
m_title = sfmt("Copy {}", path);
m_fileName = "";
m_errMsg = "";
return {};
}
void MakeCopyPopup::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool MakeCopyPopup::isOpen() const noexcept {
return m_open;
}
void MakeCopyPopup::draw(StudioContext const &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:
ig::centerNextWindow(ctx.tctx);
ImGui::SetNextWindowSize({250, 0});
constexpr auto modalFlags =
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize;
if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) {
if (ImGui::IsWindowAppearing()) {
ImGui::SetKeyboardFocusHere();
}
ig::InputText("Name", m_fileName);
if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter)) {
accept(ctx);
}
ImGui::Text("%s", m_errMsg.c_str());
bool open = true;
switch (ig::PopupControlsOkCancel(open)) {
case ig::PopupResponse::None:
break;
case ig::PopupResponse::OK:
accept(ctx);
break;
case ig::PopupResponse::Cancel:
close();
break;
}
ImGui::EndPopup();
}
break;
}
}
void MakeCopyPopup::accept(StudioContext const &ctx) noexcept {
auto const p = sfmt("{}{}", m_dirPath, m_fileName);
if (!ctx.project->exists(p)) {
makeCopy.emit(m_srcPath, p);
close();
} else {
m_errMsg = sfmt("{} already exists", p);
}
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/context.hpp>
#include <studio/imguiutil.hpp>
namespace studio {
class MakeCopyPopup {
private:
enum class Stage {
Closed,
Opening,
Open,
};
Stage m_stage = Stage::Closed;
bool m_open{};
ox::String m_errMsg;
ox::String m_title{"Copy File"};
ox::String m_srcPath;
ox::String m_dirPath;
ox::IString<255> m_fileName;
public:
ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> makeCopy;
ox::Error open(ox::StringViewCR path) noexcept;
void close() noexcept;
[[nodiscard]]
bool isOpen() const noexcept;
void draw(StudioContext const &ctx) noexcept;
private:
void accept(StudioContext const &ctx) noexcept;
};
}

View File

@ -42,6 +42,9 @@ void ProjectExplorer::fileContextMenu(ox::StringViewCR path) const noexcept {
if (ImGui::MenuItem("Rename")) {
renameItem.emit(path);
}
if (ImGui::MenuItem("Make Copy")) {
makeCopy.emit(path);
}
ImGui::EndPopup();
}
}

View File

@ -20,6 +20,7 @@ class ProjectExplorer final: public FileExplorer {
ox::Signal<ox::Error(ox::StringViewCR)> addDir;
ox::Signal<ox::Error(ox::StringViewCR)> deleteItem;
ox::Signal<ox::Error(ox::StringViewCR)> renameItem;
ox::Signal<ox::Error(ox::StringViewCR)> makeCopy;
ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveDir;
ox::Signal<ox::Error(ox::StringViewCR src, ox::StringViewCR dst)> moveItem;

View File

@ -59,11 +59,13 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
m_projectExplorer.addItem.connect(this, &StudioUI::addFile);
m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile);
m_projectExplorer.renameItem.connect(this, &StudioUI::renameFile);
m_projectExplorer.makeCopy.connect(this, &StudioUI::makeCopyDlg);
m_projectExplorer.moveDir.connect(this, &StudioUI::queueDirMove);
m_projectExplorer.moveItem.connect(this, &StudioUI::queueFileMove);
m_renameFile.moveFile.connect(this, &StudioUI::queueFileMove);
m_newProject.finished.connect(this, &StudioUI::createOpenProject);
m_newMenu.finished.connect(this, &StudioUI::openFile);
m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse);
loadModules();
// open project and files
auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_tctx));
@ -134,6 +136,8 @@ void StudioUI::draw() noexcept {
for (auto const p : m_popups) {
p->draw(m_sctx);
}
m_closeFileConfirm.draw(m_sctx);
m_copyFilePopup.draw(m_sctx);
}
ImGui::End();
handleKeyInput();
@ -214,7 +218,7 @@ void StudioUI::drawTabs() noexcept {
auto open = true;
auto const unsavedChanges = e->unsavedChanges() ? ImGuiTabItemFlags_UnsavedDocument : 0;
auto const selected = m_activeEditorUpdatePending == e.get() ? ImGuiTabItemFlags_SetSelected : 0;
auto const flags = unsavedChanges | selected;
auto const flags = unsavedChanges | selected | ImGuiTabItemFlags_NoAssumedClosure;
if (ImGui::BeginTabItem(e->itemDisplayName().c_str(), &open, flags)) {
if (m_activeEditor != e.get()) [[unlikely]] {
m_activeEditor = e.get();
@ -229,7 +233,10 @@ void StudioUI::drawTabs() noexcept {
if (m_activeEditorOnLastDraw != e.get()) [[unlikely]] {
m_activeEditor->onActivated();
}
if (open) [[likely]] {
if (m_closeActiveTab) [[unlikely]] {
ImGui::SetTabItemClosed(e->itemDisplayName().c_str());
} else if (open) [[likely]] {
e->draw(m_sctx);
}
m_activeEditorOnLastDraw = e.get();
@ -237,21 +244,39 @@ void StudioUI::drawTabs() noexcept {
ImGui::EndTabItem();
}
if (!open) {
e->close();
if (m_activeEditor == (*it).get()) {
m_activeEditor = nullptr;
}
try {
OX_THROW_ERROR(m_editors.erase(it).moveTo(it));
} 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());
if (e->unsavedChanges()) {
m_closeFileConfirm.open();
} else {
e->close();
if (m_activeEditor == (*it).get()) {
m_activeEditor = nullptr;
}
try {
OX_THROW_ERROR(m_editors.erase(it).moveTo(it));
} 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 {
++it;
}
}
if (m_closeActiveTab) [[unlikely]] {
if (m_activeEditor) {
auto const idx = find_if(
m_editors.begin(), m_editors.end(),
[this](ox::UPtr<BaseEditor> const &e) {
return m_activeEditor == e.get();
});
if (idx != m_editors.end()) {
oxLogError(m_editors.erase(idx.offset()).error);
}
m_activeEditor = nullptr;
}
m_closeActiveTab = false;
}
}
void StudioUI::loadEditorMaker(EditorMaker const&editorMaker) noexcept {
@ -328,6 +353,14 @@ void StudioUI::handleKeyInput() noexcept {
if (m_activeEditor && m_activeEditor->pasteEnabled()) {
m_activeEditor->paste();
}
} else if (ImGui::IsKeyPressed(ImGuiKey_W)) {
if (m_activeEditor) {
if (m_activeEditor->unsavedChanges()) {
m_closeFileConfirm.open();
} else {
oxLogError(closeCurrentFile());
}
}
} else if (ImGui::IsKeyPressed(ImGuiKey_X)) {
if (m_activeEditor && m_activeEditor->cutEnabled()) {
m_activeEditor->cut();
@ -393,13 +426,12 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne
}
ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept {
for (size_t i{}; auto &e : m_editors) {
for (auto &e : m_editors) {
if (path == e->itemPath()) {
oxLogError(m_editors.erase(i).error);
oxLogError(closeFile(path));
m_closeActiveTab = true;
break;
}
++i;
}
return m_projectExplorer.refreshProjectTreeModel();
}
@ -421,6 +453,7 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
m_sctx.project = m_project.get();
turbine::setWindowTitle(m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath()));
m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
m_copyFilePopup.makeCopy.connect(m_sctx.project, &Project::copyItem);
m_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);
m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
@ -483,6 +516,28 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
return {};
}
ox::Error StudioUI::makeCopyDlg(ox::StringViewCR path) noexcept {
return m_copyFilePopup.open(path);
}
ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) noexcept {
if (response == ig::PopupResponse::OK && m_activeEditor) {
return closeCurrentFile();
}
return {};
}
ox::Error StudioUI::closeCurrentFile() noexcept {
for (auto &e : m_editors) {
if (m_activeEditor == e.get()) {
oxLogError(closeFile(e->itemPath()));
m_closeActiveTab = true;
break;
}
}
return {};
}
ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept {
if (!m_openFiles.contains(path)) {
return {};

View File

@ -9,12 +9,14 @@
#include <ox/std/string.hpp>
#include <studio/editor.hpp>
#include <studio/imguiutil.hpp>
#include <studio/module.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include "aboutpopup.hpp"
#include "deleteconfirmation.hpp"
#include "makecopypopup.hpp"
#include "newdir.hpp"
#include "newmenu.hpp"
#include "newproject.hpp"
@ -40,11 +42,14 @@ class StudioUI: public ox::SignalHandler {
BaseEditor *m_activeEditorOnLastDraw = nullptr;
BaseEditor *m_activeEditor = nullptr;
BaseEditor *m_activeEditorUpdatePending = nullptr;
bool m_closeActiveTab{};
ox::Vector<ox::Pair<ox::String>> m_queuedMoves;
ox::Vector<ox::Pair<ox::String>> m_queuedDirMoves;
NewMenu m_newMenu{keelCtx(m_tctx)};
DeleteConfirmation m_deleteConfirmation;
NewDir m_newDirDialog;
ig::QuestionPopup m_closeFileConfirm{"Close File?", "This file has unsaved changes. Close?"};
MakeCopyPopup m_copyFilePopup;
RenameFile m_renameFile;
NewProject m_newProject;
AboutPopup m_aboutPopup;
@ -114,6 +119,12 @@ class StudioUI: public ox::SignalHandler {
ox::Error openFileActiveTab(ox::StringViewCR path, bool makeActiveTab) noexcept;
ox::Error makeCopyDlg(ox::StringViewCR path) noexcept;
ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept;
ox::Error closeCurrentFile() noexcept;
ox::Error closeFile(ox::StringViewCR path) noexcept;
ox::Error queueDirMove(ox::StringParam src, ox::StringParam dst) noexcept;

View File

@ -225,6 +225,18 @@ PopupResponse PopupControlsOkCancel(
[[nodiscard]]
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0});
/**
*
* @param lbl
* @param list
* @param selectedIdx
* @return true if new value selected, false otherwise
*/
bool ComboBox(
ox::CStringView lbl,
ox::SpanView<ox::CStringView> list,
size_t &selectedIdx) noexcept;
/**
*
* @param lbl
@ -291,6 +303,34 @@ class FilePicker {
};
class QuestionPopup {
private:
enum class Stage {
Closed,
Opening,
Open,
};
Stage m_stage = Stage::Closed;
bool m_open{};
ox::String m_title;
ox::String m_question;
public:
ox::Signal<ox::Error(ig::PopupResponse)> response;
QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept;
void open() noexcept;
void close() noexcept;
[[nodiscard]]
bool isOpen() const noexcept;
void draw(StudioContext &ctx, ImVec2 const &sz = {}) noexcept;
};
[[nodiscard]]
bool mainWinHasFocus() noexcept;

View File

@ -92,6 +92,8 @@ class Project: public ox::SignalHandler {
ox::Result<ox::FileStat> stat(ox::StringViewCR path) const noexcept;
ox::Error copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;
ox::Error moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept;
ox::Error moveDir(ox::StringViewCR src, ox::StringViewCR dest) noexcept;

View File

@ -90,6 +90,25 @@ bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show,
return ImGui::BeginPopupModal(popupName.c_str(), &show, modalFlags);
}
bool ComboBox(
ox::CStringView lbl,
ox::SpanView<ox::CStringView> list,
size_t &selectedIdx) noexcept {
bool out{};
auto const first = selectedIdx < list.size() ? list[selectedIdx].c_str() : "";
if (ImGui::BeginCombo(lbl.c_str(), first, 0)) {
for (auto i = 0u; i < list.size(); ++i) {
const auto selected = (selectedIdx == i);
if (ImGui::Selectable(list[i].c_str(), selected) && selectedIdx != i) {
selectedIdx = i;
out = true;
}
}
ImGui::EndCombo();
}
return out;
}
bool ComboBox(
ox::CStringView lbl,
ox::Span<const ox::String> list,
@ -206,6 +225,60 @@ void FilePicker::show() noexcept {
m_show = true;
}
QuestionPopup::QuestionPopup(ox::StringParam title, ox::StringParam question) noexcept:
m_title{std::move(title)},
m_question{std::move(question)} {
}
void QuestionPopup::open() noexcept {
m_stage = Stage::Opening;
}
void QuestionPopup::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool QuestionPopup::isOpen() const noexcept {
return m_open;
}
void QuestionPopup::draw(StudioContext &ctx, ImVec2 const &sz) 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(static_cast<ImVec2>(sz));
constexpr auto modalFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize;
if (ImGui::BeginPopupModal(m_title.c_str(), &m_open, modalFlags)) {
ImGui::Text("%s", m_question.c_str());
auto const r = PopupControlsOkCancel(m_open, "Yes", "No");
switch (r) {
case PopupResponse::None:
break;
case PopupResponse::OK:
response.emit(r);
close();
break;
case PopupResponse::Cancel:
response.emit(r);
close();
break;
}
ImGui::EndPopup();
}
break;
}
}
bool s_mainWinHasFocus{};
bool mainWinHasFocus() noexcept {
return s_mainWinHasFocus;

View File

@ -97,6 +97,14 @@ ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept {
return m_fs.stat(path);
}
ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept {
OX_REQUIRE_M(buff, loadBuff(src));
OX_REQUIRE(id, keel::regenerateUuidHeader(buff));
OX_RETURN_ERROR(writeBuff(dest, buff));
createUuidMapping(m_kctx, dest, id);
return {};
}
ox::Error Project::moveItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept {
OX_RETURN_ERROR(m_fs.move(src, dest));
OX_RETURN_ERROR(keel::updatePath(m_kctx, src, dest));

View File

@ -7,9 +7,7 @@
namespace studio {
ox::Error UndoStack::push(ox::UPtr<UndoCommand> &&cmd) noexcept {
for (auto const i = m_stackIdx; i < m_stack.size();) {
std::ignore = m_stack.erase(i);
}
m_stack.resize(m_stackIdx);
OX_RETURN_ERROR(cmd->redo());
redoTriggered.emit(cmd.get());
changeTriggered.emit(cmd.get());
@ -25,22 +23,29 @@ ox::Error UndoStack::push(ox::UPtr<UndoCommand> &&cmd) noexcept {
}
ox::Error UndoStack::redo() noexcept {
if (m_stackIdx < m_stack.size()) {
auto &c = m_stack[m_stackIdx];
OX_RETURN_ERROR(c->redo());
while (m_stackIdx < m_stack.size()) {
auto const &c = m_stack[m_stackIdx];
++m_stackIdx;
redoTriggered.emit(c.get());
changeTriggered.emit(c.get());
if (!c->isObsolete()) {
OX_RETURN_ERROR(c->redo());
redoTriggered.emit(c.get());
changeTriggered.emit(c.get());
break;
}
}
return {};
}
ox::Error UndoStack::undo() noexcept {
if (m_stackIdx) {
auto &c = m_stack[--m_stackIdx];
OX_RETURN_ERROR(c->undo());
undoTriggered.emit(c.get());
changeTriggered.emit(c.get());
while (m_stackIdx) {
--m_stackIdx;
auto const &c = m_stack[m_stackIdx];
if (!c->isObsolete()) {
OX_RETURN_ERROR(c->undo());
undoTriggered.emit(c.get());
changeTriggered.emit(c.get());
break;
}
}
return {};
}