diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 322793c9..f2f4c09a 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -7,7 +7,7 @@ 0 0 817 - 535 + 556 @@ -25,6 +25,9 @@ 0 + + Qt::FocusPolicy::ClickFocus + @@ -37,6 +40,9 @@ + + QComboBox::InsertPolicy::NoInsert + QComboBox::SizeAdjustPolicy::AdjustToContents @@ -163,7 +169,7 @@ - + Collision @@ -188,7 +194,10 @@ - + + + Qt::FocusPolicy::StrongFocus + @@ -214,7 +223,10 @@ - + + + Qt::FocusPolicy::StrongFocus + @@ -250,9 +262,22 @@ - + + + <html><head/><body><p>If checked, the image in the preview window will not be recreated when the settings are changed.</p></body></html> + - Preview actual size + Disable preview updates + + + + + + + <html><head/><body><p>If checked, the image shown in the preview window will not scale to fit into the available space. The aspect ratio of the image will never change.</p></body></html> + + + Disable preview scaling @@ -263,6 +288,9 @@ Reset + + false + @@ -283,6 +311,9 @@ Cancel + + false + @@ -290,6 +321,9 @@ Save + + false + @@ -323,16 +357,22 @@ + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + true + + Qt::AlignmentFlag::AlignCenter + 0 0 469 - 464 + 485 @@ -398,6 +438,11 @@ QComboBox
noscrollcombobox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
diff --git a/include/core/editcommands.h b/include/core/editcommands.h index eabfacc0..9a54063c 100644 --- a/include/core/editcommands.h +++ b/include/core/editcommands.h @@ -47,6 +47,13 @@ enum CommandId { #define IDMask_EventType_Trigger (1 << 11) #define IDMask_EventType_Heal (1 << 12) +#define IDMask_ConnectionDirection_Up (1 << 8) +#define IDMask_ConnectionDirection_Down (1 << 9) +#define IDMask_ConnectionDirection_Left (1 << 10) +#define IDMask_ConnectionDirection_Right (1 << 11) +#define IDMask_ConnectionDirection_Dive (1 << 12) +#define IDMask_ConnectionDirection_Emerge (1 << 13) + /// Implements a command to commit metatile paint actions /// onto the map using the pencil tool. class PaintMetatile : public QUndoCommand { @@ -400,7 +407,7 @@ public: void redo() override; bool mergeWith(const QUndoCommand *command) override; - int id() const override { return CommandId::ID_MapConnectionMove; } + int id() const override; private: MapConnection *connection; @@ -421,7 +428,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionChangeDirection; } + int id() const override; private: QPointer connection; @@ -443,7 +450,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionChangeMap; } + int id() const override; private: QPointer connection; @@ -465,7 +472,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionAdd; } + int id() const override; private: Map *map = nullptr; @@ -485,7 +492,7 @@ public: void undo() override; void redo() override; - int id() const override { return CommandId::ID_MapConnectionRemove; } + int id() const override; private: Map *map = nullptr; diff --git a/include/core/events.h b/include/core/events.h index 7c81499f..85116860 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -170,6 +170,7 @@ public: static QString typeToJsonKey(Event::Type type); static Event::Type typeFromJsonKey(QString type); static QList types(); + static QList groups(); // protected attributes protected: @@ -616,6 +617,10 @@ private: }; +inline uint qHash(const Event::Group &key, uint seed = 0) { + return qHash(static_cast(key), seed); +} + /// /// Keeps track of scripts diff --git a/include/core/map.h b/include/core/map.h index c1a14b04..9a4c2185 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -86,7 +86,7 @@ public: bool hasEvent(Event *) const; void deleteConnections(); - QList getConnections() const; + QList getConnections() const { return m_connections; } void removeConnection(MapConnection *); void addConnection(MapConnection *); void loadConnection(MapConnection *); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 21c00ac6..496e884f 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -26,13 +26,19 @@ public: QString direction() const { return m_direction; } void setDirection(const QString &direction, bool mirror = true); + bool isCardinal() const { return isCardinal(m_direction); } + bool isHorizontal() const { return isHorizontal(m_direction); } + bool isVertical() const { return isVertical(m_direction); } + bool isDiving() const { return isDiving(m_direction); } + int offset() const { return m_offset; } void setOffset(int offset, bool mirror = true); MapConnection* findMirror(); MapConnection* createMirror(); - QPixmap getPixmap(); + QPixmap render() const; + QPoint relativePos(bool clipped = false) const; static QPointer project; static const QMap oppositeDirections; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 57da8840..3b6bbf12 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -94,9 +94,12 @@ public: int getHeight() const { return height; } int getBorderWidth() const { return border_width; } int getBorderHeight() const { return border_height; } + int getBorderDrawWidth() const; + int getBorderDrawHeight() const; - bool isWithinBounds(int x, int y); - bool isWithinBorderBounds(int x, int y); + bool isWithinBounds(int x, int y) const; + bool isWithinBounds(const QRect &rect) const; + bool isWithinBorderBounds(int x, int y) const; bool getBlock(int x, int y, Block *out); void setBlock(int x, int y, Block block, bool enableScriptCallback = false); @@ -139,6 +142,8 @@ private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + static int getBorderDrawDistance(int dimension, qreal minimum); + signals: void dimensionsChanged(const QSize &size); void needsRedrawing(); diff --git a/include/editor.h b/include/editor.h index 4c7bff02..48fdfc0d 100644 --- a/include/editor.h +++ b/include/editor.h @@ -175,8 +175,6 @@ public: void eventsView_onMousePress(QMouseEvent *event); - int getBorderDrawDistance(int dimension); - bool selectingEvent = false; void deleteSelectedEvents(); diff --git a/include/mainwindow.h b/include/mainwindow.h index d8fe18a3..cdc641c2 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -301,6 +301,10 @@ public: Ui::MainWindow *ui; QPointer editor = nullptr; +signals: + void mapOpened(Map*); + void layoutOpened(Layout*); + private: QLabel *label_MapRulerStatus = nullptr; QPointer tilesetEditor = nullptr; diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 88a00447..ffc4d272 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -1,10 +1,9 @@ #ifndef MAPIMAGEEXPORTER_H #define MAPIMAGEEXPORTER_H -#include "map.h" -#include "editor.h" +#include "project.h" -#include +class QGifImage; namespace Ui { class MapImageExporter; @@ -17,21 +16,17 @@ enum ImageExporterMode { }; struct ImageExporterSettings { - bool showObjects = false; - bool showWarps = false; - bool showBGs = false; - bool showTriggers = false; - bool showHealLocations = false; - bool showUpConnections = false; - bool showDownConnections = false; - bool showLeftConnections = false; - bool showRightConnections = false; + QSet showEvents; + QSet showConnections; bool showGrid = false; bool showBorder = false; bool showCollision = false; - bool previewActualSize = false; + bool disablePreviewScaling = false; + bool disablePreviewUpdates = false; int timelapseSkipAmount = 1; int timelapseDelayMs = 200; + // Not exposed as a setting in the UI atm (our color input widget has no alpha channel). + QColor fillColor = Qt::transparent; }; class MapImageExporter : public QDialog @@ -39,32 +34,58 @@ class MapImageExporter : public QDialog Q_OBJECT public: - explicit MapImageExporter(QWidget *parent, Editor *editor, ImageExporterMode mode); + explicit MapImageExporter(QWidget *parent, Project *project, Map *map, ImageExporterMode mode = ImageExporterMode::Normal) + : MapImageExporter(parent, project, map, map->layout(), mode) {}; + explicit MapImageExporter(QWidget *parent, Project *project, Layout *layout, ImageExporterMode mode = ImageExporterMode::Normal) + : MapImageExporter(parent, project, nullptr, layout, mode) {}; ~MapImageExporter(); + ImageExporterMode mode() const { return m_mode; } + + void setMap(Map *map); + void setLayout(Layout *layout); + private: + explicit MapImageExporter(QWidget *parent, Project *project, Map *map, Layout *layout, ImageExporterMode mode); + Ui::MapImageExporter *ui; - - Layout *m_layout = nullptr; + Project *m_project = nullptr; Map *m_map = nullptr; - Editor *m_editor = nullptr; + Layout *m_layout = nullptr; QGraphicsScene *m_scene = nullptr; - - QPixmap m_preview; + QGifImage *m_timelapseGifImage = nullptr; + QBuffer *m_timelapseBuffer = nullptr; + QMovie *m_timelapseMovie = nullptr; + QGraphicsPixmapItem *m_preview = nullptr; ImageExporterSettings m_settings; ImageExporterMode m_mode = ImageExporterMode::Normal; + ImageExporterMode m_originalMode; - void updatePreview(); + void setModeSpecificUi(); + void setSelectionText(const QString &text); + void updateMapSelection(); + void resetSettings(); + QString getTitle(ImageExporterMode mode); + QString getDescription(ImageExporterMode mode); + void updatePreview(bool forceUpdate = false); void scalePreview(); - void updateShowBorderState(); + bool eventsEnabled(); + void setEventGroupEnabled(Event::Group group, bool enable); + bool connectionsEnabled(); + void setConnectionDirectionEnabled(const QString &dir, bool enable); void saveImage(); - QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); + QGifImage* createTimelapseGifImage(QProgressDialog *progress); + QPixmap getStitchedImage(QProgressDialog *progress); QPixmap getFormattedMapPixmap(); - QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder = false); - QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder = false, bool ignoreGrid = false); - void paintGrid(QPixmap *pixmap, bool ignoreBorder = false); - bool historyItemAppliesToFrame(const QUndoCommand *command); + void paintBorder(QPainter *painter, Layout *layout); + void paintCollision(QPainter *painter, Layout *layout); + void paintConnections(QPainter *painter, const Map *map); + void paintEvents(QPainter *painter, const Map *map); + void paintGrid(QPainter *painter, const Layout *layout = nullptr); + QMargins getMargins(const Map *map); + QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor); + bool currentHistoryAppliesToFrame(QUndoStack *historyStack); protected: virtual void showEvent(QShowEvent *) override; @@ -84,15 +105,16 @@ private slots: void on_checkBox_ConnectionRight_stateChanged(int state); void on_checkBox_AllConnections_stateChanged(int state); - void on_checkBox_Elevation_stateChanged(int state); + void on_checkBox_Collision_stateChanged(int state); void on_checkBox_Grid_stateChanged(int state); void on_checkBox_Border_stateChanged(int state); void on_pushButton_Reset_pressed(); - void on_spinBox_TimelapseDelay_valueChanged(int delayMs); - void on_spinBox_FrameSkip_valueChanged(int skip); + void on_spinBox_TimelapseDelay_editingFinished(); + void on_spinBox_FrameSkip_editingFinished(); - void on_checkBox_ActualSize_stateChanged(int state); + void on_checkBox_DisablePreviewScaling_stateChanged(int state); + void on_checkBox_DisablePreviewUpdates_stateChanged(int state); }; #endif // MAPIMAGEEXPORTER_H diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 38b6a0d5..8850448d 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -5,7 +5,7 @@ #include -int getEventTypeMask(QList events) { +int getEventTypeMask(const QList &events) { int eventTypeMask = 0; for (auto event : events) { Event::Group groupType = event->getEventGroup(); @@ -24,6 +24,26 @@ int getEventTypeMask(QList events) { return eventTypeMask; } +int getConnectionDirectionMask(const QList &directions) { + int mask = 0; + for (auto direction : directions) { + if (direction == "up") { + mask |= IDMask_ConnectionDirection_Up; + } else if (direction == "down") { + mask |= IDMask_ConnectionDirection_Down; + } else if (direction == "left") { + mask |= IDMask_ConnectionDirection_Left; + } else if (direction == "right") { + mask |= IDMask_ConnectionDirection_Right; + } else if (direction == "dive") { + mask |= IDMask_ConnectionDirection_Dive; + } else if (direction == "emerge") { + mask |= IDMask_ConnectionDirection_Emerge; + } + } + return mask; +} + void renderBlocks(Layout *layout, bool ignoreCache = false) { layout->layoutItem->draw(ignoreCache); layout->collisionItem->draw(ignoreCache); @@ -587,6 +607,10 @@ bool MapConnectionMove::mergeWith(const QUndoCommand *command) { return true; } +int MapConnectionMove::id() const { + return CommandId::ID_MapConnectionMove | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -629,6 +653,10 @@ void MapConnectionChangeDirection::undo() { QUndoCommand::undo(); } +int MapConnectionChangeDirection::id() const { + return CommandId::ID_MapConnectionChangeDirection | getConnectionDirectionMask({this->oldDirection, this->newDirection}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -664,6 +692,10 @@ void MapConnectionChangeMap::undo() { QUndoCommand::undo(); } +int MapConnectionChangeMap::id() const { + return CommandId::ID_MapConnectionChangeMap | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -708,6 +740,10 @@ void MapConnectionAdd::undo() { QUndoCommand::undo(); } +int MapConnectionAdd::id() const { + return CommandId::ID_MapConnectionAdd | getConnectionDirectionMask({this->connection->direction()}); +} + /****************************************************************************** ************************************************************************ ******************************************************************************/ @@ -745,3 +781,7 @@ void MapConnectionRemove::undo() { QUndoCommand::undo(); } + +int MapConnectionRemove::id() const { + return CommandId::ID_MapConnectionRemove | getConnectionDirectionMask({this->connection->direction()}); +} diff --git a/src/core/events.cpp b/src/core/events.cpp index 33d333e3..164cc34d 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -73,17 +73,23 @@ void Event::modify() { this->map->modify(); } +const QMap groupToStringMap = { + {Event::Group::Object, "Object"}, + {Event::Group::Warp, "Warp"}, + {Event::Group::Coord, "Trigger"}, + {Event::Group::Bg, "BG"}, + {Event::Group::Heal, "Heal Location"}, +}; + QString Event::groupToString(Event::Group group) { - static const QMap groupToStringMap = { - {Event::Group::Object, "Object"}, - {Event::Group::Warp, "Warp"}, - {Event::Group::Coord, "Trigger"}, - {Event::Group::Bg, "BG"}, - {Event::Group::Heal, "Heal Location"}, - }; return groupToStringMap.value(group); } +QList Event::groups() { + static QList groupList = groupToStringMap.keys(); + return groupList; +} + // These are the expected key names used in the map.json files. // We re-use them for key names in the copy/paste JSON data, const QMap typeToJsonKeyMap = { diff --git a/src/core/map.cpp b/src/core/map.cpp index 330132a1..b9fe4c90 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -232,10 +232,6 @@ void Map::deleteConnections() { m_connections.clear(); } -QList Map::getConnections() const { - return m_connections; -} - void Map::addConnection(MapConnection *connection) { if (!connection || m_connections.contains(connection)) return; @@ -243,7 +239,7 @@ void Map::addConnection(MapConnection *connection) { // Maps should only have one Dive/Emerge connection at a time. // (Users can technically have more by editing their data manually, but we will only display one at a time) // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { for (const auto &i : m_connections) { if (i->direction() == connection->direction()) { trackConnection(connection); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index c478003b..d37a8c74 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -60,7 +60,7 @@ Map* MapConnection::targetMap() const { return getMap(m_targetMapName); } -QPixmap MapConnection::getPixmap() { +QPixmap MapConnection::render() const { auto map = targetMap(); if (!map) return QPixmap(); @@ -68,6 +68,28 @@ QPixmap MapConnection::getPixmap() { return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout() : nullptr); } +// Get the position of the target map relative to its parent map. +// For right/down connections this is offset by the dimensions of the parent map. +// For left/up connections this is offset by the dimensions of the target map. +// If 'clipped' is true, only the rendered dimensions of the target map will be used, rather than its full dimensions. +QPoint MapConnection::relativePos(bool clipped) const { + int x = 0, y = 0; + if (m_direction == "right") { + if (m_parentMap) x = m_parentMap->getWidth(); + y = m_offset; + } else if (m_direction == "down") { + x = m_offset; + if (m_parentMap) y = m_parentMap->getHeight(); + } else if (m_direction == "left") { + if (targetMap()) x = !clipped ? -targetMap()->getWidth() : -targetMap()->getConnectionRect(m_direction).width(); + y = m_offset; + } else if (m_direction == "up") { + x = m_offset; + if (targetMap()) y = !clipped ? -targetMap()->getHeight() : -targetMap()->getConnectionRect(m_direction).height(); + } + return QPoint(x, y); +} + void MapConnection::setParentMap(Map* map, bool mirror) { if (map == m_parentMap) return; diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index e443a635..86ddda4f 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -50,14 +50,37 @@ Layout::Settings Layout::settings() const { return settings; } -bool Layout::isWithinBounds(int x, int y) { +bool Layout::isWithinBounds(int x, int y) const { return (x >= 0 && x < this->getWidth() && y >= 0 && y < this->getHeight()); } -bool Layout::isWithinBorderBounds(int x, int y) { +bool Layout::isWithinBounds(const QRect &rect) const { + return rect.left() >= 0 && rect.right() < this->getWidth() && rect.top() >= 0 && rect.bottom() < this->getHeight(); +} + +bool Layout::isWithinBorderBounds(int x, int y) const { return (x >= 0 && x < this->getBorderWidth() && y >= 0 && y < this->getBorderHeight()); } +int Layout::getBorderDrawWidth() const { + return getBorderDrawDistance(border_width, BORDER_DISTANCE); +} + +int Layout::getBorderDrawHeight() const { + return getBorderDrawDistance(border_height, BORDER_DISTANCE); +} + +// We need to draw sufficient border blocks to fill the area that gets loaded around the player in-game (BORDER_DISTANCE). +// Note that this is not the same as the player's view distance. +// The result will be some multiple of the input dimension, because we only draw the border in increments of its full width/height. +int Layout::getBorderDrawDistance(int dimension, qreal minimum) { + if (dimension >= minimum) + return dimension; + + // Get first multiple of dimension >= the minimum + return dimension * qCeil(minimum / qMax(dimension, 1)); +} + bool Layout::getBlock(int x, int y, Block *out) { if (isWithinBounds(x, y)) { int i = y * getWidth() + x; diff --git a/src/editor.cpp b/src/editor.cpp index 08dc0146..29b6cb5d 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -761,7 +761,7 @@ void Editor::displayConnection(MapConnection *connection) { if (!connection) return; - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { displayDivingConnection(connection); return; } @@ -826,7 +826,7 @@ void Editor::removeConnectionPixmap(MapConnection *connection) { if (!connection) return; - if (MapConnection::isDiving(connection->direction())) { + if (connection->isDiving()) { removeDivingMapPixmap(connection); return; } @@ -1795,8 +1795,8 @@ void Editor::displayMapBorder() { int borderWidth = this->layout->getBorderWidth(); int borderHeight = this->layout->getBorderHeight(); - int borderHorzDist = getBorderDrawDistance(borderWidth); - int borderVertDist = getBorderDrawDistance(borderHeight); + int borderHorzDist = this->layout->getBorderDrawWidth(); + int borderVertDist = this->layout->getBorderDrawHeight(); QPixmap pixmap = this->layout->renderBorder(); for (int y = -borderVertDist; y < this->layout->getHeight() + borderVertDist; y += borderHeight) for (int x = -borderHorzDist; x < this->layout->getWidth() + borderHorzDist; x += borderWidth) { @@ -1821,17 +1821,6 @@ void Editor::updateMapConnections() { item->render(true); } -int Editor::getBorderDrawDistance(int dimension) { - // Draw sufficient border blocks to fill the player's view (BORDER_DISTANCE) - if (dimension >= BORDER_DISTANCE) { - return dimension; - } else if (dimension) { - return dimension * (BORDER_DISTANCE / dimension + (BORDER_DISTANCE % dimension ? 1 : 0)); - } else { - return BORDER_DISTANCE; - } -} - void Editor::toggleGrid(bool checked) { if (porymapConfig.showGrid == checked) return; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index af7eaabe..650acdaf 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -960,6 +960,9 @@ bool MainWindow::setMap(QString map_name) { Scripting::cb_MapOpened(map_name); prefab.updatePrefabUi(editor->layout); updateTilesetEditor(); + + emit mapOpened(editor->map); + return true; } @@ -1013,6 +1016,8 @@ bool MainWindow::setLayout(QString layoutId) { userConfig.recentMapOrLayout = layoutId; + emit layoutOpened(editor->layout); + return true; } @@ -2555,12 +2560,23 @@ void MainWindow::on_actionImport_Map_from_Advance_Map_1_92_triggered() { void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { if (!editor->project) return; - // If the user is requesting this window again we assume it's for a new - // window (the map/mode may have changed), so delete the old window. - if (this->mapImageExporter) + // If the user is requesting this window again with a different mode + // then we'll recreate the window with the new mode. + if (this->mapImageExporter && this->mapImageExporter->mode() != mode) delete this->mapImageExporter; - this->mapImageExporter = new MapImageExporter(this, this->editor, mode); + if (!this->mapImageExporter) { + // Open new image export window + if (this->editor->map){ + this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->map, mode); + } else if (this->editor->layout) { + this->mapImageExporter = new MapImageExporter(this, this->editor->project, this->editor->layout, mode); + } + if (this->mapImageExporter) { + connect(this, &MainWindow::mapOpened, this->mapImageExporter, &MapImageExporter::setMap); + connect(this, &MainWindow::layoutOpened, this->mapImageExporter, &MapImageExporter::setLayout); + } + } openSubWindow(this->mapImageExporter); } diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 93bab7c5..f1bceac5 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -5,7 +5,7 @@ #include ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection) - : QGraphicsPixmapItem(connection->getPixmap()), + : QGraphicsPixmapItem(connection->render()), connection(connection) { this->setEditable(true); @@ -28,7 +28,7 @@ void ConnectionPixmapItem::refresh() { // Render additional visual effects on top of the base map image. void ConnectionPixmapItem::render(bool ignoreCache) { if (ignoreCache) - this->basePixmap = this->connection->getPixmap(); + this->basePixmap = this->connection->render(); QPixmap pixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); this->setZValue(-1); @@ -63,10 +63,10 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari int newOffset = this->connection->offset(); // Restrict movement to the metatile grid and perpendicular to the connection direction. - if (MapConnection::isVertical(this->connection->direction())) { + if (this->connection->isVertical()) { x = (round(newPos.x() / this->mWidth) * this->mWidth) - this->originX; newOffset = x / this->mWidth; - } else if (MapConnection::isHorizontal(this->connection->direction())) { + } else if (this->connection->isHorizontal()) { y = (round(newPos.y() / this->mHeight) * this->mHeight) - this->originY; newOffset = y / this->mHeight; } @@ -87,9 +87,9 @@ void ConnectionPixmapItem::updatePos() { qreal x = this->originX; qreal y = this->originY; - if (MapConnection::isVertical(this->connection->direction())) { + if (this->connection->isVertical()) { x += this->connection->offset() * this->mWidth; - } else if (MapConnection::isHorizontal(this->connection->direction())) { + } else if (this->connection->isHorizontal()) { y += this->connection->offset() * this->mHeight; } @@ -98,22 +98,13 @@ void ConnectionPixmapItem::updatePos() { } void ConnectionPixmapItem::updateOrigin() { - const Map *parentMap = connection->parentMap(); - const Map *targetMap = connection->targetMap(); - const QString direction = connection->direction(); - int x = 0, y = 0; - - if (direction == "right") { - if (parentMap) x = parentMap->getWidth(); - } else if (direction == "down") { - if (parentMap) y = parentMap->getHeight(); - } else if (direction == "left") { - if (targetMap) x = -targetMap->getConnectionRect(direction).width(); - } else if (direction == "up") { - if (targetMap) y = -targetMap->getConnectionRect(direction).height(); + if (this->connection->isVertical()) { + this->originX = 0; + this->originY = this->connection->relativePos(true).y() * this->mHeight; + } else if (this->connection->isHorizontal()) { + this->originX = this->connection->relativePos(true).x() * this->mWidth; + this->originY = 0; } - this->originX = x * this->mWidth; - this->originY = y * this->mHeight; updatePos(); } diff --git a/src/ui/divingmappixmapitem.cpp b/src/ui/divingmappixmapitem.cpp index 70a611fa..e20b8f25 100644 --- a/src/ui/divingmappixmapitem.cpp +++ b/src/ui/divingmappixmapitem.cpp @@ -25,7 +25,7 @@ QPixmap DivingMapPixmapItem::getBasePixmap(MapConnection* connection) { return QPixmap(); // Save some rendering time if it won't be displayed if (connection->targetMapName() == connection->parentMapName()) return QPixmap(); // If the map is connected to itself then rendering is pointless. - return connection->getPixmap(); + return connection->render(); } void DivingMapPixmapItem::updatePixmap() { diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 4b11ea95..39607bca 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -8,75 +8,114 @@ #include #include -#define STITCH_MODE_BORDER_DISTANCE 2 - -QString getTitle(ImageExporterMode mode) { +QString MapImageExporter::getTitle(ImageExporterMode mode) { switch (mode) { case ImageExporterMode::Normal: - return "Export Map Image"; + return QString("Export %1 Image").arg(m_map ? "Map" : "Layout"); case ImageExporterMode::Stitch: - return "Export Map Stitch Image"; + return QStringLiteral("Export Map Stitch Image"); case ImageExporterMode::Timelapse: - return "Export Map Timelapse Image"; + return QString("Export %1 Timelapse Image").arg(m_map ? "Map" : "Layout"); } return ""; } -QString getDescription(ImageExporterMode mode) { +QString MapImageExporter::getDescription(ImageExporterMode mode) { switch (mode) { case ImageExporterMode::Normal: - return "Exports an image of the selected map."; + return QString("Exports an image of the selected %1.").arg(m_map ? "Map" : "Layout"); case ImageExporterMode::Stitch: return "Exports a combined image of all the maps connected to the selected map."; case ImageExporterMode::Timelapse: - return "Exports a GIF of the edit history for the selected map."; + return QString("Exports a GIF of the edit history for the selected %1.").arg(m_map ? "Map" : "Layout"); } return ""; } -MapImageExporter::MapImageExporter(QWidget *parent, Editor *editor, ImageExporterMode mode) : +MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, Layout *layout, ImageExporterMode mode) : QDialog(parent), - ui(new Ui::MapImageExporter) + ui(new Ui::MapImageExporter), + m_project(project), + m_map(map), + m_layout(layout), + m_mode(mode), + m_originalMode(mode) { setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - m_map = editor->map; - m_layout = editor->layout; - m_editor = editor; - m_mode = mode; - setWindowTitle(getTitle(m_mode)); - ui->label_Description->setText(getDescription(m_mode)); - ui->groupBox_Connections->setVisible(m_mode != ImageExporterMode::Stitch); - ui->groupBox_Timelapse->setVisible(m_mode == ImageExporterMode::Timelapse); - if (m_map) { - ui->comboBox_MapSelection->addItems(editor->project->mapNames); - ui->comboBox_MapSelection->setCurrentText(m_map->name()); - ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down - } else { - // Some settings only apply to maps. When exporting an image in layout-only mode we hide them. - ui->comboBox_MapSelection->setVisible(false); - ui->label_MapSelection->setVisible(false); - ui->groupBox_Events->setVisible(false); - ui->groupBox_Connections->setVisible(false); - } + m_scene = new QGraphicsScene(this); + m_preview = m_scene->addPixmap(QPixmap()); + ui->graphicsView_Preview->setScene(m_scene); + + setModeSpecificUi(); connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); + + // Update the map selector when the text changes. + // We don't use QComboBox::currentTextChanged to avoid unnecessary re-rendering. + connect(ui->comboBox_MapSelection, QOverload::of(&QComboBox::currentIndexChanged), this, &MapImageExporter::updateMapSelection); + connect(ui->comboBox_MapSelection->lineEdit(), &QLineEdit::editingFinished, this, &MapImageExporter::updateMapSelection); + + ui->graphicsView_Preview->setFocus(); } MapImageExporter::~MapImageExporter() { - delete m_scene; + delete m_timelapseGifImage; delete ui; } +void MapImageExporter::setModeSpecificUi() { + m_mode = m_originalMode; + if (!m_map && m_mode == ImageExporterMode::Stitch) { + // Stitch mode is not valid with only a layout open. + // This could happen if a user opens the stitch exporter with a map, then opens a layout. + m_mode = ImageExporterMode::Normal; + } + + setWindowTitle(getTitle(m_mode)); + ui->label_Description->setText(getDescription(m_mode)); + ui->groupBox_Connections->setVisible(m_map && m_mode != ImageExporterMode::Stitch); + ui->groupBox_Timelapse->setVisible(m_mode == ImageExporterMode::Timelapse); + ui->groupBox_Events->setVisible(m_map != nullptr); + + // Initialize map selector + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->clear(); + if (m_map) { + ui->comboBox_MapSelection->addItems(m_project->mapNames); + ui->comboBox_MapSelection->setCurrentText(m_map->name()); + ui->label_MapSelection->setText(m_mode == ImageExporterMode::Stitch ? QStringLiteral("Starting Map") : QStringLiteral("Map")); + } else if (m_layout) { + ui->comboBox_MapSelection->addItems(m_project->layoutIds); + ui->comboBox_MapSelection->setCurrentText(m_layout->id); + ui->label_MapSelection->setText(QStringLiteral("Layout")); + } + + if (m_mode == ImageExporterMode::Timelapse) { + // TODO: At the moment edit history for events (and the DraggablePixmapItem class) + // explicitly depend on the editor and assume their map is currently open. + // Other edit commands rely on this more subtly, like triggering API callbacks or + // spending time rendering their layout (which can make creating timelapses very slow). + // Until this is resolved, the selected map/layout must remain the same as in the editor. + // We enforce this here by disabling the selector, and in MainWindow by programmatically + // changing the exporter's map/layout selection if the user opens a new one in the editor. + ui->comboBox_MapSelection->setEnabled(false); + ui->label_MapSelection->setEnabled(false); + } + + // Update for any mode-specific default settings + resetSettings(); +} + // Allow the window to open before displaying the preview. void MapImageExporter::showEvent(QShowEvent *event) { QWidget::showEvent(event); if (!event->spontaneous()) - QTimer::singleShot(0, this, &MapImageExporter::updatePreview); + QTimer::singleShot(0, this, [this](){ updatePreview(); }); } void MapImageExporter::resizeEvent(QResizeEvent *event) { @@ -84,14 +123,65 @@ void MapImageExporter::resizeEvent(QResizeEvent *event) { scalePreview(); } -void MapImageExporter::saveImage() { - // Make sure preview is up-to-date before we save. - if (m_preview.isNull()) - updatePreview(); - if (m_preview.isNull()) - return; +void MapImageExporter::setMap(Map *map) { + if (map) setSelectionText(map->name()); +} + +void MapImageExporter::setLayout(Layout *layout) { + if (layout) setSelectionText(layout->id); +} + +void MapImageExporter::setSelectionText(const QString &text) { + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(text); + updateMapSelection(); +} + +void MapImageExporter::updateMapSelection() { + auto oldMap = m_map; + auto oldLayout = m_layout; + + const QString text = ui->comboBox_MapSelection->currentText(); + if (m_project->mapNames.contains(text)) { + auto newMap = m_project->loadMap(text); + if (newMap) { + m_map = newMap; + m_layout = newMap->layout(); + } + } else if (m_project->layoutIds.contains(text)) { + auto newLayout = m_project->loadLayout(text); + if (newLayout) { + m_map = nullptr; + m_layout = newLayout; + } + } + + // Ensure text in the combo box remains valid + const QSignalBlocker b(ui->comboBox_MapSelection); + ui->comboBox_MapSelection->setCurrentText(m_map ? m_map->name() : m_layout->id); + + if (m_map != oldMap && (!m_map || !oldMap)) { + // Switching to or from layout-only mode + setModeSpecificUi(); + } + if (m_map != oldMap || m_layout != oldLayout){ + updatePreview(); + } +} + +void MapImageExporter::saveImage() { + // If the preview is empty (because progress was canceled) or if updates were disabled + // then we should ensure the image in the preview is up-to-date before exporting. + if (m_preview->pixmap().isNull() || m_settings.disablePreviewUpdates) { + updatePreview(true); + if (m_preview->pixmap().isNull()) + return; // Canceled + } + if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) { + // Shouldn't happen. We have a preview for the timelapse, but no timelapse image. + return; + } - const QString title = getTitle(m_mode); const QString itemName = m_map ? m_map->name() : m_layout->name; QString defaultFilename; switch (m_mode) @@ -112,117 +202,25 @@ void MapImageExporter::saveImage() { .arg(defaultFilename) .arg(m_mode == ImageExporterMode::Timelapse ? "gif" : "png"); QString filter = m_mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)"; - QString filepath = FileDialog::getSaveFileName(this, title, defaultFilepath, filter); + QString filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, filter); if (!filepath.isEmpty()) { switch (m_mode) { case ImageExporterMode::Normal: case ImageExporterMode::Stitch: // Normal and Stitch modes already have the image ready to go in the preview. - m_preview.save(filepath); + m_preview->pixmap().save(filepath); break; case ImageExporterMode::Timelapse: - // Timelapse will play in order of layout changes then map changes (events) - // TODO: potentially update in the future? - QGifImage timelapseImg; - timelapseImg.setDefaultDelay(m_settings.timelapseDelayMs); - timelapseImg.setDefaultTransparentColor(QColor(0, 0, 0)); - - // lambda to avoid redundancy - auto generateTimelapseFromHistory = [this, &timelapseImg](QString progressText, QUndoStack *historyStack){ - QProgressDialog progress(progressText, "Cancel", 0, 1, this); - progress.setAutoClose(true); - progress.setWindowModality(Qt::WindowModal); - progress.setModal(true); - progress.setMaximum(1); - progress.setValue(0); - - int maxWidth = m_layout->getWidth() * 16; - int maxHeight = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - maxWidth += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - maxHeight += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - } - // Rewind to the specified start of the map edit history. - int i = 0; - while (historyStack->canUndo()) { - progress.setValue(i); - historyStack->undo(); - int width = m_layout->getWidth() * 16; - int height = m_layout->getHeight() * 16; - if (m_settings.showBorder) { - width += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - height += 2 * STITCH_MODE_BORDER_DISTANCE * 16; - } - if (width > maxWidth) { - maxWidth = width; - } - if (height > maxHeight) { - maxHeight = height; - } - i++; - } - - // Draw each frame, skpping the specified number of map edits in - // the undo history. - progress.setMaximum(i); - while (i > 0) { - if (progress.wasCanceled()) { - progress.close(); - while (i > 0 && historyStack->canRedo()) { - i--; - historyStack->redo(); - } - return; - } - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - progress.setValue(progress.maximum() - i); - QPixmap pixmap = getFormattedMapPixmap(); - if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { - QPixmap pixmap2 = QPixmap(maxWidth, maxHeight); - QPainter painter(&pixmap2); - pixmap2.fill(QColor(0, 0, 0)); - painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap); - painter.end(); - pixmap = pixmap2; - } - timelapseImg.addFrame(pixmap.toImage()); - for (int j = 0; j < m_settings.timelapseSkipAmount; j++) { - if (i > 0) { - i--; - historyStack->redo(); - while (historyStack->canRedo() && - !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { - i--; - historyStack->redo(); - } - } - } - } - // The latest map state is the last animated frame. - QPixmap pixmap = getFormattedMapPixmap(); - timelapseImg.addFrame(pixmap.toImage()); - progress.close(); - }; - - if (m_layout) - generateTimelapseFromHistory("Building layout timelapse...", &m_layout->editHistory); - - if (m_map) - generateTimelapseFromHistory("Building map timelapse...", m_map->editHistory()); - - timelapseImg.save(filepath); + m_timelapseGifImage->save(filepath); break; } close(); } } -bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { - if (command->isObsolete()) +bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) { + const QUndoCommand *command = historyStack->command(historyStack->index()); + if (!command || command->isObsolete()) return false; switch (command->id() & 0xFF) { @@ -243,39 +241,167 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { case CommandId::ID_MapConnectionChangeDirection: case CommandId::ID_MapConnectionChangeMap: case CommandId::ID_MapConnectionAdd: - case CommandId::ID_MapConnectionRemove: - return m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections; + case CommandId::ID_MapConnectionRemove: { + if (!connectionsEnabled()) + return false; + uint32_t flags = 0; + if (m_settings.showConnections.contains("up")) flags |= IDMask_ConnectionDirection_Up; + if (m_settings.showConnections.contains("down")) flags |= IDMask_ConnectionDirection_Down; + if (m_settings.showConnections.contains("left")) flags |= IDMask_ConnectionDirection_Left; + if (m_settings.showConnections.contains("right")) flags |= IDMask_ConnectionDirection_Right; + return (command->id() & flags) != 0; + } case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: + case CommandId::ID_EventPaste: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { - bool eventTypeIsApplicable = - (m_settings.showObjects && (command->id() & IDMask_EventType_Object) != 0) - || (m_settings.showWarps && (command->id() & IDMask_EventType_Warp) != 0) - || (m_settings.showBGs && (command->id() & IDMask_EventType_BG) != 0) - || (m_settings.showTriggers && (command->id() & IDMask_EventType_Trigger) != 0) - || (m_settings.showHealLocations && (command->id() & IDMask_EventType_Heal) != 0); - return eventTypeIsApplicable; + if (!eventsEnabled()) + return false; + uint32_t flags = 0; + if (m_settings.showEvents.contains(Event::Group::Object)) flags |= IDMask_EventType_Object; + if (m_settings.showEvents.contains(Event::Group::Warp)) flags |= IDMask_EventType_Warp; + if (m_settings.showEvents.contains(Event::Group::Bg)) flags |= IDMask_EventType_BG; + if (m_settings.showEvents.contains(Event::Group::Coord)) flags |= IDMask_EventType_Trigger; + if (m_settings.showEvents.contains(Event::Group::Heal)) flags |= IDMask_EventType_Heal; + return (command->id() & flags) != 0; } default: return false; } } +QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor) { + if (pixmap.width() >= targetSize.width() && pixmap.height() >= targetSize.height()) + return pixmap; + + QPixmap resizedPixmap = QPixmap(targetSize); + QPainter painter(&resizedPixmap); + resizedPixmap.fill(fillColor); + + // Center the old pixmap in the new resized one. + int x = (targetSize.width() - pixmap.width()) / 2; + int y = (targetSize.height() - pixmap.height()) / 2; + painter.drawPixmap(x, y, pixmap.width(), pixmap.height(), pixmap); + return resizedPixmap; +} + +struct TimelapseStep { + QUndoStack* historyStack; + int initialStackIndex; + QString name; +}; + +QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) { + // TODO: Timelapse will play in order of layout changes then map changes (events, connections). Potentially update in the future? + QList steps; + steps.append({ + .historyStack = &m_layout->editHistory, + .initialStackIndex = m_layout->editHistory.index(), + .name = "layout", + }); + if (m_map) { + steps.append({ + .historyStack = m_map->editHistory(), + .initialStackIndex = m_map->editHistory()->index(), + .name = "map", + }); + } + + // Rewind the edit histories and get the maximum map size for the gif's canvas. + QSize canvasSize = QSize(0,0); + for (const auto &step : steps) { + progress->setLabelText(QString("Rewinding %1 edit history...").arg(step.name)); + progress->setMinimum(0); + progress->setMaximum(step.initialStackIndex); + progress->setValue(progress->minimum()); + do { + if (currentHistoryAppliesToFrame(step.historyStack) || step.historyStack->index() == step.initialStackIndex) { + // Either this is relevant edit history, or it's the final frame (which is always rendered). Record the size of the map at this point. + QMargins margins = getMargins(m_map); + canvasSize = canvasSize.expandedTo(QSize(m_layout->getWidth() * 16 + margins.left() + margins.right(), + m_layout->getHeight() * 16 + margins.top() + margins.bottom())); + } + if (step.historyStack->canUndo()){ + step.historyStack->undo(); + } else break; + progress->setValue(step.initialStackIndex - step.historyStack->index()); + } while (!progress->wasCanceled()); + } + + auto timelapseImg = new QGifImage(canvasSize); + timelapseImg->setDefaultDelay(m_settings.timelapseDelayMs); + timelapseImg->setDefaultTransparentColor(m_settings.fillColor); + + // Create the timelapse image frames + for (const auto &step : steps) { + if (step.historyStack->index() >= step.initialStackIndex) + continue; + + // Progress is represented by the number of commands we need to redo to finish the timelapse, + // which can be different than the number of image frames we need to create. + progress->setLabelText(QString("Building %1 timelapse...").arg(step.name)); + progress->setMinimum(step.historyStack->index()); + progress->setMaximum(step.initialStackIndex - step.historyStack->index()); + progress->setValue(progress->minimum()); + + int framesToSkip = m_settings.timelapseSkipAmount - 1; + while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex && !progress->wasCanceled()) { + if (currentHistoryAppliesToFrame(step.historyStack) && --framesToSkip <= 0) { + // Render frame, increasing its size if necessary to match the canvas. + QPixmap pixmap = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); + timelapseImg->addFrame(pixmap.toImage()); + framesToSkip = m_settings.timelapseSkipAmount - 1; + } + step.historyStack->redo(); + progress->setValue(step.historyStack->index() - progress->minimum()); + } + } + + // Ensure all edit histories are restored to their original states. + // We already make sure above that we don't overshoot the initial state, + // so this should only need to happen if progress was canceled. + // Restoring the edit history is required, so we will disable canceling from here on. + if (progress->wasCanceled()) { + delete timelapseImg; + timelapseImg = nullptr; + } + progress->setCancelButton(nullptr); + for (const auto &step : steps) { + if (step.historyStack->index() >= step.initialStackIndex) + continue; + progress->setLabelText(QString("Restoring %1 edit history...").arg(step.name)); + progress->setMinimum(0); + progress->setMaximum(step.initialStackIndex); + progress->setValue(step.historyStack->index()); + while (step.historyStack->canRedo() && step.historyStack->index() < step.initialStackIndex) { + step.historyStack->redo(); + progress->setValue(step.historyStack->index()); + } + } + + // Final frame should always be the current state of the map. + if (timelapseImg) { + QPixmap finalFrame = getExpandedPixmap(getFormattedMapPixmap(), canvasSize, m_settings.fillColor); + timelapseImg->addFrame(finalFrame.toImage()); + } + return timelapseImg; +} + struct StitchedMap { int x; int y; Map* map; }; -QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool includeBorder) { +QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Do a breadth-first search to gather a collection of // all reachable maps with their relative offsets. QSet visited; QList stitchedMaps; QList unvisited; - unvisited.append(StitchedMap{0, 0, m_editor->map}); + unvisited.append(StitchedMap{0, 0, m_map}); progress->setLabelText("Gathering stitched maps..."); while (!unvisited.isEmpty()) { @@ -291,276 +417,309 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu visited.insert(cur.map->name()); stitchedMaps.append(cur); - for (MapConnection *connection : cur.map->getConnections()) { - const QString direction = connection->direction(); - int x = cur.x; - int y = cur.y; - int offset = connection->offset(); - Map *connectionMap = connection->targetMap(); - if (!connectionMap) - continue; - if (direction == "up") { - x += offset; - y -= connectionMap->getHeight(); - } else if (direction == "down") { - x += offset; - y += cur.map->getHeight(); - } else if (direction == "left") { - x -= connectionMap->getWidth(); - y += offset; - } else if (direction == "right") { - x += cur.map->getWidth(); - y += offset; - } else { - // Ignore Dive/Emerge connections and unrecognized directions - continue; - } - unvisited.append(StitchedMap{x, y, connectionMap}); + for (const auto &connection : cur.map->getConnections()) { + if (!connection->isCardinal()) continue; + Map *connectedMap = connection->targetMap(); + if (!connectedMap) continue; + QPoint pos = connection->relativePos(); + unvisited.append(StitchedMap{cur.x + (pos.x() * 16), cur.y + (pos.y() * 16), connectedMap}); } } + if (stitchedMaps.isEmpty()) + return QPixmap(); - // Determine the overall dimensions of the stitched maps. - int maxX = INT_MIN; - int minX = INT_MAX; - int maxY = INT_MIN; - int minY = INT_MAX; - for (StitchedMap map : stitchedMaps) { - int left = map.x; - int right = map.x + map.map->getWidth(); - int top = map.y; - int bottom = map.y + map.map->getHeight(); - if (left < minX) - minX = left; - if (right > maxX) - maxX = right; - if (top < minY) - minY = top; - if (bottom > maxY) - maxY = bottom; - } - - if (includeBorder) { - minX -= STITCH_MODE_BORDER_DISTANCE; - maxX += STITCH_MODE_BORDER_DISTANCE; - minY -= STITCH_MODE_BORDER_DISTANCE; - maxY += STITCH_MODE_BORDER_DISTANCE; - } - - // Draw the maps on the full canvas, while taking - // their respective offsets into account. - progress->setLabelText("Drawing stitched maps..."); - progress->setValue(0); progress->setMaximum(stitchedMaps.size()); int numDrawn = 0; - QPixmap stitchedPixmap((maxX - minX) * 16, (maxY - minY) * 16); - stitchedPixmap.fill(Qt::black); - QPainter painter(&stitchedPixmap); - for (StitchedMap map : stitchedMaps) { - if (progress->wasCanceled()) { - return QPixmap(); - } - progress->setValue(numDrawn); - numDrawn++; - int pixelX = (map.x - minX) * 16; - int pixelY = (map.y - minY) * 16; - if (includeBorder) { - pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; - pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; - } - QPixmap pixmap = getFormattedMapPixmap(map.map); - painter.drawPixmap(pixelX, pixelY, pixmap); + // Determine the overall dimensions of the stitched maps. + QRect dimensions = QRect(0, 0, m_map->getWidth(), m_map->getHeight()) + getMargins(m_map); + for (const StitchedMap &map : stitchedMaps) { + dimensions |= (QRect(map.x, map.y, map.map->getWidth() * 16, map.map->getHeight() * 16) + getMargins(map.map)); } - // When including the borders, we simply draw all the maps again - // without their borders, since the first pass results in maps - // being occluded by other map borders. - if (includeBorder) { - progress->setLabelText("Drawing stitched maps without borders..."); + QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); + stitchedPixmap.fill(m_settings.fillColor); + + QPainter painter(&stitchedPixmap); + painter.translate(-dimensions.left(), -dimensions.top()); + + // Borders can occlude neighboring maps, so we draw all the borders before drawing any maps. + // Note: Borders can also overlap the borders of neighboring maps. It's not technically wrong to do this, + // but it might suggest to users that something is visible in-game that actually isn't. + // (e.g. in FRLG, Route 18's water border can overlap Fuchsia's tree border. It suggests you could + // see a jarring transition in-game from one of these maps, but because of the collision map the + // player isn't actually able to get close enough to this transition to see it). + // Perhaps some future export setting could limit the border rendering to the visibility range from walkable areas. + if (m_settings.showBorder) { + progress->setLabelText("Drawing borders..."); progress->setValue(0); - progress->setMaximum(stitchedMaps.size()); numDrawn = 0; - for (StitchedMap map : stitchedMaps) { + for (const StitchedMap &map : stitchedMaps) { if (progress->wasCanceled()) { return QPixmap(); } - progress->setValue(numDrawn); - numDrawn++; + painter.translate(map.x, map.y); + paintBorder(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); - int pixelX = (map.x - minX) * 16; - int pixelY = (map.y - minY) * 16; - QPixmap pixmapWithoutBorders = getFormattedMapPixmap(map.map, true); - painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); + progress->setValue(numDrawn++); + } + } + + // Draw the layout and collision images. + progress->setLabelText("Drawing maps..."); + progress->setValue(0); + numDrawn = 0; + for (const StitchedMap &map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + painter.translate(map.x, map.y); + painter.drawPixmap(0, 0, map.map->layout()->render(true)); + paintCollision(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); + + progress->setValue(numDrawn++); + } + + // Events can be occluded by neighboring maps if they are positioned + // near or outside the map's edge, so we draw them after all the maps. + // Nothing should be on top of the grid, so it's drawn last. + if (m_settings.showGrid || eventsEnabled()) { + progress->setLabelText("Drawing map decorations..."); + progress->setValue(0); + numDrawn = 0; + for (const StitchedMap &map : stitchedMaps) { + if (progress->wasCanceled()) { + return QPixmap(); + } + painter.translate(map.x, map.y); + paintEvents(&painter, map.map); + paintGrid(&painter, map.map->layout()); + painter.translate(-map.x, -map.y); + + progress->setValue(numDrawn++); } } return stitchedPixmap; } -void MapImageExporter::updatePreview() { - if (m_scene) { - delete m_scene; - m_scene = nullptr; - } - m_scene = new QGraphicsScene; +void MapImageExporter::updatePreview(bool forceUpdate) { + if (m_settings.disablePreviewUpdates && !forceUpdate) + return; - if (m_mode == ImageExporterMode::Stitch) { - QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); - progress.setAutoClose(true); - progress.setWindowModality(Qt::WindowModal); - progress.setModal(true); - progress.setMinimumDuration(1000); - m_preview = getStitchedImage(&progress, m_settings.showBorder); - progress.close(); - } else { - // Timelapse mode doesn't currently have a real preview. It just displays the current map as in Normal mode. - m_preview = getFormattedMapPixmap(); + QProgressDialog progress("", "Cancel", 0, 1, this); + progress.setAutoClose(true); + progress.setWindowModality(Qt::WindowModal); + progress.setModal(true); + progress.setMinimumDuration(1000); + + QPixmap previewPixmap; + if (m_mode == ImageExporterMode::Normal) { + previewPixmap = getFormattedMapPixmap(); + } else if (m_mode == ImageExporterMode::Stitch) { + previewPixmap = getStitchedImage(&progress); + } else if (m_mode == ImageExporterMode::Timelapse) { + if (m_timelapseMovie) + m_timelapseMovie->stop(); + + m_timelapseGifImage = createTimelapseGifImage(&progress); + if (!m_timelapseGifImage) { + previewPixmap = QPixmap(); + } else { + // We want to convert the QGifImage data into a QMovie for the preview display. + // Both support input/output with a QIODevice, so we use a QBuffer to translate the data. + delete m_timelapseBuffer; + m_timelapseBuffer = new QBuffer(this); + m_timelapseBuffer->open(QBuffer::ReadWrite); + m_timelapseGifImage->save(m_timelapseBuffer); + m_timelapseBuffer->close(); + + delete m_timelapseMovie; + m_timelapseMovie = new QMovie(m_timelapseBuffer, "gif", this); + m_timelapseMovie->setCacheMode(QMovie::CacheAll); + connect(m_timelapseMovie, &QMovie::frameChanged, [this](int) { + m_preview->setPixmap(m_timelapseMovie->currentPixmap()); + }); + m_timelapseMovie->start(); + previewPixmap = m_timelapseMovie->currentPixmap(); + } } - m_scene->addPixmap(m_preview); - ui->graphicsView_Preview->setScene(m_scene); + progress.close(); + + m_preview->setPixmap(previewPixmap); + m_scene->setSceneRect(m_scene->itemsBoundingRect()); scalePreview(); } void MapImageExporter::scalePreview() { - if (m_scene && !m_settings.previewActualSize){ - ui->graphicsView_Preview->fitInView(m_scene->sceneRect(), Qt::KeepAspectRatioByExpanding); - } + if (!m_preview || m_settings.disablePreviewScaling) + return; + ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding); } QPixmap MapImageExporter::getFormattedMapPixmap() { - return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); -} - -QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder, bool ignoreGrid) { - if (!layout) + if (!m_layout) return QPixmap(); - layout->render(true); - QPixmap pixmap = layout->pixmap; + m_layout->render(true); - if (m_settings.showCollision) { - QPainter collisionPainter(&pixmap); - layout->renderCollision(true); - collisionPainter.setOpacity(m_editor->collisionOpacity); - collisionPainter.drawPixmap(0, 0, layout->collision_pixmap); - collisionPainter.end(); + // Create pixmap large enough to contain the map and the marginal elements (the border, grid, etc.) + QMargins margins = getMargins(m_map); + QPixmap pixmap = QPixmap(m_layout->pixmap.width() + margins.left() + margins.right(), + m_layout->pixmap.height() + margins.top() + margins.bottom()); + pixmap.fill(m_settings.fillColor); + + QPainter painter(&pixmap); + painter.translate(margins.left(), margins.top()); + + paintBorder(&painter, m_layout); + painter.drawPixmap(0, 0, m_layout->pixmap); + paintCollision(&painter, m_layout); + if (m_map) { + paintConnections(&painter, m_map); + paintEvents(&painter, m_map); } - - // draw map border - // note: this will break when allowing map to be selected from drop down maybe - int borderHeight = 0, borderWidth = 0; - if (!ignoreBorder && m_settings.showBorder) { - int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; - layout->renderBorder(); - int borderHorzDist = m_editor->getBorderDrawDistance(layout->getBorderWidth()); - int borderVertDist = m_editor->getBorderDrawDistance(layout->getBorderHeight()); - borderWidth = borderDistance * 16; - borderHeight = borderDistance * 16; - QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); - QPainter borderPainter(&newPixmap); - for (int y = borderDistance - borderVertDist; y < layout->getHeight() + borderVertDist * 2; y += layout->getBorderHeight()) { - for (int x = borderDistance - borderHorzDist; x < layout->getWidth() + borderHorzDist * 2; x += layout->getBorderWidth()) { - borderPainter.drawPixmap(x * 16, y * 16, layout->border_pixmap); - } - } - borderPainter.drawImage(borderWidth, borderHeight, pixmap.toImage()); - borderPainter.end(); - pixmap = newPixmap; - } - - // The grid should be painted last, so if this layout pixmap is being painted - // as part of a map (which has more to paint after this) then don't paint the grid yet. - if (!ignoreGrid) - paintGrid(&pixmap, ignoreBorder); + paintGrid(&painter, m_layout); return pixmap; } -QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { - if (!map) - return QPixmap(); +QMargins MapImageExporter::getMargins(const Map *map) { + QMargins margins; + if (m_settings.showBorder) { + // The border may technically extend beyond BORDER_DISTANCE, but when the border is painted + // we will be limiting it to the visible sight range. + margins = QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16; + } else if (map && connectionsEnabled()) { + for (const auto &connection : map->getConnections()) { + const QString dir = connection->direction(); + if (!m_settings.showConnections.contains(dir)) + continue; + auto targetMap = connection->targetMap(); + if (!targetMap) continue; - QPixmap pixmap = getFormattedLayoutPixmap(map->layout(), ignoreBorder, true); - - if (!ignoreBorder && (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections)) { - // if showing connections, draw on outside of image - QPainter connectionPainter(&pixmap); - - int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; - int borderWidth = borderDistance * 16; - int borderHeight = borderDistance * 16; - // TODO: Reading the connections from the editor and not 'map' is incorrect. - for (auto connectionItem : m_editor->connection_items) { - const QString direction = connectionItem->connection->direction(); - if ((m_settings.showUpConnections && direction == "up") - || (m_settings.showDownConnections && direction == "down") - || (m_settings.showLeftConnections && direction == "left") - || (m_settings.showRightConnections && direction == "right")) - connectionPainter.drawImage(connectionItem->x() + borderWidth, connectionItem->y() + borderHeight, - connectionItem->connection->getPixmap().toImage()); + QRect rect = targetMap->getConnectionRect(dir); + if (dir == "up") margins.setTop(rect.height() * 16); + else if (dir == "down") margins.setBottom(rect.height() * 16); + else if (dir == "left") margins.setLeft(rect.width() * 16); + else if (dir == "right") margins.setRight(rect.width() * 16); } - connectionPainter.end(); } - - // draw events - if (m_settings.showObjects || m_settings.showWarps || m_settings.showBGs || m_settings.showTriggers || m_settings.showHealLocations) { - QPainter eventPainter(&pixmap); - int pixelOffset = 0; - if (!ignoreBorder && m_settings.showBorder) { - pixelOffset = m_mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; - } - const QList events = map->getEvents(); - for (const auto &event : events) { - Event::Group group = event->getEventGroup(); - if ((m_settings.showObjects && group == Event::Group::Object) - || (m_settings.showWarps && group == Event::Group::Warp) - || (m_settings.showBGs && group == Event::Group::Bg) - || (m_settings.showTriggers && group == Event::Group::Coord) - || (m_settings.showHealLocations && group == Event::Group::Heal)) { - m_editor->project->loadEventPixmap(event); - eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage()); - } - } - eventPainter.end(); - } - paintGrid(&pixmap, ignoreBorder); - - return pixmap; -} - -void MapImageExporter::paintGrid(QPixmap *pixmap, bool ignoreBorder) { - // draw grid directly onto the pixmap - // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right if (m_settings.showGrid) { - bool hasBorder = !ignoreBorder && m_settings.showBorder; - int addX = 1, addY = 1; - if (hasBorder) addY = 0; - if (hasBorder) addX = 0; + // Account for outer grid line + if (margins.right() == 0) margins.setRight(1); + if (margins.bottom() == 0) margins.setBottom(1); + } + return margins; +} - QPixmap newPixmap= QPixmap(pixmap->width() + addX, pixmap->height() + addY); - QPainter gridPainter(&newPixmap); - gridPainter.drawImage(QPoint(0, 0), pixmap->toImage()); - for (int x = 0; x < newPixmap.width(); x += 16) { - gridPainter.drawLine(x, 0, x, newPixmap.height()); - } - for (int y = 0; y < newPixmap.height(); y += 16) { - gridPainter.drawLine(0, y, newPixmap.width(), y); - } - gridPainter.end(); - *pixmap = newPixmap; +void MapImageExporter::paintCollision(QPainter *painter, Layout *layout) { + if (!m_settings.showCollision) + return; + + auto savedOpacity = painter->opacity(); + painter->setOpacity(static_cast(porymapConfig.collisionOpacity) / 100); + painter->drawPixmap(0, 0, layout->renderCollision(true)); + painter->setOpacity(savedOpacity); +} + +void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { + if (!m_settings.showBorder) + return; + + layout->renderBorder(true); + + // Clip parts of the border that would be beyond player visibility. + QRect visibleArea(0, 0, layout->getWidth() * 16, layout->getHeight() * 16); + visibleArea += (QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16); + painter->save(); + painter->setClipRect(visibleArea); + + int borderHorzDist = layout->getBorderDrawWidth(); + int borderVertDist = layout->getBorderDrawHeight(); + for (int y = -borderVertDist; y < layout->getHeight() + borderVertDist; y += layout->getBorderHeight()) + for (int x = -borderHorzDist; x < layout->getWidth() + borderHorzDist; x += layout->getBorderWidth()) { + // Skip border painting if it would be fully covered by the rest of the map + if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight()))) + continue; + painter->drawPixmap(x * 16, y * 16, layout->border_pixmap); + } + + painter->restore(); +} + +void MapImageExporter::paintConnections(QPainter *painter, const Map *map) { + if (!connectionsEnabled()) + return; + + for (const auto &connection : map->getConnections()) { + if (!m_settings.showConnections.contains(connection->direction())) + continue; + painter->drawImage(connection->relativePos(true) * 16, connection->render().toImage()); } } -void MapImageExporter::updateShowBorderState() { - // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) - bool on = (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections); - const QSignalBlocker blocker(ui->checkBox_Border); - ui->checkBox_Border->setChecked(on); - ui->checkBox_Border->setDisabled(on); - m_settings.showBorder = on; +void MapImageExporter::paintEvents(QPainter *painter, const Map *map) { + if (!eventsEnabled()) + return; + + auto savedOpacity = painter->opacity(); + for (const auto &group : Event::groups()) { + if (!m_settings.showEvents.contains(group)) + continue; + for (const auto &event : map->getEvents(group)) { + m_project->loadEventPixmap(event); + if (m_mode != ImageExporterMode::Timelapse) { + // GIF format doesn't support partial transparency, so we can't do this in Timelapse mode. + painter->setOpacity(event->getUsesDefaultPixmap() ? 0.7 : 1.0); + } + painter->drawImage(QPoint(event->getPixelX(), event->getPixelY()), event->getPixmap().toImage()); + } + } + painter->setOpacity(savedOpacity); } -void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { +void MapImageExporter::paintGrid(QPainter *painter, const Layout *layout) { + if (!m_settings.showGrid) + return; + + int w = layout->getWidth() * 16; + int h = layout->getHeight() * 16; + for (int x = 0; x <= w; x += 16) { + painter->drawLine(x, 0, x, h); + } + for (int y = 0; y <= h; y += 16) { + painter->drawLine(0, y, w, y); + } +} + +bool MapImageExporter::eventsEnabled() { + return !m_settings.showEvents.isEmpty(); +} + +void MapImageExporter::setEventGroupEnabled(Event::Group group, bool enable) { + if (enable) { + m_settings.showEvents.insert(group); + } else { + m_settings.showEvents.remove(group); + } +} + +bool MapImageExporter::connectionsEnabled() { + return !m_settings.showConnections.isEmpty() && m_mode != ImageExporterMode::Stitch; +} + +void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool enable) { + if (enable) { + m_settings.showConnections.insert(dir); + } else { + m_settings.showConnections.remove(dir); + } +} + +void MapImageExporter::on_checkBox_Collision_stateChanged(int state) { m_settings.showCollision = (state == Qt::Checked); updatePreview(); } @@ -576,27 +735,27 @@ void MapImageExporter::on_checkBox_Border_stateChanged(int state) { } void MapImageExporter::on_checkBox_Objects_stateChanged(int state) { - m_settings.showObjects = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Object, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Warps_stateChanged(int state) { - m_settings.showWarps = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Warp, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_BGs_stateChanged(int state) { - m_settings.showBGs = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Bg, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Triggers_stateChanged(int state) { - m_settings.showTriggers = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Coord, state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_HealLocations_stateChanged(int state) { - m_settings.showHealLocations = (state == Qt::Checked); + setEventGroupEnabled(Event::Group::Heal, state == Qt::Checked); updatePreview(); } @@ -607,52 +766,48 @@ void MapImageExporter::on_checkBox_AllEvents_stateChanged(int state) { const QSignalBlocker b_Objects(ui->checkBox_Objects); ui->checkBox_Objects->setChecked(on); ui->checkBox_Objects->setDisabled(on); - m_settings.showObjects = on; + setEventGroupEnabled(Event::Group::Object, on); const QSignalBlocker b_Warps(ui->checkBox_Warps); ui->checkBox_Warps->setChecked(on); ui->checkBox_Warps->setDisabled(on); - m_settings.showWarps = on; + setEventGroupEnabled(Event::Group::Warp, on); const QSignalBlocker b_BGs(ui->checkBox_BGs); ui->checkBox_BGs->setChecked(on); ui->checkBox_BGs->setDisabled(on); - m_settings.showBGs = on; + setEventGroupEnabled(Event::Group::Bg, on); const QSignalBlocker b_Triggers(ui->checkBox_Triggers); ui->checkBox_Triggers->setChecked(on); ui->checkBox_Triggers->setDisabled(on); - m_settings.showTriggers = on; + setEventGroupEnabled(Event::Group::Coord, on); const QSignalBlocker b_HealLocations(ui->checkBox_HealLocations); ui->checkBox_HealLocations->setChecked(on); ui->checkBox_HealLocations->setDisabled(on); - m_settings.showHealLocations = on; + setEventGroupEnabled(Event::Group::Heal, on); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { - m_settings.showUpConnections = (state == Qt::Checked); - updateShowBorderState(); + setConnectionDirectionEnabled("up", state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { - m_settings.showDownConnections = (state == Qt::Checked); - updateShowBorderState(); + setConnectionDirectionEnabled("down", state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { - m_settings.showLeftConnections = (state == Qt::Checked); - updateShowBorderState(); + setConnectionDirectionEnabled("left", state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { - m_settings.showRightConnections = (state == Qt::Checked); - updateShowBorderState(); + setConnectionDirectionEnabled("right", state == Qt::Checked); updatePreview(); } @@ -663,51 +818,86 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) { const QSignalBlocker b_Up(ui->checkBox_ConnectionUp); ui->checkBox_ConnectionUp->setChecked(on); ui->checkBox_ConnectionUp->setDisabled(on); - m_settings.showUpConnections = on; + setConnectionDirectionEnabled("up", on); const QSignalBlocker b_Down(ui->checkBox_ConnectionDown); ui->checkBox_ConnectionDown->setChecked(on); ui->checkBox_ConnectionDown->setDisabled(on); - m_settings.showDownConnections = on; + setConnectionDirectionEnabled("down", on); const QSignalBlocker b_Left(ui->checkBox_ConnectionLeft); ui->checkBox_ConnectionLeft->setChecked(on); ui->checkBox_ConnectionLeft->setDisabled(on); - m_settings.showLeftConnections = on; + setConnectionDirectionEnabled("left", on); const QSignalBlocker b_Right(ui->checkBox_ConnectionRight); ui->checkBox_ConnectionRight->setChecked(on); ui->checkBox_ConnectionRight->setDisabled(on); - m_settings.showRightConnections = on; + setConnectionDirectionEnabled("right", on); - updateShowBorderState(); updatePreview(); } -void MapImageExporter::on_checkBox_ActualSize_stateChanged(int state) { - m_settings.previewActualSize = (state == Qt::Checked); - if (m_settings.previewActualSize) { +void MapImageExporter::on_checkBox_DisablePreviewScaling_stateChanged(int state) { + m_settings.disablePreviewScaling = (state == Qt::Checked); + if (m_settings.disablePreviewScaling) { ui->graphicsView_Preview->resetTransform(); } else { scalePreview(); } } -void MapImageExporter::on_pushButton_Reset_pressed() { - m_settings = {}; - for (auto widget : this->findChildren()) { - const QSignalBlocker b(widget); // Prevent calls to updatePreview - widget->setChecked(false); +void MapImageExporter::on_checkBox_DisablePreviewUpdates_stateChanged(int state) { + m_settings.disablePreviewUpdates = (state == Qt::Checked); + if (m_settings.disablePreviewUpdates) { + if (m_timelapseMovie) { + m_timelapseMovie->stop(); + } + } else { + updatePreview(); } - ui->spinBox_TimelapseDelay->setValue(m_settings.timelapseDelayMs); - ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount); +} + +void MapImageExporter::on_pushButton_Reset_pressed() { + resetSettings(); updatePreview(); } -void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) { - m_settings.timelapseDelayMs = delayMs; +void MapImageExporter::resetSettings() { + m_settings = {}; + + for (auto widget : this->findChildren()) { + const QSignalBlocker b(widget); // Prevent calls to updatePreview + widget->setChecked(false); // This assumes the default state of all checkboxes settings is false. + } + + const QSignalBlocker b_TimelapseDelay(ui->spinBox_TimelapseDelay); + ui->spinBox_TimelapseDelay->setValue(m_settings.timelapseDelayMs); + + const QSignalBlocker b_FrameSkip(ui->spinBox_FrameSkip); + ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount); + + if (m_mode == ImageExporterMode::Timelapse) { + // Timelapse gif has artifacts with transparency, make sure it's disabled. + m_settings.fillColor.setAlpha(255); + } } -void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) { - m_settings.timelapseSkipAmount = skip; +// These spin boxes can be changed rapidly, so we wait for editing to finish before updating the preview. +void MapImageExporter::on_spinBox_TimelapseDelay_editingFinished() { + int delayMs = ui->spinBox_TimelapseDelay->value(); + if (delayMs == m_settings.timelapseDelayMs) + return; + + m_settings.timelapseDelayMs = delayMs; + updatePreview(); +} + +void MapImageExporter::on_spinBox_FrameSkip_editingFinished() { + int skipAmount = ui->spinBox_FrameSkip->value(); + if (skipAmount == m_settings.timelapseSkipAmount) + return; + + m_settings.timelapseSkipAmount = skipAmount; + updatePreview(); } diff --git a/src/vendor/QtGifImage/gifimage/qgifimage.cpp b/src/vendor/QtGifImage/gifimage/qgifimage.cpp index b607c7e0..d15d67a3 100644 --- a/src/vendor/QtGifImage/gifimage/qgifimage.cpp +++ b/src/vendor/QtGifImage/gifimage/qgifimage.cpp @@ -356,6 +356,15 @@ QVector QGifImage::globalColorTable() const return d->globalColorTable; } +/*! + Return canvas size. + */ +QSize QGifImage::getCanvasSize() const +{ + Q_D(const QGifImage); + return d->getCanvasSize(); +} + /*! Return background color of the gif canvas. It only makes sense when global color table is not empty. diff --git a/src/vendor/QtGifImage/gifimage/qgifimage.h b/src/vendor/QtGifImage/gifimage/qgifimage.h index f81a28ae..b1a21271 100644 --- a/src/vendor/QtGifImage/gifimage/qgifimage.h +++ b/src/vendor/QtGifImage/gifimage/qgifimage.h @@ -52,6 +52,8 @@ public: int loopCount() const; void setLoopCount(int loop); + QSize getCanvasSize() const; + int frameCount() const; QImage frame(int index) const;