[nostalgia/studio] Start on ImGui version of Studio

This commit is contained in:
Gary Talent 2021-07-26 01:39:56 -05:00
parent 6160335af3
commit ddd63bc45f
46 changed files with 1005 additions and 2269 deletions

View File

@ -19,8 +19,7 @@ if(NOSTALGIA_BUILD_TYPE STREQUAL "Native")
add_subdirectory(userland)
endif()
if(NOSTALGIA_BUILD_STUDIO)
add_subdirectory(qt)
add_subdirectory(studio)
#add_subdirectory(studio)
endif()
install(

View File

@ -28,7 +28,6 @@ constexpr uint16_t DispStat_irq_vcount = 1 << 5;
struct GbaPaletteTarget {
static constexpr auto TypeName = NostalgiaPalette::TypeName;
static constexpr auto Fields = NostalgiaPalette::Fields;
static constexpr auto TypeVersion = NostalgiaPalette::TypeVersion;
volatile uint16_t *palette = nullptr;
};
@ -44,7 +43,7 @@ struct GbaTileMapTarget {
};
template<typename T>
ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept {
constexpr ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept {
io->template setTypeInfo<GbaPaletteTarget>();
const auto colorHandler = [t](std::size_t i, Color16 *c) {
t->palette[i] = *c;
@ -54,8 +53,8 @@ ox::Error modelRead(T *io, GbaPaletteTarget *t) noexcept {
}
template<typename T>
ox::Error modelRead(T *io, GbaTileMapTarget *t) noexcept {
io->template setTypeInfo<GbaTileMapTarget>();
constexpr ox::Error modelRead(T *io, GbaTileMapTarget *t) noexcept {
io->template setTypeInfo<GbaTileMapTarget>(GbaTileMapTarget::TypeName, GbaTileMapTarget::Fields);
uint8_t bpp;
int dummy;
oxReturnError(io->field("bpp", &bpp));

View File

@ -1,22 +0,0 @@
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
add_library(
NostalgiaCore-Qt OBJECT
gfx.cpp
)
target_link_libraries(
NostalgiaCore-Qt PUBLIC
NostalgiaStudio
OxFS
OxStd
)
install(
TARGETS
NostalgiaCore-Qt
LIBRARY DESTINATION
${NOSTALGIA_DIST_LIB}/nostalgia
)

View File

@ -1,44 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "../gfx.hpp"
namespace nostalgia::core {
ox::Error initGfx(Context*) noexcept {
return OxError(1);
}
ox::Error shutdownGfx(Context*) noexcept {
return OxError(1);
}
ox::Error initConsole(Context*) noexcept {
return OxError(1);
}
ox::Error loadBgTileSheet(Context*,
int,
ox::FileAddress,
ox::FileAddress) noexcept {
return OxError(1);
}
void puts(Context *ctx, int column, int row, const char *str) noexcept {
for (int i = 0; str[i]; i++) {
setTile(ctx, 0, column + i, row, static_cast<uint8_t>(charMap[static_cast<int>(str[i])]));
}
}
void setTile(Context*, int, int, int, uint8_t) noexcept {
}
void setSprite(Context*, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned, unsigned) noexcept {
}
}

View File

@ -1,29 +1,27 @@
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
add_executable(
nostalgia-studio MACOSX_BUNDLE
json_read.cpp
json_write.cpp
filedialogmanager.cpp
main.cpp
mainwindow.cpp
oxfstreeview.cpp
studiorsrc.qrc
projectexplorer.cpp
projecttreemodel.cpp
studioapp.cpp
)
target_link_libraries(
nostalgia-studio
QDarkStyle
OxClArgs
NostalgiaCore-Studio
OxFS
NostalgiaCore-Userspace
NostalgiaStudio
NostalgiaPack
)
# enable LTO
set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
if (CMAKE_BUILD_TYPE STREQUAL "Release")
# enable LTO
set_property(TARGET nostalgia-studio PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
if(APPLE)
set_target_properties(nostalgia-studio PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
@ -44,19 +42,4 @@ install(
BUNDLE DESTINATION .
)
add_executable(
NostalgiaStudioJsonTest
json_read.cpp
json_write.cpp
json_test.cpp
)
target_link_libraries(
NostalgiaStudioJsonTest
OxStd
Qt${QT_VERSION_MAJOR}::Widgets
)
add_test("[nostalgia/studio] NostalgiaStudioJson" NostalgiaStudioJsonTest)
add_subdirectory(lib)

View File

@ -0,0 +1,47 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <imgui.h>
#include <nostalgia/core/gfx.hpp>
#include "filedialogmanager.hpp"
namespace nostalgia {
studio::TaskState FileDialogManager::update(core::Context *ctx) noexcept {
switch (m_state) {
case UpdateProjectPathState::EnableSystemCursor: {
// switch to system cursor in this update and open file dialog in the next
// the cursor state has to be set before the ImGui frame starts
auto &io = ImGui::GetIO();
io.MouseDrawCursor = false;
m_state = UpdateProjectPathState::RunFileDialog;
break;
}
case UpdateProjectPathState::RunFileDialog: {
// switch to system cursor
auto [path, err] = studio::chooseDirectory();
// Mac file dialog doesn't restore focus to main window when closed...
core::focusWindow(ctx);
if (!err) {
err = pathChosen.emitCheckError(path);
oxAssert(err, "Path chosen response failed");
}
auto &io = ImGui::GetIO();
io.MouseDrawCursor = true;
m_state = UpdateProjectPathState::None;
return nostalgia::studio::TaskState::Done;
}
case UpdateProjectPathState::None:
break;
}
return nostalgia::studio::TaskState::Running;
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/string.hpp>
#include <nostalgia/core/context.hpp>
#include "lib/filedialog.hpp"
#include "lib/task.hpp"
namespace nostalgia {
class FileDialogManager : public nostalgia::studio::Task {
private:
enum class UpdateProjectPathState {
None,
EnableSystemCursor,
RunFileDialog,
Start = EnableSystemCursor,
} m_state = UpdateProjectPathState::Start;
public:
FileDialogManager() noexcept = default;
template<class... Args>
explicit FileDialogManager(Args ...args) noexcept;
~FileDialogManager() noexcept override = default;
nostalgia::studio::TaskState update(nostalgia::core::Context *ctx) noexcept final;
// signals
ox::Signal<ox::Error(const ox::String &)> pathChosen;
};
template<class... Args>
FileDialogManager::FileDialogManager(Args ...args) noexcept {
pathChosen.connect(args...);
}
}

View File

@ -1,87 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "json_read.hpp"
namespace nostalgia::studio {
JsonReader::JsonReader(QJsonObject &obj): m_src(obj) {
}
ox::Error JsonReader::field(QString fieldName, int *dest) {
if (m_src.contains(fieldName)) {
return field(m_src[fieldName], dest);
} else {
return OxError(JSON_ERR_FIELD_MISSING);
}
}
ox::Error JsonReader::field(QString fieldName, bool *dest) {
if (m_src.contains(fieldName)) {
return field(m_src[fieldName], dest);
} else {
return OxError(JSON_ERR_FIELD_MISSING);
}
}
ox::Error JsonReader::field(QString fieldName, double *dest) {
if (m_src.contains(fieldName)) {
return field(m_src[fieldName], dest);
} else {
return OxError(JSON_ERR_FIELD_MISSING);
}
}
ox::Error JsonReader::field(QString fieldName, QString *dest) {
if (m_src.contains(fieldName)) {
return field(m_src[fieldName], dest);
} else {
return OxError(JSON_ERR_FIELD_MISSING);
}
}
ox::Error JsonReader::field(QJsonValueRef src, int *dest) {
if (src.isDouble()) {
*dest = src.toInt();
return OxError(0);
} else {
return OxError(JSON_ERR_UNEXPECTED_TYPE);
}
}
ox::Error JsonReader::field(QJsonValueRef src, bool *dest) {
if (src.isBool()) {
*dest = src.toBool();
return OxError(0);
} else {
return OxError(JSON_ERR_UNEXPECTED_TYPE);
}
}
ox::Error JsonReader::field(QJsonValueRef src, double *dest) {
if (src.isDouble()) {
*dest = src.toDouble();
return OxError(0);
} else {
return OxError(JSON_ERR_UNEXPECTED_TYPE);
}
}
ox::Error JsonReader::field(QJsonValueRef src, QString *dest) {
if (src.isString()) {
*dest = src.toString();
return OxError(0);
} else {
return OxError(JSON_ERR_UNEXPECTED_TYPE);
}
}
}

View File

@ -1,105 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <QDebug>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <ox/std/error.hpp>
#include "json_err.hpp"
namespace nostalgia::studio {
class JsonReader {
private:
QJsonObject &m_src;
public:
explicit JsonReader(QJsonObject &obj);
ox::Error setTypeInfo(const char*, int) { return OxError(0); };
ox::Error field(QString fieldName, int *dest);
ox::Error field(QString fieldName, bool *dest);
ox::Error field(QString fieldName, double *dest);
ox::Error field(QString fieldName, QString *dest);
template<typename T>
ox::Error field(QString fieldName, T *dest);
template<typename T>
ox::Error field(QString fieldName, QVector<T> *dest);
private:
static ox::Error field(QJsonValueRef src, int *dest);
static ox::Error field(QJsonValueRef src, bool *dest);
static ox::Error field(QJsonValueRef src, double *dest);
static ox::Error field(QJsonValueRef src, QString *dest);
template<typename T>
static ox::Error field(QJsonValueRef src, T *dest);
};
template<typename T>
ox::Error JsonReader::field(QString fieldName, T *dest) {
if (m_src.contains(fieldName)) {
auto obj = m_src[fieldName].toObject();
auto reader = JsonReader(obj);
return model(&reader, dest);
} else {
return OxError(JSON_ERR_FIELD_MISSING);
}
}
template<typename T>
ox::Error JsonReader::field(QString fieldName, QVector<T> *dest) {
auto err = OxError(0);
if (m_src.contains(fieldName)) {
auto a = m_src[fieldName].toArray();
dest->resize(a.size());
for (int i = 0; i < dest->size(); i++) {
oxReturnError(field(a[i], &(*dest)[i]));
}
} else {
err = OxError(JSON_ERR_FIELD_MISSING);
}
return err;
}
template<typename T>
ox::Error JsonReader::field(QJsonValueRef src, T *dest) {
auto obj = src.toObject();
auto reader = JsonReader(obj);
return model(&reader, dest);
}
template<typename T>
ox::Error readJson(QString json, T *dest) {
QJsonParseError err;
auto obj = QJsonDocument::fromJson(json.toUtf8(), &err).object();
if (err.error) {
qDebug() << "JSON parsing error:" << err.errorString();
return OxError(1);
}
JsonReader rdr(obj);
return model(&rdr, dest);
}
}

View File

@ -1,87 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <iostream>
#include <ox/std/assert.hpp>
#include "json_read.hpp"
#include "json_write.hpp"
using namespace nostalgia::studio;
struct TestStructNest {
bool Bool;
int Int;
double Double;
QString String;
};
template<typename T>
ox::Error model(T *io, TestStructNest *obj) {
oxReturnError(io->setTypeInfo("TestStructNest", 4));
oxReturnError(io->field("Bool", &obj->Bool));
oxReturnError(io->field("Int", &obj->Int));
oxReturnError(io->field("Double", &obj->Double));
oxReturnError(io->field("String", &obj->String));
return OxError(0);
}
struct TestStruct {
bool Bool;
int Int;
double Double;
QString String;
TestStructNest Struct;
};
template<typename T>
ox::Error model(T *io, TestStruct *obj) {
oxReturnError(io->setTypeInfo("TestStruct", 5));
oxReturnError(io->field("Bool", &obj->Bool));
oxReturnError(io->field("Int", &obj->Int));
oxReturnError(io->field("Double", &obj->Double));
oxReturnError(io->field("String", &obj->String));
oxReturnError(io->field("Struct", &obj->Struct));
return OxError(0);
}
int main() {
auto err = OxError(0);
QString json;
TestStruct ts = {
true,
42,
42.42,
"Test String",
{
true,
42,
42.42,
"Test String"
}
};
TestStruct tsOut;
oxReturnError(writeJson(&json, &ts));
oxReturnError(readJson(json, &tsOut));
std::cout << tsOut.Bool << '\n';
std::cout << tsOut.Int << '\n';
std::cout << tsOut.Double << '\n';
std::cout << tsOut.String.toStdString() << '\n';
oxAssert(tsOut.Bool, "Arg 1 failed");
oxAssert(tsOut.Int == 42, "Arg 2 failed");
oxAssert(tsOut.Double == 42.42, "Arg 3 failed");
oxAssert(tsOut.String == "Test String", "Arg 4 failed");
oxAssert(tsOut.Struct.Bool, "Arg 5 failed");
oxAssert(tsOut.Struct.Int == 42, "Arg 6 failed");
oxAssert(tsOut.Struct.Double == 42.42, "Arg 7 failed");
oxAssert(tsOut.Struct.String == "Test String", "Arg 8 failed");
return static_cast<int>(err);
}

View File

@ -1,36 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "json_write.hpp"
namespace nostalgia::studio {
JsonWriter::JsonWriter(QJsonObject &obj): m_dest(obj) {
}
ox::Error JsonWriter::field(QString fieldName, int *src) {
m_dest[fieldName] = *src;
return OxError(0);
}
ox::Error JsonWriter::field(QString fieldName, bool *src) {
m_dest[fieldName] = *src;
return OxError(0);
}
ox::Error JsonWriter::field(QString fieldName, double *src) {
m_dest[fieldName] = *src;
return OxError(0);
}
ox::Error JsonWriter::field(QString fieldName, QString *src) {
m_dest[fieldName] = *src;
return OxError(0);
}
}

View File

@ -1,76 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <ox/std/error.hpp>
#include "json_err.hpp"
namespace nostalgia::studio {
class JsonWriter {
private:
QJsonObject &m_dest;
public:
explicit JsonWriter(QJsonObject &obj);
ox::Error setTypeInfo(const char*, int) { return OxError(0); };
ox::Error field(QString fieldName, int *src);
ox::Error field(QString fieldName, bool *src);
ox::Error field(QString fieldName, double *src);
ox::Error field(QString fieldName, QString *src);
template<typename T>
ox::Error field(QString fieldName, T *src);
template<typename T>
ox::Error field(QString fieldName, QVector<T> *src);
};
template<typename T>
ox::Error JsonWriter::field(QString fieldName, T *src) {
auto obj = QJsonObject();
auto reader = JsonWriter(obj);
auto err = model(&reader, src);
m_dest[fieldName] = obj;
return err;
}
template<typename T>
ox::Error JsonWriter::field(QString fieldName, QVector<T> *src) {
auto err = OxError(0);
QJsonArray a;
for (int i = 0; i < src->size(); i++) {
err |= field(a[i], &src->at(i));
}
m_dest[fieldName] = a;
return err;
}
template<typename T>
ox::Error writeJson(QString *json, T *src) {
auto obj = QJsonObject();
JsonWriter rdr(obj);
auto err = model(&rdr, src);
*json = QJsonDocument(obj).toJson();
return err;
}
}

View File

@ -1,12 +1,23 @@
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
if(APPLE)
enable_language(OBJCXX)
endif()
if(NOT APPLE AND NOT WIN32)
find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 REQUIRED gtk+-3.0)
endif()
add_library(
NostalgiaStudio
configio.cpp
editor.cpp
wizard.cpp
module.cpp
project.cpp
task.cpp
undostack.cpp
widget.cpp
window.cpp
filedialog_gtk.cpp
$<$<BOOL:${APPLE}>:filedialog.mm>
)
install(TARGETS NostalgiaStudio
@ -15,19 +26,31 @@ install(TARGETS NostalgiaStudio
generate_export_header(NostalgiaStudio)
target_include_directories(NostalgiaStudio PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
find_package(imgui REQUIRED)
include_directories(
SYSTEM
${GTK3_INCLUDE_DIRS}
)
target_link_libraries(
NostalgiaStudio PUBLIC
Qt${QT_VERSION_MAJOR}::Widgets
imgui::imgui
${GTK3_LIBRARIES}
OxEvent
OxFS
OxClaw
NostalgiaCore-Userspace
NostalgiaCore-GLFW
)
install(
FILES
editor.hpp
wizard.hpp
module.hpp
filedialog.hpp
project.hpp
task.hpp
undostack.hpp
${CMAKE_CURRENT_BINARY_DIR}/nostalgiastudio_export.h
DESTINATION
include/nostalgia/studio/lib

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "configio.hpp"

View File

@ -0,0 +1,105 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <filesystem>
#include <fstream>
#include <ox/model/typenamecatcher.hpp>
#include <ox/oc/oc.hpp>
#include <ox/std/buffer.hpp>
#include <ox/std/defines.hpp>
#include <ox/std/fmt.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp>
namespace nostalgia::studio {
constexpr auto ConfigDir = [] {
switch (ox::defines::OS) {
case ox::defines::OS::Darwin:
return "{}/Library/Preferences/NostalgiaStudio";
case ox::defines::OS::DragonFlyBSD:
case ox::defines::OS::FreeBSD:
case ox::defines::OS::Linux:
case ox::defines::OS::NetBSD:
case ox::defines::OS::OpenBSD:
return "{}/.config/NostalgiaStudio";
case ox::defines::OS::BareMetal:
case ox::defines::OS::Windows:
return "";
}
}();
template<typename T>
ox::Result<T> readConfig(const ox::String &name = ox::getModelTypeName<T>()) noexcept {
const auto homeDir = std::getenv("HOME");
const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString();
const auto path = ox::sfmt("{}/{}.json", configPath, name).toStdString();
std::ifstream file(path, std::ios::binary | std::ios::ate);
if (!file.good()) {
oxErrorf("Could not find config file: {}", path);
return OxError(1, "Could not find config file");
}
try {
const auto size = file.tellg();
file.seekg(0, std::ios::beg);
ox::Buffer buff(static_cast<std::size_t>(size));
file.read(buff.data(), size);
return ox::readOC<T>(buff);
} catch (const std::ios_base::failure &e) {
oxErrorf("Could not read config file: {}", e.what());
return OxError(2, "Could not read config file");
}
}
template<typename T>
ox::Error writeConfig(const ox::String &name, T *data) noexcept {
const auto homeDir = std::getenv("HOME");
const auto configPath = ox::sfmt(ConfigDir, homeDir).toStdString();
const auto path = ox::sfmt("{}/{}.json", configPath, name).toStdString();
std::error_code ec;
std::filesystem::create_directory(configPath, ec);
if (ec) {
oxErrf("Could not create config directory: {}\n", ec.message());
}
std::ofstream file(path, std::ios::binary | std::ios::ate);
if (!file.good()) {
oxErrorf("Could not find config file: {}", path);
return OxError(1, "Could not find config file");
}
oxRequireM(buff, ox::writeOC(data));
buff.back().value = '\n';
try {
file.write(buff.data(), static_cast<ox::Signed<decltype(buff.size())>>(buff.size()));
return OxError(0);
} catch (const std::ios_base::failure &e) {
oxErrorf("Could not read config file: {}", e.what());
return OxError(2, "Could not read config file");
}
}
template<typename T>
ox::Error writeConfig(T *data) noexcept {
return writeConfig(ox::getModelTypeName<T>(), data);
}
template<typename T, typename Func>
ox::Error editConfig(const ox::String &name, Func f) noexcept {
auto c = readConfig<T>(name);
f(&c.value);
return writeConfig(name, &c.value);
}
template<typename T, typename Func>
ox::Error editConfig(Func f) noexcept {
return editConfig<T>(ox::getModelTypeName<T>(), f);
}
}

View File

@ -6,13 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <ox/std/string.hpp>
#include "editor.hpp"
namespace nostalgia::studio {
Editor::Editor(QWidget *parent): QWidget(parent) {
}
void Editor::cut() {
}
@ -32,20 +31,20 @@ void Editor::save() {
void Editor::setUnsavedChanges(bool uc) {
m_unsavedChanges = uc;
emit unsavedChangesChanged(uc);
unsavedChangesChanged.emit(uc);
}
bool Editor::unsavedChanges() const noexcept {
return m_unsavedChanges;
}
QUndoStack *Editor::undoStack() {
UndoStack *Editor::undoStack() {
return &m_cmdStack;
}
void Editor::setExportable(bool exportable) {
m_exportable = exportable;
emit exportableChanged(exportable);
exportableChanged.emit(exportable);
}
bool Editor::exportable() const {
@ -54,7 +53,7 @@ bool Editor::exportable() const {
void Editor::setCutEnabled(bool v) {
m_cutEnabled = v;
emit cutEnabledChanged(v);
cutEnabledChanged.emit(v);
}
bool Editor::cutEnabled() const {
@ -63,7 +62,7 @@ bool Editor::cutEnabled() const {
void Editor::setCopyEnabled(bool v) {
m_copyEnabled = v;
emit copyEnabledChanged(v);
copyEnabledChanged.emit(v);
}
bool Editor::copyEnabled() const {
@ -72,7 +71,7 @@ bool Editor::copyEnabled() const {
void Editor::setPasteEnabled(bool v) {
m_pasteEnabled = v;
emit pasteEnabledChanged(v);
pasteEnabledChanged.emit(v);
}
bool Editor::pasteEnabled() const {

View File

@ -8,18 +8,18 @@
#pragma once
#include <QUndoStack>
#include <QWidget>
#include <ox/event/signal.hpp>
#include "undostack.hpp"
#include "nostalgiastudio_export.h"
namespace nostalgia::studio {
class NOSTALGIASTUDIO_EXPORT Editor: public QWidget {
Q_OBJECT
class NOSTALGIASTUDIO_EXPORT Editor: public ox::SignalHandler {
private:
QUndoStack m_cmdStack;
UndoStack m_cmdStack;
bool m_unsavedChanges = false;
bool m_exportable = false;
bool m_cutEnabled = false;
@ -27,15 +27,13 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget {
bool m_pasteEnabled = false;
public:
explicit Editor(QWidget *parent);
~Editor() override = default;
/**
* Returns the name of item being edited.
*/
[[nodiscard]]
virtual QString itemName() const = 0;
virtual ox::String itemName() const = 0;
virtual void cut();
@ -63,7 +61,7 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget {
* Returns the undo stack holding changes to the item being edited.
*/
[[nodiscard]]
QUndoStack *undoStack();
UndoStack *undoStack();
void setExportable(bool);
@ -91,16 +89,12 @@ class NOSTALGIASTUDIO_EXPORT Editor: public QWidget {
*/
virtual void saveItem();
signals:
void unsavedChangesChanged(bool);
void exportableChanged(bool);
void cutEnabledChanged(bool);
void copyEnabledChanged(bool);
void pasteEnabledChanged(bool);
public:
ox::Signal<ox::Error(bool)> unsavedChangesChanged;
ox::Signal<ox::Error(bool)> exportableChanged;
ox::Signal<ox::Error(bool)> cutEnabledChanged;
ox::Signal<ox::Error(bool)> copyEnabledChanged;
ox::Signal<ox::Error(bool)> pasteEnabledChanged;
};

View File

@ -8,15 +8,8 @@
#pragma once
#include <nostalgia/core/studio/module.hpp>
namespace nostalgia::studio {
[[maybe_unused]] // GCC warns about the existence of this "unused" constexpr list in a header file...
constexpr auto BuiltinModules = {
[] {
return new core::Module();
},
};
ox::Result<ox::String> chooseDirectory() noexcept;
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#import <Cocoa/Cocoa.h>
#include <ox/std/string.hpp>
#include "filedialog.hpp"
namespace nostalgia::studio {
ox::Result<ox::String> chooseDirectory() noexcept {
auto panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
[panel setFloatingPanel:YES];
if ([panel runModal] == NSModalResponseOK) {
const auto url = [panel URL];
const auto urlStr = [url absoluteString];
const auto urlCStr = [urlStr UTF8String];
return ox::String(urlCStr).substr(7);
}
return OxError(1);
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <ox/std/defines.hpp>
#if !defined(OX_OS_Windows) && !defined(OX_OS_Darwin)
#include <gtk/gtk.h>
#include <ox/std/defer.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <ox/std/trace.hpp>
namespace nostalgia::studio {
ox::Result<ox::String> chooseDirectory() noexcept {
ox::Result<ox::String> out;
const auto action = GTK_FILE_CHOOSER_ACTION_OPEN;
gtk_init(0, nullptr);
auto dialog = gtk_file_chooser_dialog_new("Open File", nullptr, action,
("_Cancel"), GTK_RESPONSE_CANCEL,
("_Open"), GTK_RESPONSE_ACCEPT,
nullptr);
const auto res = gtk_dialog_run(GTK_DIALOG(dialog));
oxDebug("gtk_dialog_run returned");
if (res == GTK_RESPONSE_ACCEPT) {
auto chooser = GTK_FILE_CHOOSER(dialog);
auto path = gtk_file_chooser_get_filename(chooser);
out = ox::String(path);
g_free(path);
} else {
out = OxError(1);
}
gtk_widget_destroy(dialog);
return out;
}
}
#endif

View File

@ -6,80 +6,83 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <QDir>
#include <QDebug>
#include <filesystem>
#include <ox/std/std.hpp>
#include "project.hpp"
namespace nostalgia::studio {
QString filePathToName(const QString &path, const QString &prefix, const QString &suffix) {
const auto begin = prefix.size();
const auto end = path.size() - (suffix.size() + prefix.size());
return path.mid(begin, end);
ox::String filePathToName(const ox::String &path, const ox::String &prefix, const ox::String &suffix) noexcept {
const auto begin = prefix.len();
const auto end = path.len() - (suffix.len() + prefix.len());
return path.substr(begin, end);
}
Project::Project(const QString &path): m_fs(std::make_unique<ox::PassThroughFS>(path.toUtf8())) {
qDebug() << "Project:" << path;
Project::Project(const ox::String &path) noexcept: m_fs(ox::make_unique<ox::PassThroughFS>(path.c_str())) {
oxTracef("nostalgia::studio", "Project: {}", path);
m_path = path;
}
void Project::create() {
QDir().mkpath(m_path);
ox::Error Project::create() noexcept {
std::error_code ec;
std::filesystem::create_directory(m_path.toStdString(), ec);
return OxError(ec.value(), "PassThroughFS: mkdir failed");
}
ox::FileSystem *Project::romFs() {
ox::FileSystem *Project::romFs() noexcept {
return m_fs.get();
}
void Project::mkdir(const QString &path) const {
oxThrowError(m_fs->mkdir(path.toUtf8().data(), true));
emit fileUpdated(path);
ox::Error Project::mkdir(const ox::String &path) const noexcept {
oxReturnError(m_fs->mkdir(path.c_str(), true));
fileUpdated.emit(path);
return OxError(0);
}
ox::FileStat Project::stat(const QString &path) const {
oxRequireT(s, m_fs->stat(path.toUtf8().data()));
return s;
ox::Result<ox::FileStat> Project::stat(const ox::String &path) const noexcept {
return m_fs->stat(path.c_str());
}
bool Project::exists(const QString &path) const {
return m_fs->stat(path.toUtf8().data()).error == 0;
bool Project::exists(const ox::String &path) const noexcept {
return m_fs->stat(path.c_str()).error == 0;
}
void Project::writeBuff(const QString &path, uint8_t *buff, size_t buffLen) const {
oxThrowError(m_fs->write(path.toUtf8().data(), buff, buffLen));
emit fileUpdated(path);
ox::Error Project::writeBuff(const ox::String &path, const ox::Buffer &buff) const noexcept {
oxReturnError(m_fs->write(path.c_str(), buff.data(), buff.size()));
fileUpdated.emit(path);
return OxError(0);
}
ox::Buffer Project::loadBuff(const QString &path) const {
const auto csPath = path.toUtf8();
oxRequireMT(buff, m_fs->read(csPath.data()));
return std::move(buff);
ox::Result<ox::Buffer> Project::loadBuff(const ox::String &path) const noexcept {
return m_fs->read(path.c_str());
}
void Project::lsProcDir(QStringList *paths, const QString &path) const {
oxRequireT(files, m_fs->ls(path.toUtf8()));
ox::Error Project::lsProcDir(ox::Vector<ox::String> *paths, const ox::String &path) const noexcept {
oxRequire(files, m_fs->ls(path.c_str()));
for (const auto &name : files) {
const auto fullPath = path + "/" + name.c_str();
oxRequireT(stat, m_fs->stat(fullPath.toUtf8().data()));
oxRequire(stat, m_fs->stat(fullPath.c_str()));
switch (stat.fileType) {
case ox::FileType::NormalFile:
paths->push_back(fullPath);
break;
case ox::FileType::Directory:
lsProcDir(paths, fullPath);
oxReturnError(lsProcDir(paths, fullPath));
break;
case ox::FileType::None:
break;
}
}
return OxError(0);
}
QStringList Project::listFiles(const QString &path) const {
QStringList paths;
lsProcDir(&paths, path);
return ox::move(paths);
ox::Result<ox::Vector<ox::String>> Project::listFiles(const ox::String &path) const noexcept {
ox::Vector<ox::String> paths;
oxReturnError(lsProcDir(&paths, path));
return paths;
}
}

View File

@ -10,13 +10,13 @@
#include <memory>
#include <QSharedPointer>
#include <qnamespace.h>
#include <ox/claw/claw.hpp>
#include <ox/claw/read.hpp>
#include <ox/claw/write.hpp>
#include <ox/fs/fs.hpp>
#include <ox/mc/mc.hpp>
#include <ox/model/descwrite.hpp>
#include <ox/event/signal.hpp>
#include <ox/std/std.hpp>
#include "nostalgiastudio_export.h"
@ -24,6 +24,7 @@
namespace nostalgia::studio {
enum class ProjectEvent {
None,
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.
@ -33,112 +34,114 @@ enum class ProjectEvent {
};
[[nodiscard]]
QString filePathToName(const QString &path, const QString &prefix, const QString &suffix);
class NOSTALGIASTUDIO_EXPORT Project: public QObject {
Q_OBJECT
ox::String filePathToName(const ox::String &path, const ox::String &prefix, const ox::String &suffix) noexcept;
class NOSTALGIASTUDIO_EXPORT Project {
private:
QString m_path = "";
mutable std::unique_ptr<ox::FileSystem> m_fs;
ox::String m_path = "";
mutable ox::UniquePtr<ox::FileSystem> m_fs;
public:
explicit Project(const QString &path);
explicit Project(const ox::String &path) noexcept;
~Project() override = default;
ox::Error create() noexcept;
void create();
[[nodiscard]]
ox::FileSystem *romFs() noexcept;
ox::FileSystem *romFs();
void mkdir(const QString &path) const;
ox::Error mkdir(const ox::String &path) const noexcept;
/**
* Writes a MetalClaw object to the project at the given path.
*/
template<typename T>
void writeObj(const QString &path, T *obj) const;
ox::Error writeObj(const ox::String &path, T *obj) const noexcept;
template<typename T>
std::unique_ptr<T> loadObj(const QString &path) const;
ox::Result<ox::UniquePtr<T>> loadObj(const ox::String &path) const noexcept;
ox::FileStat stat(const QString &path) const;
ox::Result<ox::FileStat> stat(const ox::String &path) const noexcept;
bool exists(const QString& path) const;
[[nodiscard]]
bool exists(const ox::String& path) const noexcept;
template<typename Functor>
void subscribe(ProjectEvent e, QObject *tgt, Functor &&slot) const;
ox::Error subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept;
private:
void writeBuff(const QString &path, uint8_t *buff, size_t buffLen) const;
ox::Error writeBuff(const ox::String &path, const ox::Buffer &buff) const noexcept;
ox::Buffer loadBuff(const QString &path) const;
ox::Result<ox::Buffer> loadBuff(const ox::String &path) const noexcept;
void lsProcDir(QStringList *paths, const QString &path) const;
ox::Error lsProcDir(ox::Vector<ox::String> *paths, const ox::String &path) const noexcept;
QStringList listFiles(const QString &path = "") const;
ox::Result<ox::Vector<ox::String>> listFiles(const ox::String &path = "") const noexcept;
signals:
void fileEvent(ProjectEvent, QString path) const;
void fileAdded(QString) const;
// signals
public:
ox::Signal<ox::Error(ProjectEvent, const ox::String&)> fileEvent;
ox::Signal<ox::Error(const ox::String&)> 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.
void fileRecognized(QString) const;
void fileDeleted(QString) const;
void fileUpdated(QString) const;
ox::Signal<ox::Error(const ox::String&)> fileRecognized;
ox::Signal<ox::Error(const ox::String&)> fileDeleted;
ox::Signal<ox::Error(const ox::String&)> fileUpdated;
};
template<typename T>
void Project::writeObj(const QString &path, T *obj) const {
ox::Error Project::writeObj(const ox::String &path, T *obj) const noexcept {
// write MetalClaw
oxRequireMT(buff, ox::writeClaw(obj, ox::ClawFormat::Metal));
oxRequireM(buff, ox::writeClaw(obj, ox::ClawFormat::Metal));
// write to FS
writeBuff(path, ox::bit_cast<uint8_t*>(buff.data()), buff.size());
oxReturnError(writeBuff(path, buff));
// write type descriptor
oxRequireT(type, ox::buildTypeDef(obj));
oxRequireMT(typeOut, ox::writeClaw(type.get(), ox::ClawFormat::Organic));
oxRequire(type, ox::buildTypeDef(obj));
oxRequireM(typeOut, ox::writeClaw(type.get(), ox::ClawFormat::Organic));
// replace garbage last character with new line
typeOut.back().value = '\n';
// write to FS
QString descPath = "/.nostalgia/type_descriptors/";
ox::String descPath = "/.nostalgia/type_descriptors/";
const auto typePath = descPath + type->typeName.c_str();
mkdir(descPath);
writeBuff(typePath, ox::bit_cast<uint8_t*>(typeOut.data()), typeOut.size());
emit fileUpdated(path);
oxReturnError(mkdir(descPath));
oxReturnError(writeBuff(typePath, typeOut));
fileUpdated.emit(path);
return OxError(0);
}
template<typename T>
std::unique_ptr<T> Project::loadObj(const QString &path) const {
auto obj = std::make_unique<T>();
auto buff = loadBuff(path);
oxThrowError(ox::readClaw<T>(buff.data(), buff.size(), obj.get()));
return obj;
ox::Result<ox::UniquePtr<T>> Project::loadObj(const ox::String &path) const noexcept {
auto obj = ox::make_unique<T>();
oxRequire(buff, loadBuff(path));
return ox::readClaw<T>(buff);
}
template<typename Functor>
void Project::subscribe(ProjectEvent e, QObject *tgt, Functor &&slot) const {
ox::Error Project::subscribe(ProjectEvent e, ox::SignalHandler *tgt, Functor &&slot) const noexcept {
switch (e) {
case ProjectEvent::None:
break;
case ProjectEvent::FileAdded:
connect(this, &Project::fileAdded, tgt, (slot));
connect(this, &Project::fileAdded, tgt, slot);
break;
case ProjectEvent::FileRecognized:
{
for (auto f : listFiles()) {
oxRequire(fileList, listFiles());
for (auto f : fileList) {
slot(f);
}
connect(this, &Project::fileRecognized, tgt, (slot));
connect(this, &Project::fileRecognized, tgt, slot);
break;
}
case ProjectEvent::FileDeleted:
connect(this, &Project::fileDeleted, tgt, (slot));
connect(this, &Project::fileDeleted, tgt, slot);
break;
case ProjectEvent::FileUpdated:
connect(this, &Project::fileUpdated, tgt, (slot));
connect(this, &Project::fileUpdated, tgt, slot);
break;
}
return OxError(0);
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "task.hpp"
namespace nostalgia::studio {
void TaskRunner::update(core::Context *ctx) noexcept {
for (auto i = 0u; i < m_tasks.size(); ++i) {
if (m_tasks[i]->update(ctx) == studio::TaskState::Done) {
oxIgnoreError(m_tasks.erase(i--));
}
}
}
void TaskRunner::add(Task *task) noexcept {
m_tasks.emplace_back(task);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <nostalgia/core/context.hpp>
namespace nostalgia::studio {
enum class TaskState {
Running,
Done
};
class Task: public ox::SignalHandler {
public:
virtual ~Task() noexcept = default;
virtual TaskState update(nostalgia::core::Context *ctx) noexcept = 0;
};
class TaskRunner {
private:
ox::Vector<ox::UniquePtr<studio::Task>> m_tasks;
public:
void update(core::Context *ctx) noexcept;
void add(Task *task) noexcept;
};
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "undostack.hpp"
namespace nostalgia::studio {
void UndoStack::push(UndoCommand *cmd) noexcept {
for (const auto i = m_stackIdx; i < m_stack.size();) {
oxIgnoreError(m_stack.erase(i));
}
m_stack.emplace_back(cmd);
++m_stackIdx;
}
void UndoStack::redo() noexcept {
m_stack[m_stackIdx++]->redo();
}
void UndoStack::undo() noexcept {
m_stack[m_stackIdx--]->undo();
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/std/error.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/vector.hpp>
namespace nostalgia::studio {
class UndoCommand {
public:
virtual ~UndoCommand() noexcept = default;
virtual void redo() noexcept = 0;
virtual void undo() noexcept = 0;
};
class UndoStack {
private:
ox::Vector<ox::UniquePtr<UndoCommand>> m_stack;
std::size_t m_stackIdx = 0;
public:
void push(UndoCommand *cmd) noexcept;
void redo() noexcept;
void undo() noexcept;
};
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "widget.hpp"
namespace nostalgia::studio {
}

View File

@ -8,17 +8,15 @@
#pragma once
#include <QString>
#include <QWidget>
#include <ox/event/signal.hpp>
#include <nostalgia/core/context.hpp>
namespace nostalgia::studio {
class Context {
class Widget: public ox::SignalHandler {
public:
QString appName;
QString orgName;
QWidget *tabParent = nullptr;
const class Project *project = nullptr;
virtual void draw(core::Context*) noexcept = 0;
};
}
}

View File

@ -0,0 +1,13 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "window.hpp"

View File

@ -8,11 +8,12 @@
#pragma once
#include "widget.hpp"
namespace nostalgia::studio {
enum {
JSON_ERR_FIELD_MISSING = 1,
JSON_ERR_UNEXPECTED_TYPE = 2,
class Window: public Widget {
};
}
}

View File

@ -1,357 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <QComboBox>
#include <QDebug>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include "context.hpp"
#include "project.hpp"
#include "module.hpp"
#include "wizard.hpp"
namespace nostalgia::studio {
PathExistsValidator::PathExistsValidator(const Context *ctx, QString pathTemplate, bool shouldExist) {
m_ctx = ctx;
m_pathTemplate = pathTemplate;
m_shouldExist = shouldExist;
}
QValidator::State PathExistsValidator::validate(QString &input, int&) const {
auto path = m_pathTemplate.arg(input);
if (m_ctx->project->exists(path) == m_shouldExist) {
return QValidator::Acceptable;
}
return QValidator::Invalid;
}
WizardSelect::WizardSelect() {
m_listWidget = new QListWidget(this);
auto layout = new QVBoxLayout(this);
layout->addWidget(m_listWidget);
setLayout(layout);
connect(m_listWidget, &QListWidget::currentRowChanged, this, &WizardSelect::itemSelected);
connect(m_listWidget, &QListWidget::itemSelectionChanged, [this]() {
m_complete = true;
emit completeChanged();
}
);
connect(m_listWidget, &QListWidget::doubleClicked, [this]() {
wizard()->next();
}
);
}
void WizardSelect::initializePage() {
emit completeChanged();
}
void WizardSelect::addOption(const WizardMaker &wm) {
m_options[wm.name] = {wm.make, wm.onAccept};
m_listWidget->addItem(wm.name);
}
bool WizardSelect::isComplete() const {
return m_complete;
}
void WizardSelect::itemSelected(int row) {
auto w = dynamic_cast<Wizard*>(wizard());
if (w && row > -1) {
// remove other pages
while (nextId() > -1) {
w->removePage(nextId());
}
auto selected = m_listWidget->currentItem()->text();
if (m_options.contains(selected)) {
auto &o = m_options[selected];
for (auto p : o.make()) {
w->addPage(p);
}
w->setAccept(o.onAccept);
// for some reason the continue button only appears correctly after remove runs
w->removePage(w->addPage(new QWizardPage()));
}
}
}
WizardConclusionPage::WizardConclusionPage(QString msg, QVector<QString> fields) {
m_baseMsg = msg;
m_fields = fields;
setLayout(new QVBoxLayout(this));
}
void WizardConclusionPage::initializePage() {
QString msg = m_baseMsg;
for (auto field : m_fields) {
msg = msg.arg(this->field(field).toString());
}
auto text = new QLabel(msg, this);
if (m_text) {
layout()->replaceWidget(m_text, text);
delete m_text;
} else {
layout()->addWidget(text);
}
m_text = text;
}
void WizardFormPage::Field::setDisplayText(QString text) {
auto le = dynamic_cast<QLineEdit*>(this->valueControl);
auto cb = dynamic_cast<QComboBox*>(this->valueControl);
if (le) {
le->setText(text);
} else if (cb) {
cb->setCurrentText(text);
}
}
QString WizardFormPage::Field::getDisplayText() const {
auto le = dynamic_cast<QLineEdit*>(this->valueControl);
auto cb = dynamic_cast<QComboBox*>(this->valueControl);
if (le) {
return le->text();
} else if (cb) {
return cb->currentText();
} else {
return "";
}
}
WizardFormPage::WizardFormPage() {
m_layout = new QGridLayout(this);
m_layout->setColumnMinimumWidth(0, 20);
this->setLayout(m_layout);
}
WizardFormPage::~WizardFormPage() {
for (auto l : m_subLayout) {
delete l;
}
}
int WizardFormPage::accept() {
return 0;
}
void WizardFormPage::initializePage() {
for (auto it = m_fields.begin(); it != m_fields.end(); it++) {
auto key = it.key();
auto defaultVal = it.value().defaultValue;
m_fields[key].setDisplayText(defaultVal);
}
}
bool WizardFormPage::validatePage() {
bool retval = true;
// check validators
for (auto f : m_fields) {
if (f.validator != nullptr) {
if (f.validator(f.getDisplayText()) != 0) {
retval = false;
break;
}
}
}
// clear error
if (retval) {
showValidationError("");
}
return retval;
}
QComboBox *WizardFormPage::addComboBox(QString displayName, QString fieldName, QStringList options) {
auto lbl = new QLabel(displayName, this);
auto cb = new QComboBox(this);
lbl->setBuddy(cb);
m_layout->addWidget(lbl, m_currentLine, 0);
m_layout->addWidget(cb, m_currentLine, 1);
auto field = &m_fields[fieldName];
field->valueControl = cb;
registerField(fieldName, cb);
for (auto o : options) {
cb->addItem(o);
}
connect(cb, &QComboBox::currentTextChanged, [this, fieldName, field](QString txt) {
if (field->value == "" && txt != "") {
m_validFields++;
} else if (field->value != "" && txt == "") {
m_validFields--;
}
field->value = txt;
emit completeChanged();
}
);
m_currentLine++;
return cb;
}
QLineEdit *WizardFormPage::addLineEdit(QString displayName, QString fieldName, QString defaultVal, std::function<int(QString)> validator) {
auto lbl = new QLabel(displayName, this);
auto le = new QLineEdit(this);
lbl->setBuddy(le);
m_layout->addWidget(lbl, m_currentLine, 0);
m_layout->addWidget(le, m_currentLine, 1);
auto field = &m_fields[fieldName];
field->defaultValue = defaultVal;
field->valueControl = le;
field->validator = validator;
registerField(fieldName, le);
connect(le, &QLineEdit::textChanged, [this, fieldName, field](QString txt) {
if (field->value == "" && txt != "") {
m_validFields++;
} else if (field->value != "" && txt == "") {
m_validFields--;
}
field->value = txt;
emit completeChanged();
}
);
m_currentLine++;
return le;
}
void WizardFormPage::addPathBrowse(QString displayName, QString fieldName, QString defaultVal,
QFileDialog::FileMode fileMode, QString fileExtensions) {
auto layout = new QHBoxLayout();
auto lbl = new QLabel(displayName, this);
auto le = new QLineEdit("", this);
auto btn = new QPushButton("Browse...", this);
lbl->setBuddy(le);
layout->addWidget(le);
layout->addWidget(btn);
m_layout->addWidget(lbl, m_currentLine, 0);
m_layout->addLayout(layout, m_currentLine, 1);
m_subLayout.push_back(layout);
m_fields[fieldName].defaultValue = defaultVal;
m_fields[fieldName].valueControl = le;
m_fields[fieldName].validator = [this, fileMode](QString path) {
if (fileMode == QFileDialog::Directory) {
if (!QDir(path).exists()) {
showValidationError(tr("Specified directory path does not exist."));
return 1;
} else {
return 0;
}
} else if (fileMode == QFileDialog::ExistingFile) {
if (!QFile(path).exists()) {
showValidationError(tr("Specified directory path does not exist."));
return 1;
} else {
return 0;
}
} else {
return 2;
}
};
registerField(fieldName, le);
connect(le, &QLineEdit::textChanged, [this, fieldName](QString txt) {
if (m_fields[fieldName].value == "" && txt != "") {
m_validFields++;
} else if (m_fields[fieldName].value != "" && txt == "") {
m_validFields--;
}
m_fields[fieldName].value = txt;
emit completeChanged();
}
);
connect(btn, &QPushButton::clicked, [this, defaultVal, le, fileMode, fileExtensions]() {
auto dir = defaultVal;
if (dir == "") {
dir = QDir::homePath();
}
if (fileMode == QFileDialog::Directory) {
auto p = QFileDialog::getExistingDirectory(this, tr("Select Directory..."), dir);
if (p != "") {
le->setText(p);
}
} else if (fileMode == QFileDialog::ExistingFile) {
auto p = QFileDialog::getOpenFileName(this, tr("Select File..."), dir, fileExtensions);
if (p != "") {
le->setText(p);
}
}
}
);
m_currentLine++;
}
void WizardFormPage::showValidationError(QString msg) {
// create label if it is null
if (!m_errorMsg) {
m_errorMsg = new QLabel(this);
m_layout->addWidget(m_errorMsg, m_currentLine, 0, m_currentLine, 2);
// set text color
auto pal = m_errorMsg->palette();
pal.setColor(m_errorMsg->backgroundRole(), Qt::red);
pal.setColor(m_errorMsg->foregroundRole(), Qt::red);
m_errorMsg->setPalette(pal);
}
// set message
if (msg != "") {
m_errorMsg->setText(msg);
} else {
m_errorMsg->setText("");
}
}
Wizard::Wizard(QString windowTitle, QWidget *parent): QWizard(parent) {
setWindowTitle(windowTitle);
setModal(true);
}
void Wizard::setAccept(std::function<int(QWizard*)> acceptFunc) {
m_acceptFunc = acceptFunc;
}
void Wizard::accept() {
auto page = dynamic_cast<WizardFormPage*>(currentPage());
if ((page == nullptr || page->accept() == 0) &&
m_acceptFunc != nullptr && m_acceptFunc(this) == 0) {
QDialog::accept();
}
}
}

View File

@ -1,151 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <functional>
#include <QComboBox>
#include <QDir>
#include <QFileDialog>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QMap>
#include <QValidator>
#include <QVector>
#include <QWizard>
#include "nostalgiastudio_export.h"
namespace nostalgia::studio {
class PathExistsValidator: public QValidator {
private:
const class Context *m_ctx = nullptr;
QString m_pathTemplate;
bool m_shouldExist = true;
public:
PathExistsValidator(const class Context *ctx, QString pathTemplate, bool shouldExist);
QValidator::State validate(QString &input, int &pos) const override;
};
struct NOSTALGIASTUDIO_EXPORT WizardMaker {
QString name;
std::function<QVector<QWizardPage*>()> make;
std::function<int(QWizard*)> onAccept = [](QWizard*) { return 0; };
};
class NOSTALGIASTUDIO_EXPORT WizardSelect: public QWizardPage {
Q_OBJECT
private:
struct WizardFuncs {
std::function<QVector<QWizardPage*>()> make;
std::function<int(QWizard*)> onAccept;
};
QHash<QString, WizardFuncs> m_options;
QListWidget *m_listWidget = nullptr;
bool m_complete = false;
public:
WizardSelect();
void addOption(const WizardMaker &wm);
void initializePage() override;
bool isComplete() const override;
private slots:
void itemSelected(int row);
};
class NOSTALGIASTUDIO_EXPORT WizardFormPage: public QWizardPage {
Q_OBJECT
private:
struct Field {
QString defaultValue = "";
QString value = "";
QWidget *valueControl = nullptr;
std::function<int(QString)> validator;
void setDisplayText(QString text);
QString getDisplayText() const;
};
QLabel *m_errorMsg = nullptr;
QGridLayout *m_layout = nullptr;
QVector<QLayout*> m_subLayout;
QMap<QString, Field> m_fields;
int m_currentLine = 0;
int m_validFields = 0;
public:
WizardFormPage();
~WizardFormPage();
virtual int accept();
void initializePage() override;
bool validatePage() override;
QComboBox *addComboBox(QString displayName, QString fieldName, QStringList options);
QLineEdit *addLineEdit(QString displayName, QString fieldName,
QString defaultVal = "",
std::function<int(QString)> validator = [](QString) { return 0; });
void addPathBrowse(QString displayName, QString fieldName,
QString defaultVal = QDir::homePath(),
QFileDialog::FileMode fileMode = QFileDialog::AnyFile,
QString fileExtensions = "");
void showValidationError(QString msg);
};
class NOSTALGIASTUDIO_EXPORT WizardConclusionPage: public QWizardPage {
Q_OBJECT
private:
QString m_baseMsg = "";
QLabel *m_text = nullptr;
QVector<QString> m_fields;
public:
WizardConclusionPage(QString msg, QVector<QString> field);
~WizardConclusionPage() override = default;
void initializePage() override;
};
class NOSTALGIASTUDIO_EXPORT Wizard: public QWizard {
Q_OBJECT
private:
std::function<int(QWizard*)> m_acceptFunc;
public:
Wizard(QString windowTitle, QWidget *parent = 0);
void setAccept(std::function<int(QWizard*)> acceptFunc);
void accept();
};
}

View File

@ -6,38 +6,59 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <QApplication>
#include <QDebug>
#include <QQuickWindow>
#include <ox/clargs/clargs.hpp>
#include <ox/std/trace.hpp>
#include <qdark/theme.hpp>
#include "mainwindow.hpp"
using namespace nostalgia::studio;
#include <nostalgia/core/core.hpp>
int main(int argc, char **args) {
#include "studioapp.hpp"
namespace nostalgia {
class StudioUIDrawer: public core::Drawer {
private:
StudioUI *m_ui = nullptr;
public:
explicit StudioUIDrawer(StudioUI *ui) noexcept: m_ui(ui) {
}
protected:
void draw(core::Context *ctx) noexcept final {
m_ui->draw(ctx);
}
};
static int eventHandler(core::Context *ctx) noexcept {
auto ui = ctx->customData<StudioUI>();
ui->update(ctx);
return 16;
}
static ox::Error run(ox::FileSystem *fs) noexcept {
oxRequireM(ctx, core::init(fs));
core::setWindowTitle(ctx.get(), "Nostalgia Studio");
core::setEventHandler(ctx.get(), eventHandler);
StudioUI ui;
StudioUIDrawer drawer(&ui);
ctx->setCustomData(&ui);
ctx->drawers.emplace_back(&drawer);
return core::run(ctx.get());
}
static ox::Error run(int argc, const char **argv) noexcept {
ox::trace::init();
// get profile path from command args
ox::ClArgs clargs(argc, const_cast<const char**>(args));
QString argProfilePath = clargs.getString("profile", ":/profiles/nostalgia-studio.json").c_str();
QApplication app(argc, args);
// load theme
if constexpr(ox::defines::OS != ox::defines::OS::Darwin) {
qdark::load(&app);
}
// force QtQuick to use OpenGL (https://doc.qt.io/qt-6/quick-changes-qt6.html#changes-to-qquickwidget)
#if QT_VERSION >= 0x060000
QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
#endif
// open window
try {
MainWindow w(argProfilePath);
app.setApplicationName(w.windowTitle());
w.show();
QObject::connect(&app, &QApplication::aboutToQuit, &w, &MainWindow::onExit);
return app.exec();
} catch (const ox::Error &err) {
oxPanic(err, "Unhandled ox::Error");
if (argc >= 2) {
const auto path = argv[1];
oxRequire(fs, core::loadRomFs(path));
return run(fs.get());
} else {
return run(nullptr);
}
}
}
int main(int argc, const char **argv) {
ox::trace::init();
const auto err = nostalgia::run(argc, argv);
oxAssert(err, "Something went wrong...");
return static_cast<int>(err);
}

View File

@ -1,611 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <QApplication>
#include <QDebug>
#include <QDialog>
#include <QFileDialog>
#include <QHeaderView>
#include <QMenuBar>
#include <QPluginLoader>
#include <QScreen>
#include <QSettings>
#include <QTextStream>
#include <QVector>
#include "lib/editor.hpp"
#include "lib/project.hpp"
#include "lib/wizard.hpp"
#include "json_read.hpp"
#include "json_write.hpp"
#include "mainwindow.hpp"
namespace nostalgia::studio {
MainWindow::MainWindow(QString profilePath) {
m_profilePath = profilePath;
// load in profile file
QFile file(profilePath);
if (file.exists()) {
file.open(QIODevice::ReadOnly);
QTextStream in(&file);
oxThrowError(readJson(in.readAll(), &m_profile));
qDebug() << m_profile.appName;
}
auto screenSize = QApplication::screens().first()->geometry();
// set window to 75% of screen width, and center NostalgiaStudioProfile
constexpr auto sizePct = 0.75;
resize(static_cast<int>(screenSize.width() * sizePct), static_cast<int>(screenSize.height() * sizePct));
move(-x(), -y());
move(static_cast<int>(screenSize.width() * (1 - sizePct) / 2), static_cast<int>(screenSize.height() * (1 - sizePct) / 2));
setWindowTitle(m_profile.appName);
m_ctx.appName = m_profile.appName;
m_ctx.orgName = m_profile.orgName;
m_ctx.tabParent = m_tabs;
m_tabs = new QTabWidget(this);
auto tabBar = m_tabs->tabBar();
setCentralWidget(m_tabs);
m_tabs->setTabsClosable(true);
connect(m_tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::closeTab);
connect(tabBar, &QTabBar::tabMoved, this, &MainWindow::moveTab);
connect(tabBar, &QTabBar::currentChanged, this, &MainWindow::changeTab);
tabBar->setMovable(true);
setupMenu();
setupProjectExplorer();
statusBar(); // setup status bar
loadModules();
readState();
}
MainWindow::~MainWindow() {
closeProject();
}
void MainWindow::loadModules() {
for (auto p : BuiltinModules) {
auto module = p();
loadModule(module);
delete module;
}
for (auto dir : m_profile.modulesPath) {
QFileInfo dirInfo(m_profilePath);
dir = dirInfo.absolutePath() + "/" + dir;
if (dirInfo.exists()) {
qDebug() << "Checking module directory:" << dir;
loadModuleDir(dir);
}
}
}
void MainWindow::loadModuleDir(QString dir) {
for (auto modulePath : QDir(dir).entryList()) {
modulePath = dir + '/' + modulePath;
if (QLibrary::isLibrary(modulePath)) {
loadModule(modulePath);
}
}
}
void MainWindow::loadModule(QString modulePath) {
qDebug() << "Loading module:" << modulePath;
QPluginLoader loader(modulePath);
auto module = qobject_cast<Module*>(loader.instance());
if (module) {
loadModule(module);
delete module;
} else {
qInfo() << loader.errorString();
}
}
void MainWindow::loadModule(Module *module) {
m_modules.push_back(module);
auto editorMakers = module->editors(&m_ctx);
for (const auto &em : editorMakers) {
for (const auto &ext : em.fileTypes) {
m_editorMakers[ext] = em;
}
}
}
void MainWindow::setupMenu() {
auto menu = menuBar();
auto fileMenu = menu->addMenu(tr("&File"));
auto editMenu = menu->addMenu(tr("&Edit"));
m_viewMenu = menu->addMenu(tr("&View"));
// New...
addAction(
fileMenu,
tr("&New..."),
tr(""),
QKeySequence::New,
this,
SLOT(showNewWizard())
);
// Import...
m_importAction = addAction(
fileMenu,
tr("&Import..."),
tr(""),
this,
SLOT(showImportWizard())
);
m_importAction->setEnabled(false);
// Open Project
addAction(
fileMenu,
tr("&Open Project"),
tr(""),
QKeySequence::Open,
this,
SLOT(openProject())
);
// Save Project
m_saveAction = addAction(
fileMenu,
tr("&Save File"),
tr(""),
QKeySequence::Save,
this,
SLOT(saveFile())
);
m_saveAction->setEnabled(false);
// Save Project
m_exportAction = addAction(
fileMenu,
tr("&Export File..."),
tr(""),
this,
SLOT(exportFile())
);
m_exportAction->setEnabled(false);
// Exit
addAction(
fileMenu,
tr("E&xit"),
tr("Exit the application"),
QKeySequence::Quit,
QApplication::quit
);
// Undo
auto undoAction = m_undoGroup.createUndoAction(this, tr("&Undo"));
editMenu->addAction(undoAction);
undoAction->setShortcuts(QKeySequence::Undo);
// Redo
auto redoAction = m_undoGroup.createRedoAction(this, tr("&Redo"));
editMenu->addAction(redoAction);
redoAction->setShortcuts(QKeySequence::Redo);
editMenu->addSeparator();
// Copy
m_cutAction = addAction(
editMenu,
tr("&Cut"),
tr(""),
QKeySequence::Cut,
this,
SLOT(cutAction())
);
m_cutAction->setEnabled(false);
// Copy
m_copyAction = addAction(
editMenu,
tr("&Copy"),
tr(""),
QKeySequence::Copy,
this,
SLOT(copyAction())
);
m_copyAction->setEnabled(false);
// Paste
m_pasteAction = addAction(
editMenu,
tr("&Paste"),
tr(""),
QKeySequence::Paste,
this,
SLOT(pasteAction())
);
m_pasteAction->setEnabled(false);
}
void MainWindow::setupProjectExplorer() {
// setup dock
auto dock = new QDockWidget(tr("Project"), this);
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
dock->setObjectName("Project Explorer");
addDockWidget(Qt::LeftDockWidgetArea, dock);
resizeDocks({dock}, {static_cast<int>(width() * 0.25)}, Qt::Horizontal);
// setup tree view
m_projectExplorer = new QTreeView(dock);
m_projectExplorer->header()->hide();
dock->setWidget(m_projectExplorer);
connect(m_projectExplorer, &QTreeView::activated, this, &MainWindow::openFileSlot);
}
void MainWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockWidget) {
QMainWindow::addDockWidget(area, dockWidget);
m_viewMenu->addAction(dockWidget->toggleViewAction());
m_dockWidgets.push_back(dockWidget);
}
QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip, const QObject *tgt, const char *cb) {
auto action = menu->addAction(text);
action->setStatusTip(toolTip);
connect(action, SIGNAL(triggered()), tgt, cb);
return action;
}
QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip,
QKeySequence::StandardKey key, const QObject *tgt, const char *cb) {
auto action = menu->addAction(text);
action->setShortcuts(key);
action->setStatusTip(toolTip);
connect(action, SIGNAL(triggered()), tgt, cb);
return action;
}
QAction *MainWindow::addAction(QMenu *menu, QString text, QString toolTip,
QKeySequence::StandardKey key, void (*cb)()) {
auto action = menu->addAction(text);
action->setShortcuts(key);
action->setStatusTip(toolTip);
connect(action, &QAction::triggered, cb);
return action;
}
int MainWindow::readState() {
int err = 0;
QSettings settings(m_profile.orgName, m_profile.appName);
settings.beginGroup("MainWindow");
restoreGeometry(settings.value("geometry").toByteArray());
restoreState(settings.value("windowState").toByteArray());
auto json = settings.value("json").toString();
oxReturnError(readJson(json, &m_state));
settings.endGroup();
openProject(m_state.projectPath);
return err;
}
void MainWindow::writeState() {
// generate JSON for application specific state info
QString json;
oxIgnoreError(writeJson(&json, &m_state));
QSettings settings(m_profile.orgName, m_profile.appName);
settings.beginGroup("MainWindow");
settings.setValue("geometry", saveGeometry());
settings.setValue("windowState", saveState());
settings.setValue("json", json);
settings.endGroup();
}
/**
* Read open editor tabs for current project.
*/
QStringList MainWindow::readTabs() const {
QStringList tabs;
QSettings settings(m_profile.orgName, m_profile.appName);
settings.beginReadArray("MainWindow/Project:" + m_state.projectPath);
auto size = settings.beginReadArray("openEditors");
for (int i = 0; i < size; i++) {
settings.setArrayIndex(i);
tabs.append(settings.value("filePath").toString());
}
settings.endArray();
return tabs;
}
/**
* Write open editor tabs for current project.
*/
void MainWindow::writeTabs(QStringList tabs) const {
QSettings settings(m_profile.orgName, m_profile.appName);
settings.beginWriteArray("MainWindow/Project:" + m_state.projectPath + "/openEditors");
for (int i = 0; i < tabs.size(); i++) {
settings.setArrayIndex(i);
settings.setValue("filePath", tabs[i]);
}
settings.endArray();
}
void MainWindow::openProject(QString projectPath) {
closeProject();
auto project = new Project(projectPath);
if (m_ctx.project) {
delete m_ctx.project;
m_ctx.project = nullptr;
}
m_ctx.project = project;
m_oxfsView = new OxFSModel(project->romFs());
m_projectExplorer->setModel(m_oxfsView);
connect(m_ctx.project, &Project::fileUpdated, m_oxfsView, &OxFSModel::updateFile);
m_importAction->setEnabled(true);
m_state.projectPath = projectPath;
// reopen tabs
auto openTabs = readTabs();
// clear open tabs
writeTabs({});
for (auto t : openTabs) {
try {
openFile(t, true);
} catch (const ox::Error &err) {
qInfo().nospace() << "Error opening tab: " << t << ", " << static_cast<int>(err) << ", " << err.file << ":" << err.line;
oxTracef("nostalgia::studio::MainWindow::openProject", "Error opening tab: {}, {}, {}:{}", static_cast<int>(err), static_cast<int>(err), err.file, err.line);
} catch (...) {
qInfo() << "Error opening tab: " << t;
oxTracef("nostalgia::studio::MainWindow::openProject", "Error opening tab: {}", t);
}
}
qInfo() << "Open project:" << projectPath;
}
void MainWindow::closeProject() {
// delete tabs
while (m_tabs->count()) {
auto tab = dynamic_cast<studio::Editor*>(m_tabs->widget(0));
m_undoGroup.removeStack(tab->undoStack());
m_tabs->removeTab(0);
delete tab;
}
if (m_ctx.project) {
disconnect(m_ctx.project, &Project::fileUpdated, m_oxfsView, &OxFSModel::updateFile);
delete m_ctx.project;
m_ctx.project = nullptr;
delete m_oxfsView;
m_oxfsView = nullptr;
}
if (m_projectExplorer->model()) {
delete m_projectExplorer->model();
}
m_projectExplorer->setModel(nullptr);
m_importAction->setEnabled(false);
m_state.projectPath = "";
}
void MainWindow::openFile(QString path, bool force) {
if (!force && readTabs().contains(path)) {
return;
}
const auto dotIdx = path.lastIndexOf('.') + 1;
if (dotIdx == -1) {
return;
}
const auto ext = path.mid(dotIdx);
const auto lastSlash = path.lastIndexOf('/') + 1;
auto tabName = path.mid(lastSlash);
if (m_editorMakers.contains(ext)) {
auto tab = m_editorMakers[ext].make(path);
m_tabs->addTab(tab, tabName);
m_undoGroup.addStack(tab->undoStack());
// save new tab to application state
auto openTabs = readTabs();
if (!openTabs.contains(path)) {
openTabs.append(path);
}
writeTabs(openTabs);
}
}
void MainWindow::onExit() {
writeState();
}
// private slots
void MainWindow::openProject() {
auto projectPath = QFileDialog::getExistingDirectory(this, tr("Select Project Directory..."), QDir::homePath());
if (projectPath != "") {
openProject(projectPath);
writeState();
}
}
void MainWindow::openFileSlot(QModelIndex file) {
auto path = static_cast<OxFSFile*>(file.internalPointer())->path();
return openFile(path);
}
void MainWindow::showNewWizard() {
const QString ProjectName = "projectName";
const QString ProjectPath = "projectPath";
Wizard wizard(tr("New..."));
auto ws = new WizardSelect();
wizard.addPage(ws);
// add new project wizard
ws->addOption(
WizardMaker {
tr("Project"),
[&wizard, ProjectName, ProjectPath]() {
QVector<QWizardPage*> pgs;
auto pg = new WizardFormPage();
pg->addLineEdit(tr("Project &Name:"), ProjectName + "*", "", [ProjectPath, pg, &wizard](QString projectName) {
auto projectPath = wizard.field(ProjectPath).toString();
auto path = projectPath + "/" + projectName;
if (!QDir(path).exists()) {
return 0;
} else {
pg->showValidationError(tr("This project directory already exists."));
return 1;
}
}
);
pg->addPathBrowse(tr("Project &Path:"), ProjectPath + "*", QDir::homePath(), QFileDialog::Directory);
pgs.push_back(pg);
pgs.push_back(new WizardConclusionPage(tr("Creating project: %1/%2"), {ProjectPath, ProjectName}));
return pgs;
},
[this, ProjectName, ProjectPath](QWizard *wizard) {
auto projectName = wizard->field(ProjectName).toString();
auto projectPath = wizard->field(ProjectPath).toString();
qInfo() << "Project creation: final step";
if (QDir(projectPath).exists()) {
auto path = projectPath + "/" + projectName;
if (!QDir(path).exists()) {
Project(path).create();
openProject(path);
qInfo() << "Project creation successful:" << path;
return 0;
} else {
qInfo() << "Project file exists:" << path;
return 1;
}
} else {
qInfo() << "Project destination directory does not exist:" << projectPath;
return 2;
}
}
}
);
// add module options
for (auto p : m_modules) {
for (auto w : p->newWizards(&m_ctx)) {
ws->addOption(w);
}
}
wizard.show();
wizard.exec();
}
void MainWindow::saveFile() {
m_currentEditor->save();
}
void MainWindow::exportFile() {
m_currentEditor->exportFile();
}
void MainWindow::cutAction() {
m_currentEditor->cut();
}
void MainWindow::copyAction() {
m_currentEditor->copy();
}
void MainWindow::pasteAction() {
m_currentEditor->paste();
}
void MainWindow::closeTab(int idx) {
auto tab = dynamic_cast<studio::Editor*>(m_tabs->widget(idx));
m_undoGroup.removeStack(tab->undoStack());
m_tabs->removeTab(idx);
delete tab;
// remove from open tabs list
auto tabs = readTabs();
tabs.removeAt(idx);
writeTabs(tabs);
}
void MainWindow::moveTab(int from, int to) {
// move tab in open tabs list
auto tabs = readTabs();
tabs.move(from, to);
writeTabs(tabs);
}
void MainWindow::changeTab(int idx) {
auto tab = dynamic_cast<studio::Editor*>(m_tabs->widget(idx));
if (m_currentEditor) {
disconnect(m_currentEditor, &Editor::unsavedChangesChanged, m_saveAction, &QAction::setEnabled);
disconnect(m_currentEditor, &Editor::unsavedChangesChanged, this, &MainWindow::markUnsavedChanges);
disconnect(m_currentEditor, &Editor::exportableChanged, m_exportAction, &QAction::setEnabled);
disconnect(m_currentEditor, &Editor::cutEnabledChanged, m_cutAction, &QAction::setEnabled);
disconnect(m_currentEditor, &Editor::copyEnabledChanged, m_copyAction, &QAction::setEnabled);
disconnect(m_currentEditor, &Editor::pasteEnabledChanged, m_pasteAction, &QAction::setEnabled);
}
m_currentEditor = tab;
if (m_currentEditor) {
m_saveAction->setEnabled(m_currentEditor->unsavedChanges());
connect(m_currentEditor, &Editor::unsavedChangesChanged, m_saveAction, &QAction::setEnabled);
connect(m_currentEditor, &Editor::unsavedChangesChanged, this, &MainWindow::markUnsavedChanges);
connect(m_currentEditor, &Editor::cutEnabledChanged, m_cutAction, &QAction::setEnabled);
connect(m_currentEditor, &Editor::copyEnabledChanged, m_copyAction, &QAction::setEnabled);
connect(m_currentEditor, &Editor::pasteEnabledChanged, m_pasteAction, &QAction::setEnabled);
m_exportAction->setEnabled(m_currentEditor->exportable());
connect(m_currentEditor, &Editor::exportableChanged, m_exportAction, &QAction::setEnabled);
m_undoGroup.setActiveStack(tab->undoStack());
} else {
m_undoGroup.setActiveStack(nullptr);
}
}
void MainWindow::markUnsavedChanges(bool unsavedChanges) {
auto idx = m_tabs->indexOf(m_currentEditor);
if (idx > -1) {
const auto path = m_currentEditor->itemName();
const auto lastSlash = path.lastIndexOf('/') + 1;
const auto tabName = path.mid(lastSlash);
if (unsavedChanges) {
m_tabs->setTabText(idx, tabName + "*");
} else {
m_tabs->setTabText(idx, tabName);
}
}
}
void MainWindow::showImportWizard() {
const QString BPP = "bpp";
Wizard wizard(tr("Import..."));
auto ws = new WizardSelect();
wizard.addPage(ws);
for (auto p : m_modules) {
for (auto w : p->importWizards(&m_ctx)) {
ws->addOption(w);
}
}
wizard.show();
wizard.exec();
}
void MainWindow::refreshProjectExplorer(QString path) {
if (m_oxfsView) {
m_oxfsView->updateFile(path);
}
}
}

View File

@ -1,172 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <functional>
#include <QDockWidget>
#include <QMainWindow>
#include <QModelIndex>
#include <QPoint>
#include <QPointer>
#include <QSharedPointer>
#include <QString>
#include <QTabWidget>
#include <QTreeView>
#include <QUndoGroup>
#include <QVector>
#include <ox/std/types.hpp>
#include "lib/context.hpp"
#include "lib/module.hpp"
#include "lib/project.hpp"
#include "builtinmodules.hpp"
#include "oxfstreeview.hpp"
namespace nostalgia::studio {
struct NostalgiaStudioState {
QString projectPath;
};
template<typename T>
ox::Error model(T *io, NostalgiaStudioState *obj) {
oxReturnError(io->setTypeInfo("net.drinkingtea.nostalgia.studio.NostalgiaStudioState", 1));
oxReturnError(io->field("project_path", &obj->projectPath));
return OxError(0);
}
struct NostalgiaStudioProfile {
QString appName;
QString orgName;
QVector<QString> modulesPath;
};
template<typename T>
ox::Error model(T *io, NostalgiaStudioProfile *obj) {
oxReturnError(io->setTypeInfo("net.drinkingtea.nostalgia.studio.NostalgiaStudioProfile", 3));
oxReturnError(io->field("app_name", &obj->appName));
oxReturnError(io->field("org_name", &obj->orgName));
oxReturnError(io->field("modules_path", &obj->modulesPath));
return OxError(0);
}
class MainWindow: public QMainWindow {
Q_OBJECT
private:
QString m_profilePath;
NostalgiaStudioProfile m_profile;
NostalgiaStudioState m_state;
QAction *m_importAction = nullptr;
QAction *m_saveAction = nullptr;
QAction *m_exportAction = nullptr;
QAction *m_cutAction = nullptr;
QAction *m_copyAction = nullptr;
QAction *m_pasteAction = nullptr;
Context m_ctx;
QPointer<QMenu> m_viewMenu;
QVector<QPointer<QDockWidget>> m_dockWidgets;
QTreeView *m_projectExplorer = nullptr;
QVector<Module*> m_modules;
QHash<QString, EditorMaker> m_editorMakers;
QPointer<OxFSModel> m_oxfsView = nullptr;
QTabWidget *m_tabs = nullptr;
QUndoGroup m_undoGroup;
Editor *m_currentEditor = nullptr;
public:
explicit MainWindow(QString profilePath);
~MainWindow() override;
private:
void loadModules();
void loadModuleDir(QString path);
void loadModule(QString path);
void loadModule(Module *module);
void setupMenu();
void setupProjectExplorer();
void addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget);
static QAction *addAction(QMenu *menu, QString text, QString toolTip, const QObject *tgt, const char *cb);
static QAction *addAction(QMenu *menu, QString text, QString toolTip,
QKeySequence::StandardKey key, const QObject *tgt, const char *cb);
static QAction *addAction(QMenu *menu, QString text, QString toolTip,
QKeySequence::StandardKey key, void (*cb)());
int readState();
void writeState();
/**
* Read open editor tabs for current project.
*/
[[nodiscard]] QStringList readTabs() const;
/**
* Write open editor tabs for current project.
*/
void writeTabs(QStringList tabs) const;
void openProject(QString);
void closeProject();
/**
* @param force forces the reopening of the file even if it is already open
*/
void openFile(QString path, bool force = false);
public slots:
void onExit();
private slots:
void openProject();
void openFileSlot(QModelIndex);
void saveFile();
void exportFile();
void cutAction();
void copyAction();
void pasteAction();
void closeTab(int idx);
void moveTab(int from, int to);
void changeTab(int idx);
void markUnsavedChanges(bool);
void showNewWizard();
void showImportWizard();
void refreshProjectExplorer(QString path);
};
}

View File

@ -1,220 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <algorithm>
#include <QDebug>
#include <QDir>
#include <QVector>
#include "oxfstreeview.hpp"
namespace nostalgia::studio {
OxFSFile::OxFSFile(ox::FileSystem *fs, const QString &path, OxFSFile *parentItem) {
m_path = path;
m_parentItem = parentItem;
if (!fs) {
return;
}
// find children
oxRequireT(stat, fs->stat(static_cast<const char*>(m_path.toUtf8())));
QVector<QString> ls;
if (stat.fileType == ox::FileType::Directory) {
oxRequireT(names, fs->ls(m_path.toUtf8()));
for (const auto &name : names) {
if (name[0] != '.') {
ls.push_back(name.c_str());
}
}
std::sort(ls.begin(), ls.end());
}
auto p = m_path;
// make sure ends with path separator
if (fs->stat(p.toUtf8().data()).value.fileType == ox::FileType::Directory &&
p.size() && p.back() != QDir::separator()) {
p += QDir::separator();
}
for (const auto &name : ls) {
if (name != "." && name != "..") {
const auto filePath = m_path.size() ? m_path + '/' + name : name;
const auto ch = new OxFSFile(fs, filePath, this);
m_childItems.push_back(ch);
}
}
}
OxFSFile::~OxFSFile() {
qDeleteAll(m_childItems);
}
void OxFSFile::appendChild(OxFSModel *model, QStringList pathItems, QString currentPath) {
if (!pathItems.empty()) {
const auto &target = pathItems[0];
currentPath += "/" + target;
int index = static_cast<int>(m_childItems.size());
for (int i = 0; i < m_childItems.size(); i++) {
if (m_childItems[i]->name() >= target) {
index = i;
break;
}
}
if (m_childItems.size() == index || m_childItems[index]->name() != target) {
auto idx = model->createIndex(row(), 0, this);
model->beginInsertRows(idx, index, index);
m_childItems.insert(index, new OxFSFile(nullptr, currentPath, this));
model->endInsertRows();
}
m_childItems[index]->appendChild(model, pathItems.mid(1), currentPath);
}
}
OxFSFile *OxFSFile::child(int row) {
if (row < m_childItems.size()) {
return m_childItems[row];
} else {
return nullptr;
}
}
int OxFSFile::childCount() const {
return static_cast<int>(m_childItems.size());
}
int OxFSFile::columnCount() const {
return 1;
}
QVariant OxFSFile::data(int) const {
return name();
}
int OxFSFile::row() const {
if (m_parentItem) {
return static_cast<int>(m_parentItem->m_childItems.indexOf(const_cast<OxFSFile*>(this)));
} else {
return 0;
}
}
OxFSFile *OxFSFile::parentItem() {
return m_parentItem;
}
QString OxFSFile::name() const {
return m_path.mid(m_path.lastIndexOf("/") + 1);
}
QString OxFSFile::path() const {
return "/" + m_path;
}
// OxFSModel
OxFSModel::OxFSModel(ox::FileSystem *fs, QObject*) {
m_rootItem = new OxFSFile(fs, "");
}
OxFSModel::~OxFSModel() {
if (m_rootItem) {
delete m_rootItem;
m_rootItem = nullptr;
}
}
QVariant OxFSModel::data(const QModelIndex &index, int role) const {
if (!index.isValid() || role != Qt::DisplayRole) {
return QVariant();
} else {
auto item = static_cast<OxFSFile*>(index.internalPointer());
return item->data(index.column());
}
}
Qt::ItemFlags OxFSModel::flags(const QModelIndex &index) const {
if (!index.isValid()) {
return {};
} else {
return QAbstractItemModel::flags(index);
}
}
QVariant OxFSModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
return m_rootItem->data(section);
} else {
return QVariant();
}
}
QModelIndex OxFSModel::index(int row, int column, const QModelIndex &parent) const {
if (!hasIndex(row, column, parent)) {
return QModelIndex();
}
OxFSFile *parentItem;
if (!parent.isValid()) {
parentItem = m_rootItem;
} else {
parentItem = static_cast<OxFSFile*>(parent.internalPointer());
}
OxFSFile *childItem = parentItem->child(row);
if (childItem) {
return createIndex(row, column, childItem);
} else {
return QModelIndex();
}
}
QModelIndex OxFSModel::parent(const QModelIndex &index) const {
if (!index.isValid()) {
return QModelIndex();
}
auto childItem = static_cast<OxFSFile*>(index.internalPointer());
auto parentItem = childItem->parentItem();
if (parentItem == m_rootItem) {
return QModelIndex();
}
return createIndex(parentItem->row(), 0, parentItem);
}
int OxFSModel::rowCount(const QModelIndex &parent) const {
if (parent.column() > 0) {
return 0;
}
OxFSFile *parentItem;
if (!parent.isValid()) {
parentItem = m_rootItem;
} else {
parentItem = static_cast<OxFSFile*>(parent.internalPointer());
}
return parentItem->childCount();
}
int OxFSModel::columnCount(const QModelIndex &parent) const {
if (parent.isValid()) {
return static_cast<OxFSFile*>(parent.internalPointer())->columnCount();
} else {
return m_rootItem->columnCount();
}
}
void OxFSModel::updateFile(const QString &path) {
auto pathItems = path.split("/").mid(1);
m_rootItem->appendChild(this, pathItems, "");
}
}

View File

@ -1,88 +0,0 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <QModelIndex>
#include <QVector>
#include <QVariant>
#include <ox/fs/fs.hpp>
namespace nostalgia::studio {
class OxFSFile {
private:
OxFSFile *m_parentItem = nullptr;
QString m_path;
QVector<OxFSFile*> m_childItems;
public:
OxFSFile(ox::FileSystem *fs, const QString &path, OxFSFile *parentItem = nullptr);
~OxFSFile();
void appendChild(class OxFSModel *model, QStringList pathItems, QString fullPath);
OxFSFile *child(int row);
[[nodiscard]]
int childCount() const;
[[nodiscard]]
int columnCount() const;
[[nodiscard]]
QVariant data(int column) const;
[[nodiscard]]
int row() const;
OxFSFile *parentItem();
[[nodiscard]]
QString name() const;
[[nodiscard]]
QString path() const;
};
class OxFSModel: public QAbstractItemModel {
Q_OBJECT
friend OxFSFile;
private:
OxFSFile *m_rootItem = nullptr;
public:
explicit OxFSModel(ox::FileSystem *fs, QObject *parent = nullptr);
~OxFSModel() override;
[[nodiscard]] QVariant data(const QModelIndex &index, int role) const override;
[[nodiscard]] Qt::ItemFlags flags(const QModelIndex &index) const override;
[[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
[[nodiscard]] QModelIndex index(int row, int column,
const QModelIndex &parent) const override;
[[nodiscard]] QModelIndex parent(const QModelIndex &index) const override;
[[nodiscard]] int rowCount(const QModelIndex &parent) const override;
[[nodiscard]] int columnCount(const QModelIndex &parent) const override;
public slots:
void updateFile(const QString &path);
};
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <imgui.h>
#include <nostalgia/common/bounds.hpp>
#include "projectexplorer.hpp"
namespace nostalgia {
void ProjectExplorer::draw(core::Context *ctx) noexcept {
const auto viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + 71));
ImGui::SetNextWindowSize(ImVec2(313, viewport->Size.y - 71));
const auto flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoSavedSettings;
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::Begin("ProjectExplorer", nullptr, flags);
ImGui::PopStyleVar();
ImGui::SetNextTreeNodeOpen(true);
if (m_treeModel) {
m_treeModel->draw(ctx);
}
ImGui::End();
}
void ProjectExplorer::setModel(ox::UniquePtr<ProjectTreeModel> model) noexcept {
m_treeModel = std::move(model);
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/std/memory.hpp>
#include "lib/widget.hpp"
#include "projecttreemodel.hpp"
namespace nostalgia {
class ProjectExplorer: public studio::Widget {
private:
ox::UniquePtr<ProjectTreeModel> m_treeModel;
public:
void draw(core::Context *ctx) noexcept override;
void setModel(ox::UniquePtr<ProjectTreeModel> model) noexcept;
};
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <imgui.h>
#include "projecttreemodel.hpp"
namespace nostalgia {
ProjectTreeModel::ProjectTreeModel(const ox::String &name,
ox::Vector<ox::UniquePtr<ProjectTreeModel>> children) noexcept {
m_name = name;
m_children = std::move(children);
}
ProjectTreeModel::ProjectTreeModel(ProjectTreeModel &&other) noexcept {
m_name = std::move(other.m_name);
m_children = std::move(other.m_children);
}
void ProjectTreeModel::draw(core::Context *ctx) noexcept {
constexpr auto dirFlags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (m_children.size()) {
if (ImGui::TreeNodeEx(m_name.c_str(), dirFlags)) {
for (auto &child : m_children) {
child->draw(ctx);
}
ImGui::TreePop();
}
} else {
if (ImGui::TreeNodeEx(m_name.c_str(), ImGuiTreeNodeFlags_Leaf)) {
ImGui::TreePop();
}
}
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include <nostalgia/core/context.hpp>
namespace nostalgia {
class ProjectTreeModel {
private:
ox::String m_name;
ox::Vector <ox::UniquePtr<ProjectTreeModel>> m_children;
public:
explicit ProjectTreeModel(const ox::String &name,
ox::Vector <ox::UniquePtr<ProjectTreeModel>> children) noexcept;
ProjectTreeModel(ProjectTreeModel &&other) noexcept;
void draw(core::Context *ctx) noexcept;
};
}

View File

@ -8,8 +8,8 @@
#pragma once
#include "lib/context.hpp"
#include "lib/editor.hpp"
#include "lib/module.hpp"
#include "lib/filedialog.hpp"
#include "lib/project.hpp"
#include "lib/wizard.hpp"
#include "lib/task.hpp"
#include "lib/undostack.hpp"

View File

@ -0,0 +1,144 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include <imgui.h>
#include <nostalgia/core/core.hpp>
#include "lib/configio.hpp"
#include "filedialogmanager.hpp"
#include "studioapp.hpp"
namespace nostalgia {
struct StudioConfig {
static constexpr auto TypeName = "net.drinkingtea.nostalgia.studio.StudioConfig";
static constexpr auto TypeVersion = 1;
ox::String projectPath;
};
template<typename T>
constexpr ox::Error model(T *h, StudioConfig *config) noexcept {
oxReturnError(h->field("project_path", &config->projectPath));
return OxError(0);
}
StudioUI::StudioUI() noexcept {
m_widgets.push_back(&m_projectExplorer);
if (const auto [config, err] = studio::readConfig<StudioConfig>(); !err) {
oxIgnoreError(openProject(config.projectPath));
} else {
oxErrf("Could not open studio config file: {}\n", err.msg);
}
}
static ox::Result<ox::UniquePtr<ProjectTreeModel>>
buildProjectTreeModel(const ox::String &name, const ox::String &path, ox::FileSystem *fs) noexcept {
ox::Vector<ox::UniquePtr<ProjectTreeModel>> outChildren;
oxRequire(stat, fs->stat(path.c_str()));
if (stat.fileType == ox::FileType::Directory) {
oxRequireM(children, fs->ls(path));
//std::sort(children.begin(), children.end());
for (const auto &childName : children) {
if (childName[0] != '.') {
const auto childPath = ox::sfmt("{}/{}", path, childName);
oxRequireM(child, buildProjectTreeModel(childName, childPath, fs));
outChildren.emplace_back(std::move(child));
}
}
}
return ox::make_unique<ProjectTreeModel>(name, std::move(outChildren));
}
void StudioUI::update(core::Context *ctx) noexcept {
m_taskRunner.update(ctx);
}
void StudioUI::draw(core::Context *ctx) noexcept {
drawMenu(ctx);
drawToolbar(ctx);
for (auto &w : m_widgets) {
w->draw(ctx);
}
if (m_aboutEnabled &&
ImGui::Begin("About", &m_aboutEnabled, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) {
ImGui::End();
}
}
void StudioUI::drawMenu(core::Context *ctx) noexcept {
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New...", "Ctrl+N")) {
}
if (ImGui::MenuItem("Open Project...", "Ctrl+O")) {
m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject));
}
if (ImGui::MenuItem("Save", "Ctrl+S", false, m_saveEnabled)) {
}
if (ImGui::MenuItem("Quit", "Ctrl+Q")) {
core::shutdown(ctx);
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Module", false)) {
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("About")) {
m_aboutEnabled = true;
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void StudioUI::drawToolbar(core::Context*) noexcept {
constexpr auto BtnWidth = 96;
constexpr auto BtnHeight = 32;
const auto viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(viewport->Pos.x, viewport->Pos.y + 20));
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, 48));
const auto flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoSavedSettings;
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
ImGui::Begin("MainToolbar", nullptr, flags);
ImGui::PopStyleVar();
{
ImGui::Button("New##MainToolbar", ImVec2(BtnWidth, BtnHeight));
ImGui::SameLine();
if (ImGui::Button("Open Project##MainToolbar", ImVec2(BtnWidth, BtnHeight))) {
m_taskRunner.add(new FileDialogManager(this, &StudioUI::openProject));
}
ImGui::SameLine();
ImGui::Button("Save##MainToolbar", ImVec2(BtnWidth, BtnHeight));
}
ImGui::End();
}
ox::Error StudioUI::openProject(const ox::String &path) noexcept {
m_project = ox::make_unique<studio::Project>(path);
m_project->fileAdded.connect(this, &StudioUI::refreshProjectTreeModel);
m_project->fileDeleted.connect(this, &StudioUI::refreshProjectTreeModel);
oxReturnError(studio::editConfig<StudioConfig>([path](StudioConfig *config) {
config->projectPath = path;
}));
return refreshProjectTreeModel();
}
ox::Error StudioUI::refreshProjectTreeModel(const ox::String&) noexcept {
oxRequireM(model, buildProjectTreeModel("Project", "/", m_project->romFs()));
m_projectExplorer.setModel(std::move(model));
return OxError(0);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2016 - 2021 gary@drinkingtea.net
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#pragma once
#include <ox/event/signal.hpp>
#include <ox/std/memory.hpp>
#include <ox/std/string.hpp>
#include "lib/project.hpp"
#include "lib/task.hpp"
#include "projectexplorer.hpp"
#include "projecttreemodel.hpp"
namespace nostalgia {
class StudioUI: public ox::SignalHandler {
friend class StudioUIDrawer;
private:
ox::UniquePtr<studio::Project> m_project;
studio::TaskRunner m_taskRunner;
ox::Vector<studio::Widget*> m_widgets;
ox::UniquePtr<ProjectTreeModel> m_treeModel;
ProjectExplorer m_projectExplorer;
bool m_saveEnabled = false;
bool m_aboutEnabled = false;
public:
StudioUI() noexcept;
void update(core::Context *ctx) noexcept;
protected:
void draw(core::Context *ctx) noexcept;
private:
void drawMenu(core::Context *ctx) noexcept;
void drawToolbar(core::Context *ctx) noexcept;
ox::Error openProject(const ox::String &path) noexcept;
ox::Error refreshProjectTreeModel(const ox::String& = {}) noexcept;
};
}

View File

@ -1,6 +0,0 @@
<RCC>
<qresource prefix="/profiles">
<file>nostalgia-studio.json</file>
<file>nostalgia-studio-dev.json</file>
</qresource>
</RCC>

View File

@ -19,5 +19,5 @@ install(
)
if(NOSTALGIA_BUILD_STUDIO)
add_subdirectory(studio)
#add_subdirectory(studio)
endif()