diff --git a/forms/palettecolorsearch.ui b/forms/palettecolorsearch.ui new file mode 100644 index 00000000..b6ae87c8 --- /dev/null +++ b/forms/palettecolorsearch.ui @@ -0,0 +1,152 @@ + + + PaletteColorSearch + + + + 0 + 0 + 547 + 329 + + + + Palette Color Search + + + + + + + + + Qt::AlignmentFlag::AlignCenter + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + 32 + 32 + + + + QFrame::Shadow::Raised + + + + + + + + 0 + 0 + + + + Color + + + + + + + 1 + + + + + + + + 0 + 0 + + + + Palette + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + 2 + + + + + + + + + QDialogButtonBox::StandardButton::Close + + + + + + + + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + +
diff --git a/forms/paletteeditor.ui b/forms/paletteeditor.ui index c98d49b6..ae394040 100644 --- a/forms/paletteeditor.ui +++ b/forms/paletteeditor.ui @@ -38,6 +38,27 @@ + + + + <html><head/><body><p>Opens a search dialog to find which tilesets/metatiles are using certain colors.</p></body></html> + + + ... + + + + :/icons/magnifier.ico:/icons/magnifier.ico + + + + + + + (All colors used) + + + @@ -89,7 +110,7 @@ 0 0 883 - 784 + 779 @@ -133,8 +154,22 @@ + + + View + + + + + + Tools + + + + + @@ -164,7 +199,22 @@ Import Palette + + + true + + + Show Unused Colors + + + + + Find Color Usage... + + - + + + diff --git a/include/config.h b/include/config.h index e929d64c..25482457 100644 --- a/include/config.h +++ b/include/config.h @@ -116,6 +116,7 @@ public: bool showTilesetEditorLayerGrid; bool showTilesetEditorDivider; bool showTilesetEditorRawAttributes; + bool showPaletteEditorUnusedColors; bool monitorFiles; bool tilesetCheckerboardFill; bool newMapHeaderSectionExpanded; diff --git a/include/core/events.h b/include/core/events.h index 76a58dcf..0e8af1f7 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -122,8 +122,8 @@ public: int getZ() const { return this->elevation; } int getElevation() const { return this->elevation; } - int getPixelX() const { return (this->x * 16) - qMax(0, (pixmap.width() - 16) / 2); } - int getPixelY() const { return (this->y * 16) - qMax(0, pixmap.height() - 16); } + int getPixelX() const; + int getPixelY() const; virtual EventFrame *getEventFrame(); virtual EventFrame *createEventFrame() = 0; diff --git a/include/core/metatile.h b/include/core/metatile.h index e35e1fa3..eba4afed 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -61,7 +61,7 @@ public: static int getDefaultAttributesSize(BaseGameVersion version); static void setLayout(Project*); static QString getMetatileIdString(uint16_t metatileId); - static QString getMetatileIdStrings(const QList metatileIds); + static QString getMetatileIdStrings(const QList &metatileIds); static QString getLayerName(int layerNum); static constexpr int tileWidth() { return 2; } diff --git a/include/core/tile.h b/include/core/tile.h index a13e9fc7..11fd6a99 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -29,6 +29,7 @@ public: static constexpr int pixelWidth() { return 8; } static constexpr int pixelHeight() { return 8; } static constexpr QSize pixelSize() { return QSize(Tile::pixelWidth(), Tile::pixelHeight()); } + static constexpr int numPixels() { return Tile::pixelWidth() * Tile::pixelHeight(); } static constexpr int sizeInBytes() { return sizeof(uint16_t); } }; diff --git a/include/core/tileset.h b/include/core/tileset.h index be5bdace..3d137056 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -37,9 +37,14 @@ public: QList> palettePreviews; static QString stripPrefix(const QString &fullName); + static Tileset* getPaletteTileset(int, Tileset*, Tileset*); + static const Tileset* getPaletteTileset(int, const Tileset*, const Tileset*); static Tileset* getMetatileTileset(int, Tileset*, Tileset*); + static const Tileset* getMetatileTileset(int, const Tileset*, const Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); + static const Tileset* getTileTileset(int, const Tileset*, const Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); + static const Metatile* getMetatile(int, const Tileset*, const Tileset*); static Tileset* getMetatileLabelTileset(int, Tileset*, Tileset*); static QString getMetatileLabel(int, Tileset *, Tileset *); static QString getOwnedMetatileLabel(int, Tileset *, Tileset *); @@ -47,9 +52,9 @@ public: static bool setMetatileLabel(int, QString, Tileset *, Tileset *); QString getMetatileLabelPrefix(); static QString getMetatileLabelPrefix(const QString &name); - static QList> getBlockPalettes(Tileset*, Tileset*, bool useTruePalettes = false); - static QList getPalette(int, Tileset*, Tileset*, bool useTruePalettes = false); - static bool metatileIsValid(uint16_t metatileId, Tileset *, Tileset *); + static QList> getBlockPalettes(const Tileset*, const Tileset*, bool useTruePalettes = false); + static QList getPalette(int, const Tileset*, const Tileset*, bool useTruePalettes = false); + static bool metatileIsValid(uint16_t metatileId, const Tileset*, const Tileset*); static QHash getHeaderMemberMap(bool usingAsm); static QString getExpectedDir(QString tilesetName, bool isSecondary); QString getExpectedDir(); @@ -92,6 +97,12 @@ public: QImage tileImage(uint16_t tileId) const { return m_tiles.value(Tile::getIndexInTileset(tileId)); } + QSet getUnusedColorIds(int paletteId, const Tileset *pairedTileset, const QSet &searchColors = {}) const; + QList findMetatilesUsingColor(int paletteId, int colorId, const Tileset *pairedTileset) const; + + static constexpr int maxPalettes() { return 16; } + static constexpr int numColorsPerPalette() { return 16; } + private: QList m_metatiles; diff --git a/include/core/utility.h b/include/core/utility.h index 41c1dcde..7270b552 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -16,6 +16,7 @@ namespace Util { QString replaceExtension(const QString &path, const QString &newExtension); void setErrorStylesheet(QLineEdit *lineEdit, bool isError); QString toStylesheetString(const QFont &font); + void show(QWidget *w); } #endif // UTILITY_H diff --git a/include/editor.h b/include/editor.h index fd9a809e..ffe628ac 100644 --- a/include/editor.h +++ b/include/editor.h @@ -67,8 +67,6 @@ public: bool setLayout(QString layoutName); void unsetMap(); - Tileset *getCurrentMapPrimaryTileset(); - bool displayMap(); bool displayLayout(); diff --git a/include/mainwindow.h b/include/mainwindow.h index 813370b4..841cdcf6 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -385,7 +385,6 @@ private: void openDuplicateMapOrLayoutDialog(); void openNewMapGroupDialog(); void openNewLocationDialog(); - void openSubWindow(QWidget * window); void scrollMapList(MapTree *list, const QString &itemName); void scrollMapListToCurrentMap(MapTree *list); void scrollMapListToCurrentLayout(MapTree *list); diff --git a/include/project.h b/include/project.h index dd66c2e5..1c14f996 100644 --- a/include/project.h +++ b/include/project.h @@ -104,11 +104,11 @@ public: bool load(); QMap tilesetCache; - Tileset* loadTileset(QString, Tileset *tileset = nullptr); - Tileset* getTileset(QString, bool forceLoad = false); + Tileset* getTileset(const QString&, bool forceLoad = false); QStringList primaryTilesetLabels; QStringList secondaryTilesetLabels; QStringList tilesetLabelsOrdered; + QSet getPairedTilesetLabels(const Tileset *tileset) const; bool readMapGroups(); void addNewMapGroup(const QString &groupName); @@ -342,6 +342,7 @@ private: void resetFileCache(); void resetFileWatcher(); void logFileWatchStatus(); + void cacheTileset(const QString &label, Tileset *tileset); bool saveMapLayouts(); bool saveMapGroups(); diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index 17b823ce..f1edc04a 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -11,14 +11,14 @@ class Layout; 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, const Layout*, bool useTruePalettes = false); +QImage getMetatileImage(const Metatile*, const Layout*, bool useTruePalettes = false); +QImage getMetatileImage(uint16_t, const Tileset*, const Tileset*, const QList& = {0,1,2}, const QList& = {}, bool useTruePalettes = false); +QImage getMetatileImage(const Metatile*, const Tileset*, const Tileset*, const QList& = {0,1,2}, const QList& = {}, bool useTruePalettes = false); -QImage getMetatileSheetImage(Layout *layout, int numMetatilesWIde, bool useTruePalettes = false); -QImage getMetatileSheetImage(Tileset *primaryTileset, - Tileset *secondaryTileset, +QImage getMetatileSheetImage(const Layout *layout, int numMetatilesWIde, bool useTruePalettes = false); +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, uint16_t metatileIdStart, uint16_t metatileIdEnd, int numMetatilesWIde, @@ -26,8 +26,8 @@ QImage getMetatileSheetImage(Tileset *primaryTileset, const QList &layerOpacity = {}, const QSize &metatileSize = Metatile::pixelSize(), bool useTruePalettes = false); -QImage getMetatileSheetImage(Tileset *primaryTileset, - Tileset *secondaryTileset, +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, int numMetatilesWide, const QList &layerOrder, const QList &layerOpacity = {}, @@ -35,10 +35,10 @@ QImage getMetatileSheetImage(Tileset *primaryTileset, bool useTruePalettes = false); -QImage getTileImage(uint16_t, Tileset*, Tileset*); -QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); -QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette); -QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset); +QImage getTileImage(uint16_t, const Tileset*, const Tileset*); +QImage getPalettedTileImage(uint16_t, const Tileset*, const Tileset*, int, bool useTruePalettes = false); +QImage getColoredTileImage(uint16_t tileId, const Tileset *, const Tileset *, const QList &palette); +QImage getGreyscaleTileImage(uint16_t tileId, const Tileset *, const Tileset *); void flattenTo4bppImage(QImage * image); diff --git a/include/ui/numericsorttableitem.h b/include/ui/numericsorttableitem.h new file mode 100644 index 00000000..e33f48cb --- /dev/null +++ b/include/ui/numericsorttableitem.h @@ -0,0 +1,20 @@ +#ifndef NUMERICSORTTABLEITEM_H +#define NUMERICSORTTABLEITEM_H + +#include +#include + +class NumericSortTableItem : public QTableWidgetItem +{ +public: + explicit NumericSortTableItem(const QString &text) : QTableWidgetItem(text) {}; + +protected: + virtual bool operator<(const QTableWidgetItem &other) const override { + QCollator collator; + collator.setNumericMode(true); + return collator.compare(text(), other.text()) < 0; + } +}; + +#endif // NUMERICSORTTABLEITEM_H diff --git a/include/ui/palettecolorsearch.h b/include/ui/palettecolorsearch.h new file mode 100644 index 00000000..967f6efa --- /dev/null +++ b/include/ui/palettecolorsearch.h @@ -0,0 +1,67 @@ +#ifndef PALETTECOLORSEARCH_H +#define PALETTECOLORSEARCH_H + +#include +#include +#include + +class Tileset; +class Project; + +namespace Ui { +class PaletteColorSearch; +} + +class PaletteColorSearch : public QDialog +{ + Q_OBJECT + +public: + explicit PaletteColorSearch(Project *project, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, + QWidget *parent = nullptr); + ~PaletteColorSearch(); + + void setPaletteId(int paletteId); + int currentPaletteId() const; + + void setColorId(int colorId); + int currentColorId() const; + + void setTilesets(const Tileset *primaryTileset, const Tileset *secondaryTileset); + const Tileset* currentTileset() const; + +signals: + void metatileSelected(uint16_t metatileId); + void paletteIdChanged(int paletteId); + +private: + struct RowData { + QString tilesetName; + QString pairedTilesetName; + QString metatileId; + QIcon metatileIcon; + }; + + enum ResultsColumn { + TilesetName, + Metatile, + }; + + Ui::PaletteColorSearch *ui; + Project *m_project; + const Tileset *m_primaryTileset; + const Tileset *m_secondaryTileset; + + QMap> m_resultsCache; + + void addTableEntry(const RowData &rowData); + QList search(int colorId) const; + QList search(int colorId, const Tileset *tileset, const Tileset *pairedTileset) const; + void refresh(); + void updateResults(); + void cellDoubleClicked(int row, int col); +}; + +#endif // PALETTECOLORSEARCH_H diff --git a/include/ui/paletteeditor.h b/include/ui/paletteeditor.h index 63581eb1..95fc80b8 100644 --- a/include/ui/paletteeditor.h +++ b/include/ui/paletteeditor.h @@ -2,10 +2,12 @@ #define PALETTEEDITOR_H #include +#include #include "colorinputwidget.h" #include "project.h" #include "history.h" +#include "palettecolorsearch.h" namespace Ui { class PaletteEditor; @@ -24,25 +26,39 @@ class PaletteEditor : public QMainWindow { public: explicit PaletteEditor(Project*, Tileset*, Tileset*, int paletteId, QWidget *parent = nullptr); ~PaletteEditor(); + void setPaletteId(int); + int currentPaletteId() const; + void setTilesets(Tileset*, Tileset*); + bool showingUnusedColors() const; + +signals: + void metatileSelected(uint16_t metatileId); + private: Ui::PaletteEditor *ui; - Project *project = nullptr; - QList colorInputs; - + Project *project; Tileset *primaryTileset; Tileset *secondaryTileset; + QList colorInputs; QList> palettesHistory; + QMap> unusedColorCache; + QPointer colorSearchWindow; - Tileset* getTileset(int paletteId); + Tileset* getTileset(int paletteId) const; void refreshColorInputs(); + void refreshPaletteId(); void commitEditHistory(); void commitEditHistory(int paletteId); void restoreWindowState(); + void invalidateCache(); void closeEvent(QCloseEvent*); + void setColorInputTitles(bool show); + QSet getUnusedColorIds(); + void openColorSearch(); void setRgb(int index, QRgb rgb); void setPalette(int paletteId, const QList &palette); @@ -50,14 +66,13 @@ private: void setBitDepth(int bits); int bitDepth = 24; - static const int numColors = 16; + static const int numColors = Tileset::numColorsPerPalette(); signals: void closed(); void changedPaletteColor(); void changedPalette(int); private slots: - void on_spinBox_PaletteId_valueChanged(int arg1); void on_actionUndo_triggered(); void on_actionRedo_triggered(); void on_actionImport_Palette_triggered(); diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index dc5b935a..1266da14 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -2,6 +2,7 @@ #define TILESETEDITOR_H #include +#include #include "project.h" #include "history.h" #include "paletteeditor.h" @@ -54,6 +55,9 @@ public: QObjectList shortcutableObjects() const; + void setPaletteId(int paletteId); + int paletteId() const; + public slots: void applyUserShortcuts(); void onSelectedMetatileChanged(uint16_t); @@ -65,13 +69,9 @@ private slots: void onHoveredTileChanged(const Tile&); void onHoveredTileChanged(uint16_t); void onHoveredTileCleared(); - void onSelectedTilesChanged(); void onMetatileLayerTileChanged(int, int); void onMetatileLayerSelectionChanged(QPoint, int, int); void onPaletteEditorChangedPaletteColor(); - void onPaletteEditorChangedPalette(int); - - void on_spinBox_paletteSelector_valueChanged(int arg1); void on_actionChange_Metatiles_Count_triggered(); @@ -136,23 +136,20 @@ private: void commitTerrainType(); void commitLayerType(); void setRawAttributesVisible(bool visible); - void setXFlip(bool enabled); - void setYFlip(bool enabled); + void refreshTileFlips(); + void refreshPaletteId(); Ui::TilesetEditor *ui; History metatileHistory; TilesetEditorMetatileSelector *metatileSelector = nullptr; TilesetEditorTileSelector *tileSelector = nullptr; MetatileLayersItem *metatileLayersItem = nullptr; - PaletteEditor *paletteEditor = nullptr; + QPointer paletteEditor = nullptr; Project *project = nullptr; Layout *layout = nullptr; Metatile *metatile = nullptr; Metatile *copiedMetatile = nullptr; QString copiedMetatileLabel; - int paletteId; - bool tileXFlip; - bool tileYFlip; bool hasUnsavedChanges; Tileset *primaryTileset = nullptr; Tileset *secondaryTileset = nullptr; diff --git a/include/ui/wildmonsearch.h b/include/ui/wildmonsearch.h index ccf0fe94..4fe608b8 100644 --- a/include/ui/wildmonsearch.h +++ b/include/ui/wildmonsearch.h @@ -2,24 +2,11 @@ #define WILDMONSEARCH_H #include -#include -#include + +#include "numericsorttableitem.h" class Project; -class NumericSortTableItem : public QTableWidgetItem -{ -public: - explicit NumericSortTableItem(const QString &text) : QTableWidgetItem(text) {}; - -protected: - virtual bool operator<(const QTableWidgetItem &other) const override { - QCollator collator; - collator.setNumericMode(true); - return collator.compare(text(), other.text()) < 0; - } -}; - namespace Ui { class WildMonSearch; } diff --git a/porymap.pro b/porymap.pro index 9c6327b7..6b2245c1 100644 --- a/porymap.pro +++ b/porymap.pro @@ -116,6 +116,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/montabwidget.cpp \ src/ui/encountertablemodel.cpp \ src/ui/encountertabledelegates.cpp \ + src/ui/palettecolorsearch.cpp \ src/ui/paletteeditor.cpp \ src/ui/selectablepixmapitem.cpp \ src/ui/tileseteditor.cpp \ @@ -234,6 +235,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/encountertablemodel.h \ include/ui/encountertabledelegates.h \ include/ui/adjustingstackedwidget.h \ + include/ui/palettecolorsearch.h \ include/ui/paletteeditor.h \ include/ui/selectablepixmapitem.h \ include/ui/tileseteditor.h \ @@ -287,6 +289,7 @@ FORMS += forms/mainwindow.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ + forms/palettecolorsearch.ui \ forms/paletteeditor.ui \ forms/regionmapeditor.ui \ forms/newmapdialog.ui \ diff --git a/src/config.cpp b/src/config.cpp index d5c5b5c9..b53f171b 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -346,6 +346,7 @@ void PorymapConfig::reset() { this->showTilesetEditorLayerGrid = true; this->showTilesetEditorDivider = false; this->showTilesetEditorRawAttributes = false; + this->showPaletteEditorUnusedColors = false; this->monitorFiles = true; this->tilesetCheckerboardFill = true; this->newMapHeaderSectionExpanded = false; @@ -469,6 +470,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->showTilesetEditorDivider = getConfigBool(key, value); } else if (key == "show_tileset_editor_raw_attributes") { this->showTilesetEditorRawAttributes = getConfigBool(key, value); + } else if (key == "show_palette_editor_unused_colors") { + this->showPaletteEditorUnusedColors = getConfigBool(key, value); } else if (key == "monitor_files") { this->monitorFiles = getConfigBool(key, value); } else if (key == "tileset_checkerboard_fill") { @@ -609,6 +612,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("show_tileset_editor_layer_grid", this->showTilesetEditorLayerGrid ? "1" : "0"); map.insert("show_tileset_editor_divider", this->showTilesetEditorDivider ? "1" : "0"); map.insert("show_tileset_editor_raw_attributes", this->showTilesetEditorRawAttributes ? "1" : "0"); + map.insert("show_palette_editor_unused_colors", this->showPaletteEditorUnusedColors ? "1" : "0"); map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); map.insert("new_map_header_section_expanded", this->newMapHeaderSectionExpanded ? "1" : "0"); diff --git a/src/core/events.cpp b/src/core/events.cpp index a4ab33a9..58a2b997 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -3,6 +3,7 @@ #include "eventframes.h" #include "project.h" #include "config.h" +#include "metatile.h" Event* Event::create(Event::Type type) { switch (type) { @@ -23,6 +24,14 @@ Event::~Event() { delete this->eventFrame; } +int Event::getPixelX() const { + return (this->x * Metatile::pixelWidth()) - qMax(0, (this->pixmap.width() - Metatile::pixelWidth()) / 2); +} + +int Event::getPixelY() const { + return (this->y * Metatile::pixelHeight()) - qMax(0, this->pixmap.height() - Metatile::pixelHeight()); +} + EventFrame *Event::getEventFrame() { if (!this->eventFrame) createEventFrame(); return this->eventFrame; diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index f6c0d382..00fff2ba 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -48,7 +48,7 @@ QString Metatile::getMetatileIdString(uint16_t metatileId) { return Util::toHexString(metatileId, numMetatileIdChars); }; -QString Metatile::getMetatileIdStrings(const QList metatileIds) { +QString Metatile::getMetatileIdStrings(const QList &metatileIds) { QStringList metatiles; for (auto metatileId : metatileIds) metatiles << Metatile::getMetatileIdString(metatileId); diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index 5f7ac20b..d76b0138 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -8,6 +8,7 @@ #include #include +#include Tileset::Tileset(const Tileset &other) @@ -110,7 +111,26 @@ int Tileset::maxTiles() const { return this->is_secondary ? Project::getNumTilesSecondary() : Project::getNumTilesPrimary(); } +Tileset* Tileset::getPaletteTileset(int paletteId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getPaletteTileset(paletteId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +const Tileset* Tileset::getPaletteTileset(int paletteId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + if (paletteId < Project::getNumPalettesPrimary()) { + return primaryTileset; + } else if (paletteId < Project::getNumPalettesTotal()) { + return secondaryTileset; + } else { + return nullptr; + } +} + Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getTileTileset(tileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +// Get the tileset *expected* to contain the given 'tileId'. Note that this does not mean the tile actually exists in that tileset. +const Tileset* Tileset::getTileTileset(int tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { if (tileId < Project::getNumTilesPrimary()) { return primaryTileset; } else if (tileId < Project::getNumTilesTotal()) { @@ -121,6 +141,11 @@ Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *s } Tileset* Tileset::getMetatileTileset(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getMetatileTileset(metatileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +// Get the tileset *expected* to contain the given 'metatileId'. Note that this does not mean the metatile actually exists in that tileset. +const Tileset* Tileset::getMetatileTileset(int metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { if (metatileId < Project::getNumMetatilesPrimary()) { return primaryTileset; } else if (metatileId < Project::getNumMetatilesTotal()) { @@ -131,7 +156,11 @@ Tileset* Tileset::getMetatileTileset(int metatileId, Tileset *primaryTileset, Ti } Metatile* Tileset::getMetatile(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset); + return const_cast(getMetatile(metatileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +const Metatile* Tileset::getMetatile(int metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + const Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset); if (!tileset) { return nullptr; } @@ -220,12 +249,12 @@ QString Tileset::getMetatileLabelPrefix(const QString &name) return QString("%1%2_").arg(labelPrefix).arg(Tileset::stripPrefix(name)); } -bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { +bool Tileset::metatileIsValid(uint16_t metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { return (primaryTileset && primaryTileset->contains(metatileId)) || (secondaryTileset && secondaryTileset->contains(metatileId)); } -QList> Tileset::getBlockPalettes(Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) { +QList> Tileset::getBlockPalettes(const Tileset *primaryTileset, const Tileset *secondaryTileset, bool useTruePalettes) { QList> palettes; QList> primaryPalettes; @@ -247,9 +276,9 @@ QList> Tileset::getBlockPalettes(Tileset *primaryTileset, Tileset *s return palettes; } -QList Tileset::getPalette(int paletteId, Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) { +QList Tileset::getPalette(int paletteId, const Tileset *primaryTileset, const Tileset *secondaryTileset, bool useTruePalettes) { QList paletteTable; - Tileset *tileset = paletteId < Project::getNumPalettesPrimary() + const Tileset *tileset = paletteId < Project::getNumPalettesPrimary() ? primaryTileset : secondaryTileset; if (!tileset) { @@ -334,7 +363,7 @@ bool Tileset::appendToGraphics(const QString &filepath, const QString &friendlyN dataString.append(QString("\t.incbin \"%1\"\n").arg(tilesPath)); } else { // Append to C file - dataString.append(QString("const u16 gTilesetPalettes_%1[][16] =\n{\n").arg(friendlyName)); + dataString.append(QString("const u16 gTilesetPalettes_%1[][%2] =\n{\n").arg(friendlyName).arg(Tileset::numColorsPerPalette())); for (int i = 0; i < Project::getNumPalettesTotal(); i++) dataString.append(QString(" INCBIN_U16(\"%1%2%3\"),\n").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0')).arg(palettesExt)); dataString.append("};\n"); @@ -546,13 +575,13 @@ bool Tileset::loadTilesImage(QImage *importedImage) { return false; } - // Validate image contains 16 colors. + // Validate the number of colors in the image. int colorCount = image.colorCount(); - if (colorCount > 16) { + if (colorCount > Tileset::numColorsPerPalette()) { flattenTo4bppImage(&image); - } else if (colorCount < 16) { + } else if (colorCount < Tileset::numColorsPerPalette()) { QVector colorTable = image.colorTable(); - for (int i = colorTable.length(); i < 16; i++) { + for (int i = colorTable.length(); i < Tileset::numColorsPerPalette(); i++) { colorTable.append(0); } image.setColorTable(colorTable); @@ -616,8 +645,9 @@ bool Tileset::loadPalettes() { // Either the palette failed to load, or no palette exists. // We expect tilesets to have a certain number of palettes, // so fill this palette with dummy colors. - for (int j = 0; j < 16; j++) { - palette.append(qRgb(j * 16, j * 16, j * 16)); + for (int j = 0; j < Tileset::numColorsPerPalette(); j++) { + int colorComponent = j * (256 / Tileset::numColorsPerPalette()); + palette.append(qRgb(colorComponent, colorComponent, colorComponent)); } } this->palettes.append(palette); @@ -630,7 +660,7 @@ bool Tileset::savePalettes() { bool success = true; int numPalettes = qMin(this->palettePaths.length(), this->palettes.length()); for (int i = 0; i < numPalettes; i++) { - if (!PaletteUtil::writeJASC(this->palettePaths.at(i), this->palettes.at(i).toVector(), 0, 16)) + if (!PaletteUtil::writeJASC(this->palettePaths.at(i), this->palettes.at(i).toVector(), 0, Tileset::numColorsPerPalette())) success = false; } return success; @@ -658,3 +688,85 @@ bool Tileset::save() { QString Tileset::stripPrefix(const QString &fullName) { return QString(fullName).replace(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix), ""); } + +// Find which of the specified color IDs in 'searchColors' are not used by any of this tileset's metatiles. +// The 'pairedTileset' may be used to get the tile images for any tiles that don't belong to this tileset. +// If 'searchColors' is empty, it will for search for all unused colors. +QSet Tileset::getUnusedColorIds(int paletteId, const Tileset *pairedTileset, const QSet &searchColors) const { + QSet unusedColors = searchColors; + if (unusedColors.isEmpty()) { + // Search for all colors + for (int i = 0; i < Tileset::numColorsPerPalette(); i++) { + unusedColors.insert(i); + } + } + const Tileset *primaryTileset = this->is_secondary ? pairedTileset : this; + const Tileset *secondaryTileset = this->is_secondary ? this : pairedTileset; + QSet seenTileIds; + for (const auto &metatile : m_metatiles) + for (const auto &tile : metatile->tiles) { + if (tile.palette != paletteId) + continue; + + // Save time by ignoring tiles we've already inspected. + if (seenTileIds.contains(tile.tileId)) + continue; + seenTileIds.insert(tile.tileId); + + QImage image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); + if (image.isNull() || image.sizeInBytes() < Tile::numPixels()) + continue; + + const uchar * pixels = image.constBits(); + for (int i = 0; i < Tile::numPixels(); i++) { + auto it = unusedColors.constFind(pixels[i]); + if (it != unusedColors.constEnd()) { + unusedColors.erase(it); + if (unusedColors.isEmpty()) { + return {}; + } + } + } + } + return unusedColors; +} + +// Returns the list of metatile IDs representing all the metatiles in this tileset that use the specified color ID. +QList Tileset::findMetatilesUsingColor(int paletteId, int colorId, const Tileset *pairedTileset) const { + const Tileset *primaryTileset = this->is_secondary ? pairedTileset : this; + const Tileset *secondaryTileset = this->is_secondary ? this : pairedTileset; + QSet metatileIdSet; + QHash tileContainsColor; + uint16_t metatileIdBase = firstMetatileId(); + for (int i = 0; i < m_metatiles.length(); i++) { + uint16_t metatileId = i + metatileIdBase; + for (const auto &tile : m_metatiles.at(i)->tiles) { + if (tile.palette != paletteId) + continue; + + // Save time on tiles we've already inspected by getting the cached result. + auto tileIt = tileContainsColor.constFind(tile.tileId); + if (tileIt != tileContainsColor.constEnd()) { + if (tileIt.value()) metatileIdSet.insert(metatileId); + continue; + } + tileContainsColor[tile.tileId] = false; + + QImage image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); + if (image.isNull() || image.sizeInBytes() < Tile::numPixels()) + continue; + + const uchar * pixels = image.constBits(); + for (int j = 0; j < Tile::numPixels(); j++) { + if (pixels[j] == colorId) { + metatileIdSet.insert(metatileId); + tileContainsColor[tile.tileId] = true; + break; + } + } + } + } + QList metatileIds(metatileIdSet.constBegin(), metatileIdSet.constEnd()); + std::sort(metatileIds.begin(), metatileIds.end()); + return metatileIds; +} diff --git a/src/core/utility.cpp b/src/core/utility.cpp index ff06838f..92cd1a4a 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -112,3 +112,16 @@ void Util::setErrorStylesheet(QLineEdit *lineEdit, bool isError) { static const QString stylesheet = QStringLiteral("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); lineEdit->setStyleSheet(isError ? stylesheet : ""); } + +void Util::show(QWidget *w) { + if (!w) return; + + if (!w->isVisible()) { + w->show(); + } else if (w->isMinimized()) { + w->showNormal(); + } else { + w->raise(); + w->activateWindow(); + } +} diff --git a/src/editor.cpp b/src/editor.cpp index 5f1dfe0f..b4f97576 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2060,12 +2060,6 @@ void Editor::updateCustomMapAttributes() map->modify(); } -Tileset* Editor::getCurrentMapPrimaryTileset() -{ - QString tilesetLabel = this->layout->tileset_primary_label; - return project->getTileset(tilesetLabel); -} - void Editor::redrawAllEvents() { if (this->map) redrawEvents(this->map->getEvents()); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index c191ea07..ac7c2385 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -345,7 +345,7 @@ void MainWindow::checkForUpdates(bool requestedByUser) { if (requestedByUser) { - openSubWindow(this->updatePromoter); + Util::show(this->updatePromoter); } else { // This is an automatic update check. Only run if we haven't done one in the last 5 minutes QDateTime lastCheck = porymapConfig.lastUpdateCheckTime; @@ -419,7 +419,7 @@ void MainWindow::initEditor() { } void MainWindow::openEditHistory() { - openSubWindow(this->undoView); + Util::show(this->undoView); } void MainWindow::initMiscHeapObjects() { @@ -936,19 +936,6 @@ void MainWindow::refreshRecentProjectsMenu() { clearAction->setEnabled(!recentProjects.isEmpty()); } -void MainWindow::openSubWindow(QWidget * window) { - if (!window) return; - - if (!window->isVisible()) { - window->show(); - } else if (window->isMinimized()) { - window->showNormal(); - } else { - window->raise(); - window->activateWindow(); - } -} - void MainWindow::showFileWatcherWarning() { if (!porymapConfig.monitorFiles || !isProjectOpen()) return; @@ -2266,7 +2253,7 @@ void MainWindow::on_actionGrid_Settings_triggered() { connect(this->gridSettingsDialog, &GridSettingsDialog::changedGridSettings, this->editor, &Editor::updateMapGrid); connect(this->gridSettingsDialog, &GridSettingsDialog::accepted, [this] { porymapConfig.gridSettings = this->editor->gridSettings; }); } - openSubWindow(this->gridSettingsDialog); + Util::show(this->gridSettingsDialog); } void MainWindow::on_actionShortcuts_triggered() @@ -2274,7 +2261,7 @@ void MainWindow::on_actionShortcuts_triggered() if (!shortcutsEditor) initShortcutsEditor(); - openSubWindow(shortcutsEditor); + Util::show(shortcutsEditor); } void MainWindow::initShortcutsEditor() { @@ -2698,7 +2685,7 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { } } - openSubWindow(this->mapImageExporter); + Util::show(this->mapImageExporter); } void MainWindow::on_pushButton_AddConnection_clicked() { @@ -2726,7 +2713,7 @@ void MainWindow::on_pushButton_SummaryChart_clicked() { connect(this->editor, &Editor::wildMonTableClosed, this->wildMonChart, &WildMonChart::clearTable); connect(this->editor, &Editor::wildMonTableEdited, this->wildMonChart, &WildMonChart::refresh); } - openSubWindow(this->wildMonChart); + Util::show(this->wildMonChart); } void MainWindow::on_toolButton_WildMonSearch_clicked() { @@ -2735,7 +2722,7 @@ void MainWindow::on_toolButton_WildMonSearch_clicked() { connect(this->wildMonSearch, &WildMonSearch::openWildMonTableRequested, this, &MainWindow::openWildMonTable); connect(this->editor, &Editor::wildMonTableEdited, this->wildMonSearch, &WildMonSearch::refresh); } - openSubWindow(this->wildMonSearch); + Util::show(this->wildMonSearch); } void MainWindow::openWildMonTable(const QString &mapName, const QString &groupName, const QString &fieldName) { @@ -2843,7 +2830,7 @@ void MainWindow::on_actionTileset_Editor_triggered() initTilesetEditor(); } - openSubWindow(this->tilesetEditor); + Util::show(this->tilesetEditor); MetatileSelection selection = this->editor->metatile_selector_item->getMetatileSelection(); if (!selection.metatileItems.isEmpty()) { @@ -2895,7 +2882,7 @@ void MainWindow::on_actionAbout_Porymap_triggered() { if (!this->aboutWindow) this->aboutWindow = new AboutPorymap(this); - openSubWindow(this->aboutWindow); + Util::show(this->aboutWindow); } void MainWindow::on_actionOpen_Log_File_triggered() { @@ -2927,7 +2914,7 @@ void MainWindow::on_actionPreferences_triggered() { connect(preferenceEditor, &PreferenceEditor::reloadProjectRequested, this, &MainWindow::on_action_Reload_Project_triggered); } - openSubWindow(preferenceEditor); + Util::show(preferenceEditor); } void MainWindow::togglePreferenceSpecificUi() { @@ -2944,7 +2931,7 @@ void MainWindow::openProjectSettingsEditor(int tab) { this, &MainWindow::on_action_Reload_Project_triggered); } this->projectSettingsEditor->setTab(tab); - openSubWindow(this->projectSettingsEditor); + Util::show(this->projectSettingsEditor); } void MainWindow::on_actionProject_Settings_triggered() { @@ -2982,7 +2969,7 @@ void MainWindow::on_actionCustom_Scripts_triggered() { if (!this->customScriptsEditor) initCustomScriptsEditor(); - openSubWindow(this->customScriptsEditor); + Util::show(this->customScriptsEditor); } void MainWindow::initCustomScriptsEditor() { @@ -3062,7 +3049,7 @@ void MainWindow::on_actionRegion_Map_Editor_triggered() { } } - openSubWindow(this->regionMapEditor); + Util::show(this->regionMapEditor); } void MainWindow::on_pushButton_CreatePrefab_clicked() { @@ -3120,7 +3107,7 @@ bool MainWindow::closeSupplementaryWindows() { if (widget != this && widget->isWindow()) { // Make sure the window is raised and activated before closing in case it has a confirmation prompt. if (widget->isVisible()) { - openSubWindow(widget); + Util::show(widget); } if (!widget->close()) { QString message = QStringLiteral("Aborted project close"); diff --git a/src/project.cpp b/src/project.cpp index b76821f3..9c5fd9af 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -186,6 +186,8 @@ bool Project::load() { this->parser.setUpdatesSplashScreen(true); resetFileWatcher(); resetFileCache(); + QPixmapCache::clear(); + this->disabledSettingsNames.clear(); bool success = readGlobalConstants() && readMapLayouts() @@ -273,6 +275,17 @@ void Project::clearTilesetCache() { this->tilesetCache.clear(); } +void Project::cacheTileset(const QString &name, Tileset *tileset) { + auto it = this->tilesetCache.constFind(name); + if (it != this->tilesetCache.constEnd() && it.value() && tileset != it.value()) { + // Callers of this function should ensure this doesn't happen, + // but in case it does we should avoid leaking memory. + logWarn(QString("New tileset %1 overwrote existing tileset.").arg(name)); + delete it.value(); + } + this->tilesetCache.insert(name, tileset); +} + Map* Project::loadMap(const QString &mapName) { if (mapName == getDynamicMapName()) { // Silently ignored, caller is expected to handle this if they want this to be an error. @@ -1180,7 +1193,21 @@ bool Project::loadLayoutTilesets(Layout *layout) { return layout->tileset_primary && layout->tileset_secondary; } -Tileset* Project::loadTileset(QString label, Tileset *tileset) { +Tileset* Project::getTileset(const QString &label, bool forceLoad) { + Tileset *tileset = nullptr; + + auto it = this->tilesetCache.constFind(label); + if (it != this->tilesetCache.constEnd()) { + tileset = it.value(); + if (!forceLoad) { + return tileset; + } + } else { + // Create a cache entry even if we don't end up loading the tileset successfully. + // This will prevent repeated file reads if the tileset fails to load. + cacheTileset(label, nullptr); + } + auto memberMap = Tileset::getHeaderMemberMap(this->usingAsmTilesets); if (this->usingAsmTilesets) { // Read asm tileset header. Backwards compatibility @@ -1225,7 +1252,7 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { return nullptr; } - tilesetCache.insert(label, tileset); + cacheTileset(tileset->name, tileset); return tileset; } @@ -1517,7 +1544,7 @@ void Project::readTilesetPaths(Tileset* tileset) { tileset->metatile_attrs_path = defaultPath + "/metatile_attributes.bin"; if (tileset->palettePaths.isEmpty()) { QString palettes_dir_path = defaultPath + "/palettes/"; - for (int i = 0; i < 16; i++) { + for (int i = 0; i < Tileset::maxPalettes(); i++) { tileset->palettePaths.append(palettes_dir_path + QString("%1").arg(i, 2, 10, QLatin1Char('0')) + ".pal"); } } @@ -1579,9 +1606,9 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa } // Create default palettes - for(int i = 0; i < 16; ++i) { + for(int i = 0; i < Tileset::maxPalettes(); ++i) { QList currentPal; - for(int i = 0; i < 16;++i) { + for(int i = 0; i < Tileset::numColorsPerPalette();++i) { currentPal.append(qRgb(0,0,0)); } tileset->palettes.append(currentPal); @@ -1617,15 +1644,14 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa metatilesFilepath.append(projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles)); } ignoreWatchedFilesTemporarily({headersFilepath, graphicsFilepath, metatilesFilepath}); - name = Tileset::stripPrefix(name); - tileset->appendToHeaders(headersFilepath, name, this->usingAsmTilesets); - tileset->appendToGraphics(graphicsFilepath, name, this->usingAsmTilesets); - tileset->appendToMetatiles(metatilesFilepath, name, this->usingAsmTilesets); + QString baseName = Tileset::stripPrefix(name); + tileset->appendToHeaders(headersFilepath, baseName, this->usingAsmTilesets); + tileset->appendToGraphics(graphicsFilepath, baseName, this->usingAsmTilesets); + tileset->appendToMetatiles(metatilesFilepath, baseName, this->usingAsmTilesets); tileset->save(); - this->tilesetCache.insert(tileset->name, tileset); - + cacheTileset(tileset->name, tileset); emit tilesetCreated(tileset); return tileset; } @@ -1680,20 +1706,6 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) { } } -Tileset* Project::getTileset(QString label, bool forceLoad) { - Tileset *existingTileset = nullptr; - if (tilesetCache.contains(label)) { - existingTileset = tilesetCache.value(label); - } - - if (existingTileset && !forceLoad) { - return existingTileset; - } else { - Tileset *tileset = loadTileset(label, existingTileset); - return tileset; - } -} - bool Project::saveTextFile(const QString &path, const QString &text) { QFile file(path); if (!file.open(QIODevice::WriteOnly)) { @@ -2266,6 +2278,7 @@ bool Project::readTilesetLabels() { this->primaryTilesetLabels.clear(); this->secondaryTilesetLabels.clear(); this->tilesetLabelsOrdered.clear(); + clearTilesetCache(); QString filename = projectConfig.getFilePath(ProjectFilePath::tilesets_headers); QFileInfo fileInfo(this->root + "/" + filename); @@ -2347,7 +2360,7 @@ bool Project::readFieldmapProperties() { logWarn(QString("Value for '%1' not found. Using default (%2) instead.").arg(name).arg(*dest)); } }; - loadDefine(numPalsTotalName, &Project::num_pals_total, 2, INT_MAX); // In reality the max would be 16, but as far as Porymap is concerned it doesn't matter. + loadDefine(numPalsTotalName, &Project::num_pals_total, 2, Tileset::maxPalettes()); loadDefine(numTilesTotalName, &Project::num_tiles_total, 2, 1024); // 1024 is fixed because we store tile IDs in a 10-bit field. loadDefine(numPalsPrimaryName, &Project::num_pals_primary, 1, Project::num_pals_total - 1); loadDefine(numTilesPrimaryName, &Project::num_tiles_primary, 1, Project::num_tiles_total - 1); @@ -3518,3 +3531,18 @@ bool Project::hasUnsavedChanges() { } return false; } + +// Searches the project's map layouts to find the names of the tilesets that the provided tileset gets paired with. +QSet Project::getPairedTilesetLabels(const Tileset *tileset) const { + QSet pairedLabels; + for (const auto &layout : this->mapLayouts) { + if (tileset->is_secondary) { + if (layout->tileset_secondary_label == tileset->name) { + pairedLabels.insert(layout->tileset_primary_label); + } + } else if (layout->tileset_primary_label == tileset->name) { + pairedLabels.insert(layout->tileset_secondary_label); + } + } + return pairedLabels; +} diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index a1730791..472dd23a 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -346,10 +346,7 @@ void MainWindow::setTilesetPalette(Tileset *tileset, int paletteIndex, QList= tileset->palettes.size()) return; - if (colors.size() != 16) - return; - - for (int i = 0; i < 16; i++) { + for (int i = 0; i < qMin(colors.length(), Tileset::numColorsPerPalette()); i++) { if (colors[i].size() != 3) continue; tileset->palettes[paletteIndex][i] = qRgb(colors[i][0], colors[i][1], colors[i][2]); @@ -457,10 +454,7 @@ void MainWindow::setTilesetPalettePreview(Tileset *tileset, int paletteIndex, QL return; if (paletteIndex >= tileset->palettePreviews.size()) return; - if (colors.size() != 16) - return; - - for (int i = 0; i < 16; i++) { + for (int i = 0; i < qMin(colors.length(), Tileset::numColorsPerPalette()); i++) { if (colors[i].size() != 3) continue; tileset->palettePreviews[paletteIndex][i] = qRgb(colors[i][0], colors[i][1], colors[i][2]); @@ -798,14 +792,13 @@ QJSValue MainWindow::getTilePixels(int tileId) { if (tileId < 0 || !this->editor || !this->editor->layout) return QJSValue(); - const int numPixels = Tile::pixelWidth() * Tile::pixelHeight(); QImage tileImage = getTileImage(tileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (tileImage.isNull() || tileImage.sizeInBytes() < numPixels) + if (tileImage.isNull() || tileImage.sizeInBytes() < Tile::numPixels()) return QJSValue(); const uchar * pixels = tileImage.constBits(); - QJSValue pixelArray = Scripting::getEngine()->newArray(numPixels); - for (int i = 0; i < numPixels; i++) { + QJSValue pixelArray = Scripting::getEngine()->newArray(Tile::numPixels()); + for (int i = 0; i < Tile::numPixels(); i++) { pixelArray.setProperty(i, pixels[i]); } return pixelArray; diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index ce233066..5efa1e91 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -12,14 +12,14 @@ QImage getCollisionMetatileImage(int collision, int elevation) { return image ? *image : QImage(); } -QImage getMetatileImage(uint16_t metatileId, Layout *layout, bool useTruePalettes) { +QImage getMetatileImage(uint16_t metatileId, const Layout *layout, bool useTruePalettes) { Metatile* metatile = Tileset::getMetatile(metatileId, layout ? layout->tileset_primary : nullptr, layout ? layout->tileset_secondary : nullptr); return getMetatileImage(metatile, layout, useTruePalettes); } -QImage getMetatileImage(Metatile *metatile, Layout *layout, bool useTruePalettes) { +QImage getMetatileImage(const Metatile *metatile, const Layout *layout, bool useTruePalettes) { if (!layout) { return getMetatileImage(metatile, nullptr, nullptr, {}, {}, useTruePalettes); } @@ -33,8 +33,8 @@ QImage getMetatileImage(Metatile *metatile, Layout *layout, bool useTruePalettes QImage getMetatileImage( uint16_t metatileId, - Tileset *primaryTileset, - Tileset *secondaryTileset, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, const QList &layerOrder, const QList &layerOpacity, bool useTruePalettes) @@ -54,9 +54,9 @@ QColor getInvalidImageColor() { } QImage getMetatileImage( - Metatile *metatile, - Tileset *primaryTileset, - Tileset *secondaryTileset, + const Metatile *metatile, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, const QList &layerOrder, const QList &layerOpacity, bool useTruePalettes) @@ -141,12 +141,12 @@ QImage getMetatileImage( return metatileImage; } -QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - Tileset *tileset = Tileset::getTileTileset(tileId, primaryTileset, secondaryTileset); +QImage getTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + const Tileset *tileset = Tileset::getTileTileset(tileId, primaryTileset, secondaryTileset); return tileset ? tileset->tileImage(tileId) : QImage(); } -QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette) { +QImage getColoredTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset, const QList &palette) { QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset); if (tileImage.isNull()) { // Some tiles specify tile IDs or palette IDs that are outside the valid range. @@ -155,19 +155,19 @@ QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *se tileImage = QImage(Tile::pixelSize(), QImage::Format_RGBA8888); tileImage.fill(getInvalidImageColor()); } else { - for (int i = 0; i < 16; i++) { + for (int i = 0; i < Tileset::numColorsPerPalette(); i++) { tileImage.setColor(i, palette.value(i, getInvalidImageColor().rgb())); } } return tileImage; } -QImage getPalettedTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, int paletteId, bool useTruePalettes) { +QImage getPalettedTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset, int paletteId, bool useTruePalettes) { QList palette = Tileset::getPalette(paletteId, primaryTileset, secondaryTileset, useTruePalettes); return getColoredTileImage(tileId, primaryTileset, secondaryTileset, palette); } -QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { +QImage getGreyscaleTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { return getColoredTileImage(tileId, primaryTileset, secondaryTileset, greyscalePalette); } @@ -181,8 +181,8 @@ void flattenTo4bppImage(QImage * image) { } // Constructs a grid image of the metatiles in the specified ID range. -QImage getMetatileSheetImage(Tileset *primaryTileset, - Tileset *secondaryTileset, +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, uint16_t metatileIdStart, uint16_t metatileIdEnd, int numMetatilesWide, @@ -219,15 +219,15 @@ QImage getMetatileSheetImage(Tileset *primaryTileset, // Constructs a grid image of the metatiles in the primary and secondary tileset, // rounding as necessary to keep the two tilesets on separate rows. // The unused metatiles (if any) between the primary and secondary tilesets are skipped. -QImage getMetatileSheetImage(Tileset *primaryTileset, - Tileset *secondaryTileset, +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, int numMetatilesWide, const QList &layerOrder, const QList &layerOpacity, const QSize &metatileSize, bool useTruePalettes) { - auto createSheetImage = [=](uint16_t start, Tileset *tileset) { + auto createSheetImage = [=](uint16_t start, const Tileset *tileset) { uint16_t end = start; if (tileset) { if (tileset->numMetatiles() == 0) @@ -259,7 +259,7 @@ QImage getMetatileSheetImage(Tileset *primaryTileset, return image; } -QImage getMetatileSheetImage(Layout *layout, int numMetatilesWide, bool useTruePalettes) { +QImage getMetatileSheetImage(const Layout *layout, int numMetatilesWide, bool useTruePalettes) { if (!layout) return QImage(); return getMetatileSheetImage(layout->tileset_primary, diff --git a/src/ui/palettecolorsearch.cpp b/src/ui/palettecolorsearch.cpp new file mode 100644 index 00000000..e1f47a90 --- /dev/null +++ b/src/ui/palettecolorsearch.cpp @@ -0,0 +1,207 @@ +#include "palettecolorsearch.h" +#include "ui_palettecolorsearch.h" +#include "project.h" +#include "tileset.h" +#include "imageproviders.h" +#include "eventfilters.h" +#include "log.h" +#include "numericsorttableitem.h" + +enum ResultsDataRole { + PairedTilesetName = Qt::UserRole, +}; + +PaletteColorSearch::PaletteColorSearch(Project *project, const Tileset *primaryTileset, const Tileset *secondaryTileset, QWidget *parent) : + QDialog(parent), + ui(new Ui::PaletteColorSearch), + m_project(project), + m_primaryTileset(primaryTileset), + m_secondaryTileset(secondaryTileset) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + + ui->buttonBox->setVisible(isModal()); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); + + // Rather than try to keep track of metatile/tile changes that affect which colors are used, + // we'll just refresh when the window is activated. + ActiveWindowFilter *filter = new ActiveWindowFilter(this); + connect(filter, &ActiveWindowFilter::activated, this, &PaletteColorSearch::refresh); + this->installEventFilter(filter); + + ui->spinBox_ColorId->setRange(0, Tileset::numColorsPerPalette() - 1); + connect(ui->spinBox_ColorId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::updateResults); + + ui->spinBox_PaletteId->setRange(0, Project::getNumPalettesTotal() - 1); + connect(ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::updateResults); + connect(ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::paletteIdChanged); + + // Set up table header + static const QStringList labels = {"Tileset", "Metatile"}; + ui->table_Results->setHorizontalHeaderLabels(labels); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::TilesetName, QHeaderView::ResizeToContents); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Metatile, QHeaderView::Stretch); + + // Table is read-only + ui->table_Results->setEditTriggers(QAbstractItemView::NoEditTriggers); + ui->table_Results->setSelectionMode(QAbstractItemView::NoSelection); + + connect(ui->table_Results, &QTableWidget::cellDoubleClicked, this, &PaletteColorSearch::cellDoubleClicked); +} + +PaletteColorSearch::~PaletteColorSearch() { + delete ui; +} + +void PaletteColorSearch::setPaletteId(int paletteId) { + ui->spinBox_PaletteId->setValue(paletteId); +} + +int PaletteColorSearch::currentPaletteId() const { + return ui->spinBox_PaletteId->value(); +} + +void PaletteColorSearch::setColorId(int colorId) { + ui->spinBox_ColorId->setValue(colorId); +} + +int PaletteColorSearch::currentColorId() const { + return ui->spinBox_ColorId->value(); +} + +void PaletteColorSearch::setTilesets(const Tileset *primaryTileset, const Tileset *secondaryTileset) { + m_primaryTileset = primaryTileset; + m_secondaryTileset = secondaryTileset; + refresh(); +} + +const Tileset* PaletteColorSearch::currentTileset() const { + return Tileset::getPaletteTileset(currentPaletteId(), m_primaryTileset, m_secondaryTileset); +} + +void PaletteColorSearch::addTableEntry(const RowData &rowData) { + int row = ui->table_Results->rowCount(); + ui->table_Results->insertRow(row); + + auto tilesetNameItem = new NumericSortTableItem(rowData.tilesetName); + tilesetNameItem->setData(ResultsDataRole::PairedTilesetName, rowData.pairedTilesetName); + + ui->table_Results->setItem(row, ResultsColumn::TilesetName, tilesetNameItem); + ui->table_Results->setItem(row, ResultsColumn::Metatile, new QTableWidgetItem(rowData.metatileIcon, rowData.metatileId)); +} + +QList PaletteColorSearch::search(int colorId) const { + QList results; + + // Check our current tilesets for color usage. + results.append(search(colorId, m_primaryTileset, m_secondaryTileset)); + results.append(search(colorId, m_secondaryTileset, m_primaryTileset)); + + // The current palette comes from either the primary or secondary tileset. + // We need to check all the other tilesets that are paired with the tileset that owns this palette. + const Tileset *paletteTileset = currentTileset(); + QSet tilesetsToSearch = m_project->getPairedTilesetLabels(paletteTileset); + + // We exclude the currently-loaded pair (we already checked them, and because they're being + // edited in the Tileset Editor they may differ from their copies saved in the layout). + tilesetsToSearch.remove(m_primaryTileset->name); + tilesetsToSearch.remove(m_secondaryTileset->name); + + for (const auto &label : tilesetsToSearch) { + Tileset *searchTileset = m_project->getTileset(label); + if (searchTileset) { + results.append(search(colorId, searchTileset, paletteTileset)); + } + } + return results; +} + +QList PaletteColorSearch::search(int colorId, const Tileset *tileset, const Tileset *pairedTileset) const { + QList results; + QList metatileIds = tileset->findMetatilesUsingColor(currentPaletteId(), colorId, pairedTileset); + auto primaryTileset = tileset->is_secondary ? pairedTileset : tileset; + auto secondaryTileset = tileset->is_secondary ? tileset : pairedTileset; + for (const auto &metatileId : metatileIds) { + QImage metatileImage = getMetatileImage(metatileId, primaryTileset, secondaryTileset); + RowData rowData = { + .tilesetName = tileset->name, + .pairedTilesetName = pairedTileset->name, + .metatileId = Metatile::getMetatileIdString(metatileId), + .metatileIcon = QIcon(QPixmap::fromImage(metatileImage)), + }; + results.append(rowData); + } + return results; +} + +void PaletteColorSearch::refresh() { + m_resultsCache.clear(); + updateResults(); +} + +void PaletteColorSearch::updateResults() { + const Tileset *tileset = currentTileset(); + int paletteId = currentPaletteId(); + int colorId = currentColorId(); + + // Update color icon + QRgb color = tileset->palettePreviews.value(paletteId).value(colorId); + ui->frame_Color->setStyleSheet(QString("background-color: rgb(%1, %2, %3);").arg(qRed(color)).arg(qGreen(color)).arg(qBlue(color))); + + // Update title + ui->label_Title->setText(QString("Searching for usage of %1's palette %2.").arg(tileset->name).arg(paletteId)); + + // Update table + ui->table_Results->clearContents(); + ui->table_Results->setRowCount(0); + + QString cacheKey = QString("%1#%2").arg(paletteId).arg(colorId); + auto it = m_resultsCache.constFind(cacheKey); + bool inCache = (it != m_resultsCache.constEnd()); + const QList results = inCache ? it.value() : search(colorId); + + if (results.isEmpty()) { + static const RowData noResults = { + .tilesetName = QStringLiteral("This color is unused."), + .pairedTilesetName = "", + .metatileId = QStringLiteral("--"), + .metatileIcon = QIcon(), + }; + addTableEntry(noResults); + } else { + for (const auto &entry : results) { + addTableEntry(entry); + } + } + + ui->table_Results->sortByColumn(ResultsColumn::TilesetName, Qt::AscendingOrder); + + if (!inCache) m_resultsCache.insert(cacheKey, results); +} + +// Double-clicking row data selects the corresponding metatile in the Tileset Editor. +void PaletteColorSearch::cellDoubleClicked(int row, int) { + auto tilesetNameItem = ui->table_Results->item(row, ResultsColumn::TilesetName); + auto metatileItem = ui->table_Results->item(row, ResultsColumn::Metatile); + if (!tilesetNameItem || !metatileItem) + return; + + // The Tileset Editor (as of writing) has no way to change the selected tilesets independently of + // the main editor's layout, so if the metatile is not in the current tileset we do nothing. + // To compare the tileset names, rather than sort out which was the primary or secondary we + // just make sure it's the same set of names. + QSet currentTilesets; + currentTilesets.insert(m_primaryTileset->name); + currentTilesets.insert(m_secondaryTileset->name); + + QSet metatileTilesets; + metatileTilesets.insert(tilesetNameItem->text()); + metatileTilesets.insert(tilesetNameItem->data(ResultsDataRole::PairedTilesetName).toString()); + if (currentTilesets != metatileTilesets) + return; + + bool ok; + uint16_t metatileId = metatileItem->text().toUInt(&ok, 0); + if (ok) emit metatileSelected(metatileId); +} diff --git a/src/ui/paletteeditor.cpp b/src/ui/paletteeditor.cpp index 1078ecc6..571185d7 100644 --- a/src/ui/paletteeditor.cpp +++ b/src/ui/paletteeditor.cpp @@ -5,23 +5,23 @@ #include "log.h" #include "filedialog.h" #include "message.h" +#include "eventfilters.h" +#include "utility.h" PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset *secondaryTileset, int paletteId, QWidget *parent) : QMainWindow(parent), - ui(new Ui::PaletteEditor) + ui(new Ui::PaletteEditor), + project(project), + primaryTileset(primaryTileset), + secondaryTileset(secondaryTileset) { - this->project = project; - this->primaryTileset = primaryTileset; - this->secondaryTileset = secondaryTileset; + setAttribute(Qt::WA_DeleteOnClose); this->ui->setupUi(this); - this->ui->spinBox_PaletteId->setMinimum(0); - this->ui->spinBox_PaletteId->setMaximum(Project::getNumPalettesTotal() - 1); - this->colorInputs.clear(); const int numColorsPerRow = 4; for (int i = 0; i < this->numColors; i++) { - auto colorInput = new ColorInputWidget(QString("Color %1").arg(i)); + auto colorInput = new ColorInputWidget; connect(colorInput, &ColorInputWidget::colorChanged, [this, i](QRgb color) { setRgb(i, color); }); connect(colorInput, &ColorInputWidget::editingFinished, [this] { commitEditHistory(); }); this->colorInputs.append(colorInput); @@ -42,23 +42,48 @@ PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset setBitDepth(bitDepth); // Connect bit depth buttons - connect(this->ui->bit_depth_15, &QRadioButton::toggled, [this](bool checked){ if (checked) this->setBitDepth(15); }); - connect(this->ui->bit_depth_24, &QRadioButton::toggled, [this](bool checked){ if (checked) this->setBitDepth(24); }); + connect(this->ui->bit_depth_15, &QRadioButton::toggled, [this](bool checked){ if (checked) setBitDepth(15); }); + connect(this->ui->bit_depth_24, &QRadioButton::toggled, [this](bool checked){ if (checked) setBitDepth(24); }); - this->setPaletteId(paletteId); - this->commitEditHistory(); - this->restoreWindowState(); + this->ui->actionShow_Unused_Colors->setChecked(porymapConfig.showPaletteEditorUnusedColors); + connect(this->ui->actionShow_Unused_Colors, &QAction::toggled, this, &PaletteEditor::setColorInputTitles); + + connect(this->ui->toolButton_ColorSearch, &QToolButton::clicked, this, &PaletteEditor::openColorSearch); + connect(this->ui->actionFind_Color_Usage, &QAction::triggered, this, &PaletteEditor::openColorSearch); + + // Rather than try to keep track of metatile/tile changes that affect which colors are used, + // we'll just refresh when the window is activated. + ActiveWindowFilter *filter = new ActiveWindowFilter(this); + connect(filter, &ActiveWindowFilter::activated, this, &PaletteEditor::invalidateCache); + this->installEventFilter(filter); + + this->ui->spinBox_PaletteId->setRange(0, Project::getNumPalettesTotal() - 1); + this->ui->spinBox_PaletteId->setValue(paletteId); + connect(this->ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteEditor::refreshPaletteId); + connect(this->ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteEditor::changedPalette); + + refreshPaletteId(); + restoreWindowState(); } -PaletteEditor::~PaletteEditor() -{ +PaletteEditor::~PaletteEditor() { delete ui; } -Tileset* PaletteEditor::getTileset(int paletteId) { - return (paletteId < Project::getNumPalettesPrimary()) - ? this->primaryTileset - : this->secondaryTileset; +int PaletteEditor::currentPaletteId() const { + return ui->spinBox_PaletteId->value(); +} + +void PaletteEditor::setPaletteId(int paletteId) { + ui->spinBox_PaletteId->setValue(paletteId); +} + +bool PaletteEditor::showingUnusedColors() const { + return ui->actionShow_Unused_Colors->isChecked(); +} + +Tileset* PaletteEditor::getTileset(int paletteId) const { + return Tileset::getPaletteTileset(paletteId, this->primaryTileset, this->secondaryTileset); } void PaletteEditor::setBitDepth(int bits) { @@ -70,62 +95,63 @@ void PaletteEditor::setBitDepth(int bits) { } void PaletteEditor::setRgb(int colorIndex, QRgb rgb) { - const int paletteId = this->ui->spinBox_PaletteId->value(); - + const int paletteId = currentPaletteId(); Tileset *tileset = getTileset(paletteId); tileset->palettes[paletteId][colorIndex] = rgb; tileset->palettePreviews[paletteId][colorIndex] = rgb; - emit changedPaletteColor(); } void PaletteEditor::setPalette(int paletteId, const QList &palette) { Tileset *tileset = getTileset(paletteId); for (int i = 0; i < this->numColors; i++) { - tileset->palettes[paletteId][i] = palette.at(i); - tileset->palettePreviews[paletteId][i] = palette.at(i); + tileset->palettes[paletteId][i] = palette.value(i); + tileset->palettePreviews[paletteId][i] = palette.value(i); } refreshColorInputs(); emit changedPaletteColor(); } void PaletteEditor::refreshColorInputs() { - const int paletteId = ui->spinBox_PaletteId->value(); + const int paletteId = currentPaletteId(); Tileset *tileset = getTileset(paletteId); - for (int i = 0; i < this->numColors; i++) { + for (int i = 0; i < this->colorInputs.length(); i++) { auto colorInput = this->colorInputs.at(i); const QSignalBlocker b(colorInput); - colorInput->setColor(tileset->palettes.at(paletteId).at(i)); + colorInput->setColor(tileset->palettes.value(paletteId).value(i)); } + setColorInputTitles(showingUnusedColors()); } -void PaletteEditor::setPaletteId(int paletteId) { - const QSignalBlocker b(ui->spinBox_PaletteId); - this->ui->spinBox_PaletteId->setValue(paletteId); - this->refreshColorInputs(); +void PaletteEditor::refreshPaletteId() { + refreshColorInputs(); + + int paletteId = currentPaletteId(); + if (!this->palettesHistory[paletteId].current()) { + commitEditHistory(paletteId); + } + if (this->colorSearchWindow) { + this->colorSearchWindow->setPaletteId(paletteId); + } } void PaletteEditor::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; - this->refreshColorInputs(); -} - -void PaletteEditor::on_spinBox_PaletteId_valueChanged(int paletteId) { - this->refreshColorInputs(); - if (!this->palettesHistory[paletteId].current()) { - this->commitEditHistory(paletteId); + invalidateCache(); + if (this->colorSearchWindow) { + this->colorSearchWindow->setTilesets(primaryTileset, secondaryTileset); } - emit this->changedPalette(paletteId); + refreshColorInputs(); } void PaletteEditor::commitEditHistory() { - commitEditHistory(ui->spinBox_PaletteId->value()); + commitEditHistory(currentPaletteId()); } void PaletteEditor::commitEditHistory(int paletteId) { QList colors; - for (int i = 0; i < this->numColors; i++) { + for (int i = 0; i < this->colorInputs.length(); i++) { colors.append(this->colorInputs.at(i)->color()); } PaletteHistoryItem *commit = new PaletteHistoryItem(colors); @@ -135,28 +161,25 @@ void PaletteEditor::commitEditHistory(int paletteId) { void PaletteEditor::restoreWindowState() { logInfo("Restoring palette editor geometry from previous session."); QMap geometry = porymapConfig.getPaletteEditorGeometry(); - this->restoreGeometry(geometry.value("palette_editor_geometry")); - this->restoreState(geometry.value("palette_editor_state")); + restoreGeometry(geometry.value("palette_editor_geometry")); + restoreState(geometry.value("palette_editor_state")); } -void PaletteEditor::on_actionUndo_triggered() -{ - int paletteId = this->ui->spinBox_PaletteId->value(); +void PaletteEditor::on_actionUndo_triggered() { + int paletteId = currentPaletteId(); PaletteHistoryItem *prev = this->palettesHistory[paletteId].back(); if (prev) setPalette(paletteId, prev->colors); } -void PaletteEditor::on_actionRedo_triggered() -{ - int paletteId = this->ui->spinBox_PaletteId->value(); +void PaletteEditor::on_actionRedo_triggered() { + int paletteId = currentPaletteId(); PaletteHistoryItem *next = this->palettesHistory[paletteId].next(); if (next) setPalette(paletteId, next->colors); } -void PaletteEditor::on_actionImport_Palette_triggered() -{ +void PaletteEditor::on_actionImport_Palette_triggered() { QString filepath = FileDialog::getOpenFileName(this, "Import Tileset Palette", "", "Palette Files (*.pal *.act *tpl *gpl)"); if (filepath.isEmpty()) { return; @@ -171,14 +194,83 @@ void PaletteEditor::on_actionImport_Palette_triggered() palette.append(0); } - const int paletteId = ui->spinBox_PaletteId->value(); + const int paletteId = currentPaletteId(); setPalette(paletteId, palette); commitEditHistory(paletteId); } +void PaletteEditor::openColorSearch() { + if (!this->colorSearchWindow) { + this->colorSearchWindow = new PaletteColorSearch(this->project, this->primaryTileset, this->secondaryTileset, this); + this->colorSearchWindow->setPaletteId(currentPaletteId()); + connect(this->colorSearchWindow, &PaletteColorSearch::metatileSelected, this, &PaletteEditor::metatileSelected); + connect(this->colorSearchWindow, &PaletteColorSearch::paletteIdChanged, this, &PaletteEditor::setPaletteId); + } + Util::show(this->colorSearchWindow); +} + +void PaletteEditor::invalidateCache() { + this->unusedColorCache.clear(); + if (showingUnusedColors()) { + setColorInputTitles(true); + } +} + +QSet PaletteEditor::getUnusedColorIds() { + const int paletteId = currentPaletteId(); + + if (this->unusedColorCache.contains(paletteId)) { + return this->unusedColorCache.value(paletteId); + } + this->unusedColorCache[paletteId] = {}; + + // Check our current tilesets for color usage. + QSet unusedColorIds = this->primaryTileset->getUnusedColorIds(paletteId, this->secondaryTileset); + if (unusedColorIds.isEmpty()) + return {}; + unusedColorIds = this->secondaryTileset->getUnusedColorIds(paletteId, this->primaryTileset, unusedColorIds); + if (unusedColorIds.isEmpty()) + return {}; + + // The current palette comes from either the primary or secondary tileset. + // We need to check all the other tilesets that are paired with the tileset that owns this palette. + Tileset *paletteTileset = getTileset(paletteId); + QSet tilesetsToSearch = this->project->getPairedTilesetLabels(paletteTileset); + + // We exclude the currently-loaded pair (we already checked them, and because they're being + // edited in the Tileset Editor they may differ from their copies saved in the layout). + tilesetsToSearch.remove(this->primaryTileset->name); + tilesetsToSearch.remove(this->secondaryTileset->name); + + for (const auto &label : tilesetsToSearch) { + Tileset *searchTileset = this->project->getTileset(label); + if (!searchTileset) continue; + unusedColorIds = searchTileset->getUnusedColorIds(paletteId, paletteTileset, unusedColorIds); + if (unusedColorIds.isEmpty()) + return {}; + } + + this->unusedColorCache[paletteId] = unusedColorIds; + return unusedColorIds; +} + +void PaletteEditor::setColorInputTitles(bool showUnused) { + porymapConfig.showPaletteEditorUnusedColors = showUnused; + + QSet unusedColorIds = showUnused ? getUnusedColorIds() : QSet(); + ui->label_AllColorsUsed->setVisible(showUnused && unusedColorIds.isEmpty()); + for (int i = 0; i < this->colorInputs.length(); i++) { + QString title = QString("Color %1").arg(i); + if (unusedColorIds.contains(i)) { + title.append(QStringLiteral(" (Unused)")); + } + this->colorInputs.at(i)->setTitle(title); + } +} + void PaletteEditor::closeEvent(QCloseEvent*) { porymapConfig.setPaletteEditorGeometry( - this->saveGeometry(), - this->saveState() + saveGeometry(), + saveState() ); } diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 4df24065..b676c9ee 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -26,14 +26,23 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); + ui->spinBox_paletteSelector->setRange(0, Project::getNumPalettesTotal() - 1); + + auto validator = new IdentifierValidator(this); + validator->setAllowEmpty(true); + ui->lineEdit_metatileLabel->setValidator(validator); + + ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); + ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); + + ActiveWindowFilter *filter = new ActiveWindowFilter(this); + connect(filter, &ActiveWindowFilter::activated, this, &TilesetEditor::onWindowActivated); + this->installEventFilter(filter); + 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); - - this->tileXFlip = ui->checkBox_xFlip->isChecked(); - this->tileYFlip = ui->checkBox_yFlip->isChecked(); - this->paletteId = ui->spinBox_paletteSelector->value(); + connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::refreshTileFlips); + connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::refreshTileFlips); connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); @@ -51,19 +60,7 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) connect(ui->actionExport_Metatiles_Image, &QAction::triggered, [this] { exportMetatilesImage(); }); - ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); - ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); - - ui->spinBox_paletteSelector->setMinimum(0); - ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); - - auto validator = new IdentifierValidator(this); - validator->setAllowEmpty(true); - ui->lineEdit_metatileLabel->setValidator(validator); - - ActiveWindowFilter *filter = new ActiveWindowFilter(this); - connect(filter, &ActiveWindowFilter::activated, this, &TilesetEditor::onWindowActivated); - this->installEventFilter(filter); + connect(ui->spinBox_paletteSelector, QOverload::of(&QSpinBox::valueChanged), this, &TilesetEditor::refreshPaletteId); initAttributesUi(); initMetatileSelector(); @@ -131,11 +128,11 @@ void TilesetEditor::setTilesets(QString primaryTilesetLabel, QString secondaryTi this->metatileReloadQueue.clear(); Tileset *primaryTileset = project->getTileset(primaryTilesetLabel); Tileset *secondaryTileset = project->getTileset(secondaryTilesetLabel); - if (this->primaryTileset) delete this->primaryTileset; - if (this->secondaryTileset) delete this->secondaryTileset; + delete this->primaryTileset; + delete this->secondaryTileset; this->primaryTileset = new Tileset(*primaryTileset); this->secondaryTileset = new Tileset(*secondaryTileset); - if (paletteEditor) paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); + if (this->paletteEditor) this->paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); this->initMetatileHistory(); } @@ -265,10 +262,8 @@ void TilesetEditor::initTileSelector() connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileChanged, [this](uint16_t tileId) { onHoveredTileChanged(tileId); }); - connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileCleared, - this, &TilesetEditor::onHoveredTileCleared); - connect(this->tileSelector, &TilesetEditorTileSelector::selectedTilesChanged, - this, &TilesetEditor::onSelectedTilesChanged); + connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); + connect(this->tileSelector, &TilesetEditorTileSelector::selectedTilesChanged, this, &TilesetEditor::drawSelectedTiles); this->tileSelector->showDivider = this->ui->actionShow_Tileset_Divider->isChecked(); @@ -475,10 +470,6 @@ void TilesetEditor::onHoveredTileCleared() { this->ui->statusbar->clearMessage(); } -void TilesetEditor::onSelectedTilesChanged() { - this->drawSelectedTiles(); -} - void TilesetEditor::onMetatileLayerTileChanged(int x, int y) { static const QList tileCoords = QList{ QPoint(0, 0), @@ -543,39 +534,32 @@ void TilesetEditor::onMetatileLayerSelectionChanged(QPoint selectionOrigin, int this->tileSelector->setExternalSelection(width, height, tiles, tileIdxs); if (width == 1 && height == 1) { - ui->spinBox_paletteSelector->setValue(tiles[0].palette); + setPaletteId(tiles[0].palette); this->tileSelector->highlight(static_cast(tiles[0].tileId)); this->redrawTileSelector(); } this->metatileLayersItem->clearLastModifiedCoords(); } -void TilesetEditor::on_spinBox_paletteSelector_valueChanged(int paletteId) -{ - this->ui->spinBox_paletteSelector->blockSignals(true); - this->ui->spinBox_paletteSelector->setValue(paletteId); - this->ui->spinBox_paletteSelector->blockSignals(false); - this->paletteId = paletteId; - this->tileSelector->setPaletteId(paletteId); +void TilesetEditor::setPaletteId(int paletteId) { + ui->spinBox_paletteSelector->setValue(paletteId); +} + +int TilesetEditor::paletteId() const { + return ui->spinBox_paletteSelector->value(); +} + +void TilesetEditor::refreshPaletteId() { + this->tileSelector->setPaletteId(paletteId()); this->drawSelectedTiles(); if (this->paletteEditor) { - this->paletteEditor->setPaletteId(paletteId); + this->paletteEditor->setPaletteId(paletteId()); } this->metatileLayersItem->clearLastModifiedCoords(); } -void TilesetEditor::setXFlip(bool enabled) -{ - this->tileXFlip = enabled; - this->tileSelector->setTileFlips(this->tileXFlip, this->tileYFlip); - this->drawSelectedTiles(); - this->metatileLayersItem->clearLastModifiedCoords(); -} - -void TilesetEditor::setYFlip(bool enabled) -{ - this->tileYFlip = enabled; - this->tileSelector->setTileFlips(this->tileXFlip, this->tileYFlip); +void TilesetEditor::refreshTileFlips() { + this->tileSelector->setTileFlips(ui->checkBox_xFlip->isChecked(), ui->checkBox_yFlip->isChecked()); this->drawSelectedTiles(); this->metatileLayersItem->clearLastModifiedCoords(); } @@ -872,21 +856,12 @@ void TilesetEditor::on_actionChange_Palettes_triggered() { if (!this->paletteEditor) { this->paletteEditor = new PaletteEditor(this->project, this->primaryTileset, - this->secondaryTileset, this->paletteId, this); - connect(this->paletteEditor, &PaletteEditor::changedPaletteColor, - this, &TilesetEditor::onPaletteEditorChangedPaletteColor); - connect(this->paletteEditor, &PaletteEditor::changedPalette, - this, &TilesetEditor::onPaletteEditorChangedPalette); - } - - if (!this->paletteEditor->isVisible()) { - this->paletteEditor->show(); - } else if (this->paletteEditor->isMinimized()) { - this->paletteEditor->showNormal(); - } else { - this->paletteEditor->raise(); - this->paletteEditor->activateWindow(); + this->secondaryTileset, this->paletteId(), this); + connect(this->paletteEditor, &PaletteEditor::changedPaletteColor, this, &TilesetEditor::onPaletteEditorChangedPaletteColor); + connect(this->paletteEditor, &PaletteEditor::changedPalette, this, &TilesetEditor::setPaletteId); + connect(this->paletteEditor, &PaletteEditor::metatileSelected, this, &TilesetEditor::selectMetatile); } + Util::show(this->paletteEditor); } void TilesetEditor::onPaletteEditorChangedPaletteColor() { @@ -894,10 +869,6 @@ void TilesetEditor::onPaletteEditorChangedPaletteColor() { this->hasUnsavedChanges = true; } -void TilesetEditor::onPaletteEditorChangedPalette(int paletteId) { - this->on_spinBox_paletteSelector_valueChanged(paletteId); -} - bool TilesetEditor::replaceMetatile(uint16_t metatileId, const Metatile * src, QString newLabel) { Metatile * dest = Tileset::getMetatile(metatileId, this->primaryTileset, this->secondaryTileset); @@ -996,7 +967,7 @@ void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel) 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 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 = primary ? this->tileSelector->buildPrimaryTilesIndexedImage() : this->tileSelector->buildSecondaryTilesIndexedImage(); diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp index 9957f33a..c8053bdb 100644 --- a/src/ui/wildmonsearch.cpp +++ b/src/ui/wildmonsearch.cpp @@ -122,7 +122,10 @@ void WildMonSearch::updateResults(const QString &species) { if (ui->comboBox_Search->findText(species) < 0) return; // Not a species name, no need to search wild encounter data. - const QList results = this->resultsCache.value(species, search(species)); + auto it = this->resultsCache.constFind(species); + bool inCache = (it != this->resultsCache.constEnd()); + const QList results = inCache ? it.value() : search(species); + if (results.isEmpty()) { static const RowData noResults = { .mapName = "", @@ -140,7 +143,7 @@ void WildMonSearch::updateResults(const QString &species) { ui->table_Results->setSortingEnabled(true); - this->resultsCache.insert(species, results); + if (!inCache) this->resultsCache.insert(species, results); } // Double-clicking row data opens the corresponding map/table on the Wild Pokémon tab.