diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui
index 805ca6f2..f2f4c09a 100644
--- a/forms/mapimageexporter.ui
+++ b/forms/mapimageexporter.ui
@@ -25,6 +25,9 @@
0
+
+ Qt::FocusPolicy::ClickFocus
+
-
@@ -166,7 +169,7 @@
-
-
+
Collision
@@ -191,7 +194,10 @@
-
-
+
+
+ Qt::FocusPolicy::StrongFocus
+
@@ -217,7 +223,10 @@
-
-
+
+
+ Qt::FocusPolicy::StrongFocus
+
@@ -253,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
@@ -266,6 +288,9 @@
Reset
+
+ false
+
-
@@ -286,6 +311,9 @@
Cancel
+
+ false
+
-
@@ -293,6 +321,9 @@
Save
+
+ false
+
@@ -407,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/ui/mapimageexporter.h b/include/ui/mapimageexporter.h
index ffd893e5..ffc4d272 100644
--- a/include/ui/mapimageexporter.h
+++ b/include/ui/mapimageexporter.h
@@ -21,10 +21,12 @@ struct ImageExporterSettings {
bool showGrid = false;
bool showBorder = false;
bool showCollision = false;
- bool previewActualSize = false;
+ bool disablePreviewScaling = false;
+ bool disablePreviewUpdates = false;
int timelapseSkipAmount = 1;
int timelapseDelayMs = 200;
- QColor fillColor = Qt::transparent; // Not exposed as a setting in the UI atm.
+ // 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
@@ -63,9 +65,10 @@ private:
void setModeSpecificUi();
void setSelectionText(const QString &text);
void updateMapSelection();
+ void resetSettings();
QString getTitle(ImageExporterMode mode);
QString getDescription(ImageExporterMode mode);
- void updatePreview();
+ void updatePreview(bool forceUpdate = false);
void scalePreview();
bool eventsEnabled();
void setEventGroupEnabled(Event::Group group, bool enable);
@@ -81,7 +84,7 @@ private:
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 &minSize, const QColor &fillColor);
+ QPixmap getExpandedPixmap(const QPixmap &pixmap, const QSize &targetSize, const QColor &fillColor);
bool currentHistoryAppliesToFrame(QUndoStack *historyStack);
protected:
@@ -102,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 0cc378fc..801bdd43 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/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp
index c98fccce..23b1b8a0 100644
--- a/src/ui/mapimageexporter.cpp
+++ b/src/ui/mapimageexporter.cpp
@@ -96,22 +96,26 @@ void MapImageExporter::setModeSpecificUi() {
}
if (m_mode == ImageExporterMode::Timelapse) {
- // At the moment edit history for events (and the DraggablePixmapItem class)
- // depend on the editor and assume their map is the current map.
- // Until this is resolved, the selected map and the editor's map must remain the same.
+ // 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);
-
- // Timelapse gif has artifacts with transparency, make sure it's disabled.
- m_settings.fillColor.setAlpha(255);
}
+
+ // 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) {
@@ -166,12 +170,12 @@ void MapImageExporter::updateMapSelection() {
}
void MapImageExporter::saveImage() {
- // If the preview is empty it's because progress was canceled.
- // Try again to create it, and if it's canceled again we'll stop the export.
- if (m_preview->pixmap().isNull()) {
- updatePreview();
+ // 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;
+ return; // Canceled
}
if (m_mode == ImageExporterMode::Timelapse && !m_timelapseGifImage) {
// Shouldn't happen. We have a preview for the timelapse, but no timelapse image.
@@ -237,8 +241,15 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) {
case CommandId::ID_MapConnectionChangeDirection:
case CommandId::ID_MapConnectionChangeMap:
case CommandId::ID_MapConnectionAdd:
- case CommandId::ID_MapConnectionRemove:
- return connectionsEnabled();
+ case CommandId::ID_MapConnectionRemove: {
+ if (!connectionsEnabled())
+ return false;
+ if (command->id() & IDMask_ConnectionDirection_Up) return m_settings.showConnections.contains("up");
+ if (command->id() & IDMask_ConnectionDirection_Down) return m_settings.showConnections.contains("down");
+ if (command->id() & IDMask_ConnectionDirection_Left) return m_settings.showConnections.contains("left");
+ if (command->id() & IDMask_ConnectionDirection_Right) return m_settings.showConnections.contains("right");
+ return false;
+ }
case CommandId::ID_EventMove:
case CommandId::ID_EventShift:
case CommandId::ID_EventCreate:
@@ -257,15 +268,18 @@ bool MapImageExporter::currentHistoryAppliesToFrame(QUndoStack *historyStack) {
}
}
-QPixmap MapImageExporter::getExpandedPixmap(const QPixmap &pixmap, const QSize &minSize, const QColor &fillColor) {
- if (pixmap.width() >= minSize.width() && pixmap.height() >= minSize.height())
+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(minSize);
+ QPixmap resizedPixmap = QPixmap(targetSize);
QPainter painter(&resizedPixmap);
resizedPixmap.fill(fillColor);
- painter.drawPixmap(0, 0, pixmap.width(), pixmap.height(), pixmap);
- painter.end();
+
+ // 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;
}
@@ -278,13 +292,11 @@ struct TimelapseStep {
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;
- if (m_layout) {
- steps.append({
- .historyStack = &m_layout->editHistory,
- .initialStackIndex = m_layout->editHistory.index(),
- .name = "layout",
- });
- }
+ steps.append({
+ .historyStack = &m_layout->editHistory,
+ .initialStackIndex = m_layout->editHistory.index(),
+ .name = "layout",
+ });
if (m_map) {
steps.append({
.historyStack = m_map->editHistory(),
@@ -301,8 +313,8 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress)
progress->setMaximum(step.initialStackIndex);
progress->setValue(progress->minimum());
do {
- if (currentHistoryAppliesToFrame(step.historyStack)) {
- // This command is relevant, record the size of the map at this point.
+ 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()));
@@ -489,7 +501,10 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) {
return stitchedPixmap;
}
-void MapImageExporter::updatePreview() {
+void MapImageExporter::updatePreview(bool forceUpdate) {
+ if (m_settings.disablePreviewUpdates && !forceUpdate)
+ return;
+
QProgressDialog progress("", "Cancel", 0, 1, this);
progress.setAutoClose(true);
progress.setWindowModality(Qt::WindowModal);
@@ -535,7 +550,7 @@ void MapImageExporter::updatePreview() {
}
void MapImageExporter::scalePreview() {
- if (!m_preview || m_settings.previewActualSize)
+ if (!m_preview || m_settings.disablePreviewScaling)
return;
ui->graphicsView_Preview->fitInView(m_preview, Qt::KeepAspectRatioByExpanding);
}
@@ -689,7 +704,7 @@ void MapImageExporter::setEventGroupEnabled(Event::Group group, bool enable) {
}
bool MapImageExporter::connectionsEnabled() {
- return !m_settings.showConnections.isEmpty();
+ return !m_settings.showConnections.isEmpty() && m_mode != ImageExporterMode::Stitch;
}
void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool enable) {
@@ -700,7 +715,7 @@ void MapImageExporter::setConnectionDirectionEnabled(const QString &dir, bool en
}
}
-void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) {
+void MapImageExporter::on_checkBox_Collision_stateChanged(int state) {
m_settings.showCollision = (state == Qt::Checked);
updatePreview();
}
@@ -819,21 +834,37 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) {
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_checkBox_DisablePreviewUpdates_stateChanged(int state) {
+ m_settings.disablePreviewUpdates = (state == Qt::Checked);
+ if (m_settings.disablePreviewUpdates) {
+ if (m_timelapseMovie) {
+ m_timelapseMovie->stop();
+ }
+ } else {
+ updatePreview();
+ }
+}
+
void MapImageExporter::on_pushButton_Reset_pressed() {
- m_settings = {};
+ resetSettings();
+ updatePreview();
+}
+
+void MapImageExporter::resetSettings() {
+ m_settings = {};
for (auto widget : this->findChildren()) {
const QSignalBlocker b(widget); // Prevent calls to updatePreview
- widget->setChecked(false);
+ widget->setChecked(false); // This assumes the default state of all checkboxes settings is false.
}
const QSignalBlocker b_TimelapseDelay(ui->spinBox_TimelapseDelay);
@@ -842,15 +873,27 @@ void MapImageExporter::on_pushButton_Reset_pressed() {
const QSignalBlocker b_FrameSkip(ui->spinBox_FrameSkip);
ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount);
- updatePreview();
+ 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_TimelapseDelay_valueChanged(int delayMs) {
+// 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_valueChanged(int skip) {
- m_settings.timelapseSkipAmount = skip;
+void MapImageExporter::on_spinBox_FrameSkip_editingFinished() {
+ int skipAmount = ui->spinBox_FrameSkip->value();
+ if (skipAmount == m_settings.timelapseSkipAmount)
+ return;
+
+ m_settings.timelapseSkipAmount = skipAmount;
updatePreview();
}