diff --git a/CHANGELOG.md b/CHANGELOG.md index 66d26e1b..a39341a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,20 +8,21 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ## [Unreleased] ### Added -- Add the ability to edit layouts with no corresponding map. -- Add ``Duplicate Map`` / ``Duplicate Layout`` options, accessible by right-clicking a map or layout in the map list. -- Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access. +- Redesigned the map list, adding new features including opening/editing layouts with no associated map, duplicating maps or layouts (accessible via right-click), editing the names of map groups, rearranging maps and map groups, and hiding empty folders. +- Add a drop-down for changing the layout of the currently opened map. +- Redesigned the Connections tab, adding new features including the option to open or display diving maps and a list UI for easier edit access. - Add a `Close Project` option -- Add charts to the `Wild Pokémon` tab that show species and level distributions. +- Add a search button to the `Wild Pokémon` tab that shows the encounter data for a species across all maps. +- Add charts to the `Wild Pokémon` tab that show species and level distributions for the current map. - Add options for customizing the map grid under `View -> Grid Settings`. - Add an option to display a dividing line between tilesets in the Tileset Editor. - An alert will be displayed when attempting to open a seemingly invalid project. - Add support for defining project values with `enum` where `#define` was expected. -- Add button to enable editing map groups including renaming groups and rearranging the maps within them. - Add buttons to hide and show empty folders in each map tree view. - Add a setting to specify the tile values to use for the unused metatile layer. ### Changed +- `Change Dimensions` now has an interactive resizing rectangle. - Redesigned the new map dialog, including better error checking and a collapsible section for header data. - Map groups and ``MAPSEC`` names specified when creating a new map will be added automatically if they don't already exist. - Edits to map connections now have Undo/Redo and can be viewed in exported timelapses. @@ -32,10 +33,12 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - It's now possible to cancel quitting if there are unsaved changes in sub-windows. - The triple-layer metatiles setting can now be set automatically using a project constant. - `Export Map Stitch Image` now shows a preview of the full image, not just the current map. -- Maps and layouts were internally separated. +- `Custom Attributes` tables now display numbers using spin boxes. The `type` column was removed, because `value`'s type is now obvious. - Unrecognized map names in Event or Connections data will no longer be overwritten. +- Reduced diff noise when saving maps. - Map names and ``MAP_NAME`` constants are no longer required to match. - Porymap will no longer overwrite ``include/constants/map_groups.h`` or ``include/constants/layouts.h``. +- Primary/secondary metatile images are now kept on separate rows, rather than blending together if the primary size is not divisible by 8. ### Fixed - Fix `Add Region Map...` not updating the region map settings file. @@ -49,7 +52,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix the `Edit History` window not raising to the front when reactivated. - New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded. - Fix invalid species names clearing from wild pokémon data when revisited. -- Fix editing wild pokémon data not marking the map as edited. +- Fix editing wild pokémon data not marking the map as unsaved. +- Fix editing an event's `Custom Attributes` not marking the map as unsaved. - Fix changes to map connections not marking connected maps as unsaved. - Fix numerous issues related to connecting a map to itself. - Fix incorrect map connections getting selected when opening a map by double-clicking a map connection. @@ -72,8 +76,11 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix exported tile images containing garbage pixels after the end of the tiles. - Fix fully transparent pixels rendering with the incorrect color. - Fix the values for some config fields shuffling their order every save. +- Fix `key`s in `Custom Attributes` disappearing if given an empty name or the name of an existing field. - Fix some problems with tileset detection when importing maps from AdvanceMap. - Fix certain input fields allowing invalid identifiers, like names starting with numbers. +- Fix crash in the Shortcuts Editor when applying changes after closing certain windows. +- Fix `Display Metatile Usage Counts` sometimes changing the counts after repeated use. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index fe47f67f..b645f49a 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -153,9 +153,9 @@ Callbacks .. js:function:: onMapOpened(mapName) - Called when a map is opened. + Called when a map or layout is opened. - :param mapName: the name of the opened map + :param mapName: the name of the opened map or layout :type mapName: string .. js:function:: onBlockChanged(x, y, prevBlock, newBlock) @@ -1995,7 +1995,28 @@ All utility functions are callable via the global ``utility`` object. Gets the list of map names. - :returns: the list of map names + :returns: the list of map names (e.g. `PetalburgCity`) + :rtype: array + +.. js:function:: utility.getMapConstants() + + Gets the list of map IDs (e.g. `MAP_PETALBURG_CITY`) + + :returns: the list of map IDs + :rtype: array + +.. js:function:: utility.getLayoutNames() + + Gets the list of layout names. + + :returns: the list of layout names (e.g. `PetalburgCity_Layout`) + :rtype: array + +.. js:function:: utility.getLayoutConstants() + + Gets the list of layout IDs (e.g. `LAYOUT_PETALBURG_CITY`) + + :returns: the list of layout IDs :rtype: array .. js:function:: utility.getTilesetNames() diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index a7a6d112..057df05d 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1484,7 +1484,7 @@ - + 1 @@ -1499,7 +1499,7 @@ 1 - + true @@ -1582,7 +1582,7 @@ - + 0 diff --git a/forms/mapimageexporter.ui b/forms/mapimageexporter.ui index 933aab67..322793c9 100644 --- a/forms/mapimageexporter.ui +++ b/forms/mapimageexporter.ui @@ -29,7 +29,7 @@ - + Map diff --git a/forms/resizelayoutpopup.ui b/forms/resizelayoutpopup.ui new file mode 100644 index 00000000..c98fe10f --- /dev/null +++ b/forms/resizelayoutpopup.ui @@ -0,0 +1,276 @@ + + + ResizeLayoutPopup + + + + 0 + 0 + 598 + 378 + + + + Dialog + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Resize Layout + + + Qt::AlignCenter + + + 8 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Reset + + + true + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Width + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 64 + 0 + + + + + + + + Height + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 64 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Border Width + + + + + + + + 64 + 0 + + + + + + + + Border Height + + + + + + + + 64 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + + + buttonBox + accepted() + ResizeLayoutPopup + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ResizeLayoutPopup + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/include/core/editcommands.h b/include/core/editcommands.h index 5bf99784..eabfacc0 100644 --- a/include/core/editcommands.h +++ b/include/core/editcommands.h @@ -8,6 +8,7 @@ #include #include #include +#include class Map; class Layout; @@ -203,7 +204,7 @@ private: /// Implements a command to commit a map or border resize action. class ResizeLayout : public QUndoCommand { public: - ResizeLayout(Layout *layout, QSize oldLayoutDimensions, QSize newLayoutDimensions, + ResizeLayout(Layout *layout, QSize oldLayoutDimensions, QMargins newLayoutMargins, const Blockdata &oldMetatiles, const Blockdata &newMetatiles, QSize oldBorderDimensions, QSize newBorderDimensions, const Blockdata &oldBorder, const Blockdata &newBorder, @@ -220,8 +221,7 @@ private: int oldLayoutWidth; int oldLayoutHeight; - int newLayoutWidth; - int newLayoutHeight; + QMargins newLayoutMargins; int oldBorderWidth; int oldBorderHeight; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 1579b4ad..da58c4b7 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -103,6 +103,7 @@ public: void setBlock(int x, int y, Block block, bool enableScriptCallback = false); void setBlockdata(Blockdata blockdata, bool enableScriptCallback = false); + void adjustDimensions(QMargins margins, bool setNewBlockdata = true); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); diff --git a/include/core/tileset.h b/include/core/tileset.h index 999e731d..32d18858 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -80,7 +80,7 @@ public: Metatile* metatileAt(unsigned int i) const { return m_metatiles.at(i); } void clearMetatiles(); - void resizeMetatiles(unsigned int newNumMetatiles); + void resizeMetatiles(int newNumMetatiles); int numMetatiles() const { return m_metatiles.length(); } private: diff --git a/include/editor.h b/include/editor.h index 104cda9a..ae61404d 100644 --- a/include/editor.h +++ b/include/editor.h @@ -108,14 +108,15 @@ public: void toggleBorderVisibility(bool visible, bool enableScriptCallback = true); void updateCustomMapAttributes(); - DraggablePixmapItem *addMapEvent(Event *event); + DraggablePixmapItem *addEventPixmapItem(Event *event); + void removeEventPixmapItem(Event *event); bool eventLimitReached(Map *, Event::Type); - void selectMapEvent(DraggablePixmapItem *object, bool toggle = false); + void selectMapEvent(DraggablePixmapItem *item, bool toggle = false); DraggablePixmapItem *addNewEvent(Event::Type type); void updateSelectedEvents(); void duplicateSelectedEvents(); - void redrawObject(DraggablePixmapItem *item); - QList getObjects(); + void redrawEventPixmapItem(DraggablePixmapItem *item); + QList getEventPixmapItems(); void updateCursorRectPos(int x, int y); void setCursorRectVisible(bool visible); @@ -155,7 +156,7 @@ public: enum class EditAction { None, Paint, Select, Fill, Shift, Pick, Move }; EditAction mapEditAction = EditAction::Paint; - EditAction objectEditAction = EditAction::Select; + EditAction eventEditAction = EditAction::Select; enum class EditMode { None, Disabled, Metatiles, Collision, Header, Events, Connections, Encounters }; EditMode editMode = EditMode::None; @@ -169,7 +170,7 @@ public: void setEditingMetatiles(); void setEditingCollision(); void setEditingHeader(); - void setEditingObjects(); + void setEditingEvents(); void setEditingConnections(); void setEditingEncounters(); @@ -179,7 +180,9 @@ public: qreal collisionOpacity = 0.5; static QList> collisionIcons; - void objectsView_onMousePress(QMouseEvent *event); + int eventShiftActionId = 0; + + void eventsView_onMousePress(QMouseEvent *event); int getBorderDrawDistance(int dimension); @@ -253,7 +256,7 @@ private slots: void onWheelZoom(int); signals: - void objectsChanged(); + void eventsChanged(); void openConnectedMap(MapConnection*); void wildMonTableOpened(EncounterTableModel*); void wildMonTableClosed(); diff --git a/include/lib/fex/array_value.h b/include/lib/fex/array_value.h index 642cc129..3339d21a 100644 --- a/include/lib/fex/array_value.h +++ b/include/lib/fex/array_value.h @@ -96,6 +96,7 @@ namespace fex case Type::kValuePair: return pair_.first + " = " + pair_.second->ToString() + "\n"; } + return ""; } static ArrayValue Empty() diff --git a/include/mainwindow.h b/include/mainwindow.h index 42d346f3..3fe187f3 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -221,8 +221,8 @@ private slots: void addNewEvent(Event::Type type); void tryAddEventTab(QWidget * tab); void displayEventTabs(); - void updateSelectedObjects(); - void updateObjects(); + void updateSelectedEvents(); + void updateEvents(); void on_toolButton_Paint_clicked(); void on_toolButton_Select_clicked(); @@ -325,6 +325,7 @@ private: QAction *undoAction = nullptr; QAction *redoAction = nullptr; + QPointer undoView = nullptr; QAction *copyAction = nullptr; QAction *pasteAction = nullptr; @@ -353,6 +354,7 @@ private: bool setProjectUI(); void clearProjectUI(); + void openEditHistory(); void openNewMapDialog(); void openDuplicateMapDialog(const QString &mapName); NewLayoutDialog* createNewLayoutDialog(const Layout *layoutToCopy = nullptr); @@ -380,7 +382,7 @@ private: void updateMapList(); void openMapListItem(const QModelIndex &index); - void saveMapListTab(int index); + void onMapListTabChanged(int index); void displayMapProperties(); void checkToolButtons(); diff --git a/include/scriptutility.h b/include/scriptutility.h index 0d6fca75..78272eec 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -42,6 +42,9 @@ public: Q_INVOKABLE QList getMetatileLayerOpacity(); Q_INVOKABLE void setMetatileLayerOpacity(QList order); Q_INVOKABLE QList getMapNames(); + Q_INVOKABLE QList getMapConstants(); + Q_INVOKABLE QList getLayoutNames(); + Q_INVOKABLE QList getLayoutConstants(); Q_INVOKABLE QList getTilesetNames(); Q_INVOKABLE QList getPrimaryTilesetNames(); Q_INVOKABLE QList getSecondaryTilesetNames(); diff --git a/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h index 6fe54a91..157e0771 100644 --- a/include/ui/encountertablemodel.h +++ b/include/ui/encountertablemodel.h @@ -30,7 +30,7 @@ public: WildMonInfo encounterData() const { return m_monInfo; } EncounterField encounterField() const { return m_encounterField; } - QList percentages() const { return m_slotPercentages; } + QVector percentages() const { return m_slotPercentages; } private: int m_numRows = 0; @@ -38,7 +38,7 @@ private: WildMonInfo m_monInfo; EncounterField m_encounterField; QMap m_groupNames; - QList m_slotPercentages; + QVector m_slotPercentages; signals: void edited(); diff --git a/include/ui/filterchildrenproxymodel.h b/include/ui/filterchildrenproxymodel.h index 507693b3..93934a7e 100644 --- a/include/ui/filterchildrenproxymodel.h +++ b/include/ui/filterchildrenproxymodel.h @@ -3,15 +3,28 @@ #include -class FilterChildrenProxyModel : public QSortFilterProxyModel +class NumericSortProxyModel : public QSortFilterProxyModel { Q_OBJECT public: - explicit FilterChildrenProxyModel(QObject *parent = nullptr); - void setHideEmpty(bool hidden) { this->hideEmpty = hidden; } + explicit NumericSortProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {}; + protected: - bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const; + virtual bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; +}; + +class FilterChildrenProxyModel : public NumericSortProxyModel +{ + Q_OBJECT + +public: + explicit FilterChildrenProxyModel(QObject *parent = nullptr) : NumericSortProxyModel(parent) {}; + void setHideEmpty(bool hidden) { this->hideEmpty = hidden; } + +protected: + virtual bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override; + private: bool hideEmpty = false; }; diff --git a/include/ui/mapheaderform.h b/include/ui/mapheaderform.h index 8f9adda8..79f4f6c8 100644 --- a/include/ui/mapheaderform.h +++ b/include/ui/mapheaderform.h @@ -9,9 +9,9 @@ #include #include -#include "mapheader.h" -class Project; +#include "mapheader.h" +#include "project.h" namespace Ui { class MapHeaderForm; diff --git a/include/ui/maplistmodels.h b/include/ui/maplistmodels.h index b973f438..681f0955 100644 --- a/include/ui/maplistmodels.h +++ b/include/ui/maplistmodels.h @@ -79,7 +79,6 @@ protected: QString activeItemName; QString folderTypeName; - bool sortingEnabled = false; bool editable = false; QIcon mapGrayIcon; diff --git a/include/ui/maplisttoolbar.h b/include/ui/maplisttoolbar.h index bf749a3e..9890b584 100644 --- a/include/ui/maplisttoolbar.h +++ b/include/ui/maplisttoolbar.h @@ -37,6 +37,8 @@ public: void setFilterLocked(bool locked) { m_filterLocked = locked; } bool isFilterLocked() const { return m_filterLocked; } + void setSearchFocus(); + signals: void filterCleared(MapTree*); void addFolderClicked(); diff --git a/include/ui/metatileselector.h b/include/ui/metatileselector.h index ae11c58e..fba0993d 100644 --- a/include/ui/metatileselector.h +++ b/include/ui/metatileselector.h @@ -79,6 +79,7 @@ private: bool positionIsValid(const QPoint &pos) const; bool selectionIsValid(); void hoverChanged(); + int numPrimaryMetatilesRounded() const; signals: void hoveredMetatileSelectionChanged(uint16_t); diff --git a/include/ui/movablerect.h b/include/ui/movablerect.h index efd85847..56798a0c 100644 --- a/include/ui/movablerect.h +++ b/include/ui/movablerect.h @@ -5,12 +5,13 @@ #include #include -class MovableRect : public QGraphicsItem + + +class MovableRect : public QGraphicsRectItem { public: MovableRect(bool *enabled, int width, int height, QRgb color); - QRectF boundingRect() const override - { + QRectF boundingRect() const override { qreal penWidth = 4; return QRectF(-penWidth, -penWidth, @@ -18,21 +19,65 @@ public: 20 * 8 + penWidth * 2); } - void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override - { + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { if (!(*enabled)) return; painter->setPen(this->color); - painter->drawRect(x() - 2, y() - 2, this->width + 3, this->height + 3); + painter->drawRect(this->rect().x() - 2, this->rect().y() - 2, this->rect().width() + 3, this->rect().height() + 3); painter->setPen(QColor(0, 0, 0)); - painter->drawRect(x() - 3, y() - 3, this->width + 5, this->height + 5); - painter->drawRect(x() - 1, y() - 1, this->width + 1, this->height + 1); + painter->drawRect(this->rect().x() - 3, this->rect().y() - 3, this->rect().width() + 5, this->rect().height() + 5); + painter->drawRect(this->rect().x() - 1, this->rect().y() - 1, this->rect().width() + 1, this->rect().height() + 1); } void updateLocation(int x, int y); bool *enabled; -private: - int width; - int height; + +protected: QRgb color; }; + + +/// A MovableRect with the addition of being resizable. +class ResizableRect : public QObject, public MovableRect +{ + Q_OBJECT +public: + ResizableRect(QObject *parent, bool *enabled, int width, int height, QRgb color); + + QRectF boundingRect() const override { + return QRectF(this->rect() + QMargins(lineWidth, lineWidth, lineWidth, lineWidth)); + } + + QPainterPath shape() const override { + QPainterPath path; + path.addRect(this->rect() + QMargins(lineWidth, lineWidth, lineWidth, lineWidth)); + path.addRect(this->rect() - QMargins(lineWidth, lineWidth, lineWidth, lineWidth)); + return path; + } + + void updatePosFromRect(QRect newPos); + void setLimit(QRect limit) { this->limit = limit; } + +protected: + void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent *) override; + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; + +private: + enum class Edge { None, Left, Right, Top, Bottom, TopLeft, BottomLeft, TopRight, BottomRight }; + ResizableRect::Edge detectEdge(int x, int y); + + // Variables for keeping state of original rect while resizing + ResizableRect::Edge clickedEdge = ResizableRect::Edge::None; + QPointF clickedPos = QPointF(); + QRect clickedRect; + + QRect limit = QRect(); + + int lineWidth = 8; + +signals: + void rectUpdated(QRect rect); +}; + #endif // MOVABLERECT_H diff --git a/include/ui/newlocationdialog.h b/include/ui/newlocationdialog.h index 33af5c9c..a98b2d61 100644 --- a/include/ui/newlocationdialog.h +++ b/include/ui/newlocationdialog.h @@ -5,7 +5,7 @@ #include #include -class Project; +#include "project.h" namespace Ui { class NewLocationDialog; diff --git a/include/ui/noscrollspinbox.h b/include/ui/noscrollspinbox.h index 0cc11043..0615da5a 100644 --- a/include/ui/noscrollspinbox.h +++ b/include/ui/noscrollspinbox.h @@ -12,6 +12,8 @@ public: void wheelEvent(QWheelEvent *event) override; void focusOutEvent(QFocusEvent *event) override; + void setLineEditEnabled(bool enabled); + unsigned getActionId(); private: diff --git a/include/ui/resizelayoutpopup.h b/include/ui/resizelayoutpopup.h new file mode 100644 index 00000000..1f79580e --- /dev/null +++ b/include/ui/resizelayoutpopup.h @@ -0,0 +1,109 @@ +#ifndef RESIZELAYOUTPOPUP_H +#define RESIZELAYOUTPOPUP_H + +#include +#include +#include +#include +#include +#include + +class ResizableRect; +class Editor; +namespace Ui { + class ResizeLayoutPopup; +} + + + +/// Custom scene that paints its background a gray checkered pattern. +/// Additionally there is a definable "valid" area which will paint the checkerboard green inside. +class CheckeredBgScene : public QGraphicsScene { + Q_OBJECT + +public: + CheckeredBgScene(QObject *parent = nullptr); + void setValidRect(int x, int y, int width, int height) { + this->validRect = QRect(x * this->gridSize, y * this->gridSize, width * this->gridSize, height * this->gridSize); + } + void setValidRect(QRect rect) { + this->validRect = rect; + } + QRect getValidRect() { return this->validRect; } + +protected: + void drawBackground(QPainter *painter, const QRectF &rect) override; + +private: + int gridSize = 16; // virtual pixels + QRect validRect = QRect(); +}; + + + +/// PixmapItem subclass which allows for creating a boundary which determine whether +/// the pixmap paints normally or with a black tint. +/// This item is movable and snaps on a 16x16 grid. +class BoundedPixmapItem : public QGraphicsPixmapItem { +public: + BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent = nullptr); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; + + void setBoundary(ResizableRect *rect) { this->boundary = rect; } + +protected: + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + +private: + ResizableRect *boundary = nullptr; + QPointF clickedPos = QPointF(); +}; + + + +/// The main (modal) dialog window for resizing layout and border dimensions. +/// The dialog itself is minimal, and is connected to the parent widget's geometry. +class ResizeLayoutPopup : public QDialog +{ + Q_OBJECT + +public: + ResizeLayoutPopup(QWidget *parent, Editor *editor); + ~ResizeLayoutPopup(); + + void setupLayoutView(); + + void resetPosition(); + + QMargins getResult(); + QSize getBorderResult(); + +protected: + void moveEvent(QMoveEvent *) override { + // Prevent the dialog from being moved + this->resetPosition(); + } + + void resizeEvent(QResizeEvent *) override { + // Prevent the dialog from being resized + this->resetPosition(); + } + +private slots: + void on_spinBox_width_valueChanged(int value); + void on_spinBox_height_valueChanged(int value); + void on_buttonBox_clicked(QAbstractButton *button); + +private: + QWidget *parent = nullptr; + Editor *editor = nullptr; + + Ui::ResizeLayoutPopup *ui; + + ResizableRect *outline = nullptr; + BoundedPixmapItem *layoutPixmap = nullptr; + + QPointer scene = nullptr; +}; + +#endif // RESIZELAYOUTPOPUP_H diff --git a/include/ui/shortcutseditor.h b/include/ui/shortcutseditor.h index 31b0279e..e44c8896 100644 --- a/include/ui/shortcutseditor.h +++ b/include/ui/shortcutseditor.h @@ -35,9 +35,9 @@ signals: private: Ui::ShortcutsEditor *ui; QWidget *main_container; - QMultiMap labels_objects; + QMultiMap> labels_objects; QHash contexts_layouts; - QHash multiKeyEdits_objects; + QHash> multiKeyEdits_objects; void parseObjectList(const QObjectList &objectList); QString getLabel(const QObject *object) const; diff --git a/include/ui/tileseteditormetatileselector.h b/include/ui/tileseteditormetatileselector.h index 760da8e4..afc77ffe 100644 --- a/include/ui/tileseteditormetatileselector.h +++ b/include/ui/tileseteditormetatileselector.h @@ -51,6 +51,7 @@ private: void drawCounts(); QImage buildAllMetatilesImage(); QImage buildImage(int metatileIdStart, int numMetatiles); + int numPrimaryMetatilesRounded() const; signals: void hoveredMetatileChanged(uint16_t); diff --git a/include/ui/wildmonsearch.h b/include/ui/wildmonsearch.h index 49e04b57..ccf0fe94 100644 --- a/include/ui/wildmonsearch.h +++ b/include/ui/wildmonsearch.h @@ -2,9 +2,24 @@ #define WILDMONSEARCH_H #include +#include +#include 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 f3599c1a..97265940 100644 --- a/porymap.pro +++ b/porymap.pro @@ -35,6 +35,7 @@ DEFINES += PORYMAP_VERSION=\\\"$$VERSION\\\" SOURCES += src/core/advancemapparser.cpp \ src/core/block.cpp \ + src/ui/resizelayoutpopup.cpp \ src/core/bitpacker.cpp \ src/core/blockdata.cpp \ src/core/events.cpp \ @@ -255,7 +256,8 @@ HEADERS += include/core/advancemapparser.h \ include/ui/uintspinbox.h \ include/ui/updatepromoter.h \ include/ui/wildmonchart.h \ - include/ui/wildmonsearch.h + include/ui/wildmonsearch.h \ + include/ui/resizelayoutpopup.h FORMS += forms/mainwindow.ui \ forms/colorinputwidget.ui \ @@ -288,7 +290,8 @@ FORMS += forms/mainwindow.ui \ forms/customattributesdialog.ui \ forms/updatepromoter.ui \ forms/wildmonchart.ui \ - forms/wildmonsearch.ui + forms/wildmonsearch.ui \ + forms/resizelayoutpopup.ui RESOURCES += \ resources/images.qrc \ diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index c500c8c0..94354896 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -177,7 +177,7 @@ bool ShiftMetatiles::mergeWith(const QUndoCommand *command) { ************************************************************************ ******************************************************************************/ -ResizeLayout::ResizeLayout(Layout *layout, QSize oldLayoutDimensions, QSize newLayoutDimensions, +ResizeLayout::ResizeLayout(Layout *layout, QSize oldLayoutDimensions, QMargins newLayoutMargins, const Blockdata &oldMetatiles, const Blockdata &newMetatiles, QSize oldBorderDimensions, QSize newBorderDimensions, const Blockdata &oldBorder, const Blockdata &newBorder, @@ -189,8 +189,7 @@ ResizeLayout::ResizeLayout(Layout *layout, QSize oldLayoutDimensions, QSize newL this->oldLayoutWidth = oldLayoutDimensions.width(); this->oldLayoutHeight = oldLayoutDimensions.height(); - this->newLayoutWidth = newLayoutDimensions.width(); - this->newLayoutHeight = newLayoutDimensions.height(); + this->newLayoutMargins = newLayoutMargins; this->oldMetatiles = oldMetatiles; this->newMetatiles = newMetatiles; @@ -210,12 +209,14 @@ void ResizeLayout::redo() { if (!layout) return; - layout->blockdata = newMetatiles; - layout->setDimensions(newLayoutWidth, newLayoutHeight, false, true); - layout->border = newBorder; layout->setBorderDimensions(newBorderWidth, newBorderHeight, false, true); + layout->width = oldLayoutWidth; + layout->height = oldLayoutHeight; + layout->adjustDimensions(this->newLayoutMargins); + layout->blockdata = newMetatiles; + layout->lastCommitBlocks.layoutDimensions = QSize(layout->getWidth(), layout->getHeight()); layout->lastCommitBlocks.borderDimensions = QSize(layout->getBorderWidth(), layout->getBorderHeight()); @@ -225,12 +226,14 @@ void ResizeLayout::redo() { void ResizeLayout::undo() { if (!layout) return; - layout->blockdata = oldMetatiles; - layout->setDimensions(oldLayoutWidth, oldLayoutHeight, false, true); - layout->border = oldBorder; layout->setBorderDimensions(oldBorderWidth, oldBorderHeight, false, true); + layout->width = oldLayoutWidth + newLayoutMargins.left() + newLayoutMargins.right(); + layout->height = oldLayoutHeight + newLayoutMargins.top() + newLayoutMargins.bottom(); + layout->adjustDimensions(-this->newLayoutMargins); + layout->blockdata = oldMetatiles; + layout->lastCommitBlocks.layoutDimensions = QSize(layout->getWidth(), layout->getHeight()); layout->lastCommitBlocks.borderDimensions = QSize(layout->getBorderWidth(), layout->getBorderHeight()); @@ -324,9 +327,7 @@ void EventCreate::redo() { QUndoCommand::redo(); map->addEvent(event); - - editor->project->setEventPixmap(event); - editor->addMapEvent(event); + editor->addEventPixmapItem(event); // select this event editor->selected_events->clear(); @@ -335,12 +336,7 @@ void EventCreate::redo() { void EventCreate::undo() { map->removeEvent(event); - - if (editor->scene->items().contains(event->getPixmapItem())) { - editor->scene->removeItem(event->getPixmapItem()); - } - editor->selected_events->removeOne(event->getPixmapItem()); - + editor->removeEventPixmapItem(event); editor->shouldReselectEvents(); QUndoCommand::undo(); @@ -375,11 +371,7 @@ void EventDelete::redo() { for (Event *event : selectedEvents) { map->removeEvent(event); - - if (editor->scene->items().contains(event->getPixmapItem())) { - editor->scene->removeItem(event->getPixmapItem()); - } - editor->selected_events->removeOne(event->getPixmapItem()); + editor->removeEventPixmapItem(event); } editor->selected_events->clear(); @@ -391,8 +383,7 @@ void EventDelete::redo() { void EventDelete::undo() { for (Event *event : selectedEvents) { map->addEvent(event); - editor->project->setEventPixmap(event); - editor->addMapEvent(event); + editor->addEventPixmapItem(event); } // select these events @@ -433,8 +424,7 @@ void EventDuplicate::redo() { for (Event *event : selectedEvents) { map->addEvent(event); - editor->project->setEventPixmap(event); - editor->addMapEvent(event); + editor->addEventPixmapItem(event); } // select these events @@ -448,11 +438,7 @@ void EventDuplicate::redo() { void EventDuplicate::undo() { for (Event *event : selectedEvents) { map->removeEvent(event); - - if (editor->scene->items().contains(event->getPixmapItem())) { - editor->scene->removeItem(event->getPixmapItem()); - } - editor->selected_events->removeOne(event->getPixmapItem()); + editor->removeEventPixmapItem(event); } editor->shouldReselectEvents(); diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 94e6c291..35fcc2f7 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -176,6 +176,34 @@ void Layout::setDimensions(int newWidth, int newHeight, bool setNewBlockdata, bo emit layoutDimensionsChanged(QSize(getWidth(), getHeight())); } +void Layout::adjustDimensions(QMargins margins, bool setNewBlockdata) { + int oldWidth = this->width; + int oldHeight = this->height; + int newWidth = this->width + margins.left() + margins.right(); + int newHeight = this->height + margins.top() + margins.bottom(); + + if (setNewBlockdata) { + // Fill new blockdata + Blockdata newBlockdata; + for (int y = 0; y < newHeight; y++) + for (int x = 0; x < newWidth; x++) { + if ((x < margins.left()) || (x >= newWidth - margins.right()) || (y < margins.top()) || (y >= newHeight - margins.bottom())) { + newBlockdata.append(0); + } else { + int index = (y - margins.top()) * oldWidth + (x - margins.left()); + newBlockdata.append(this->blockdata.value(index)); + } + } + this->blockdata = newBlockdata; + } + + this->width = newWidth; + this->height = newHeight; + + emit layoutChanged(this); + emit layoutDimensionsChanged(QSize(getWidth(), getHeight())); +} + void Layout::setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata, bool enableScriptCallback) { if (setNewBlockdata) { setNewBorderDimensionsBlockdata(newWidth, newHeight); diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index e79e7706..20590369 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -83,7 +83,8 @@ void Tileset::addMetatile(Metatile* metatile) { m_metatiles.append(metatile); } -void Tileset::resizeMetatiles(unsigned int newNumMetatiles) { +void Tileset::resizeMetatiles(int newNumMetatiles) { + if (newNumMetatiles < 0) newNumMetatiles = 0; while (m_metatiles.length() > newNumMetatiles) { delete m_metatiles.takeLast(); } diff --git a/src/editor.cpp b/src/editor.cpp index ceb7aa67..823d1196 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -135,36 +135,26 @@ void Editor::setEditorView() { return; } + map_item->setEditsEnabled(this->editMode != EditMode::Connections); map_item->draw(); collision_item->draw(); current_view->setVisible(true); updateBorderVisibility(); - this->cursorMapTileRect->setSingleTileMode(); - this->cursorMapTileRect->setActive(true); - switch (this->editMode) { - case EditMode::Metatiles: - case EditMode::Collision: - map_item->setEditsEnabled(true); - this->editGroup.setActiveStack(&this->layout->editHistory); - break; - case EditMode::Connections: - this->cursorMapTileRect->setActive(false); - map_item->setEditsEnabled(false); - case EditMode::Events: - if (this->map) { - this->editGroup.setActiveStack(this->map->editHistory()); - } - break; - case EditMode::Header: - case EditMode::Encounters: - default: - this->editGroup.setActiveStack(nullptr); - break; + QUndoStack *editStack = this->map ? this->map->editHistory() : nullptr; + bool usesCursor = false; + if (this->editMode == EditMode::Metatiles || this->editMode == EditMode::Collision) { + if (this->layout) editStack = &this->layout->editHistory; + usesCursor = true; } + this->cursorMapTileRect->setSingleTileMode(); + this->cursorMapTileRect->setActive(usesCursor); + this->editGroup.setActiveStack(editStack); + + if (this->events_group) { this->events_group->setVisible(this->editMode == EditMode::Events); } @@ -189,7 +179,7 @@ void Editor::setEditingHeader() { setEditorView(); } -void Editor::setEditingObjects() { +void Editor::setEditingEvents() { this->editMode = EditMode::Events; setEditorView(); @@ -1306,7 +1296,7 @@ void Editor::setStraightPathCursorMode(QGraphicsSceneMouseEvent *event) { } void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item) { - // TODO: add event tab object painting tool buttons stuff here + // TODO: add event tab event painting tool buttons stuff here if (!item->getEditsEnabled()) { return; } @@ -1361,10 +1351,10 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i item->shift(event); } } else if (this->editMode == EditMode::Events) { - if (objectEditAction == EditAction::Paint && event->type() == QEvent::GraphicsSceneMousePress) { + if (eventEditAction == EditAction::Paint && event->type() == QEvent::GraphicsSceneMousePress) { // Right-clicking while in paint mode will change mode to select. if (event->buttons() & Qt::RightButton) { - this->objectEditAction = EditAction::Select; + this->eventEditAction = EditAction::Select; this->settings->mapCursor = QCursor(); this->cursorMapTileRect->setSingleTileMode(); this->ui->toolButton_Paint->setChecked(false); @@ -1380,18 +1370,17 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i DraggablePixmapItem *newEvent = addNewEvent(eventType); if (newEvent) { newEvent->move(pos.x(), pos.y()); - emit objectsChanged(); + emit eventsChanged(); selectMapEvent(newEvent); } } - } else if (objectEditAction == EditAction::Select) { + } else if (eventEditAction == EditAction::Select) { // do nothing here, at least for now - } else if (objectEditAction == EditAction::Shift) { + } else if (eventEditAction == EditAction::Shift) { static QPoint selection_origin; - static unsigned actionId = 0; if (event->type() == QEvent::GraphicsSceneMouseRelease) { - actionId++; + this->eventShiftActionId++; } else { if (event->type() == QEvent::GraphicsSceneMousePress) { selection_origin = QPoint(pos.x(), pos.y()); @@ -1402,12 +1391,12 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i QList selectedEvents; - for (DraggablePixmapItem *pixmapItem : getObjects()) { + for (DraggablePixmapItem *pixmapItem : getEventPixmapItems()) { selectedEvents.append(pixmapItem->event); } selection_origin = QPoint(pos.x(), pos.y()); - map->commit(new EventShift(selectedEvents, xDelta, yDelta, actionId)); + map->commit(new EventShift(selectedEvents, xDelta, yDelta, this->eventShiftActionId)); } } } @@ -1702,15 +1691,13 @@ void Editor::displayMovementPermissionSelector() { void Editor::clearMapEvents() { if (events_group) { + if (events_group->scene()) { + events_group->scene()->removeItem(events_group); + } for (QGraphicsItem *child : events_group->childItems()) { events_group->removeFromGroup(child); delete child; } - - if (events_group->scene()) { - events_group->scene()->removeItem(events_group); - } - delete events_group; events_group = nullptr; } @@ -1724,18 +1711,29 @@ void Editor::displayMapEvents() { scene->addItem(events_group); for (const auto &event : map->getEvents()) { - project->setEventPixmap(event); - addMapEvent(event); + addEventPixmapItem(event); } - //objects_group->setFiltersChildEvents(false); + events_group->setHandlesChildEvents(false); } -DraggablePixmapItem *Editor::addMapEvent(Event *event) { - DraggablePixmapItem *object = new DraggablePixmapItem(event, this); - this->redrawObject(object); - events_group->addToGroup(object); - return object; +DraggablePixmapItem *Editor::addEventPixmapItem(Event *event) { + this->project->setEventPixmap(event); + auto item = new DraggablePixmapItem(event, this); + redrawEventPixmapItem(item); + this->events_group->addToGroup(item); + return item; +} + +void Editor::removeEventPixmapItem(Event *event) { + auto item = event->getPixmapItem(); + if (!item) return; + + this->events_group->removeFromGroup(item); + this->selected_events->removeOne(item); + + event->setPixmapItem(nullptr); + delete item; } void Editor::clearMapConnections() { @@ -1987,7 +1985,7 @@ Tileset* Editor::getCurrentMapPrimaryTileset() return project->getTileset(tilesetLabel); } -QList Editor::getObjects() { +QList Editor::getEventPixmapItems() { QList list; for (QGraphicsItem *child : events_group->childItems()) { list.append(static_cast(child)); @@ -1995,7 +1993,7 @@ QList Editor::getObjects() { return list; } -void Editor::redrawObject(DraggablePixmapItem *item) { +void Editor::redrawEventPixmapItem(DraggablePixmapItem *item) { if (item && item->event && !item->event->getPixmap().isNull()) { qreal opacity = item->event->getUsingSprite() ? 1.0 : 0.7; item->setOpacity(opacity); @@ -2050,27 +2048,27 @@ void Editor::shouldReselectEvents() { } void Editor::updateSelectedEvents() { - for (DraggablePixmapItem *item : getObjects()) { - redrawObject(item); + for (DraggablePixmapItem *item : getEventPixmapItems()) { + redrawEventPixmapItem(item); } - emit objectsChanged(); + emit eventsChanged(); } -void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { - if (!selected_events || !object) +void Editor::selectMapEvent(DraggablePixmapItem *item, bool toggle) { + if (!selected_events || !item) return; if (!toggle) { // Selecting just this event selected_events->clear(); - selected_events->append(object); - } else if (!selected_events->contains(object)) { + selected_events->append(item); + } else if (!selected_events->contains(item)) { // Adding event to group selection - selected_events->append(object); + selected_events->append(item); } else if (selected_events->length() > 1) { // Removing event from group selection - selected_events->removeOne(object); + selected_events->removeOne(item); } else { // Attempting to toggle the only currently-selected event. // Unselecting an event this way would be unexpected, so we ignore it. @@ -2083,17 +2081,9 @@ void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) { int event_offs = Event::getIndexOffset(eventGroup); index = index - event_offs; Event *event = this->map->getEvent(eventGroup, index); - DraggablePixmapItem *selectedEvent = nullptr; - for (QGraphicsItem *child : this->events_group->childItems()) { - DraggablePixmapItem *item = static_cast(child); - if (item->event == event) { - selectedEvent = item; - break; - } - } - if (selectedEvent) { - this->selectMapEvent(selectedEvent); + if (event && event->getPixmapItem()) { + this->selectMapEvent(event->getPixmapItem()); } else { updateSelectedEvents(); } @@ -2295,13 +2285,13 @@ bool Editor::startDetachedProcess(const QString &command, const QString &working // Since the DraggablePixmapItem's event fires first, we can set a temp // variable "selectingEvent" so that we can detect whether or not the user // is clicking on the background instead of an event. -void Editor::objectsView_onMousePress(QMouseEvent *event) { - // make sure we are in object editing mode +void Editor::eventsView_onMousePress(QMouseEvent *event) { + // make sure we are in event editing mode if (map_item && this->editMode != EditMode::Events) { return; } - if (this->objectEditAction == EditAction::Paint && event->buttons() & Qt::RightButton) { - this->objectEditAction = EditAction::Select; + if (this->eventEditAction == EditAction::Paint && event->buttons() & Qt::RightButton) { + this->eventEditAction = EditAction::Select; this->settings->mapCursor = QCursor(); this->cursorMapTileRect->setSingleTileMode(); this->ui->toolButton_Paint->setChecked(false); diff --git a/src/lib/orderedjson.cpp b/src/lib/orderedjson.cpp index d7dd35e8..e1a600aa 100644 --- a/src/lib/orderedjson.cpp +++ b/src/lib/orderedjson.cpp @@ -116,6 +116,10 @@ static void dump(const QString &value, QString &out, int *indent, bool isKey = f static void dump(const Json::array &values, QString &out, int *indent) { bool first = true; if (!out.endsWith(": ")) out += QString(*indent * 2, ' '); + if (values.empty()) { + out += "[]"; + return; + } out += "[\n"; *indent += 1; for (const auto &value : values) { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5e05517e..277aaf97 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -23,6 +23,7 @@ #include "newmapconnectiondialog.h" #include "config.h" #include "filedialog.h" +#include "resizelayoutpopup.h" #include "newmapdialog.h" #include "newtilesetdialog.h" #include "newmapgroupdialog.h" @@ -92,6 +93,7 @@ MainWindow::~MainWindow() saveGlobalConfigs(); delete label_MapRulerStatus; + delete undoView; delete editor; delete ui; } @@ -332,14 +334,14 @@ void MainWindow::checkForUpdates(bool) {} void MainWindow::initEditor() { this->editor = new Editor(ui); - connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects); + connect(this->editor, &Editor::eventsChanged, this, &MainWindow::updateEvents); connect(this->editor, &Editor::openConnectedMap, this, &MainWindow::onOpenConnectedMap); connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap); connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonTableEdited, [this] { this->markMapEdited(); }); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); - connect(ui->toolButton_deleteObject, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents); + connect(ui->toolButton_deleteEvent, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents); this->loadUserSettings(); @@ -354,15 +356,15 @@ void MainWindow::initEditor() { ui->menuEdit->addAction(undoAction); ui->menuEdit->addAction(redoAction); - QUndoView *undoView = new QUndoView(&editor->editGroup); - undoView->setWindowTitle(tr("Edit History")); - undoView->setAttribute(Qt::WA_QuitOnClose, false); + this->undoView = new QUndoView(&editor->editGroup); + this->undoView->setWindowTitle(tr("Edit History")); + this->undoView->setAttribute(Qt::WA_QuitOnClose, false); // Show the EditHistory dialog with Ctrl+E QAction *showHistory = new QAction("Show Edit History...", this); showHistory->setObjectName("action_ShowEditHistory"); showHistory->setShortcut(QKeySequence("Ctrl+E")); - connect(showHistory, &QAction::triggered, [this, undoView](){ openSubWindow(undoView); }); + connect(showHistory, &QAction::triggered, this, &MainWindow::openEditHistory); ui->menuEdit->addAction(showHistory); @@ -387,6 +389,10 @@ void MainWindow::initEditor() { }); } +void MainWindow::openEditHistory() { + openSubWindow(this->undoView); +} + void MainWindow::initMiscHeapObjects() { ui->tabWidget_EventType->clear(); } @@ -450,7 +456,7 @@ void MainWindow::initMapList() { connect(ui->mapListToolBar_Locations, &MapListToolBar::addFolderClicked, this, &MainWindow::openNewLocationDialog); connect(ui->mapListToolBar_Layouts, &MapListToolBar::addFolderClicked, this, &MainWindow::openNewLayoutDialog); - connect(ui->mapListContainer, &QTabWidget::currentChanged, this, &MainWindow::saveMapListTab); + connect(ui->mapListContainer, &QTabWidget::currentChanged, this, &MainWindow::onMapListTabChanged); } void MainWindow::updateWindowTitle() { @@ -830,12 +836,7 @@ void MainWindow::on_action_Open_Project_triggered() } void MainWindow::on_action_Reload_Project_triggered() { - // TODO: when undo history is complete show only if has unsaved changes - WarningMessage msgBox(QStringLiteral("Reloading this project will discard any unsaved changes."), this); - msgBox.addButton(QMessageBox::Cancel); - msgBox.setDefaultButton(QMessageBox::Cancel); - if (msgBox.exec() == QMessageBox::Ok) - openProject(editor->project->root); + openProject(editor->project->root); } void MainWindow::on_action_Close_Project_triggered() { @@ -953,6 +954,7 @@ bool MainWindow::setLayout(QString layoutId) { connect(editor->layout, &Layout::needsRedrawing, this, &MainWindow::redrawMapScene, Qt::UniqueConnection); + Scripting::cb_MapOpened(layout->name); updateTilesetEditor(); userConfig.recentMapOrLayout = layoutId; @@ -961,8 +963,8 @@ bool MainWindow::setLayout(QString layoutId) { } void MainWindow::redrawMapScene() { - editor->displayMap(); editor->displayLayout(); + editor->displayMap(); refreshMapScene(); } @@ -1008,13 +1010,12 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_ int index = event_id - Event::getIndexOffset(event_group); Event* event = editor->map->getEvent(event_group, index); if (event) { - for (DraggablePixmapItem *item : editor->getObjects()) { - if (item->event == event) { - editor->selected_events->clear(); - editor->selected_events->append(item); - editor->updateSelectedEvents(); - return; - } + auto item = event->getPixmapItem(); + if (item) { + editor->selected_events->clear(); + editor->selected_events->append(item); + editor->updateSelectedEvents(); + return; } } // Can still warp to this map, but can't select the specified event @@ -1106,11 +1107,15 @@ bool MainWindow::setProjectUI() { this->locationListProxyModel = new FilterChildrenProxyModel(); locationListProxyModel->setSourceModel(this->mapLocationModel); ui->locationList->setModel(locationListProxyModel); + ui->locationList->setSortingEnabled(true); + ui->locationList->sortByColumn(0, Qt::SortOrder::AscendingOrder); this->layoutTreeModel = new LayoutTreeModel(editor->project); this->layoutListProxyModel = new FilterChildrenProxyModel(); this->layoutListProxyModel->setSourceModel(this->layoutTreeModel); ui->layoutList->setModel(layoutListProxyModel); + ui->layoutList->setSortingEnabled(true); + ui->layoutList->sortByColumn(0, Qt::SortOrder::AscendingOrder); ui->mapCustomAttributesFrame->table()->setRestrictedKeys(project->topLevelMapFields); @@ -1465,8 +1470,15 @@ void MainWindow::currentMetatilesSelectionChanged() { scrollMetatileSelectorToSelection(); } -void MainWindow::saveMapListTab(int index) { +void MainWindow::onMapListTabChanged(int index) { + // Save current tab for future sessions. porymapConfig.mapListTab = index; + + // After changing a map list tab the old tab's search widget can keep focus, which isn't helpful + // (and might be a little confusing to the user, because they don't know that each search bar is secretly a separate object). + // When we change tabs we'll automatically focus in on the search bar. This should also make finding maps a little quicker. + auto toolbar = getCurrentMapListToolBar(); + if (toolbar) toolbar->setSearchFocus(); } void MainWindow::openMapListItem(const QModelIndex &index) { @@ -1737,7 +1749,7 @@ void MainWindow::paste() { if (!newEvents.empty()) { editor->map->commit(new EventPaste(this->editor, editor->map, newEvents)); - updateObjects(); + updateEvents(); } break; @@ -1793,8 +1805,8 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) clickToolButtonFromEditAction(editor->mapEditAction); } else if (index == MainTab::Events) { ui->stackedWidget_MapEvents->setCurrentIndex(1); - editor->setEditingObjects(); - clickToolButtonFromEditAction(editor->objectEditAction); + editor->setEditingEvents(); + clickToolButtonFromEditAction(editor->eventEditAction); } else if (index == MainTab::Connections) { editor->setEditingConnections(); ui->graphicsView_Connections->setFocus(); // Avoid opening tab with focus on something editable @@ -1935,13 +1947,13 @@ void MainWindow::resetMapViewScale() { void MainWindow::addNewEvent(Event::Type type) { if (editor && editor->project) { - DraggablePixmapItem *object = editor->addNewEvent(type); - if (object) { + DraggablePixmapItem *item = editor->addNewEvent(type); + if (item) { auto halfSize = ui->graphicsView_Map->size() / 2; auto centerPos = ui->graphicsView_Map->mapToScene(halfSize.width(), halfSize.height()); - object->moveTo(Metatile::coordFromPixmapCoord(centerPos)); - updateObjects(); - editor->selectMapEvent(object); + item->moveTo(Metatile::coordFromPixmapCoord(centerPos)); + updateEvents(); + editor->selectMapEvent(item); } else { WarningMessage msgBox(QStringLiteral("Failed to add new event."), this); if (Event::typeToGroup(type) == Event::Group::Object) { @@ -1973,17 +1985,17 @@ void MainWindow::displayEventTabs() { tryAddEventTab(ui->tab_HealLocations); } -void MainWindow::updateObjects() { - QList all_objects = editor->getObjects(); +void MainWindow::updateEvents() { + QList items = editor->getEventPixmapItems(); for (auto i = this->lastSelectedEvent.cbegin(), end = this->lastSelectedEvent.cend(); i != end; i++) { - if (i.value() && !all_objects.contains(i.value())) + if (i.value() && !items.contains(i.value())) this->lastSelectedEvent.insert(i.key(), nullptr); } displayEventTabs(); - updateSelectedObjects(); + updateSelectedEvents(); } -void MainWindow::updateSelectedObjects() { +void MainWindow::updateSelectedEvents() { QList events; if (editor->selected_events && editor->selected_events->length()) { @@ -1998,7 +2010,7 @@ void MainWindow::updateSelectedObjects() { DraggablePixmapItem *selectedEvent = all_events.first()->getPixmapItem(); if (selectedEvent) { editor->selected_events->append(selectedEvent); - editor->redrawObject(selectedEvent); + editor->redrawEventPixmapItem(selectedEvent); events.append(selectedEvent); } } @@ -2143,7 +2155,7 @@ Event::Group MainWindow::getEventGroupFromTabWidget(QWidget *tab) { void MainWindow::eventTabChanged(int index) { if (editor->map) { Event::Group group = getEventGroupFromTabWidget(ui->tabWidget_EventType->widget(index)); - DraggablePixmapItem *selectedEvent = this->lastSelectedEvent.value(group, nullptr); + DraggablePixmapItem *selectedItem = this->lastSelectedEvent.value(group, nullptr); switch (group) { case Event::Group::Object: @@ -2166,18 +2178,11 @@ void MainWindow::eventTabChanged(int index) { } if (!isProgrammaticEventTabChange) { - if (!selectedEvent && editor->map->getNumEvents(group)) { + if (!selectedItem) { Event *event = editor->map->getEvent(group, 0); - for (QGraphicsItem *child : editor->events_group->childItems()) { - DraggablePixmapItem *item = static_cast(child); - if (item->event == event) { - selectedEvent = item; - break; - } - } + if (event) selectedItem = event->getPixmapItem(); } - - if (selectedEvent) editor->selectMapEvent(selectedEvent); + if (selectedItem) editor->selectMapEvent(selectedItem); } } @@ -2246,7 +2251,7 @@ void MainWindow::on_toolButton_Paint_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Paint; else - editor->objectEditAction = Editor::EditAction::Paint; + editor->eventEditAction = Editor::EditAction::Paint; editor->settings->mapCursor = QCursor(QPixmap(":/icons/pencil_cursor.ico"), 10, 10); @@ -2267,7 +2272,7 @@ void MainWindow::on_toolButton_Select_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Select; else - editor->objectEditAction = Editor::EditAction::Select; + editor->eventEditAction = Editor::EditAction::Select; editor->settings->mapCursor = QCursor(); editor->cursorMapTileRect->setSingleTileMode(); @@ -2286,7 +2291,7 @@ void MainWindow::on_toolButton_Fill_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Fill; else - editor->objectEditAction = Editor::EditAction::Fill; + editor->eventEditAction = Editor::EditAction::Fill; editor->settings->mapCursor = QCursor(QPixmap(":/icons/fill_color_cursor.ico"), 10, 10); editor->cursorMapTileRect->setSingleTileMode(); @@ -2305,7 +2310,7 @@ void MainWindow::on_toolButton_Dropper_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Pick; else - editor->objectEditAction = Editor::EditAction::Pick; + editor->eventEditAction = Editor::EditAction::Pick; editor->settings->mapCursor = QCursor(QPixmap(":/icons/pipette_cursor.ico"), 10, 10); editor->cursorMapTileRect->setSingleTileMode(); @@ -2324,7 +2329,7 @@ void MainWindow::on_toolButton_Move_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Move; else - editor->objectEditAction = Editor::EditAction::Move; + editor->eventEditAction = Editor::EditAction::Move; editor->settings->mapCursor = QCursor(QPixmap(":/icons/move.ico"), 7, 7); editor->cursorMapTileRect->setSingleTileMode(); @@ -2343,7 +2348,7 @@ void MainWindow::on_toolButton_Shift_clicked() if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->mapEditAction = Editor::EditAction::Shift; else - editor->objectEditAction = Editor::EditAction::Shift; + editor->eventEditAction = Editor::EditAction::Shift; editor->settings->mapCursor = QCursor(QPixmap(":/icons/shift_cursor.ico"), 10, 10); editor->cursorMapTileRect->setSingleTileMode(); @@ -2362,7 +2367,7 @@ void MainWindow::checkToolButtons() { if (ui->mainTabBar->currentIndex() == MainTab::Map) { editAction = editor->mapEditAction; } else { - editAction = editor->objectEditAction; + editAction = editor->eventEditAction; if (editAction == Editor::EditAction::Select && editor->map_ruler) editor->map_ruler->setEnabled(true); else if (editor->map_ruler) @@ -2589,87 +2594,39 @@ void MainWindow::on_comboBox_SecondaryTileset_currentTextChanged(const QString & void MainWindow::on_pushButton_ChangeDimensions_clicked() { if (!editor || !editor->layout) return; - QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); - dialog.setWindowTitle("Change Map Dimensions"); - dialog.setWindowModality(Qt::NonModal); + ResizeLayoutPopup popup(this->ui->graphicsView_Map, this->editor); + popup.show(); + popup.setupLayoutView(); + if (popup.exec() == QDialog::Accepted) { + Layout *layout = this->editor->layout; + Map *map = this->editor->map; - QFormLayout form(&dialog); - - QSpinBox *widthSpinBox = new QSpinBox(); - QSpinBox *heightSpinBox = new QSpinBox(); - QSpinBox *bwidthSpinBox = new QSpinBox(); - QSpinBox *bheightSpinBox = new QSpinBox(); - widthSpinBox->setMinimum(1); - heightSpinBox->setMinimum(1); - bwidthSpinBox->setMinimum(1); - bheightSpinBox->setMinimum(1); - widthSpinBox->setMaximum(editor->project->getMaxMapWidth()); - heightSpinBox->setMaximum(editor->project->getMaxMapHeight()); - bwidthSpinBox->setMaximum(MAX_BORDER_WIDTH); - bheightSpinBox->setMaximum(MAX_BORDER_HEIGHT); - widthSpinBox->setValue(editor->layout->getWidth()); - heightSpinBox->setValue(editor->layout->getHeight()); - bwidthSpinBox->setValue(editor->layout->getBorderWidth()); - bheightSpinBox->setValue(editor->layout->getBorderHeight()); - if (projectConfig.useCustomBorderSize) { - form.addRow(new QLabel("Map Width"), widthSpinBox); - form.addRow(new QLabel("Map Height"), heightSpinBox); - form.addRow(new QLabel("Border Width"), bwidthSpinBox); - form.addRow(new QLabel("Border Height"), bheightSpinBox); - } else { - form.addRow(new QLabel("Width"), widthSpinBox); - form.addRow(new QLabel("Height"), heightSpinBox); - } - - QLabel *errorLabel = new QLabel(); - errorLabel->setStyleSheet("QLabel { color: red }"); - errorLabel->setVisible(false); - - QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); - form.addRow(&buttonBox); - connect(&buttonBox, &QDialogButtonBox::accepted, [&dialog, &widthSpinBox, &heightSpinBox, &errorLabel, this](){ - // Ensure width and height are an acceptable size. - // The maximum number of metatiles in a map is the following: - // max = (width + 15) * (height + 14) - // This limit can be found in fieldmap.c in pokeruby/pokeemerald/pokefirered. - int numMetatiles = editor->project->getMapDataSize(widthSpinBox->value(), heightSpinBox->value()); - int maxMetatiles = editor->project->getMaxMapDataSize(); - if (numMetatiles <= maxMetatiles) { - dialog.accept(); - } else { - QString errorText = QString("Error: The specified width and height are too large.\n" - "The maximum layout width and height is the following: (width + 15) * (height + 14) <= %1\n" - "The specified layout width and height was: (%2 + 15) * (%3 + 14) = %4") - .arg(maxMetatiles) - .arg(widthSpinBox->value()) - .arg(heightSpinBox->value()) - .arg(numMetatiles); - errorLabel->setText(errorText); - errorLabel->setVisible(true); - } - }); - connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - form.addRow(errorLabel); - - if (dialog.exec() == QDialog::Accepted) { - Layout *layout = editor->layout; - Blockdata oldMetatiles = layout->blockdata; - Blockdata oldBorder = layout->border; - QSize oldMapDimensions(layout->getWidth(), layout->getHeight()); + QMargins result = popup.getResult(); + QSize borderResult = popup.getBorderResult(); + QSize oldLayoutDimensions(layout->getWidth(), layout->getHeight()); QSize oldBorderDimensions(layout->getBorderWidth(), layout->getBorderHeight()); - QSize newMapDimensions(widthSpinBox->value(), heightSpinBox->value()); - QSize newBorderDimensions(bwidthSpinBox->value(), bheightSpinBox->value()); - if (oldMapDimensions != newMapDimensions || oldBorderDimensions != newBorderDimensions) { - layout->setDimensions(newMapDimensions.width(), newMapDimensions.height(), true, true); - layout->setBorderDimensions(newBorderDimensions.width(), newBorderDimensions.height(), true, true); - editor->layout->editHistory.push(new ResizeLayout(layout, - oldMapDimensions, newMapDimensions, + if (!result.isNull() || (borderResult != oldBorderDimensions)) { + Blockdata oldMetatiles = layout->blockdata; + Blockdata oldBorder = layout->border; + + layout->adjustDimensions(result); + layout->setBorderDimensions(borderResult.width(), borderResult.height(), true, true); + layout->editHistory.push(new ResizeLayout(layout, + oldLayoutDimensions, result, oldMetatiles, layout->blockdata, - oldBorderDimensions, newBorderDimensions, + oldBorderDimensions, borderResult, oldBorder, layout->border )); } + // If we're in map-editing mode, adjust the events' position by the same amount. + if (map) { + auto events = map->getEvents(); + int deltaX = result.left(); + int deltaY = result.top(); + if ((deltaX || deltaY) && !events.isEmpty()) { + map->commit(new EventShift(events, deltaX, deltaY, this->editor->eventShiftActionId++)); + } + } } } @@ -2848,8 +2805,15 @@ void MainWindow::reloadScriptEngine() { Scripting::populateGlobalObject(this); // Lying to the scripts here, simulating a project reload Scripting::cb_ProjectOpened(projectConfig.projectDir); - if (editor && editor->map) - Scripting::cb_MapOpened(editor->map->name()); // TODO: API should have equivalent for layout + if (this->editor) { + QString curName; + if (this->editor->map) + curName = this->editor->map->name(); + else if (editor->layout) + curName = this->editor->layout->name; + + Scripting::cb_MapOpened(curName); + } } void MainWindow::on_horizontalSlider_MetatileZoom_valueChanged(int value) { diff --git a/src/project.cpp b/src/project.cpp index 690ecdcc..fb2d3d2b 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -398,6 +398,7 @@ Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* t this->mapNameToMapSectionName.insert(map->name(), map->header()->location()); map->setIsPersistedToFile(false); + this->mapCache.insert(map->name(), map); emit mapCreated(map, settings.group); @@ -966,7 +967,7 @@ bool Project::loadLayoutTilesets(Layout *layout) { layout->tileset_primary = getTileset(layout->tileset_primary_label); if (!layout->tileset_primary) { QString defaultTileset = this->getDefaultPrimaryTilesetLabel(); - logWarn(QString("Map layout %1 has invalid primary tileset '%2'. Using default '%3'").arg(layout->id).arg(layout->tileset_primary_label).arg(defaultTileset)); + logWarn(QString("%1 has invalid primary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_primary_label).arg(defaultTileset)); layout->tileset_primary_label = defaultTileset; layout->tileset_primary = getTileset(layout->tileset_primary_label); if (!layout->tileset_primary) { @@ -978,7 +979,7 @@ bool Project::loadLayoutTilesets(Layout *layout) { layout->tileset_secondary = getTileset(layout->tileset_secondary_label); if (!layout->tileset_secondary) { QString defaultTileset = this->getDefaultSecondaryTilesetLabel(); - logWarn(QString("Map layout %1 has invalid secondary tileset '%2'. Using default '%3'").arg(layout->id).arg(layout->tileset_secondary_label).arg(defaultTileset)); + logWarn(QString("%1 has invalid secondary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_secondary_label).arg(defaultTileset)); layout->tileset_secondary_label = defaultTileset; layout->tileset_secondary = getTileset(layout->tileset_secondary_label); if (!layout->tileset_secondary) { @@ -1047,7 +1048,8 @@ bool Project::loadBlockdata(Layout *layout) { layout->lastCommitBlocks.layoutDimensions = QSize(layout->getWidth(), layout->getHeight()); if (layout->blockdata.count() != layout->getWidth() * layout->getHeight()) { - logWarn(QString("Layout blockdata length %1 does not match dimensions %2x%3 (should be %4). Resizing blockdata.") + logWarn(QString("%1 blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing blockdata.") + .arg(layout->name) .arg(layout->blockdata.count()) .arg(layout->getWidth()) .arg(layout->getHeight()) @@ -1084,7 +1086,8 @@ bool Project::loadLayoutBorder(Layout *layout) { int borderLength = layout->getBorderWidth() * layout->getBorderHeight(); if (layout->border.count() != borderLength) { - logWarn(QString("Layout border blockdata length %1 must be %2. Resizing border blockdata.") + logWarn(QString("%1 border blockdata length %2 must be %3. Resizing border blockdata.") + .arg(layout->name) .arg(layout->border.count()) .arg(borderLength)); layout->border.resize(borderLength); @@ -1207,7 +1210,7 @@ void Project::saveMap(Map *map) { } mapObj["connections"] = connectionsArr; } else { - mapObj["connections"] = QJsonValue::Null; + mapObj["connections"] = OrderedJson(); } if (map->sharedEventsMap().isEmpty()) { diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 2532f132..0ee3316e 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -794,7 +794,7 @@ void MainWindow::setMetatileTile(int metatileId, int tileIndex, QJSValue tileObj } QJSValue MainWindow::getTilePixels(int tileId) { - if (tileId < 0 || !this->editor || !this->editor->project || !this->editor->map || !this->editor->layout) + if (tileId < 0 || !this->editor || !this->editor->layout) return QJSValue(); QImage tileImage = getTileImage(tileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); if (tileImage.isNull() || tileImage.sizeInBytes() < 64) diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index e5cebc54..5d2072a9 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -249,6 +249,27 @@ QList ScriptUtility::getMapNames() { return window->editor->project->mapNames; } +QList ScriptUtility::getMapConstants() { + if (!window || !window->editor || !window->editor->project) + return QList(); + return window->editor->project->mapConstantsToMapNames.keys(); +} + +QList ScriptUtility::getLayoutNames() { + QList names; + if (!window || !window->editor || !window->editor->project) + return names; + for (const auto &layout : window->editor->project->mapLayouts) + names.append(layout->name); + return names; +} + +QList ScriptUtility::getLayoutConstants() { + if (!window || !window->editor || !window->editor->project) + return QList(); + return window->editor->project->layoutIds; +} + QList ScriptUtility::getTilesetNames() { if (!window || !window->editor || !window->editor->project) return QList(); diff --git a/src/ui/draggablepixmapitem.cpp b/src/ui/draggablepixmapitem.cpp index 03f1bb99..4880f530 100644 --- a/src/ui/draggablepixmapitem.cpp +++ b/src/ui/draggablepixmapitem.cpp @@ -29,7 +29,7 @@ void DraggablePixmapItem::emitPositionChanged() { void DraggablePixmapItem::updatePixmap() { editor->project->setEventPixmap(event, true); this->updatePosition(); - editor->redrawObject(this); + editor->redrawEventPixmapItem(this); emit spriteChanged(event->getPixmap()); } diff --git a/src/ui/filterchildrenproxymodel.cpp b/src/ui/filterchildrenproxymodel.cpp index 99464ae6..06d7a184 100644 --- a/src/ui/filterchildrenproxymodel.cpp +++ b/src/ui/filterchildrenproxymodel.cpp @@ -1,10 +1,6 @@ #include "filterchildrenproxymodel.h" -FilterChildrenProxyModel::FilterChildrenProxyModel(QObject *parent) : - QSortFilterProxyModel(parent) -{ - -} +#include bool FilterChildrenProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const { @@ -42,3 +38,16 @@ bool FilterChildrenProxyModel::filterAcceptsRow(int source_row, const QModelInde // parent call for initial behaviour return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); } + +bool NumericSortProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const { + QVariant l = (source_left.model() ? source_left.model()->data(source_left, sortRole()) : QVariant()); + QVariant r = (source_right.model() ? source_right.model()->data(source_right, sortRole()) : QVariant()); + + if (l.canConvert() && r.canConvert()) { + // We need to override lexical comparison of strings to do a numeric sort. + QCollator collator; + collator.setNumericMode(true); + return collator.compare(l.toString(), r.toString()) < 0; + } + return QSortFilterProxyModel::lessThan(source_left, source_right); +} diff --git a/src/ui/graphicsview.cpp b/src/ui/graphicsview.cpp index 73827211..68479e98 100644 --- a/src/ui/graphicsview.cpp +++ b/src/ui/graphicsview.cpp @@ -5,7 +5,7 @@ void GraphicsView::mousePressEvent(QMouseEvent *event) { QGraphicsView::mousePressEvent(event); if (editor) { - editor->objectsView_onMousePress(event); + editor->eventsView_onMousePress(event); } } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 4596ba36..0237795a 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -55,6 +55,12 @@ MapImageExporter::MapImageExporter(QWidget *parent, Editor *editor, ImageExporte ui->comboBox_MapSelection->addItems(editor->project->mapNames); ui->comboBox_MapSelection->setCurrentText(m_map->name()); ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down + } else { + // Some settings only apply to maps. When exporting an image in layout-only mode we hide them. + ui->comboBox_MapSelection->setVisible(false); + ui->label_MapSelection->setVisible(false); + ui->groupBox_Events->setVisible(false); + ui->groupBox_Connections->setVisible(false); } connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); diff --git a/src/ui/maplistmodels.cpp b/src/ui/maplistmodels.cpp index d08bb2cf..0d42b896 100644 --- a/src/ui/maplistmodels.cpp +++ b/src/ui/maplistmodels.cpp @@ -135,9 +135,6 @@ QStandardItem *MapListModel::insertMapItem(const QString &mapName, const QString if (folder) { folder->appendRow(map); } - - if (this->sortingEnabled) - this->sort(0, Qt::AscendingOrder); return map; } @@ -147,8 +144,6 @@ QStandardItem *MapListModel::insertMapFolderItem(const QString &folderName) { QStandardItem *item = createMapFolderItem(folderName); this->root->appendRow(item); - if (this->sortingEnabled) - this->sort(0, Qt::AscendingOrder); return item; } @@ -432,9 +427,6 @@ MapLocationModel::MapLocationModel(Project *project, QObject *parent) : MapListM for (const auto &mapName : this->project->mapNames) { insertMapItem(mapName, this->project->mapNameToMapSectionName.value(mapName)); } - - this->sortingEnabled = true; - sort(0, Qt::AscendingOrder); } void MapLocationModel::removeItem(QStandardItem *item) { @@ -459,9 +451,6 @@ LayoutTreeModel::LayoutTreeModel(Project *project, QObject *parent) : MapListMod for (const auto &mapName : this->project->mapNames) { insertMapItem(mapName, this->project->mapNameToLayoutId.value(mapName)); } - - this->sortingEnabled = true; - sort(0, Qt::AscendingOrder); } void LayoutTreeModel::removeItem(QStandardItem *) { diff --git a/src/ui/maplisttoolbar.cpp b/src/ui/maplisttoolbar.cpp index d03aa838..10a33c47 100644 --- a/src/ui/maplisttoolbar.cpp +++ b/src/ui/maplisttoolbar.cpp @@ -139,3 +139,7 @@ void MapListToolBar::applyFilter(const QString &filterText) { void MapListToolBar::clearFilter() { applyFilter(""); } + +void MapListToolBar::setSearchFocus() { + ui->lineEdit_filterBox->setFocus(); +} diff --git a/src/ui/metatileselector.cpp b/src/ui/metatileselector.cpp index 1214e289..b8bafefc 100644 --- a/src/ui/metatileselector.cpp +++ b/src/ui/metatileselector.cpp @@ -9,12 +9,17 @@ QPoint MetatileSelector::getSelectionDimensions() { return SelectablePixmapItem::getSelectionDimensions(); } +int MetatileSelector::numPrimaryMetatilesRounded() const { + // We round up the number of primary metatiles to keep the tilesets on separate rows. + return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; +} + void MetatileSelector::draw() { if (!this->primaryTileset || !this->secondaryTileset) { this->setPixmap(QPixmap()); } - int primaryLength = this->primaryTileset->numMetatiles(); + int primaryLength = this->numPrimaryMetatilesRounded(); int length_ = primaryLength + this->secondaryTileset->numMetatiles(); int height_ = length_ / this->numMetatilesWide; if (length_ % this->numMetatilesWide != 0) { @@ -149,7 +154,7 @@ void MetatileSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { void MetatileSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { QPoint pos = this->getCellPos(event->pos()); - if (!positionIsValid(pos) || this->cellPos == pos) + if (this->cellPos == pos) return; this->cellPos = pos; @@ -199,10 +204,11 @@ void MetatileSelector::updateExternalSelectedMetatiles() { uint16_t MetatileSelector::getMetatileId(int x, int y) const { int index = y * this->numMetatilesWide + x; - if (index < this->primaryTileset->numMetatiles()) { + int numPrimary = this->numPrimaryMetatilesRounded(); + if (index < numPrimary) { return static_cast(index); } else { - return static_cast(Project::getNumMetatilesPrimary() + index - this->primaryTileset->numMetatiles()); + return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); } } @@ -215,7 +221,7 @@ QPoint MetatileSelector::getMetatileIdCoords(uint16_t metatileId) { int index = metatileId < Project::getNumMetatilesPrimary() ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->primaryTileset->numMetatiles(); + : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); } diff --git a/src/ui/movablerect.cpp b/src/ui/movablerect.cpp index 55327dba..ba323185 100644 --- a/src/ui/movablerect.cpp +++ b/src/ui/movablerect.cpp @@ -1,17 +1,175 @@ +#include +#include +#include + #include "movablerect.h" MovableRect::MovableRect(bool *enabled, int width, int height, QRgb color) + : QGraphicsRectItem(0, 0, width, height) { this->enabled = enabled; - this->width = width; - this->height = height; this->color = color; this->setVisible(*enabled); } -void MovableRect::updateLocation(int x, int y) -{ - this->setX((x * 16) - this->width / 2 + 8); - this->setY((y * 16) - this->height / 2 + 8); +/// Center rect on grid position (x, y) +void MovableRect::updateLocation(int x, int y) { + this->setRect((x * 16) - this->rect().width() / 2 + 8, (y * 16) - this->rect().height() / 2 + 8, this->rect().width(), this->rect().height()); this->setVisible(*this->enabled); } + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +int roundUp(int numToRound, int multiple) { + return (numToRound + multiple - 1) & -multiple; +} + +ResizableRect::ResizableRect(QObject *parent, bool *enabled, int width, int height, QRgb color) + : QObject(parent), + MovableRect(enabled, width * 16, height * 16, color) +{ + setZValue(0xFFFFFFFF); // ensure on top of view + setAcceptHoverEvents(true); + setFlags(this->flags() | QGraphicsItem::ItemIsMovable); +} + +ResizableRect::Edge ResizableRect::detectEdge(int x, int y) { + QRectF edge = this->boundingRect(); + if (x <= edge.left() + this->lineWidth) { + if (y >= edge.top() + 2 * this->lineWidth) { + if (y <= edge.bottom() - 2 * this->lineWidth) { + return ResizableRect::Edge::Left; + } + else { + return ResizableRect::Edge::BottomLeft; + } + } + else { + return ResizableRect::Edge::TopLeft; + } + } + else if (x >= edge.right() - this->lineWidth) { + if (y >= edge.top() + 2 * this->lineWidth) { + if (y <= edge.bottom() - 2 * this->lineWidth) { + return ResizableRect::Edge::Right; + } + else { + return ResizableRect::Edge::BottomRight; + } + } + else { + return ResizableRect::Edge::TopRight; + } + } + else { + if (y <= edge.top() + this->lineWidth) { + return ResizableRect::Edge::Top; + } + else if (y >= edge.bottom() - this->lineWidth) { + return ResizableRect::Edge::Bottom; + } + } + return ResizableRect::Edge::None; +} + +void ResizableRect::updatePosFromRect(QRect newRect) { + prepareGeometryChange(); + this->setRect(newRect); + emit this->rectUpdated(newRect); +} + +void ResizableRect::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { + switch (this->detectEdge(event->pos().x(), event->pos().y())) { + case ResizableRect::Edge::None: + default: + break; + case ResizableRect::Edge::Left: + case ResizableRect::Edge::Right: + this->setCursor(Qt::SizeHorCursor); + break; + case ResizableRect::Edge::Top: + case ResizableRect::Edge::Bottom: + this->setCursor(Qt::SizeVerCursor); + break; + case ResizableRect::Edge::TopRight: + case ResizableRect::Edge::BottomLeft: + this->setCursor(Qt::SizeBDiagCursor); + break; + case ResizableRect::Edge::TopLeft: + case ResizableRect::Edge::BottomRight: + this->setCursor(Qt::SizeFDiagCursor); + break; + } +} + +void ResizableRect::hoverLeaveEvent(QGraphicsSceneHoverEvent *) { + this->unsetCursor(); +} + +void ResizableRect::mousePressEvent(QGraphicsSceneMouseEvent *event) { + int x = event->pos().x(); + int y = event->pos().y(); + this->clickedPos = event->scenePos(); + this->clickedRect = this->rect().toAlignedRect(); + this->clickedEdge = this->detectEdge(x, y); +} + +void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + int dx = roundUp(event->scenePos().x() - this->clickedPos.x(), 16); + int dy = roundUp(event->scenePos().y() - this->clickedPos.y(), 16); + + QRect resizedRect = this->clickedRect; + + switch (this->clickedEdge) { + case ResizableRect::Edge::None: + default: + return; + case ResizableRect::Edge::Left: + resizedRect.adjust(dx, 0, 0, 0); + break; + case ResizableRect::Edge::Right: + resizedRect.adjust(0, 0, dx, 0); + break; + case ResizableRect::Edge::Top: + resizedRect.adjust(0, dy, 0, 0); + break; + case ResizableRect::Edge::Bottom: + resizedRect.adjust(0, 0, 0, dy); + break; + case ResizableRect::Edge::TopRight: + resizedRect.adjust(0, dy, dx, 0); + break; + case ResizableRect::Edge::BottomLeft: + resizedRect.adjust(dx, 0, 0, dy); + break; + case ResizableRect::Edge::TopLeft: + resizedRect.adjust(dx, dy, 0, 0); + break; + case ResizableRect::Edge::BottomRight: + resizedRect.adjust(0, 0, dx, dy); + break; + } + + // Lower limits: smallest possible size is 16x16 square + if (resizedRect.width() < 16) { + if (dx < 0) { // right sided adjustment made + resizedRect.setWidth(16); + } else { // left sided adjustment slightly more complicated + int dxMax = this->clickedRect.right() - this->clickedRect.left() - 16; + resizedRect.adjust(dxMax - dx, 0, 0, 0); + } + } + if (resizedRect.height() < 16) { + if (dy < 0) { // bottom + resizedRect.setHeight(16); + } else { // top + int dyMax = this->clickedRect.bottom() - this->clickedRect.top() - 16; + resizedRect.adjust(0, dyMax - dy, 0, 0); + } + } + + // Upper limits: clip resized to limit rect + this->updatePosFromRect(resizedRect & this->limit); +} diff --git a/src/ui/neweventtoolbutton.cpp b/src/ui/neweventtoolbutton.cpp index dee658e4..f1a70b42 100644 --- a/src/ui/neweventtoolbutton.cpp +++ b/src/ui/neweventtoolbutton.cpp @@ -50,7 +50,7 @@ void NewEventToolButton::init() this->newSecretBaseAction->setIcon(QIcon(":/icons/add.ico")); connect(this->newSecretBaseAction, &QAction::triggered, this, &NewEventToolButton::newSecretBase); - QMenu *alignMenu = new QMenu(); + QMenu *alignMenu = new QMenu(this); alignMenu->addAction(this->newObjectAction); alignMenu->addAction(this->newCloneObjectAction); alignMenu->addAction(this->newWarpAction); diff --git a/src/ui/noscrollspinbox.cpp b/src/ui/noscrollspinbox.cpp index f8d1d444..3493d4ee 100644 --- a/src/ui/noscrollspinbox.cpp +++ b/src/ui/noscrollspinbox.cpp @@ -1,5 +1,6 @@ #include "noscrollspinbox.h" #include +#include unsigned actionId = 0xffff; @@ -25,6 +26,10 @@ void NoScrollSpinBox::focusOutEvent(QFocusEvent *event) { QSpinBox::focusOutEvent(event); } +void NoScrollSpinBox::setLineEditEnabled(bool enabled) { + this->lineEdit()->setReadOnly(!enabled); +} + unsigned NoScrollSpinBox::getActionId() { return actionId; } diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 1bb17307..1250e8dd 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -112,7 +112,7 @@ void ProjectSettingsEditor::initUi() { // Validate that the border metatiles text is a comma-separated list of metatile values static const QString regex_Hex = "(0[xX])?[A-Fa-f0-9]+"; static const QRegularExpression expression_HexList(QString("^(%1,)*%1$").arg(regex_Hex)); // Comma-separated list of hex values - QRegularExpressionValidator *validator_HexList = new QRegularExpressionValidator(expression_HexList); + QRegularExpressionValidator *validator_HexList = new QRegularExpressionValidator(expression_HexList, this); ui->lineEdit_BorderMetatiles->setValidator(validator_HexList); this->setBorderMetatilesUi(projectConfig.useCustomBorderSize); diff --git a/src/ui/resizelayoutpopup.cpp b/src/ui/resizelayoutpopup.cpp new file mode 100644 index 00000000..45559b70 --- /dev/null +++ b/src/ui/resizelayoutpopup.cpp @@ -0,0 +1,210 @@ +#include "resizelayoutpopup.h" +#include "editor.h" +#include "movablerect.h" +#include "config.h" + +#include "ui_resizelayoutpopup.h" + +// TODO: put this in a util file or something +extern int roundUp(int, int); + +CheckeredBgScene::CheckeredBgScene(QObject *parent) : QGraphicsScene(parent) { } + +void CheckeredBgScene::drawBackground(QPainter *painter, const QRectF &rect) { + QRect r = rect.toRect(); + int xMin = r.left() - r.left() % this->gridSize - this->gridSize; + int yMin = r.top() - r.top() % this->gridSize - this->gridSize; + int xMax = r.right() - r.right() % this->gridSize + this->gridSize; + int yMax = r.bottom() - r.bottom() % this->gridSize + this->gridSize; + + // draw grid 16x16 from top to bottom of scene + QColor paintColor(0x00ff00); + for (int x = xMin, xTile = 0; x <= xMax; x += this->gridSize, xTile++) { + for (int y = yMin, yTile = 0; y <= yMax; y += this->gridSize, yTile++) { + if (!((xTile ^ yTile) & 1)) { // tile numbers have same parity (evenness) + if (this->validRect.contains(x, y)) + paintColor = QColor(132, 217, 165); // green light color + else + paintColor = 0xbcbcbc; // normal light color + } + else { + if (this->validRect.contains(x, y)) + paintColor = QColor(76, 178, 121); // green dark color + else + paintColor = 0x969696; // normal dark color + } + painter->fillRect(QRect(x, y, this->gridSize, this->gridSize), paintColor); + } + } +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +BoundedPixmapItem::BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { + setFlags(this->flags() | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges | QGraphicsItem::ItemIsSelectable); +} + +void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { + // Draw the pixmap darkened in the background + painter->fillRect(this->boundingRect().toAlignedRect(), QColor(0x444444)); + painter->setCompositionMode(QPainter::CompositionMode_Multiply); + painter->drawPixmap(this->boundingRect().toAlignedRect(), this->pixmap()); + + // draw the normal pixmap on top, cropping to validRect as needed + painter->setCompositionMode(QPainter::CompositionMode_SourceOver); + QRect intersection = this->mapRectFromScene(this->boundary->rect()).toAlignedRect() & this->boundingRect().toAlignedRect(); + QPixmap cropped = this->pixmap().copy(intersection); + painter->drawPixmap(intersection, cropped); +} + +QVariant BoundedPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { + if (change == ItemPositionChange && scene()) { + QPointF newPos = value.toPointF(); + return QPointF(roundUp(newPos.x(), 16), roundUp(newPos.y(), 16)); + } + else + return QGraphicsItem::itemChange(change, value); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +ResizeLayoutPopup::ResizeLayoutPopup(QWidget *parent, Editor *editor) : + QDialog(parent), + parent(parent), + editor(editor), + ui(new Ui::ResizeLayoutPopup) +{ + ui->setupUi(this); + this->resetPosition(); + this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint); + this->setWindowModality(Qt::ApplicationModal); + + this->scene = new CheckeredBgScene(this); + this->ui->graphicsView->setScene(this->scene); + this->ui->graphicsView->setRenderHints(QPainter::Antialiasing); + this->ui->graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); +} + +ResizeLayoutPopup::~ResizeLayoutPopup() { + delete ui; +} + +/// Reset position of the dialog to cover the MainWindow's layout metatile scene +void ResizeLayoutPopup::resetPosition() { + this->setGeometry(QRect(parent->mapToGlobal(QPoint(0, 0)), parent->size())); +} + +void ResizeLayoutPopup::on_buttonBox_clicked(QAbstractButton *button) { + if(button == this->ui->buttonBox->button(QDialogButtonBox::Reset) ) { + this->scene->clear(); + setupLayoutView(); + } +} + +/// Custom scene contains +/// (1) pixmap representing the current layout / not resizable / drag-movable +/// (1) layout outline / resizable / not movable +void ResizeLayoutPopup::setupLayoutView() { + if (!this->editor || !this->editor->layout) return; + + // Border stuff + bool bordersEnabled = projectConfig.useCustomBorderSize; + if (bordersEnabled) { + this->ui->spinBox_borderWidth->setMinimum(1); + this->ui->spinBox_borderHeight->setMinimum(1); + this->ui->spinBox_borderWidth->setMaximum(MAX_BORDER_WIDTH); + this->ui->spinBox_borderHeight->setMaximum(MAX_BORDER_HEIGHT); + } else { + this->ui->frame_border->setVisible(false); + } + this->ui->spinBox_borderWidth->setValue(this->editor->layout->getBorderWidth()); + this->ui->spinBox_borderHeight->setValue(this->editor->layout->getBorderHeight()); + + // Layout stuff + QPixmap pixmap = this->editor->layout->pixmap; + this->layoutPixmap = new BoundedPixmapItem(pixmap); + this->scene->addItem(layoutPixmap); + int maxWidth = this->editor->project->getMaxMapWidth(); + int maxHeight = this->editor->project->getMaxMapHeight(); + QGraphicsRectItem *cover = new QGraphicsRectItem(-maxWidth * 8, -maxHeight * 8, maxWidth * 16, maxHeight * 16); + this->scene->addItem(cover); + + this->ui->spinBox_width->setMinimum(1); + this->ui->spinBox_width->setMaximum(maxWidth); + this->ui->spinBox_height->setMinimum(1); + this->ui->spinBox_height->setMaximum(maxHeight); + + static bool layoutSizeRectVisible = true; + + this->outline = new ResizableRect(this, &layoutSizeRectVisible, this->editor->layout->getWidth(), this->editor->layout->getHeight(), qRgb(255, 0, 255)); + this->outline->setLimit(cover->rect().toAlignedRect()); + connect(outline, &ResizableRect::rectUpdated, [=](QRect rect){ + // Note: this extra limit check needs access to the project values, so it is done here and not ResizableRect::mouseMoveEvent + // Upper limits: maximum metatiles in a map formula: + // max = (width + 15) * (height + 14) + // This limit can be found in fieldmap.c in pokeruby/pokeemerald/pokefirered. + int numMetatiles = editor->project->getMapDataSize(rect.width() / 16, rect.height() / 16); + int maxMetatiles = editor->project->getMaxMapDataSize(); + if (numMetatiles > maxMetatiles) { + QString errorText = QString("The maximum layout width and height is the following: (width + 15) * (height + 14) <= %1\n" + "The specified layout width and height was: (%2 + 15) * (%3 + 14) = %4") + .arg(maxMetatiles) + .arg(rect.width() / 16) + .arg(rect.height() / 16) + .arg(numMetatiles); + QMessageBox warning; + warning.setIcon(QMessageBox::Warning); + warning.setText("The specified width and height are too large."); + warning.setInformativeText(errorText); + warning.setStandardButtons(QMessageBox::Ok); + warning.setDefaultButton(QMessageBox::Ok); + warning.exec(); + // adjust rect to last accepted size + rect = this->scene->getValidRect(); + } + this->scene->setValidRect(rect); + this->outline->setRect(rect); + this->ui->spinBox_width->setValue(rect.width() / 16); + this->ui->spinBox_height->setValue(rect.height() / 16); + }); + scene->addItem(outline); + + layoutPixmap->setBoundary(outline); + this->outline->rectUpdated(outline->rect().toAlignedRect()); + + // TODO: is this an ideal size for all maps, or should this adjust based on starting dimensions? + this->ui->graphicsView->setTransform(QTransform::fromScale(0.5, 0.5)); + this->ui->graphicsView->centerOn(layoutPixmap); +} + +void ResizeLayoutPopup::on_spinBox_width_valueChanged(int value) { + if (!this->outline) return; + QRectF rect = this->outline->rect(); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), value * 16, rect.height())); +} + +void ResizeLayoutPopup::on_spinBox_height_valueChanged(int value) { + if (!this->outline) return; + QRectF rect = this->outline->rect(); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), rect.width(), value * 16)); +} + +/// Result is the number of metatiles to add (or subtract) to each side of the map after dimension changes +QMargins ResizeLayoutPopup::getResult() { + QMargins result = QMargins(); + + result.setLeft(this->layoutPixmap->x() - this->outline->rect().left()); + result.setTop(this->layoutPixmap->y() - this->outline->rect().top()); + result.setRight(this->outline->rect().right() - (this->layoutPixmap->x() + this->layoutPixmap->pixmap().width())); + result.setBottom(this->outline->rect().bottom() - (this->layoutPixmap->y() + this->layoutPixmap->pixmap().height())); + + return result / 16; +} + +QSize ResizeLayoutPopup::getBorderResult() { + return QSize(this->ui->spinBox_borderWidth->value(), this->ui->spinBox_borderHeight->value()); +} diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp index 36bc30d0..d0ef943d 100644 --- a/src/ui/shortcutseditor.cpp +++ b/src/ui/shortcutseditor.cpp @@ -2,6 +2,7 @@ #include "ui_shortcutseditor.h" #include "config.h" #include "multikeyedit.h" +#include "message.h" #include "log.h" #include @@ -46,6 +47,15 @@ void ShortcutsEditor::setShortcutableObjects(const QObjectList &shortcutableObje void ShortcutsEditor::saveShortcuts() { QMultiMap objects_keySequences; for (auto it = multiKeyEdits_objects.cbegin(); it != multiKeyEdits_objects.cend(); ++it) { + if (!it.value()) { + // Some shortcuts cannot be saved. Pointers in the object map can become null if they are + // deleted externally while the shortcuts editor is open. Ideally we should try to restore + // the original object so the shortcut can be saved. Alternatively this could generally be + // prevented by making the shortcuts editor modal. For now, saving these shortcuts is skipped, + // and we warn the user that this happened. + ErrorMessage::show(QStringLiteral("Some shortcuts failed to save. Please close the Shortcuts Editor and retry."), this); + return; + } if (it.key()->keySequences().isEmpty()) objects_keySequences.insert(it.value(), QKeySequence()); for (auto keySequence : it.key()->keySequences()) @@ -60,6 +70,7 @@ void ShortcutsEditor::saveShortcuts() { // Restores default shortcuts but doesn't save until Apply or OK is clicked. void ShortcutsEditor::resetShortcuts() { for (auto it = multiKeyEdits_objects.begin(); it != multiKeyEdits_objects.end(); ++it) { + if (!it.value()) continue; it.key()->blockSignals(true); const auto defaults = shortcutsConfig.defaultShortcuts(it.value()); it.key()->setKeySequences(defaults); @@ -90,6 +101,7 @@ bool ShortcutsEditor::stringPropertyIsNotEmpty(const QObject *object, const char void ShortcutsEditor::populateMainContainer() { for (auto object : labels_objects) { + if (!object) continue; const auto shortcutContext = getShortcutContext(object); if (!contexts_layouts.contains(shortcutContext)) addNewContextGroup(shortcutContext); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index e05750dc..23942a8a 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -29,13 +29,7 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) this->tileYFlip = ui->checkBox_yFlip->isChecked(); this->paletteId = ui->spinBox_paletteSelector->value(); - // TODO: The dividing line at the moment is only accurate if the number of primary metatiles is divisible by 8. - // If it's not, the secondary metatiles will wrap above the line. This has other problems (like skewing - // metatile groups the user may have designed) so this should be fixed by filling the primary metatiles - // image with invalid magenta metatiles until it's divisible by 8. Then the line can be re-enabled as-is. - ui->actionShow_Tileset_Divider->setChecked(/*porymapConfig.showTilesetEditorDivider*/false); - ui->actionShow_Tileset_Divider->setVisible(false); - + ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); ui->spinBox_paletteSelector->setMinimum(0); ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); ui->lineEdit_metatileLabel->setValidator(new IdentifierValidator(this)); @@ -759,7 +753,7 @@ void TilesetEditor::on_actionChange_Metatiles_Count_triggered() { QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); dialog.setWindowTitle("Change Number of Metatiles"); - dialog.setWindowModality(Qt::NonModal); + dialog.setWindowModality(Qt::WindowModal); QFormLayout form(&dialog); @@ -1051,20 +1045,19 @@ void TilesetEditor::countMetatileUsage() { // do not double count metatileSelector->usedMetatiles.fill(0); - for (auto layout : this->project->mapLayouts.values()) { - bool usesPrimary = false; - bool usesSecondary = false; + for (auto layout : this->project->mapLayouts) { + // It's possible for a layout's tileset labels to change if they are invalid, + // so we need to load all the tilesets even if they aren't the tileset we're looking for. + // Otherwise the metatile usage counts may change because the layouts with invalid tilesets + // were updated to use a tileset we were looking for. + this->project->loadLayoutTilesets(layout); - if (layout->tileset_primary_label == this->primaryTileset->name) { - usesPrimary = true; - } - - if (layout->tileset_secondary_label == this->secondaryTileset->name) { - usesSecondary = true; - } + bool usesPrimary = (layout->tileset_primary_label == this->primaryTileset->name); + bool usesSecondary = (layout->tileset_secondary_label == this->secondaryTileset->name); if (usesPrimary || usesSecondary) { - this->project->loadLayout(layout); + if (!this->project->loadLayout(layout)) + continue; // for each block in the layout, mark in the vector that it is used for (int i = 0; i < layout->blockdata.length(); i++) { @@ -1097,9 +1090,9 @@ void TilesetEditor::countTileUsage() { QSet secondaryTilesets; for (auto layout : this->project->mapLayouts.values()) { + this->project->loadLayoutTilesets(layout); if (layout->tileset_primary_label == this->primaryTileset->name || layout->tileset_secondary_label == this->secondaryTileset->name) { - this->project->loadLayoutTilesets(layout); // need to check metatiles if (layout->tileset_primary && layout->tileset_secondary) { primaryTilesets.insert(layout->tileset_primary); diff --git a/src/ui/tileseteditormetatileselector.cpp b/src/ui/tileseteditormetatileselector.cpp index 6175a923..07e50bba 100644 --- a/src/ui/tileseteditormetatileselector.cpp +++ b/src/ui/tileseteditormetatileselector.cpp @@ -22,11 +22,16 @@ int TilesetEditorMetatileSelector::numRows(int numMetatiles) { } int TilesetEditorMetatileSelector::numRows() { - return this->numRows(this->primaryTileset->numMetatiles() + this->secondaryTileset->numMetatiles()); + return this->numRows(this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); +} + +int TilesetEditorMetatileSelector::numPrimaryMetatilesRounded() const { + // We round up the number of primary metatiles to keep the tilesets on separate rows. + return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; } QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() { - return this->buildImage(0, this->primaryTileset->numMetatiles() + this->secondaryTileset->numMetatiles()); + return this->buildImage(0, this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); } QImage TilesetEditorMetatileSelector::buildPrimaryMetatilesImage() { @@ -39,11 +44,11 @@ QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() { QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) { int numMetatilesHigh = this->numRows(numMetatiles); - int numPrimary = this->primaryTileset->numMetatiles(); + int numPrimary = this->numPrimaryMetatilesRounded(); int maxPrimary = Project::getNumMetatilesPrimary(); bool includesPrimary = metatileIdStart < maxPrimary; - QImage image(this->numMetatilesWide * 32, numMetatilesHigh * 32, QImage::Format_RGBA8888); + 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++) { @@ -57,10 +62,10 @@ QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMet this->layout->metatileLayerOrder, this->layout->metatileLayerOpacity, true) - .scaled(32, 32); + .scaled(this->cellWidth, this->cellHeight); int map_y = i / this->numMetatilesWide; int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * 32, map_y * 32); + QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); painter.drawImage(metatile_origin, metatile_image); } painter.end(); @@ -107,10 +112,11 @@ uint16_t TilesetEditorMetatileSelector::getSelectedMetatileId() { uint16_t TilesetEditorMetatileSelector::getMetatileId(int x, int y) { int index = y * this->numMetatilesWide + x; - if (index < this->primaryTileset->numMetatiles()) { + int numPrimary = numPrimaryMetatilesRounded(); + if (index < numPrimary) { return static_cast(index); } else { - return static_cast(Project::getNumMetatilesPrimary() + index - this->primaryTileset->numMetatiles()); + return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); } } @@ -156,7 +162,7 @@ QPoint TilesetEditorMetatileSelector::getMetatileIdCoords(uint16_t metatileId) { } int index = metatileId < Project::getNumMetatilesPrimary() ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->primaryTileset->numMetatiles(); + : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); } @@ -176,12 +182,12 @@ void TilesetEditorMetatileSelector::drawGrid() { const int numColumns = this->numMetatilesWide; const int numRows = this->numRows(); for (int column = 1; column < numColumns; column++) { - int x = column * 32; - painter.drawLine(x, 0, x, numRows * 32); + int x = column * this->cellWidth; + painter.drawLine(x, 0, x, numRows * this->cellHeight); } for (int row = 1; row < numRows; row++) { - int y = row * 32; - painter.drawLine(0, y, numColumns * 32, y); + int y = row * this->cellHeight; + painter.drawLine(0, y, numColumns * this->cellWidth, y); } painter.end(); this->setPixmap(pixmap); @@ -191,12 +197,12 @@ void TilesetEditorMetatileSelector::drawDivider() { if (!this->showDivider) return; - const int y = this->numRows(this->primaryTileset->numMetatiles()) * 32; + const int y = this->numRows(this->numPrimaryMetatilesRounded()) * this->cellHeight; QPixmap pixmap = this->pixmap(); QPainter painter(&pixmap); painter.setPen(Qt::white); - painter.drawLine(0, y, this->numMetatilesWide * 32, y); + painter.drawLine(0, y, this->numMetatilesWide * this->cellWidth, y); painter.end(); this->setPixmap(pixmap); } @@ -212,7 +218,7 @@ void TilesetEditorMetatileSelector::drawFilters() { void TilesetEditorMetatileSelector::drawUnused() { // setup the circle with a line through it image to layer above unused metatiles - QPixmap redX(32, 32); + QPixmap redX(this->cellWidth, this->cellHeight); redX.fill(Qt::transparent); QPen whitePen(Qt::white); @@ -223,21 +229,21 @@ void TilesetEditorMetatileSelector::drawUnused() { QPainter oPainter(&redX); oPainter.setPen(whitePen); - oPainter.drawEllipse(QRect(1, 1, 30, 30)); + oPainter.drawEllipse(QRect(1, 1, this->cellWidth - 2, this->cellHeight - 2)); oPainter.setPen(pinkPen); - oPainter.drawEllipse(QRect(2, 2, 28, 28)); - oPainter.drawEllipse(QRect(3, 3, 26, 26)); + oPainter.drawEllipse(QRect(2, 2, this->cellWidth - 4, this->cellHeight - 4)); + oPainter.drawEllipse(QRect(3, 3, this->cellWidth - 6, this->cellHeight - 6)); oPainter.setPen(whitePen); - oPainter.drawEllipse(QRect(4, 4, 24, 24)); + oPainter.drawEllipse(QRect(4, 4, this->cellHeight - 8, this->cellHeight - 8)); whitePen.setWidth(5); oPainter.setPen(whitePen); - oPainter.drawLine(0, 0, 31, 31); + oPainter.drawLine(0, 0, this->cellWidth - 1, this->cellHeight - 1); pinkPen.setWidth(3); oPainter.setPen(pinkPen); - oPainter.drawLine(2, 2, 29, 29); + oPainter.drawLine(2, 2, this->cellWidth - 3, this->cellHeight - 3); oPainter.end(); @@ -247,19 +253,13 @@ void TilesetEditorMetatileSelector::drawUnused() { QPainter unusedPainter(&metatilesPixmap); unusedPainter.setOpacity(0.5); - int primaryLength = this->primaryTileset->numMetatiles(); - int length_ = primaryLength + this->secondaryTileset->numMetatiles(); - - for (int i = 0; i < length_; i++) { - int tile = i; - if (i >= primaryLength) { - tile += Project::getNumMetatilesPrimary() - primaryLength; - } - if (!usedMetatiles[tile]) { - unusedPainter.drawPixmap((i % 8) * 32, (i / 8) * 32, redX); - } + for (int metatileId = 0; metatileId < this->usedMetatiles.size(); metatileId++) { + if (this->usedMetatiles.at(metatileId) || !Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + continue; + // Adjust position from center to top-left corner + QPoint pos = getMetatileIdCoordsOnWidget(metatileId) - QPoint(this->cellWidth / 2, this->cellHeight / 2); + unusedPainter.drawPixmap(pos.x(), pos.y(), redX); } - unusedPainter.end(); this->setPixmap(metatilesPixmap); @@ -268,38 +268,28 @@ void TilesetEditorMetatileSelector::drawUnused() { void TilesetEditorMetatileSelector::drawCounts() { QPen blackPen(Qt::black); blackPen.setWidth(1); - - QPixmap metatilesPixmap = this->pixmap(); - - QPainter countPainter(&metatilesPixmap); - countPainter.setPen(blackPen); - - for (int tile = 0; tile < this->usedMetatiles.size(); tile++) { - int count = usedMetatiles[tile]; - QString countText = QString::number(count); - if (count > 1000) countText = ">1k"; - countPainter.drawText((tile % 8) * 32, (tile / 8) * 32 + 32, countText); - } - - // write in white and black for contrast QPen whitePen(Qt::white); whitePen.setWidth(1); - countPainter.setPen(whitePen); - int primaryLength = this->primaryTileset->numMetatiles(); - int length_ = primaryLength + this->secondaryTileset->numMetatiles(); + QPixmap metatilesPixmap = this->pixmap(); + QPainter countPainter(&metatilesPixmap); - for (int i = 0; i < length_; i++) { - int tile = i; - if (i >= primaryLength) { - tile += Project::getNumMetatilesPrimary() - primaryLength; - } - int count = usedMetatiles[tile]; - QString countText = QString::number(count); - if (count > 1000) countText = ">1k"; - countPainter.drawText((i % 8) * 32 + 1, (i / 8) * 32 + 32 - 1, countText); + for (int metatileId = 0; metatileId < this->usedMetatiles.size(); metatileId++) { + if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + continue; + + int count = this->usedMetatiles.at(metatileId); + QString countText = (count > 1000) ? QStringLiteral(">1k") : QString::number(count); + + // Adjust position from center to bottom-left corner + QPoint pos = getMetatileIdCoordsOnWidget(metatileId) + QPoint(-(this->cellWidth / 2), this->cellHeight / 2); + + // write in black and white for contrast + countPainter.setPen(blackPen); + countPainter.drawText(pos.x(), pos.y(), countText); + countPainter.setPen(whitePen); + countPainter.drawText(pos.x() + 1, pos.y() - 1, countText); } - countPainter.end(); this->setPixmap(metatilesPixmap); diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 80c41f81..c27e7968 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -109,7 +109,7 @@ void WildMonChart::readTable() { } // Read data from the table, combining data for duplicate entries - const QList tableFrequencies = this->table->percentages(); + const QVector tableFrequencies = this->table->percentages(); const QVector tablePokemon = this->table->encounterData().wildPokemon; const int numRows = qMin(tableFrequencies.length(), tablePokemon.length()); const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix); diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp index 684d2947..f43a0dab 100644 --- a/src/ui/wildmonsearch.cpp +++ b/src/ui/wildmonsearch.cpp @@ -58,13 +58,13 @@ void WildMonSearch::addTableEntry(const RowData &rowData) { int row = ui->table_Results->rowCount(); ui->table_Results->insertRow(row); - auto groupItem = new QTableWidgetItem(rowData.groupName); + auto groupItem = new NumericSortTableItem(rowData.groupName); groupItem->setData(ResultsDataRole::MapName, rowData.mapName); ui->table_Results->setItem(row, ResultsColumn::Group, groupItem); - ui->table_Results->setItem(row, ResultsColumn::Field, new QTableWidgetItem(rowData.fieldName)); - ui->table_Results->setItem(row, ResultsColumn::Level, new QTableWidgetItem(rowData.levelRange)); - ui->table_Results->setItem(row, ResultsColumn::Chance, new QTableWidgetItem(rowData.chance)); + ui->table_Results->setItem(row, ResultsColumn::Field, new NumericSortTableItem(rowData.fieldName)); + ui->table_Results->setItem(row, ResultsColumn::Level, new NumericSortTableItem(rowData.levelRange)); + ui->table_Results->setItem(row, ResultsColumn::Chance, new NumericSortTableItem(rowData.chance)); } QList WildMonSearch::search(const QString &species) const { @@ -137,7 +137,6 @@ void WildMonSearch::updateResults(const QString &species) { } } - // TODO: This does a lexical sort... We might need custom item delegates to get proper numerical sorting in this table. ui->table_Results->setSortingEnabled(true); this->resultsCache.insert(species, results);