Merge commit '7b7d59cf63d77cf7ab6daf6ed7122eef97954555'

This commit is contained in:
Gary Talent 2025-01-19 13:39:31 -06:00
commit 89d543bcbc
30 changed files with 521 additions and 120 deletions

View File

@ -5,23 +5,27 @@ BUILDCORE_PATH=deps/buildcore
include ${BUILDCORE_PATH}/base.mk
ifeq ($(BC_VAR_OS),darwin)
NOSTALGIA_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio.app/Contents/MacOS/${BC_VAR_PROJECT_NAME_CAP}Studio
PROJECT_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio.app/Contents/MacOS/${BC_VAR_PROJECT_NAME_CAP}Studio
MGBA=/Applications/mGBA.app/Contents/MacOS/mGBA
else
NOSTALGIA_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio
PROJECT_STUDIO=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}Studio
MGBA=mgba-qt
endif
PROJECT_PLAYER=./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME_CAP}
.PHONY: pkg-gba
pkg-gba: build
${BC_CMD_ENVRUN} ${BC_PY3} ./util/scripts/pkg-gba.py sample_project ${BC_VAR_PROJECT_NAME}
.PHONY: build-player
build-player:
${BC_CMD_CMAKE_BUILD} ${BC_VAR_BUILD_PATH} ${BC_VAR_PROJECT_NAME_CAP}
.PHONY: run
run: build
./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME} sample_project
run: build-player
${PROJECT_PLAYER} sample_project
.PHONY: run-studio
run-studio: build
${NOSTALGIA_STUDIO}
${PROJECT_STUDIO}
.PHONY: gba-run
gba-run: pkg-gba
${MGBA} ${BC_VAR_PROJECT_NAME}.gba
@ -30,7 +34,7 @@ debug: build
${BC_CMD_HOST_DEBUGGER} ./build/${BC_VAR_CURRENT_BUILD}/bin/${BC_VAR_PROJECT_NAME} sample_project
.PHONY: debug-studio
debug-studio: build
${BC_CMD_HOST_DEBUGGER} ${NOSTALGIA_STUDIO}
${BC_CMD_HOST_DEBUGGER} ${PROJECT_STUDIO}
.PHONY: configure-gba
configure-gba:

View File

@ -63,18 +63,6 @@ Error FileSystem::read(const FileAddress &addr, std::size_t readStart, std::size
}
}
Error FileSystem::remove(const FileAddress &addr, bool recursive) noexcept {
switch (addr.type()) {
case FileAddressType::Inode:
return remove(addr.getInode().value, recursive);
case FileAddressType::ConstPath:
case FileAddressType::Path:
return remove(StringView(addr.getPath().value), recursive);
default:
return ox::Error(1);
}
}
Error FileSystem::write(const FileAddress &addr, const void *buffer, uint64_t size, FileType fileType) noexcept {
switch (addr.type()) {
case FileAddressType::Inode:

View File

@ -57,9 +57,9 @@ class FileSystem {
virtual Result<Vector<String>> ls(StringViewCR dir) const noexcept = 0;
virtual Error remove(StringViewCR path, bool recursive) noexcept = 0;
Error remove(const FileAddress &addr, bool recursive = false) noexcept;
Error remove(StringViewCR path, bool recursive = false) noexcept {
return removePath(path, recursive);
}
virtual Error resize(uint64_t size, void *buffer) noexcept = 0;
@ -142,6 +142,8 @@ class FileSystem {
virtual Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept = 0;
virtual Error removePath(StringViewCR path, bool recursive) noexcept = 0;
virtual Error writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType fileType) noexcept = 0;
virtual Error writeFileInode(uint64_t inode, const void *buffer, uint64_t size, FileType fileType) noexcept = 0;
@ -209,6 +211,8 @@ class FileSystemTemplate: public MemFS {
Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override;
Error removePath(StringViewCR path, bool recursive) noexcept override;
Result<const char*> directAccessInode(uint64_t) const noexcept override;
Result<Vector<String>> ls(StringViewCR dir) const noexcept override;
@ -216,8 +220,6 @@ class FileSystemTemplate: public MemFS {
template<typename F>
Error ls(StringViewCR path, F cb) const;
Error remove(StringViewCR path, bool recursive) noexcept override;
/**
* Resizes FileSystem to minimum possible size.
*/
@ -356,6 +358,25 @@ Error FileSystemTemplate<FileStore, Directory>::readFileInodeRange(uint64_t inod
return m_fs.read(inode, readStart, readSize, reinterpret_cast<uint8_t*>(buffer), size);
}
template<typename FileStore, typename Directory>
Error FileSystemTemplate<FileStore, Directory>::removePath(StringViewCR path, bool recursive) noexcept {
OX_REQUIRE(fd, fileSystemData());
Directory rootDir(m_fs, fd.rootDirInode);
OX_REQUIRE(inode, rootDir.find(path));
OX_REQUIRE(st, statInode(inode));
if (st.fileType == FileType::NormalFile || recursive) {
if (auto err = rootDir.remove(path)) {
// removal failed, try putting the index back
oxLogError(rootDir.write(path, inode));
return err;
}
} else {
oxTrace("FileSystemTemplate.remove.fail", "Tried to remove directory without recursive setting.");
return ox::Error(1);
}
return ox::Error(0);
}
template<typename FileStore, typename Directory>
Result<const char*> FileSystemTemplate<FileStore, Directory>::directAccessInode(uint64_t inode) const noexcept {
auto data = m_fs.read(inode);
@ -384,25 +405,6 @@ Error FileSystemTemplate<FileStore, Directory>::ls(StringViewCR path, F cb) cons
return dir.ls(cb);
}
template<typename FileStore, typename Directory>
Error FileSystemTemplate<FileStore, Directory>::remove(StringViewCR path, bool recursive) noexcept {
OX_REQUIRE(fd, fileSystemData());
Directory rootDir(m_fs, fd.rootDirInode);
OX_REQUIRE(inode, rootDir.find(path));
OX_REQUIRE(st, statInode(inode));
if (st.fileType == FileType::NormalFile || recursive) {
if (auto err = rootDir.remove(path)) {
// removal failed, try putting the index back
oxLogError(rootDir.write(path, inode));
return err;
}
} else {
oxTrace("FileSystemTemplate.remove.fail", "Tried to remove directory without recursive setting.");
return ox::Error(1);
}
return ox::Error(0);
}
template<typename FileStore, typename Directory>
Error FileSystemTemplate<FileStore, Directory>::resize() noexcept {
return m_fs.resize();

View File

@ -75,14 +75,6 @@ Result<Vector<String>> PassThroughFS::ls(StringViewCR dir) const noexcept {
return out;
}
Error PassThroughFS::remove(StringViewCR path, bool recursive) noexcept {
if (recursive) {
return ox::Error(std::filesystem::remove_all(m_path / stripSlash(path)) != 0);
} else {
return ox::Error(std::filesystem::remove(m_path / stripSlash(path)) != 0);
}
}
Error PassThroughFS::resize(uint64_t, void*) noexcept {
// unsupported
return ox::Error(1, "resize is not supported by PassThroughFS");
@ -167,6 +159,14 @@ Error PassThroughFS::readFileInodeRange(uint64_t, std::size_t, std::size_t, void
return ox::Error(1, "read(uint64_t, std::size_t, std::size_t, void*, std::size_t*) is not supported by PassThroughFS");
}
Error PassThroughFS::removePath(StringViewCR path, bool const recursive) noexcept {
if (recursive) {
return ox::Error{std::filesystem::remove_all(m_path / stripSlash(path)) == 0};
} else {
return ox::Error{!std::filesystem::remove(m_path / stripSlash(path))};
}
}
Error PassThroughFS::writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType) noexcept {
const auto p = (m_path / stripSlash(path));
try {

View File

@ -45,8 +45,6 @@ class PassThroughFS: public FileSystem {
template<typename F>
Error ls(StringViewCR dir, F cb) const noexcept;
Error remove(StringViewCR path, bool recursive) noexcept override;
Error resize(uint64_t size, void *buffer) noexcept override;
Result<FileStat> statInode(uint64_t inode) const noexcept override;
@ -75,6 +73,8 @@ class PassThroughFS: public FileSystem {
Error readFileInodeRange(uint64_t inode, std::size_t readStart, std::size_t readSize, void *buffer, std::size_t *size) noexcept override;
Error removePath(StringViewCR path, bool recursive) noexcept override;
Error writeFilePath(StringViewCR path, const void *buffer, uint64_t size, FileType fileType) noexcept override;
Error writeFileInode(uint64_t inode, const void *buffer, uint64_t size, FileType fileType) noexcept override;

View File

@ -58,9 +58,9 @@ class AnyPtrT {
template<typename T>
constexpr AnyPtrT(T *ptr) noexcept {
if (std::is_constant_evaluated()) {
m_wrapPtr = new Wrap(ptr);
m_wrapPtr = new Wrap<T>(ptr);
} else {
m_wrapPtr = new(m_wrapData.data()) Wrap(ptr);
m_wrapPtr = new(m_wrapData.data()) Wrap<T>(ptr);
}
}

View File

@ -423,9 +423,10 @@ constexpr BasicString<SmallStringSize_v> BasicString<SmallStringSize_v>::operato
const std::size_t strLen = src.len();
const auto currentLen = len();
BasicString<SmallStringSize_v> cpy(currentLen + strLen);
cpy.m_buff.resize(m_buff.size() + strLen);
cpy.m_buff.resize(m_buff.size() + strLen + 1);
ox::listcpy(&cpy.m_buff[0], m_buff.data(), currentLen);
ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen + 1);
ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen);
cpy.m_buff[cpy.m_buff.size() - 1] = 0;
return cpy;
}
@ -436,7 +437,8 @@ constexpr BasicString<SmallStringSize_v> BasicString<SmallStringSize_v>::operato
BasicString<SmallStringSize_v> cpy(currentLen + strLen);
cpy.m_buff.resize(m_buff.size() + strLen);
ox::listcpy(&cpy.m_buff[0], m_buff.data(), currentLen);
ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen + 1);
ox::listcpy(&cpy.m_buff[currentLen], src.data(), strLen);
cpy.m_buff[cpy.m_buff.size() - 1] = 0;
return cpy;
}

View File

@ -443,6 +443,16 @@ void TileSheetEditorImGui::drawPaletteMenu() noexcept {
if (ig::ComboBox("Palette", files, m_selectedPaletteIdx)) {
oxLogError(m_model.setPalette(files[m_selectedPaletteIdx]));
}
if (ig::DragDropTarget const dragDropTarget; dragDropTarget) {
auto const [ref, err] = ig::getDragDropPayload<studio::FileRef>("FileRef");
if (!err) {
auto const oldVal = m_selectedPaletteIdx;
std::ignore = ox::findIdx(files.begin(), files.end(), ref.path).moveTo(m_selectedPaletteIdx);
if (oldVal != m_selectedPaletteIdx) {
oxLogError(m_model.setPalette(files[m_selectedPaletteIdx]));
}
}
}
auto const pages = m_model.pal().pages.size();
if (pages > 1) {
ig::IndentStackItem const indentStackItem{20};

View File

@ -1,26 +1,26 @@
add_executable(
nostalgia WIN32
Nostalgia WIN32
app.cpp
)
# enable LTO
if(NOT WIN32)
set_property(TARGET nostalgia PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
set_property(TARGET Nostalgia PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
if(COMMAND OBJCOPY_FILE)
set_target_properties(nostalgia
set_target_properties(Nostalgia
PROPERTIES
LINK_FLAGS ${LINKER_FLAGS}
COMPILER_FLAGS "-mthumb -mthumb-interwork"
)
OBJCOPY_FILE(nostalgia)
#PADBIN_FILE(nostalgia)
OBJCOPY_FILE(Nostalgia)
#PADBIN_FILE(Nostalgia)
endif()
target_link_libraries(
nostalgia
Nostalgia
NostalgiaKeelModules
NostalgiaProfile
OlympicApplib
@ -29,7 +29,7 @@ target_link_libraries(
install(
TARGETS
nostalgia
Nostalgia
DESTINATION
bin
)

View File

@ -1,8 +0,0 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <ox/std/memory.hpp>
#include <ox/fs/fs.hpp>
typename ox::Error run(ox::UniquePtr<ox::FileSystem> &&fs) noexcept;

View File

@ -2,8 +2,10 @@ add_library(
StudioAppLib
aboutpopup.cpp
clawviewer.cpp
deleteconfirmation.cpp
filedialogmanager.cpp
main.cpp
newdir.cpp
newmenu.cpp
newproject.cpp
projectexplorer.cpp

View File

@ -0,0 +1,55 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/imguiutil.hpp>
#include "deleteconfirmation.hpp"
namespace studio {
DeleteConfirmation::DeleteConfirmation() noexcept {
setTitle("Delete Item");
}
void DeleteConfirmation::openPath(ox::StringViewCR path) noexcept {
open();
m_path = path;
}
void DeleteConfirmation::open() noexcept {
m_path = "";
m_stage = Stage::Opening;
}
void DeleteConfirmation::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool DeleteConfirmation::isOpen() const noexcept {
return m_open;
}
void DeleteConfirmation::draw(StudioContext &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:
break;
case Stage::Opening:
ImGui::OpenPopup(title().c_str());
m_stage = Stage::Open;
m_open = true;
[[fallthrough]];
case Stage::Open:
drawWindow(ctx.tctx, m_open, [this] {
ImGui::Text("Are you sure you want to delete %s?", m_path.c_str());
if (ig::PopupControlsOkCancel(m_open, "Yes", "No") != ig::PopupResponse::None) {
deleteFile.emit(m_path);
close();
}
});
break;
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/context.hpp>
#include <studio/popup.hpp>
namespace studio {
class DeleteConfirmation final: public Popup {
private:
enum class Stage {
Closed,
Opening,
Open,
};
Stage m_stage = Stage::Closed;
bool m_open{};
ox::String m_path;
public:
ox::Signal<ox::Error(ox::StringViewCR path)> deleteFile;
DeleteConfirmation() noexcept;
void openPath(ox::StringViewCR path) noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(StudioContext &ctx) noexcept override;
};
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#include <studio/imguiutil.hpp>
#include "newdir.hpp"
namespace studio {
NewDir::NewDir() noexcept {
setTitle("New Directory");
setSize({280, 0});
}
void NewDir::openPath(ox::StringViewCR path) noexcept {
open();
m_path = path;
}
void NewDir::open() noexcept {
m_path = "";
m_stage = Stage::Opening;
}
void NewDir::close() noexcept {
m_stage = Stage::Closed;
m_open = false;
}
bool NewDir::isOpen() const noexcept {
return m_open;
}
void NewDir::draw(StudioContext &ctx) noexcept {
switch (m_stage) {
case Stage::Closed:
break;
case Stage::Opening:
ImGui::OpenPopup(title().c_str());
m_open = true;
[[fallthrough]];
case Stage::Open:
drawWindow(ctx.tctx, m_open, [this] {
if (m_stage == Stage::Opening) {
ImGui::SetKeyboardFocusHere();
}
ig::InputText("Name", m_str);
if (ImGui::IsItemFocused() && ImGui::IsKeyPressed(ImGuiKey_Enter)) {
newDir.emit(m_path + "/" + m_str);
close();
}
if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) {
newDir.emit(m_path + "/" + m_str);
close();
}
});
m_stage = Stage::Open;
break;
}
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <studio/context.hpp>
#include <studio/popup.hpp>
namespace studio {
class NewDir final: public Popup {
private:
enum class Stage {
Closed,
Opening,
Open,
};
Stage m_stage = Stage::Closed;
bool m_open{};
ox::String m_path;
ox::IString<255> m_str;
public:
ox::Signal<ox::Error(ox::StringViewCR path)> newDir;
NewDir() noexcept;
void openPath(ox::StringViewCR path) noexcept;
void open() noexcept override;
void close() noexcept override;
[[nodiscard]]
bool isOpen() const noexcept override;
void draw(StudioContext &ctx) noexcept override;
[[nodiscard]]
constexpr ox::CStringView value() const noexcept {
return m_str;
}
};
}

View File

@ -22,6 +22,12 @@ void NewMenu::open() noexcept {
m_selectedType = 0;
m_itemName = "";
m_typeName = "";
m_path = "";
}
void NewMenu::openPath(ox::StringParam path) noexcept {
open();
m_path = std::move(path);
}
void NewMenu::close() noexcept {
@ -158,7 +164,7 @@ void NewMenu::finish(StudioContext &sctx) noexcept {
oxLogError(ox::Error{1, "New file error: no file name"});
return;
}
auto const&im = *m_types[static_cast<std::size_t>(m_selectedType)];
auto const&im = *m_types[m_selectedType];
if (sctx.project->exists(im.itemPath(m_itemName))) {
oxLogError(ox::Error{1, "New file error: file already exists"});
return;

View File

@ -30,6 +30,7 @@ class NewMenu final: public Popup {
Stage m_stage = Stage::Closed;
ox::String m_typeName;
ox::IString<255> m_itemName;
ox::String m_path;
ox::Vector<ox::UPtr<studio::ItemMaker>> m_types;
size_t m_selectedType = 0;
size_t m_selectedTemplate = 0;
@ -38,6 +39,8 @@ class NewMenu final: public Popup {
public:
NewMenu() noexcept;
void openPath(ox::StringParam path) noexcept;
void open() noexcept override;
void close() noexcept override;

View File

@ -12,12 +12,12 @@ namespace studio {
static ox::Result<ox::UniquePtr<ProjectTreeModel>> buildProjectTreeModel(
ProjectExplorer &explorer,
ox::StringView name,
ox::StringParam name,
ox::StringView path,
ProjectTreeModel *parent) noexcept {
auto const fs = explorer.romFs();
OX_REQUIRE(stat, fs->stat(path));
auto out = ox::make_unique<ProjectTreeModel>(explorer, ox::String(name), parent);
auto out = ox::make_unique<ProjectTreeModel>(explorer, std::move(name), parent);
if (stat.fileType == ox::FileType::Directory) {
OX_REQUIRE_M(children, fs->ls(path));
std::sort(children.begin(), children.end());
@ -37,7 +37,7 @@ static ox::Result<ox::UniquePtr<ProjectTreeModel>> buildProjectTreeModel(
ProjectExplorer::ProjectExplorer(turbine::Context &ctx) noexcept: m_ctx(ctx) {
}
void ProjectExplorer::draw(studio::StudioContext &ctx) noexcept {
void ProjectExplorer::draw(StudioContext &ctx) noexcept {
auto const viewport = ImGui::GetContentRegionAvail();
ImGui::BeginChild("ProjectExplorer", ImVec2(300, viewport.y), true);
ImGui::SetNextItemOpen(true);
@ -54,7 +54,7 @@ void ProjectExplorer::setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept {
ox::Error ProjectExplorer::refreshProjectTreeModel(ox::StringViewCR) noexcept {
OX_REQUIRE_M(model, buildProjectTreeModel(*this, "Project", "/", nullptr));
setModel(std::move(model));
return ox::Error(0);
return {};
}
}

View File

@ -12,27 +12,31 @@
namespace studio {
class ProjectExplorer: public studio::Widget {
class ProjectExplorer: public Widget {
private:
ox::UPtr<ProjectTreeModel> m_treeModel;
turbine::Context &m_ctx;
public:
// slots
ox::Signal<ox::Error(ox::StringViewCR)> fileChosen;
ox::Signal<ox::Error(ox::StringViewCR)> addItem;
ox::Signal<ox::Error(ox::StringViewCR)> addDir;
ox::Signal<ox::Error(ox::StringViewCR)> deleteItem;
explicit ProjectExplorer(turbine::Context &ctx) noexcept;
void draw(studio::StudioContext &ctx) noexcept override;
void draw(StudioContext &ctx) noexcept override;
void setModel(ox::UPtr<ProjectTreeModel> &&model) noexcept;
ox::Error refreshProjectTreeModel(ox::StringViewCR = {}) noexcept;
[[nodiscard]]
inline ox::FileSystem *romFs() noexcept {
ox::FileSystem *romFs() noexcept {
return rom(m_ctx);
}
// slots
public:
ox::Signal<ox::Error(ox::StringView const&)> fileChosen;
};
}

View File

@ -4,13 +4,18 @@
#include <imgui.h>
#include <studio/dragdrop.hpp>
#include <studio/imguiutil.hpp>
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace studio {
ProjectTreeModel::ProjectTreeModel(ProjectExplorer &explorer, ox::String name,
ProjectTreeModel *parent) noexcept:
ProjectTreeModel::ProjectTreeModel(
ProjectExplorer &explorer,
ox::StringParam name,
ProjectTreeModel *parent) noexcept:
m_explorer(explorer),
m_parent(parent),
m_name(std::move(name)) {
@ -23,14 +28,18 @@ ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept:
m_children(std::move(other.m_children)) {
}
void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
void ProjectTreeModel::draw(turbine::Context &tctx) const noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (!m_children.empty()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
drawDirContextMenu();
for (auto const&child : m_children) {
child->draw(ctx);
child->draw(tctx);
}
ImGui::TreePop();
} else {
ig::IDStackItem const idStackItem{m_name};
drawDirContextMenu();
}
} else {
auto const path = fullPath();
@ -39,7 +48,17 @@ void ProjectTreeModel::draw(turbine::Context &ctx) const noexcept {
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
m_explorer.fileChosen.emit(path);
}
if (ImGui::BeginPopupContextItem("FileMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Delete")) {
m_explorer.deleteItem.emit(path);
}
ImGui::EndPopup();
}
ImGui::TreePop();
std::ignore = ig::dragDropSource([this] {
ImGui::Text("%s", m_name.c_str());
return ig::setDragDropPayload("FileRef", FileRef{fullPath<ox::String>()});
});
}
}
}
@ -48,11 +67,19 @@ void ProjectTreeModel::setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> childr
m_children = std::move(children);
}
ox::BasicString<255> ProjectTreeModel::fullPath() const noexcept {
if (m_parent) {
return m_parent->fullPath() + "/" + ox::StringView(m_name);
}
return {};
void ProjectTreeModel::drawDirContextMenu() const noexcept {
if (ImGui::BeginPopupContextItem("DirMenu", ImGuiPopupFlags_MouseButtonRight)) {
if (ImGui::MenuItem("Add Item")) {
m_explorer.addItem.emit(fullPath());
}
if (ImGui::MenuItem("Add Directory")) {
m_explorer.addDir.emit(fullPath());
}
if (ImGui::MenuItem("Delete")) {
m_explorer.deleteItem.emit(fullPath());
}
ImGui::EndPopup();
}
}
}

View File

@ -17,19 +17,30 @@ class ProjectTreeModel {
ProjectTreeModel *m_parent = nullptr;
ox::String m_name;
ox::Vector<ox::UPtr<ProjectTreeModel>> m_children;
public:
explicit ProjectTreeModel(class ProjectExplorer &explorer, ox::String name,
ProjectTreeModel *parent = nullptr) noexcept;
explicit ProjectTreeModel(
ProjectExplorer &explorer, ox::StringParam name,
ProjectTreeModel *parent = nullptr) noexcept;
ProjectTreeModel(ProjectTreeModel &&other) noexcept;
void draw(turbine::Context &ctx) const noexcept;
void draw(turbine::Context &tctx) const noexcept;
void setChildren(ox::Vector<ox::UPtr<ProjectTreeModel>> children) noexcept;
private:
void drawDirContextMenu() const noexcept;
template<typename Str = ox::BasicString<255>>
[[nodiscard]]
ox::BasicString<255> fullPath() const noexcept;
Str fullPath() const noexcept {
if (m_parent) {
return m_parent->fullPath<Str>() + "/" + m_name;
}
return {};
}
};
}

View File

@ -52,6 +52,9 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
m_aboutPopup(m_tctx) {
turbine::setApplicationData(m_tctx, &m_sctx);
m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
m_projectExplorer.addDir.connect(this, &StudioUI::addDir);
m_projectExplorer.addItem.connect(this, &StudioUI::addFile);
m_projectExplorer.deleteItem.connect(this, &StudioUI::deleteFile);
m_newProject.finished.connect(this, &StudioUI::createOpenProject);
m_newMenu.finished.connect(this, &StudioUI::openFile);
ImGui::GetIO().IniFilename = nullptr;
@ -342,6 +345,21 @@ void StudioUI::handleKeyInput() noexcept {
}
}
ox::Error StudioUI::addDir(ox::StringViewCR path) noexcept {
m_newDirDialog.openPath(path);
return {};
}
ox::Error StudioUI::addFile(ox::StringViewCR path) noexcept {
m_newMenu.openPath(path);
return {};
}
ox::Error StudioUI::deleteFile(ox::StringViewCR path) noexcept {
m_deleteConfirmation.openPath(path);
return {};
}
ox::Error StudioUI::createOpenProject(ox::StringViewCR path) noexcept {
std::error_code ec;
std::filesystem::create_directories(toStdStringView(path), ec);
@ -356,9 +374,11 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
OX_RETURN_ERROR(
ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir)
.moveTo(m_project));
auto const sctx = applicationData<StudioContext>(m_tctx);
sctx->project = m_project.get();
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_newDirDialog.newDir.connect(m_sctx.project, &Project::mkdir);
m_project->dirAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileAdded.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_project->fileDeleted.connect(&m_projectExplorer, &ProjectExplorer::refreshProjectTreeModel);
m_openFiles.clear();

View File

@ -12,7 +12,10 @@
#include <studio/module.hpp>
#include <studio/project.hpp>
#include <studio/task.hpp>
#include "aboutpopup.hpp"
#include "deleteconfirmation.hpp"
#include "newdir.hpp"
#include "newmenu.hpp"
#include "newproject.hpp"
#include "projectexplorer.hpp"
@ -38,12 +41,16 @@ class StudioUI: public ox::SignalHandler {
BaseEditor *m_activeEditor = nullptr;
BaseEditor *m_activeEditorUpdatePending = nullptr;
NewMenu m_newMenu;
DeleteConfirmation m_deleteConfirmation;
NewDir m_newDirDialog;
NewProject m_newProject;
AboutPopup m_aboutPopup;
ox::Array<Popup*, 3> const m_popups = {
ox::Array<Popup*, 5> const m_popups = {
&m_newMenu,
&m_newProject,
&m_aboutPopup
&m_aboutPopup,
&m_deleteConfirmation,
&m_newDirDialog,
};
bool m_showProjectExplorer = true;
@ -83,6 +90,12 @@ class StudioUI: public ox::SignalHandler {
void handleKeyInput() noexcept;
ox::Error addDir(ox::StringViewCR path) noexcept;
ox::Error addFile(ox::StringViewCR path) noexcept;
ox::Error deleteFile(ox::StringViewCR path) noexcept;
ox::Error createOpenProject(ox::StringViewCR path) noexcept;
ox::Error openProjectPath(ox::StringParam path) noexcept;

View File

@ -0,0 +1,22 @@
/*
* Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
*/
#pragma once
#include <ox/std/string.hpp>
#include <ox/model/def.hpp>
namespace studio {
struct FileRef {
static constexpr auto TypeName = "net.drinkingtea.studio.FileRef";
static constexpr auto TypeVersion = 1;
ox::String path;
};
OX_MODEL_BEGIN(FileRef)
OX_MODEL_FIELD(path)
OX_MODEL_END()
}

View File

@ -63,6 +63,7 @@ auto dragDropSource(auto const&cb) noexcept {
if (ig::DragDropSource const tgt; tgt) [[unlikely]] {
return cb();
}
return ox::Error{};
} else {
if (ig::DragDropSource const tgt; tgt) [[unlikely]] {
cb();
@ -175,9 +176,16 @@ enum class PopupResponse {
Cancel,
};
PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen);
PopupResponse PopupControlsOkCancel(
float popupWidth,
bool &popupOpen,
ox::CStringViewCR ok = "OK",
ox::CStringViewCR cancel = "Cancel");
PopupResponse PopupControlsOkCancel(bool &popupOpen);
PopupResponse PopupControlsOkCancel(
bool &popupOpen,
ox::CStringViewCR ok = "OK",
ox::CStringViewCR cancel = "Cancel");
[[nodiscard]]
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz = {285, 0});

View File

@ -128,6 +128,16 @@ class ItemMaker {
return m_templates;
}
[[nodiscard]]
ox::String const&defaultPath() const noexcept {
return m_parentDir;
}
[[nodiscard]]
ox::String itemPath(ox::StringViewCR pName, ox::StringViewCR pPath) const noexcept {
return ox::sfmt("/{}/{}.{}", pPath, pName, m_fileExt);
}
[[nodiscard]]
ox::String itemPath(ox::StringViewCR pName) const noexcept {
return ox::sfmt("/{}/{}.{}", m_parentDir, pName, m_fileExt);
@ -142,12 +152,40 @@ class ItemMaker {
/**
* Returns path of the file created.
* @param ctx
* @param pPath
* @param pTemplateIdx
* @return path of file or error in Result
*/
ox::Error write(
StudioContext &ctx,
ox::StringViewCR pPath,
size_t pTemplateIdx) const noexcept {
return writeItem(ctx, pPath, pTemplateIdx);
}
/**
* Returns path of the file created.
* @param ctx
* @param pPath
* @param pName
* @param pTemplateIdx
* @return path of file or error in Result
*/
virtual ox::Result<ox::String> write(
StudioContext &ctx, ox::StringViewCR pName, size_t pTemplateIdx) const noexcept = 0;
ox::Error write(
StudioContext &ctx,
ox::StringViewCR pPath,
ox::StringViewCR pName,
size_t pTemplateIdx) const noexcept {
auto const path = itemPath(pName, pPath);
return writeItem(ctx, path, pTemplateIdx);
}
protected:
virtual ox::Error writeItem(
StudioContext &ctx,
ox::StringViewCR pPath,
size_t pTemplateIdx) const noexcept = 0;
};
template<typename T>
@ -205,20 +243,19 @@ class ItemMakerT final: public ItemMaker {
return ox::ModelTypeVersion_v<T>;
}
ox::Result<ox::String> write(
StudioContext &sctx,
ox::StringViewCR pName,
ox::Error writeItem(
StudioContext &ctx,
ox::StringViewCR pPath,
size_t const pTemplateIdx) const noexcept override {
auto const path = itemPath(pName);
createUuidMapping(keelCtx(sctx.tctx), path, ox::UUID::generate().unwrap());
createUuidMapping(keelCtx(ctx.tctx), pPath, ox::UUID::generate().unwrap());
auto const&templates = itemTemplates();
auto const tmplIdx = pTemplateIdx < templates.size() ? pTemplateIdx : 0;
OX_REQUIRE_M(tmpl, templates[tmplIdx]->getItem(
keelCtx(sctx), typeName(), typeVersion()));
keelCtx(ctx), typeName(), typeVersion()));
auto item = tmpl.template get<T>();
OX_RETURN_ERROR(sctx.project->writeObj(path, *item, m_fmt));
OX_RETURN_ERROR(ctx.project->writeObj(pPath, *item, m_fmt));
tmpl.free();
return path;
return {};
}
};

View File

@ -19,6 +19,7 @@ namespace studio {
enum class ProjectEvent {
None,
DirAdded,
FileAdded,
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a file.
@ -45,7 +46,7 @@ constexpr ox::StringView parentDir(ox::StringView path) noexcept {
return substr(path, 0, extStart);
}
class Project {
class Project: public ox::SignalHandler {
private:
ox::SmallMap<ox::String, ox::Optional<ox::ClawFormat>> m_typeFmt;
keel::Context &m_ctx;
@ -91,6 +92,8 @@ class Project {
ox::Result<ox::FileStat> stat(ox::StringViewCR path) const noexcept;
ox::Error deleteItem(ox::StringViewCR path) noexcept;
[[nodiscard]]
bool exists(ox::StringViewCR path) const noexcept;
@ -118,6 +121,7 @@ class Project {
// signals
public:
ox::Signal<ox::Error(ProjectEvent, ox::StringViewCR)> fileEvent;
ox::Signal<ox::Error(ox::StringViewCR)> dirAdded;
ox::Signal<ox::Error(ox::StringViewCR)> fileAdded;
// FileRecognized is triggered for all matching files upon a new
// subscription to a section of the project and upon the addition of a
@ -175,6 +179,9 @@ ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&s
switch (e) {
case ProjectEvent::None:
break;
case ProjectEvent::DirAdded:
connect(this, &Project::dirAdded, tgt, slot);
break;
case ProjectEvent::FileAdded:
connect(this, &Project::fileAdded, tgt, slot);
break;

View File

@ -6,6 +6,7 @@
#include <studio/configio.hpp>
#include <studio/context.hpp>
#include <studio/dragdrop.hpp>
#include <studio/editor.hpp>
#include <studio/filedialog.hpp>
#include <studio/imguiutil.hpp>

View File

@ -54,18 +54,22 @@ bool PushButton(ox::CStringViewCR lbl, ImVec2 const&btnSz) noexcept {
return ImGui::Button(lbl.c_str(), btnSz);
}
PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) {
PopupResponse PopupControlsOkCancel(
float popupWidth,
bool &popupOpen,
ox::CStringViewCR ok,
ox::CStringViewCR cancel) {
auto out = PopupResponse::None;
constexpr auto btnSz = ImVec2{50, BtnSz.y};
ImGui::Separator();
ImGui::SetCursorPosX(popupWidth - 118);
if (ImGui::Button("OK", btnSz)) {
if (ImGui::Button(ok.c_str(), btnSz)) {
ImGui::CloseCurrentPopup();
popupOpen = false;
out = PopupResponse::OK;
}
ImGui::SameLine();
if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button("Cancel", btnSz)) {
if (ImGui::IsKeyDown(ImGuiKey_Escape) || ImGui::Button(cancel.c_str(), btnSz)) {
ImGui::CloseCurrentPopup();
popupOpen = false;
out = PopupResponse::Cancel;
@ -73,8 +77,11 @@ PopupResponse PopupControlsOkCancel(float popupWidth, bool &popupOpen) {
return out;
}
PopupResponse PopupControlsOkCancel(bool &popupOpen) {
return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen);
PopupResponse PopupControlsOkCancel(
bool &popupOpen,
ox::CStringViewCR ok,
ox::CStringViewCR cancel) {
return PopupControlsOkCancel(ImGui::GetContentRegionAvail().x + 17, popupOpen, ok, cancel);
}
bool BeginPopup(turbine::Context &ctx, ox::CStringViewCR popupName, bool &show, ImVec2 const&sz) {

View File

@ -59,16 +59,41 @@ ox::Error Project::mkdir(ox::StringViewCR path) const noexcept {
auto const [stat, err] = m_fs.stat(path);
if (err) {
OX_RETURN_ERROR(m_fs.mkdir(path, true));
fileUpdated.emit(path, {});
dirAdded.emit(path);
}
return stat.fileType == ox::FileType::Directory ?
ox::Error{} : ox::Error(1, "path exists as normal file");
ox::Error{} : ox::Error{1, "path exists as normal file"};
}
ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept {
return m_fs.stat(path);
}
ox::Error Project::deleteItem(ox::StringViewCR path) noexcept {
OX_REQUIRE(stat, m_fs.stat(path));
if (stat.fileType == ox::FileType::Directory) {
bool partialRemoval{};
OX_REQUIRE(members, m_fs.ls(path));
for (auto const&p : members) {
partialRemoval = m_fs.remove(ox::sfmt("{}/{}", path, p)) || partialRemoval;
}
if (partialRemoval) {
return ox::Error{1, "failed to remove one or more directory members"};
}
auto const err = m_fs.remove(path);
if (!err) {
fileDeleted.emit(path);
}
return err;
} else {
auto const err = m_fs.remove(path);
if (!err) {
fileDeleted.emit(path);
}
return err;
}
}
bool Project::exists(ox::StringViewCR path) const noexcept {
return m_fs.stat(path).error == 0;
}