diff --git a/CMakeLists.txt b/CMakeLists.txt index 21fa3050..a800d801 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,8 @@ set(PROJECT_SOURCES source/undostack/undostack.h source/undostack/command.cpp source/undostack/command.h + source/undostack/undomacro.cpp + source/undostack/undomacro.h ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) diff --git a/source/mainwindow.cpp b/source/mainwindow.cpp index 86626a4c..30a25ea8 100644 --- a/source/mainwindow.cpp +++ b/source/mainwindow.cpp @@ -53,6 +53,13 @@ MainWindow::MainWindow() this->redoAction = new QAction("Redo", this); this->redoAction->setShortcuts(QKeySequence::Redo); this->ui->menuEdit->addAction(this->redoAction); + + // Initialize widgets used for Undo/Redo + this->m_progressDialog = std::make_unique(this); + this->m_progressDialog->close(); + this->m_progressDialog->setAutoReset(true); + this->m_progressDialog->reset(); + this->m_progressDialog->setWindowModality(Qt::WindowModal); this->ui->menuEdit->addSeparator(); this->ui->menuEdit->addAction(this->undoAction); @@ -748,7 +755,7 @@ void MainWindow::openPalFiles(QStringList filePaths, PaletteWidget *widget) this->palWidget->selectPath(firstFound); } } else if (widget == this->trnUniqueWidget) { - for (QString path : filePaths) { + for (const QString &path : filePaths) { if (this->loadTrnUnique(path) && firstFound.isEmpty()) { firstFound = path; } @@ -1681,6 +1688,38 @@ void MainWindow::on_actionClose_Translation_triggered() this->trnWidget->selectPath(D1Trn::IDENTITY_PATH); } +void MainWindow::setupUndoMacroWidget(std::unique_ptr &userData, enum OperationType opType) +{ + this->m_currMacroOpType = opType; + int prevMaximum = this->m_progressDialog->maximum(); + + this->m_progressDialog->reset(); + this->m_progressDialog->show(); + this->m_progressDialog->setLabelText(userData->labelText()); + this->m_progressDialog->setCancelButtonText(userData->cancelButtonText()); + this->m_progressDialog->setMinimum(userData->min()); + this->m_progressDialog->setMaximum(userData->max()); + + if (opType == OperationType::Undo && prevMaximum == userData->max()) + this->m_currProgDialogPos = this->m_progressDialog->maximum() - m_currProgDialogPos; + else if (this->m_currProgDialogPos == prevMaximum || prevMaximum != userData->max()) + this->m_currProgDialogPos = 0; +} + +void MainWindow::updateUndoMacroWidget(bool &result) +{ + this->m_currProgDialogPos++; + + this->m_progressDialog->setValue(m_currProgDialogPos); + if (this->m_progressDialog->wasCanceled()) { + this->m_progressDialog->reset(); + result = true; + return; + } + + QCoreApplication::processEvents(); +} + #if defined(Q_OS_WIN) #define OS_TYPE "Windows" #elif defined(Q_OS_QNX) diff --git a/source/mainwindow.h b/source/mainwindow.h index 5184102d..0711caf1 100644 --- a/source/mainwindow.h +++ b/source/mainwindow.h @@ -106,6 +106,10 @@ public slots: void actionReplaceTile_triggered(); void actionDelTile_triggered(); + // slots used for UndoMacro signals + void setupUndoMacroWidget(std::unique_ptr &userData, enum OperationType opType); + void updateUndoMacroWidget(bool &result); + void buildRecentFilesMenu(); void addRecentFile(QString filePath); void on_actionClear_History_triggered(); @@ -197,6 +201,11 @@ private slots: QMap trns; // key: path, value: pointer to translation QMap trnsUnique; // key: path, value: pointer to translation + std::unique_ptr m_progressDialog; + // Palette hits are instantiated in main window to make them available to the three PaletteWidgets QPointer palHits; + + int m_currProgDialogPos { 0 }; + enum OperationType m_currMacroOpType; }; diff --git a/source/undostack/command.cpp b/source/undostack/command.cpp index 5da9b18d..3d54cb37 100644 --- a/source/undostack/command.cpp +++ b/source/undostack/command.cpp @@ -20,3 +20,13 @@ bool Command::isObsolete() const { return m_isObsolete; } + +void Command::setMacroID(unsigned int macroID) +{ + m_macroID = macroID; +} + +unsigned int Command::macroID() const +{ + return m_macroID; +} diff --git a/source/undostack/command.h b/source/undostack/command.h index 67d6ccba..f1ef8297 100644 --- a/source/undostack/command.h +++ b/source/undostack/command.h @@ -7,9 +7,12 @@ class Command { void setObsolete(bool isObsolete); bool isObsolete() const; + void setMacroID(unsigned int macroID); + unsigned int macroID() const; virtual ~Command() = default; private: + unsigned int m_macroID { 0 }; bool m_isObsolete = false; }; diff --git a/source/undostack/framecmds.cpp b/source/undostack/framecmds.cpp index 96a91504..e66ca540 100644 --- a/source/undostack/framecmds.cpp +++ b/source/undostack/framecmds.cpp @@ -40,39 +40,18 @@ void ReplaceFrameCommand::redo() emit this->replaced(frameIndexToReplace, imgToReplace); } -AddFrameCommand::AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath) - : startingIndex(index) - , mode(mode) +AddFrameCommand::AddFrameCommand(int index, QImage &img) + : m_index(index) + , m_image(std::move(img)) { - QImageReader reader = QImageReader(imagefilePath); - int numImages = 0; - - // FIXME: this loop should have some sort of a progress bar, we support - // status bar, but if user loads a .gif which could contain up to hundreds - // of frames, loading might take quite a bit - while (true) { - QImage image = reader.read(); - if (image.isNull()) { - break; - } - - images.emplace_back(image); - numImages++; - } - - if (mode != IMAGE_FILE_MODE::AUTO && numImages == 0) { - throw std::exception(); - } - - endingIndex = startingIndex + numImages; } void AddFrameCommand::undo() { - emit this->undoAdded(startingIndex, endingIndex); + emit this->undoAdded(m_index); } void AddFrameCommand::redo() { - emit this->added(startingIndex, images, mode); + emit this->added(m_index, m_image); } diff --git a/source/undostack/framecmds.h b/source/undostack/framecmds.h index eeeb0501..58e0c85d 100644 --- a/source/undostack/framecmds.h +++ b/source/undostack/framecmds.h @@ -48,19 +48,17 @@ class AddFrameCommand : public QObject, public Command { Q_OBJECT public: - explicit AddFrameCommand(IMAGE_FILE_MODE mode, int index, const QString imagefilePath); + explicit AddFrameCommand(int index, QImage &image); ~AddFrameCommand() = default; void undo() override; void redo() override; signals: - void undoAdded(int startingIndex, int endingIndex); - void added(int startingIndex, const std::vector &images, IMAGE_FILE_MODE mode); + void undoAdded(int index); + void added(int index, const QImage &image); private: - std::vector images; - int startingIndex = 0; - int endingIndex = 0; - IMAGE_FILE_MODE mode; + QImage m_image; + int m_index = 0; }; diff --git a/source/undostack/undomacro.cpp b/source/undostack/undomacro.cpp new file mode 100644 index 00000000..71c4620e --- /dev/null +++ b/source/undostack/undomacro.cpp @@ -0,0 +1,60 @@ +#include "undomacro.h" + +#include + +UserData::UserData(QString labelText, QString cancelButtonText, std::pair &&minMax) + : m_labelText(std::move(labelText)) + , m_cancelButtonText(std::move(cancelButtonText)) + , m_minMax(minMax) +{ +} + +UserData::UserData(QString labelText, QString cancelButtonText) + : m_labelText(std::move(labelText)) + , m_cancelButtonText(std::move(cancelButtonText)) + , m_minMax({ 0, 0 }) +{ +} + +UndoMacroFactory::UndoMacroFactory(UserData &&userData) + : m_userData(std::make_unique(userData.labelText(), userData.cancelButtonText(), std::make_pair(userData.min(), userData.max()))) +{ +} + +void UndoMacroFactory::setUserData(const UserData &&userData) +{ + m_userData = std::make_unique(userData.labelText(), userData.cancelButtonText(), std::make_pair(userData.min(), userData.max())); +} + +void UndoMacroFactory::add(std::unique_ptr cmd) +{ + m_commands.push_back(std::move(cmd)); +} + +UndoMacro::UndoMacro(std::unique_ptr userData, std::pair rangeIdxs) + : m_userData(std::move(userData)) + , m_rangeIdxs(rangeIdxs) +{ +} + +UndoMacro::UndoMacro(UndoMacro &&undoMacro) noexcept + : m_userData(std::move(undoMacro.m_userData)) + , m_rangeIdxs(undoMacro.m_rangeIdxs) +{ +} + +UndoMacro &UndoMacro::operator=(UndoMacro &&undoMacro) noexcept +{ + m_userData = std::move(undoMacro.m_userData); + m_rangeIdxs = undoMacro.m_rangeIdxs; + return *this; +} + +void UndoMacro::setLastIndex(int index) +{ + m_rangeIdxs.second = index; + /* TODO: later on, if/when we will implement more types of macros, we could use some update method of UserData + * instead of using setters directly + */ + m_userData->setMax((m_rangeIdxs.second - m_rangeIdxs.first) + 1); +} diff --git a/source/undostack/undomacro.h b/source/undostack/undomacro.h new file mode 100644 index 00000000..60687422 --- /dev/null +++ b/source/undostack/undomacro.h @@ -0,0 +1,93 @@ +#pragma once + +#include "command.h" +#include +#include +#include +#include + +class UserData { +private: + QString m_labelText; + QString m_cancelButtonText; + std::pair m_minMax; + +public: + UserData(QString labelText, QString cancelButtonText, std::pair &&minMax); + UserData(QString labelText, QString cancelButtonText); + ~UserData() = default; + + [[nodiscard]] int min() const + { + return m_minMax.first; + } + [[nodiscard]] int max() const + { + return m_minMax.second; + } + [[nodiscard]] const QString &labelText() const + { + return m_labelText; + } + [[nodiscard]] const QString &cancelButtonText() const + { + return m_cancelButtonText; + } + void setMin(int min) + { + m_minMax.first = min; + } + void setMax(int max) + { + m_minMax.second = max; + } +}; + +class UndoMacroFactory { +private: + std::unique_ptr m_userData; + std::vector> m_commands; + +public: + UndoMacroFactory(UserData &&userData); + UndoMacroFactory() = default; + ~UndoMacroFactory() = default; + + void setUserData(const UserData &&userData); + + void add(std::unique_ptr cmd); + [[nodiscard]] std::vector> &cmds() + { + return m_commands; + }; + [[nodiscard]] std::unique_ptr &userdata() + { + return m_userData; + }; +}; + +class UndoMacro { +private: + std::unique_ptr m_userData; + std::pair m_rangeIdxs; + +public: + UndoMacro(std::unique_ptr userData, std::pair rangeIdxs); + UndoMacro(UndoMacro &&undoMacro) noexcept; + UndoMacro &operator=(UndoMacro &&undoMacro) noexcept; + ~UndoMacro() = default; + + [[nodiscard]] std::unique_ptr &userdata() + { + return m_userData; + }; + [[nodiscard]] int beginIndex() const + { + return m_rangeIdxs.first; + } + [[nodiscard]] int lastIndex() const + { + return m_rangeIdxs.second; + } + void setLastIndex(int index); +}; diff --git a/source/undostack/undostack.cpp b/source/undostack/undostack.cpp index e470c800..dede81a8 100644 --- a/source/undostack/undostack.cpp +++ b/source/undostack/undostack.cpp @@ -1,4 +1,5 @@ #include "undostack.h" +#include /** * @brief Pushes new commands onto the commands stack (Undostack) @@ -19,12 +20,7 @@ void UndoStack::push(std::unique_ptr cmd) */ } - // Erase any command that was set to obsolete - std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); - - // Drop any command that's after currently undo'd index - if (m_cmds.begin() + (m_undoPos + 1) <= m_cmds.end()) - m_cmds.erase(m_cmds.begin() + (m_undoPos + 1), m_cmds.end()); + eraseRedundantCmds(); m_cmds.push_back(std::move(cmd)); m_canUndo = true; @@ -44,13 +40,31 @@ void UndoStack::undo() // Erase any command that was previously set as obsolete std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); - if (m_undoPos == 0) - m_canUndo = false; + /* If current processed command has a macroID higher than 0, then it means it's a macro. + * So we need to start going through commands backwards in a loop + */ + unsigned int macroID = m_cmds[m_undoPos]->macroID(); + if (macroID > 0) { + emit initializeWidget(m_macros[macroID - 1].userdata(), OperationType::Undo); - m_cmds[m_undoPos]->undo(); + while (m_cmds[m_undoPos]->macroID() > 0) { + bool result = false; + m_cmds[m_undoPos]->undo(); + m_undoPos--; + emit updateWidget(result); + if (result || m_undoPos < 0 || m_cmds[m_undoPos + 1]->macroID() != m_cmds[m_undoPos]->macroID()) { + break; + } + } + } else { + m_cmds[m_undoPos]->undo(); + m_undoPos--; + } + + if (m_undoPos < 0) + m_canUndo = false; m_canRedo = true; - m_undoPos--; } /** @@ -65,12 +79,27 @@ void UndoStack::redo() // erase any command that was previously set as obsolete std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); - m_cmds[m_undoPos + 1]->redo(); + unsigned int macroID = m_cmds[m_undoPos + 1]->macroID(); + if (macroID > 0) { + emit initializeWidget(m_macros[macroID - 1].userdata(), OperationType::Redo); + + while (m_cmds[m_undoPos + 1]->macroID() > 0) { + bool result = false; + m_cmds[m_undoPos + 1]->redo(); + m_undoPos++; + emit updateWidget(result); + if (result || m_undoPos + 1 >= m_cmds.size() || m_cmds[m_undoPos + 1]->macroID() != m_cmds[m_undoPos]->macroID()) { + break; + } + } + } else { + m_cmds[m_undoPos + 1]->redo(); + m_undoPos++; + } - m_undoPos++; m_canUndo = true; - if (m_undoPos + 1 > m_cmds.size() - 1) + if (m_undoPos + 1 >= m_cmds.size()) m_canRedo = false; } @@ -99,7 +128,76 @@ bool UndoStack::canUndo() const */ void UndoStack::clear() { - m_undoPos = 0; + m_undoPos = -1; m_canUndo = m_canRedo = false; m_cmds.clear(); + m_macros.clear(); +} + +/** + * @brief Adds a macro to undo stack + * + * This function is being called whenever someone wants to insert + * a macro in the undo stack. It calls redo() on all commands contained + * in the macro, and updates undo stack position accordingly, it also + * sets macroIDs depending on the macros' vector size. + * + */ +void UndoStack::addMacro(UndoMacroFactory ¯oFactory) +{ + eraseRedundantCmds(); + + emit initializeWidget(macroFactory.userdata(), OperationType::Redo); + m_macros.emplace_back(std::move(macroFactory.userdata()), std::make_pair(m_cmds.size(), (m_cmds.size() + macroFactory.cmds().size()) - 1)); + + for (auto &cmd : macroFactory.cmds()) { + bool result = false; + cmd->redo(); + m_undoPos++; + + emit updateWidget(result); + if (result) { + m_canRedo = true; + break; + } + } + + // For each command that will be inserted set a macroID so it is located in the same span + std::for_each(macroFactory.cmds().begin(), macroFactory.cmds().end(), [&](const std::unique_ptr &cmd) { + cmd->setMacroID(m_macros.size()); + }); + + m_cmds.insert(m_cmds.end(), std::make_move_iterator(macroFactory.cmds().begin()), std::make_move_iterator(macroFactory.cmds().end())); + m_canUndo = true; +} + +/** + * @brief Erases redundant commands and macros from the undo stack + * + * This function erases redundant commands and macros from both vectors + * in undostack. Redundant in this case means commands which will no longer + * be available, i.e. command being obsolete (having obsolete flag set to true), or + * all commands + macros after currently pushed command - upon insertion undo stack removes all + * commands + macros that are after current undo stack position (were undo'd) and are possible + * to redo. + * + */ +void UndoStack::eraseRedundantCmds() +{ + // Erase any command that was set to obsolete + std::erase_if(m_cmds, [](const auto &cmd) { return cmd->isObsolete(); }); + + if (m_cmds.begin() + (m_undoPos + 1) < m_cmds.end()) { + // Drop any command that's after currently undo'd index + m_cmds.erase(m_cmds.begin() + (m_undoPos + 1), m_cmds.end()); + + // Drop any macro that's after currently undo'd index + std::erase_if(m_macros, [&](const auto ¯o) { return macro.beginIndex() > m_undoPos; }); + + // If undoPos is currently on a macro, then update it's ending index because we could have removed some of + // it's commands + unsigned int macroID = m_cmds[m_undoPos]->macroID(); + if (macroID > 0) + m_macros[macroID - 1].setLastIndex(m_undoPos); + } } diff --git a/source/undostack/undostack.h b/source/undostack/undostack.h index c639f5fe..a8407a5e 100644 --- a/source/undostack/undostack.h +++ b/source/undostack/undostack.h @@ -1,12 +1,22 @@ #pragma once #include "command.h" +#include "undomacro.h" +#include #include #include +#include #include -class UndoStack { +enum OperationType { + Undo, + Redo +}; + +class UndoStack : public QObject { + Q_OBJECT + public: UndoStack() = default; ~UndoStack() = default; @@ -19,10 +29,18 @@ class UndoStack { [[nodiscard]] bool canRedo() const; void clear(); + void addMacro(UndoMacroFactory ¯oFactory); + +signals: + void updateWidget(bool &userCancelled); + void initializeWidget(std::unique_ptr &userData, enum OperationType opType); private: bool m_canUndo = false; bool m_canRedo = false; - int8_t m_undoPos = 0; + int8_t m_undoPos { -1 }; std::vector> m_cmds; // holds all the commands on the stack + std::vector m_macros; + + void eraseRedundantCmds(); }; diff --git a/source/views/celview.cpp b/source/views/celview.cpp index b70ace9b..92e4eba8 100644 --- a/source/views/celview.cpp +++ b/source/views/celview.cpp @@ -1,8 +1,9 @@ #include "celview.h" - #include #include + +#include "mainwindow.h" #include #include #include @@ -11,9 +12,9 @@ #include #include -#include "mainwindow.h" #include "ui_celview.h" #include "undostack/framecmds.h" +#include "undostack/undomacro.h" CelScene::CelScene(QWidget *v) : QGraphicsScene() @@ -182,20 +183,18 @@ void CelView::insertImageFile(int frameIdx, const QImage img) this->displayFrame(); } -void CelView::insertFrames(int startingIndex, const std::vector &images) +void CelView::insertFrames(int index, const QImage &image) { int prevFrameCount = this->gfx->getFrameCount(); - for (int idx = 0; idx < images.size(); idx++) { - this->gfx->insertFrame(startingIndex + idx, images[idx]); - } + this->gfx->insertFrame(index, image); int deltaFrameCount = this->gfx->getFrameCount() - prevFrameCount; if (deltaFrameCount == 0) { return; // no new frame -> done } - this->currentFrameIndex = startingIndex; + this->currentFrameIndex = index; this->updateGroupIndex(); // update the view @@ -203,28 +202,62 @@ void CelView::insertFrames(int startingIndex, const std::vector &images) this->displayFrame(); } -void CelView::removeFrames(int startingIndex, int endingIndex) +void CelView::removeFrames(int index) { - int idx = startingIndex; - while (startingIndex != endingIndex) { - this->removeCurrentFrame(idx); - startingIndex++; - } + this->removeCurrentFrame(index); } void CelView::sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath) { - std::unique_ptr command; - try { - command = std::make_unique(mode, index, imagefilePath); - } catch (...) { - QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); + QImageReader reader = QImageReader(imagefilePath); + + auto readImage = [&](QImage &img) -> bool { + if (!reader.read(&img)) { + QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); + return false; + } + + return true; + }; + + auto connectCommand = [&](QImage &img) -> std::unique_ptr { + auto command = std::make_unique(index, img); + + // Connect signals which will be called upon redo/undo operations of the undostack + QObject::connect(command.get(), &AddFrameCommand::added, this, &CelView::insertFrames); + QObject::connect(command.get(), &AddFrameCommand::undoAdded, this, &CelView::removeFrames); + return command; + }; + + // If we have more than one image, then we want to use a macro + if (reader.imageCount() > 1) { + QObject::connect(this->undoStack.get(), &UndoStack::initializeWidget, dynamic_cast(this->window()), &MainWindow::setupUndoMacroWidget, Qt::UniqueConnection); + QObject::connect(this->undoStack.get(), &UndoStack::updateWidget, dynamic_cast(this->window()), &MainWindow::updateUndoMacroWidget, Qt::UniqueConnection); + + UndoMacroFactory macroFactory({ "Inserting frames...", "Abort", { 0, reader.imageCount() } }); + + int numImages = 0; + while (numImages != reader.imageCount()) { + QImage image; + if (!readImage(image)) + return; + + auto command = connectCommand(image); + + macroFactory.add(std::move(command)); + + numImages++; + } + + undoStack->addMacro(macroFactory); return; } - // send a command to undostack, making adding frame undo/redoable - QObject::connect(command.get(), &AddFrameCommand::added, this, &CelView::insertFrames); - QObject::connect(command.get(), &AddFrameCommand::undoAdded, this, &CelView::removeFrames); + QImage image; + if (!readImage(image)) + return; + + auto command = connectCommand(image); undoStack->push(std::move(command)); } diff --git a/source/views/celview.h b/source/views/celview.h index f7460d93..cec78e7b 100644 --- a/source/views/celview.h +++ b/source/views/celview.h @@ -56,7 +56,6 @@ class CelView : public QWidget { void initialize(D1Gfx *gfx); void sendRemoveFrameCmd(); void sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); - void updateCurrentFrameIndex(int frameIdx); int getCurrentFrameIndex(); void framePixelClicked(unsigned x, unsigned y); void insertImageFiles(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); @@ -74,8 +73,8 @@ class CelView : public QWidget { private: void update(); - void removeFrames(int startingIndex, int endingIndex); - void insertFrames(int startingIndex, const std::vector &images); + void removeFrames(int index); + void insertFrames(int index, const QImage &image); void setGroupIndex(); private slots: diff --git a/source/views/levelcelview.cpp b/source/views/levelcelview.cpp index ed4a9b51..b3cdba61 100644 --- a/source/views/levelcelview.cpp +++ b/source/views/levelcelview.cpp @@ -18,6 +18,95 @@ #include #include +namespace { + +int getClickedSubtile(unsigned x, unsigned y, unsigned width, unsigned height) +{ + // | | + // | | + // 2| 0 |1 + // | | + // | | + + // The perspective lets us know the floor heigth based on the width + int wallHeight = height - (width / 4 + width / 8); + + if (y < wallHeight) { + if (x < width / 4) { + return 2; + } + if (x > width - width / 4) { + return 1; + } + return 0; + } + + // \0/ + // 2 X 1 + // / \ + // / 3 \ + // / \ + + y -= height - width / 2; + + int y1 = width / 2 * x / width; // y of 1st diagonal at x + int y2 = width / 2 - y1; // y of 2nd diagonal at x + + if (y < y1) { + if (y < y2) + return 0; // top + else + return 1; // right + } else { + if (y < y2) + return 2; // left + else + return 3; // bottom + } +} + +/** + * @brief Extracts subimages from given image + * + * This function extracts 32x32 subimages from an image that is + * 32x32 divisible and returns them as an input parameter to the std::vector. + * This is because LevelCelView supports only 32x32 frames. + * + * @return Returns QList of frame indices to be replaced if needed + */ +QList extractSubImages(const QImage &image, std::vector &images, int frameIndex) +{ + QList frameIndicesList; + + // TODO: merge with LevelCelView::insertSubtile ? + QImage subImage = QImage(MICRO_WIDTH, MICRO_HEIGHT, QImage::Format_ARGB32); + for (int y = 0; y < image.height(); y += MICRO_HEIGHT) { + for (int x = 0; x < image.width(); x += MICRO_WIDTH) { + + bool hasColor = false; + for (int j = 0; j < MICRO_HEIGHT; j++) { + for (int i = 0; i < MICRO_WIDTH; i++) { + const QColor color = image.pixelColor(x + i, y + j); + if (color.alpha() >= COLOR_ALPHA_LIMIT) { + hasColor = true; + } + subImage.setPixelColor(i, j, color); + } + } + frameIndicesList.append(hasColor ? frameIndex + 1 : 0); + if (!hasColor) { + continue; + } + + images.push_back(subImage); + } + } + + return frameIndicesList; +} + +} // namespace + LevelCelView::LevelCelView(std::shared_ptr us, QWidget *parent) : QWidget(parent) , undoStack(std::move(us)) @@ -95,69 +184,6 @@ void LevelCelView::update() this->tabFrameWidget->initialize(this, this->gfx); } -int LevelCelView::getCurrentFrameIndex() -{ - return this->currentFrameIndex; -} - -int LevelCelView::getCurrentSubtileIndex() -{ - return this->currentSubtileIndex; -} - -int LevelCelView::getCurrentTileIndex() -{ - return this->currentTileIndex; -} - -namespace { - -int getClickedSubtile(unsigned x, unsigned y, unsigned width, unsigned height) -{ - // | | - // | | - // 2| 0 |1 - // | | - // | | - - // The perspective lets us know the floor heigth based on the width - int wallHeight = height - (width / 4 + width / 8); - - if (y < wallHeight) { - if (x < width / 4) { - return 2; - } - if (x > width - width / 4) { - return 1; - } - return 0; - } - - // \0/ - // 2 X 1 - // / \ - // / 3 \ - // / \ - - y -= height - width / 2; - - int y1 = width / 2 * x / width; // y of 1st diagonal at x - int y2 = width / 2 - y1; // y of 2nd diagonal at x - - if (y < y1) { - if (y < y2) - return 0; // top - else - return 1; // right - } else { - if (y < y2) - return 2; // left - else - return 3; // bottom - } -} -} // namespace - void LevelCelView::framePixelClicked(unsigned x, unsigned y) { quint8 index = 0; @@ -241,79 +267,12 @@ void LevelCelView::insertImageFiles(IMAGE_FILE_MODE mode, const QStringList &ima } } -void LevelCelView::assignFrames(const QImage &image, int subtileIndex, int frameIndex) -{ - QList frameIndicesList; - - // TODO: merge with LevelCelView::insertSubtile ? - QImage subImage = QImage(MICRO_WIDTH, MICRO_HEIGHT, QImage::Format_ARGB32); - for (int y = 0; y < image.height(); y += MICRO_HEIGHT) { - for (int x = 0; x < image.width(); x += MICRO_WIDTH) { - // subImage.fill(Qt::transparent); - - bool hasColor = false; - for (int j = 0; j < MICRO_HEIGHT; j++) { - for (int i = 0; i < MICRO_WIDTH; i++) { - const QColor color = image.pixelColor(x + i, y + j); - if (color.alpha() >= COLOR_ALPHA_LIMIT) { - hasColor = true; - } - subImage.setPixelColor(i, j, color); - } - } - frameIndicesList.append(hasColor ? frameIndex + 1 : 0); - if (!hasColor) { - continue; - } - - D1GfxFrame *frame = this->gfx->insertFrame(frameIndex, subImage); - LevelTabFrameWidget::selectFrameType(frame); - frameIndex++; - } - } - - if (subtileIndex >= 0) { - this->min->getCelFrameIndices(subtileIndex).swap(frameIndicesList); - // reset subtile flags - this->sol->setSubtileProperties(subtileIndex, 0); - } -} - -void LevelCelView::insertFrame(IMAGE_FILE_MODE mode, int index, const QImage &image) -{ - // FIXME: investigate if adding multiple frames, and having a frame that is not in - // proper dimensions will screw up frame list, especially in appending operations - if ((image.width() % MICRO_WIDTH) != 0 || (image.height() % MICRO_HEIGHT) != 0) { - QMessageBox::critical(this, tr("Error!"), tr("Wrong frame dimensions!\n" - "Image should have dimensions %1x%2px (w x h).\n" - "Image that you wanted to insert has %3x%4px dimensions.") - .arg(MICRO_WIDTH) - .arg(MICRO_HEIGHT) - .arg(image.width()) - .arg(image.height())); - return; - } - - if (mode == IMAGE_FILE_MODE::AUTO) { - // check for subtile dimensions to be more lenient than EXPORT_LVLFRAMES_PER_LINE - unsigned subtileWidth = this->min->getSubtileWidth() * MICRO_WIDTH; - unsigned subtileHeight = this->min->getSubtileHeight() * MICRO_HEIGHT; - - if ((image.width() % subtileWidth) == 0 && (image.height() % subtileHeight) == 0) { - return; // this is a subtile or a tile (or subtiles or tiles) -> ignore - } - } - - this->assignFrames(image, -1, index); -} - -void LevelCelView::insertFrames(int startingIndex, const std::vector &images, IMAGE_FILE_MODE mode) +void LevelCelView::insertFrames(int index, const QImage &image) { int prevFrameCount = this->gfx->getFrameCount(); - for (int idx = 0; idx < images.size(); idx++) { - this->insertFrame(mode, startingIndex + idx, images[idx]); - } + D1GfxFrame *frame = this->gfx->insertFrame(index, image); + LevelTabFrameWidget::selectFrameType(frame); int deltaFrameCount = this->gfx->getFrameCount() - prevFrameCount; if (deltaFrameCount == 0) { @@ -324,9 +283,9 @@ void LevelCelView::insertFrames(int startingIndex, const std::vector &im // shift all tiles references higher than inserted frame's index to the right. // Otherwise, if we are appending - just update currentFrameIndex to the one first // appended frame - if (startingIndex + 1 != this->gfx->getFrameCount()) { + if (index + 1 != this->gfx->getFrameCount()) { // shift references - unsigned refIndex = startingIndex + 1; + unsigned refIndex = index + 1; // shift frame indices of the subtiles for (int i = 0; i < this->min->getSubtileCount(); i++) { QList &frameIndices = this->min->getCelFrameIndices(i); @@ -343,15 +302,13 @@ void LevelCelView::insertFrames(int startingIndex, const std::vector &im // If this function is used in undo stack and this operation came after redo, // then we have to renew previously used indices of frames if (!tilesAndFramesIdxStack.empty()) { - for (int idx = 0; idx < images.size(); idx++) { - auto &vec = tilesAndFramesIdxStack.top(); - for (auto &pair : vec) { - QList &frameIndices = this->min->getCelFrameIndices(pair.first); - frameIndices[pair.second] = (startingIndex + idx) + 1; - } - - tilesAndFramesIdxStack.pop(); + auto &vec = tilesAndFramesIdxStack.top(); + for (auto &pair : vec) { + QList &frameIndices = this->min->getCelFrameIndices(pair.first); + frameIndices[pair.second] = index + 1; } + + tilesAndFramesIdxStack.pop(); } // update the view @@ -376,26 +333,122 @@ void LevelCelView::insertFrames(IMAGE_FILE_MODE mode, const QStringList &imagefi void LevelCelView::sendAddFrameCmd(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath) { - std::unique_ptr command; - try { - command = std::make_unique(mode, index, imagefilePath); - } catch (...) { - QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); + QImageReader reader = QImageReader(imagefilePath); + + auto readImage = [&](QImage &img) -> bool { + if (!reader.read(&img)) { + QMessageBox::critical(this, "Error", "Failed to read image file: " + imagefilePath); + return false; + } + + // FIXME: investigate if adding multiple frames, and having a frame that is not in + // proper dimensions will screw up frame list, especially in appending operations + if ((img.width() % MICRO_WIDTH) != 0 || (img.height() % MICRO_HEIGHT) != 0) { + QMessageBox::critical(this, tr("Error!"), tr("Wrong frame dimensions!\n" + "Image should have dimensions %1x%2px (w x h).\n" + "Image that you wanted to insert has %3x%4px dimensions.") + .arg(MICRO_WIDTH) + .arg(MICRO_HEIGHT) + .arg(img.width()) + .arg(img.height())); + return false; + } + + if (mode == IMAGE_FILE_MODE::AUTO) { + // check for subtile dimensions to be more lenient than EXPORT_LVLFRAMES_PER_LINE + unsigned subtileWidth = this->min->getSubtileWidth() * MICRO_WIDTH; + unsigned subtileHeight = this->min->getSubtileHeight() * MICRO_HEIGHT; + + if ((img.width() % subtileWidth) == 0 && (img.height() % subtileHeight) == 0) { + return false; // this is a subtile or a tile (or subtiles or tiles) -> ignore + } + } + + return true; + }; + + auto connectCommand = [&](std::unique_ptr &command) { + // Connect signals which will be called upon redo/undo operations of the undostack + QObject::connect(command.get(), &AddFrameCommand::added, this, static_cast(&LevelCelView::insertFrames)); + QObject::connect(command.get(), &AddFrameCommand::undoAdded, this, &LevelCelView::removeCurrentFrame); + }; + + auto addSubImagesToMacro = [&](QImage &image, UndoMacroFactory ¯oFactory) -> int { + // Here we are extracting sub-images from the image which are 32x32 divisible + std::vector subImages; + subImages.reserve(image.width() / MICRO_WIDTH * image.height() / MICRO_HEIGHT); + extractSubImages(image, subImages, index); + + for (auto &subImage : subImages) { + auto command = std::make_unique(index, subImage); + connectCommand(command); + macroFactory.add(std::move(command)); + index++; + } + + // Return number of subimages extracted + return static_cast(subImages.size()); + }; + + // If we have more than one image, then we want to use a macro + if (reader.imageCount() > 1) { + QObject::connect(this->undoStack.get(), &UndoStack::initializeWidget, dynamic_cast(this->window()), &MainWindow::setupUndoMacroWidget, Qt::UniqueConnection); + QObject::connect(this->undoStack.get(), &UndoStack::updateWidget, dynamic_cast(this->window()), &MainWindow::updateUndoMacroWidget, Qt::UniqueConnection); + + int totalNumSubImages = 0; + UserData userData("Inserting frames...", "Abort"); + UndoMacroFactory macroFactory; + + int numImages = 0; + while (numImages != reader.imageCount()) { + QImage image; + if (!readImage(image)) + return; + + int numSubImages = addSubImagesToMacro(image, macroFactory); + totalNumSubImages += numSubImages; + + numImages++; + } + + // We have to set maximum after, because we don't know how many sub images there will be + userData.setMax(totalNumSubImages); + macroFactory.setUserData(std::move(userData)); + + undoStack->addMacro(macroFactory); return; } - // send a command to undostack, making adding frame undo/redoable - QObject::connect(command.get(), &AddFrameCommand::added, this, static_cast &images, IMAGE_FILE_MODE mode)>(&LevelCelView::insertFrames)); - QObject::connect(command.get(), &AddFrameCommand::undoAdded, this, &LevelCelView::removeFrames); + QImage image; + if (!readImage(image)) + return; - undoStack->push(std::move(command)); + // If the image is not exactly 32x32, then we have subimages. So extract them and pass them as a macro + // Otherwise insert them as a normal UndoStack command. + if (image.width() != MICRO_WIDTH && image.height() != MICRO_HEIGHT) { + QObject::connect(this->undoStack.get(), &UndoStack::initializeWidget, dynamic_cast(this->window()), &MainWindow::setupUndoMacroWidget, Qt::UniqueConnection); + QObject::connect(this->undoStack.get(), &UndoStack::updateWidget, dynamic_cast(this->window()), &MainWindow::updateUndoMacroWidget, Qt::UniqueConnection); + + int numOfSubImages = image.width() / MICRO_WIDTH * image.height() / MICRO_HEIGHT; + UndoMacroFactory macroFactory({ "Inserting frames...", "Abort", { 0, numOfSubImages } }); + + addSubImagesToMacro(image, macroFactory); + + undoStack->addMacro(macroFactory); + } else { + auto command = std::make_unique(index, image); + connectCommand(command); + + undoStack->push(std::move(command)); + } } void LevelCelView::insertFrame(int index, const QImage image) { int prevFrameCount = this->gfx->getFrameCount(); - this->insertFrame(IMAGE_FILE_MODE::FRAME, index, image); + D1GfxFrame *frame = this->gfx->insertFrame(index, image); + LevelTabFrameWidget::selectFrameType(frame); int deltaFrameCount = this->gfx->getFrameCount() - prevFrameCount; if (deltaFrameCount == 0) { @@ -808,25 +861,15 @@ void LevelCelView::sendRemoveFrameCmd() this->undoStack->push(std::move(command)); } -void LevelCelView::removeCurrentFrame(int frameIdx) +void LevelCelView::removeCurrentFrame(int index) { - // remove the current frame - this->removeFrame(frameIdx); + // remove current frame + this->removeFrame(index); // update the view // FIXME: decouple that somehow, why is it taking variables and then assigns it back // to the same variables? Probably would be enough to call this->update() this->initialize(this->gfx, this->min, this->til, this->sol, this->amp); - this->displayFrame(); -} - -void LevelCelView::removeFrames(int startingIdx, int endingIndex) -{ - for (int idx = startingIdx; idx < endingIndex; idx++) { - this->removeCurrentFrame(idx); - } - - // update the view this->update(); this->displayFrame(); } @@ -868,7 +911,22 @@ void LevelCelView::replaceCurrentSubtile(const QString &imagefilePath) } int subtileIndex = this->currentSubtileIndex; - this->assignFrames(image, subtileIndex, this->gfx->getFrameCount()); + int frameIndex = this->gfx->getFrameCount(); + + // Here we are extracting sub-images from the image which are 32x32 divisible + std::vector subImages; + subImages.reserve(image.width() / MICRO_WIDTH * image.height() / MICRO_HEIGHT); + QList frameIndicesList = extractSubImages(image, subImages, frameIndex); + + for (auto &subImage : subImages) { + D1GfxFrame *frame = this->gfx->insertFrame(frameIndex, subImage); + LevelTabFrameWidget::selectFrameType(frame); + frameIndex++; + } + + this->min->getCelFrameIndices(subtileIndex).swap(frameIndicesList); + // reset subtile flags + this->sol->setSubtileProperties(subtileIndex, 0); // update the view this->update(); diff --git a/source/views/levelcelview.h b/source/views/levelcelview.h index f801bcff..6f98e691 100644 --- a/source/views/levelcelview.h +++ b/source/views/levelcelview.h @@ -48,9 +48,18 @@ class LevelCelView : public QWidget { void initialize(D1Gfx *gfx, D1Min *min, D1Til *til, D1Sol *sol, D1Amp *amp); - int getCurrentFrameIndex(); - int getCurrentSubtileIndex(); - int getCurrentTileIndex(); + [[nodiscard]] int getCurrentFrameIndex() const + { + return this->currentFrameIndex; + } + [[nodiscard]] int getCurrentSubtileIndex() const + { + return this->currentSubtileIndex; + } + [[nodiscard]] int getCurrentTileIndex() const + { + return this->currentTileIndex; + } void framePixelClicked(unsigned x, unsigned y); @@ -62,7 +71,7 @@ class LevelCelView : public QWidget { void replaceCurrentFrame(int frameIdx, const QImage &image); void sendRemoveFrameCmd(); - void removeCurrentFrame(int idx); + void removeCurrentFrame(int index); void createSubtile(); void cloneSubtile(); @@ -89,9 +98,8 @@ class LevelCelView : public QWidget { void update(); void collectFrameUsers(int frameIndex, QList &users) const; void collectSubtileUsers(int subtileIndex, QList &users) const; - void insertFrame(IMAGE_FILE_MODE mode, int index, const QImage &image); void insertFrames(IMAGE_FILE_MODE mode, const QStringList &imagefilePaths, bool append); - void insertFrames(int startingIndex, const std::vector &images, IMAGE_FILE_MODE mode); + void insertFrames(int index, const QImage &image); void insertSubtile(int subtileIndex, const QImage &image); void insertSubtiles(IMAGE_FILE_MODE mode, int index, const QImage &image); void insertSubtiles(IMAGE_FILE_MODE mode, int index, const QString &imagefilePath); @@ -103,7 +111,6 @@ class LevelCelView : public QWidget { void assignFrames(const QImage &image, int subtileIndex, int frameIndex); void assignSubtiles(const QImage &image, int tileIndex, int subtileIndex); void removeFrame(int frameIndex); - void removeFrames(int startingIdx, int endingIndex); void removeSubtile(int subtileIndex); void removeUnusedFrames(QString &report); void removeUnusedSubtiles(QString &report);