[nostalgia/studio] Start on ImGui version of Studio
This commit is contained in:
parent
6160335af3
commit
ddd63bc45f
@ -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(
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
)
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
47
src/nostalgia/studio/filedialogmanager.cpp
Normal file
47
src/nostalgia/studio/filedialogmanager.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
50
src/nostalgia/studio/filedialogmanager.hpp
Normal file
50
src/nostalgia/studio/filedialogmanager.hpp
Normal 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...);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
13
src/nostalgia/studio/lib/configio.cpp
Normal file
13
src/nostalgia/studio/lib/configio.cpp
Normal 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"
|
||||
|
||||
|
||||
|
||||
|
105
src/nostalgia/studio/lib/configio.hpp
Normal file
105
src/nostalgia/studio/lib/configio.hpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
32
src/nostalgia/studio/lib/filedialog.mm
Normal file
32
src/nostalgia/studio/lib/filedialog.mm
Normal 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);
|
||||
}
|
||||
|
||||
}
|
45
src/nostalgia/studio/lib/filedialog_gtk.cpp
Normal file
45
src/nostalgia/studio/lib/filedialog_gtk.cpp
Normal 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
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
25
src/nostalgia/studio/lib/task.cpp
Normal file
25
src/nostalgia/studio/lib/task.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
36
src/nostalgia/studio/lib/task.hpp
Normal file
36
src/nostalgia/studio/lib/task.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
29
src/nostalgia/studio/lib/undostack.cpp
Normal file
29
src/nostalgia/studio/lib/undostack.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
37
src/nostalgia/studio/lib/undostack.hpp
Normal file
37
src/nostalgia/studio/lib/undostack.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
13
src/nostalgia/studio/lib/widget.cpp
Normal file
13
src/nostalgia/studio/lib/widget.cpp
Normal 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 {
|
||||
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
13
src/nostalgia/studio/lib/window.cpp
Normal file
13
src/nostalgia/studio/lib/window.cpp
Normal 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"
|
||||
|
||||
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
};
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -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, "");
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
||||
};
|
||||
|
||||
}
|
41
src/nostalgia/studio/projectexplorer.cpp
Normal file
41
src/nostalgia/studio/projectexplorer.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
27
src/nostalgia/studio/projectexplorer.hpp
Normal file
27
src/nostalgia/studio/projectexplorer.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
42
src/nostalgia/studio/projecttreemodel.cpp
Normal file
42
src/nostalgia/studio/projecttreemodel.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
31
src/nostalgia/studio/projecttreemodel.hpp
Normal file
31
src/nostalgia/studio/projecttreemodel.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
@ -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"
|
||||
|
144
src/nostalgia/studio/studioapp.cpp
Normal file
144
src/nostalgia/studio/studioapp.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
52
src/nostalgia/studio/studioapp.hpp
Normal file
52
src/nostalgia/studio/studioapp.hpp
Normal 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;
|
||||
};
|
||||
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/profiles">
|
||||
<file>nostalgia-studio.json</file>
|
||||
<file>nostalgia-studio-dev.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
@ -19,5 +19,5 @@ install(
|
||||
)
|
||||
|
||||
if(NOSTALGIA_BUILD_STUDIO)
|
||||
add_subdirectory(studio)
|
||||
#add_subdirectory(studio)
|
||||
endif()
|
||||
|
Loading…
x
Reference in New Issue
Block a user