diff --git a/CHANGELOG.md b/CHANGELOG.md index dfee00511..db886e709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,14 @@ The **"Breaking Changes"** listed below are changes that have been made in the d ## [Unreleased] ### Added +- 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. - Add a `Close Project` option - An alert will be displayed when attempting to open a seemingly invalid project. ### Changed +- Edits to map connections now have Undo/Redo and can be viewed in exported timelapses. +- Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. +- A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". - It's now possible to cancel quitting if there are unsaved changes in sub-windows. @@ -25,6 +29,13 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix selections with multiple Events not always clearing when making a new selection. - Fix `About porymap` opening a new window each time it's activated. - 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 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. +- Fix a visual issue when quickly dragging map connections around. +- Fix map connections rendering incorrectly if their direction name was unknown. +- Fix map connections rendering incorrectly if their dimensions were smaller than the border draw distance. ## [5.4.1] - 2024-03-21 ### Fixed diff --git a/docsrc/manual/editing-map-connections.rst b/docsrc/manual/editing-map-connections.rst index 88194cee8..46af01208 100644 --- a/docsrc/manual/editing-map-connections.rst +++ b/docsrc/manual/editing-map-connections.rst @@ -2,35 +2,40 @@ Editing Map Connections *********************** -Maps can be connected together so that the player can seamlessly walk between them. These connections can be edited in the Connections tab. +Maps can be connected together so that the player can seamlessly walk between them. These connections can be edited in the Connections tab. .. figure:: images/editing-map-connections/map-connections.png :alt: Map Connections View Map Connections View -A connection has a direction, offset, and destination map. To add new connection, press the plus button |add-connection-button|. To delete a connection, select it and press the delete button |remove-connection-button|. +A connection has a direction, offset, and destination map. To add a new connection, press the |add-connection-button| button. To delete a connection you can either press the |remove-connection-button| button on its entry in the list, or select the connection and press the delete key. + +The |open-connection-button| button will open the destination map. You may also open the destination map by double-clicking the connection itself (this can be done from the ``Map`` and ``Events`` tabs as well). .. |add-connection-button| image:: images/editing-map-connections/add-connection-button.png + :height: 24 .. |remove-connection-button| image:: images/editing-map-connections/remove-connection-button.png + :height: 24 + +.. |open-connection-button| + image:: images/editing-map-connections/open-connection-button.png + :height: 24 To change the connection's vertical or horizontal offset, it's easiest to click and drag the connection to the desired offset. Dive & Emerge Warps ------------------- -Dive & emerge warps are used for the HM move Dive. They don't have offsets or directions--only a destination map. To add a dive or emerge warp, simply add a value in the Dive Map and/or Emerge Map dropdown menus. +Dive & emerge warps are used for the HM move Dive. They don't have offsets or directions--only a destination map. To add a dive or emerge warp, simply add a value in the Dive Map and/or Emerge Map dropdown menus. + +You can select the ``Show Emerge/Dive Maps`` checkbox to view your connected dive/emerge maps overlaid on the current map. The slider will change the opacity of this overlay. If you have both an emerge and a dive map connected you will see two sliders; the top slider is for the emerge map's opacity, and the bottom slider is for the dive map's opacity. Mirror Connections ------------------ An extremely useful feature is the *Mirror to Connecting Maps* checkbox in the top-right corner. Connections are one-way, which means that you must keep the two connections in sync between the two maps. For example, Petalburg City has a west connection to Route 104, and Route 104 has an east connection to Petalburg City. If *Mirror to Connecting Maps* is enabled, then Porymap will automatically update both sides of the connection. Be sure to *File -> Save All* (``Ctrl+Shift+S``) when saving, since you will need to save both maps' connections. - -Follow Connections ------------------- - -Double-clicking on a connection will open the destination map. This is very useful for navigating through your maps, similar to double-clicking on :ref:`Warp Events `. diff --git a/docsrc/manual/images/editing-map-connections/add-connection-button.png b/docsrc/manual/images/editing-map-connections/add-connection-button.png index a0419ed3e..fe897bc6c 100644 Binary files a/docsrc/manual/images/editing-map-connections/add-connection-button.png and b/docsrc/manual/images/editing-map-connections/add-connection-button.png differ diff --git a/docsrc/manual/images/editing-map-connections/map-connections.png b/docsrc/manual/images/editing-map-connections/map-connections.png index 8d8516c56..1cf7b660e 100644 Binary files a/docsrc/manual/images/editing-map-connections/map-connections.png and b/docsrc/manual/images/editing-map-connections/map-connections.png differ diff --git a/docsrc/manual/images/editing-map-connections/open-connection-button.png b/docsrc/manual/images/editing-map-connections/open-connection-button.png new file mode 100644 index 000000000..e16007030 Binary files /dev/null and b/docsrc/manual/images/editing-map-connections/open-connection-button.png differ diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui new file mode 100644 index 000000000..bf04e8be1 --- /dev/null +++ b/forms/connectionslistitem.ui @@ -0,0 +1,132 @@ + + + ConnectionsListItem + + + + 0 + 0 + 178 + 157 + + + + + 0 + 0 + + + + .ConnectionsListItem { border-width: 1px; } + + + QFrame::StyledPanel + + + + + + + 0 + 0 + + + + Map + + + + + + + + 0 + 0 + + + + Offset + + + + + + + + 0 + 0 + + + + Direction + + + + + + + Remove this connection. + + + ... + + + + :/icons/delete.ico:/icons/delete.ico + + + + + + + Where the connected map should be positioned relative to the current map. + + + + + + + The name of the map to connect to the current map. + + + + + + + The number of spaces to move the connected map perpendicular to its connected direction. + + + + + + + Open the connected map. + + + ... + + + + :/icons/map_go.ico:/icons/map_go.ico + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + + + +
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 8e586198c..8c9059bb4 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1715,7 +1715,7 @@ 0 0 100 - 16 + 30 @@ -1809,7 +1809,7 @@ 0 0 100 - 16 + 30 @@ -1903,7 +1903,7 @@ 0 0 100 - 16 + 30 @@ -2003,7 +2003,7 @@ 0 0 100 - 16 + 30 @@ -2097,7 +2097,7 @@ 0 0 100 - 16 + 30 @@ -2541,7 +2541,7 @@ 0 - + 0 @@ -2554,24 +2554,9 @@ QFrame::Raised - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - + + + 0 @@ -2590,10 +2575,7 @@ QFrame::Raised - - - 4 - + 4 @@ -2606,64 +2588,58 @@ 4 - - - - - 0 - 0 - - + + - <html><head/><body><p>Add a new connection.</p></body></html> - - - + <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - :/icons/add.ico - + + true - - + + - <html><head/><body><p>Remove the currently-selected connection.</p></body></html> + If enabled, connections will automatically be updated on the connected map. - + Mirror to Connecting Maps - - - :/icons/delete.ico - + + true - - + + - Number of Connections: + Dive Map - - + + - + Emerge Map - - + + + + <html><head/><body><p>Destination map name when emerging using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> + + + true + + + + + Qt::Horizontal - - QSizePolicy::Expanding - 40 @@ -2672,256 +2648,274 @@ - - - - - 0 - 0 - - + + - <html><head/><body><p>If enabled, connections will automatically be updated on the connected map.</p></body></html> + Open the selected Dive Map - Mirror to Connecting Maps - - - true + ... - - - - - - - - - - 0 - 0 - - - - false - - - false - - - Qt::ScrollBarAsNeeded - - - Qt::ScrollBarAsNeeded - - - QAbstractScrollArea::AdjustIgnored - - - QGraphicsView::NoDrag - - - QGraphicsView::AnchorUnderMouse - - - QGraphicsView::AnchorUnderMouse - - - - - - - - 0 - 0 - - - - - 0 - 32 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 6 - - - 4 - - - 4 - - - 4 - - - 4 - - - - - Map + + + :/icons/map_go.ico:/icons/map_go.ico - - + + - <html><head/><body><p>The destination map name of the connection.</p></body></html> + If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider. - - true + + Show Emerge/Dive Maps - - - - - - Offset - - - - - - - <html><head/><body><p>The number of metatiles to offset the connection.</p></body></html> - - - -999 + + true - - 999 + + true - - - - - - <html><head/><body><p>The direction of the connection.</p></body></html> + + true - - - up + + + 0 - - - - right + + 0 - - - - down + + 0 - - - - left + + 0 - + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + 90 + + + 30 + + + Qt::Horizontal + + + + + + + + - - - - Qt::Horizontal + + + + Open the selected Emerge Map - - - 40 - 20 - + + ... - + + + :/icons/map_go.ico:/icons/map_go.ico + + - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised + + + + Qt::Horizontal - - - 4 + + + + 0 + 0 + - - 4 + + false - - 4 + + false - - 4 + + Qt::ScrollBarAsNeeded - - 4 + + Qt::ScrollBarAsNeeded - - - - Dive Map - - - - - - - <html><head/><body><p>Destination map name when using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - true - - - - - - - Emerge Map - - - - - - - <html><head/><body><p>Destination map name when emerging using <span style=" font-weight:600;">Dive</span>. If empty, no such connection will exist.</p></body></html> - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + QAbstractScrollArea::AdjustIgnored + + + QGraphicsView::NoDrag + + + QGraphicsView::AnchorUnderMouse + + + QGraphicsView::AnchorUnderMouse + + + + + + 230 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Add Connection + + + + :/icons/add.ico:/icons/add.ico + + + + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 0 + 0 + 365 + 651 + + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + @@ -3085,6 +3079,7 @@ + @@ -3424,6 +3419,17 @@ Ctrl+W + + + true + + + true + + + Dive/Emerge Map + + diff --git a/forms/newmapconnectiondialog.ui b/forms/newmapconnectiondialog.ui new file mode 100644 index 000000000..9b3a3b6e9 --- /dev/null +++ b/forms/newmapconnectiondialog.ui @@ -0,0 +1,136 @@ + + + NewMapConnectionDialog + + + + 0 + 0 + 234 + 162 + + + + Add New Map Connection + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map + + + + + + + The name of the map to connect to the current map. + + + + + + + Direction + + + + + + + Where the connected map should be positioned relative to the current map. + + + + + + + color: rgb(255, 0, 0) + + + 'Map' must be the name of an existing map. + + + true + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + + + buttonBox + accepted() + NewMapConnectionDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + NewMapConnectionDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/include/config.h b/include/config.h index 3f77aa906..7486919fc 100644 --- a/include/config.h +++ b/include/config.h @@ -58,6 +58,11 @@ class PorymapConfig: public KeyValueConfigBase this->reopenOnLaunch = true; this->mapSortOrder = MapSortOrder::Group; this->prettyCursors = true; + this->mirrorConnectingMaps = true; + this->showDiveEmergeMaps = false; + this->diveEmergeMapOpacity = 30; + this->diveMapOpacity = 15; + this->emergeMapOpacity = 15; this->collisionOpacity = 50; this->collisionZoom = 30; this->metatilesZoom = 30; @@ -103,6 +108,11 @@ class PorymapConfig: public KeyValueConfigBase bool projectManuallyClosed; MapSortOrder mapSortOrder; bool prettyCursors; + bool mirrorConnectingMaps; + bool showDiveEmergeMaps; + int diveEmergeMapOpacity; + int diveMapOpacity; + int emergeMapOpacity; int collisionOpacity; int collisionZoom; int metatilesZoom; diff --git a/include/core/editcommands.h b/include/core/editcommands.h index ea66b7229..40f0cd62e 100644 --- a/include/core/editcommands.h +++ b/include/core/editcommands.h @@ -3,9 +3,11 @@ #define EDITCOMMANDS_H #include "blockdata.h" +#include "mapconnection.h" #include #include +#include class MapPixmapItem; class Map; @@ -31,6 +33,11 @@ enum CommandId { ID_EventDelete, ID_EventDuplicate, ID_EventPaste, + ID_MapConnectionMove, + ID_MapConnectionChangeDirection, + ID_MapConnectionChangeMap, + ID_MapConnectionAdd, + ID_MapConnectionRemove, }; #define IDMask_EventType_Object (1 << 8) @@ -379,4 +386,113 @@ class ScriptEditMap : public QUndoCommand { int newBorderHeight; }; + + +/// Implements a command to commit Map Connectien move actions. +/// Actions are merged into one until the mouse is released when editing by click-and-drag, +/// or when the offset spin box loses focus when editing with the list UI. +class MapConnectionMove : public QUndoCommand { +public: + MapConnectionMove(MapConnection *connection, int newOffset, unsigned actionId, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + bool mergeWith(const QUndoCommand *command) override; + int id() const override { return CommandId::ID_MapConnectionMove; } + +private: + MapConnection *connection; + int newOffset; + int oldOffset; + bool mirrored; + unsigned actionId; +}; + + + +/// Implements a command to commit changes to a Map Connectien's 'direction' field. +class MapConnectionChangeDirection : public QUndoCommand { +public: + MapConnectionChangeDirection(MapConnection *connection, QString newDirection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionChangeDirection; } + +private: + QPointer connection; + QString newDirection; + QString oldDirection; + int oldOffset; + int newOffset; + bool mirrored; +}; + + + +/// Implements a command to commit changes to a Map Connectien's 'map' field. +class MapConnectionChangeMap : public QUndoCommand { +public: + MapConnectionChangeMap(MapConnection *connection, QString newMapName, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionChangeMap; } + +private: + QPointer connection; + QString newMapName; + QString oldMapName; + int oldOffset; + int newOffset; + bool mirrored; +}; + + + +/// Implements a command to commit adding a Map Connection to a map. +class MapConnectionAdd : public QUndoCommand { +public: + MapConnectionAdd(Map *map, MapConnection *connection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionAdd; } + +private: + Map *map = nullptr; + Map *mirrorMap = nullptr; + QPointer connection = nullptr; + QPointer mirror = nullptr; +}; + + + +/// Implements a command to commit removing a Map Connection from a map. +class MapConnectionRemove : public QUndoCommand { +public: + MapConnectionRemove(Map *map, MapConnection *connection, + QUndoCommand *parent = nullptr); + + void undo() override; + void redo() override; + + int id() const override { return CommandId::ID_MapConnectionRemove; } + +private: + Map *map = nullptr; + Map *mirrorMap = nullptr; + QPointer connection = nullptr; + QPointer mirror = nullptr; +}; + + #endif // EDITCOMMANDS_H diff --git a/include/core/map.h b/include/core/map.h index c214fbb76..2333c862a 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -69,7 +69,6 @@ class Map : public QObject QMap> events; QList ownedEvents; // for memory management - QList connections; QList metatileLayerOrder; QList metatileLayerOpacity; @@ -98,7 +97,13 @@ class Map : public QObject QStringList getScriptLabels(Event::Group group = Event::Group::None); void removeEvent(Event *); void addEvent(Event *); - QPixmap renderConnection(MapConnection, MapLayout *); + void deleteConnections(); + QList getConnections() const; + void removeConnection(MapConnection *); + void addConnection(MapConnection *); + void loadConnection(MapConnection *); + QRect getConnectionRect(const QString &direction, MapLayout *fromLayout = nullptr); + QPixmap renderConnection(const QString &direction, MapLayout *fromLayout = nullptr); QPixmap renderBorder(bool ignoreCache = false); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); @@ -122,17 +127,24 @@ class Map : public QObject QUndoStack editHistory; void modify(); void clean(); + void pruneEditHistory(); private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + void trackConnection(MapConnection*); + + // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. + QList connections; + QSet ownedConnections; signals: - void mapChanged(Map *map); void modified(); void mapDimensionsChanged(const QSize &size); void mapNeedsRedrawing(); void openScriptRequested(QString label); + void connectionAdded(MapConnection*); + void connectionRemoved(MapConnection*); }; #endif // MAP_H diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 11aa0e595..21c00ac6e 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -3,21 +3,61 @@ #define MAPCONNECTION_H #include -#include +#include +#include -class MapConnection { +class Project; +class Map; + +class MapConnection : public QObject +{ + Q_OBJECT public: - QString direction; - int offset; - QString map_name; -}; + MapConnection(const QString &targetMapName, const QString &direction, int offset = 0); + + Map* parentMap() const { return m_parentMap; } + QString parentMapName() const; + void setParentMap(Map* map, bool mirror = true); + + Map* targetMap() const; + QString targetMapName() const { return m_targetMapName; } + void setTargetMapName(const QString &targetMapName, bool mirror = true); + + QString direction() const { return m_direction; } + void setDirection(const QString &direction, bool mirror = true); + + int offset() const { return m_offset; } + void setOffset(int offset, bool mirror = true); -inline bool operator==(const MapConnection &c1, const MapConnection &c2) { - return c1.map_name == c2.map_name; -} + MapConnection* findMirror(); + MapConnection* createMirror(); -inline uint qHash(const MapConnection &key) { - return qHash(key.map_name); -} + QPixmap getPixmap(); + + static QPointer project; + static const QMap oppositeDirections; + static const QStringList cardinalDirections; + static bool isCardinal(const QString &direction); + static bool isHorizontal(const QString &direction); + static bool isVertical(const QString &direction); + static bool isDiving(const QString &direction); + static QString oppositeDirection(const QString &direction) { return oppositeDirections.value(direction, direction); } + static bool areMirrored(const MapConnection*, const MapConnection*); + +private: + Map* m_parentMap; + QString m_targetMapName; + QString m_direction; + int m_offset; + + void markMapEdited(); + Map* getMap(const QString& mapName) const; + +signals: + void parentMapChanged(Map* before, Map* after); + void targetMapNameChanged(QString before, QString after); + void directionChanged(QString before, QString after); + void offsetChanged(int before, int after); +}; #endif // MAPCONNECTION_H diff --git a/include/editor.h b/include/editor.h index 8e30ae70c..89b6ccb59 100644 --- a/include/editor.h +++ b/include/editor.h @@ -18,6 +18,7 @@ #include "ui_mainwindow.h" #include "bordermetatilespixmapitem.h" #include "connectionpixmapitem.h" +#include "divingmappixmapitem.h" #include "currentselectedmetatilespixmapitem.h" #include "collisionpixmapitem.h" #include "mappixmapitem.h" @@ -46,6 +47,7 @@ class Editor : public QObject QPointer project = nullptr; Map *map = nullptr; Settings *settings; + void setProject(Project * project); void saveProject(); void save(); void closeProject(); @@ -74,18 +76,17 @@ class Editor : public QObject void setEditingObjects(); void setEditingConnections(); void setMapEditingButtonsEnabled(bool enabled); - void setCurrentConnectionDirection(QString curDirection); - void updateCurrentConnectionDirection(QString curDirection); void setConnectionsVisibility(bool visible); - void updateConnectionOffset(int offset); - void setConnectionMap(QString mapName); - void addNewConnection(); - void removeCurrentConnection(); + void updateDivingMapsVisibility(); + void renderDivingConnections(); + void addConnection(MapConnection* connection); + void removeConnection(MapConnection* connection); + void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void updateDiveMap(QString mapName); void updateEmergeMap(QString mapName); - void setSelectedConnectionFromMap(QString mapName); + void setSelectedConnection(MapConnection *connection); void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); void updateSecondaryTileset(QString tilesetLabel, bool forceLoad = false); void toggleBorderVisibility(bool visible, bool enableScriptCallback = true); @@ -110,8 +111,8 @@ class Editor : public QObject QPointer scene = nullptr; QGraphicsPixmapItem *current_view = nullptr; QPointer map_item = nullptr; - ConnectionPixmapItem* selected_connection_item = nullptr; - QList connection_items; + QList> connection_items; + QMap> diving_map_items; QGraphicsPathItem *connection_mask = nullptr; QPointer collision_item = nullptr; QGraphicsItemGroup *events_group = nullptr; @@ -132,6 +133,8 @@ class Editor : public QObject QPointer movement_permissions_selector_item = nullptr; QList *selected_events = nullptr; + QPointer selected_connection_item = nullptr; + QPointer connection_to_select = nullptr; QString map_edit_mode = "paint"; QString obj_edit_mode = "select"; @@ -175,25 +178,20 @@ public slots: void clearBorderMetatiles(); void clearCurrentMetatilesSelection(); void clearMapEvents(); - //void clearMapConnections(); + void clearMapConnections(); + void clearConnectionMask(); void clearMapBorder(); void clearMapGrid(); void clearWildMonTables(); - void setConnectionItemsVisible(bool); - void setBorderItemsVisible(bool, qreal = 1); - void setConnectionEditControlValues(MapConnection*); - void setConnectionEditControlsEnabled(bool); - void setConnectionsEditable(bool); - void createConnectionItem(MapConnection* connection); - void populateConnectionMapPickers(); - void setDiveEmergeControls(); - void updateDiveEmergeMap(QString mapName, QString direction); - void onConnectionOffsetChanged(int newOffset); - void removeMirroredConnection(MapConnection*); - void updateMirroredConnectionOffset(MapConnection*); - void updateMirroredConnectionDirection(MapConnection*, QString); - void updateMirroredConnectionMap(MapConnection*, QString); - void updateMirroredConnection(MapConnection*, QString, QString, bool isDelete = false); + void updateBorderVisibility(); + void disconnectMapConnection(MapConnection *connection); + QPoint getConnectionOrigin(MapConnection *connection); + void removeConnectionPixmap(MapConnection *connection); + void updateConnectionPixmap(ConnectionPixmapItem *connectionItem); + void displayConnection(MapConnection *connection); + void displayDivingConnection(MapConnection *connection); + void setDivingMapName(QString mapName, QString direction); + void removeDivingMapPixmap(MapConnection *connection); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); @@ -209,10 +207,7 @@ private slots: void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item); void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); - void onConnectionMoved(MapConnection*); - void onConnectionItemSelected(ConnectionPixmapItem* connectionItem); - void onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void onConnectionDirectionChanged(QString newDirection); + void setSelectedConnectionItem(ConnectionPixmapItem *connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); @@ -227,12 +222,11 @@ private slots: signals: void objectsChanged(); - void loadMapRequested(QString, QString); + void openConnectedMap(MapConnection*); void wildMonDataChanged(); void warpEventDoubleClicked(QString, int, Event::Group); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void editedMapData(); void tilesetUpdated(QString); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index d58d11c01..018239459 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -179,14 +179,14 @@ private slots: void copy(); void paste(); - void onLoadMapRequested(QString, QString); - void onMapChanged(Map *map); + void onOpenConnectedMap(MapConnection*); void onMapNeedsRedrawing(); void onTilesetsSaved(QString, QString); void onWildMonDataChanged(); void openNewMapPopupWindow(); void onNewMapCreated(); void onMapCacheCleared(); + void onMapLoaded(Map *map); void importMapFromAdvanceMap1_92(); void onMapRulerStatusChanged(const QString &); void applyUserShortcuts(); @@ -219,6 +219,7 @@ private slots: void on_actionMove_triggered(); void on_actionMap_Shift_triggered(); + void onDeleteKeyPressed(); void on_toolButton_deleteObject_clicked(); void addNewEvent(Event::Type type); @@ -245,11 +246,9 @@ private slots: void on_actionExport_Map_Timelapse_Image_triggered(); void on_actionImport_Map_from_Advance_Map_1_92_triggered(); - void on_comboBox_ConnectionDirection_currentTextChanged(const QString &arg1); - void on_spinBox_ConnectionOffset_valueChanged(int offset); - void on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName); void on_pushButton_AddConnection_clicked(); - void on_pushButton_RemoveConnection_clicked(); + void on_button_OpenDiveMap_clicked(); + void on_button_OpenEmergeMap_clicked(); void on_comboBox_DiveMap_currentTextChanged(const QString &mapName); void on_comboBox_EmergeMap_currentTextChanged(const QString &mapName); void on_comboBox_PrimaryTileset_currentTextChanged(const QString &arg1); @@ -272,6 +271,12 @@ private slots: void eventTabChanged(int index); + void on_checkBox_MirrorConnections_stateChanged(int selected); + void on_actionDive_Emerge_Map_triggered(); + void on_groupBox_DiveMapOpacity_toggled(bool on); + void on_slider_DiveEmergeMapOpacity_valueChanged(int value); + void on_slider_DiveMapOpacity_valueChanged(int value); + void on_slider_EmergeMapOpacity_valueChanged(int value); void on_horizontalSlider_CollisionTransparency_valueChanged(int value); void on_toolButton_ExpandAll_clicked(); void on_toolButton_CollapseAll_clicked(); @@ -318,9 +323,7 @@ private slots: QStandardItemModel *mapListModel; QList *mapGroupItemsList; QMap mapListIndexes; - QIcon* mapIcon; - QIcon* mapEditedIcon; - QIcon* mapOpenedIcon; + QIcon mapIcon; QAction *undoAction = nullptr; QAction *redoAction = nullptr; @@ -331,10 +334,10 @@ private slots: QMap lastSelectedEvent; bool isProgrammaticEventTabChange; - bool projectHasUnsavedChanges; bool newMapDefaultsSet = false; bool tilesetNeedsRedraw = false; + bool userSetMap(QString, bool scrollTreeView = false); bool setMap(QString, bool scrollTreeView = false); void redrawMapScene(); void refreshMapScene(); @@ -353,7 +356,7 @@ private slots: QStandardItem* createMapItem(QString mapName, int groupNum, int inGroupNum); void refreshRecentProjectsMenu(); - void drawMapListIcons(QAbstractItemModel *model); + void updateMapListIcon(const QString &mapName); void updateMapList(); void displayMapProperties(); @@ -361,6 +364,7 @@ private slots: void clickToolButtonFromEditMode(QString editMode); void markMapEdited(); + void markMapEdited(Map*); void showWindowTitle(); void initWindow(); @@ -398,6 +402,7 @@ private slots: int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); + void setDivingMapsVisible(bool visible); }; enum MapListUserRoles { @@ -406,4 +411,23 @@ enum MapListUserRoles { TypeRole2, // Used for various extra data needed. }; +// These are namespaced in a struct to avoid colliding with e.g. class Map. +struct MainTab { + enum { + Map, + Events, + Header, + Connections, + WildPokemon, + }; +}; + +struct MapViewTab { + enum { + Metatiles, + Collision, + Prefabs, + }; +}; + #endif // MAINWINDOW_H diff --git a/include/project.h b/include/project.h index c9a1a0c08..8c08e5085 100644 --- a/include/project.h +++ b/include/project.h @@ -257,6 +257,7 @@ class Project : public QObject void reloadProject(); void uncheckMonitorFilesAction(); void mapCacheCleared(); + void mapLoaded(Map *map); }; #endif // PROJECT_H diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index c3d98cff4..62eda6fe4 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -4,44 +4,46 @@ #include "mapconnection.h" #include #include +#include class ConnectionPixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - ConnectionPixmapItem(QPixmap pixmap, MapConnection* connection, int x, int y, int baseMapWidth, int baseMapHeight): QGraphicsPixmapItem(pixmap) { - this->basePixmap = pixmap; - this->connection = connection; - setFlag(ItemIsMovable); - setFlag(ItemSendsGeometryChanges); - this->initialX = x; - this->initialY = y; - this->initialOffset = connection->offset; - this->baseMapWidth = baseMapWidth; - this->baseMapHeight = baseMapHeight; - } - QPixmap basePixmap; - MapConnection* connection; - int initialX; - int initialY; - int initialOffset; - int baseMapWidth; - int baseMapHeight; - void render(qreal opacity = 1); - int getMinOffset(); - int getMaxOffset(); + ConnectionPixmapItem(MapConnection* connection, int originX, int originY); + ConnectionPixmapItem(MapConnection* connection, QPoint origin); + + const QPointer connection; + + void setOrigin(int x, int y); + void setOrigin(QPoint pos); + void setEditable(bool editable); bool getEditable(); - void updateHighlight(bool selected); + + void setSelected(bool selected); + + void updatePos(); + void render(bool ignoreCache = false); + +private: + QPixmap basePixmap; + qreal originX; + qreal originY; + bool selected = false; + unsigned actionId = 0; + + static const int mWidth = 16; + static const int mHeight = 16; protected: - QVariant itemChange(GraphicsItemChange change, const QVariant &value); - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*); + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override; signals: - void connectionItemSelected(ConnectionPixmapItem* connectionItem); - void connectionItemDoubleClicked(ConnectionPixmapItem* connectionItem); - void connectionMoved(MapConnection*); + void connectionItemDoubleClicked(MapConnection*); + void selectionChanged(bool selected); }; #endif // CONNECTIONPIXMAPITEM_H diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h new file mode 100644 index 000000000..bbe0f2d31 --- /dev/null +++ b/include/ui/connectionslistitem.h @@ -0,0 +1,52 @@ +#ifndef CONNECTIONSLISTITEM_H +#define CONNECTIONSLISTITEM_H + +#include "mapconnection.h" +#include "map.h" + +#include +#include +#include + +namespace Ui { +class ConnectionsListItem; +} + +// We show the data for each map connection in the panel on the right side of the Connections tab. +// An instance of this class is used for each item in that list. +// It communicates with the ConnectionPixmapItem on the map through a shared MapConnection pointer. +class ConnectionsListItem : public QFrame +{ + Q_OBJECT + +public: + explicit ConnectionsListItem(QWidget *parent, MapConnection *connection, const QStringList &mapNames); + ~ConnectionsListItem(); + + void updateUI(); + void setSelected(bool selected); + +private: + Ui::ConnectionsListItem *ui; + QPointer connection; + Map *map; + bool isSelected = false; + unsigned actionId = 0; + +protected: + void mousePressEvent(QMouseEvent*) override; + +signals: + void selected(); + void removed(MapConnection*); + void openMapClicked(MapConnection*); + +private slots: + void on_comboBox_Direction_currentTextChanged(QString direction); + void on_comboBox_Map_currentTextChanged(QString mapName); + void on_spinBox_Offset_valueChanged(int offset); + void on_button_Delete_clicked(); + void on_button_OpenMap_clicked(); +}; + +#endif // CONNECTIONSLISTITEM_H diff --git a/include/ui/divingmappixmapitem.h b/include/ui/divingmappixmapitem.h new file mode 100644 index 000000000..7eceba070 --- /dev/null +++ b/include/ui/divingmappixmapitem.h @@ -0,0 +1,30 @@ +#ifndef DIVINGMAPPIXMAPITEM_H +#define DIVINGMAPPIXMAPITEM_H + +#include "mapconnection.h" + +#include +#include +#include + +class DivingMapPixmapItem : public QObject, public QGraphicsPixmapItem { + Q_OBJECT +public: + DivingMapPixmapItem(MapConnection *connection, QComboBox *combo); + ~DivingMapPixmapItem(); + + MapConnection* connection() const { return m_connection; } + void updatePixmap(); + +private: + QPointer m_connection; + QPointer m_combo; + + void setComboText(const QString &text); + static QPixmap getBasePixmap(MapConnection* connection); + +private slots: + void onTargetMapChanged(); +}; + +#endif // DIVINGMAPPIXMAPITEM_H diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 6d8ae643d..b9cc20bda 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -50,9 +50,10 @@ class MapImageExporter : public QDialog ImageExporterMode mode = ImageExporterMode::Normal; void updatePreview(); + void updateShowBorderState(); void saveImage(); QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); - QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder); + QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder = false); bool historyItemAppliesToFrame(const QUndoCommand *command); private slots: diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h new file mode 100644 index 000000000..4781c9719 --- /dev/null +++ b/include/ui/newmapconnectiondialog.h @@ -0,0 +1,32 @@ +#ifndef NEWMAPCONNECTIONDIALOG_H +#define NEWMAPCONNECTIONDIALOG_H + +#include +#include "map.h" +#include "mapconnection.h" + +namespace Ui { +class NewMapConnectionDialog; +} + +class NewMapConnectionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames); + ~NewMapConnectionDialog(); + + virtual void accept() override; + +signals: + void accepted(MapConnection *result); + +private: + Ui::NewMapConnectionDialog *ui; + + bool mapNameIsValid(); + void setWarningVisible(bool visible); +}; + +#endif // NEWMAPCONNECTIONDIALOG_H diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 65b6ab5bc..32966b3a5 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -13,11 +13,15 @@ class NoScrollComboBox : public QComboBox void setTextItem(const QString &text); void setNumberItem(int value); void setHexItem(uint32_t value); + void setClearButtonEnabled(bool enabled); void setEditable(bool editable); void setLineEdit(QLineEdit *edit); + void setFocusedScrollingEnabled(bool enabled); private: void setItem(int index, const QString &text); + + bool focusedScrollingEnabled = true; }; #endif // NOSCROLLCOMBOBOX_H diff --git a/porymap.pro b/porymap.pro index dfc3306ee..744edaef1 100644 --- a/porymap.pro +++ b/porymap.pro @@ -24,6 +24,7 @@ SOURCES += src/core/block.cpp \ src/core/heallocation.cpp \ src/core/imageexport.cpp \ src/core/map.cpp \ + src/core/mapconnection.cpp \ src/core/maplayout.cpp \ src/core/mapparser.cpp \ src/core/metatile.cpp \ @@ -46,13 +47,16 @@ SOURCES += src/core/block.cpp \ src/scriptapi/apiutility.cpp \ src/scriptapi/scripting.cpp \ src/ui/aboutporymap.cpp \ + src/ui/connectionslistitem.cpp \ src/ui/customscriptseditor.cpp \ src/ui/customscriptslistitem.cpp \ + src/ui/divingmappixmapitem.cpp \ src/ui/draggablepixmapitem.cpp \ src/ui/bordermetatilespixmapitem.cpp \ src/ui/collisionpixmapitem.cpp \ src/ui/connectionpixmapitem.cpp \ src/ui/currentselectedmetatilespixmapitem.cpp \ + src/ui/newmapconnectiondialog.cpp \ src/ui/overlay.cpp \ src/ui/prefab.cpp \ src/ui/projectsettingseditor.cpp \ @@ -139,13 +143,16 @@ HEADERS += include/core/block.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ + include/ui/connectionslistitem.h \ include/ui/customscriptseditor.h \ include/ui/customscriptslistitem.h \ + include/ui/divingmappixmapitem.h \ include/ui/draggablepixmapitem.h \ include/ui/bordermetatilespixmapitem.h \ include/ui/collisionpixmapitem.h \ include/ui/connectionpixmapitem.h \ include/ui/currentselectedmetatilespixmapitem.h \ + include/ui/newmapconnectiondialog.h \ include/ui/prefabframe.h \ include/ui/projectsettingseditor.h \ include/ui/regionmaplayoutpixmapitem.h \ @@ -205,6 +212,8 @@ HEADERS += include/core/block.h \ include/ui/updatepromoter.h FORMS += forms/mainwindow.ui \ + forms/connectionslistitem.ui \ + forms/newmapconnectiondialog.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ diff --git a/resources/icons/map_go.ico b/resources/icons/map_go.ico new file mode 100755 index 000000000..8ca93206b Binary files /dev/null and b/resources/icons/map_go.ico differ diff --git a/resources/images.qrc b/resources/images.qrc index 86d56cb20..bdac1a474 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -65,5 +65,6 @@ images/Entities_16x16.png images/pokemon_icon_placeholder.png icons/clipboard.ico + icons/map_go.ico diff --git a/src/config.cpp b/src/config.cpp index 16853e8f9..818dd714a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -330,6 +330,16 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->mainSplitterState = bytesFromString(value); } else if (key == "metatiles_splitter_state") { this->metatilesSplitterState = bytesFromString(value); + } else if (key == "mirror_connecting_maps") { + this->mirrorConnectingMaps = getConfigBool(key, value); + } else if (key == "show_dive_emerge_maps") { + this->showDiveEmergeMaps = getConfigBool(key, value); + } else if (key == "dive_emerge_map_opacity") { + this->diveEmergeMapOpacity = getConfigInteger(key, value, 10, 90, 30); + } else if (key == "dive_map_opacity") { + this->diveMapOpacity = getConfigInteger(key, value, 10, 90, 15); + } else if (key == "emerge_map_opacity") { + this->emergeMapOpacity = getConfigInteger(key, value, 10, 90, 15); } else if (key == "collision_opacity") { this->collisionOpacity = getConfigInteger(key, value, 0, 100, 50); } else if (key == "tileset_editor_geometry") { @@ -439,6 +449,11 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState)); map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); + map.insert("mirror_connecting_maps", this->mirrorConnectingMaps ? "1" : "0"); + map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0"); + map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity)); + map.insert("dive_map_opacity", QString::number(this->diveMapOpacity)); + map.insert("emerge_map_opacity", QString::number(this->emergeMapOpacity)); map.insert("collision_opacity", QString::number(this->collisionOpacity)); map.insert("collision_zoom", QString::number(this->collisionZoom)); map.insert("metatiles_zoom", QString::number(this->metatilesZoom)); diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp index 394c51cbc..640cb43fc 100644 --- a/src/core/editcommands.cpp +++ b/src/core/editcommands.cpp @@ -569,3 +569,205 @@ void ScriptEditMap::undo() { QUndoCommand::undo(); } + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionMove::MapConnectionMove(MapConnection *connection, int newOffset, unsigned actionId, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Move Map Connection"); + + this->connection = connection; + this->oldOffset = connection->offset(); + this->newOffset = newOffset; + + this->mirrored = porymapConfig.mirrorConnectingMaps; + + this->actionId = actionId; +} + +void MapConnectionMove::redo() { + QUndoCommand::redo(); + if (this->connection) + this->connection->setOffset(this->newOffset, this->mirrored); +} + +void MapConnectionMove::undo() { + if (this->connection) + this->connection->setOffset(this->oldOffset, this->mirrored); + QUndoCommand::undo(); +} + +bool MapConnectionMove::mergeWith(const QUndoCommand *command) { + if (this->id() != command->id()) + return false; + + const MapConnectionMove *other = static_cast(command); + if (this->connection != other->connection) + return false; + if (this->actionId != other->actionId) + return false; + + this->newOffset = other->newOffset; + return true; +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionChangeDirection::MapConnectionChangeDirection(MapConnection *connection, QString newDirection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Change Map Connection Direction"); + + this->connection = connection; + + this->oldDirection = connection->direction(); + this->newDirection = newDirection; + + this->oldOffset = connection->offset(); + + // If the direction changes between vertical/horizontal then the old offset may not make sense, so we reset it. + if (MapConnection::isHorizontal(this->oldDirection) != MapConnection::isHorizontal(this->newDirection) + || MapConnection::isVertical(this->oldDirection) != MapConnection::isVertical(this->newDirection)) { + this->newOffset = 0; + } else { + this->newOffset = oldOffset; + } + + this->mirrored = porymapConfig.mirrorConnectingMaps; +} + +void MapConnectionChangeDirection::redo() { + QUndoCommand::redo(); + if (this->connection) { + this->connection->setDirection(this->newDirection, this->mirrored); + this->connection->setOffset(this->newOffset, this->mirrored); + } +} + +void MapConnectionChangeDirection::undo() { + if (this->connection) { + this->connection->setDirection(this->oldDirection, this->mirrored); + this->connection->setOffset(this->oldOffset, this->mirrored); + } + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionChangeMap::MapConnectionChangeMap(MapConnection *connection, QString newMapName, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Change Map Connection Map"); + + this->connection = connection; + + this->oldMapName = connection->targetMapName(); + this->newMapName = newMapName; + + this->oldOffset = connection->offset(); + this->newOffset = 0; // The old offset may not make sense, so we reset it + + this->mirrored = porymapConfig.mirrorConnectingMaps; +} + +void MapConnectionChangeMap::redo() { + QUndoCommand::redo(); + if (this->connection) { + this->connection->setTargetMapName(this->newMapName, this->mirrored); + this->connection->setOffset(this->newOffset, this->mirrored); + } +} + +void MapConnectionChangeMap::undo() { + if (this->connection) { + this->connection->setTargetMapName(this->oldMapName, this->mirrored); + this->connection->setOffset(this->oldOffset, this->mirrored); + } + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionAdd::MapConnectionAdd(Map *map, MapConnection *connection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Add Map Connection"); + + this->map = map; + this->connection = connection; + + // Set this now because it's needed to create a mirror below. + // It would otherwise be set by Map::addConnection. + this->connection->setParentMap(this->map, false); + + if (porymapConfig.mirrorConnectingMaps) { + this->mirror = this->connection->createMirror(); + this->mirrorMap = this->connection->targetMap(); + } +} + +void MapConnectionAdd::redo() { + QUndoCommand::redo(); + + this->map->addConnection(this->connection); + if (this->mirrorMap) + this->mirrorMap->addConnection(this->mirror); +} + +void MapConnectionAdd::undo() { + if (this->mirrorMap) { + // We can't guarantee that the mirror we created earlier is still our connection's + // mirror because there is no strict source->mirror pairing for map connections + // (a different identical map connection can take its place during any mirrored change). + if (!MapConnection::areMirrored(this->connection, this->mirror)) + this->mirror = this->connection->findMirror(); + + this->mirrorMap->removeConnection(this->mirror); + } + this->map->removeConnection(this->connection); + + QUndoCommand::undo(); +} + +/****************************************************************************** + ************************************************************************ + ******************************************************************************/ + +MapConnectionRemove::MapConnectionRemove(Map *map, MapConnection *connection, + QUndoCommand *parent) : QUndoCommand(parent) { + setText("Remove Map Connection"); + + this->map = map; + this->connection = connection; + + if (porymapConfig.mirrorConnectingMaps) { + this->mirror = this->connection->findMirror(); + this->mirrorMap = this->connection->targetMap(); + } +} + +void MapConnectionRemove::redo() { + QUndoCommand::redo(); + + if (this->mirrorMap) { + // See comment in MapConnectionAdd::undo + if (!MapConnection::areMirrored(this->connection, this->mirror)) + this->mirror = this->connection->findMirror(); + + this->mirrorMap->removeConnection(this->mirror); + } + this->map->removeConnection(this->connection); +} + +void MapConnectionRemove::undo() { + this->map->addConnection(this->connection); + if (this->mirrorMap) + this->mirrorMap->addConnection(this->mirror); + + QUndoCommand::undo(); +} diff --git a/src/core/map.cpp b/src/core/map.cpp index 7e584db85..86df7947c 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -17,11 +17,9 @@ Map::Map(QObject *parent) : QObject(parent) } Map::~Map() { - // delete all associated events - while (!ownedEvents.isEmpty()) { - Event *last = ownedEvents.takeLast(); - if (last) delete last; - } + qDeleteAll(ownedEvents); + ownedEvents.clear(); + deleteConnections(); } void Map::setName(QString mapName) { @@ -217,38 +215,47 @@ QPixmap Map::renderBorder(bool ignoreCache) { return layout->border_pixmap; } -QPixmap Map::renderConnection(MapConnection connection, MapLayout * fromLayout) { - int x, y, w, h; - if (connection.direction == "up") { - x = 0; - y = getHeight() - BORDER_DISTANCE; - w = getWidth(); - h = BORDER_DISTANCE; - } else if (connection.direction == "down") { - x = 0; - y = 0; - w = getWidth(); - h = BORDER_DISTANCE; - } else if (connection.direction == "left") { - x = getWidth() - BORDER_DISTANCE; - y = 0; - w = BORDER_DISTANCE; - h = getHeight(); - } else if (connection.direction == "right") { - x = 0; - y = 0; - w = BORDER_DISTANCE; - h = getHeight(); +// Get the portion of the map that can be rendered when rendered as a map connection. +// Cardinal connections render the nearest segment of their map and within the bounds of the border draw distance, +// Dive/Emerge connections are rendered normally within the bounds of their parent map. +QRect Map::getConnectionRect(const QString &direction, MapLayout * fromLayout) { + int x = 0, y = 0; + int w = getWidth(), h = getHeight(); + + if (direction == "up") { + h = qMin(h, BORDER_DISTANCE); + y = getHeight() - h; + } else if (direction == "down") { + h = qMin(h, BORDER_DISTANCE); + } else if (direction == "left") { + w = qMin(w, BORDER_DISTANCE); + x = getWidth() - w; + } else if (direction == "right") { + w = qMin(w, BORDER_DISTANCE); + } else if (MapConnection::isDiving(direction)) { + if (fromLayout) { + w = qMin(w, fromLayout->getWidth()); + h = qMin(h, fromLayout->getHeight()); + } } else { - // this should not happen - x = 0; - y = 0; - w = getWidth(); - h = getHeight(); + // Unknown direction + return QRect(); } + return QRect(x, y, w, h); +} + +QPixmap Map::renderConnection(const QString &direction, MapLayout * fromLayout) { + QRect bounds = getConnectionRect(direction, fromLayout); + if (!bounds.isValid()) + return QPixmap(); - render(true, fromLayout, QRect(x, y, w, h)); - QImage connection_image = image.copy(x * 16, y * 16, w * 16, h * 16); + // 'fromLayout' will be used in 'render' to get the palettes from the parent map. + // Dive/Emerge connections render normally with their own palettes, so we ignore this. + if (MapConnection::isDiving(direction)) + fromLayout = nullptr; + + render(true, fromLayout, bounds); + QImage connection_image = image.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); return QPixmap::fromImage(connection_image); } @@ -304,8 +311,8 @@ void Map::setDimensions(int newWidth, int newHeight, bool setNewBlockdata, bool Scripting::cb_MapResized(oldWidth, oldHeight, newWidth, newHeight); } - emit mapChanged(this); emit mapDimensionsChanged(QSize(getWidth(), getHeight())); + modify(); } void Map::setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata, bool enableScriptCallback) { @@ -322,7 +329,7 @@ void Map::setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata, Scripting::cb_BorderResized(oldWidth, oldHeight, newWidth, newHeight); } - emit mapChanged(this); + modify(); } void Map::openScript(QString label) { @@ -523,6 +530,72 @@ void Map::addEvent(Event *event) { if (!ownedEvents.contains(event)) ownedEvents.append(event); } +void Map::deleteConnections() { + qDeleteAll(this->ownedConnections); + this->ownedConnections.clear(); + this->connections.clear(); +} + +QList Map::getConnections() const { + return this->connections; +} + +void Map::addConnection(MapConnection *connection) { + if (!connection || this->connections.contains(connection)) + return; + + // Maps should only have one Dive/Emerge connection at a time. + // (Users can technically have more by editing their data manually, but we will only display one at a time) + // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. + if (MapConnection::isDiving(connection->direction())) { + for (auto i : this->connections) { + if (i->direction() == connection->direction()) { + trackConnection(connection); + return; + } + } + } + + loadConnection(connection); + modify(); + emit connectionAdded(connection); + return; +} + +void Map::loadConnection(MapConnection *connection) { + if (!connection) + return; + + if (!this->connections.contains(connection)) + this->connections.append(connection); + + trackConnection(connection); +} + +void Map::trackConnection(MapConnection *connection) { + connection->setParentMap(this, false); + + if (!this->ownedConnections.contains(connection)) { + this->ownedConnections.insert(connection); + connect(connection, &MapConnection::parentMapChanged, [=](Map *, Map *after) { + if (after != this && after != nullptr) { + // MapConnection's parent has been reassigned, it's no longer our responsibility + this->ownedConnections.remove(connection); + QObject::disconnect(connection, &MapConnection::parentMapChanged, this, nullptr); + } + }); + } +} + +// We retain ownership of this MapConnection until it's assigned to a new parent map. +void Map::removeConnection(MapConnection *connection) { + if (!this->connections.removeOne(connection)) + return; + connection->setParentMap(nullptr, false); + modify(); + emit connectionRemoved(connection); +} + void Map::modify() { emit modified(); } @@ -535,6 +608,27 @@ bool Map::hasUnsavedChanges() { return !editHistory.isClean() || hasUnsavedDataChanges || !isPersistedToFile; } +void Map::pruneEditHistory() { + // Edit history for map connections gets messy because edits on other maps can affect the current map. + // To avoid complications we clear MapConnection edit history when the user opens a different map. + // No other edits within a single map depend on MapConnections so they can be pruned safely. + static const QSet mapConnectionIds = { + ID_MapConnectionMove, + ID_MapConnectionChangeDirection, + ID_MapConnectionChangeMap, + ID_MapConnectionAdd, + ID_MapConnectionRemove + }; + for (int i = 0; i < this->editHistory.count(); i++) { + // Qt really doesn't expect editing commands in the stack to be valid (fair). + // A better future design might be to have separate edit histories per map tab, + // and dumping the entire Connections tab history with QUndoStack::clear. + auto command = const_cast(this->editHistory.command(i)); + if (mapConnectionIds.contains(command->id())) + command->setObsolete(true); + } +} + bool Map::isWithinBounds(int x, int y) { return (x >= 0 && x < this->getWidth() && y >= 0 && y < this->getHeight()); } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp new file mode 100644 index 000000000..b80b8d097 --- /dev/null +++ b/src/core/mapconnection.cpp @@ -0,0 +1,163 @@ +#include "mapconnection.h" +#include "project.h" + +QPointer MapConnection::project = nullptr; + +const QMap MapConnection::oppositeDirections = { + {"up", "down"}, {"down", "up"}, + {"right", "left"}, {"left", "right"}, + {"dive", "emerge"}, {"emerge", "dive"} +}; + +MapConnection::MapConnection(const QString &targetMapName, const QString &direction, int offset) { + m_parentMap = nullptr; + m_targetMapName = targetMapName; + m_direction = direction; + m_offset = offset; +} + +bool MapConnection::areMirrored(const MapConnection* a, const MapConnection* b) { + if (!a || !b || !a->m_parentMap || !b->m_parentMap) + return false; + + return a->parentMapName() == b->m_targetMapName + && a->m_targetMapName == b->parentMapName() + && a->m_offset == -b->m_offset + && a->m_direction == oppositeDirection(b->m_direction); +} + +MapConnection* MapConnection::findMirror() { + auto map = targetMap(); + if (!map) + return nullptr; + + // Find the matching connection in the connected map. + // Note: There is no strict source -> mirror pairing, i.e. we are not guaranteed + // to always get the same MapConnection if there are multiple identical copies. + for (auto connection : map->getConnections()) { + if (this != connection && areMirrored(this, connection)) + return connection; + } + return nullptr; +} + +MapConnection * MapConnection::createMirror() { + auto mirror = new MapConnection(parentMapName(), oppositeDirection(m_direction), -m_offset); + mirror->setParentMap(targetMap(), false); + return mirror; +} + +void MapConnection::markMapEdited() { + if (m_parentMap) + m_parentMap->modify(); +} + +Map* MapConnection::getMap(const QString& mapName) const { + return project ? project->getMap(mapName) : nullptr; +} + +Map* MapConnection::targetMap() const { + return getMap(m_targetMapName); +} + +QPixmap MapConnection::getPixmap() { + auto map = targetMap(); + if (!map) + return QPixmap(); + + return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout : nullptr); +} + +void MapConnection::setParentMap(Map* map, bool mirror) { + if (map == m_parentMap) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setTargetMapName(map ? map->name : QString(), false); + } + + if (m_parentMap) + m_parentMap->removeConnection(this); + + auto before = m_parentMap; + m_parentMap = map; + + if (m_parentMap) + m_parentMap->addConnection(this); + + emit parentMapChanged(before, m_parentMap); +} + +QString MapConnection::parentMapName() const { + return m_parentMap ? m_parentMap->name : QString(); +} + +void MapConnection::setTargetMapName(const QString &targetMapName, bool mirror) { + if (targetMapName == m_targetMapName) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setParentMap(getMap(targetMapName), false); + } + + auto before = m_targetMapName; + m_targetMapName = targetMapName; + emit targetMapNameChanged(before, m_targetMapName); + markMapEdited(); +} + +void MapConnection::setDirection(const QString &direction, bool mirror) { + if (direction == m_direction) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setDirection(oppositeDirection(direction), false); + } + + auto before = m_direction; + m_direction = direction; + emit directionChanged(before, m_direction); + markMapEdited(); +} + +void MapConnection::setOffset(int offset, bool mirror) { + if (offset == m_offset) + return; + + if (mirror) { + auto connection = findMirror(); + if (connection) + connection->setOffset(-offset, false); + } + + auto before = m_offset; + m_offset = offset; + emit offsetChanged(before, m_offset); + markMapEdited(); +} + +const QStringList MapConnection::cardinalDirections = { + "up", "down", "left", "right" +}; + +bool MapConnection::isCardinal(const QString &direction) { + return cardinalDirections.contains(direction); +} + +bool MapConnection::isHorizontal(const QString &direction) { + return direction == "left" || direction == "right"; +} + +bool MapConnection::isVertical(const QString &direction) { + return direction == "up" || direction == "down"; +} + +bool MapConnection::isDiving(const QString &direction) { + return direction == "dive" || direction == "emerge"; +} diff --git a/src/editor.cpp b/src/editor.cpp index 46aa04585..229577907 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2,7 +2,7 @@ #include "draggablepixmapitem.h" #include "imageproviders.h" #include "log.h" -#include "mapconnection.h" +#include "connectionslistitem.h" #include "currentselectedmetatilespixmapitem.h" #include "mapsceneeventfilter.h" #include "metatile.h" @@ -78,6 +78,14 @@ void Editor::saveUiFields() { saveEncounterTabData(); } +void Editor::setProject(Project * project) { + if (this->project) { + closeProject(); + } + this->project = project; + MapConnection::project = project; +} + void Editor::closeProject() { if (!this->project) return; @@ -92,7 +100,6 @@ void Editor::setEditingMap() { current_view = map_item; if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::Metatiles; - displayMapConnections(); map_item->draw(); map_item->setVisible(true); } @@ -102,9 +109,7 @@ void Editor::setEditingMap() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->stopSingleTileMode(); this->cursorMapTileRect->setActive(true); @@ -114,7 +119,6 @@ void Editor::setEditingMap() { void Editor::setEditingCollision() { current_view = collision_item; if (collision_item) { - displayMapConnections(); collision_item->draw(); collision_item->setVisible(true); } @@ -126,9 +130,7 @@ void Editor::setEditingCollision() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(true); @@ -142,16 +144,13 @@ void Editor::setEditingObjects() { } if (map_item) { map_item->paintingMode = MapPixmapItem::PaintMode::EventObjects; - displayMapConnections(); map_item->draw(); map_item->setVisible(true); } if (collision_item) { collision_item->setVisible(false); } - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionItemsVisible(ui->checkBox_ToggleBorder->isChecked()); - setConnectionsEditable(false); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); updateWarpEventWarnings(); @@ -181,17 +180,6 @@ void Editor::setEditingConnections() { map_item->paintingMode = MapPixmapItem::PaintMode::Disabled; map_item->draw(); map_item->setVisible(true); - populateConnectionMapPickers(); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - setDiveEmergeControls(); - bool controlsEnabled = selected_connection_item != nullptr; - setConnectionEditControlsEnabled(controlsEnabled); - if (selected_connection_item) { - onConnectionOffsetChanged(selected_connection_item->connection->offset); - setConnectionMap(selected_connection_item->connection->map_name); - setCurrentConnectionDirection(selected_connection_item->connection->direction); - } - maskNonVisibleConnectionTiles(); } if (collision_item) { collision_item->setVisible(false); @@ -199,9 +187,7 @@ void Editor::setEditingConnections() { if (events_group) { events_group->setVisible(false); } - setBorderItemsVisible(true, 0.4); - setConnectionItemsVisible(true); - setConnectionsEditable(true); + updateBorderVisibility(); this->cursorMapTileRect->setSingleTileMode(); this->cursorMapTileRect->setActive(false); } @@ -747,193 +733,297 @@ void Editor::updateEncounterFields(EncounterFields newFields) { project->wildMonFields = newFields; } -void Editor::setDiveEmergeControls() { - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); - ui->comboBox_DiveMap->setCurrentText(""); - ui->comboBox_EmergeMap->setCurrentText(""); - for (MapConnection* connection : map->connections) { - if (connection->direction == "dive") { - ui->comboBox_DiveMap->setCurrentText(connection->map_name); - } else if (connection->direction == "emerge") { - ui->comboBox_EmergeMap->setCurrentText(connection->map_name); - } +void Editor::disconnectMapConnection(MapConnection *connection) { + // Disconnect MapConnection's signals used by the display. + // It'd be nice if we could just 'connection->disconnect(this)' but that doesn't account for lambda functions. + QObject::disconnect(connection, &MapConnection::targetMapNameChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::directionChanged, nullptr, nullptr); + QObject::disconnect(connection, &MapConnection::offsetChanged, nullptr, nullptr); +} + +void Editor::displayConnection(MapConnection *connection) { + if (!connection) + return; + + if (MapConnection::isDiving(connection->direction())) { + displayDivingConnection(connection); + return; + } + + // Create connection image + ConnectionPixmapItem *pixmapItem = new ConnectionPixmapItem(connection, getConnectionOrigin(connection)); + pixmapItem->render(); + scene->addItem(pixmapItem); + maskNonVisibleConnectionTiles(); + + // Create item for the list panel + ConnectionsListItem *listItem = new ConnectionsListItem(ui->scrollAreaContents_ConnectionsList, pixmapItem->connection, project->mapNames); + ui->layout_ConnectionsList->insertWidget(ui->layout_ConnectionsList->count() - 1, listItem); // Insert above the vertical spacer + + // Double clicking the pixmap or clicking the list item's map button opens the connected map + connect(listItem, &ConnectionsListItem::openMapClicked, this, &Editor::openConnectedMap); + connect(pixmapItem, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::openConnectedMap); + + // Sync the selection highlight between the list UI and the pixmap + connect(pixmapItem, &ConnectionPixmapItem::selectionChanged, [=](bool selected) { + listItem->setSelected(selected); + if (selected) setSelectedConnectionItem(pixmapItem); + }); + connect(listItem, &ConnectionsListItem::selected, [=] { + setSelectedConnectionItem(pixmapItem); + }); + + // Sync edits to 'offset' between the list UI and the pixmap + connect(connection, &MapConnection::offsetChanged, [=](int, int) { + listItem->updateUI(); + pixmapItem->updatePos(); + maskNonVisibleConnectionTiles(); + }); + + // Sync edits to 'direction' between the list UI and the pixmap + connect(connection, &MapConnection::directionChanged, [=](QString, QString) { + listItem->updateUI(); + updateConnectionPixmap(pixmapItem); + }); + + // Sync edits to 'map' between the list UI and the pixmap + connect(connection, &MapConnection::targetMapNameChanged, [=](QString, QString) { + listItem->updateUI(); + updateConnectionPixmap(pixmapItem); + }); + + // When the pixmap is deleted, remove its associated list item + connect(pixmapItem, &ConnectionPixmapItem::destroyed, listItem, &ConnectionsListItem::deleteLater); + + connection_items.append(pixmapItem); + + // If this was a recent addition from the user we should select it. + // We intentionally exclude connections added programmatically, e.g. by mirroring. + if (connection_to_select == connection) { + connection_to_select = nullptr; + setSelectedConnectionItem(pixmapItem); } - ui->comboBox_DiveMap->blockSignals(false); - ui->comboBox_EmergeMap->blockSignals(false); } -void Editor::populateConnectionMapPickers() { - ui->comboBox_ConnectedMap->blockSignals(true); - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); +void Editor::addConnection(MapConnection *connection) { + if (!connection) + return; - ui->comboBox_ConnectedMap->clear(); - ui->comboBox_ConnectedMap->addItems(project->mapNames); - ui->comboBox_DiveMap->clear(); - ui->comboBox_DiveMap->addItems(project->mapNames); - ui->comboBox_EmergeMap->clear(); - ui->comboBox_EmergeMap->addItems(project->mapNames); + // Mark this connection to be selected once its display elements have been created. + // It's possible this is a Dive/Emerge connection, but that's ok (no selection will occur). + connection_to_select = connection; - ui->comboBox_ConnectedMap->blockSignals(false); - ui->comboBox_DiveMap->blockSignals(true); - ui->comboBox_EmergeMap->blockSignals(true); + this->map->editHistory.push(new MapConnectionAdd(this->map, connection)); } -void Editor::setConnectionItemsVisible(bool visible) { - for (ConnectionPixmapItem* item : connection_items) { - item->setVisible(visible); - item->setEnabled(visible); - } +void Editor::removeConnection(MapConnection *connection) { + if (!connection) + return; + this->map->editHistory.push(new MapConnectionRemove(this->map, connection)); } -void Editor::setBorderItemsVisible(bool visible, qreal opacity) { - for (QGraphicsPixmapItem* item : borderItems) { - item->setVisible(visible); - item->setOpacity(opacity); - } +void Editor::removeSelectedConnection() { + if (selected_connection_item) + removeConnection(selected_connection_item->connection); } -void Editor::setCurrentConnectionDirection(QString curDirection) { - if (!selected_connection_item) +void Editor::removeConnectionPixmap(MapConnection *connection) { + if (!connection) return; - Map *connected_map = project->getMap(selected_connection_item->connection->map_name); - if (!connected_map) { + + disconnectMapConnection(connection); + + if (MapConnection::isDiving(connection->direction())) { + removeDivingMapPixmap(connection); return; } - selected_connection_item->connection->direction = curDirection; + int i; + for (i = 0; i < connection_items.length(); i++) { + if (connection_items.at(i)->connection == connection) + break; + } + if (i == connection_items.length()) + return; // Connection is not displayed, nothing to do. - QPixmap pixmap = connected_map->renderConnection(*selected_connection_item->connection, map->layout); - int offset = selected_connection_item->connection->offset; - selected_connection_item->initialOffset = offset; - int x = 0, y = 0; - if (selected_connection_item->connection->direction == "up") { - x = offset * 16; - y = -pixmap.height(); - } else if (selected_connection_item->connection->direction == "down") { - x = offset * 16; - y = map->getHeight() * 16; - } else if (selected_connection_item->connection->direction == "left") { - x = -pixmap.width(); - y = offset * 16; - } else if (selected_connection_item->connection->direction == "right") { - x = map->getWidth() * 16; - y = offset * 16; - } - - selected_connection_item->basePixmap = pixmap; - QPainter painter(&pixmap); - painter.setPen(QColor(255, 0, 255)); - painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); - painter.end(); - selected_connection_item->setPixmap(pixmap); - selected_connection_item->initialX = x; - selected_connection_item->initialY = y; - selected_connection_item->blockSignals(true); - selected_connection_item->setX(x); - selected_connection_item->setY(y); - selected_connection_item->setZValue(-1); - selected_connection_item->blockSignals(false); - - setConnectionEditControlValues(selected_connection_item->connection); -} - -void Editor::updateCurrentConnectionDirection(QString curDirection) { - if (!selected_connection_item) + auto pixmapItem = connection_items.takeAt(i); + if (pixmapItem == selected_connection_item) { + // This was the selected connection, select the next one up in the list. + selected_connection_item = nullptr; + if (i != 0) i--; + if (connection_items.length() > i) + setSelectedConnectionItem(connection_items.at(i)); + } + + if (pixmapItem->scene()) + pixmapItem->scene()->removeItem(pixmapItem); + + delete pixmapItem; +} + +void Editor::displayDivingConnection(MapConnection *connection) { + if (!connection) return; - QString originalDirection = selected_connection_item->connection->direction; - setCurrentConnectionDirection(curDirection); - updateMirroredConnectionDirection(selected_connection_item->connection, originalDirection); - maskNonVisibleConnectionTiles(); + const QString direction = connection->direction(); + if (!MapConnection::isDiving(direction)) + return; + + // Note: We only support editing 1 Dive and Emerge connection per map. + // In a vanilla game only the first Dive/Emerge connection is considered, so allowing + // users to have multiple is likely to lead to confusion. In case users have changed + // this we won't delete extra diving connections, but we'll only display the first one. + if (diving_map_items.value(direction)) + return; + + // Create map display + auto comboBox = (direction == "dive") ? ui->comboBox_DiveMap : ui->comboBox_EmergeMap; + auto item = new DivingMapPixmapItem(connection, comboBox); + scene->addItem(item); + diving_map_items.insert(direction, item); + + updateDivingMapsVisibility(); } -void Editor::onConnectionMoved(MapConnection* connection) { - updateMirroredConnectionOffset(connection); - onConnectionOffsetChanged(connection->offset); - maskNonVisibleConnectionTiles(); +void Editor::renderDivingConnections() { + for (auto item : diving_map_items.values()) + item->updatePixmap(); } -void Editor::onConnectionOffsetChanged(int newOffset) { - ui->spinBox_ConnectionOffset->blockSignals(true); - ui->spinBox_ConnectionOffset->setValue(newOffset); - ui->spinBox_ConnectionOffset->blockSignals(false); +void Editor::removeDivingMapPixmap(MapConnection *connection) { + if (!connection) + return; + + const QString direction = connection->direction(); + if (!diving_map_items.contains(direction)) + return; + + // If the diving map being removed is different than the one that's currently displayed we don't need to do anything. + if (diving_map_items.value(direction)->connection() != connection) + return; + + // Delete map image + auto pixmapItem = diving_map_items.take(direction); + if (pixmapItem->scene()) + pixmapItem->scene()->removeItem(pixmapItem); + delete pixmapItem; + + // Reveal any previously-hidden connection (because we only ever display one diving map of each type). + // Note: When this occurs as a result of the user clicking the 'X' clear button it seems the QComboBox + // doesn't expect the line edit to be immediately repopulated, and the 'X' doesn't reappear. + // As a workaround we wait before displaying the new text. The wait time is essentially arbitrary. + for (auto i : map->getConnections()) { + if (i->direction() == direction) { + QTimer::singleShot(10, Qt::CoarseTimer, [this, i]() { displayDivingConnection(i); }); + break; + } + } + updateDivingMapsVisibility(); +} +void Editor::updateDiveMap(QString mapName) { + setDivingMapName(mapName, "dive"); } -void Editor::setConnectionEditControlValues(MapConnection* connection) { - QString mapName = connection ? connection->map_name : ""; - QString direction = connection ? connection->direction : ""; - int offset = connection ? connection->offset : 0; +void Editor::updateEmergeMap(QString mapName) { + setDivingMapName(mapName, "emerge"); +} - ui->comboBox_ConnectedMap->blockSignals(true); - ui->comboBox_ConnectionDirection->blockSignals(true); - ui->spinBox_ConnectionOffset->blockSignals(true); +void Editor::setDivingMapName(QString mapName, QString direction) { + auto pixmapItem = diving_map_items.value(direction); + MapConnection *connection = pixmapItem ? pixmapItem->connection() : nullptr; - ui->comboBox_ConnectedMap->setCurrentText(mapName); - ui->comboBox_ConnectionDirection->setCurrentText(direction); - ui->spinBox_ConnectionOffset->setValue(offset); + if (connection) { + if (mapName == connection->targetMapName()) + return; // No change - ui->comboBox_ConnectedMap->blockSignals(false); - ui->comboBox_ConnectionDirection->blockSignals(false); - ui->spinBox_ConnectionOffset->blockSignals(false); + // Update existing connection + if (mapName.isEmpty()) { + removeConnection(connection); + } else { + map->editHistory.push(new MapConnectionChangeMap(connection, mapName)); + } + } else if (!mapName.isEmpty()) { + // Create new connection + addConnection(new MapConnection(mapName, direction)); + } } -void Editor::setConnectionEditControlsEnabled(bool enabled) { - ui->comboBox_ConnectionDirection->setEnabled(enabled); - ui->comboBox_ConnectedMap->setEnabled(enabled); - ui->spinBox_ConnectionOffset->setEnabled(enabled); +void Editor::updateDivingMapsVisibility() { + auto dive = diving_map_items.value("dive"); + auto emerge = diving_map_items.value("emerge"); - if (!enabled) { - setConnectionEditControlValues(nullptr); + if (dive && emerge) { + // Both connections in use, use separate sliders + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(0); + dive->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveMapOpacity) / 100); + emerge->setOpacity(!porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.emergeMapOpacity) / 100); + } else { + // One connection in use (or none), use single slider + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); + qreal opacity = !porymapConfig.showDiveEmergeMaps ? 0 : static_cast(porymapConfig.diveEmergeMapOpacity) / 100; + if (dive) dive->setOpacity(opacity); + else if (emerge) emerge->setOpacity(opacity); } } -void Editor::setConnectionsEditable(bool editable) { - for (ConnectionPixmapItem* item : connection_items) { - item->setEditable(editable); - item->updateHighlight(item == selected_connection_item); +// Get the 'origin' point for the connection's pixmap, i.e. where it should be positioned in the editor when connection->offset() == 0. +// This differs depending on the connection's direction and the dimensions of its target map or parent map. +QPoint Editor::getConnectionOrigin(MapConnection *connection) { + if (!connection) + return QPoint(0, 0); + + Map *parentMap = connection->parentMap(); + Map *targetMap = connection->targetMap(); + const QString direction = connection->direction(); + int x = 0, y = 0; + + if (direction == "right") { + if (parentMap) x = parentMap->getWidth(); + } else if (direction == "down") { + if (parentMap) y = parentMap->getHeight(); + } else if (direction == "left") { + if (targetMap) x = -targetMap->getConnectionRect(direction).width(); + } else if (direction == "up") { + if (targetMap) y = -targetMap->getConnectionRect(direction).height(); } + return QPoint(x * 16, y * 16); } -void Editor::onConnectionItemSelected(ConnectionPixmapItem* connectionItem) { - if (!connectionItem) +void Editor::updateConnectionPixmap(ConnectionPixmapItem *pixmapItem) { + if (!pixmapItem) return; - selected_connection_item = connectionItem; - for (ConnectionPixmapItem* item : connection_items) - item->updateHighlight(item == selected_connection_item); - setConnectionEditControlsEnabled(true); - setConnectionEditControlValues(selected_connection_item->connection); - ui->spinBox_ConnectionOffset->setMaximum(selected_connection_item->getMaxOffset()); - ui->spinBox_ConnectionOffset->setMinimum(selected_connection_item->getMinOffset()); - onConnectionOffsetChanged(selected_connection_item->connection->offset); -} + pixmapItem->setOrigin(getConnectionOrigin(pixmapItem->connection)); + pixmapItem->render(true); // Full render to reflect map changes -void Editor::setSelectedConnectionFromMap(QString mapName) { - // Search for the first connection that connects to the given map map. - for (ConnectionPixmapItem* item : connection_items) { - if (item->connection->map_name == mapName) { - onConnectionItemSelected(item); - break; - } - } + maskNonVisibleConnectionTiles(); } -void Editor::onConnectionItemDoubleClicked(ConnectionPixmapItem* connectionItem) { - emit loadMapRequested(connectionItem->connection->map_name, map->name); +void Editor::setSelectedConnectionItem(ConnectionPixmapItem *pixmapItem) { + if (!pixmapItem || pixmapItem == selected_connection_item) + return; + + if (selected_connection_item) selected_connection_item->setSelected(false); + selected_connection_item = pixmapItem; + selected_connection_item->setSelected(true); } -void Editor::onConnectionDirectionChanged(QString newDirection) { - ui->comboBox_ConnectionDirection->blockSignals(true); - ui->comboBox_ConnectionDirection->setCurrentText(newDirection); - ui->comboBox_ConnectionDirection->blockSignals(false); +void Editor::setSelectedConnection(MapConnection *connection) { + if (!connection) + return; + + for (auto item : connection_items) { + if (item->connection == connection) { + setSelectedConnectionItem(item); + break; + } + } } void Editor::onBorderMetatilesChanged() { displayMapBorder(); - setBorderItemsVisible(ui->checkBox_ToggleBorder->isChecked()); + updateBorderVisibility(); } void Editor::onHoveredMovementPermissionChanged(uint16_t collision, uint16_t elevation) { @@ -1113,7 +1203,10 @@ bool Editor::setMap(QString map_name) { // disconnect previous map's signals so they are not firing // multiple times if set again in the future if (map) { + map->pruneEditHistory(); map->disconnect(this); + for (auto connection : map->getConnections()) + disconnectMapConnection(connection); } if (project) { @@ -1133,6 +1226,8 @@ bool Editor::setMap(QString map_name) { map_ruler->setMapDimensions(QSize(map->getWidth(), map->getHeight())); connect(map, &Map::mapDimensionsChanged, map_ruler, &MapRuler::setMapDimensions); connect(map, &Map::openScriptRequested, this, &Editor::openScript); + connect(map, &Map::connectionAdded, this, &Editor::displayConnection); + connect(map, &Map::connectionRemoved, this, &Editor::removeConnectionPixmap); updateSelectedEvents(); } @@ -1354,16 +1449,13 @@ void Editor::clearMap() { clearBorderMetatiles(); clearCurrentMetatilesSelection(); clearMapEvents(); - //clearMapConnections(); + clearMapConnections(); clearMapBorder(); clearMapGrid(); clearWildMonTables(); + clearConnectionMask(); - // TODO: Handle connections after redesign PR. - selected_connection_item = nullptr; - connection_items.clear(); - connection_mask = nullptr; - + // Clear pointers to objects deleted elsewhere current_view = nullptr; map = nullptr; @@ -1393,6 +1485,7 @@ bool Editor::displayMap() { displayMapBorder(); displayMapGrid(); displayWildMonTables(); + maskNonVisibleConnectionTiles(); this->map_ruler->setZValue(1000); scene->addItem(this->map_ruler); @@ -1611,66 +1704,43 @@ DraggablePixmapItem *Editor::addMapEvent(Event *event) { return object; } -void Editor::displayMapConnections() { - for (ConnectionPixmapItem* item : connection_items) { - if (item->scene()) { +void Editor::clearMapConnections() { + for (auto item : connection_items) { + if (item->scene()) item->scene()->removeItem(item); - } delete item; } - selected_connection_item = nullptr; connection_items.clear(); - for (MapConnection *connection : map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") { - continue; - } - createConnectionItem(connection); - } + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + ui->comboBox_DiveMap->setCurrentText(""); + ui->comboBox_EmergeMap->setCurrentText(""); - if (!connection_items.empty()) { - onConnectionItemSelected(connection_items.first()); + for (auto item : diving_map_items.values()) { + if (item->scene()) + item->scene()->removeItem(item); + delete item; } + diving_map_items.clear(); - maskNonVisibleConnectionTiles(); + // Reset to single opacity slider + ui->stackedWidget_DiveMapOpacity->setCurrentIndex(1); + + selected_connection_item = nullptr; } -void Editor::createConnectionItem(MapConnection* connection) { - Map *connected_map = project->getMap(connection->map_name); - if (!connected_map) { - return; - } +void Editor::displayMapConnections() { + clearMapConnections(); - QPixmap pixmap = connected_map->renderConnection(*connection, map->layout); - int offset = connection->offset; - int x = 0, y = 0; - if (connection->direction == "up") { - x = offset * 16; - y = -pixmap.height(); - } else if (connection->direction == "down") { - x = offset * 16; - y = map->getHeight() * 16; - } else if (connection->direction == "left") { - x = -pixmap.width(); - y = offset * 16; - } else if (connection->direction == "right") { - x = map->getWidth() * 16; - y = offset * 16; - } - - ConnectionPixmapItem *item = new ConnectionPixmapItem(pixmap, connection, x, y, map->getWidth(), map->getHeight()); - item->setX(x); - item->setY(y); - item->setZValue(-1); - scene->addItem(item); - connect(item, &ConnectionPixmapItem::connectionMoved, this, &Editor::onConnectionMoved); - connect(item, &ConnectionPixmapItem::connectionItemSelected, this, &Editor::onConnectionItemSelected); - connect(item, &ConnectionPixmapItem::connectionItemDoubleClicked, this, &Editor::onConnectionItemDoubleClicked); - connection_items.append(item); + for (auto connection : map->getConnections()) + displayConnection(connection); + + if (!connection_items.isEmpty()) + setSelectedConnectionItem(connection_items.first()); } -// Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). -void Editor::maskNonVisibleConnectionTiles() { +void Editor::clearConnectionMask() { if (connection_mask) { if (connection_mask->scene()) { connection_mask->scene()->removeItem(connection_mask); @@ -1678,6 +1748,11 @@ void Editor::maskNonVisibleConnectionTiles() { delete connection_mask; connection_mask = nullptr; } +} + +// Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). +void Editor::maskNonVisibleConnectionTiles() { + clearConnectionMask(); QPainterPath mask; mask.addRect(scene->itemsBoundingRect().toRect()); @@ -1719,7 +1794,7 @@ void Editor::displayMapBorder() { item->setX(x * 16); item->setY(y * 16); item->setZValue(-3); - scene->addItem(item); // TODO: If the scene is taking ownership here is a double-free possible? + scene->addItem(item); borderItems.append(item); } } @@ -1732,17 +1807,8 @@ void Editor::updateMapBorder() { } void Editor::updateMapConnections() { - for (int i = 0; i < connection_items.size(); i++) { - Map *connected_map = project->getMap(connection_items[i]->connection->map_name); - if (!connected_map) - continue; - - QPixmap pixmap = connected_map->renderConnection(*(connection_items[i]->connection), map->layout); - connection_items[i]->basePixmap = pixmap; - connection_items[i]->setPixmap(pixmap); - } - - maskNonVisibleConnectionTiles(); + for (auto item : connection_items) + item->render(true); } int Editor::getBorderDrawDistance(int dimension) { @@ -1792,214 +1858,6 @@ void Editor::displayMapGrid() { connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::onToggleGridClicked); } -void Editor::updateConnectionOffset(int offset) { - if (!selected_connection_item) - return; - - selected_connection_item->blockSignals(true); - offset = qMin(offset, selected_connection_item->getMaxOffset()); - offset = qMax(offset, selected_connection_item->getMinOffset()); - selected_connection_item->connection->offset = offset; - if (selected_connection_item->connection->direction == "up" || selected_connection_item->connection->direction == "down") { - selected_connection_item->setX(selected_connection_item->initialX + (offset - selected_connection_item->initialOffset) * 16); - } else if (selected_connection_item->connection->direction == "left" || selected_connection_item->connection->direction == "right") { - selected_connection_item->setY(selected_connection_item->initialY + (offset - selected_connection_item->initialOffset) * 16); - } - selected_connection_item->blockSignals(false); - updateMirroredConnectionOffset(selected_connection_item->connection); - maskNonVisibleConnectionTiles(); -} - -void Editor::setConnectionMap(QString mapName) { - if (!mapName.isEmpty() && !project->mapNames.contains(mapName)) { - logError(QString("Invalid map name '%1' specified for connection.").arg(mapName)); - return; - } - if (!selected_connection_item) - return; - - if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { - removeCurrentConnection(); - return; - } - - QString originalMapName = selected_connection_item->connection->map_name; - setConnectionEditControlsEnabled(true); - selected_connection_item->connection->map_name = mapName; - setCurrentConnectionDirection(selected_connection_item->connection->direction); - - // New map may have a different minimum offset than the last one. The maximum will be the same. - int min = selected_connection_item->getMinOffset(); - ui->spinBox_ConnectionOffset->setMinimum(min); - onConnectionOffsetChanged(qMax(min, selected_connection_item->connection->offset)); - - updateMirroredConnectionMap(selected_connection_item->connection, originalMapName); - maskNonVisibleConnectionTiles(); -} - -void Editor::addNewConnection() { - // Find direction with least number of connections. - QMap directionCounts = QMap({{"up", 0}, {"right", 0}, {"down", 0}, {"left", 0}}); - for (MapConnection* connection : map->connections) { - directionCounts[connection->direction]++; - } - QString minDirection = "up"; - int minCount = INT_MAX; - for (QString direction : directionCounts.keys()) { - if (directionCounts[direction] < minCount) { - minDirection = direction; - minCount = directionCounts[direction]; - } - } - - // Don't connect the map to itself. - QString defaultMapName = project->mapNames.first(); - if (defaultMapName == map->name) { - defaultMapName = project->mapNames.value(1); - } - - MapConnection* newConnection = new MapConnection; - newConnection->direction = minDirection; - newConnection->offset = 0; - newConnection->map_name = defaultMapName; - map->connections.append(newConnection); - createConnectionItem(newConnection); - onConnectionItemSelected(connection_items.last()); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - - updateMirroredConnection(newConnection, newConnection->direction, newConnection->map_name); -} - -void Editor::updateMirroredConnectionOffset(MapConnection* connection) { - updateMirroredConnection(connection, connection->direction, connection->map_name); -} -void Editor::updateMirroredConnectionDirection(MapConnection* connection, QString originalDirection) { - updateMirroredConnection(connection, originalDirection, connection->map_name); -} -void Editor::updateMirroredConnectionMap(MapConnection* connection, QString originalMapName) { - updateMirroredConnection(connection, connection->direction, originalMapName); -} -void Editor::removeMirroredConnection(MapConnection* connection) { - updateMirroredConnection(connection, connection->direction, connection->map_name, true); -} -void Editor::updateMirroredConnection(MapConnection* connection, QString originalDirection, QString originalMapName, bool isDelete) { - if (!ui->checkBox_MirrorConnections->isChecked()) - return; - Map* otherMap = project->getMap(originalMapName); - if (!otherMap) - return; - - static QMap oppositeDirections = QMap({ - {"up", "down"}, {"right", "left"}, - {"down", "up"}, {"left", "right"}, - {"dive", "emerge"},{"emerge", "dive"}}); - QString oppositeDirection = oppositeDirections.value(originalDirection); - - // Find the matching connection in the connected map. - MapConnection* mirrorConnection = nullptr; - for (MapConnection* conn : otherMap->connections) { - if (conn->direction == oppositeDirection && conn->map_name == map->name) { - mirrorConnection = conn; - } - } - - if (isDelete) { - if (mirrorConnection) { - otherMap->connections.removeOne(mirrorConnection); - delete mirrorConnection; - } - return; - } - - if (connection->direction != originalDirection || connection->map_name != originalMapName) { - if (mirrorConnection) { - otherMap->connections.removeOne(mirrorConnection); - delete mirrorConnection; - mirrorConnection = nullptr; - otherMap = project->getMap(connection->map_name); - } - } - - // Create a new mirrored connection, if a matching one doesn't already exist. - if (!mirrorConnection) { - mirrorConnection = new MapConnection; - mirrorConnection->direction = oppositeDirections.value(connection->direction); - mirrorConnection->map_name = map->name; - otherMap->connections.append(mirrorConnection); - } - - mirrorConnection->offset = -connection->offset; -} - -void Editor::removeCurrentConnection() { - if (!selected_connection_item) - return; - - map->connections.removeOne(selected_connection_item->connection); - connection_items.removeOne(selected_connection_item); - removeMirroredConnection(selected_connection_item->connection); - - if (selected_connection_item && selected_connection_item->scene()) { - selected_connection_item->scene()->removeItem(selected_connection_item); - delete selected_connection_item; - } - - selected_connection_item = nullptr; - setConnectionEditControlsEnabled(false); - ui->spinBox_ConnectionOffset->setValue(0); - ui->label_NumConnections->setText(QString::number(map->connections.length())); - - if (connection_items.length() > 0) { - onConnectionItemSelected(connection_items.last()); - } -} - -void Editor::updateDiveMap(QString mapName) { - updateDiveEmergeMap(mapName, "dive"); -} - -void Editor::updateEmergeMap(QString mapName) { - updateDiveEmergeMap(mapName, "emerge"); -} - -void Editor::updateDiveEmergeMap(QString mapName, QString direction) { - if (!mapName.isEmpty() && !project->mapNamesToMapConstants.contains(mapName)) { - logError(QString("Invalid %1 connection map name: '%2'").arg(direction).arg(mapName)); - return; - } - - MapConnection* connection = nullptr; - for (MapConnection* conn : map->connections) { - if (conn->direction == direction) { - connection = conn; - break; - } - } - - if (mapName.isEmpty() || mapName == DYNAMIC_MAP_NAME) { - // Remove dive/emerge connection - if (connection) { - map->connections.removeOne(connection); - removeMirroredConnection(connection); - } - } else { - if (!connection) { - connection = new MapConnection; - connection->direction = direction; - connection->offset = 0; - connection->map_name = mapName; - map->connections.append(connection); - updateMirroredConnection(connection, connection->direction, connection->map_name); - } else { - QString originalMapName = connection->map_name; - connection->map_name = mapName; - updateMirroredConnectionMap(connection, originalMapName); - } - } - - ui->label_NumConnections->setText(QString::number(map->connections.length())); -} - void Editor::updatePrimaryTileset(QString tilesetLabel, bool forceLoad) { if (map->layout->tileset_primary_label != tilesetLabel || forceLoad) @@ -2022,17 +1880,42 @@ void Editor::updateSecondaryTileset(QString tilesetLabel, bool forceLoad) void Editor::toggleBorderVisibility(bool visible, bool enableScriptCallback) { - this->setBorderItemsVisible(visible); - this->setConnectionItemsVisible(visible); porymapConfig.showBorder = visible; + updateBorderVisibility(); if (enableScriptCallback) Scripting::cb_BorderVisibilityToggled(visible); } +void Editor::updateBorderVisibility() { + // On the connections tab the border is always visible, and the connections can be edited. + bool editingConnections = (ui->mainTabBar->currentIndex() == MainTab::Connections); + bool visible = (editingConnections || ui->checkBox_ToggleBorder->isChecked()); + + // Update border + const qreal borderOpacity = editingConnections ? 0.4 : 1; + for (QGraphicsPixmapItem* item : borderItems) { + item->setVisible(visible); + item->setOpacity(borderOpacity); + } + + // Update map connections + for (ConnectionPixmapItem* item : connection_items) { + item->setVisible(visible); + item->setEditable(editingConnections); + item->setEnabled(visible); + + // When connecting a map to itself we don't bother to re-render the map connections in real-time, + // i.e. if the user paints a new metatile on the map this isn't immediately reflected in the connection. + // We're rendering them now, so we take the opportunity to do a full re-render for self-connections. + bool fullRender = (this->map && item->connection && this->map->name == item->connection->targetMapName()); + item->render(fullRender); + } +} + void Editor::updateCustomMapHeaderValues(QTableWidget *table) { map->customHeaders = CustomAttributesTable::getAttributes(table); - emit editedMapData(); + map->modify(); } Tileset* Editor::getCurrentMapPrimaryTileset() diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 39167597c..92fbe5b57 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -18,6 +18,7 @@ #include "prefab.h" #include "montabwidget.h" #include "imageexport.h" +#include "newmapconnectiondialog.h" #include #include @@ -150,9 +151,9 @@ void MainWindow::initExtraShortcuts() { shortcutDuplicate_Events->setWhatsThis("Duplicate Selected Event(s)"); auto *shortcutDelete_Object = new Shortcut( - {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(on_toolButton_deleteObject_clicked())); + {QKeySequence("Del"), QKeySequence("Backspace")}, this, SLOT(onDeleteKeyPressed())); shortcutDelete_Object->setObjectName("shortcutDelete_Object"); - shortcutDelete_Object->setWhatsThis("Delete Selected Event(s)"); + shortcutDelete_Object->setWhatsThis("Delete Selected Item(s)"); auto *shortcutToggle_Border = new Shortcut(QKeySequence(), ui->checkBox_ToggleBorder, SLOT(toggle())); shortcutToggle_Border->setObjectName("shortcutToggle_Border"); @@ -208,16 +209,23 @@ void MainWindow::applyUserShortcuts() { shortcut->setKeys(shortcutsConfig.userShortcuts(shortcut)); } +static const QMap mainTabNames = { + {MainTab::Map, "Map"}, + {MainTab::Events, "Events"}, + {MainTab::Header, "Header"}, + {MainTab::Connections, "Connections"}, + {MainTab::WildPokemon, "Wild Pokemon"}, +}; + void MainWindow::initCustomUI() { // Set up the tab bar while (ui->mainTabBar->count()) ui->mainTabBar->removeTab(0); - ui->mainTabBar->addTab("Map"); - ui->mainTabBar->setTabIcon(0, QIcon(QStringLiteral(":/icons/map.ico"))); - ui->mainTabBar->addTab("Events"); - ui->mainTabBar->addTab("Header"); - ui->mainTabBar->addTab("Connections"); - ui->mainTabBar->addTab("Wild Pokemon"); - ui->mainTabBar->setTabIcon(4, QIcon(QStringLiteral(":/icons/tall_grass.ico"))); + + for (int i = 0; i < mainTabNames.count(); i++) + ui->mainTabBar->addTab(mainTabNames.value(i)); + + ui->mainTabBar->setTabIcon(MainTab::Map, QIcon(QStringLiteral(":/icons/map.ico"))); + ui->mainTabBar->setTabIcon(MainTab::WildPokemon, QIcon(QStringLiteral(":/icons/tall_grass.ico"))); } void MainWindow::initExtraSignals() { @@ -298,12 +306,11 @@ void MainWindow::checkForUpdates(bool) {} void MainWindow::initEditor() { this->editor = new Editor(ui); connect(this->editor, &Editor::objectsChanged, this, &MainWindow::updateObjects); - connect(this->editor, &Editor::loadMapRequested, this, &MainWindow::onLoadMapRequested); + 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::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); - connect(this->editor, &Editor::editedMapData, this, &MainWindow::markMapEdited); connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts); connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this->editor, &Editor::openProjectInTextEditor); @@ -355,9 +362,7 @@ void MainWindow::initEditor() { } void MainWindow::initMiscHeapObjects() { - mapIcon = new QIcon(QStringLiteral(":/icons/map.ico")); - mapEditedIcon = new QIcon(QStringLiteral(":/icons/map_edited.ico")); - mapOpenedIcon = new QIcon(QStringLiteral(":/icons/map_opened.ico")); + mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); mapListModel = new QStandardItemModel; mapGroupItemsList = new QList; @@ -400,10 +405,17 @@ void MainWindow::showWindowTitle() { } void MainWindow::markMapEdited() { - if (editor && editor->map) { - editor->map->hasUnsavedDataChanges = true; + if (editor) markMapEdited(editor->map); +} + +void MainWindow::markMapEdited(Map* map) { + if (!map) + return; + map->hasUnsavedDataChanges = true; + + updateMapListIcon(map->name); + if (editor && editor->map == map) showWindowTitle(); - } } void MainWindow::mapSortOrder_changed(QAction *action) @@ -448,6 +460,13 @@ void MainWindow::applyMapListFilter(QString filterText) } void MainWindow::loadUserSettings() { + const QSignalBlocker blocker1(ui->horizontalSlider_CollisionTransparency); + const QSignalBlocker blocker2(ui->slider_DiveEmergeMapOpacity); + const QSignalBlocker blocker3(ui->slider_DiveMapOpacity); + const QSignalBlocker blocker4(ui->slider_EmergeMapOpacity); + const QSignalBlocker blocker5(ui->horizontalSlider_MetatileZoom); + const QSignalBlocker blocker6(ui->horizontalSlider_CollisionZoom); + ui->actionBetter_Cursors->setChecked(porymapConfig.prettyCursors); this->editor->settings->betterCursors = porymapConfig.prettyCursors; ui->actionPlayer_View_Rectangle->setChecked(porymapConfig.showPlayerView); @@ -456,17 +475,17 @@ void MainWindow::loadUserSettings() { this->editor->settings->cursorTileRectEnabled = porymapConfig.showCursorTile; ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->checkBox_ToggleGrid->setChecked(porymapConfig.showGrid); - ui->horizontalSlider_CollisionTransparency->blockSignals(true); + ui->checkBox_MirrorConnections->setChecked(porymapConfig.mirrorConnectingMaps); this->editor->collisionOpacity = static_cast(porymapConfig.collisionOpacity) / 100; ui->horizontalSlider_CollisionTransparency->setValue(porymapConfig.collisionOpacity); - ui->horizontalSlider_CollisionTransparency->blockSignals(false); - ui->horizontalSlider_MetatileZoom->blockSignals(true); + ui->slider_DiveEmergeMapOpacity->setValue(porymapConfig.diveEmergeMapOpacity); + ui->slider_DiveMapOpacity->setValue(porymapConfig.diveMapOpacity); + ui->slider_EmergeMapOpacity->setValue(porymapConfig.emergeMapOpacity); ui->horizontalSlider_MetatileZoom->setValue(porymapConfig.metatilesZoom); - ui->horizontalSlider_MetatileZoom->blockSignals(false); - ui->horizontalSlider_CollisionZoom->blockSignals(true); ui->horizontalSlider_CollisionZoom->setValue(porymapConfig.collisionZoom); - ui->horizontalSlider_CollisionZoom->blockSignals(false); + setTheme(porymapConfig.theme); + setDivingMapsVisible(porymapConfig.showDiveEmergeMaps); } void MainWindow::restoreWindowState() { @@ -537,15 +556,17 @@ bool MainWindow::openProject(QString dir, bool initial) { Scripting::init(this); // Create the project - this->editor->project = new Project(this); - QObject::connect(this->editor->project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); - QObject::connect(this->editor->project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); - QObject::connect(this->editor->project, &Project::uncheckMonitorFilesAction, [this]() { + auto project = new Project(this); + project->set_root(dir); + QObject::connect(project, &Project::reloadProject, this, &MainWindow::on_action_Reload_Project_triggered); + QObject::connect(project, &Project::mapCacheCleared, this, &MainWindow::onMapCacheCleared); + QObject::connect(project, &Project::mapLoaded, this, &MainWindow::onMapLoaded); + QObject::connect(project, &Project::uncheckMonitorFilesAction, [this]() { porymapConfig.monitorFiles = false; if (this->preferenceEditor) this->preferenceEditor->updateFields(); }); - this->editor->project->set_root(dir); + this->editor->setProject(project); // Make sure project looks reasonable before attempting to load it if (!checkProjectSanity()) { @@ -720,9 +741,34 @@ void MainWindow::on_action_Close_Project_triggered() { porymapConfig.projectManuallyClosed = true; } +// setMap, but with a visible error message in case of failure. +// Use when the user is specifically requesting a map to open. +bool MainWindow::userSetMap(QString map_name, bool scrollTreeView) { + if (editor->map && editor->map->name == map_name) + return true; // Already set + + if (map_name == DYNAMIC_MAP_NAME) { + QMessageBox msgBox(this); + QString errorMsg = QString("The map '%1' can't be opened, it's a placeholder to indicate the specified map will be set programmatically.").arg(map_name); + msgBox.critical(nullptr, "Error Opening Map", errorMsg); + return false; + } + + if (!setMap(map_name, scrollTreeView)) { + QMessageBox msgBox(this); + QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") + .arg(map_name) + .arg(getLogPath()) + .arg(getMostRecentError()); + msgBox.critical(nullptr, "Error Opening Map", errorMsg); + return false; + } + return true; +} + bool MainWindow::setMap(QString map_name, bool scrollTreeView) { logInfo(QString("Setting map to '%1'").arg(map_name)); - if (map_name.isEmpty()) { + if (map_name.isEmpty() || map_name == DYNAMIC_MAP_NAME) { return false; } @@ -750,12 +796,13 @@ bool MainWindow::setMap(QString map_name, bool scrollTreeView) { showWindowTitle(); - connect(editor->map, &Map::mapChanged, this, &MainWindow::onMapChanged); connect(editor->map, &Map::mapNeedsRedrawing, this, &MainWindow::onMapNeedsRedrawing); - connect(editor->map, &Map::modified, [this](){ this->markMapEdited(); }); + // Swap the "currently-open" icon from the old map to the new map + if (!userConfig.recentMap.isEmpty() && userConfig.recentMap != map_name) + updateMapListIcon(userConfig.recentMap); userConfig.recentMap = map_name; - updateMapList(); + updateMapListIcon(userConfig.recentMap); Scripting::cb_MapOpened(map_name); prefab.updatePrefabUi(editor->map); @@ -801,10 +848,6 @@ void MainWindow::refreshMapScene() } void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_group) { - // Can't warp to dynamic maps - if (map_name == DYNAMIC_MAP_NAME) - return; - // Ensure valid destination map name. if (!editor->project->mapNames.contains(map_name)) { logError(QString("Invalid map name '%1'").arg(map_name)); @@ -812,15 +855,8 @@ void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_ } // Open the destination map. - if (!setMap(map_name, true)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(map_name) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); + if (!userSetMap(map_name, true)) return; - } // Select the target event. int index = event_id - Event::getIndexOffset(event_group); @@ -869,12 +905,8 @@ void MainWindow::displayMapProperties() { ui->frame_3->setEnabled(true); Map *map = editor->map; - ui->comboBox_PrimaryTileset->blockSignals(true); - ui->comboBox_SecondaryTileset->blockSignals(true); ui->comboBox_PrimaryTileset->setCurrentText(map->layout->tileset_primary_label); ui->comboBox_SecondaryTileset->setCurrentText(map->layout->tileset_secondary_label); - ui->comboBox_PrimaryTileset->blockSignals(false); - ui->comboBox_SecondaryTileset->blockSignals(false); ui->comboBox_Song->setCurrentText(map->song); ui->comboBox_Location->setCurrentText(map->location); @@ -997,6 +1029,8 @@ bool MainWindow::setProjectUI() { const QSignalBlocker blocker5(ui->comboBox_Weather); const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + const QSignalBlocker blocker8(ui->comboBox_DiveMap); + const QSignalBlocker blocker9(ui->comboBox_EmergeMap); // Set up project comboboxes ui->comboBox_Song->clear(); @@ -1013,14 +1047,21 @@ bool MainWindow::setProjectUI() { ui->comboBox_BattleScene->addItems(project->mapBattleScenes); ui->comboBox_Type->clear(); ui->comboBox_Type->addItems(project->mapTypes); + ui->comboBox_DiveMap->clear(); + ui->comboBox_DiveMap->addItems(project->mapNames); + ui->comboBox_DiveMap->setClearButtonEnabled(true); + ui->comboBox_DiveMap->setFocusedScrollingEnabled(false); + ui->comboBox_EmergeMap->clear(); + ui->comboBox_EmergeMap->addItems(project->mapNames); + ui->comboBox_EmergeMap->setClearButtonEnabled(true); + ui->comboBox_EmergeMap->setFocusedScrollingEnabled(false); sortMapList(); // Show/hide parts of the UI that are dependent on the user's project settings // Wild Encounters tab - // TODO: This index should come from an enum - ui->mainTabBar->setTabEnabled(4, editor->project->wildEncountersLoaded); + ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, editor->project->wildEncountersLoaded); bool hasFlags = projectConfig.mapAllowFlagsEnabled; ui->checkBox_AllowRunning->setVisible(hasFlags); @@ -1055,6 +1096,8 @@ void MainWindow::clearProjectUI() { const QSignalBlocker blocker5(ui->comboBox_Weather); const QSignalBlocker blocker6(ui->comboBox_BattleScene); const QSignalBlocker blocker7(ui->comboBox_Type); + const QSignalBlocker blocker8(ui->comboBox_DiveMap); + const QSignalBlocker blocker9(ui->comboBox_EmergeMap); ui->comboBox_Song->clear(); ui->comboBox_Location->clear(); @@ -1063,6 +1106,8 @@ void MainWindow::clearProjectUI() { ui->comboBox_Weather->clear(); ui->comboBox_BattleScene->clear(); ui->comboBox_Type->clear(); + ui->comboBox_DiveMap->clear(); + ui->comboBox_EmergeMap->clear(); // Clear map list mapListModel->clear(); @@ -1180,7 +1225,7 @@ void MainWindow::sortMapList() { QStandardItem* MainWindow::createMapItem(QString mapName, int groupNum, int inGroupNum) { QStandardItem *map = new QStandardItem; map->setText(QString("[%1.%2] ").arg(groupNum).arg(inGroupNum, 2, 10, QLatin1Char('0')) + mapName); - map->setIcon(*mapIcon); + map->setIcon(mapIcon); map->setEditable(false); map->setData(mapName, Qt::UserRole); map->setData("map_name", MapListUserRoles::TypeRole); @@ -1268,6 +1313,16 @@ void MainWindow::onNewMapCreated() { sortMapList(); setMap(newMapName, true); + // Refresh any combo box that displays map names and persists between maps + // (other combo boxes like for warp destinations are repopulated when the map changes). + int index = this->editor->project->mapNames.indexOf(newMapName); + if (index >= 0) { + const QSignalBlocker blocker1(ui->comboBox_DiveMap); + const QSignalBlocker blocker2(ui->comboBox_EmergeMap); + ui->comboBox_DiveMap->insertItem(index, newMapName); + ui->comboBox_EmergeMap->insertItem(index, newMapName); + } + if (newMap->needsHealLocation) { addNewEvent(Event::Type::HealLocation); editor->project->saveHealLocations(newMap); @@ -1472,59 +1527,61 @@ void MainWindow::currentMetatilesSelectionChanged() { void MainWindow::on_mapList_activated(const QModelIndex &index) { QVariant data = index.data(Qt::UserRole); - if (index.data(MapListUserRoles::TypeRole) == "map_name" && !data.isNull()) { - QString mapName = data.toString(); - if (!setMap(mapName)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(mapName) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); - } + if (index.data(MapListUserRoles::TypeRole) == "map_name" && !data.isNull()) + userSetMap(data.toString()); +} + +void MainWindow::updateMapListIcon(const QString &mapName) { + if (!editor->project || !editor->project->mapCache.contains(mapName)) + return; + + QStandardItem *item = mapListModel->itemFromIndex(mapListIndexes.value(mapName)); + if (!item) + return; + + static const QIcon mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); + static const QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); + + if (editor->map && editor->map->name == mapName) { + item->setIcon(mapOpenedIcon); + } else if (editor->project->mapCache.value(mapName)->hasUnsavedChanges()) { + item->setIcon(mapEditedIcon); + } else { + item->setIcon(mapIcon); } } -void MainWindow::drawMapListIcons(QAbstractItemModel *model) { - projectHasUnsavedChanges = false; +void MainWindow::updateMapList() { QList list; list.append(QModelIndex()); while (list.length()) { QModelIndex parent = list.takeFirst(); - for (int i = 0; i < model->rowCount(parent); i++) { - QModelIndex index = model->index(i, 0, parent); - if (model->hasChildren(index)) { + for (int i = 0; i < mapListModel->rowCount(parent); i++) { + QModelIndex index = mapListModel->index(i, 0, parent); + if (mapListModel->hasChildren(index)) { list.append(index); } QVariant data = index.data(Qt::UserRole); if (!data.isNull()) { - QString map_name = data.toString(); - if (editor->project && editor->project->mapCache.contains(map_name)) { - QStandardItem *map = mapListModel->itemFromIndex(mapListIndexes.value(map_name)); - map->setIcon(*mapIcon); - if (editor->project->mapCache.value(map_name)->hasUnsavedChanges()) { - map->setIcon(*mapEditedIcon); - projectHasUnsavedChanges = true; - } - if (editor->map->name == map_name) { - map->setIcon(*mapOpenedIcon); - } - } + updateMapListIcon(data.toString()); } } } } -void MainWindow::updateMapList() { - drawMapListIcons(mapListModel); -} - void MainWindow::on_action_Save_Project_triggered() { editor->saveProject(); updateMapList(); showWindowTitle(); } +void MainWindow::on_action_Save_triggered() { + editor->save(); + if (editor->map) + updateMapListIcon(editor->map->name); + showWindowTitle(); +} + void MainWindow::duplicate() { editor->duplicateSelectedEvents(); } @@ -1574,7 +1631,7 @@ void MainWindow::copy() { { default: break; - case 0: + case MainTab::Map: { // copy the map image QPixmap pixmap = editor->map ? editor->map->render(true) : QPixmap(); @@ -1582,7 +1639,7 @@ void MainWindow::copy() { logInfo("Copied current map image to clipboard"); break; } - case 1: + case MainTab::Events: { if (!editor || !editor->project) break; @@ -1622,7 +1679,7 @@ void MainWindow::copy() { } } } - else if (this->ui->mainTabBar->currentIndex() == 4) { + else if (this->ui->mainTabBar->currentIndex() == MainTab::WildPokemon) { QWidget *w = this->ui->stackedWidget_WildMons->currentWidget(); if (w) { MonTabWidget *mtw = static_cast(w); @@ -1653,7 +1710,7 @@ void MainWindow::paste() { QClipboard *clipboard = QGuiApplication::clipboard(); QString clipboardText(clipboard->text()); - if (ui->mainTabBar->currentIndex() == 4) { + if (ui->mainTabBar->currentIndex() == MainTab::WildPokemon) { QWidget *w = this->ui->stackedWidget_WildMons->currentWidget(); if (w) { w->setFocus(); @@ -1681,7 +1738,7 @@ void MainWindow::paste() { { default: break; - case 0: + case MainTab::Map: { // can only paste currently selected metatiles on this tab if (pasteObject["object"].toString() != "metatile_selection") { @@ -1777,12 +1834,6 @@ void MainWindow::paste() { } } -void MainWindow::on_action_Save_triggered() { - editor->save(); - updateMapList(); - showWindowTitle(); -} - void MainWindow::on_mapViewTab_tabBarClicked(int index) { int oldIndex = ui->mapViewTab->currentIndex(); @@ -1790,11 +1841,11 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) if (index != oldIndex) Scripting::cb_MapViewTabChanged(oldIndex, index); - if (index == 0) { + if (index == MapViewTab::Metatiles) { editor->setEditingMap(); - } else if (index == 1) { + } else if (index == MapViewTab::Collision) { editor->setEditingCollision(); - } else if (index == 2) { + } else if (index == MapViewTab::Prefabs) { editor->setEditingMap(); if (projectConfig.prefabFilepath.isEmpty() && !projectConfig.prefabImportPrompted) { // User hasn't set up prefabs and hasn't been prompted before. @@ -1813,25 +1864,33 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) if (index != oldIndex) Scripting::cb_MainTabChanged(oldIndex, index); - int tabIndexToStackIndex[5] = {0, 0, 1, 2, 3}; - ui->mainStackedWidget->setCurrentIndex(tabIndexToStackIndex[index]); + static const QMap tabIndexToStackIndex = { + {MainTab::Map, 0}, + {MainTab::Events, 0}, + {MainTab::Header, 1}, + {MainTab::Connections, 2}, + {MainTab::WildPokemon, 3}, + }; + ui->mainStackedWidget->setCurrentIndex(tabIndexToStackIndex.value(index)); - if (index == 0) { + if (index == MainTab::Map) { ui->stackedWidget_MapEvents->setCurrentIndex(0); on_mapViewTab_tabBarClicked(ui->mapViewTab->currentIndex()); clickToolButtonFromEditMode(editor->map_edit_mode); - } else if (index == 1) { + } else if (index == MainTab::Events) { ui->stackedWidget_MapEvents->setCurrentIndex(1); editor->setEditingObjects(); clickToolButtonFromEditMode(editor->obj_edit_mode); - } else if (index == 3) { + } else if (index == MainTab::Connections) { editor->setEditingConnections(); + // Stop the Dive/Emerge combo boxes from getting the initial focus + ui->graphicsView_Connections->setFocus(); } - if (index != 4) { + if (index != MainTab::WildPokemon) { if (editor->project && editor->project->wildEncountersLoaded) editor->saveEncounterTabData(); } - if (index != 1) { + if (index != MainTab::Events) { editor->map_ruler->setEnabled(false); } } @@ -2197,17 +2256,73 @@ void MainWindow::eventTabChanged(int index) { isProgrammaticEventTabChange = false; } +void MainWindow::on_actionDive_Emerge_Map_triggered() { + setDivingMapsVisible(ui->actionDive_Emerge_Map->isChecked()); +} + +void MainWindow::on_groupBox_DiveMapOpacity_toggled(bool on) { + setDivingMapsVisible(on); +} + +void MainWindow::setDivingMapsVisible(bool visible) { + // Qt doesn't change the style of disabled sliders, so we do it ourselves + QString stylesheet = visible ? "" : "QSlider::groove:horizontal {border: 1px solid #999999; border-radius: 3px; height: 2px; background: #B1B1B1;}" + "QSlider::handle:horizontal {border: 1px solid #444444; border-radius: 3px; width: 10px; height: 9px; margin: -5px -1px; background: #5C5C5C; }"; + ui->slider_DiveEmergeMapOpacity->setStyleSheet(stylesheet); + ui->slider_DiveMapOpacity->setStyleSheet(stylesheet); + ui->slider_EmergeMapOpacity->setStyleSheet(stylesheet); + + // Sync UI toggle elements + const QSignalBlocker blocker1(ui->groupBox_DiveMapOpacity); + const QSignalBlocker blocker2(ui->actionDive_Emerge_Map); + ui->groupBox_DiveMapOpacity->setChecked(visible); + ui->actionDive_Emerge_Map->setChecked(visible); + + porymapConfig.showDiveEmergeMaps = visible; + + if (visible) { + // We skip rendering diving maps if this setting is not enabled, + // so when we enable it we need to make sure they've rendered. + this->editor->renderDivingConnections(); + } + this->editor->updateDivingMapsVisibility(); +} + +// Normally a map only has either a Dive map connection or an Emerge map connection, +// in which case we only show a single opacity slider to modify the one in use. +// If a user has both connections we show two separate opacity sliders so they can +// modify them independently. +void MainWindow::on_slider_DiveEmergeMapOpacity_valueChanged(int value) { + porymapConfig.diveEmergeMapOpacity = value; + this->editor->updateDivingMapsVisibility(); +} + +void MainWindow::on_slider_DiveMapOpacity_valueChanged(int value) { + porymapConfig.diveMapOpacity = value; + this->editor->updateDivingMapsVisibility(); +} + +void MainWindow::on_slider_EmergeMapOpacity_valueChanged(int value) { + porymapConfig.emergeMapOpacity = value; + this->editor->updateDivingMapsVisibility(); +} + void MainWindow::on_horizontalSlider_CollisionTransparency_valueChanged(int value) { this->editor->collisionOpacity = static_cast(value) / 100; porymapConfig.collisionOpacity = value; this->editor->collision_item->draw(true); } -void MainWindow::on_toolButton_deleteObject_clicked() { - if (ui->mainTabBar->currentIndex() != 1) { - // do not delete an event when not on event tab - return; +void MainWindow::onDeleteKeyPressed() { + auto tab = ui->mainTabBar->currentIndex(); + if (tab == MainTab::Events) { + on_toolButton_deleteObject_clicked(); + } else if (tab == MainTab::Connections) { + if (editor) editor->removeSelectedConnection(); } +} + +void MainWindow::on_toolButton_deleteObject_clicked() { if (editor && editor->selected_events) { if (editor->selected_events->length()) { DraggablePixmapItem *nextSelectedEvent = nullptr; @@ -2254,15 +2369,14 @@ void MainWindow::on_toolButton_deleteObject_clicked() { void MainWindow::on_toolButton_Paint_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "paint"; else editor->obj_edit_mode = "paint"; editor->settings->mapCursor = QCursor(QPixmap(":/icons/pencil_cursor.ico"), 10, 10); - // do not stop single tile mode when editing collision - if (ui->mapViewTab->currentIndex() != 1) + if (ui->mapViewTab->currentIndex() != MapViewTab::Collision) editor->cursorMapTileRect->stopSingleTileMode(); ui->graphicsView_Map->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -2276,7 +2390,7 @@ void MainWindow::on_toolButton_Paint_clicked() void MainWindow::on_toolButton_Select_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "select"; else editor->obj_edit_mode = "select"; @@ -2295,7 +2409,7 @@ void MainWindow::on_toolButton_Select_clicked() void MainWindow::on_toolButton_Fill_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "fill"; else editor->obj_edit_mode = "fill"; @@ -2314,7 +2428,7 @@ void MainWindow::on_toolButton_Fill_clicked() void MainWindow::on_toolButton_Dropper_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "pick"; else editor->obj_edit_mode = "pick"; @@ -2333,7 +2447,7 @@ void MainWindow::on_toolButton_Dropper_clicked() void MainWindow::on_toolButton_Move_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "move"; else editor->obj_edit_mode = "move"; @@ -2352,7 +2466,7 @@ void MainWindow::on_toolButton_Move_clicked() void MainWindow::on_toolButton_Shift_clicked() { - if (ui->mainTabBar->currentIndex() == 0) + if (ui->mainTabBar->currentIndex() == MainTab::Map) editor->map_edit_mode = "shift"; else editor->obj_edit_mode = "shift"; @@ -2371,7 +2485,7 @@ void MainWindow::on_toolButton_Shift_clicked() void MainWindow::checkToolButtons() { QString edit_mode; - if (ui->mainTabBar->currentIndex() == 0) { + if (ui->mainTabBar->currentIndex() == MainTab::Map) { edit_mode = editor->map_edit_mode; } else { edit_mode = editor->obj_edit_mode; @@ -2405,21 +2519,11 @@ void MainWindow::clickToolButtonFromEditMode(QString editMode) { } } -void MainWindow::onLoadMapRequested(QString mapName, QString fromMapName) { - if (!setMap(mapName, true)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(mapName) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); +void MainWindow::onOpenConnectedMap(MapConnection *connection) { + if (!connection) return; - } - editor->setSelectedConnectionFromMap(fromMapName); -} - -void MainWindow::onMapChanged(Map *) { - updateMapList(); + if (userSetMap(connection->targetMapName(), true)) + editor->setSelectedConnection(connection->findMirror()); } void MainWindow::onMapNeedsRedrawing() { @@ -2430,6 +2534,10 @@ void MainWindow::onMapCacheCleared() { editor->map = nullptr; } +void MainWindow::onMapLoaded(Map *map) { + connect(map, &Map::modified, [this, map] { this->markMapEdited(map); }); +} + void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryTilesetLabel) { // If saved tilesets are currently in-use, update them and redraw // Otherwise overwrite the cache for the saved tileset @@ -2532,36 +2640,13 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { openSubWindow(this->mapImageExporter); } -void MainWindow::on_comboBox_ConnectionDirection_currentTextChanged(const QString &direction) -{ - editor->updateCurrentConnectionDirection(direction); - markMapEdited(); -} - -void MainWindow::on_spinBox_ConnectionOffset_valueChanged(int offset) -{ - editor->updateConnectionOffset(offset); - markMapEdited(); -} - -void MainWindow::on_comboBox_ConnectedMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { - editor->setConnectionMap(mapName); - markMapEdited(); - } -} - -void MainWindow::on_pushButton_AddConnection_clicked() -{ - editor->addNewConnection(); - markMapEdited(); -} +void MainWindow::on_pushButton_AddConnection_clicked() { + if (!this->editor || !this->editor->map || !this->editor->project) + return; -void MainWindow::on_pushButton_RemoveConnection_clicked() -{ - editor->removeCurrentConnection(); - markMapEdited(); + auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); + connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); + dialog->exec(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { @@ -2576,20 +2661,27 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { editor->configureEncounterJSON(this); } -void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { +void MainWindow::on_button_OpenDiveMap_clicked() { + const QString mapName = ui->comboBox_DiveMap->currentText(); + if (editor->project->mapNames.contains(mapName)) + userSetMap(mapName, true); +} + +void MainWindow::on_button_OpenEmergeMap_clicked() { + const QString mapName = ui->comboBox_EmergeMap->currentText(); + if (editor->project->mapNames.contains(mapName)) + userSetMap(mapName, true); +} + +void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { + // Include empty names as an update (user is deleting the connection) + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) editor->updateDiveMap(mapName); - markMapEdited(); - } } -void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) -{ - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) { +void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { + if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) editor->updateEmergeMap(mapName); - markMapEdited(); - } } void MainWindow::on_comboBox_PrimaryTileset_currentTextChanged(const QString &tilesetLabel) @@ -2715,8 +2807,12 @@ void MainWindow::on_checkBox_smartPaths_stateChanged(int selected) void MainWindow::on_checkBox_ToggleBorder_stateChanged(int selected) { - bool visible = selected != 0; - editor->toggleBorderVisibility(visible); + editor->toggleBorderVisibility(selected != 0); +} + +void MainWindow::on_checkBox_MirrorConnections_stateChanged(int selected) +{ + porymapConfig.mirrorConnectingMaps = (selected == Qt::Checked); } void MainWindow::on_actionTileset_Editor_triggered() @@ -3030,7 +3126,16 @@ bool MainWindow::closeProject() { if (!isProjectOpen()) return true; - if (projectHasUnsavedChanges || (editor->map && editor->map->hasUnsavedChanges())) { + // Check loaded maps for unsaved changes + bool unsavedChanges = false; + for (auto map : editor->project->mapCache.values()) { + if (map && map->hasUnsavedChanges()) { + unsavedChanges = true; + break; + } + } + + if (unsavedChanges) { QMessageBox::StandardButton result = QMessageBox::question( this, "porymap", "The project has been modified, save changes?", QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); diff --git a/src/project.cpp b/src/project.cpp index 7048812a1..10ace3961 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -171,6 +171,9 @@ void Project::clearTilesetCache() { } Map* Project::loadMap(QString map_name) { + if (map_name == DYNAMIC_MAP_NAME) + return nullptr; + Map *map; if (mapCache.contains(map_name)) { map = mapCache.value(map_name); @@ -183,17 +186,16 @@ Map* Project::loadMap(QString map_name) { map->setName(map_name); } - if (!(loadMapData(map) && loadMapLayout(map))) + if (!(loadMapData(map) && loadMapLayout(map))){ + delete map; return nullptr; + } mapCache.insert(map_name, map); + emit mapLoaded(map); return map; } -void Project::setNewMapConnections(Map *map) { - map->connections.clear(); -} - const QSet defaultTopLevelMapFields = { "id", "name", @@ -362,18 +364,17 @@ bool Project::loadMapData(Map* map) { } } - map->connections.clear(); + map->deleteConnections(); QJsonArray connectionsArr = mapObj["connections"].toArray(); if (!connectionsArr.isEmpty()) { for (int i = 0; i < connectionsArr.size(); i++) { QJsonObject connectionObj = connectionsArr[i].toObject(); - MapConnection *connection = new MapConnection; - connection->direction = ParseUtil::jsonToQString(connectionObj["direction"]); - connection->offset = ParseUtil::jsonToInt(connectionObj["offset"]); - QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); + const QString direction = ParseUtil::jsonToQString(connectionObj["direction"]); + int offset = ParseUtil::jsonToInt(connectionObj["offset"]); + const QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); if (mapConstantsToMapNames.contains(mapConstant)) { - connection->map_name = mapConstantsToMapNames.value(mapConstant); - map->connections.append(connection); + // Successully read map connection + map->loadConnection(new MapConnection(mapConstantsToMapNames.value(mapConstant), direction, offset)); } else { logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); } @@ -1274,17 +1275,18 @@ void Project::saveMap(Map *map) { mapObj["battle_scene"] = map->battle_scene; // Connections - if (map->connections.length() > 0) { + auto connections = map->getConnections(); + if (connections.length() > 0) { OrderedJson::array connectionsArr; - for (MapConnection* connection : map->connections) { - if (mapNamesToMapConstants.contains(connection->map_name)) { + for (auto connection : connections) { + if (mapNamesToMapConstants.contains(connection->targetMapName())) { OrderedJson::object connectionObj; - connectionObj["map"] = this->mapNamesToMapConstants.value(connection->map_name); - connectionObj["offset"] = connection->offset; - connectionObj["direction"] = connection->direction; + connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName()); + connectionObj["offset"] = connection->offset(); + connectionObj["direction"] = connection->direction(); connectionsArr.append(connectionObj); } else { - logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->map_name)); + logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->targetMapName())); } } mapObj["connections"] = connectionsArr; @@ -1816,18 +1818,22 @@ bool Project::readMapGroups() { } Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool existingLayout, bool importedMap) { - mapNames.append(mapName); - mapGroups.insert(mapName, groupNum); - groupedMapNames[groupNum].append(mapName); + int mapNamePos = 0; + for (int i = 0; i <= groupNum; i++) + mapNamePos += this->groupedMapNames.value(i).length(); + + this->mapNames.insert(mapNamePos, mapName); + this->mapGroups.insert(mapName, groupNum); + this->groupedMapNames[groupNum].append(mapName); newMap->isPersistedToFile = false; newMap->setName(mapName); - mapConstantsToMapNames.insert(newMap->constantName, newMap->name); - mapNamesToMapConstants.insert(newMap->name, newMap->constantName); + this->mapConstantsToMapNames.insert(newMap->constantName, newMap->name); + this->mapNamesToMapConstants.insert(newMap->name, newMap->constantName); if (!existingLayout) { - mapLayouts.insert(newMap->layoutId, newMap->layout); - mapLayoutsTable.append(newMap->layoutId); + this->mapLayouts.insert(newMap->layoutId, newMap->layout); + this->mapLayoutsTable.append(newMap->layoutId); if (!importedMap) { setNewMapBlockdata(newMap); } @@ -1838,7 +1844,6 @@ Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool loadLayoutTilesets(newMap->layout); setNewMapEvents(newMap); - setNewMapConnections(newMap); return newMap; } diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index d2b8ebf3c..a4aeae966 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -164,7 +164,10 @@ int ScriptUtility::getMapViewTab() { } void ScriptUtility::setMapViewTab(int index) { - if (this->getMainTab() != 0 || !window->ui->mapViewTab || index < 0 || index >= window->ui->mapViewTab->count()) + if (this->getMainTab() != MainTab::Map || !window->ui->mapViewTab || index < 0 || index >= window->ui->mapViewTab->count()) + return; + // Can't select tab if it's disabled + if (!window->ui->mapViewTab->isTabEnabled(index)) return; window->on_mapViewTab_tabBarClicked(index); } @@ -178,7 +181,7 @@ bool ScriptUtility::getGridVisibility() { } void ScriptUtility::setBorderVisibility(bool visible) { - window->editor->toggleBorderVisibility(visible, false); + window->ui->checkBox_ToggleBorder->setChecked(visible); } bool ScriptUtility::getBorderVisibility() { diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 256c556a0..f84120128 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -1,30 +1,48 @@ #include "connectionpixmapitem.h" +#include "editcommands.h" +#include "map.h" #include -void ConnectionPixmapItem::render(qreal opacity) { - QPixmap newPixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); - if (opacity < 1) { - QPainter painter(&newPixmap); - int alpha = static_cast(255 * (1 - opacity)); - painter.fillRect(0, 0, newPixmap.width(), newPixmap.height(), QColor(0, 0, 0, alpha)); - painter.end(); - } - this->setPixmap(newPixmap); +ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection, int x, int y) + : QGraphicsPixmapItem(connection->getPixmap()), + connection(connection) +{ + this->setEditable(true); + this->basePixmap = pixmap(); + this->setOrigin(x, y); } -int ConnectionPixmapItem::getMinOffset() { - if (this->connection->direction == "up" || this->connection->direction == "down") - return -(this->pixmap().width() / 16) - 6; - else - return -(this->pixmap().height() / 16) - 6; -} +ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection, QPoint pos) + : ConnectionPixmapItem(connection, pos.x(), pos.y()) +{} -int ConnectionPixmapItem::getMaxOffset() { - if (this->connection->direction == "up" || this->connection->direction == "down") - return this->baseMapWidth + 6; - else - return this->baseMapHeight + 6; +// Render additional visual effects on top of the base map image. +void ConnectionPixmapItem::render(bool ignoreCache) { + if (ignoreCache) + this->basePixmap = this->connection->getPixmap(); + + QPixmap pixmap = this->basePixmap.copy(0, 0, this->basePixmap.width(), this->basePixmap.height()); + this->setZValue(-1); + + // When editing is inactive the current selection is ignored, all connections should appear normal. + if (this->getEditable()) { + if (this->selected) { + // Draw highlight + QPainter painter(&pixmap); + painter.setPen(QColor(255, 0, 255)); + painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); + painter.end(); + } else { + // Darken the image + this->setZValue(-2); + QPainter painter(&pixmap); + int alpha = static_cast(255 * 0.25); + painter.fillRect(0, 0, pixmap.width(), pixmap.height(), QColor(0, 0, 0, alpha)); + painter.end(); + } + } + this->setPixmap(pixmap); } QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) @@ -32,32 +50,23 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari if (change == ItemPositionChange) { QPointF newPos = value.toPointF(); - qreal x, y; - int newOffset = this->initialOffset; - if (this->connection->direction == "up" || this->connection->direction == "down") { - x = round(newPos.x() / 16) * 16; - newOffset += (x - initialX) / 16; - newOffset = qMin(newOffset, this->getMaxOffset()); - newOffset = qMax(newOffset, this->getMinOffset()); - x = newOffset * 16; - } - else { - x = this->initialX; - } + qreal x = this->originX; + qreal y = this->originY; + int newOffset = this->connection->offset(); - if (this->connection->direction == "right" || this->connection->direction == "left") { - y = round(newPos.y() / 16) * 16; - newOffset += (y - this->initialY) / 16; - newOffset = qMin(newOffset, this->getMaxOffset()); - newOffset = qMax(newOffset, this->getMinOffset()); - y = newOffset * 16; - } - else { - y = this->initialY; + // Restrict movement to the metatile grid and perpendicular to the connection direction. + if (MapConnection::isVertical(this->connection->direction())) { + x = (round(newPos.x() / this->mWidth) * this->mWidth) - this->originX; + newOffset = x / this->mWidth; + } else if (MapConnection::isHorizontal(this->connection->direction())) { + y = (round(newPos.y() / this->mHeight) * this->mHeight) - this->originY; + newOffset = y / this->mHeight; } - this->connection->offset = newOffset; - emit connectionMoved(this->connection); + // This is convoluted because of how our edit history works; this would otherwise just be 'this->connection->setOffset(newOffset);' + if (this->connection->parentMap() && newOffset != this->connection->offset()) + this->connection->parentMap()->editHistory.push(new MapConnectionMove(this->connection, newOffset, this->actionId)); + return QPointF(x, y); } else { @@ -65,6 +74,32 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari } } +// If connection->offset changed externally we call this to correct our position. +void ConnectionPixmapItem::updatePos() { + const QSignalBlocker blocker(this); + + qreal x = this->originX; + qreal y = this->originY; + + if (MapConnection::isVertical(this->connection->direction())) { + x += this->connection->offset() * this->mWidth; + } else if (MapConnection::isHorizontal(this->connection->direction())) { + y += this->connection->offset() * this->mHeight; + } + + this->setPos(x, y); +} + +// Set the pixmap's external origin point, i.e. the pixmap's position when connection->offset == 0 +void ConnectionPixmapItem::setOrigin(int x, int y) { + this->originX = x; + this->originY = y; + updatePos(); +} +void ConnectionPixmapItem::setOrigin(QPoint pos) { + this->setOrigin(pos.x(), pos.y()); +} + void ConnectionPixmapItem::setEditable(bool editable) { setFlag(ItemIsMovable, editable); setFlag(ItemSendsGeometryChanges, editable); @@ -74,28 +109,25 @@ bool ConnectionPixmapItem::getEditable() { return (this->flags() & ItemIsMovable) != 0; } -void ConnectionPixmapItem::updateHighlight(bool selected) { - bool editable = this->getEditable(); - int zValue = (selected || !editable) ? -1 : -2; - qreal opacity = (selected || !editable) ? 1 : 0.75; - this->setZValue(zValue); - this->render(opacity); - if (editable && selected) { - QPixmap pixmap = this->pixmap(); - QPainter painter(&pixmap); - painter.setPen(QColor(255, 0, 255)); - painter.drawRect(0, 0, pixmap.width() - 1, pixmap.height() - 1); - painter.end(); - this->setPixmap(pixmap); - } +void ConnectionPixmapItem::setSelected(bool selected) { + if (this->selected == selected) + return; + this->selected = selected; + this->render(); + emit selectionChanged(selected); } void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { if (!this->getEditable()) return; - emit connectionItemSelected(this); + this->setSelected(true); +} + +void ConnectionPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { + this->actionId++; // Distinguish between move actions for the edit history + QGraphicsPixmapItem::mouseReleaseEvent(event); } void ConnectionPixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { - emit connectionItemDoubleClicked(this); + emit connectionItemDoubleClicked(this->connection); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp new file mode 100644 index 000000000..a5b8759a3 --- /dev/null +++ b/src/ui/connectionslistitem.cpp @@ -0,0 +1,103 @@ +#include "connectionslistitem.h" +#include "ui_connectionslistitem.h" +#include "editcommands.h" +#include "map.h" + +#include + +ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : + QFrame(parent), + ui(new Ui::ConnectionsListItem) +{ + ui->setupUi(this); + + const QSignalBlocker blocker1(ui->comboBox_Direction); + const QSignalBlocker blocker2(ui->comboBox_Map); + const QSignalBlocker blocker3(ui->spinBox_Offset); + + ui->comboBox_Direction->setEditable(false); + ui->comboBox_Direction->setMinimumContentsLength(0); + ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); + + ui->comboBox_Map->setMinimumContentsLength(6); + ui->comboBox_Map->addItems(mapNames); + ui->comboBox_Map->setFocusedScrollingEnabled(false); // Scrolling could cause rapid changes to many different maps + ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); + + ui->spinBox_Offset->setMinimum(INT_MIN); + ui->spinBox_Offset->setMaximum(INT_MAX); + + // Invalid map names are not considered a change. If editing finishes with an invalid name, restore the previous name. + connect(ui->comboBox_Map->lineEdit(), &QLineEdit::editingFinished, [this] { + const QSignalBlocker blocker(ui->comboBox_Map); + if (ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) + ui->comboBox_Map->setTextItem(this->connection->targetMapName()); + }); + + // Distinguish between move actions for the edit history + connect(ui->spinBox_Offset, &QSpinBox::editingFinished, [this] { this->actionId++; }); + + this->connection = connection; + this->map = connection->parentMap(); + this->updateUI(); +} + +ConnectionsListItem::~ConnectionsListItem() +{ + delete ui; +} + +void ConnectionsListItem::updateUI() { + if (!this->connection) + return; + + const QSignalBlocker blocker1(ui->comboBox_Direction); + const QSignalBlocker blocker2(ui->comboBox_Map); + const QSignalBlocker blocker3(ui->spinBox_Offset); + + ui->comboBox_Direction->setTextItem(this->connection->direction()); + ui->comboBox_Map->setTextItem(this->connection->targetMapName()); + ui->spinBox_Offset->setValue(this->connection->offset()); +} + +void ConnectionsListItem::setSelected(bool selected) { + if (selected == this->isSelected) + return; + this->isSelected = selected; + + this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" + : ".ConnectionsListItem { border-width: 1px; }"); + if (selected) + emit this->selected(); +} + +void ConnectionsListItem::mousePressEvent(QMouseEvent *) { + this->setSelected(true); +} + +void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(QString direction) { + this->setSelected(true); + if (this->map) + this->map->editHistory.push(new MapConnectionChangeDirection(this->connection, direction)); +} + +void ConnectionsListItem::on_comboBox_Map_currentTextChanged(QString mapName) { + this->setSelected(true); + if (this->map && ui->comboBox_Map->findText(mapName) >= 0) + this->map->editHistory.push(new MapConnectionChangeMap(this->connection, mapName)); +} + +void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { + this->setSelected(true); + if (this->map) + this->map->editHistory.push(new MapConnectionMove(this->connection, offset, this->actionId)); +} + +void ConnectionsListItem::on_button_Delete_clicked() { + if (this->map) + this->map->editHistory.push(new MapConnectionRemove(this->map, this->connection)); +} + +void ConnectionsListItem::on_button_OpenMap_clicked() { + emit openMapClicked(this->connection); +} diff --git a/src/ui/divingmappixmapitem.cpp b/src/ui/divingmappixmapitem.cpp new file mode 100644 index 000000000..70a611fa0 --- /dev/null +++ b/src/ui/divingmappixmapitem.cpp @@ -0,0 +1,46 @@ +#include "divingmappixmapitem.h" +#include "config.h" + +DivingMapPixmapItem::DivingMapPixmapItem(MapConnection *connection, QComboBox *combo) + : QGraphicsPixmapItem(getBasePixmap(connection)) +{ + m_connection = connection; + m_combo = combo; + + setComboText(connection->targetMapName()); + + // Update display if the connected map is swapped. + connect(m_connection, &MapConnection::targetMapNameChanged, this, &DivingMapPixmapItem::onTargetMapChanged); +} + +DivingMapPixmapItem::~DivingMapPixmapItem() { + // Clear map name from combo box + setComboText(""); +} + +QPixmap DivingMapPixmapItem::getBasePixmap(MapConnection* connection) { + if (!connection) + return QPixmap(); + if (!porymapConfig.showDiveEmergeMaps) + return QPixmap(); // Save some rendering time if it won't be displayed + if (connection->targetMapName() == connection->parentMapName()) + return QPixmap(); // If the map is connected to itself then rendering is pointless. + return connection->getPixmap(); +} + +void DivingMapPixmapItem::updatePixmap() { + setPixmap(getBasePixmap(m_connection)); +} + +void DivingMapPixmapItem::onTargetMapChanged() { + updatePixmap(); + setComboText(m_connection->targetMapName()); +} + +void DivingMapPixmapItem::setComboText(const QString &text) { + if (!m_combo) + return; + + const QSignalBlocker blocker(m_combo); + m_combo->setCurrentText(text); +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index e36d96068..65811474c 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -33,7 +33,7 @@ MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExpor this->editor = editor_; this->mode = mode; this->setWindowTitle(getTitle(this->mode)); - this->ui->groupBox_Connections->setVisible(this->mode == ImageExporterMode::Normal); + this->ui->groupBox_Connections->setVisible(this->mode != ImageExporterMode::Stitch); this->ui->groupBox_Timelapse->setVisible(this->mode == ImageExporterMode::Timelapse); this->ui->comboBox_MapSelection->addItems(editor->project->mapNames); @@ -144,7 +144,7 @@ void MapImageExporter::saveImage() { this->map->editHistory.redo(); } progress.setValue(progress.maximum() - i); - QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); + QPixmap pixmap = this->getFormattedMapPixmap(this->map); if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { QPixmap pixmap2 = QPixmap(maxWidth, maxHeight); QPainter painter(&pixmap2); @@ -167,7 +167,7 @@ void MapImageExporter::saveImage() { } } // The latest map state is the last animated frame. - QPixmap pixmap = this->getFormattedMapPixmap(this->map, !this->showBorder); + QPixmap pixmap = this->getFormattedMapPixmap(this->map); timelapseImg.addFrame(pixmap.toImage()); timelapseImg.save(filepath); progress.close(); @@ -178,6 +178,9 @@ void MapImageExporter::saveImage() { } bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { + if (command->isObsolete()) + return false; + switch (command->id() & 0xFF) { case CommandId::ID_PaintMetatile: case CommandId::ID_BucketFillMetatile: @@ -192,6 +195,12 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { return this->showCollision; case CommandId::ID_PaintBorder: return this->showBorder; + case CommandId::ID_MapConnectionMove: + case CommandId::ID_MapConnectionChangeDirection: + case CommandId::ID_MapConnectionChangeMap: + case CommandId::ID_MapConnectionAdd: + case CommandId::ID_MapConnectionRemove: + return this->showUpConnections || this->showDownConnections || this->showLeftConnections || this->showRightConnections; case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: @@ -238,25 +247,29 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu visited.insert(cur.map->name); stitchedMaps.append(cur); - for (MapConnection *connection : cur.map->connections) { - if (connection->direction == "dive" || connection->direction == "emerge") - continue; + for (MapConnection *connection : cur.map->getConnections()) { + const QString direction = connection->direction(); int x = cur.x; int y = cur.y; - int offset = connection->offset; - Map *connectionMap = this->editor->project->loadMap(connection->map_name); - if (connection->direction == "up") { + int offset = connection->offset(); + Map *connectionMap = connection->targetMap(); + if (!connectionMap) + continue; + if (direction == "up") { x += offset; y -= connectionMap->getHeight(); - } else if (connection->direction == "down") { + } else if (direction == "down") { x += offset; y += cur.map->getHeight(); - } else if (connection->direction == "left") { + } else if (direction == "left") { x -= connectionMap->getWidth(); y += offset; - } else if (connection->direction == "right") { + } else if (direction == "right") { x += cur.map->getWidth(); y += offset; + } else { + // Ignore Dive/Emerge connections and unrecognized directions + continue; } unvisited.append(StitchedMap{x, y, connectionMap}); } @@ -310,7 +323,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; } - QPixmap pixmap = this->getFormattedMapPixmap(map.map, false); + QPixmap pixmap = this->getFormattedMapPixmap(map.map); painter.drawPixmap(pixelX, pixelY, pixmap); } @@ -345,7 +358,7 @@ void MapImageExporter::updatePreview() { scene = nullptr; } - preview = getFormattedMapPixmap(this->map, false); + preview = getFormattedMapPixmap(this->map); scene = new QGraphicsScene; scene->addPixmap(preview); this->scene->setSceneRect(this->scene->itemsBoundingRect()); @@ -373,8 +386,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { // draw map border // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; - bool forceDrawBorder = showUpConnections || showDownConnections || showLeftConnections || showRightConnections; - if (!ignoreBorder && (showBorder || forceDrawBorder)) { + if (!ignoreBorder && this->showBorder) { int borderDistance = this->mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; map->renderBorder(); int borderHorzDist = editor->getBorderDrawDistance(map->getBorderWidth()); @@ -393,17 +405,17 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { pixmap = newPixmap; } - if (!this->mode) { + if (!ignoreBorder && (this->showUpConnections || this->showDownConnections || this->showLeftConnections || this->showRightConnections)) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); for (auto connectionItem : editor->connection_items) { - QString direction = connectionItem->connection->direction; + const QString direction = connectionItem->connection->direction(); if ((showUpConnections && direction == "up") || (showDownConnections && direction == "down") || (showLeftConnections && direction == "left") || (showRightConnections && direction == "right")) - connectionPainter.drawImage(connectionItem->initialX + borderWidth, connectionItem->initialY + borderHeight, - connectionItem->basePixmap.toImage()); + connectionPainter.drawImage(connectionItem->x() + borderWidth, connectionItem->y() + borderHeight, + connectionItem->connection->getPixmap().toImage()); } connectionPainter.end(); } @@ -412,7 +424,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { QPainter eventPainter(&pixmap); QList events = map->getAllEvents(); int pixelOffset = 0; - if (!ignoreBorder && showBorder) { + if (!ignoreBorder && this->showBorder) { pixelOffset = this->mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; } for (Event *event : events) { @@ -450,6 +462,18 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { return pixmap; } +void MapImageExporter::updateShowBorderState() { + // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) + const QSignalBlocker blocker(ui->checkBox_Border); + if (showUpConnections || showDownConnections || showLeftConnections || showRightConnections) { + ui->checkBox_Border->setChecked(true); + ui->checkBox_Border->setDisabled(true); + showBorder = true; + } else { + ui->checkBox_Border->setDisabled(false); + } +} + void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { showCollision = (state == Qt::Checked); updatePreview(); @@ -492,21 +516,25 @@ void MapImageExporter::on_checkBox_HealSpots_stateChanged(int state) { void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { showUpConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { showDownConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { showLeftConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { showRightConnections = (state == Qt::Checked); + updateShowBorderState(); updatePreview(); } diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp new file mode 100644 index 000000000..af06789fa --- /dev/null +++ b/src/ui/newmapconnectiondialog.cpp @@ -0,0 +1,72 @@ +#include "newmapconnectiondialog.h" +#include "ui_newmapconnectiondialog.h" + +NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames) : + QDialog(parent), + ui(new Ui::NewMapConnectionDialog) +{ + ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + + ui->comboBox_Direction->setEditable(false); + ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); + + ui->comboBox_Map->addItems(mapNames); + ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); + + // Choose default direction + QMap directionCounts; + for (auto connection : map->getConnections()) { + directionCounts[connection->direction()]++; + } + QString defaultDirection; + int minCount = INT_MAX; + for (auto direction : MapConnection::cardinalDirections) { + if (directionCounts[direction] < minCount) { + defaultDirection = direction; + minCount = directionCounts[direction]; + } + } + ui->comboBox_Direction->setTextItem(defaultDirection); + + // Choose default map + QString defaultMapName; + if (mapNames.isEmpty()) { + defaultMapName = QString(); + } else if (mapNames.first() == map->name && mapNames.length() > 1) { + // Prefer not to connect the map to itself + defaultMapName = mapNames.at(1); + } else { + defaultMapName = mapNames.first(); + } + ui->comboBox_Map->setTextItem(defaultMapName); + + connect(ui->comboBox_Map, &QComboBox::currentTextChanged, [this] { + if (ui->label_Warning->isVisible() && mapNameIsValid()) + setWarningVisible(false); + }); + setWarningVisible(false); +} + +NewMapConnectionDialog::~NewMapConnectionDialog() +{ + delete ui; +} + +bool NewMapConnectionDialog::mapNameIsValid() { + return ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) >= 0; +} + +void NewMapConnectionDialog::setWarningVisible(bool visible) { + ui->label_Warning->setVisible(visible); + adjustSize(); +} + +void NewMapConnectionDialog::accept() { + if (!mapNameIsValid()) { + setWarningVisible(true); + return; + } + emit accepted(new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText())); + QDialog::accept(); +} diff --git a/src/ui/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index a634af7f0..3db2a9567 100644 --- a/src/ui/noscrollcombobox.cpp +++ b/src/ui/noscrollcombobox.cpp @@ -1,6 +1,7 @@ #include "noscrollcombobox.h" #include +#include NoScrollComboBox::NoScrollComboBox(QWidget *parent) : QComboBox(parent) @@ -36,11 +37,17 @@ void NoScrollComboBox::setLineEdit(QLineEdit *edit) { void NoScrollComboBox::wheelEvent(QWheelEvent *event) { - // Only allow scrolling to modify contents when it explicitly has focus. - if (hasFocus()) + // By default NoScrollComboBoxes will allow scrolling to modify its contents only when it explicitly has focus. + // If focusedScrollingEnabled is false it won't allow scrolling even with focus. + if (this->focusedScrollingEnabled && hasFocus()) QComboBox::wheelEvent(event); } +void NoScrollComboBox::setFocusedScrollingEnabled(bool enabled) +{ + this->focusedScrollingEnabled = enabled; +} + void NoScrollComboBox::setItem(int index, const QString &text) { if (index >= 0) { @@ -73,3 +80,8 @@ void NoScrollComboBox::setHexItem(uint32_t value) { this->setItem(this->findData(value), "0x" + QString::number(value, 16).toUpper()); } + +void NoScrollComboBox::setClearButtonEnabled(bool enabled) { + if (this->lineEdit()) + this->lineEdit()->setClearButtonEnabled(enabled); +}