From 38e27722137f85a38bc0907928bb48529f36f8ec Mon Sep 17 00:00:00 2001 From: GriffinR Date: Wed, 2 Jul 2025 14:23:57 -0400 Subject: [PATCH] Add new metatile image export window --- forms/metatileimageexporter.ui | 526 +++++++++++++++++++++ forms/tileseteditor.ui | 185 ++++---- include/config.h | 9 +- include/core/maplayout.h | 26 +- include/core/metatile.h | 8 + include/core/tile.h | 5 + include/core/tileset.h | 1 + include/core/utility.h | 2 +- include/ui/imageproviders.h | 6 +- include/ui/metatileimageexporter.h | 101 ++++ include/ui/tileseteditor.h | 20 +- include/ui/tileseteditormetatileselector.h | 4 - include/ui/uintspinbox.h | 3 + porymap.pro | 3 + src/config.cpp | 16 +- src/core/maplayout.cpp | 23 +- src/core/metatile.cpp | 9 +- src/core/tileset.cpp | 10 +- src/core/utility.cpp | 2 +- src/editor.cpp | 4 +- src/main.cpp | 2 + src/project.cpp | 8 +- src/scriptapi/apiutility.cpp | 8 +- src/ui/imageproviders.cpp | 85 +++- src/ui/metatileimageexporter.cpp | 321 +++++++++++++ src/ui/metatilelayersitem.cpp | 4 +- src/ui/metatileselector.cpp | 23 +- src/ui/movablerect.cpp | 4 +- src/ui/projectsettingseditor.cpp | 7 +- src/ui/resizelayoutpopup.cpp | 2 +- src/ui/selectablepixmapitem.cpp | 4 +- src/ui/tileseteditor.cpp | 90 ++-- src/ui/tileseteditormetatileselector.cpp | 54 +-- src/ui/uintspinbox.cpp | 25 +- src/ui/wildmonchart.cpp | 2 +- 35 files changed, 1276 insertions(+), 326 deletions(-) create mode 100644 forms/metatileimageexporter.ui create mode 100644 include/ui/metatileimageexporter.h create mode 100644 src/ui/metatileimageexporter.cpp diff --git a/forms/metatileimageexporter.ui b/forms/metatileimageexporter.ui new file mode 100644 index 00000000..7c6bfe49 --- /dev/null +++ b/forms/metatileimageexporter.ui @@ -0,0 +1,526 @@ + + + MetatileImageExporter + + + + 0 + 0 + 649 + 601 + + + + Export Metatiles Image + + + true + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::ClickFocus + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + true + + + + + 0 + 0 + 304 + 532 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Tilesets + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full secondary tileset.</p></body></html> + + + Secondary Tileset + + + + + + + false + + + false + + + + + + + false + + + false + + + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full primary tileset.</p></body></html> + + + Primary Tileset + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 1 + 20 + + + + + + + + + + + false + + + Metatile Range + + + + 6 + + + 6 + + + + + Start + + + + + + + <html><head/><body><p>The metatile ID to start the rendered image at.</p></body></html> + + + + + + + End + + + + + + + <html><head/><body><p>The metatile ID to end the rendered image at.</p></body></html> + + + + + + + + + + <html><head/><body><p>Each metatile consists of 3 layers of tiles. These layers can be toggled here by clicking the checkbox, or rearranged by clicking and dragging them up or down in the list.</p></body></html> + + + Layers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow + + + true + + + QAbstractItemView::DragDropMode::InternalMove + + + Qt::DropAction::MoveAction + + + QListView::ResizeMode::Adjust + + + Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Transparency + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered with alpha of 0.</p></body></html> + + + Normal + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered as black. This is the default in-game behavior.</p></body></html> + + + Black + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered using the first color in tileset palette 0. This is the default behavior of the GBA.</p></body></html> + + + First palette color + + + + + + + + + + Miscellaneous + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, display the placeholder tiles that are rendered for the unused layer in-game. For a given metatile only 2 of the 3 tile layers are used, and the 3rd layer is filled with these placeholder tiles. The unused layer and placeholder tile change depending on the metatile's layer type.</p></body></html> + + + Render placeholder metatiles + + + + + + + Width (metatiles) + + + + + + + <html><head/><body><p>Width of the output image in metatiles.</p></body></html> + + + + + + + Width (pixels) + + + + + + + <html><head/><body><p>Width of the output image in pixels. Automatically rounded up to a multiple of a metatile's pixel width.</p></body></html> + + + 16 + + + 128 + + + 16 + + + 128 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + + + + Reset + + + false + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + false + + + + + + + Save + + + false + + + + + + + + + + + + Preview + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + 0 + 0 + 285 + 551 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + false + + + false + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored + + + QGraphicsView::DragMode::NoDrag + + + + + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + UIntSpinBox + QAbstractSpinBox +
uintspinbox.h
+
+ + UIntHexSpinBox + UIntSpinBox +
uintspinbox.h
+
+ + ReorderableListWidget + QListWidget +
metatileimageexporter.h
+
+
+ + +
diff --git a/forms/tileseteditor.ui b/forms/tileseteditor.ui index 6c696bf3..36aa92f7 100644 --- a/forms/tileseteditor.ui +++ b/forms/tileseteditor.ui @@ -11,7 +11,7 @@ - Qt::FocusPolicy::ClickFocus + Qt::ClickFocus Tileset Editor @@ -21,7 +21,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal false @@ -34,10 +34,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain @@ -58,14 +58,14 @@ true - Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop + Qt::AlignHCenter|Qt::AlignTop 0 0 - 239 + 253 659 @@ -88,17 +88,17 @@ - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::Orientation::Vertical + Qt::Vertical @@ -121,7 +121,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -129,10 +129,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Raised + QFrame::Raised @@ -162,14 +162,14 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Raised + QFrame::Raised - QLayout::SizeConstraint::SetMinimumSize + QLayout::SetMinimumSize 0 @@ -255,7 +255,7 @@ - QComboBox::InsertPolicy::NoInsert + QComboBox::NoInsert @@ -274,10 +274,10 @@ - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff @@ -290,17 +290,17 @@ - QComboBox::InsertPolicy::NoInsert + QComboBox::NoInsert - Qt::Orientation::Horizontal + Qt::Horizontal - QSizePolicy::Policy::Maximum + QSizePolicy::Maximum @@ -319,7 +319,7 @@ - QComboBox::InsertPolicy::NoInsert + QComboBox::NoInsert @@ -361,7 +361,7 @@ - Qt::Orientation::Vertical + Qt::Vertical @@ -412,7 +412,7 @@ - Qt::LayoutDirection::LeftToRight + Qt::LeftToRight @@ -436,10 +436,10 @@ - Qt::Orientation::Vertical + Qt::Vertical - QSizePolicy::Policy::Fixed + QSizePolicy::Fixed @@ -464,10 +464,10 @@ - QFrame::Shape::NoFrame + QFrame::NoFrame - QFrame::Shadow::Plain + QFrame::Plain @@ -510,13 +510,13 @@ - QFrame::Shape::StyledPanel + QFrame::StyledPanel - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff @@ -526,7 +526,7 @@ - Qt::Orientation::Vertical + Qt::Vertical @@ -548,14 +548,14 @@ true - Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop + Qt::AlignHCenter|Qt::AlignTop 0 0 - 499 + 446 241 @@ -575,17 +575,17 @@ - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::ScrollBarPolicy::ScrollBarAlwaysOff + Qt::ScrollBarAlwaysOff - Qt::Orientation::Vertical + Qt::Vertical @@ -602,7 +602,7 @@ - Qt::Orientation::Horizontal + Qt::Horizontal @@ -625,18 +625,34 @@ File + + + Export Tiles Image + + + + + + + Import Metatiles from Advance Map 1.92 + + + + + + + Import Tiles Image + + + + - - + + - - - - - - - + + @@ -685,16 +701,6 @@ Ctrl+S - - - Import Primary Tiles Image... - - - - - Import Secondary Tiles Image... - - Change Number of Metatiles... @@ -745,36 +751,6 @@ Ctrl+Y - - - Export Primary Tiles Image... - - - - - Export Secondary Tiles Image... - - - - - Import Primary Metatiles from Advance Map 1.92... - - - - - Import Secondary Metatiles from Advance Map 1.92... - - - - - Export Primary Metatiles Image... - - - - - Export Secondary Metatiles Image... - - Cut @@ -834,6 +810,41 @@ Show Raw Metatile Attributes + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Export Metatiles Image... + + diff --git a/include/config.h b/include/config.h index 234fa76d..51c11a74 100644 --- a/include/config.h +++ b/include/config.h @@ -54,7 +54,8 @@ protected: static bool getConfigBool(const QString &key, const QString &value); static int getConfigInteger(const QString &key, const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); static uint32_t getConfigUint32(const QString &key, const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); - static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = Qt::black); + static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = QColor(Qt::black)); + static QString toConfigColor(const QColor &color); QString m_root; QString m_filename; @@ -82,7 +83,7 @@ public: this->collisionOpacity = 50; this->collisionZoom = 30; this->metatilesZoom = 30; - this->tilesetEditorMetatilesZoom = 45; + this->tilesetEditorMetatilesZoom = 30; this->tilesetEditorTilesZoom = 30; this->showPlayerView = false; this->showCursorTile = true; @@ -351,7 +352,7 @@ public: this->prefabImportPrompted = false; this->tilesetsHaveCallback = true; this->tilesetsHaveIsCompressed = true; - this->setTransparentPixelsBlack = true; + this->transparencyColor = QColor(Qt::black); this->preserveMatchingOnlyData = false; this->filePaths.clear(); this->eventIconPaths.clear(); @@ -426,7 +427,7 @@ public: bool prefabImportPrompted; bool tilesetsHaveCallback; bool tilesetsHaveIsCompressed; - bool setTransparentPixelsBlack; + QColor transparencyColor; bool preserveMatchingOnlyData; int metatileAttributesSize; uint32_t metatileBehaviorMask; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 280f3006..8091469a 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -65,14 +65,24 @@ public: } lastCommitBlocks; // to track map changes void setMetatileLayerOrder(const QList &layerOrder) { m_metatileLayerOrder = layerOrder; } - QList metatileLayerOrder() const; - static void setDefaultMetatileLayerOrder(const QList &layerOrder) { s_defaultMetatileLayerOrder = layerOrder; } - static QList defaultMetatileLayerOrder(); + const QList &metatileLayerOrder() const { + return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::globalMetatileLayerOrder(); + } + static void setGlobalMetatileLayerOrder(const QList &layerOrder) { s_globalMetatileLayerOrder = layerOrder; } + static const QList &globalMetatileLayerOrder() { + static const QList defaultLayerOrder = {0, 1, 2}; + return !s_globalMetatileLayerOrder.isEmpty() ? s_globalMetatileLayerOrder : defaultLayerOrder; + } void setMetatileLayerOpacity(const QList &layerOpacity) { m_metatileLayerOpacity = layerOpacity; } - QList metatileLayerOpacity() const; - static void setDefaultMetatileLayerOpacity(const QList &layerOpacity) { s_defaultMetatileLayerOpacity = layerOpacity; } - static QList defaultMetatileLayerOpacity(); + const QList &metatileLayerOpacity() const { + return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::globalMetatileLayerOpacity(); + } + static void setGlobalMetatileLayerOpacity(const QList &layerOpacity) { s_globalMetatileLayerOpacity = layerOpacity; } + static const QList &globalMetatileLayerOpacity() { + static const QList defaultLayerOpacity = {1.0, 1.0, 1.0}; + return !s_globalMetatileLayerOpacity.isEmpty() ? s_globalMetatileLayerOpacity : defaultLayerOpacity; + } LayoutPixmapItem *layoutItem = nullptr; CollisionPixmapItem *collisionItem = nullptr; @@ -166,8 +176,8 @@ private: QList m_metatileLayerOrder; QList m_metatileLayerOpacity; - static QList s_defaultMetatileLayerOrder; - static QList s_defaultMetatileLayerOpacity; + static QList s_globalMetatileLayerOrder; + static QList s_globalMetatileLayerOpacity; signals: void dimensionsChanged(const QSize &size); diff --git a/include/core/metatile.h b/include/core/metatile.h index a1c805d1..a57717fe 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -62,6 +62,14 @@ public: static void setLayout(Project*); static QString getMetatileIdString(uint16_t metatileId); static QString getMetatileIdStrings(const QList metatileIds); + static QString getLayerName(int layerNum); + + static int tileWidth() { return 2; } + static int tileHeight() { return 2; } + static int tilesPerLayer() { return Metatile::tileWidth() * Metatile::tileHeight(); } + static int pixelWidth() { return Metatile::tileWidth() * Tile::pixelWidth(); } + static int pixelHeight() { return Metatile::tileHeight() * Tile::pixelHeight(); } + static QSize pixelSize() { return QSize(pixelWidth(), pixelHeight()); } inline bool operator==(const Metatile &other) { return this->tiles == other.tiles && this->attributes == other.attributes; diff --git a/include/core/tile.h b/include/core/tile.h index 1ae7e23d..d7ae5061 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -3,6 +3,7 @@ #define TILE_H #include +#include class Tile { @@ -24,6 +25,10 @@ public: static int getIndexInTileset(int); static const uint16_t maxValue; + + static int pixelWidth() { return 8; } + static int pixelHeight() { return 8; } + static QSize pixelSize() { return QSize(Tile::pixelWidth(), Tile::pixelHeight()); } }; inline bool operator==(const Tile &a, const Tile &b) { diff --git a/include/core/tileset.h b/include/core/tileset.h index abb31ab1..80fef000 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -38,6 +38,7 @@ public: QList> palettes; QList> palettePreviews; + static QString stripPrefix(const QString &fullName); static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); diff --git a/include/core/utility.h b/include/core/utility.h index f7f5da61..41c1dcde 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -7,7 +7,7 @@ namespace Util { void numericalModeSort(QStringList &list); - int roundUp(int numToRound, int multiple); + int roundUpToMultiple(int numToRound, int multiple); QString toDefineCase(QString input); QString toHexString(uint32_t value, int minLength = 0); QString toHtmlParagraph(const QString &text); diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index d3136580..0cfe8511 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -12,8 +12,10 @@ QImage getCollisionMetatileImage(Block); QImage getCollisionMetatileImage(int, int); QImage getMetatileImage(uint16_t, Layout*, bool useTruePalettes = false); QImage getMetatileImage(Metatile*, Layout*, bool useTruePalettes = false); -QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); -QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); +QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList& = {}, bool useTruePalettes = false); +QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList& = {}, bool useTruePalettes = false); +QImage getMetatileSheetImage(Layout *, int, bool useTruePalettes = false); +QImage getMetatileSheetImage(Tileset *, Tileset *, uint16_t, int, int, const QList &, const QList & = {}, const QSize &size = Metatile::pixelSize(), bool useTruePalettes = false); QImage getTileImage(uint16_t, Tileset*, Tileset*); QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset); diff --git a/include/ui/metatileimageexporter.h b/include/ui/metatileimageexporter.h new file mode 100644 index 00000000..a6b8f6cd --- /dev/null +++ b/include/ui/metatileimageexporter.h @@ -0,0 +1,101 @@ +#ifndef METATILEIMAGEEXPORTER_H +#define METATILEIMAGEEXPORTER_H + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +class Tileset; + +namespace Ui { +class MetatileImageExporter; +} + +class ReorderableListWidget : public QListWidget +{ + Q_OBJECT +public: + explicit ReorderableListWidget(QWidget *parent = nullptr) : QListWidget(parent) { + setDragEnabled(true); + setDragDropMode(QAbstractItemView::InternalMove); + setDefaultDropAction(Qt::MoveAction); + }; + +signals: + void reordered(); + +protected: + virtual void dropEvent(QDropEvent *event) override { + QListWidget::dropEvent(event); + if (event->isAccepted()) { + emit reordered(); + } + } +}; + +class MetatileImageExporter : public QDialog +{ + Q_OBJECT + +public: + struct Settings { + OrderedMap layerOrder = { + {2, true}, + {1, true}, + {0, true}, + }; + uint16_t metatileStart = 0; + uint16_t metatileEnd = 0xFFFF; + uint16_t numMetatilesWide = 8; + bool usePrimaryTileset = true; + bool useSecondaryTileset = false; + bool renderPlaceholders = false; + int transparencyMode = 0; + }; + + explicit MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings = nullptr); + ~MetatileImageExporter(); + +protected: + virtual void showEvent(QShowEvent *) override; + virtual void closeEvent(QCloseEvent *) override; + +private: + Ui::MetatileImageExporter *ui; + + Tileset *m_primaryTileset; + Tileset *m_secondaryTileset; + Settings *m_savedSettings; + + QGraphicsScene *m_scene = nullptr; + QGraphicsPixmapItem *m_preview = nullptr; + bool m_previewUpdateQueued = false; + QList m_layerOrder; + ProjectConfig m_savedConfig; + QList m_transparencyButtons; + + void applySettings(const Settings &settings); + void updatePreview(); + void tryUpdatePreview(); + void queuePreviewUpdate(); + void updateTilesetUI(); + void syncPixelWidth(); + void syncMetatileWidth(); + void validateMetatileStart(); + void validateMetatileEnd(); + uint16_t getExpectedMetatileStart(); + uint16_t getExpectedMetatileEnd(); + void updateMetatileRange(); + void copyRenderSettings(); + void restoreRenderSettings(); + void saveImage(); + void reset(); +}; + +#endif // METATILEIMAGEEXPORTER_H diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index b6a60a61..b89709df 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -8,6 +8,7 @@ #include "tileseteditormetatileselector.h" #include "tileseteditortileselector.h" #include "metatilelayersitem.h" +#include "metatileimageexporter.h" class NoScrollComboBox; class Layout; @@ -71,10 +72,6 @@ private slots: void on_spinBox_paletteSelector_valueChanged(int arg1); - void on_actionImport_Primary_Tiles_triggered(); - - void on_actionImport_Secondary_Tiles_triggered(); - void on_actionChange_Metatiles_Count_triggered(); void on_actionChange_Palettes_triggered(); @@ -91,14 +88,6 @@ private slots: void on_lineEdit_metatileLabel_editingFinished(); - void on_actionExport_Primary_Tiles_Image_triggered(); - void on_actionExport_Secondary_Tiles_Image_triggered(); - void on_actionExport_Primary_Metatiles_Image_triggered(); - void on_actionExport_Secondary_Metatiles_Image_triggered(); - - void on_actionImport_Primary_Metatiles_triggered(); - void on_actionImport_Secondary_Metatiles_triggered(); - void on_copyButton_metatileLabel_clicked(); void on_actionCut_triggered(); @@ -122,8 +111,10 @@ private: void drawSelectedTiles(); void redrawTileSelector(); void redrawMetatileSelector(); - void importTilesetTiles(Tileset*, bool); - void importTilesetMetatiles(Tileset*, bool); + void importTilesetTiles(Tileset*); + void importAdvanceMapMetatiles(Tileset*); + void exportTilesImage(Tileset*); + void exportMetatilesImage(); void refresh(); void commitMetatileLabel(); void closeEvent(QCloseEvent*); @@ -170,6 +161,7 @@ private: QGraphicsScene *metatileLayersScene = nullptr; bool lockSelection = false; QSet metatileReloadQueue; + MetatileImageExporter::Settings *metatileImageExportSettings = nullptr; bool save(); diff --git a/include/ui/tileseteditormetatileselector.h b/include/ui/tileseteditormetatileselector.h index 8a3fdfa7..77a5c4e4 100644 --- a/include/ui/tileseteditormetatileselector.h +++ b/include/ui/tileseteditormetatileselector.h @@ -21,8 +21,6 @@ public: uint16_t getSelectedMetatileId(); void updateSelectedMetatile(); QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId); - QImage buildPrimaryMetatilesImage(); - QImage buildSecondaryMetatilesImage(); QVector usedMetatiles; bool selectorShowUnused = false; @@ -56,8 +54,6 @@ private: void drawFilters(); void drawUnused(); void drawCounts(); - QImage buildAllMetatilesImage(); - QImage buildImage(int metatileIdStart, int numMetatiles); int numPrimaryMetatilesRounded() const; signals: diff --git a/include/ui/uintspinbox.h b/include/ui/uintspinbox.h index bc217ec2..aa8bcb77 100644 --- a/include/ui/uintspinbox.h +++ b/include/ui/uintspinbox.h @@ -21,6 +21,7 @@ public: uint32_t value() const { return m_value; } uint32_t minimum() const { return m_minimum; } uint32_t maximum() const { return m_maximum; } + uint32_t singleStep() const { return m_singleStep; } QString prefix() const { return m_prefix; } int displayIntegerBase() const { return m_displayIntegerBase; } bool hasPadding() const { return m_hasPadding; } @@ -28,6 +29,7 @@ public: void setMinimum(uint32_t min); void setMaximum(uint32_t max); void setRange(uint32_t min, uint32_t max); + void setSingleStep(uint32_t val); void setPrefix(const QString &prefix); void setDisplayIntegerBase(int base); void setHasPadding(bool enabled); @@ -36,6 +38,7 @@ private: uint32_t m_minimum; uint32_t m_maximum; uint32_t m_value; + uint32_t m_singleStep; QString m_prefix; int m_displayIntegerBase; bool m_hasPadding; diff --git a/porymap.pro b/porymap.pro index 4045e230..982fbd37 100644 --- a/porymap.pro +++ b/porymap.pro @@ -124,6 +124,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/regionmapeditor.cpp \ src/ui/newmapdialog.cpp \ src/ui/mapimageexporter.cpp \ + src/ui/metatileimageexporter.cpp \ src/ui/newtilesetdialog.cpp \ src/ui/flowlayout.cpp \ src/ui/mapruler.cpp \ @@ -240,6 +241,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/regionmapeditor.h \ include/ui/newmapdialog.h \ include/ui/mapimageexporter.h \ + include/ui/metatileimageexporter.h \ include/ui/newtilesetdialog.h \ include/ui/overlay.h \ include/ui/flowlayout.h \ @@ -289,6 +291,7 @@ FORMS += forms/mainwindow.ui \ forms/aboutporymap.ui \ forms/newtilesetdialog.ui \ forms/mapimageexporter.ui \ + forms/metatileimageexporter.ui \ forms/shortcutseditor.ui \ forms/preferenceeditor.ui \ forms/regionmappropertiesdialog.ui \ diff --git a/src/config.cpp b/src/config.cpp index 4f5188bd..8b2f24a9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -283,7 +283,7 @@ int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &valu logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) { @@ -293,7 +293,7 @@ uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString & logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) { @@ -305,6 +305,10 @@ QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &val return color; } +QString KeyValueConfigBase::toConfigColor(const QColor &color) { + return color.name().remove("#"); // Our text config treats '#' as the start of a comment. +} + PorymapConfig porymapConfig; PorymapConfig::PorymapConfig() : KeyValueConfigBase(QStringLiteral("porymap.cfg")) { @@ -571,7 +575,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("grid_x", QString::number(this->gridSettings.offsetX)); map.insert("grid_y", QString::number(this->gridSettings.offsetY)); map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style)); - map.insert("grid_color", this->gridSettings.color.name().remove("#")); // Our text config treats '#' as the start of a comment. + map.insert("grid_color", toConfigColor(this->gridSettings.color)); QStringList logTypesStrings; for (const auto &type : this->statusBarLogTypes) { @@ -898,8 +902,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetsHaveCallback = getConfigBool(key, value); } else if (key == "tilesets_have_is_compressed") { this->tilesetsHaveIsCompressed = getConfigBool(key, value); - } else if (key == "set_transparent_pixels_black") { - this->setTransparentPixelsBlack = getConfigBool(key, value); + } else if (key == "transparency_color") { + this->transparencyColor = getConfigColor(key, value); } else if (key == "preserve_matching_only_data") { this->preserveMatchingOnlyData = getConfigBool(key, value); } else if (key == "event_icon_path_object") { @@ -1005,7 +1009,7 @@ QMap ProjectConfig::getKeyValueMap() { } map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); - map.insert("set_transparent_pixels_black", QString::number(this->setTransparentPixelsBlack)); + map.insert("transparency_color", toConfigColor(this->transparencyColor)); map.insert("preserve_matching_only_data", QString::number(this->preserveMatchingOnlyData)); map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize)); map.insert("metatile_behavior_mask", Util::toHexString(this->metatileBehaviorMask)); diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index a9885042..7ab13ec7 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -6,6 +6,9 @@ #include "imageproviders.h" #include "utility.h" +QList Layout::s_globalMetatileLayerOrder; +QList Layout::s_globalMetatileLayerOpacity; + Layout::Layout(const Layout &other) : Layout() { copyFrom(&other); } @@ -612,23 +615,3 @@ Blockdata Layout::readBlockdata(const QString &path, QString *error) { return blockdata; } - -QList Layout::metatileLayerOrder() const { - return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::defaultMetatileLayerOrder(); -} - -QList Layout::s_defaultMetatileLayerOrder; -QList Layout::defaultMetatileLayerOrder() { - static const QList initialDefault = {0, 1, 2}; - return !s_defaultMetatileLayerOrder.isEmpty() ? s_defaultMetatileLayerOrder : initialDefault; -} - -QList Layout::metatileLayerOpacity() const { - return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::defaultMetatileLayerOpacity(); -} - -QList Layout::s_defaultMetatileLayerOpacity; -QList Layout::defaultMetatileLayerOpacity() { - static const QList initialDefault = {1.0, 1.0, 1.0}; - return !s_defaultMetatileLayerOpacity.isEmpty() ? s_defaultMetatileLayerOpacity : initialDefault; -} diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 73ff9c86..f6c0d382 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -36,8 +36,8 @@ int Metatile::getIndexInTileset(int metatileId) { } QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { - int x = static_cast(pixelCoord.x()) / 16; - int y = static_cast(pixelCoord.y()) / 16; + int x = static_cast(pixelCoord.x()) / pixelWidth(); + int y = static_cast(pixelCoord.y()) / pixelHeight(); if (pixelCoord.x() < 0) x--; if (pixelCoord.y() < 0) y--; return QPoint(x, y); @@ -55,6 +55,11 @@ QString Metatile::getMetatileIdStrings(const QList metatileIds) { return metatiles.join(","); }; +QString Metatile::getLayerName(int layerNum) { + static const QStringList layerTitles = { "Bottom", "Middle", "Top"}; + return layerTitles.value(layerNum); +} + // Read and pack together this metatile's attributes. uint32_t Metatile::getAttributes() const { uint32_t data = 0; diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index 0ae8512f..75c9c362 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -200,9 +200,8 @@ QString Tileset::getMetatileLabelPrefix() QString Tileset::getMetatileLabelPrefix(const QString &name) { // Default is "gTileset_Name" --> "METATILE_Name_" - const QString tilesetPrefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); const QString labelPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix); - return QString("%1%2_").arg(labelPrefix).arg(QString(name).replace(tilesetPrefix, "")); + return QString("%1%2_").arg(labelPrefix).arg(Tileset::stripPrefix(name)); } bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { @@ -382,8 +381,7 @@ QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary) : projectConfig.getFilePath(ProjectFilePath::data_primary_tilesets_folders); static const QRegularExpression re("([a-z])([A-Z0-9])"); - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); - return basePath + tilesetName.replace(prefix, "").replace(re, "\\1_\\2").toLower(); + return basePath + Tileset::stripPrefix(tilesetName).replace(re, "\\1_\\2").toLower(); } // Get the expected positions of the members in struct Tileset. @@ -600,3 +598,7 @@ bool Tileset::save() { if (!savePalettes()) success = false; return success; } + +QString Tileset::stripPrefix(const QString &fullName) { + return QString(fullName).replace(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix), ""); +} diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 0ecfd13e..ff06838f 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -14,7 +14,7 @@ void Util::numericalModeSort(QStringList &list) { std::sort(list.begin(), list.end(), collator); } -int Util::roundUp(int numToRound, int multiple) { +int Util::roundUpToMultiple(int numToRound, int multiple) { if (multiple <= 0) return numToRound; diff --git a/src/editor.cpp b/src/editor.cpp index bd15f4c5..d301d60e 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -1176,8 +1176,8 @@ void Editor::setPlayerViewRect(const QRectF &rect) { } void Editor::setCursorRectPos(const QPoint &pos) { - int x = qMax(0, qMin(pos.x(), this->layout ? this->layout->getWidth() - 1 : 0)); - int y = qMax(0, qMin(pos.y(), this->layout ? this->layout->getHeight() - 1 : 0)); + int x = qBound(0, pos.x(), this->layout ? this->layout->getWidth() - 1 : 0); + int y = qBound(0, pos.y(), this->layout ? this->layout->getHeight() - 1 : 0); if (this->playerViewRect) this->playerViewRect->updateLocation(x, y); diff --git a/src/main.cpp b/src/main.cpp index d85c1b38..dfd3bff2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,8 @@ int main(int argc, char *argv[]) { QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + QCoreApplication::setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true); + QApplication a(argc, argv); a.setStyle("fusion"); diff --git a/src/project.cpp b/src/project.cpp index 75b32474..9a65ac38 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1618,7 +1618,7 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa metatilesFilepath.append(projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles)); } ignoreWatchedFilesTemporarily({headersFilepath, graphicsFilepath, metatilesFilepath}); - name.remove(0, prefix.length()); // Strip prefix from name to get base name for use in other symbols. + name = Tileset::stripPrefix(name); tileset->appendToHeaders(headersFilepath, name, this->usingAsmTilesets); tileset->appendToGraphics(graphicsFilepath, name, this->usingAsmTilesets); tileset->appendToMetatiles(metatilesFilepath, name, this->usingAsmTilesets); @@ -3462,12 +3462,12 @@ void Project::applyParsedLimits() { Block::setLayout(); Metatile::setLayout(this); - Project::num_metatiles_primary = qMin(qMax(Project::num_metatiles_primary, 1), Block::getMaxMetatileId() + 1); + Project::num_metatiles_primary = qBound(1, Project::num_metatiles_primary, Block::getMaxMetatileId() + 1); projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); - projectConfig.collisionSheetSize.setHeight(qMin(qMax(projectConfig.collisionSheetSize.height(), 1), Block::getMaxElevation() + 1)); - projectConfig.collisionSheetSize.setWidth(qMin(qMax(projectConfig.collisionSheetSize.width(), 1), Block::getMaxCollision() + 1)); + projectConfig.collisionSheetSize.setHeight(qBound(1, projectConfig.collisionSheetSize.height(), Block::getMaxElevation() + 1)); + projectConfig.collisionSheetSize.setWidth(qBound(1, projectConfig.collisionSheetSize.width(), Block::getMaxCollision() + 1)); } bool Project::hasUnsavedChanges() { diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index 900c8701..5058e2de 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -201,7 +201,7 @@ QList ScriptUtility::getCustomScripts() { } QList ScriptUtility::getMetatileLayerOrder() { - return Layout::defaultMetatileLayerOrder(); + return Layout::globalMetatileLayerOrder(); } bool ScriptUtility::validateMetatileLayerOrder(const QList &order) { @@ -220,16 +220,16 @@ bool ScriptUtility::validateMetatileLayerOrder(const QList &order) { void ScriptUtility::setMetatileLayerOrder(const QList &order) { if (!validateMetatileLayerOrder(order)) return; - Layout::setDefaultMetatileLayerOrder(order); + Layout::setGlobalMetatileLayerOrder(order); if (window) window->refreshAfterPalettePreviewChange(); } QList ScriptUtility::getMetatileLayerOpacity() { - return Layout::defaultMetatileLayerOpacity(); + return Layout::globalMetatileLayerOpacity(); } void ScriptUtility::setMetatileLayerOpacity(const QList &opacities) { - Layout::setDefaultMetatileLayerOpacity(opacities); + Layout::setGlobalMetatileLayerOpacity(opacities); if (window) window->refreshAfterPalettePreviewChange(); } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 4072baf8..0997c1e1 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -56,11 +56,9 @@ QImage getMetatileImage( const QList &layerOpacity, bool useTruePalettes) { - const int numTilesWide = 2; - const int numTilesTall = 2; - QImage metatile_image(8 * numTilesWide, 8 * numTilesTall, QImage::Format_RGBA8888); + QImage metatile_image(Metatile::pixelWidth(), Metatile::pixelHeight(), QImage::Format_RGBA8888); if (!metatile) { - metatile_image.fill(Qt::magenta); + metatile_image.fill(projectConfig.transparencyColor == QColor(Qt::transparent) ? projectConfig.transparencyColor : QColor(Qt::magenta)); return metatile_image; } @@ -70,21 +68,20 @@ QImage getMetatileImage( // tile pixels line up across layers we will still have something to render. // The GBA renders transparent pixels using palette 0 color 0. We have this color, // but all 3 games actually overwrite it with black when loading the tileset palettes, - // so we have a setting to choose between these two behaviors. - metatile_image.fill(projectConfig.setTransparentPixelsBlack ? QColor("black") : QColor(palettes.value(0).value(0))); + // so we have a setting to specify an override transparency color. + metatile_image.fill(projectConfig.transparencyColor.isValid() ? projectConfig.transparencyColor : QColor(palettes.value(0).value(0))); QPainter metatile_painter(&metatile_image); uint32_t layerType = metatile->layerType(); - const int numTilesPerLayer = numTilesWide * numTilesTall; for (const auto &layer : layerOrder) - for (int y = 0; y < numTilesTall; y++) - for (int x = 0; x < numTilesWide; x++) { + for (int y = 0; y < Metatile::tileHeight(); y++) + for (int x = 0; x < Metatile::tileWidth(); x++) { // Get the tile to render next Tile tile; - int tileOffset = (y * numTilesWide) + x; + int tileOffset = (y * Metatile::tileWidth()) + x; if (projectConfig.tripleLayerMetatilesEnabled) { - tile = metatile->tiles.value(tileOffset + (layer * numTilesPerLayer)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. // The remaining 4 tiles are rendered using user-specified tiles depending on layer type. @@ -95,19 +92,19 @@ QImage getMetatileImage( if (layer == 0) tile = Tile(projectConfig.unusedTileNormal); else // Tiles are on layers 1 and 2 - tile = metatile->tiles.value(tileOffset + ((layer - 1) * numTilesPerLayer)); + tile = metatile->tiles.value(tileOffset + ((layer - 1) * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Covered: if (layer == 2) tile = Tile(projectConfig.unusedTileCovered); else // Tiles are on layers 0 and 1 - tile = metatile->tiles.value(tileOffset + (layer * numTilesPerLayer)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Split: if (layer == 1) tile = Tile(projectConfig.unusedTileSplit); else // Tiles are on layers 0 and 2 - tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * numTilesPerLayer)); + tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * Metatile::tilesPerLayer())); break; } } @@ -130,7 +127,7 @@ QImage getMetatileImage( logWarn(QString("Tile '%1' is referring to invalid palette number: '%2'").arg(tile.tileId).arg(tile.palette)); } - QPoint origin = QPoint(x*8, y*8); + QPoint origin = QPoint(x * Tile::pixelWidth(), y * Tile::pixelHeight()); float opacity = layerOpacity.value(layer, 1.0); if (opacity < 1.0) { int alpha = 255 * opacity; @@ -165,9 +162,9 @@ QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondary QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette) { QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset); if (tileImage.isNull()) { - tileImage = QImage(8, 8, QImage::Format_RGBA8888); + tileImage = QImage(Tile::pixelWidth(), Tile::pixelHeight(), QImage::Format_RGBA8888); QPainter painter(&tileImage); - painter.fillRect(0, 0, 8, 8, palette.at(0)); + painter.fillRect(0, 0, tileImage.width(), tileImage.height(), palette.at(0)); } else { for (int i = 0; i < 16; i++) { tileImage.setColor(i, palette.at(i)); @@ -194,3 +191,57 @@ void flattenTo4bppImage(QImage * image) { for (int i = 0; i < image->sizeInBytes(); i++, pixel++) *pixel %= 16; } + +QImage getMetatileSheetImage(Layout *layout, int numMetatilesWide, bool useTruePalettes) { + return getMetatileSheetImage(layout->tileset_primary, + layout->tileset_secondary, + 0, + -1, + numMetatilesWide, + layout->metatileLayerOrder(), + layout->metatileLayerOpacity(), + Metatile::pixelSize(), + useTruePalettes); +} + +QImage getMetatileSheetImage(Tileset *primaryTileset, + Tileset *secondaryTileset, + uint16_t metatileIdStart, + int numMetatilesToDraw, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity, + const QSize &metatileSize, + bool useTruePalettes) +{ + // We round up the number of primary metatiles to keep the tilesets on separate rows. + int numPrimary = Util::roundUpToMultiple(primaryTileset ? primaryTileset->numMetatiles() : 0, numMetatilesWide); + int maxPrimary = Project::getNumMetatilesPrimary(); + bool includesPrimary = metatileIdStart < maxPrimary; + + // Negative values are used to indicate 'draw all metatiles' + if (numMetatilesToDraw < 0) { + numMetatilesToDraw = numPrimary + (secondaryTileset ? secondaryTileset->numMetatiles() : 0) - metatileIdStart; + } + + // Round up height for incomplete last row + int numMetatilesTall = ceil((double)numMetatilesToDraw / numMetatilesWide); + + QImage image(numMetatilesWide * metatileSize.width(), numMetatilesTall * metatileSize.height(), QImage::Format_RGBA8888); + image.fill(projectConfig.transparencyColor == QColor(Qt::transparent) ? projectConfig.transparencyColor : QColor(Qt::magenta)); + + QPainter painter(&image); + for (int i = 0; i < numMetatilesToDraw; i++) { + uint16_t metatileId = i + metatileIdStart; + if (includesPrimary && metatileId >= numPrimary) + metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset + QImage metatile_image = getMetatileImage(metatileId, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes) + .scaled(metatileSize.width(), metatileSize.height()); + int map_y = i / numMetatilesWide; + int map_x = i % numMetatilesWide; + QPoint metatile_origin = QPoint(map_x * metatileSize.width(), map_y * metatileSize.height()); + painter.drawImage(metatile_origin, metatile_image); + } + painter.end(); + return image; +} diff --git a/src/ui/metatileimageexporter.cpp b/src/ui/metatileimageexporter.cpp new file mode 100644 index 00000000..d5e94f13 --- /dev/null +++ b/src/ui/metatileimageexporter.cpp @@ -0,0 +1,321 @@ +#include "metatileimageexporter.h" +#include "ui_metatileimageexporter.h" +#include "filedialog.h" +#include "imageproviders.h" +#include "utility.h" +#include "project.h" +#include "metatile.h" + +#include + +MetatileImageExporter::MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings) : + QDialog(parent), + ui(new Ui::MetatileImageExporter), + m_primaryTileset(primaryTileset), + m_secondaryTileset(secondaryTileset), + m_savedSettings(savedSettings) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + m_transparencyButtons = { + ui->radioButton_TransparencyNormal, + ui->radioButton_TransparencyBlack, + ui->radioButton_TransparencyFirst, + }; + + m_scene = new QGraphicsScene(this); + m_preview = m_scene->addPixmap(QPixmap()); + ui->graphicsView_Preview->setScene(m_scene); + + if (projectConfig.tripleLayerMetatilesEnabled) { + // When triple-layer metatiles are enabled there is no unused layer, + // so this setting becomes pointless. Disable it. + ui->checkBox_Placeholders->setVisible(false); + } + + uint16_t maxMetatileId = Block::getMaxMetatileId(); + ui->spinBox_MetatileStart->setMaximum(maxMetatileId); + ui->spinBox_MetatileEnd->setMaximum(maxMetatileId); + ui->spinBox_WidthMetatiles->setRange(1, maxMetatileId); + ui->spinBox_WidthPixels->setRange(1 * Metatile::pixelWidth(), maxMetatileId * Metatile::pixelWidth()); + + if (m_primaryTileset) { + ui->comboBox_PrimaryTileset->setTextItem(m_primaryTileset->name); + } + if (m_secondaryTileset) { + ui->comboBox_SecondaryTileset->setTextItem(m_secondaryTileset->name); + } + + if (m_savedSettings) { + applySettings(*m_savedSettings); + } else { + applySettings({}); + } + + connect(ui->listWidget_Layers, &ReorderableListWidget::itemChanged, this, &MetatileImageExporter::updatePreview); + connect(ui->listWidget_Layers, &ReorderableListWidget::reordered, this, &MetatileImageExporter::updatePreview); + + connect(ui->pushButton_Save, &QPushButton::pressed, this, &MetatileImageExporter::saveImage); + connect(ui->pushButton_Close, &QPushButton::pressed, this, &MetatileImageExporter::close); + connect(ui->pushButton_Reset, &QPushButton::pressed, this, &MetatileImageExporter::reset); + + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncPixelWidth); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncMetatileWidth); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::syncPixelWidth); // Round pixel width to multiple of 16 + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileEnd); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileStart); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + // If we used toggled instead of clicked we'd get two preview updates instead of one when the setting changes. + connect(ui->radioButton_TransparencyNormal, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyBlack, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyFirst, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + + connect(ui->checkBox_Placeholders, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updateTilesetUI); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updateTilesetUI); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + + ui->graphicsView_Preview->setFocus(); +} + +MetatileImageExporter::~MetatileImageExporter() { + delete ui; +} + +// Allow the window to open before displaying the preview. +// Metatile sheet image creation is generally quick, so this only +// really matters so that the graphics view can adjust the scale properly. +void MetatileImageExporter::showEvent(QShowEvent *event) { + QDialog::showEvent(event); + if (!event->spontaneous()) { + QTimer::singleShot(0, this, &MetatileImageExporter::updatePreview); + } +} + +void MetatileImageExporter::closeEvent(QCloseEvent *event) { + if (m_savedSettings) { + m_savedSettings->metatileStart = ui->spinBox_MetatileStart->value(); + m_savedSettings->metatileEnd = ui->spinBox_MetatileEnd->value(); + m_savedSettings->numMetatilesWide = ui->spinBox_WidthMetatiles->value(); + m_savedSettings->usePrimaryTileset = ui->checkBox_PrimaryTileset->isChecked(); + m_savedSettings->useSecondaryTileset = ui->checkBox_SecondaryTileset->isChecked(); + m_savedSettings->renderPlaceholders = ui->checkBox_Placeholders->isChecked(); + for (int i = 0; i < m_transparencyButtons.length(); i++) { + if (m_transparencyButtons.at(i)->isChecked()) { + m_savedSettings->transparencyMode = i; + break; + } + } + m_savedSettings->layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + int layerNum = item->data(Qt::UserRole).toInt(); + m_savedSettings->layerOrder[layerNum] = (item->checkState() == Qt::Checked); + } + } + QDialog::closeEvent(event); +} + +void MetatileImageExporter::applySettings(const Settings &settings) { + ui->spinBox_MetatileStart->setValue(settings.metatileStart); + ui->spinBox_MetatileEnd->setValue(settings.metatileEnd); + ui->spinBox_WidthMetatiles->setValue(settings.numMetatilesWide); + ui->spinBox_WidthPixels->setValue(settings.numMetatilesWide * Metatile::pixelWidth()); + ui->checkBox_PrimaryTileset->setChecked(settings.usePrimaryTileset); + ui->checkBox_SecondaryTileset->setChecked(settings.useSecondaryTileset); + ui->checkBox_Placeholders->setChecked(settings.renderPlaceholders); + if (m_transparencyButtons.value(settings.transparencyMode)) { + m_transparencyButtons[settings.transparencyMode]->setChecked(true); + } + + // Build layer list from settings + ui->listWidget_Layers->clear(); + for (auto it = settings.layerOrder.cbegin(); it != settings.layerOrder.cend(); it++) { + int layerNum = it.key(); + bool enabled = it.value(); + + auto item = new QListWidgetItem(Metatile::getLayerName(layerNum)); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); + item->setData(Qt::UserRole, layerNum); // Save the original index to identify the layer + ui->listWidget_Layers->addItem(item); + } + // Don't give extra unnecessary space to the list + ui->listWidget_Layers->setFixedHeight(ui->listWidget_Layers->sizeHintForRow(0) * ui->listWidget_Layers->count() + 4); + + updateTilesetUI(); +} + +void MetatileImageExporter::reset() { + applySettings({}); + updatePreview(); +} + +void MetatileImageExporter::saveImage() { + // Ensure the image in the preview is up-to-date before exporting. + updatePreview(); + + QString defaultFilename; + if (m_layerOrder.length() == 1) { + // Exporting a metatile layer image is an expected use case for Porytiles, which expects certain file names. + // We can make the process a little easier by setting the default file name to those expected file names. + static const QStringList layerFilenames = { "bottom", "middle", "top" }; + defaultFilename = (layerFilenames.at(m_layerOrder.constFirst())); + } else { + if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_primaryTileset->name))); + } + if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_secondaryTileset->name))); + } + if (!m_layerOrder.isEmpty() && m_layerOrder != QList({0,1,2})) { + for (int i = m_layerOrder.length() - 1; i >= 0; i--) { + defaultFilename.append(Metatile::getLayerName(m_layerOrder.at(i))); + } + defaultFilename.append("_"); + } + defaultFilename.append("Metatiles"); + } + + QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultFilename); + QString filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, QStringLiteral("Image Files (*.png *.jpg *.bmp)")); + if (!filepath.isEmpty()) { + m_preview->pixmap().save(filepath); + } +} + +void MetatileImageExporter::queuePreviewUpdate() { + m_previewUpdateQueued = true; +} + +// For updating only when a change has been recorded. +// Useful for something that might happen often, like an input widget losing focus. +void MetatileImageExporter::tryUpdatePreview() { + if (m_preview->pixmap().isNull() || m_previewUpdateQueued) { + updatePreview(); + } +} + +void MetatileImageExporter::updatePreview() { + copyRenderSettings(); + + int numMetatilesWide = ui->spinBox_WidthMetatiles->value(); + int metatileStart = ui->spinBox_MetatileStart->value(); + int numMetatiles = Util::roundUpToMultiple(ui->spinBox_MetatileEnd->value() - metatileStart + 1, numMetatilesWide); + + m_layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + if (item->checkState() == Qt::Checked) { + int layerNum = item->data(Qt::UserRole).toInt(); + m_layerOrder.prepend(qBound(0, layerNum, 2)); + } + } + + QImage previewImage = getMetatileSheetImage(m_primaryTileset, + m_secondaryTileset, + metatileStart, + numMetatiles, + numMetatilesWide, + m_layerOrder); + m_preview->setPixmap(QPixmap::fromImage(previewImage)); + m_scene->setSceneRect(m_scene->itemsBoundingRect()); + m_previewUpdateQueued = false; + + restoreRenderSettings(); +} + +void MetatileImageExporter::validateMetatileStart() { + const QSignalBlocker b(ui->spinBox_MetatileStart); + ui->spinBox_MetatileStart->setValue(qMin(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +void MetatileImageExporter::validateMetatileEnd() { + const QSignalBlocker b(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileEnd->setValue(qMax(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +uint16_t MetatileImageExporter::getExpectedMetatileStart() { + if (ui->checkBox_PrimaryTileset->isChecked()) return 0; + if (ui->checkBox_SecondaryTileset->isChecked()) return Project::getNumMetatilesPrimary(); + return ui->spinBox_MetatileStart->value(); +} + +// TODO: Combining tilesets is not rendering the correct range of metatiles +uint16_t MetatileImageExporter::getExpectedMetatileEnd() { + if (ui->checkBox_SecondaryTileset->isChecked()) return Project::getNumMetatilesPrimary() + (m_secondaryTileset ? (m_secondaryTileset->numMetatiles() - 1) : 0); + if (ui->checkBox_PrimaryTileset->isChecked()) return m_primaryTileset ? (m_primaryTileset->numMetatiles() - 1) : 0; + return ui->spinBox_MetatileEnd->value(); +} + +void MetatileImageExporter::updateMetatileRange() { + const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart); + const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileStart->setValue(getExpectedMetatileStart()); + ui->spinBox_MetatileEnd->setValue(getExpectedMetatileEnd()); +} + +void MetatileImageExporter::updateTilesetUI() { + // Users can either specify which tileset(s) to render, or specify a range of metatiles, but not both. + if (ui->checkBox_PrimaryTileset->isChecked() || ui->checkBox_SecondaryTileset->isChecked()) { + updateMetatileRange(); + ui->groupBox_MetatileRange->setDisabled(true); + } else { + ui->groupBox_MetatileRange->setDisabled(false); + } +} + +void MetatileImageExporter::syncPixelWidth() { + const QSignalBlocker b(ui->spinBox_WidthPixels); + ui->spinBox_WidthPixels->setValue(ui->spinBox_WidthMetatiles->value() * Metatile::pixelWidth()); +} + +void MetatileImageExporter::syncMetatileWidth() { + const QSignalBlocker b(ui->spinBox_WidthMetatiles); + ui->spinBox_WidthMetatiles->setValue(Util::roundUpToMultiple(ui->spinBox_WidthPixels->value(), Metatile::pixelWidth()) / Metatile::pixelWidth()); +} + +// These settings control some rendering behavior that make metatiles render accurately to their in-game appearance, +// which may be undesirable when exporting metatile images for editing. +// The settings are buried in getMetatileImage at the moment, to change them we'll temporarily overwrite them. +void MetatileImageExporter::copyRenderSettings() { + m_savedConfig.transparencyColor = projectConfig.transparencyColor; + m_savedConfig.unusedTileNormal = projectConfig.unusedTileNormal; + m_savedConfig.unusedTileCovered = projectConfig.unusedTileCovered; + m_savedConfig.unusedTileSplit = projectConfig.unusedTileSplit; + + if (ui->radioButton_TransparencyNormal->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::transparent); + } else if (ui->radioButton_TransparencyBlack->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::black); + } else { + projectConfig.transparencyColor = QColor(); + } + + if (!ui->checkBox_Placeholders->isChecked()) { + projectConfig.unusedTileNormal = 0; + projectConfig.unusedTileCovered = 0; + projectConfig.unusedTileSplit = 0; + } +} + +void MetatileImageExporter::restoreRenderSettings() { + projectConfig.transparencyColor = m_savedConfig.transparencyColor; + projectConfig.unusedTileNormal = m_savedConfig.unusedTileNormal; + projectConfig.unusedTileCovered = m_savedConfig.unusedTileCovered; + projectConfig.unusedTileSplit = m_savedConfig.unusedTileSplit; +} diff --git a/src/ui/metatilelayersitem.cpp b/src/ui/metatilelayersitem.cpp index a26ed14b..b0279e4e 100644 --- a/src/ui/metatilelayersitem.cpp +++ b/src/ui/metatilelayersitem.cpp @@ -148,6 +148,6 @@ void MetatileLayersItem::clearLastHoveredCoords() { QPoint MetatileLayersItem::getBoundedPos(const QPointF &pos) { int x = static_cast(pos.x()) / this->cellWidth; int y = static_cast(pos.y()) / this->cellHeight; - return QPoint( qMax(0, qMin(x, this->maxSelectionWidth - 1)), - qMax(0, qMin(y, this->maxSelectionHeight - 1)) ); + return QPoint(qBound(0, x, this->maxSelectionWidth - 1), + qBound(0, y, this->maxSelectionHeight - 1)); } diff --git a/src/ui/metatileselector.cpp b/src/ui/metatileselector.cpp index 321f2925..d8f50410 100644 --- a/src/ui/metatileselector.cpp +++ b/src/ui/metatileselector.cpp @@ -15,28 +15,7 @@ int MetatileSelector::numPrimaryMetatilesRounded() const { } void MetatileSelector::updateBasePixmap() { - int primaryLength = this->numPrimaryMetatilesRounded(); - int length_ = primaryLength + this->secondaryTileset()->numMetatiles(); - int height_ = length_ / this->numMetatilesWide; - if (length_ % this->numMetatilesWide != 0) { - height_++; - } - QImage image(this->numMetatilesWide * this->cellWidth, height_ * this->cellHeight, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < length_; i++) { - int metatileId = i; - if (i >= primaryLength) { - metatileId += Project::getNumMetatilesPrimary() - primaryLength; - } - QImage metatile_image = getMetatileImage(metatileId, this->layout); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - this->basePixmap = QPixmap::fromImage(image); + this->basePixmap = QPixmap::fromImage(getMetatileSheetImage(this->layout, this->numMetatilesWide)); } void MetatileSelector::draw() { diff --git a/src/ui/movablerect.cpp b/src/ui/movablerect.cpp index 3fc73934..70e08e46 100644 --- a/src/ui/movablerect.cpp +++ b/src/ui/movablerect.cpp @@ -114,8 +114,8 @@ void ResizableRect::mousePressEvent(QGraphicsSceneMouseEvent *event) { } void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - int dx = Util::roundUp(event->scenePos().x() - this->clickedPos.x(), 16); - int dy = Util::roundUp(event->scenePos().y() - this->clickedPos.y(), 16); + int dx = Util::roundUpToMultiple(event->scenePos().x() - this->clickedPos.x(), 16); + int dy = Util::roundUpToMultiple(event->scenePos().y() - this->clickedPos.y(), 16); QRect resizedRect = this->clickedRect; diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index c4becaa5..f9f820ab 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -475,10 +475,11 @@ void ProjectSettingsEditor::refresh() { ui->checkBox_PreserveMatchingOnlyData->setChecked(projectConfig.preserveMatchingOnlyData); // Radio buttons - if (projectConfig.setTransparentPixelsBlack) + // TODO: Replace + /*if (projectConfig.setTransparentPixelsBlack) ui->radioButton_RenderBlack->setChecked(true); else - ui->radioButton_RenderFirstPalColor->setChecked(true); + ui->radioButton_RenderFirstPalColor->setChecked(true);*/ // Set spin box values ui->spinBox_Elevation->setValue(projectConfig.defaultElevation); @@ -574,7 +575,7 @@ void ProjectSettingsEditor::save() { projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked(); projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked(); porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked(); - projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); + //projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); // TODO projectConfig.preserveMatchingOnlyData = ui->checkBox_PreserveMatchingOnlyData->isChecked(); // Save spin box settings diff --git a/src/ui/resizelayoutpopup.cpp b/src/ui/resizelayoutpopup.cpp index 7f11836f..e6b814b1 100644 --- a/src/ui/resizelayoutpopup.cpp +++ b/src/ui/resizelayoutpopup.cpp @@ -61,7 +61,7 @@ void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem QVariant BoundedPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && scene()) { QPointF newPos = value.toPointF(); - return QPointF(Util::roundUp(newPos.x(), 16), Util::roundUp(newPos.y(), 16)); + return QPointF(Util::roundUpToMultiple(newPos.x(), 16), Util::roundUpToMultiple(newPos.y(), 16)); } else return QGraphicsItem::itemChange(change, value); diff --git a/src/ui/selectablepixmapitem.cpp b/src/ui/selectablepixmapitem.cpp index e15824ad..3b212579 100644 --- a/src/ui/selectablepixmapitem.cpp +++ b/src/ui/selectablepixmapitem.cpp @@ -19,8 +19,8 @@ void SelectablePixmapItem::select(int x, int y, int width, int height) { this->selectionInitialX = x; this->selectionInitialY = y; - this->selectionOffsetX = qMax(0, qMin(width, this->maxSelectionWidth)); - this->selectionOffsetY = qMax(0, qMin(height, this->maxSelectionHeight)); + this->selectionOffsetX = qBound(0, width, this->maxSelectionWidth); + this->selectionOffsetY = qBound(0, height, this->maxSelectionHeight); this->draw(); emit this->selectionChanged(x, y, width, height); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 6fe96f0f..55545616 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -24,8 +24,10 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) hasUnsavedChanges(false) { setAttribute(Qt::WA_DeleteOnClose); - setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); ui->setupUi(this); + + setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); + connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::setXFlip); connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::setYFlip); @@ -35,6 +37,17 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); + connect(ui->actionImport_Primary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->secondaryTileset); }); + + connect(ui->actionImport_Primary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->secondaryTileset); }); + + connect(ui->actionExport_Primary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->primaryTileset); }); + connect(ui->actionExport_Secondary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->secondaryTileset); }); + + connect(ui->actionExport_Metatiles_Image, &QAction::triggered, [this] { exportMetatilesImage(); }); + ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); @@ -74,6 +87,7 @@ TilesetEditor::~TilesetEditor() delete selectedTileScene; delete metatileLayersScene; delete copiedMetatile; + delete metatileImageExportSettings; this->metatileHistory.clear(); } @@ -694,17 +708,8 @@ bool TilesetEditor::save() { return success; } -void TilesetEditor::on_actionImport_Primary_Tiles_triggered() -{ - this->importTilesetTiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Tiles_triggered() -{ - this->importTilesetTiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { +void TilesetEditor::importTilesetTiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptor = primary ? "primary" : "secondary"; QString descriptorCaps = primary ? "Primary" : "Secondary"; @@ -968,62 +973,27 @@ void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel) this->commitMetatileAndLabelChange(prevMetatile, prevLabel); } -void TilesetEditor::on_actionExport_Primary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->primaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Tiles Image", defaultFilepath, "Image Files (*.png)"); +void TilesetEditor::exportTilesImage(Tileset *tileset) { + bool primary = !tileset->is_secondary; + QString defaultFilepath = QString("%1/%2_Tiles_Pal%3.png").arg(FileDialog::getDirectory()).arg(tileset->name).arg(this->paletteId); + QString filepath = FileDialog::getSaveFileName(this, QString("Export %1 Tiles Image").arg(primary ? "Primary" : "Secondary"), defaultFilepath, "Image Files (*.png)"); if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildPrimaryTilesIndexedImage(); + QImage image = primary ? this->tileSelector->buildPrimaryTilesIndexedImage() : this->tileSelector->buildSecondaryTilesIndexedImage(); exportIndexed4BPPPng(image, filepath); } } -void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->secondaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Tiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildSecondaryTilesIndexedImage(); - exportIndexed4BPPPng(image, filepath); +// There are many more options for exporting metatile images than tile images, so we open a separate dialog to ask the user for settings. +void TilesetEditor::exportMetatilesImage() { + if (!this->metatileImageExportSettings) { + this->metatileImageExportSettings = new MetatileImageExporter::Settings; } + auto dialog = new MetatileImageExporter(this, this->primaryTileset, this->secondaryTileset, this->metatileImageExportSettings); + dialog->open(); } -void TilesetEditor::on_actionExport_Primary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->primaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildPrimaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionExport_Secondary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->secondaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildSecondaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionImport_Primary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) -{ +void TilesetEditor::importAdvanceMapMetatiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptorCaps = primary ? "Primary" : "Secondary"; QString filepath = FileDialog::getOpenFileName(this, QString("Import %1 Tileset Metatiles from Advance Map 1.92").arg(descriptorCaps), "", "Advance Map 1.92 Metatile Files (*.bvd)"); diff --git a/src/ui/tileseteditormetatileselector.cpp b/src/ui/tileseteditormetatileselector.cpp index 79888681..60f4b0f5 100644 --- a/src/ui/tileseteditormetatileselector.cpp +++ b/src/ui/tileseteditormetatileselector.cpp @@ -4,7 +4,7 @@ #include TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout) - : SelectablePixmapItem(16, 16, 1, 1) { + : SelectablePixmapItem(32, 32, 1, 1) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; this->numMetatilesWide = 8; @@ -31,48 +31,6 @@ int TilesetEditorMetatileSelector::numPrimaryMetatilesRounded() const { return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; } -QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() { - return this->buildImage(0, this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildPrimaryMetatilesImage() { - return this->buildImage(0, this->primaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() { - return this->buildImage(Project::getNumMetatilesPrimary(), this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) { - int numMetatilesHigh = this->numRows(numMetatiles); - int numPrimary = this->numPrimaryMetatilesRounded(); - int maxPrimary = Project::getNumMetatilesPrimary(); - bool includesPrimary = metatileIdStart < maxPrimary; - - QImage image(this->numMetatilesWide * this->cellWidth, numMetatilesHigh * this->cellHeight, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < numMetatiles; i++) { - int metatileId = i + metatileIdStart; - if (includesPrimary && metatileId >= numPrimary) - metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset - QImage metatile_image = getMetatileImage( - metatileId, - this->primaryTileset, - this->secondaryTileset, - this->layout->metatileLayerOrder(), - this->layout->metatileLayerOpacity(), - true) - .scaled(this->cellWidth, this->cellHeight); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - return image; -} - void TilesetEditorMetatileSelector::drawMetatile(uint16_t metatileId) { QPoint pos = getMetatileIdCoords(metatileId); @@ -97,7 +55,15 @@ void TilesetEditorMetatileSelector::drawSelectedMetatile() { } void TilesetEditorMetatileSelector::updateBasePixmap() { - this->baseImage = buildAllMetatilesImage(); + this->baseImage = getMetatileSheetImage(this->primaryTileset, + this->secondaryTileset, + 0, + this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles(), + this->numMetatilesWide, + this->layout->metatileLayerOrder(), + this->layout->metatileLayerOpacity(), + QSize(this->cellWidth, this->cellHeight), + true); this->basePixmap = QPixmap::fromImage(this->baseImage); } diff --git a/src/ui/uintspinbox.cpp b/src/ui/uintspinbox.cpp index 53f6df78..cfe19b72 100644 --- a/src/ui/uintspinbox.cpp +++ b/src/ui/uintspinbox.cpp @@ -2,24 +2,24 @@ #include UIntSpinBox::UIntSpinBox(QWidget *parent) - : QAbstractSpinBox(parent) + : QAbstractSpinBox(parent), + m_minimum(0), + m_maximum(99), + m_value(m_minimum), + m_singleStep(1), + m_displayIntegerBase(10), + m_hasPadding(false), + m_numChars(2) { // Don't let scrolling hijack focus. setFocusPolicy(Qt::StrongFocus); - m_minimum = 0; - m_maximum = 99; - m_value = m_minimum; - m_displayIntegerBase = 10; - m_numChars = 2; - m_hasPadding = false; - this->updateEdit(); connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(onEditFinished())); }; void UIntSpinBox::setValue(uint32_t val) { - val = qMax(m_minimum, qMin(m_maximum, val)); + val = qBound(m_minimum, val, m_maximum); if (m_value != val) { m_value = val; emit valueChanged(m_value); @@ -69,6 +69,12 @@ void UIntSpinBox::setRange(uint32_t min, uint32_t max) { this->updateEdit(); } +void UIntSpinBox::setSingleStep(uint32_t val) { + if (m_singleStep != val) { + m_singleStep = val; + } +} + void UIntSpinBox::setPrefix(const QString &prefix) { if (m_prefix != prefix) { m_prefix = prefix; @@ -127,6 +133,7 @@ void UIntSpinBox::onEditFinished() { } void UIntSpinBox::stepBy(int steps) { + steps *= m_singleStep; auto newValue = m_value; if (steps < 0 && newValue + steps > newValue) { newValue = 0; diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index fd2d9216..ea5b4814 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -367,7 +367,7 @@ QChart* WildMonChart::createLevelDistributionChart() { series->attachAxis(axisY); // We round the y-axis max up to a multiple of 5. - axisY->setMax(Util::roundUp(qCeil(axisY->max()), 5)); + axisY->setMax(Util::roundUpToMultiple(qCeil(axisY->max()), 5)); return chart; }