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
+
+ NoScrollSpinBox
+ QSpinBox
+
+
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;