diff --git a/CMakeLists.txt b/CMakeLists.txt index f6791e816..a83a0d07a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,9 @@ set(RGM_SOURCES Components/Utility.cpp Components/QMenuView.cpp Components/RecentFiles.cpp + Components/GitTreeItem.cpp + Components/GitTreeStyledDelegate.cpp + Components/EGMManager.cpp Models/TreeSortFilterProxyModel.cpp Models/MessageModel.cpp Models/RepeatedImageModel.cpp @@ -72,6 +75,7 @@ set(RGM_SOURCES Models/EventsListModel.cpp Models/EventTypesListModel.cpp Models/EventTypesListSortFilterProxyModel.cpp + Models/GitHistoryModel.cpp Editors/ObjectEditor.cpp Editors/PathEditor.cpp Editors/CodeEditor.cpp @@ -112,6 +116,9 @@ set(RGM_HEADERS Components/ArtManager.h Components/QMenuView_p.h Components/Utility.h + Components/GitTreeItem.h + Components/GitTreeStyledDelegate.h + Components/EGMManager.h Models/TreeModel.h Models/RepeatedStringModel.h Models/ResourceModelMap.h @@ -126,6 +133,7 @@ set(RGM_HEADERS Models/EventsListModel.h Models/EventTypesListModel.h Models/EventTypesListSortFilterProxyModel.h + Models/GitHistoryModel.cpp Editors/FontEditor.h Editors/PathEditor.h Editors/SpriteEditor.h @@ -276,6 +284,12 @@ find_package(Freetype REQUIRED) include_directories(${FREETYPE_INCLUDE_DIRS}) target_link_libraries(${EXE} PRIVATE ${FREETYPE_LIBRARIES}) +# Find libgit2 +find_path(GIT2_INCLUDE_PATH NAMES git2.h) +include_directories(${GIT2_INCLUDE_PATH}) +find_library(GIT2_LIBRARIES git2) +target_link_libraries(${EXE} PRIVATE ${GIT2_LIBRARIES}) + # Find JPEG find_library(LIB_JPEG NAMES jpeg) target_link_libraries(${EXE} PRIVATE ${LIB_JPEG}) diff --git a/Components/EGMManager.cpp b/Components/EGMManager.cpp new file mode 100644 index 000000000..44b4de95d --- /dev/null +++ b/Components/EGMManager.cpp @@ -0,0 +1,239 @@ +#include "MainWindow.h" +#include "EGMManager.h" +#include "gmk.h" +#include "gmx.h" +#include "yyp.h" +#include "filesystem.h" + +#include +#include +#include +#include +#include + +EGMManager::EGMManager() : _repo(nullptr) { + LoadEventData(MainWindow::EnigmaRoot.absolutePath() + "/events.ey"); + qDebug() << "Intializing git library"; + git_libgit2_init(); + connect(&_gitHistoryModel, &GitHistoryModel::GitError, this, &EGMManager::GitError); +} + +EGMManager::~EGMManager() { + git_tree_free(_git_tree); + git_signature_free(_git_sig); + git_repository_free(_repo); + git_libgit2_shutdown(); +} + +buffers::Project* EGMManager::NewProject() { + _resChangeModel.ClearChanges(); + + _project = std::make_unique(); + QTemporaryDir dir; + dir.setAutoRemove(false); + _rootPath = dir.path(); + InitRepo(); + return _project.get(); +} + +buffers::Project* EGMManager::LoadProject(const QString& fPath) { + _resChangeModel.ClearChanges(); + + QFileInfo fileInfo(fPath); + const QString suffix = fileInfo.suffix(); + + _project = egm::LoadProject(fPath.toStdString()); + + return _project.get(); +} + +buffers::Game* EGMManager::GetGame() { return _project->mutable_game(); } + +ResourceChangesModel& EGMManager::GetResourceChangesModel() { + return _resChangeModel; +} + +GitHistoryModel& EGMManager::GetGitHistoryModel() { + return _gitHistoryModel; +} + +void EGMManager::ResourceChanged(Resource& res, ResChange change, const QString& oldName) { + _resChangeModel.ResourceChanged(res, change, oldName); + + QString dir =_rootPath;// + "get the res path somehow???"; + + // Clear the existing folder I guess + //if (FolderExists(dir.toStdString())) RemoveDir(dir); + + if (change != ResChange::Removed) { + egm::WriteResource(res.buffer, dir.toStdString()); + //AddFile(res.name + ".ext"); ?? + } +} + +bool EGMManager::LoadEventData(const QString& fPath) { + QFile f(fPath); + if (f.exists()) { + _event_data = std::make_unique(ParseEventFile(fPath.toStdString())); + } else { + qDebug() << "Error: Failed to load events file. Loading internal events.ey."; + QFile internal_events(":/events.ey"); + internal_events.open(QIODevice::ReadOnly | QFile::Text); + std::stringstream ss; + ss << internal_events.readAll().toStdString(); + _event_data = std::make_unique(ParseEventFile(ss)); + } + + egm::LibEGMInit(_event_data.get()); + + return _event_data->events().size() > 0; +} + +EventData* EGMManager::GetEventData() { return _event_data.get(); } + +git_commit* EGMManager::GitLookUp(const git_oid* id) { + git_commit* commit = nullptr; + int error = git_commit_lookup(&commit, _repo, id); + if (error != 0) GitError(); + return commit; +} + +bool EGMManager::InitRepo() { + qDebug() << "Intializing git repository at: " << _rootPath; + git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT; + opts.description = "ENIGMA Game"; + int error = git_repository_init_ext(&_repo, _rootPath.toStdString().c_str(), &opts); + if (error != 0) { + GitError(); + return false; + } + + error = git_signature_default(&_git_sig, _repo); + if (error != 0) { + GitError(); + return false; + } + + error = git_repository_index(&_git_index, _repo); + if (error != 0) { + GitError(); + return false; + } + error = git_index_write_tree(&_git_tree_id, _git_index); + if (error != 0) { + GitError(); + return false; + } + error = git_tree_lookup(&_git_tree, _repo, &_git_tree_id); + if (error != 0) { + GitError(); + return false; + } + + error = git_commit_create_v(&_git_commit_id, _repo, "HEAD", _git_sig, _git_sig, NULL, "Initial commit", _git_tree, 0); + if (error != 0) { + GitError(); + return false; + } + + _gitHistoryModel.LoadRepo(_repo); + + return CreateBranch("RadialGM") && Checkout("refs/heads/RadialGM"); +} + +bool EGMManager::CreateBranch(const QString& branchName) { + qDebug() << "Creating git branch: " << branchName; + git_commit* c = GitLookUp(&_git_commit_id); + git_reference* ref = nullptr; + int error = git_branch_create(&ref, _repo, branchName.toStdString().c_str(), c, 0); + git_reference_free(ref); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +bool EGMManager::Checkout(const QString& ref) { + qDebug() << "Checking out git reference: " << ref; + int error = git_repository_set_head(_repo, ref.toStdString().c_str()); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +/*bool EGMManager::CheckoutFile(const QString& ref, const QString& file) { +}*/ + +bool EGMManager::AddFile(const QString& file) { + qDebug() << "Adding file to EGM: " << file; + int error = git_index_add_bypath(_git_index, file.toStdString().c_str()); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +bool EGMManager::RemoveFile(const QString& file) { + qDebug() << "Removing file from EGM: " << file; + int error = git_index_remove_bypath(_git_index, file.toStdString().c_str()); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +bool EGMManager::RemoveDir(const QString& dir) { + qDebug() << "Removing directory from EGM: " << dir; + DeleteFolder(dir.toStdString()); + int error = git_index_remove_directory(_git_index, dir.toStdString().c_str(), 0); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +bool EGMManager::CommitChanges(const QString& message) { + qDebug() << "Commiting changes"; + int error = git_commit_create_v(&_git_commit_id, _repo, "HEAD", _git_sig, _git_sig, NULL, + message.toStdString().c_str(), _git_tree, 0); + if (error != 0) { + GitError(); + return false; + } + return true; +} + +/*bool EGMManager::AddRemote(const QString& url) { + +} + +bool EGMManager::ChangeRemote(unsigned index, const QString& url) { + +} + +bool EGMManager::RemoveRemote(unsigned index) { + +} + +const QStringList& EGMManager::GetRemotes() const { + +} + +bool EGMManager::PushChanges() { + +}*/ + +bool EGMManager::Save(const QString& fPath) { + return false; +} + +void EGMManager::GitError() { + const git_error* e = git_error_last(); + qDebug() << QString("Git Error: ") + e->message; +} diff --git a/Components/EGMManager.h b/Components/EGMManager.h new file mode 100644 index 000000000..968585b97 --- /dev/null +++ b/Components/EGMManager.h @@ -0,0 +1,66 @@ +#ifndef EGMMANAGER_H +#define EGMMANAGER_H + +#include "Models/ResourceChangesModel.h" +#include "Models/GitHistoryModel.h" + +#include "event_reader/event_parser.h" +#include "egm.h" + +#include +#include +#include + +#include + +class EGMManager : public QObject { + Q_OBJECT +public: + EGMManager(); + ~EGMManager(); + buffers::Project* NewProject(); + buffers::Project* LoadProject(const QString& fPath); + buffers::Game *GetGame(); + ResourceChangesModel& GetResourceChangesModel(); + GitHistoryModel& GetGitHistoryModel(); + void ResourceChanged(Resource &res, ResChange change, const QString &oldName); + bool LoadEventData(const QString& fPath); + EventData* GetEventData(); + bool InitRepo(); + git_commit* GitLookUp(const git_oid* id); + bool CreateBranch(const QString& branchName); + bool Checkout(const QString& ref); + //bool CheckoutFile(const QString& ref, const QString& file); + bool AddFile(const QString& file); + bool RemoveFile(const QString& file); + bool RemoveDir(const QString& dir); + bool CommitChanges(const QString& message); + /*bool AddRemote(const QString& url); + bool ChangeRemote(unsigned index, const QString& url); + bool RemoveRemote(unsigned index); + const QStringList& GetRemotes() const; + bool PushChanges();*/ + +signals: + QStringList FilesEditedExternally(); + QStringList ConflictedFilesDetected(); + +public slots: + void GitError(); + bool Save(const QString& fPath); + +protected: + std::unique_ptr _project; + ResourceChangesModel _resChangeModel; + GitHistoryModel _gitHistoryModel; + QStringList _remoteURLs; + std::unique_ptr _event_data; + QString _rootPath; + git_repository* _repo; + git_signature* _git_sig; + git_index* _git_index; + git_oid _git_tree_id, _git_commit_id; + git_tree* _git_tree; +}; + +#endif // EGMMANAGER_H diff --git a/Components/GitTreeItem.cpp b/Components/GitTreeItem.cpp new file mode 100644 index 000000000..e98e5d3d4 --- /dev/null +++ b/Components/GitTreeItem.cpp @@ -0,0 +1,18 @@ +#include "GitTreeItem.h" +#include + +GitTreeItem::GitTreeItem() {} + +constexpr int scale = 8; + +void GitTreeItem::paint(QPainter *painter, const QRect &rect, const QPalette &palette, EditMode mode) const { + painter->save(); + painter->setBrush(QBrush(Qt::black)); + painter->drawEllipse(rect.left() + (rect.width() - scale)/2, rect.top() + (rect.height() - scale)/2, scale, scale); + painter->drawLine(rect.left() + (rect.width())/2, rect.top(), rect.left() + (rect.width())/2, rect.bottom()); + painter->restore(); +} + +QSize GitTreeItem::sizeHint() const { + return QSize(scale, scale); +} diff --git a/Components/GitTreeItem.h b/Components/GitTreeItem.h new file mode 100644 index 000000000..e580bf4d9 --- /dev/null +++ b/Components/GitTreeItem.h @@ -0,0 +1,18 @@ +#ifndef GITTREEITEM_H +#define GITTREEITEM_H + +#include +#include + +class GitTreeItem +{ +public: + enum class EditMode { ReadOnly }; + GitTreeItem(); + void paint(QPainter *painter, const QRect &rect, const QPalette &palette, EditMode mode) const; + QSize sizeHint() const; +}; + +Q_DECLARE_METATYPE(GitTreeItem) + +#endif // GITTREEITEM_H diff --git a/Components/GitTreeStyledDelegate.cpp b/Components/GitTreeStyledDelegate.cpp new file mode 100644 index 000000000..bcf941fac --- /dev/null +++ b/Components/GitTreeStyledDelegate.cpp @@ -0,0 +1,20 @@ +#include "GitTreeStyledDelegate.h" +#include "GitTreeItem.h" + +#include + +GitTreeStyledDelegate::GitTreeStyledDelegate(QObject* parent) : QStyledItemDelegate(parent) {} + +void GitTreeStyledDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + if (index.data().canConvert()) { + GitTreeItem i = qvariant_cast(index.data()); + i.paint(painter, option.rect, option.palette, GitTreeItem::EditMode::ReadOnly); + } else QStyledItemDelegate::paint(painter, option, index); +} + +QSize GitTreeStyledDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + if (index.data().canConvert()) { + GitTreeItem i = qvariant_cast(index.data()); + return i.sizeHint(); + } else return QStyledItemDelegate::sizeHint(option, index); +} diff --git a/Components/GitTreeStyledDelegate.h b/Components/GitTreeStyledDelegate.h new file mode 100644 index 000000000..a5209d1ff --- /dev/null +++ b/Components/GitTreeStyledDelegate.h @@ -0,0 +1,15 @@ +#ifndef GITTREESTYLEDDELEGATE_H +#define GITTREESTYLEDDELEGATE_H + +#include + +class GitTreeStyledDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + GitTreeStyledDelegate(QObject* parent = nullptr); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + +#endif // GITTREESTYLEDDELEGATE_H diff --git a/Editors/BaseEditor.cpp b/Editors/BaseEditor.cpp index 28d9180a6..077e47b94 100644 --- a/Editors/BaseEditor.cpp +++ b/Editors/BaseEditor.cpp @@ -1,5 +1,6 @@ #include "BaseEditor.h" #include "Models/MessageModel.h" +#include "MainWindow.h" #include #include @@ -27,6 +28,9 @@ void BaseEditor::closeEvent(QCloseEvent* event) { return; } else if (reply == QMessageBox::No) { _nodeMapper->clearMapping(); + buffers::TreeNode* n = static_cast(_nodeMapper->GetModel()->GetBuffer()); + Resource res = {QString::fromStdString(n->name()), n, _model}; + MainWindow::ResourceChanged(res, ResChange::Reverted); if (!_resMapper->RestoreBackup()) { // This should never happen but here incase someone decides to incorrectly null the backup qDebug() << "Failed to revert editor changes"; @@ -49,6 +53,8 @@ void BaseEditor::dataChanged(const QModelIndex& topLeft, const QModelIndex& /*bo emit ResourceRenamed(n->type_case(), oldValue.toString(), QString::fromStdString(n->name())); } _resMapper->SetDirty(true); + Resource res = {QString::fromStdString(n->name()), n, _model}; + MainWindow::ResourceChanged(res, ResChange::Modified); } void BaseEditor::RebindSubModels() { diff --git a/MainWindow.cpp b/MainWindow.cpp index a6fc240d4..7807f9223 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -18,6 +18,7 @@ #include "Components/ArtManager.h" #include "Components/Logger.h" +#include "Components/GitTreeStyledDelegate.h" #include "Plugins/RGMPlugin.h" @@ -25,26 +26,22 @@ #include "Plugins/ServerPlugin.h" #endif -#include "gmk.h" -#include "gmx.h" -#include "yyp.h" - #include +#include #include #include -#include -#include #undef GetMessage -QList MainWindow::EnigmaSearchPaths = {QDir::currentPath(), "./enigma-dev", "../enigma-dev", "../RadialGM/Submodules/enigma-dev"}; +QList MainWindow::EnigmaSearchPaths = {QDir::currentPath(), "./enigma-dev", "../enigma-dev", + "../RadialGM/Submodules/enigma-dev"}; QFileInfo MainWindow::EnigmaRoot = MainWindow::getEnigmaRoot(); QList MainWindow::systemCache; MainWindow *MainWindow::_instance = nullptr; +EGMManager MainWindow::egmManager; QScopedPointer MainWindow::resourceMap; QScopedPointer MainWindow::treeModel; -std::unique_ptr MainWindow::_event_data; static QTextEdit *diagnosticTextEdit = nullptr; static QAction *toggleDiagnosticsAction = nullptr; @@ -94,7 +91,7 @@ QFileInfo MainWindow::getEnigmaRoot() { auto entryList = dir.entryInfoList(QStringList({"ENIGMAsystem"}), filters, QDir::SortFlag::NoSort); if (!entryList.empty()) { EnigmaRoot = entryList.first(); - break; + break; } } @@ -103,19 +100,6 @@ QFileInfo MainWindow::getEnigmaRoot() { MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), _ui(new Ui::MainWindow){ - if (!EnigmaRoot.filePath().isEmpty()) { - _event_data = std::make_unique(ParseEventFile((EnigmaRoot.absolutePath() + "/events.ey").toStdString())); - } else { - qDebug() << "Error: Failed to locate ENIGMA sources. Loading internal events.ey.\n" << "Search Paths:\n" << MainWindow::EnigmaSearchPaths; - QFile internal_events(":/events.ey"); - internal_events.open(QIODevice::ReadOnly | QFile::Text); - std::stringstream ss; - ss << internal_events.readAll().toStdString(); - _event_data = std::make_unique(ParseEventFile(ss)); - } - - egm::LibEGMInit(_event_data.get()); - ArtManager::Init(); _instance = this; @@ -126,6 +110,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), _ui(new Ui::MainW setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); _ui->setupUi(this); + this->tabifyDockWidget(_ui->outputDockWidget, _ui->gitDockWidget); QToolBar *outputTB = new QToolBar(this); outputTB->setIconSize(QSize(24, 24)); @@ -185,10 +170,19 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), _ui(new Ui::MainW connect(_ui->actionCreateExecutable, &QAction::triggered, pluginServer, &RGMPlugin::CreateExecutable); #endif + _ui->gitLogView->setModel(&egmManager.GetGitHistoryModel()); + _ui->gitLogView->setItemDelegate(new GitTreeStyledDelegate); + _ui->gitLogView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + + _ui->changesListView->setModel(&egmManager.GetResourceChangesModel()); + openNewProject(); } -MainWindow::~MainWindow() { diagnosticTextEdit = nullptr; delete _ui; } +MainWindow::~MainWindow() { + diagnosticTextEdit = nullptr; + delete _ui; +} void MainWindow::setCurrentConfig(const buffers::resources::Settings &settings) { emit _instance->CurrentConfigChanged(settings); @@ -293,9 +287,9 @@ void MainWindow::updateWindowMenu() { numberString = numberString.insert(numberString.length() - 1, '&'); QString text = tr("%1 %2").arg(numberString).arg(windowTitle); - QAction *action = _ui->menuWindow->addAction( - mdiSubWindow->windowIcon(), text, mdiSubWindow, - [this, mdiSubWindow]() { _ui->mdiArea->setActiveSubWindow(mdiSubWindow); }); + QAction *action = + _ui->menuWindow->addAction(mdiSubWindow->windowIcon(), text, mdiSubWindow, + [this, mdiSubWindow]() { _ui->mdiArea->setActiveSubWindow(mdiSubWindow); }); windowActions.append(action); action->setCheckable(true); action->setChecked(mdiSubWindow == _ui->mdiArea->activeSubWindow()); @@ -303,9 +297,7 @@ void MainWindow::updateWindowMenu() { } void MainWindow::openFile(QString fName) { - QFileInfo fileInfo(fName); - - std::unique_ptr loadedProject = egm::LoadProject(fName.toStdString()); + buffers::Project* loadedProject = egmManager.LoadProject(fName); if (!loadedProject) { QMessageBox::warning(this, tr("Failed To Open Project"), tr("There was a problem loading the project: ") + fName, @@ -313,14 +305,14 @@ void MainWindow::openFile(QString fName) { return; } - MainWindow::setWindowTitle(fileInfo.fileName() + " - ENIGMA"); + MainWindow::setWindowTitle(fName + " - ENIGMA"); _recentFiles->prependFile(fName); - openProject(std::move(loadedProject)); + openProject(loadedProject); } void MainWindow::openNewProject() { MainWindow::setWindowTitle(tr(" - ENIGMA")); - auto newProject = std::unique_ptr(new buffers::Project()); + auto newProject = egmManager.NewProject(); auto *root = newProject->mutable_game()->mutable_root(); QList defaultGroups = {tr("Sprites"), tr("Sounds"), tr("Backgrounds"), tr("Paths"), tr("Scripts"), tr("Shaders"), tr("Fonts"), tr("Timelines"), @@ -330,17 +322,15 @@ void MainWindow::openNewProject() { groupNode->set_folder(true); groupNode->set_name(groupName.toStdString()); } - openProject(std::move(newProject)); + openProject(newProject); } -void MainWindow::openProject(std::unique_ptr openedProject) { +void MainWindow::openProject(buffers::Project* openedProject) { this->_ui->mdiArea->closeAllSubWindows(); ArtManager::clearCache(); - _project = std::move(openedProject); - - resourceMap.reset(new ResourceModelMap(_project->mutable_game()->mutable_root(), nullptr)); - treeModel.reset(new TreeModel(_project->mutable_game()->mutable_root(), resourceMap.get(), nullptr)); + resourceMap.reset(new ResourceModelMap(openedProject->mutable_game()->mutable_root(), nullptr)); + treeModel.reset(new TreeModel(openedProject->mutable_game()->mutable_root(), resourceMap.get(), nullptr)); _ui->treeView->setModel(treeModel.get()); treeModel->connect(treeModel.get(), &TreeModel::ResourceRenamed, resourceMap.get(), @@ -394,7 +384,9 @@ void MainWindow::on_actionCloseOthers_triggered() { } } -void MainWindow::on_actionToggleTabbedView_triggered() { this->setTabbedMode(_ui->actionToggleTabbedView->isChecked()); } +void MainWindow::on_actionToggleTabbedView_triggered() { + this->setTabbedMode(_ui->actionToggleTabbedView->isChecked()); +} void MainWindow::on_actionNext_triggered() { _ui->mdiArea->activateNextSubWindow(); } @@ -424,8 +416,7 @@ void MainWindow::on_actionExploreENIGMA_triggered() { QDesktopServices::openUrl( void MainWindow::on_actionAbout_triggered() { QMessageBox aboutBox(QMessageBox::Information, tr("About"), - tr("ENIGMA is a free, open-source, and cross-platform game engine."), - QMessageBox::Ok, this); + tr("ENIGMA is a free, open-source, and cross-platform game engine."), QMessageBox::Ok, this); QAbstractButton *aboutQtButton = aboutBox.addButton(tr("About Qt"), QMessageBox::HelpRole); aboutBox.exec(); @@ -566,12 +557,9 @@ void MainWindow::on_actionDelete_triggered() { selectedNames += (node == *selectedNodes.begin() ? "" : ", ") + QString::fromStdString(node->name()); } - QMessageBox mb( - QMessageBox::Icon::Question, - tr("Delete Resources"), - tr("Do you want to delete the selected resources from the project?"), - QMessageBox::Yes | QMessageBox::No, this - ); + QMessageBox mb(QMessageBox::Icon::Question, tr("Delete Resources"), + tr("Do you want to delete the selected resources from the project?"), + QMessageBox::Yes | QMessageBox::No, this); mb.setDetailedText(selectedNames); int ret = mb.exec(); if (ret != QMessageBox::Yes) return; @@ -599,3 +587,7 @@ void MainWindow::on_actionSortByName_triggered() { void MainWindow::on_treeView_customContextMenuRequested(const QPoint &pos) { _ui->menuEdit->exec(_ui->treeView->mapToGlobal(pos)); } + +void MainWindow::ResourceChanged(Resource &res, ResChange change, QString oldName) { + egmManager.ResourceChanged(res, change, oldName); +} diff --git a/MainWindow.h b/MainWindow.h index 4b946813c..b0038df12 100644 --- a/MainWindow.h +++ b/MainWindow.h @@ -6,19 +6,18 @@ #include "Models/TreeModel.h" class MainWindow; +#include "Components/EGMManager.h" #include "Components/RecentFiles.h" #include "project.pb.h" #include "server.pb.h" -#include "event_reader/event_parser.h" -#include "egm.h" +#include #include #include #include #include #include -#include namespace Ui { class MainWindow; @@ -27,6 +26,21 @@ class MainWindow; class MainWindow : public QMainWindow { Q_OBJECT + private: + void closeEvent(QCloseEvent *event) override; + + static MainWindow *_instance; + static EGMManager egmManager; + QHash _subWindows; + Ui::MainWindow *_ui; + QPointer _recentFiles; + + void openSubWindow(buffers::TreeNode *item); + void readSettings(); + void writeSettings(); + void setTabbedMode(bool enabled); + static QFileInfo getEnigmaRoot(); + public: static QScopedPointer resourceMap; static QScopedPointer treeModel; @@ -34,12 +48,13 @@ class MainWindow : public QMainWindow { explicit MainWindow(QWidget *parent); ~MainWindow(); - void openProject(std::unique_ptr openedProject); - buffers::Game *Game() const { return this->_project->mutable_game(); } + void openProject(buffers::Project* openedProject); + buffers::Game *Game() const { return egmManager.GetGame(); } + static EventData *GetEventData() { return egmManager.GetEventData(); } + static void ResourceChanged(Resource &res, ResChange change, QString oldName = ""); static QList EnigmaSearchPaths; static QFileInfo EnigmaRoot; - static EventData* GetEventData() { return _event_data.get(); } signals: void CurrentConfigChanged(const buffers::resources::Settings &settings); @@ -101,26 +116,6 @@ class MainWindow : public QMainWindow { void on_treeView_doubleClicked(const QModelIndex &index); void on_treeView_customContextMenuRequested(const QPoint &pos); - - private: - void closeEvent(QCloseEvent *event) override; - - static MainWindow *_instance; - - QHash _subWindows; - - Ui::MainWindow *_ui; - - std::unique_ptr _project; - QPointer _recentFiles; - - static std::unique_ptr _event_data; - - void openSubWindow(buffers::TreeNode *item); - void readSettings(); - void writeSettings(); - void setTabbedMode(bool enabled); - static QFileInfo getEnigmaRoot(); }; #endif // MAINWINDOW_H diff --git a/MainWindow.ui b/MainWindow.ui index b8926488a..9f84f5a46 100644 --- a/MainWindow.ui +++ b/MainWindow.ui @@ -100,7 +100,7 @@ 0 0 1200 - 31 + 18 @@ -497,6 +497,95 @@ + + + Git + + + 8 + + + + + + + Qt::Horizontal + + + + + + + + + + 24 + + + + + + + + + 1 + 0 + + + + true + + + false + + + false + + + + + + + + + + ... + + + + :/actions/add.png:/actions/add.png + + + + + + + ... + + + + :/actions/find.png:/actions/find.png + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/Models/GitHistoryModel.cpp b/Models/GitHistoryModel.cpp new file mode 100644 index 000000000..9aa29012c --- /dev/null +++ b/Models/GitHistoryModel.cpp @@ -0,0 +1,83 @@ +#include "GitHistoryModel.h" +#include "Components/GitTreeItem.h" + +#include + +GitHistoryModel::GitHistoryModel(QObject* parent) : QAbstractTableModel(parent) {} + +GitHistoryModel::~GitHistoryModel() { + for (git_commit* c : _commits) + git_commit_free(c); +} + +void GitHistoryModel::LoadRepo(git_repository *repo) { + beginResetModel(); + + for (git_commit* c : _commits) + git_commit_free(c); + + _commits.clear(); + + git_revwalk *walker; + + int error = git_revwalk_new(&walker, repo); + if (error != 0) emit GitError(); + + error = git_revwalk_push_head(walker); + if (error != 0) emit GitError(); + + error = git_revwalk_push_ref(walker, "HEAD"); + if (error != 0) emit GitError(); + + git_revwalk_sorting(walker, GIT_SORT_NONE); + + git_oid oid; + while (!git_revwalk_next(&oid, walker)) { + git_commit* commit = nullptr; + git_commit_lookup(&commit, repo, &oid); + _commits.append(commit); + } + + git_revwalk_free(walker); + + endResetModel(); +} + +int GitHistoryModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) return 0; + return _commits.count(); +} + +int GitHistoryModel::columnCount(const QModelIndex &parent) const { + if (parent.isValid()) return 0; + return 5; +} + +QVariant GitHistoryModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role != Qt::DisplayRole || orientation != Qt::Orientation::Horizontal) return QVariant(); + + switch(section) { + case 0: return tr("Tree"); + case 1: return tr("ID"); + case 2: return tr("Description"); + case 3: return tr("Author"); + case 4: return tr("Email"); + } + + return QVariant(); +} + +QVariant GitHistoryModel::data(const QModelIndex &index, int role) const { + if (role != Qt::DisplayRole) return QVariant(); + git_commit* commit = _commits[index.row()]; + + switch(index.column()) { + case 0: return QVariant::fromValue(GitTreeItem()); + case 1: return git_oid_tostr_s(git_commit_id(commit)); + case 2: return git_commit_summary(commit); + case 3: return git_commit_author(commit)->name; + case 4: return git_commit_author(commit)->email; + } + + return QVariant(); +} diff --git a/Models/GitHistoryModel.h b/Models/GitHistoryModel.h new file mode 100644 index 000000000..9841b0172 --- /dev/null +++ b/Models/GitHistoryModel.h @@ -0,0 +1,27 @@ +#ifndef GITHISTORYMODEL_H +#define GITHISTORYMODEL_H + +#include +#include + +#include + +class GitHistoryModel : public QAbstractTableModel { + Q_OBJECT +public: + GitHistoryModel(QObject *parent = nullptr); + ~GitHistoryModel(); + void LoadRepo(git_repository *repo); + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + +signals: + void GitError(); + +protected: + QVector _commits; +}; + +#endif // GITHISTORYMODEL_H diff --git a/Models/MessageModel.cpp b/Models/MessageModel.cpp index b0b53fb1c..fdbf09b22 100644 --- a/Models/MessageModel.cpp +++ b/Models/MessageModel.cpp @@ -6,6 +6,8 @@ #include "RepeatedMessageModel.h" #include "ResourceModelMap.h" +#include + MessageModel::MessageModel(ProtoModel *parent, Message *protobuf) : ProtoModel(parent, protobuf) { RebuildSubModels(); } MessageModel::MessageModel(QObject *parent, Message *protobuf) : ProtoModel(parent, protobuf) { RebuildSubModels(); } @@ -85,7 +87,12 @@ bool MessageModel::setData(const QModelIndex &index, const QVariant &value, int case CppType::CPPTYPE_ENUM: refl->SetEnum(_protobuf, field, field->enum_type()->FindValueByNumber(value.toInt())); break; - case CppType::CPPTYPE_STRING: refl->SetString(_protobuf, field, value.toString().toStdString()); break; + case CppType::CPPTYPE_STRING: { + if (field->full_name() == "buffers.TreeNode.name") { + if (!MainWindow::resourceMap->ValidResourceName(value.toString())) return false; + } + refl->SetString(_protobuf, field, value.toString().toStdString()); break; + } } SetDirty(true); diff --git a/Models/ResourceChangesModel.cpp b/Models/ResourceChangesModel.cpp new file mode 100644 index 000000000..e1aaef777 --- /dev/null +++ b/Models/ResourceChangesModel.cpp @@ -0,0 +1,78 @@ +#include "ResourceChangesModel.h" + +#include +#include + +ResourceChangesModel::ResourceChangesModel(QObject *parent) : QAbstractListModel(parent) {} + +void ResourceChangesModel::ResourceChanged(Resource& res, ResChange change, const QString& oldName) { + if (res.name.isEmpty()) return; + + beginResetModel(); + + if (change == ResChange::Reverted) _changes.remove(res.name); + + if (_changes.contains(res.name)) { + // Add then remove shouldn't be on list + if (_changes[res.name] == ResChange::Added && change == ResChange::Removed) { + _changes.remove(res.name); + endResetModel(); + return; + } + // modified shouldnt overwrite added + if (_changes[res.name] == ResChange::Added && change == ResChange::Modified) return; + // modified shouldnt overwrite renamed + if (_changes[res.name] == ResChange::Renamed && change == ResChange::Modified) return; + } + + // if renamed before commited change the added name + if (_changes[oldName] == ResChange::Added && change == ResChange::Renamed) { + _changes.remove(oldName); + _changes[res.name] = ResChange::Added; + endResetModel(); + return; + } + + _changes[res.name] = change; + + endResetModel(); +} + +void ResourceChangesModel::ClearChanges() { + beginResetModel(); + _changes.clear(); + endResetModel(); +} + +int ResourceChangesModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) + return 0; + else + return _changes.size(); +} + +QVariant ResourceChangesModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) return QVariant(); + + QList keys = _changes.keys(); + + if (role == Qt::DecorationRole) { + switch (_changes[keys[index.row()]]) { + case ResChange::Added: return QIcon(":/actions/add.png"); + case ResChange::Modified: return QIcon(":/actions/edit.png"); + case ResChange::Renamed: return QIcon(":/actions/edit.png"); + case ResChange::Removed: return QIcon(":/actions/delete.png"); + case ResChange::Reverted: return QVariant(); + } + } else if (role == Qt::DisplayRole) { + switch (_changes[keys[index.row()]]) { + case ResChange::Added: return keys[index.row()] + " " + tr("Added"); + case ResChange::Modified: return keys[index.row()] + " " + tr("Modified"); + case ResChange::Renamed: return keys[index.row()] + " " + tr("Renamed"); + case ResChange::Removed: return keys[index.row()] + " " + tr("Removed"); + case ResChange::Reverted: return QVariant(); + } + } + + return QVariant(); +} diff --git a/Models/ResourceChangesModel.h b/Models/ResourceChangesModel.h new file mode 100644 index 000000000..d69f553a7 --- /dev/null +++ b/Models/ResourceChangesModel.h @@ -0,0 +1,32 @@ +#ifndef RESOURCECHANGESMODEL_H +#define RESOURCECHANGESMODEL_H + +#include "ResourceModelMap.h" + +#include +#include +#include + +enum class ResChange : int { + Added, + Modified, + Renamed, + Removed, + Reverted +}; + +class ResourceChangesModel : public QAbstractListModel +{ + Q_OBJECT +public: + ResourceChangesModel(QObject* parent = nullptr); + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, int role) const override; + void ResourceChanged(Resource &res, ResChange change, const QString &oldName); + void ClearChanges(); + +protected: + QMap _changes; +}; + +#endif // RESOURCECHANGESMODEL_H diff --git a/Models/ResourceModelMap.cpp b/Models/ResourceModelMap.cpp index 191f9aadf..7d9dee166 100644 --- a/Models/ResourceModelMap.cpp +++ b/Models/ResourceModelMap.cpp @@ -26,12 +26,20 @@ void ResourceModelMap::AddResource(buffers::TreeNode* child) { if (model != nullptr) { connect(model, &ProtoModel::DataChanged, [this]() { emit DataChanged(); }); } + + if (!child->has_folder()) { + Resource res = {QString::fromStdString(child->name()), child, model}; + MainWindow::ResourceChanged(res, ResChange::Added); + } } void ResourceModelMap::RemoveResource(TypeCase type, const QString& name) { if (!_resources.contains(type)) return; if (!_resources[type].contains(name)) return; + Resource res = {name, _resources[type][name].first, _resources[type][name].second}; + MainWindow::ResourceChanged(res, ResChange::Removed); + // Delete all instances of this object type if (type == TypeCase::kObject) { for (auto room : _resources[TypeCase::kRoom]) { @@ -128,6 +136,9 @@ void ResourceModelMap::ResourceRenamed(TypeCase type, const QString& oldName, co if (oldName == newName || !_resources[type].contains(oldName)) return; _resources[type][newName] = _resources[type][oldName]; + Resource res = {newName, _resources[type][newName].first, _resources[type][newName].second}; + MainWindow::ResourceChanged(res, ResChange::Renamed, oldName); + for (auto res : _resources) { for (auto model : res) { UpdateReferences(model.second, ResTypeAsString(type), oldName, newName); @@ -139,6 +150,19 @@ void ResourceModelMap::ResourceRenamed(TypeCase type, const QString& oldName, co emit DataChanged(); } +bool ResourceModelMap::ValidResourceName(const QString& name) { + if (name.isEmpty()) return false; + for (auto res : _resources) { + if (res.contains(name)) return false; + } + + for (const QChar& c : name) { + if (!c.isLetterOrNumber() && c != '_') return false; + } + + return true; +} + MessageModel* GetObjectSprite(const std::string& objName) { return GetObjectSprite(QString::fromStdString(objName)); } MessageModel* GetObjectSprite(const QString& objName) { diff --git a/Models/ResourceModelMap.h b/Models/ResourceModelMap.h index 451dd076c..7011220e6 100644 --- a/Models/ResourceModelMap.h +++ b/Models/ResourceModelMap.h @@ -7,6 +7,12 @@ #include #include +struct Resource { + QString name; + buffers::TreeNode* buffer; + MessageModel* model; +}; + class ResourceModelMap : public QObject { Q_OBJECT public: @@ -17,6 +23,7 @@ class ResourceModelMap : public QObject { void RemoveResource(TypeCase type, const QString& name); QString CreateResourceName(TreeNode* node); QString CreateResourceName(int type, const QString& typeName); + bool ValidResourceName(const QString& name); public slots: void ResourceRenamed(TypeCase type, const QString& oldName, const QString& newName); diff --git a/Models/TreeModel.cpp b/Models/TreeModel.cpp index 98b18c3bb..f64a57aed 100644 --- a/Models/TreeModel.cpp +++ b/Models/TreeModel.cpp @@ -5,6 +5,8 @@ #include "Models/RepeatedImageModel.h" #include "Models/ResourceModelMap.h" +#include "MainWindow.h" + #include #include @@ -42,9 +44,12 @@ bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int rol R_EXPECT(index.isValid(), false) << "Supplied index was invalid:" << index; if (role != Qt::EditRole) return false; + if (!MainWindow::resourceMap->ValidResourceName(value.toString())) return false; + buffers::TreeNode *item = static_cast(index.internalPointer()); const QString oldName = QString::fromStdString(item->name()); const QString newName = value.toString(); + if (oldName == newName) return true; item->set_name(newName.toStdString()); emit ResourceRenamed(item->type_case(), oldName, value.toString()); diff --git a/RadialGM.pro b/RadialGM.pro index c20f30c0c..53148631d 100644 --- a/RadialGM.pro +++ b/RadialGM.pro @@ -65,9 +65,13 @@ LIBS += -L$$PWD/Submodules/enigma-dev/CommandLine/libEGM/ \ -L$$PWD/Submodules/enigma-dev/ \ -lProtocols \ -lpugixml \ - -lgrpc++ + -lgrpc++ \ + -lgit2 SOURCES += \ + Components/EGMManager.cpp \ + Components/GitTreeItem.cpp \ + Components/GitTreeStyledDelegate.cpp \ Dialogs/EventArgumentsDialog.cpp \ Dialogs/TimelineChangeMoment.cpp \ Editors/IncludeEditor.cpp \ @@ -76,10 +80,12 @@ SOURCES += \ Models/EventTypesListModel.cpp \ Models/EventTypesListSortFilterProxyModel.cpp \ Models/EventsListModel.cpp \ + Models/GitHistoryModel.cpp \ Models/MessageModel.cpp \ Models/RepeatedImageModel.cpp \ Models/RepeatedMessageModel.cpp \ Models/RepeatedStringModel.cpp \ + Models/ResourceChangesModel.cpp \ Widgets/AssetScrollAreaBackground.cpp \ Widgets/PathView.cpp \ Widgets/SpriteSubimageListView.cpp \ @@ -117,6 +123,9 @@ SOURCES += \ Models/TreeSortFilterProxyModel.cpp HEADERS += \ + Components/EGMManager.h \ + Components/GitTreeItem.h \ + Components/GitTreeStyledDelegate.h \ Dialogs/EventArgumentsDialog.h \ Dialogs/TimelineChangeMoment.h \ Editors/IncludeEditor.h \ @@ -136,11 +145,13 @@ HEADERS += \ Models/EventTypesListModel.h \ Models/EventTypesListSortFilterProxyModel.h \ Models/EventsListModel.h \ + Models/GitHistoryModel.h \ Models/MessageModel.h \ Models/RepeatedImageModel.h \ Models/RepeatedMessageModel.h \ Models/RepeatedModel.h \ Models/RepeatedStringModel.h \ + Models/ResourceChangesModel.h \ Widgets/AssetScrollArea.h \ Widgets/AssetScrollAreaBackground.h \ Widgets/BackgroundView.h \