diff --git a/CHANGELOG.md b/CHANGELOG.md index fe1a6fa6..57898b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,58 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project somewhat adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The MAJOR version number is bumped when there are **"Breaking Changes"** in the pret projects. For more on this, see [the manual page on breaking changes](https://huderlem.github.io/porymap/manual/breaking-changes.html). ## [Unreleased] -Nothing, yet. +### Added +- Add `View > Show Unused Colors` to the Palette Editor. +- Add `Tools > Find Color Usage` to the Palette Editor. This opens a dialog showing which metatiles use a particular color. +- Add `Edit > Swap Metatiles` to the Tileset Editor. While in this mode, selecting two metatiles in the selector will swap their positions. When changes to the tilesets are saved these relocations will be applied to all layouts that use the relevant tileset(s). +- Add `View > Layer Arrangement` to the Tileset Editor, which changes whether the metatile layer view is oriented vertically (default) or horizontally. +- Add an `Export Metatiles Image` option to the Tileset Editor that provides many more options for customizing metatile images. +- Add an `Export Porytiles Layer Images` option to the Tileset Editor, which is a shortcut for individually exporting layer images that Porytiles can use. +- Add an option under `Preferences` to include common scripts in the autocomplete for Script labels. +- Add a setting under `Project Settings` to change the width of the metatile selectors. +- Add versions of the API functions `[get|set]MetatileLayerOrder` and `[get|set]MetatileLayerOpacity` that work globally, rather than on individual layouts. +- A link to Porymap's manual is now available under `Help`. + +### Changed +- The Player View Rectangle is now visible on the Events tab, as is the Cursor Tile Outline for certain tools. +- When hovering over tiles in the Tileset Editor their palette and x/yflip are now listed alongside the tile ID. +- The scroll position of the map view now remains the same between the Connections tab and the Map/Events tabs. +- The Move tool now behaves more like a traditional pan tool (with no momentum). +- The map image exporter now uses a checkered background to indicate transparency. +- Invalid tile IDs are now rendered as magenta (like invalid metatiles), instead of rendering the same as a transparent tile. +- While holding down `Ctrl` (`Cmd` on macOS) painting on the metatile layer view will now only change the tile's palette. +- Full menu paths are now listed for shortcuts in the Shortcuts Editor. +- Adding new event data to a map that has a `shared_events_map` will now remove the `shared_events_map`, rather than discard the event data. + +### Fixed +- Fix crash when rendering tiles with invalid palette numbers. +- Fix crash when opening the Tileset Editor for tilesets with no metatiles. +- Fix crash when changing the map/border size in certain API callbacks. +- Fix metatile images exporting at 2x scale. +- Fix display errors when a project's metatile limits are not divisible by 8. +- Fix incorrect dividing line position for primary tiles images that are smaller than the maximum size. +- Fix the checkered background of the `Change Dimensions` popup shifting while scrolling around. +- Fix pasting Wild Pokémon data then changing maps resetting the pasted data. +- Fix click-drag map selections behaving unexpectedly when the cursor is outside the map grid. +- Fix events being dragged in negative coordinates lagging behind the cursor. +- Fix the shortcut for duplicating events working while on the Connections tab. +- Fix the Shortcuts Editor displaying the duplicate shortcut prompt repeatedly. +- Fix the clear text button on the left in each row of the Shortcuts Editor also clearing the shortcut on the right. +- Fix Undo/Redo ignoring the automatic resizing that occurs if a layout/border was an unexpected size. +- Fix Undo/Redo in the Tileset and Palette Editors and Paste in the Tileset Editor appearing enabled even when they don't do anything. +- Fix `Ctrl+Shift+Z` not being set as a default shortcut for Redo in the Palette Editor like it is for other windows. +- Fix the Tileset Editor's status bar not updating while selecting tiles in the metatile layer view, or when pasting metatiles. +- Fix the main window's status bar not immediately reflecting changes made while painting metatiles / movement permissions. +- Fix cleared metatile labels not updating until the project is reloaded. +- Fix some changes in the Tileset Editor being discarded if the window is closed too quickly. +- Fix the Region Map Editor incorrectly displaying whether a `MAPSEC` has region map data. +- Fix the Primary/Secondary Tileset selectors allowing invalid text, and considering a map unsaved if changed to invalid text then back again. +- Fix broken error message for the primary tileset on the new map/layout dialogs. +- Fix the dialog for duplicating/importing a map layout not allowing the tilesets to be changed. +- Fix warning not appearing when the log file exceeds maximum size. +- Fix possible lag while using the Tileset Editor's tile selector. +- Fix unnecessary resources being used to watch files. +- Fix possible crash on Linux if too many inotify instances are requested. ## [6.1.0] - 2025-06-09 ### Added diff --git a/docsrc/manual/scripting-capabilities.rst b/docsrc/manual/scripting-capabilities.rst index 0651f7d9..e18bd8e3 100644 --- a/docsrc/manual/scripting-capabilities.rst +++ b/docsrc/manual/scripting-capabilities.rst @@ -11,6 +11,9 @@ Porymap is extensible via scripting capabilities. This allows the user to write - Procedurally Generated Maps - Randomize Grass Patterns +.. note:: + If you are compiling Porymap yourself, these features will only be available if Qt's ``qml`` module is installed. + Custom Scripts Editor --------------------- @@ -1222,6 +1225,58 @@ All tileset functions are callable via the global ``map`` object. :returns: the pixel data :rtype: array +.. |describe-metatile-layer-order| + replace:: where ``0`` is the bottom layer, ``1`` is the middle layer, and ``2`` is the top layer. The default order is ``[0, 1, 2]`` + +.. |describe-metatile-layer-order-handling| + replace:: If no elements are provided the layer order will be reset to the default. Any layer not listed in the provided ``order`` will not be rendered. Any additional elements after the first 3 are ignored + +.. js:function:: map.getMetatileLayerOrder() + + Gets the order that metatile layers are rendered for the current layout, |describe-metatile-layer-order|. + + If you'd like to get the default metatile layer order for all layouts, see :js:func:`utility.getMetatileLayerOrder` instead. + + :returns: array of layers + :rtype: array + +.. js:function:: map.setMetatileLayerOrder(order) + + Sets the order that metatile layers are rendered for the current layout, |describe-metatile-layer-order|. + + |describe-metatile-layer-order-handling|. + + If you'd like to set the default metatile layer order for all layouts, see :js:func:`utility.setMetatileLayerOrder` instead. + + :param order: array of layers + :type order: array + +.. |describe-metatile-layer-opacity| + replace:: where the first element is the bottom layer, the second element is the middle layer, and the third element is the top layer. The default opacities are ``[1.0, 1.0, 1.0]`` + +.. |describe-metatile-layer-opacity-handling| + replace:: Any additional elements after the first 3 are ignored. Any elements not provided will be rendered with opacity ``1.0`` + +.. js:function:: map.getMetatileLayerOpacity() + + Gets the opacities that metatile layers are rendered with for the current layout, |describe-metatile-layer-opacity|. + + If you'd like to get the default metatile layer opacities for all layouts, see :js:func:`utility.getMetatileLayerOpacity` instead. + + :returns: array of opacities for each layer + :rtype: array + +.. js:function:: map.setMetatileLayerOpacity(opacities) + + Sets the opacities that metatile layers are rendered with for the current layout, |describe-metatile-layer-opacity|. + + |describe-metatile-layer-opacity-handling|. + + If you'd like to set the default metatile layer opacities for all layouts, see :js:func:`utility.setMetatileLayerOpacity` instead. + + :param opacities: array of opacities for each layer + :type opacities: array + Overlay Functions ^^^^^^^^^^^^^^^^^ @@ -1811,30 +1866,42 @@ All settings functions are callable via the global ``utility`` object. .. js:function:: utility.getMetatileLayerOrder() - Gets the order that metatile layers are rendered. + Gets the order that metatile layers are rendered by default, |describe-metatile-layer-order|. - :returns: array of layers. The bottom layer is represented as 0. + If you'd like to get the metatile layer order for only the current layout, see :js:func:`map.getMetatileLayerOrder` instead. + + :returns: array of layers :rtype: array .. js:function:: utility.setMetatileLayerOrder(order) - Sets the order that metatile layers are rendered. + Sets the order that metatile layers are rendered by default, |describe-metatile-layer-order|. - :param order: array of layers. The bottom layer is represented as 0. + |describe-metatile-layer-order-handling|. + + If you'd like to set the metatile layer order for only the current layout, see :js:func:`map.setMetatileLayerOrder` instead. + + :param order: array of layers :type order: array .. js:function:: utility.getMetatileLayerOpacity() - Gets the opacities that metatile layers are rendered with. + Gets the opacities that metatile layers are rendered with by default, |describe-metatile-layer-opacity|. - :returns: array of opacities for each layer. The bottom layer is the first element. + If you'd like to get the metatile layer opacities for only the current layout, see :js:func:`map.getMetatileLayerOpacity` instead. + + :returns: array of opacities for each layer :rtype: array .. js:function:: utility.setMetatileLayerOpacity(opacities) - Sets the opacities that metatile layers are rendered with. + Sets the opacities that metatile layers are rendered with by default, |describe-metatile-layer-opacity|. - :param opacities: array of opacities for each layer. The bottom layer is the first element. + |describe-metatile-layer-opacity-handling|. + + If you'd like to set the metatile layer opacities for only the current layout, see :js:func:`map.setMetatileLayerOpacity` instead. + + :param opacities: array of opacities for each layer :type opacities: array diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 996dff3b..96567305 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1638,9 +1638,6 @@ - - There are no events on the current map. - Qt::AlignmentFlag::AlignCenter @@ -2926,6 +2923,7 @@ Help + @@ -3267,6 +3265,11 @@ Alt+Right + + + Open Manual + + diff --git a/forms/metatileimageexporter.ui b/forms/metatileimageexporter.ui new file mode 100644 index 00000000..7c6bfe49 --- /dev/null +++ b/forms/metatileimageexporter.ui @@ -0,0 +1,526 @@ + + + MetatileImageExporter + + + + 0 + 0 + 649 + 601 + + + + Export Metatiles Image + + + true + + + + + + + 0 + 0 + + + + Qt::FocusPolicy::ClickFocus + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::Shape::NoFrame + + + true + + + + + 0 + 0 + 304 + 532 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Tilesets + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full secondary tileset.</p></body></html> + + + Secondary Tileset + + + + + + + false + + + false + + + + + + + false + + + false + + + + + + + <html><head/><body><p>If checked, automatically update the metatile range to include the full primary tileset.</p></body></html> + + + Primary Tileset + + + true + + + + + + + Qt::Orientation::Horizontal + + + + 1 + 20 + + + + + + + + + + + false + + + Metatile Range + + + + 6 + + + 6 + + + + + Start + + + + + + + <html><head/><body><p>The metatile ID to start the rendered image at.</p></body></html> + + + + + + + End + + + + + + + <html><head/><body><p>The metatile ID to end the rendered image at.</p></body></html> + + + + + + + + + + <html><head/><body><p>Each metatile consists of 3 layers of tiles. These layers can be toggled here by clicking the checkbox, or rearranged by clicking and dragging them up or down in the list.</p></body></html> + + + Layers + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow + + + true + + + QAbstractItemView::DragDropMode::InternalMove + + + Qt::DropAction::MoveAction + + + QListView::ResizeMode::Adjust + + + Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + Transparency + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered with alpha of 0.</p></body></html> + + + Normal + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered as black. This is the default in-game behavior.</p></body></html> + + + Black + + + + + + + <html><head/><body><p>If checked, transparent pixels in the image will be rendered using the first color in tileset palette 0. This is the default behavior of the GBA.</p></body></html> + + + First palette color + + + + + + + + + + Miscellaneous + + + + 6 + + + 6 + + + + + <html><head/><body><p>If checked, display the placeholder tiles that are rendered for the unused layer in-game. For a given metatile only 2 of the 3 tile layers are used, and the 3rd layer is filled with these placeholder tiles. The unused layer and placeholder tile change depending on the metatile's layer type.</p></body></html> + + + Render placeholder metatiles + + + + + + + Width (metatiles) + + + + + + + <html><head/><body><p>Width of the output image in metatiles.</p></body></html> + + + + + + + Width (pixels) + + + + + + + <html><head/><body><p>Width of the output image in pixels. Automatically rounded up to a multiple of a metatile's pixel width.</p></body></html> + + + 16 + + + 128 + + + 16 + + + 128 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + + + + Reset + + + false + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + false + + + + + + + Save + + + false + + + + + + + + + + + + Preview + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + + + true + + + Qt::AlignmentFlag::AlignCenter + + + + + 0 + 0 + 285 + 551 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + false + + + false + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored + + + QGraphicsView::DragMode::NoDrag + + + + + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + UIntSpinBox + QAbstractSpinBox +
uintspinbox.h
+
+ + UIntHexSpinBox + UIntSpinBox +
uintspinbox.h
+
+ + ReorderableListWidget + QListWidget +
metatileimageexporter.h
+
+
+ + +
diff --git a/forms/palettecolorsearch.ui b/forms/palettecolorsearch.ui new file mode 100644 index 00000000..b6ae87c8 --- /dev/null +++ b/forms/palettecolorsearch.ui @@ -0,0 +1,152 @@ + + + PaletteColorSearch + + + + 0 + 0 + 547 + 329 + + + + Palette Color Search + + + + + + + + + Qt::AlignmentFlag::AlignCenter + + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 32 + 32 + + + + + 32 + 32 + + + + QFrame::Shadow::Raised + + + + + + + + 0 + 0 + + + + Color + + + + + + + 1 + + + + + + + + 0 + 0 + + + + Palette + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + 2 + + + + + + + + + QDialogButtonBox::StandardButton::Close + + + + + + + + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + +
diff --git a/forms/paletteeditor.ui b/forms/paletteeditor.ui index c98d49b6..76374514 100644 --- a/forms/paletteeditor.ui +++ b/forms/paletteeditor.ui @@ -38,6 +38,27 @@
+ + + + <html><head/><body><p>Opens a search dialog to find which tilesets/metatiles are using certain colors.</p></body></html> + + + ... + + + + :/icons/magnifier.ico:/icons/magnifier.ico + + + + + + + (All colors used) + + + @@ -89,7 +110,7 @@ 0 0 883 - 784 + 779 @@ -133,11 +154,28 @@ + + + View + + + + + + Tools + + + + + + + false + Undo @@ -152,6 +190,9 @@ + + false + Redo @@ -164,7 +205,22 @@ Import Palette + + + true + + + Show Unused Colors + + + + + Find Color Usage... + + - + + + diff --git a/forms/preferenceeditor.ui b/forms/preferenceeditor.ui index d3ec7cea..63017212 100644 --- a/forms/preferenceeditor.ui +++ b/forms/preferenceeditor.ui @@ -104,7 +104,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -159,7 +159,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -209,7 +209,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -253,13 +253,42 @@ - - - <html><head/><body><p>If checked, the list of suggestions when typing in an Event's Script field will include all global script labels in the project. Enabling this setting will make Porymap's startup slower.</p></body></html> - - - Autocomplete Script labels using all possible scripts + + + Script label autocomplete + + + + + <html><head/><body><p>If checked, the list of suggestions when typing in an Event's Script field will include all global script labels in the project. This is the slowest option for Porymap's project opening.</p></body></html> + + + All possible scripts + + + + + + + <html><head/><body><p>If checked, the list of suggestions when typing in an Event's Script field will include script labels from the current map's scripts file, scripts in-use by the map's other events, and all script files in the <span style=" font-family:'SFMono-Regular','Menlo','Monaco','Consolas','Liberation Mono','Courier New','Courier','monospace'; font-size:11px; color:#e74c3c; background-color:#ffffff;">data_scripts_folders </span>folder.</p></body></html> + + + Current map, and global script files + + + + + + + <html><head/><body><p>If checked, the list of suggestions when typing in an Event's Script field will only include script labels from the current map's scripts file and scripts in-use by the map's other events. This is the fastest option for Porymap's project opening.</p></body></html> + + + Current map only + + + + @@ -294,7 +323,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -323,7 +352,7 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame true @@ -333,13 +362,13 @@ 0 0 - 476 - 343 + 495 + 376 - QLayout::SetMinimumSize + QLayout::SizeConstraint::SetMinimumSize @@ -347,7 +376,7 @@ <html><head/><body><p>When this command is set a button will appear next to the <span style=" font-weight:600; font-style:italic;">Script</span> combo-box in the <span style=" font-weight:600; font-style:italic;">Events</span> tab which executes this command.<span style=" font-weight:600;"> %F</span> will be substituted with the file path of the script and <span style=" font-weight:600;">%L</span> will be substituted with the line number of the script in that file. <span style=" font-weight:600;">%F </span><span style=" font-style:italic;">must</span> be given if <span style=" font-weight:600;">%L</span> is given. If <span style=" font-weight:600;">%F</span> is <span style=" font-style:italic;">not</span> given then the script's file path will be added to the end of the command. If the script can't be found then the current map's scripts file is opened.</p></body></html> - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop true @@ -380,7 +409,7 @@ <html><head/><body><p>This is the command that is executed when clicking <span style=" font-weight:600; font-style:italic;">Open Project in Text Editor</span> in the <span style=" font-weight:600; font-style:italic;">Tools</span> menu. <span style=" font-weight:600;">%D</span> will be substituted with the project's root directory. If <span style=" font-weight:600;">%D</span> is <span style=" font-style:italic;">not</span> specified then the project directory will be added to the end of the command.</p></body></html> - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop true @@ -410,10 +439,10 @@ - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -426,7 +455,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -447,7 +476,7 @@ - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index f732f335..f2284c07 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -852,7 +852,7 @@ 0 0 570 - 798 + 837 @@ -973,6 +973,27 @@ + + + + + + Metatile Selector Width + + + + + + + <html><head/><body><p>The width (in metatiles) of the metatile selectors on the Map tab and in the Tileset Editor.</p></body></html> + + + 1 + + + + + @@ -1873,16 +1894,6 @@ QComboBox
noscrollcombobox.h
- - NoScrollSpinBox - QSpinBox -
noscrollspinbox.h
-
- - NoScrollTextEdit - QTextEdit -
noscrolltextedit.h
-
UIntSpinBox QAbstractSpinBox @@ -1893,6 +1904,16 @@ UIntSpinBox
uintspinbox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+ + NoScrollTextEdit + QTextEdit +
noscrolltextedit.h
+
diff --git a/forms/tileseteditor.ui b/forms/tileseteditor.ui index d1817859..eb2580b7 100644 --- a/forms/tileseteditor.ui +++ b/forms/tileseteditor.ui @@ -6,7 +6,7 @@ 0 0 - 733 + 748 784
@@ -65,7 +65,7 @@ 0 0 - 239 + 241 659
@@ -158,7 +158,7 @@ 0 - 166 + 190 @@ -194,177 +194,291 @@ false - - - - - true - - - - - + + + - + 0 0 - - - 185 - 0 - - + + + 0 + + + 0 + + + 0 + + + 2 + + + + + Raw Attributes Value + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + - - - - Metatile Label (Optional) - - - - - - - Bottom/Top - - - - - - - Terrain Type - - - - - - - Layer Type - - - - - + + - + 0 0 - - QComboBox::InsertPolicy::NoInsert - + + + 0 + + + 0 + + + 0 + + + + + Layer Type + + + + + + + + 0 + 0 + + + + + 185 + 0 + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + - - + + - + 0 0 - - - 66 - 34 - - - - - 96 - 34 - - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - - - Qt::ScrollBarPolicy::ScrollBarAlwaysOff - + + + 0 + + + 0 + + + 0 + + + 2 + + + + + <html><head/><body><p>Copies the full metatile label to the clipboard.</p></body></html> + + + ... + + + + :/icons/clipboard.ico:/icons/clipboard.ico + + + + + + + true + + + + + + + Metatile Label (Optional) + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + - - + + - + 0 0 - - QComboBox::InsertPolicy::NoInsert - + + + 0 + + + 0 + + + 0 + + + 2 + + + + + Terrain Type + + + + + + + + 0 + 0 + + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + - - - - Qt::Orientation::Horizontal - - - QSizePolicy::Policy::Maximum - - - - 10 - 20 - - - - - - + + - + 0 0 - - QComboBox::InsertPolicy::NoInsert - + + + 0 + + + 0 + + + 0 + + + 2 + + + + + Encounter Type + + + + + + + + 0 + 0 + + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + - - - - Metatile Behavior - - - - - - - Raw Attributes Value - - - - - - - Encounter Type - - - - - - - <html><head/><body><p>Copies the full metatile label to the clipboard.</p></body></html> - - - ... - - - - :/icons/clipboard.ico:/icons/clipboard.ico - - - - + Qt::Orientation::Vertical @@ -377,8 +491,119 @@ - - + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 2 + + + + + Metatile Behavior + + + + + + + + 0 + 0 + + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + Bottom/Top + + + + + + + + 0 + 0 + + + + + 1 + 1 + + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + @@ -457,12 +682,6 @@ - - - 0 - 0 - - 98 @@ -503,16 +722,22 @@ + + + 0 + 0 + + - 18 - 18 + 98 + 98 98 - 34 + 98 @@ -526,22 +751,22 @@ + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + +
- - - - Qt::Orientation::Vertical - - - - 20 - 10 - - - -
@@ -561,8 +786,8 @@ 0 0 - 445 - 237 + 458 + 203 @@ -623,7 +848,7 @@ 0 0 - 733 + 748 37 @@ -631,18 +856,42 @@ File + + + Export Tiles Image + + + + + + + Import Metatiles from Advance Map 1.92 + + + + + + + Import Tiles Image + + + + + + + Export Porytiles Layer Images + + + + - - + + - - - - - - - + + + @@ -663,11 +912,20 @@ + View + + + Layer Arrangement + + + + + @@ -691,16 +949,6 @@ Ctrl+S - - - Import Primary Tiles Image... - - - - - Import Secondary Tiles Image... - - Change Number of Metatiles... @@ -736,6 +984,9 @@ + + false + Undo @@ -744,6 +995,9 @@ + + false + Redo @@ -751,36 +1005,6 @@ Ctrl+Y - - - Export Primary Tiles Image... - - - - - Export Secondary Tiles Image... - - - - - Import Primary Metatiles from Advance Map 1.92... - - - - - Import Secondary Metatiles from Advance Map 1.92... - - - - - Export Primary Metatiles Image... - - - - - Export Secondary Metatiles Image... - - Cut @@ -798,6 +1022,9 @@ + + false + Paste @@ -840,6 +1067,78 @@ Show Raw Metatile Attributes + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Primary... + + + + + Secondary... + + + + + Export Metatiles Image... + + + + + Primary... + + + + + Secondary... + + + + + true + + + Horizontal + + + + + true + + + Vertical + + + + + true + + + Swap Metatiles + + + X + + diff --git a/include/config.h b/include/config.h index 171e3850..1f2a1a14 100644 --- a/include/config.h +++ b/include/config.h @@ -27,6 +27,12 @@ extern const QVersionNumber porymapVersion; #define CONFIG_BACKWARDS_COMPATABILITY +enum ScriptAutocompleteMode { + MapOnly, + MapAndCommon, + All, +}; + class KeyValueConfigBase { public: @@ -54,7 +60,8 @@ protected: static bool getConfigBool(const QString &key, const QString &value); static int getConfigInteger(const QString &key, const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); static uint32_t getConfigUint32(const QString &key, const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); - static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = Qt::black); + static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = QColor(Qt::black)); + static QString toConfigColor(const QColor &color); QString m_root; QString m_filename; @@ -65,57 +72,7 @@ class PorymapConfig: public KeyValueConfigBase { public: PorymapConfig(); - virtual void reset() override { - setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); - this->recentProjects.clear(); - this->projectManuallyClosed = false; - this->reopenOnLaunch = true; - this->mapListTab = 0; - this->mapListEditGroupsEnabled = false; - this->mapListHideEmptyEnabled.clear(); - 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; - this->tilesetEditorMetatilesZoom = 30; - this->tilesetEditorTilesZoom = 30; - this->showPlayerView = false; - this->showCursorTile = true; - this->showBorder = true; - this->showGrid = false; - this->showTilesetEditorMetatileGrid = false; - this->showTilesetEditorLayerGrid = true; - this->showTilesetEditorDivider = false; - this->showTilesetEditorRawAttributes = false; - this->monitorFiles = true; - this->tilesetCheckerboardFill = true; - this->newMapHeaderSectionExpanded = false; - this->theme = "default"; - this->wildMonChartTheme = ""; - this->textEditorOpenFolder = ""; - this->textEditorGotoLine = ""; - this->paletteEditorBitDepth = 24; - this->projectSettingsTab = 0; - this->loadAllEventScripts = false; - this->warpBehaviorWarningDisabled = false; - this->eventDeleteWarningDisabled = false; - this->eventOverlayEnabled = false; - this->checkForUpdates = true; - this->lastUpdateCheckTime = QDateTime(); - this->lastUpdateCheckVersion = porymapVersion; - this->rateLimitTimes.clear(); - this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; - this->shownInGameReloadMessage = false; - this->gridSettings = GridSettings(); - this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; - this->applicationFont = QFont(); - this->mapListFont = PorymapConfig::defaultMapListFont(); - } + virtual void reset() override; void addRecentProject(QString project); void setRecentProjects(QStringList projects); QString getRecentProject(); @@ -151,6 +108,7 @@ public: int metatilesZoom; int tilesetEditorMetatilesZoom; int tilesetEditorTilesZoom; + Qt::Orientation tilesetEditorLayerOrientation; bool showPlayerView; bool showCursorTile; bool showBorder; @@ -159,6 +117,7 @@ public: bool showTilesetEditorLayerGrid; bool showTilesetEditorDivider; bool showTilesetEditorRawAttributes; + bool showPaletteEditorUnusedColors; bool monitorFiles; bool tilesetCheckerboardFill; bool newMapHeaderSectionExpanded; @@ -168,7 +127,7 @@ public: QString textEditorGotoLine; int paletteEditorBitDepth; int projectSettingsTab; - bool loadAllEventScripts; + ScriptAutocompleteMode scriptAutocompleteMode; bool warpBehaviorWarningDisabled; bool eventDeleteWarningDisabled; bool eventOverlayEnabled; @@ -351,7 +310,7 @@ public: this->prefabImportPrompted = false; this->tilesetsHaveCallback = true; this->tilesetsHaveIsCompressed = true; - this->setTransparentPixelsBlack = true; + this->transparencyColor = QColor(Qt::black); this->preserveMatchingOnlyData = false; this->filePaths.clear(); this->eventIconPaths.clear(); @@ -368,6 +327,7 @@ public: this->unusedTileSplit = 0x0000; this->maxEventsPerGroup = 255; this->forcedMajorVersion = 0; + this->metatileSelectorWidth = 8; this->globalConstantsFilepaths.clear(); this->globalConstants.clear(); this->identifiers.clear(); @@ -426,7 +386,7 @@ public: bool prefabImportPrompted; bool tilesetsHaveCallback; bool tilesetsHaveIsCompressed; - bool setTransparentPixelsBlack; + QColor transparencyColor; bool preserveMatchingOnlyData; int metatileAttributesSize; uint32_t metatileBehaviorMask; @@ -447,6 +407,7 @@ public: QList warpBehaviors; int maxEventsPerGroup; int forcedMajorVersion; + int metatileSelectorWidth; QStringList globalConstantsFilepaths; QMap globalConstants; diff --git a/include/core/events.h b/include/core/events.h index 76a58dcf..816cf953 100644 --- a/include/core/events.h +++ b/include/core/events.h @@ -122,8 +122,8 @@ public: int getZ() const { return this->elevation; } int getElevation() const { return this->elevation; } - int getPixelX() const { return (this->x * 16) - qMax(0, (pixmap.width() - 16) / 2); } - int getPixelY() const { return (this->y * 16) - qMax(0, pixmap.height() - 16); } + int getPixelX() const; + int getPixelY() const; virtual EventFrame *getEventFrame(); virtual EventFrame *createEventFrame() = 0; @@ -161,6 +161,7 @@ public: QString getIdName() const { return this->idName; } static QString groupToString(Event::Group group); + static QString groupToJsonKey(Event::Group group); static QString typeToString(Event::Type type); static QString typeToJsonKey(Event::Type type); static Event::Type typeFromJsonKey(QString type); diff --git a/include/core/history.h b/include/core/history.h index c999a4e1..ced8a590 100644 --- a/include/core/history.h +++ b/include/core/history.h @@ -25,6 +25,7 @@ public: if (head > 0) { return history.at(--head); } + head = -1; return NULL; } @@ -37,9 +38,7 @@ public: void push(T commit) { while (head + 1 < history.length()) { - T item = history.last(); - history.removeLast(); - delete item; + delete history.takeLast(); } if (saved > head) { saved = -1; @@ -48,7 +47,7 @@ public: head++; } - T current() { + T current() const { if (head < 0 || history.length() == 0) { return NULL; } @@ -59,10 +58,30 @@ public: saved = head; } - bool isSaved() { + bool isSaved() const { return saved == head; } + int length() const { + return history.length(); + } + + bool isEmpty() const { + return history.isEmpty(); + } + + int index() const { + return head; + } + + bool canUndo() const { + return head >= 0; + } + + bool canRedo() const { + return (head + 1) < history.length(); + } + private: QList history; int head = -1; diff --git a/include/core/map.h b/include/core/map.h index d5e28075..f3a377c1 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -51,10 +51,12 @@ public: void setLayoutId(const QString &layoutId) { m_layoutId = layoutId; } QString layoutId() const { return layout() ? layout()->id : m_layoutId; } - int getWidth() const; - int getHeight() const; - int getBorderWidth() const; - int getBorderHeight() const; + int getWidth() const { return m_layout ? m_layout->getWidth() : 0; } + int getHeight() const { return m_layout ? m_layout->getHeight() : 0; } + int getBorderWidth() const { return m_layout ? m_layout->getBorderWidth() : 0; } + int getBorderHeight() const { return m_layout ? m_layout->getBorderHeight() : 0; } + int pixelWidth() const { return m_layout ? m_layout->pixelWidth() : 0; } + int pixelHeight() const { return m_layout ? m_layout->pixelHeight() : 0; } void setHeader(const MapHeader &header) { *m_header = header; } MapHeader* header() const { return m_header; } @@ -62,6 +64,8 @@ public: void setSharedEventsMap(const QString &sharedEventsMap) { m_sharedEventsMap = sharedEventsMap; } void setSharedScriptsMap(const QString &sharedScriptsMap); + bool isInheritingEvents() const { return !m_sharedEventsMap.isEmpty() && !hasEvents(); } + bool isInheritingScripts() const { return !m_sharedScriptsMap.isEmpty(); } QString sharedEventsMap() const { return m_sharedEventsMap; } QString sharedScriptsMap() const { return m_sharedScriptsMap; } @@ -83,6 +87,7 @@ public: void addEvent(Event *); int getIndexOfEvent(Event *) const; bool hasEvent(Event *) const; + bool hasEvents() const; QStringList getScriptLabels(Event::Group group = Event::Group::None); QString getScriptsFilepath() const; @@ -133,9 +138,6 @@ private: QMap> m_events; QSet m_ownedEvents; // for memory management - QList m_metatileLayerOrder; - QList m_metatileLayerOpacity; - void trackConnection(MapConnection*); // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. @@ -149,7 +151,7 @@ signals: void modified(); void scriptsModified(); void mapDimensionsChanged(const QSize &size); - void openScriptRequested(QString label); + void openScriptRequested(const QString &label); void connectionAdded(MapConnection*); void connectionRemoved(MapConnection*); void layoutChanged(); diff --git a/include/core/mapconnection.h b/include/core/mapconnection.h index 5df071bd..ba7c95a8 100644 --- a/include/core/mapconnection.h +++ b/include/core/mapconnection.h @@ -42,7 +42,7 @@ public: MapConnection* createMirror(); QPixmap render() const; - QPoint relativePos(bool clipped = false) const; + QPoint relativePixelPos(bool clipped = false) const; static QPointer project; static const QMap oppositeDirections; diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 5a2e23b3..92f0264e 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -17,7 +17,7 @@ class BorderMetatilesPixmapItem; class Layout : public QObject { Q_OBJECT public: - Layout() {} + Layout() {}; Layout(const Layout &other); static QString layoutConstantFromName(const QString &name); @@ -32,6 +32,9 @@ public: int height; int border_width; int border_height; + int pixelWidth() const { return this->width * Metatile::pixelWidth(); } + int pixelHeight() const { return this->height * Metatile::pixelHeight(); } + QSize pixelSize() const { return QSize(pixelWidth(), pixelHeight()); } QString border_path; QString blockdata_path; @@ -64,8 +67,25 @@ public: QSize borderDimensions; } lastCommitBlocks; // to track map changes - QList metatileLayerOrder; - QList metatileLayerOpacity; + void setMetatileLayerOrder(const QList &layerOrder) { m_metatileLayerOrder = layerOrder; } + const QList &metatileLayerOrder() const { + return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::globalMetatileLayerOrder(); + } + static void setGlobalMetatileLayerOrder(const QList &layerOrder) { s_globalMetatileLayerOrder = layerOrder; } + static const QList &globalMetatileLayerOrder() { + static const QList defaultLayerOrder = {0, 1, 2}; + return !s_globalMetatileLayerOrder.isEmpty() ? s_globalMetatileLayerOrder : defaultLayerOrder; + } + + void setMetatileLayerOpacity(const QList &layerOpacity) { m_metatileLayerOpacity = layerOpacity; } + const QList &metatileLayerOpacity() const { + return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::globalMetatileLayerOpacity(); + } + static void setGlobalMetatileLayerOpacity(const QList &layerOpacity) { s_globalMetatileLayerOpacity = layerOpacity; } + static const QList &globalMetatileLayerOpacity() { + static const QList defaultLayerOpacity = {1.0, 1.0, 1.0}; + return !s_globalMetatileLayerOpacity.isEmpty() ? s_globalMetatileLayerOpacity : defaultLayerOpacity; + } LayoutPixmapItem *layoutItem = nullptr; CollisionPixmapItem *collisionItem = nullptr; @@ -100,13 +120,17 @@ public: QRect getVisibleRect() const; bool isWithinBounds(int x, int y) const; + bool isWithinBounds(const QPoint &pos) const; bool isWithinBounds(const QRect &rect) const; bool isWithinBorderBounds(int x, int y) const; - bool getBlock(int x, int y, Block *out); + bool getBlock(int x, int y, Block *out) const; void setBlock(int x, int y, Block block, bool enableScriptCallback = false); void setBlockdata(Blockdata blockdata, bool enableScriptCallback = false); + uint16_t getMetatileId(int x, int y) const; + bool setMetatileId(int x, int y, uint16_t metatileId, bool enableScriptCallback = false); + void adjustDimensions(const QMargins &margins, bool setNewBlockdata = true); void setDimensions(int newWidth, int newHeight, bool setNewBlockdata = true); void setBorderDimensions(int newWidth, int newHeight, bool setNewBlockdata = true, bool enableScriptCallback = false); @@ -135,9 +159,8 @@ public: void _floodFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); void magicFillCollisionElevation(int x, int y, uint16_t collision, uint16_t elevation); - QPixmap render(bool ignoreCache = false, Layout *fromLayout = nullptr, QRect bounds = QRect(0, 0, -1, -1)); + QPixmap render(bool ignoreCache = false, Layout *fromLayout = nullptr, const QRect &bounds = QRect(0, 0, -1, -1)); QPixmap renderCollision(bool ignoreCache); - // QPixmap renderConnection(MapConnection, Layout *); QPixmap renderBorder(bool ignoreCache = false); QPixmap getLayoutItemPixmap(); @@ -146,6 +169,8 @@ public: void setCollisionItem(CollisionPixmapItem *item) { collisionItem = item; } void setBorderItem(BorderMetatilesPixmapItem *item) { borderItem = item; } + bool metatileIsValid(uint16_t metatileId) { return Tileset::metatileIsValid(metatileId, this->tileset_primary, this->tileset_secondary); } + private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); @@ -154,6 +179,11 @@ private: static int getBorderDrawDistance(int dimension, qreal minimum); + QList m_metatileLayerOrder; + QList m_metatileLayerOpacity; + static QList s_globalMetatileLayerOrder; + static QList s_globalMetatileLayerOpacity; + signals: void dimensionsChanged(const QSize &size); void needsRedrawing(); diff --git a/include/core/metatile.h b/include/core/metatile.h index a1c805d1..eba4afed 100644 --- a/include/core/metatile.h +++ b/include/core/metatile.h @@ -61,7 +61,15 @@ public: static int getDefaultAttributesSize(BaseGameVersion version); static void setLayout(Project*); static QString getMetatileIdString(uint16_t metatileId); - static QString getMetatileIdStrings(const QList metatileIds); + static QString getMetatileIdStrings(const QList &metatileIds); + static QString getLayerName(int layerNum); + + static constexpr int tileWidth() { return 2; } + static constexpr int tileHeight() { return 2; } + static constexpr int tilesPerLayer() { return Metatile::tileWidth() * Metatile::tileHeight(); } + static constexpr int pixelWidth() { return Metatile::tileWidth() * Tile::pixelWidth(); } + static constexpr int pixelHeight() { return Metatile::tileHeight() * Tile::pixelHeight(); } + static constexpr QSize pixelSize() { return QSize(pixelWidth(), pixelHeight()); } inline bool operator==(const Metatile &other) { return this->tiles == other.tiles && this->attributes == other.attributes; diff --git a/include/core/network.h b/include/core/network.h index 321b3e99..23dc7eff 100644 --- a/include/core/network.h +++ b/include/core/network.h @@ -26,10 +26,14 @@ }); */ +#if __has_include() #include #include #include #include +#endif + +#ifdef QT_NETWORK_LIB class NetworkReplyData : public QObject { @@ -84,4 +88,6 @@ private: const QNetworkRequest getRequest(const QUrl &url); }; +#endif // QT_NETWORK_LIB + #endif // NETWORK_H diff --git a/include/core/tile.h b/include/core/tile.h index 1ae7e23d..11fd6a99 100644 --- a/include/core/tile.h +++ b/include/core/tile.h @@ -3,6 +3,7 @@ #define TILE_H #include +#include class Tile { @@ -24,6 +25,12 @@ public: static int getIndexInTileset(int); static const uint16_t maxValue; + + static constexpr int pixelWidth() { return 8; } + static constexpr int pixelHeight() { return 8; } + static constexpr QSize pixelSize() { return QSize(Tile::pixelWidth(), Tile::pixelHeight()); } + static constexpr int numPixels() { return Tile::pixelWidth() * Tile::pixelHeight(); } + static constexpr int sizeInBytes() { return sizeof(uint16_t); } }; inline bool operator==(const Tile &a, const Tile &b) { diff --git a/include/core/tileset.h b/include/core/tileset.h index abb31ab1..dcf55ec1 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -30,17 +30,21 @@ public: QString metatile_attrs_label; QString metatile_attrs_path; QString tilesImagePath; - QImage tilesImage; QStringList palettePaths; - QList tiles; QHash metatileLabels; QList> palettes; QList> palettePreviews; + static QString stripPrefix(const QString &fullName); + static Tileset* getPaletteTileset(int, Tileset*, Tileset*); + static const Tileset* getPaletteTileset(int, const Tileset*, const Tileset*); static Tileset* getMetatileTileset(int, Tileset*, Tileset*); + static const Tileset* getMetatileTileset(int, const Tileset*, const Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); + static const Tileset* getTileTileset(int, const Tileset*, const Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); + static const Metatile* getMetatile(int, const Tileset*, const Tileset*); static Tileset* getMetatileLabelTileset(int, Tileset*, Tileset*); static QString getMetatileLabel(int, Tileset *, Tileset *); static QString getOwnedMetatileLabel(int, Tileset *, Tileset *); @@ -48,9 +52,9 @@ public: static bool setMetatileLabel(int, QString, Tileset *, Tileset *); QString getMetatileLabelPrefix(); static QString getMetatileLabelPrefix(const QString &name); - static QList> getBlockPalettes(Tileset*, Tileset*, bool useTruePalettes = false); - static QList getPalette(int, Tileset*, Tileset*, bool useTruePalettes = false); - static bool metatileIsValid(uint16_t metatileId, Tileset *, Tileset *); + static QList> getBlockPalettes(const Tileset*, const Tileset*, bool useTruePalettes = false); + static QList getPalette(int, const Tileset*, const Tileset*, bool useTruePalettes = false); + static bool metatileIsValid(uint16_t metatileId, const Tileset*, const Tileset*); static QHash getHeaderMemberMap(bool usingAsm); static QString getExpectedDir(QString tilesetName, bool isSecondary); QString getExpectedDir(); @@ -76,15 +80,38 @@ public: void setMetatiles(const QList &metatiles); void addMetatile(Metatile* metatile); - QList metatiles() const { return m_metatiles; } - Metatile* metatileAt(unsigned int i) const { return m_metatiles.at(i); } + const QList &metatiles() const { return m_metatiles; } + const Metatile* metatileAt(unsigned int i) const { return m_metatiles.at(i); } void clearMetatiles(); void resizeMetatiles(int newNumMetatiles); int numMetatiles() const { return m_metatiles.length(); } + int maxMetatiles() const; + + uint16_t firstMetatileId() const; + uint16_t lastMetatileId() const; + bool containsMetatileId(uint16_t metatileId) const { return metatileId >= firstMetatileId() && metatileId <= lastMetatileId(); } + + uint16_t firstTileId() const; + uint16_t lastTileId() const; + bool containsTileId(uint16_t tileId) const { return tileId >= firstTileId() && tileId <= lastTileId(); } + + int numTiles() const { return m_tiles.length(); } + int maxTiles() const; + + QImage tileImage(uint16_t tileId) const { return m_tiles.value(Tile::getIndexInTileset(tileId)); } + + QSet getUnusedColorIds(int paletteId, const Tileset *pairedTileset, const QSet &searchColors = {}) const; + QList findMetatilesUsingColor(int paletteId, int colorId, const Tileset *pairedTileset) const; + + static constexpr int maxPalettes() { return 16; } + static constexpr int numColorsPerPalette() { return 16; } private: QList m_metatiles; + + QList m_tiles; + QImage m_tilesImage; bool m_hasUnsavedTilesImage = false; }; diff --git a/include/core/utility.h b/include/core/utility.h index f7f5da61..7270b552 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -7,7 +7,7 @@ namespace Util { void numericalModeSort(QStringList &list); - int roundUp(int numToRound, int multiple); + int roundUpToMultiple(int numToRound, int multiple); QString toDefineCase(QString input); QString toHexString(uint32_t value, int minLength = 0); QString toHtmlParagraph(const QString &text); @@ -16,6 +16,7 @@ namespace Util { QString replaceExtension(const QString &path, const QString &newExtension); void setErrorStylesheet(QLineEdit *lineEdit, bool isError); QString toStylesheetString(const QFont &font); + void show(QWidget *w); } #endif // UTILITY_H diff --git a/include/editor.h b/include/editor.h index a5601196..21368f61 100644 --- a/include/editor.h +++ b/include/editor.h @@ -67,8 +67,6 @@ public: bool setLayout(QString layoutName); void unsetMap(); - Tileset *getCurrentMapPrimaryTileset(); - bool displayMap(); bool displayLayout(); @@ -122,9 +120,10 @@ public: void updateEventPixmapItemZValue(EventPixmapItem *item); qreal getEventOpacity(const Event *event) const; + bool isMouseInMap() const; void setPlayerViewRect(const QRectF &rect); - void updateCursorRectPos(int x, int y); - void setCursorRectVisible(bool visible); + void setCursorRectPos(const QPoint &pos); + void updateCursorRectVisibility(); void onEventDragged(Event *event, const QPoint &oldPosition, const QPoint &newPosition); void onEventReleased(Event *event, const QPoint &position); @@ -171,7 +170,7 @@ public: void setEditMode(EditMode editMode); EditMode getEditMode() const { return this->editMode; } - bool getEditingLayout(); + bool getEditingLayout() const; void setMapEditingButtonsEnabled(bool enabled); @@ -208,7 +207,8 @@ public: public slots: void openMapScripts() const; - void openScript(const QString &scriptLabel) const; + bool openScript(const QString &scriptLabel) const; + bool openScriptInFile(const QString &scriptLabel, const QString &filepath) const; void openProjectInTextEditor() const; void maskNonVisibleConnectionTiles(); void onBorderMetatilesChanged(); @@ -251,26 +251,26 @@ private: QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); void setCollisionTabSpinBoxes(uint16_t collision, uint16_t elevation); + void adjustStraightPathPos(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item, QPoint *pos) const; static bool startDetachedProcess(const QString &command, const QString &workingDirectory = QString(), qint64 *pid = nullptr); - -private slots: + bool canPaintMetatiles() const; void onMapStartPaint(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item); void onMapEndPaint(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item); + void setStatusFromMapPos(const QPoint &pos); + +private slots: void setSmartPathCursorMode(QGraphicsSceneMouseEvent *event); - void setStraightPathCursorMode(QGraphicsSceneMouseEvent *event); void mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item); - void mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item); void setSelectedConnectionItem(ConnectionPixmapItem *connectionItem); void onHoveredMovementPermissionChanged(uint16_t, uint16_t); void onHoveredMovementPermissionCleared(); void onHoveredMetatileSelectionChanged(uint16_t); void onHoveredMetatileSelectionCleared(); - void onHoveredMapMetatileChanged(const QPoint &pos); - void onHoveredMapMetatileCleared(); - void onHoveredMapMovementPermissionChanged(int, int); - void onHoveredMapMovementPermissionCleared(); + void onMapHoverEntered(const QPoint &pos); + void onMapHoverChanged(const QPoint &pos); + void onMapHoverCleared(); void onSelectedMetatilesChanged(); void onWheelZoom(int); @@ -283,7 +283,6 @@ signals: void wildMonTableEdited(); void currentMetatilesSelectionChanged(); void mapRulerStatusChanged(const QString &); - void tilesetUpdated(QString); void gridToggled(bool); void editActionSet(EditAction newEditAction); }; diff --git a/include/mainwindow.h b/include/mainwindow.h index 3b3003aa..d15bde31 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -11,7 +11,6 @@ #include #include #include -#include #include "project.h" #include "orderedjson.h" #include "config.h" @@ -36,6 +35,10 @@ #include "message.h" #include "resizelayoutpopup.h" +#if __has_include() +#include +#endif + namespace Ui { @@ -56,9 +59,14 @@ public: void initialize(); + Q_INVOKABLE void setPrimaryTileset(const QString &tileset); + Q_INVOKABLE void setSecondaryTileset(const QString &tileset); + // Scripting API +#ifdef QT_QML_LIB Q_INVOKABLE QJSValue getBlock(int x, int y); void tryRedrawMapArea(bool forceRedraw); + void redrawResizedMapArea(); void tryCommitMapChanges(bool commitChanges); Q_INVOKABLE void setBlock(int x, int y, int metatileId, int collision, int elevation, bool forceRedraw = true, bool commitChanges = true); Q_INVOKABLE void setBlock(int x, int y, int rawValue, bool forceRedraw = true, bool commitChanges = true); @@ -118,8 +126,6 @@ public: Q_INVOKABLE int getNumSecondaryTilesetTiles(); Q_INVOKABLE QString getPrimaryTileset(); Q_INVOKABLE QString getSecondaryTileset(); - Q_INVOKABLE void setPrimaryTileset(QString tileset); - Q_INVOKABLE void setSecondaryTileset(QString tileset); void saveMetatilesByMetatileId(int metatileId); void saveMetatileAttributesByMetatileId(int metatileId); Metatile * getMetatile(int metatileId); @@ -145,6 +151,10 @@ public: Q_INVOKABLE void setMetatileTiles(int metatileId, QJSValue tilesObj, int tileStart = 0, int tileEnd = -1, bool forceRedraw = true); Q_INVOKABLE void setMetatileTiles(int metatileId, int tileId, bool xflip, bool yflip, int palette, int tileStart = 0, int tileEnd = -1, bool forceRedraw = true); Q_INVOKABLE QJSValue getTilePixels(int tileId); + Q_INVOKABLE QList getMetatileLayerOrder() const; + Q_INVOKABLE void setMetatileLayerOrder(const QList &order); + Q_INVOKABLE QList getMetatileLayerOpacity() const; + Q_INVOKABLE void setMetatileLayerOpacity(const QList &opacities); Q_INVOKABLE QString getSong(); Q_INVOKABLE void setSong(QString song); Q_INVOKABLE QString getLocation(); @@ -167,6 +177,7 @@ public: Q_INVOKABLE void setAllowEscaping(bool allow); Q_INVOKABLE int getFloorNumber(); Q_INVOKABLE void setFloorNumber(int floorNumber); +#endif // QT_QML_LIB public slots: void on_mainTabBar_tabBarClicked(int index); @@ -246,8 +257,6 @@ private slots: void on_pushButton_AddConnection_clicked(); void on_button_OpenDiveMap_clicked(); void on_button_OpenEmergeMap_clicked(); - void on_comboBox_PrimaryTileset_currentTextChanged(const QString &arg1); - void on_comboBox_SecondaryTileset_currentTextChanged(const QString &arg1); void on_pushButton_ChangeDimensions_clicked(); void resetMapViewScale(); @@ -286,6 +295,7 @@ private slots: void on_spinBox_SelectedCollision_valueChanged(int collision); void on_actionRegion_Map_Editor_triggered(); void on_actionPreferences_triggered(); + void on_actionOpen_Manual_triggered(); void on_actionCheck_for_Updates_triggered(); void togglePreferenceSpecificUi(); void on_actionProject_Settings_triggered(); @@ -321,8 +331,11 @@ private: QPointer layoutListProxyModel = nullptr; QPointer layoutTreeModel = nullptr; +#ifdef QT_NETWORK_LIB QPointer updatePromoter = nullptr; QPointer networkAccessManager = nullptr; +#endif + QPointer aboutWindow = nullptr; QPointer wildMonChart = nullptr; QPointer wildMonSearch = nullptr; @@ -380,7 +393,6 @@ private: void openDuplicateMapOrLayoutDialog(); void openNewMapGroupDialog(); void openNewLocationDialog(); - void openSubWindow(QWidget * window); void scrollMapList(MapTree *list, const QString &itemName); void scrollMapListToCurrentMap(MapTree *list); void scrollMapListToCurrentLayout(MapTree *list); diff --git a/include/project.h b/include/project.h index 5c519bc3..0ea25a4a 100644 --- a/include/project.h +++ b/include/project.h @@ -61,7 +61,6 @@ public: QMap metatileBehaviorMap; QMap metatileBehaviorMapInverse; ParseUtil parser; - QFileSystemWatcher fileWatcher; QSet modifiedFiles; bool usingAsmTilesets; QSet disabledSettingsNames; @@ -105,11 +104,12 @@ public: bool load(); QMap tilesetCache; - Tileset* loadTileset(QString, Tileset *tileset = nullptr); - Tileset* getTileset(QString, bool forceLoad = false); + Tileset* getTileset(const QString&, bool forceLoad = false); QStringList primaryTilesetLabels; QStringList secondaryTilesetLabels; QStringList tilesetLabelsOrdered; + QSet getPairedTilesetLabels(const Tileset *tileset) const; + QSet getTilesetLayoutIds(const Tileset *priamryTileset, const Tileset *secondaryTileset) const; bool readMapGroups(); void addNewMapGroup(const QString &groupName); @@ -216,7 +216,11 @@ public: static QString getScriptFileExtension(bool usePoryScript); QString getScriptDefaultString(bool usePoryScript, QString mapName) const; - QStringList getEventScriptsFilepaths() const; + + QStringList getAllEventScriptsFilepaths() const; + QStringList getMapScriptsFilepaths() const; + QStringList getCommonEventScriptsFilepaths() const; + QStringList findScriptsFiles(const QString &searchDir, const QStringList &fileNames = {"*"}) const; void insertGlobalScriptLabels(QStringList &scriptLabels) const; QString getDefaultPrimaryTilesetLabel() const; @@ -252,17 +256,22 @@ public: static QString getDynamicMapDefineName(); static QString getDynamicMapName(); static QString getEmptySpeciesName(); + static QMargins getPixelViewDistance(); static QMargins getMetatileViewDistance(); static int getNumTilesPrimary() { return num_tiles_primary; } static int getNumTilesTotal() { return num_tiles_total; } + static int getNumTilesSecondary() { return getNumTilesTotal() - getNumTilesPrimary(); } static int getNumMetatilesPrimary() { return num_metatiles_primary; } static int getNumMetatilesTotal() { return Block::getMaxMetatileId() + 1; } + static int getNumMetatilesSecondary() { return getNumMetatilesTotal() - getNumMetatilesPrimary(); } static int getNumPalettesPrimary(){ return num_pals_primary; } static int getNumPalettesTotal() { return num_pals_total; } + static int getNumPalettesSecondary() { return getNumPalettesTotal() - getNumPalettesPrimary(); } static QString getEmptyMapsecName(); static QString getMapGroupPrefix(); private: + QPointer fileWatcher; QMap modifiedFileTimestamps; QMap facingDirections; QHash speciesToIconPath; @@ -332,6 +341,9 @@ private: void ignoreWatchedFilesTemporarily(const QStringList &filepaths); void recordFileChange(const QString &filepath); void resetFileCache(); + void resetFileWatcher(); + void logFileWatchStatus(); + void cacheTileset(const QString &label, Tileset *tileset); bool saveMapLayouts(); bool saveMapGroups(); diff --git a/include/scripting.h b/include/scripting.h index b85e9e26..b700df8f 100644 --- a/include/scripting.h +++ b/include/scripting.h @@ -2,12 +2,19 @@ #ifndef SCRIPTING_H #define SCRIPTING_H -#include "mainwindow.h" -#include "block.h" +#include #include "scriptutility.h" -#include +class Block; +class Tile; +class MainWindow; + +#if __has_include() #include +#endif + + +#ifdef QT_QML_LIB // !! New callback functions or changes to existing callback function names/arguments // should be synced to resources/text/script_template.txt and docsrc/manual/scripting-capabilities.rst @@ -51,7 +58,7 @@ public: static void cb_MapResized(int oldWidth, int oldHeight, const QMargins &delta); static void cb_BorderResized(int oldWidth, int oldHeight, int newWidth, int newHeight); static void cb_MapShifted(int xDelta, int yDelta); - static void cb_TilesetUpdated(QString tilesetName); + static void cb_TilesetUpdated(const QString &tilesetName); static void cb_MainTabChanged(int oldTab, int newTab); static void cb_MapViewTabChanged(int oldTab, int newTab); static void cb_BorderVisibilityToggled(bool visible); @@ -78,4 +85,34 @@ private: void invokeCallback(CallbackType type, QJSValueList args); }; +#else + +class Scripting +{ +public: + Scripting(MainWindow *) {} + ~Scripting() {} + static void init(MainWindow *) {} + static void stop() {} + static void populateGlobalObject(MainWindow *) {} + + static void cb_ProjectOpened(QString) {}; + static void cb_ProjectClosed(QString) {}; + static void cb_MetatileChanged(int, int, Block, Block) {}; + static void cb_BorderMetatileChanged(int, int, uint16_t, uint16_t) {}; + static void cb_BlockHoverChanged(int, int) {}; + static void cb_BlockHoverCleared() {}; + static void cb_MapOpened(QString) {}; + static void cb_LayoutOpened(QString) {}; + static void cb_MapResized(int, int, const QMargins &) {}; + static void cb_BorderResized(int, int, int, int) {}; + static void cb_MapShifted(int, int) {}; + static void cb_TilesetUpdated(const QString &) {}; + static void cb_MainTabChanged(int, int) {}; + static void cb_MapViewTabChanged(int, int) {}; + static void cb_BorderVisibilityToggled(bool) {}; +}; + +#endif // QT_QML_LIB + #endif // SCRIPTING_H diff --git a/include/scriptutility.h b/include/scriptutility.h index 78272eec..ae60d316 100644 --- a/include/scriptutility.h +++ b/include/scriptutility.h @@ -2,14 +2,24 @@ #ifndef SCRIPTUTILITY_H #define SCRIPTUTILITY_H -#include "mainwindow.h" +#if __has_include() +#include +#endif + +#ifdef QT_QML_LIB + +#include +#include +#include +#include + +class MainWindow; class ScriptUtility : public QObject { Q_OBJECT - public: - ScriptUtility(MainWindow *mainWindow); + ScriptUtility(MainWindow *mainWindow) : window(mainWindow) {} ~ScriptUtility(); QString getActionFunctionName(int actionIndex); @@ -38,9 +48,9 @@ public: Q_INVOKABLE bool getSmartPathsEnabled(); Q_INVOKABLE QList getCustomScripts(); Q_INVOKABLE QList getMetatileLayerOrder(); - Q_INVOKABLE void setMetatileLayerOrder(QList order); + Q_INVOKABLE void setMetatileLayerOrder(const QList &order); Q_INVOKABLE QList getMetatileLayerOpacity(); - Q_INVOKABLE void setMetatileLayerOpacity(QList order); + Q_INVOKABLE void setMetatileLayerOpacity(const QList &order); Q_INVOKABLE QList getMapNames(); Q_INVOKABLE QList getMapConstants(); Q_INVOKABLE QList getLayoutNames(); @@ -57,6 +67,8 @@ public: Q_INVOKABLE bool isPrimaryTileset(QString tilesetName); Q_INVOKABLE bool isSecondaryTileset(QString tilesetName); + static bool validateMetatileLayerOrder(const QList &order); + private: void callTimeoutFunction(QJSValue callback); void runMessageBox(QString text, QString informativeText, QString detailedText, QMessageBox::Icon icon); @@ -67,4 +79,6 @@ private: QHash actionMap; }; +#endif // QT_QML_LIB + #endif // SCRIPTUTILITY_H diff --git a/include/settings.h b/include/settings.h index d37283cd..6e64fd2a 100644 --- a/include/settings.h +++ b/include/settings.h @@ -10,7 +10,6 @@ public: Settings(); bool smartPathsEnabled; bool betterCursors; - QCursor mapCursor; bool playerViewRectEnabled; bool cursorTileRectEnabled; }; diff --git a/include/ui/checkeredbgscene.h b/include/ui/checkeredbgscene.h new file mode 100644 index 00000000..bf0452a7 --- /dev/null +++ b/include/ui/checkeredbgscene.h @@ -0,0 +1,39 @@ +#ifndef CHECKEREDBGSCENE_H +#define CHECKEREDBGSCENE_H + +#include + +// Custom scene that paints its background a gray checkered pattern. +// Additionally there is a definable "valid" area which will paint the checkerboard green inside. +class CheckeredBgScene : public QGraphicsScene { + Q_OBJECT + +public: + CheckeredBgScene(const QSize &gridSize, QObject *parent = nullptr) + : QGraphicsScene(parent), + gridSize(gridSize) + {}; + CheckeredBgScene(int width, int height, QObject *parent = nullptr) + : CheckeredBgScene(QSize(width, height), parent) + {}; + + void setValidRect(int x, int y, int width, int height) { + this->validRect = QRect(x * this->gridSize.width(), + y * this->gridSize.height(), + width * this->gridSize.width(), + height * this->gridSize.height()); + } + void setValidRect(const QRect &rect) { + this->validRect = rect; + } + QRect getValidRect() { return this->validRect; } + +protected: + void drawBackground(QPainter *painter, const QRectF &rect) override; + +private: + QSize gridSize; + QRect validRect; +}; + +#endif // CHECKEREDBGSCENE_H diff --git a/include/ui/collisionpixmapitem.h b/include/ui/collisionpixmapitem.h index 1419f2e4..d4f7e182 100644 --- a/include/ui/collisionpixmapitem.h +++ b/include/ui/collisionpixmapitem.h @@ -23,29 +23,14 @@ public: QSpinBox * selectedElevation; qreal *opacity; void updateMovementPermissionSelection(QGraphicsSceneMouseEvent *event); - virtual void paint(QGraphicsSceneMouseEvent*); - virtual void floodFill(QGraphicsSceneMouseEvent*); - virtual void magicFill(QGraphicsSceneMouseEvent*); - virtual void pick(QGraphicsSceneMouseEvent*); - void draw(bool ignoreCache = false); + virtual void paint(QGraphicsSceneMouseEvent*) override; + virtual void floodFill(QGraphicsSceneMouseEvent*) override; + virtual void magicFill(QGraphicsSceneMouseEvent*) override; + virtual void pick(QGraphicsSceneMouseEvent*) override; + void draw(bool ignoreCache = false) override; private: - unsigned actionId_ = 0; - QPoint previousPos; void updateSelection(QPoint pos); - -signals: - void mouseEvent(QGraphicsSceneMouseEvent *, CollisionPixmapItem *); - void hoveredMapMovementPermissionChanged(int, int); - void hoveredMapMovementPermissionCleared(); - -protected: - void hoverMoveEvent(QGraphicsSceneHoverEvent*); - void hoverEnterEvent(QGraphicsSceneHoverEvent*); - void hoverLeaveEvent(QGraphicsSceneHoverEvent*); - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseMoveEvent(QGraphicsSceneMouseEvent*); - void mouseReleaseEvent(QGraphicsSceneMouseEvent*); }; #endif // COLLISIONPIXMAPITEM_H diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 32e309f9..5106ba8f 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -2,6 +2,7 @@ #define CONNECTIONPIXMAPITEM_H #include "mapconnection.h" +#include "metatile.h" #include #include #include @@ -31,8 +32,8 @@ private: bool selected = false; unsigned actionId = 0; - static const int mWidth = 16; - static const int mHeight = 16; + static const int mWidth = Metatile::pixelWidth(); + static const int mHeight = Metatile::pixelHeight(); void updatePos(); void updateOrigin(); diff --git a/include/ui/cursortilerect.h b/include/ui/cursortilerect.h index f53ae5aa..fd150dc2 100644 --- a/include/ui/cursortilerect.h +++ b/include/ui/cursortilerect.h @@ -8,78 +8,56 @@ class CursorTileRect : public QGraphicsItem { public: - CursorTileRect(bool *enabled, QRgb color); - QRectF boundingRect() const override - { - int width = this->width; - int height = this->height; - if (this->singleTileMode) { - width = 16; - height = 16; - } else if (!this->rightClickSelectionAnchored && this->smartPathMode && this->selectionHeight == 3 && this->selectionWidth == 3) { - width = 32; - height = 32; - } + CursorTileRect(const QSize &tileSize, const QRgb &color, QGraphicsItem *parent = nullptr); + + QSize size() const; + + QRectF boundingRect() const override { + auto s = size(); qreal penWidth = 4; return QRectF(-penWidth, -penWidth, - width + penWidth * 2, - height + penWidth * 2); + s.width() + penWidth * 2, + s.height() + penWidth * 2); } - void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override - { - if (!(*enabled)) return; - int width = this->width; - int height = this->height; - if (this->singleTileMode) { - width = 16; - height = 16; - } else if (this->smartPathInEffect()) { - width = 32; - height = 32; - } - - painter->setPen(this->color); - painter->drawRect(x() - 1, y() - 1, width + 2, height + 2); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { + if (!isVisible()) return; + auto rect = QRectF(pos(), size()); + painter->setPen(m_color); + painter->drawRect(rect + QMargins(1,1,1,1)); // Fill painter->setPen(QColor(0, 0, 0)); - painter->drawRect(x() - 2, y() - 2, width + 4, height + 4); - painter->drawRect(x(), y(), width, height); + painter->drawRect(rect + QMargins(2,2,2,2)); // Outer border + painter->drawRect(rect); // Inner border } + void initAnchor(int coordX, int coordY); void stopAnchor(); void initRightClickSelectionAnchor(int coordX, int coordY); void stopRightClickSelectionAnchor(); - void setSmartPathMode(bool enable) { this->smartPathMode = enable; } - bool getSmartPathMode() const { return this->smartPathMode; } + void setSmartPathMode(bool enable) { m_smartPathMode = enable; } + bool getSmartPathMode() const { return m_smartPathMode; } - void setStraightPathMode(bool enable) { this->straightPathMode = enable; } - bool getStraightPathMode() const { return this->straightPathMode; } - - void setSingleTileMode(bool enable) { this->singleTileMode = enable; } - bool getSingleTileMode() const { return this->singleTileMode; } + void setSingleTileMode(bool enable) { m_singleTileMode = enable; } + bool getSingleTileMode() const { return m_singleTileMode; } void updateLocation(int x, int y); - void updateSelectionSize(int width, int height); - void setActive(bool active); - bool getActive(); - bool *enabled; + void updateSelectionSize(const QSize &size); + void updateSelectionSize(int width, int height) { updateSelectionSize(QSize(width, height)); } + private: - bool active; - int width; - int height; - bool anchored; - bool rightClickSelectionAnchored; - bool smartPathMode; - bool straightPathMode; - bool singleTileMode; - int anchorCoordX; - int anchorCoordY; - int selectionWidth; - int selectionHeight; - QRgb color; - bool smartPathInEffect(); + const QSize m_tileSize; + QSize m_selectionSize; + QPoint m_anchorCoord; + QRgb m_color; + + bool m_anchored = false; + bool m_rightClickSelectionAnchored = false; + bool m_smartPathMode = false; + bool m_singleTileMode = false; + + bool smartPathInEffect() const; }; diff --git a/include/ui/graphicsview.h b/include/ui/graphicsview.h index c587d2a9..bf3a7e42 100644 --- a/include/ui/graphicsview.h +++ b/include/ui/graphicsview.h @@ -4,11 +4,26 @@ #include #include -class NoScrollGraphicsView : public QGraphicsView +// For general utility features that we add to QGraphicsView +class GraphicsView : public QGraphicsView { Q_OBJECT public: - NoScrollGraphicsView(QWidget *parent = nullptr) : QGraphicsView(parent) {} + GraphicsView(QWidget *parent = nullptr) : QGraphicsView(parent) {} + + void centerOn(const QGraphicsView *other) { + if (other && other->viewport()) { + QPoint center = other->viewport()->rect().center(); + QGraphicsView::centerOn(other->mapToScene(center)); + } + } +}; + +class NoScrollGraphicsView : public GraphicsView +{ + Q_OBJECT +public: + NoScrollGraphicsView(QWidget *parent = nullptr) : GraphicsView(parent) {} protected: void wheelEvent(QWheelEvent *event) { @@ -32,11 +47,11 @@ signals: void clicked(QMouseEvent *event); }; -class ConnectionsView : public QGraphicsView +class ConnectionsView : public GraphicsView { Q_OBJECT public: - ConnectionsView(QWidget *parent = nullptr) : QGraphicsView(parent) {} + ConnectionsView(QWidget *parent = nullptr) : GraphicsView(parent) {} signals: void pressedDelete(); diff --git a/include/ui/imageproviders.h b/include/ui/imageproviders.h index 806ffd5b..f1edc04a 100644 --- a/include/ui/imageproviders.h +++ b/include/ui/imageproviders.h @@ -6,13 +6,40 @@ #include #include +class Layout; + QImage getCollisionMetatileImage(Block); QImage getCollisionMetatileImage(int, int); -QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); -QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList&, const QList&, bool useTruePalettes = false); -QImage getTileImage(uint16_t, Tileset*, Tileset*); -QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false); -QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset); + +QImage getMetatileImage(uint16_t, const Layout*, bool useTruePalettes = false); +QImage getMetatileImage(const Metatile*, const Layout*, bool useTruePalettes = false); +QImage getMetatileImage(uint16_t, const Tileset*, const Tileset*, const QList& = {0,1,2}, const QList& = {}, bool useTruePalettes = false); +QImage getMetatileImage(const Metatile*, const Tileset*, const Tileset*, const QList& = {0,1,2}, const QList& = {}, bool useTruePalettes = false); + +QImage getMetatileSheetImage(const Layout *layout, int numMetatilesWIde, bool useTruePalettes = false); +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, + uint16_t metatileIdStart, + uint16_t metatileIdEnd, + int numMetatilesWIde, + const QList &layerOrder, + const QList &layerOpacity = {}, + const QSize &metatileSize = Metatile::pixelSize(), + bool useTruePalettes = false); +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity = {}, + const QSize &metatileSize = Metatile::pixelSize(), + bool useTruePalettes = false); + + +QImage getTileImage(uint16_t, const Tileset*, const Tileset*); +QImage getPalettedTileImage(uint16_t, const Tileset*, const Tileset*, int, bool useTruePalettes = false); +QImage getColoredTileImage(uint16_t tileId, const Tileset *, const Tileset *, const QList &palette); +QImage getGreyscaleTileImage(uint16_t tileId, const Tileset *, const Tileset *); + void flattenTo4bppImage(QImage * image); static QList greyscalePalette({ diff --git a/include/ui/layoutpixmapitem.h b/include/ui/layoutpixmapitem.h index 695472a5..44970363 100644 --- a/include/ui/layoutpixmapitem.h +++ b/include/ui/layoutpixmapitem.h @@ -61,20 +61,22 @@ public: void magicFill( int initialX, int initialY, - QPoint selectionDimensions, - QList selectedMetatiles, - QList selectedCollisions, + const QSize &selectionDimensions, + const QList &selectedMetatiles, + const QList &selectedCollisions, bool fromScriptCall = false); void floodFill(int x, int y, bool fromScriptCall = false); void floodFill(int x, int y, uint16_t metatileId, bool fromScriptCall = false); void floodFill(int initialX, int initialY, - QPoint selectionDimensions, - QList selectedMetatiles, - QList selectedCollisions, + const QSize &selectionDimensions, + const QList &selectedMetatiles, + const QList &selectedCollisions, bool fromScriptCall = false); void floodFillSmartPath(int initialX, int initialY, bool fromScriptCall = false); + static bool isSmartPathSize(const QSize &size) { return size.width() == smartPathWidth && size.height() == smartPathHeight; } + virtual void pick(QGraphicsSceneMouseEvent*); virtual void select(QGraphicsSceneMouseEvent*); virtual void shift(QGraphicsSceneMouseEvent*); @@ -86,32 +88,33 @@ public: void lockNondominantAxis(QGraphicsSceneMouseEvent *event); QPoint adjustCoords(QPoint pos); - void setEditsEnabled(bool enabled) { this->editsEnabled = enabled; } - bool getEditsEnabled() { return this->editsEnabled; } +protected: + unsigned actionId_ = 0; private: void paintSmartPath(int x, int y, bool fromScriptCall = false); + static bool isValidSmartPathSelection(MetatileSelection selection); static QList smartPathTable; + static constexpr int smartPathWidth = 3; + static constexpr int smartPathHeight = 3; + static constexpr int smartPathMiddleIndex = (smartPathWidth / 2) + ((smartPathHeight / 2) * smartPathWidth); QPoint lastMetatileSelectionPos = QPoint(-1,-1); - unsigned actionId_ = 0; - - bool editsEnabled = true; - signals: void startPaint(QGraphicsSceneMouseEvent *, LayoutPixmapItem *); void endPaint(QGraphicsSceneMouseEvent *, LayoutPixmapItem *); void mouseEvent(QGraphicsSceneMouseEvent *, LayoutPixmapItem *); - void hoveredMapMetatileChanged(const QPoint &pos); - void hoveredMapMetatileCleared(); + void hoverEntered(const QPoint &pos); + void hoverChanged(const QPoint &pos); + void hoverCleared(); protected: - void hoverMoveEvent(QGraphicsSceneHoverEvent*); - void hoverEnterEvent(QGraphicsSceneHoverEvent*); - void hoverLeaveEvent(QGraphicsSceneHoverEvent*); - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseMoveEvent(QGraphicsSceneMouseEvent*); - void mouseReleaseEvent(QGraphicsSceneMouseEvent*); + virtual void hoverMoveEvent(QGraphicsSceneHoverEvent*) override; + virtual void hoverEnterEvent(QGraphicsSceneHoverEvent*) override; + virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; + virtual void mousePressEvent(QGraphicsSceneMouseEvent*) override; + virtual void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; + virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; }; #endif // MAPPIXMAPITEM_H diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 51c1afe3..789cf9b9 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -2,6 +2,7 @@ #define MAPIMAGEEXPORTER_H #include "project.h" +#include "checkeredbgscene.h" class QGifImage; @@ -52,7 +53,7 @@ private: Project *m_project = nullptr; Map *m_map = nullptr; Layout *m_layout = nullptr; - QGraphicsScene *m_scene = nullptr; + CheckeredBgScene *m_scene = nullptr; QGifImage *m_timelapseGifImage = nullptr; QBuffer *m_timelapseBuffer = nullptr; QMovie *m_timelapseMovie = nullptr; diff --git a/include/ui/mapruler.h b/include/ui/mapruler.h index 151492e3..160978cc 100644 --- a/include/ui/mapruler.h +++ b/include/ui/mapruler.h @@ -3,6 +3,7 @@ #include #include +#include "metatile.h" class MapRuler : public QGraphicsObject, private QLine @@ -63,8 +64,8 @@ private: QPoint snapToWithinBounds(QPoint pos) const; void updateGeometry(); void updateStatus(Qt::Corner corner); - int pixWidth() const { return width() * 16; } - int pixHeight() const { return height() * 16; } + int pixWidth() const { return width() * Metatile::pixelWidth(); } + int pixHeight() const { return height() * Metatile::pixelHeight(); } }; #endif // MAPRULER_H diff --git a/include/ui/mapview.h b/include/ui/mapview.h index 5520ec80..34610bb1 100644 --- a/include/ui/mapview.h +++ b/include/ui/mapview.h @@ -1,20 +1,23 @@ #ifndef MAPVIEW_H #define MAPVIEW_H +#if __has_include() #include +#endif + #include "graphicsview.h" #include "overlay.h" #include "tile.h" class Editor; -class MapView : public QGraphicsView +class MapView : public GraphicsView { Q_OBJECT public: - MapView() : QGraphicsView() {} - MapView(QWidget *parent) : QGraphicsView(parent) {} + MapView() : GraphicsView() {} + MapView(QWidget *parent) : GraphicsView(parent) {} Editor *editor; @@ -22,6 +25,7 @@ public: void clearOverlayMap(); // Overlay scripting API +#ifdef QT_QML_LIB Q_INVOKABLE void clear(int layer); Q_INVOKABLE void clear(); Q_INVOKABLE void hide(int layer); @@ -74,6 +78,7 @@ public: Q_INVOKABLE void addTileImage(int x, int y, int tileId, bool xflip, bool yflip, int paletteId, bool setTransparency = false, int layer = 0); Q_INVOKABLE void addTileImage(int x, int y, QJSValue tileObj, bool setTransparency = false, int layer = 0); Q_INVOKABLE void addMetatileImage(int x, int y, int metatileId, bool setTransparency = false, int layer = 0); +#endif // QT_QML_LIB protected: virtual void drawForeground(QPainter *painter, const QRectF &rect) override; diff --git a/include/ui/metatileimageexporter.h b/include/ui/metatileimageexporter.h new file mode 100644 index 00000000..1d486bd4 --- /dev/null +++ b/include/ui/metatileimageexporter.h @@ -0,0 +1,103 @@ +#ifndef METATILEIMAGEEXPORTER_H +#define METATILEIMAGEEXPORTER_H + +#include +#include +#include +#include +#include +#include + +#include "config.h" +#include "checkeredbgscene.h" + +class Tileset; + +namespace Ui { +class MetatileImageExporter; +} + +class ReorderableListWidget : public QListWidget +{ + Q_OBJECT +public: + explicit ReorderableListWidget(QWidget *parent = nullptr) : QListWidget(parent) { + setDragEnabled(true); + setDragDropMode(QAbstractItemView::InternalMove); + setDefaultDropAction(Qt::MoveAction); + }; + +signals: + void reordered(); + +protected: + virtual void dropEvent(QDropEvent *event) override { + QListWidget::dropEvent(event); + if (event->isAccepted()) { + emit reordered(); + } + } +}; + +class MetatileImageExporter : public QDialog +{ + Q_OBJECT + +public: + struct Settings { + OrderedMap layerOrder = { + {2, true}, + {1, true}, + {0, true}, + }; + uint16_t metatileStart = 0; + uint16_t metatileEnd = 0xFFFF; + uint16_t numMetatilesWide = projectConfig.metatileSelectorWidth; + bool usePrimaryTileset = true; + bool useSecondaryTileset = false; + bool renderPlaceholders = false; + int transparencyMode = 0; + }; + + explicit MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings = nullptr); + ~MetatileImageExporter(); + + bool saveImage(QString filepath = QString()); + QImage getImage(); + QString getDefaultFileName() const; + void applySettings(const Settings &settings); + void reset(); + +protected: + virtual void showEvent(QShowEvent *) override; + virtual void closeEvent(QCloseEvent *) override; + +private: + Ui::MetatileImageExporter *ui; + + Tileset *m_primaryTileset; + Tileset *m_secondaryTileset; + Settings *m_savedSettings; + + CheckeredBgScene *m_scene = nullptr; + QGraphicsPixmapItem *m_preview = nullptr; + bool m_previewUpdateQueued = false; + QList m_layerOrder; + ProjectConfig m_savedConfig; + QList m_transparencyButtons; + + void populate(const Settings &settings); + void updatePreview(); + void tryUpdatePreview(); + void queuePreviewUpdate(); + void tryEnforceMetatileRange(); + void syncPixelWidth(); + void syncMetatileWidth(); + void validateMetatileStart(); + void validateMetatileEnd(); + void updateMetatileRange(); + void copyRenderSettings(); + void restoreRenderSettings(); +}; + +#endif // METATILEIMAGEEXPORTER_H diff --git a/include/ui/metatilelayersitem.h b/include/ui/metatilelayersitem.h index ef05a77d..18e799f4 100644 --- a/include/ui/metatilelayersitem.h +++ b/include/ui/metatilelayersitem.h @@ -9,38 +9,50 @@ class MetatileLayersItem: public SelectablePixmapItem { Q_OBJECT public: - MetatileLayersItem(Metatile *metatile, Tileset *primaryTileset, Tileset *secondaryTileset): SelectablePixmapItem(16, 16, 6, 2) { - this->metatile = metatile; - this->primaryTileset = primaryTileset; - this->secondaryTileset = secondaryTileset; - this->clearLastModifiedCoords(); - this->clearLastHoveredCoords(); - setAcceptHoverEvents(true); - } - void draw(); + MetatileLayersItem(Metatile *metatile, + Tileset *primaryTileset, + Tileset *secondaryTileset, + Qt::Orientation orientation = Qt::Vertical); + + void draw() override; void setTilesets(Tileset*, Tileset*); void setMetatile(Metatile*); - void clearLastModifiedCoords(); - void clearLastHoveredCoords(); + + bool hasCursor() const { return this->cursorCellPos != QPoint(-1,-1); } + Tile tileUnderCursor() const; + + QPoint tileIndexToPos(int index) const { return this->tilePositions.value(index); } + int posToTileIndex(const QPoint &pos) const { return this->tilePositions.indexOf(pos); } + int posToTileIndex(int x, int y) const { return posToTileIndex(QPoint(x, y)); } + + void setOrientation(Qt::Orientation orientation); + bool showGrid; private: Metatile* metatile; Tileset *primaryTileset; Tileset *secondaryTileset; - QPoint prevChangedPos; - QPoint prevHoveredPos; + Qt::Orientation orientation; + + QPoint cursorCellPos = QPoint(-1,-1); + + QList tilePositions; + QPoint getBoundedPos(const QPointF &); + void updateSelection(); + bool setCursorCellPos(const QPoint &pos); signals: - void tileChanged(int, int); - void selectedTilesChanged(QPoint, int, int); - void hoveredTileChanged(uint16_t); + void tileChanged(const QPoint &pos); + void paletteChanged(const QPoint &pos); + void selectedTilesChanged(const QPoint &pos, const QSize &dimensions); + void hoveredTileChanged(const Tile &tile); void hoveredTileCleared(); protected: - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseMoveEvent(QGraphicsSceneMouseEvent*); - void mouseReleaseEvent(QGraphicsSceneMouseEvent*); - void hoverMoveEvent(QGraphicsSceneHoverEvent*); - void hoverLeaveEvent(QGraphicsSceneHoverEvent*); + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent*) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; }; #endif // METATILELAYERSITEM_H diff --git a/include/ui/metatileselector.h b/include/ui/metatileselector.h index 7bb3f131..663ccd5a 100644 --- a/include/ui/metatileselector.h +++ b/include/ui/metatileselector.h @@ -9,20 +9,20 @@ struct MetatileSelectionItem { - bool enabled; - uint16_t metatileId; + bool enabled = false; + uint16_t metatileId = 0; }; struct CollisionSelectionItem { - bool enabled; - uint16_t collision; - uint16_t elevation; + bool enabled = false; + uint16_t collision = 0; + uint16_t elevation = 0; }; struct MetatileSelection { - QPoint dimensions; + QSize dimensions; bool hasCollision; QList metatileItems; QList collisionItems; @@ -31,33 +31,34 @@ struct MetatileSelection class MetatileSelector: public SelectablePixmapItem { Q_OBJECT public: - MetatileSelector(int numMetatilesWide, Layout *layout): SelectablePixmapItem(16, 16) { + MetatileSelector(int numMetatilesWide, Layout *layout) + : SelectablePixmapItem(Metatile::pixelSize()), + numMetatilesWide(qMax(numMetatilesWide, 1)) + { this->externalSelection = false; this->prefabSelection = false; - this->numMetatilesWide = numMetatilesWide; this->layout = layout; - this->primaryTileset = layout->tileset_primary; - this->secondaryTileset = layout->tileset_secondary; this->selection = MetatileSelection{}; this->cellPos = QPoint(-1, -1); setAcceptHoverEvents(true); } - QPoint getSelectionDimensions() override; + QSize getSelectionDimensions() const override; void draw() override; + void refresh(); bool select(uint16_t metatile); void selectFromMap(uint16_t metatileId, uint16_t collision, uint16_t elevation); - void setTilesets(Tileset*, Tileset*); - MetatileSelection getMetatileSelection(); + MetatileSelection getMetatileSelection() const { return this->selection; } void setPrefabSelection(MetatileSelection selection); - void setExternalSelection(int, int, QList, QList>); - QPoint getMetatileIdCoordsOnWidget(uint16_t); + void setExternalSelection(int, int, const QList&, const QList>&); + QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId) const; void setLayout(Layout *layout); bool isInternalSelection() const { return (!this->externalSelection && !this->prefabSelection); } - Tileset *primaryTileset; - Tileset *secondaryTileset; + Tileset *primaryTileset() const { return this->layout->tileset_primary; } + Tileset *secondaryTileset() const { return this->layout->tileset_secondary; } + protected: void mousePressEvent(QGraphicsSceneMouseEvent*) override; void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; @@ -66,10 +67,10 @@ protected: void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; void drawSelection() override; private: + const int numMetatilesWide; QPixmap basePixmap; bool externalSelection; bool prefabSelection; - int numMetatilesWide; Layout *layout; int externalSelectionWidth; int externalSelectionHeight; @@ -80,8 +81,9 @@ private: void updateBasePixmap(); void updateSelectedMetatiles(); void updateExternalSelectedMetatiles(); - uint16_t getMetatileId(int x, int y) const; - QPoint getMetatileIdCoords(uint16_t); + uint16_t posToMetatileId(int x, int y, bool *ok = nullptr) const; + uint16_t posToMetatileId(const QPoint &pos, bool *ok = nullptr) const; + QPoint metatileIdToPos(uint16_t metatileId, bool *ok = nullptr) const; bool positionIsValid(const QPoint &pos) const; bool selectionIsValid(); void hoverChanged(); diff --git a/include/ui/montabwidget.h b/include/ui/montabwidget.h index 8292d28e..004fa475 100644 --- a/include/ui/montabwidget.h +++ b/include/ui/montabwidget.h @@ -33,6 +33,9 @@ public slots: void setTabActive(int index, bool active = true); void deactivateTab(int tabIndex); +signals: + void edited(); + private: void actionCopyTab(int index); void actionAddDeleteTab(int index); diff --git a/include/ui/movablerect.h b/include/ui/movablerect.h index 92dd43f7..7940e592 100644 --- a/include/ui/movablerect.h +++ b/include/ui/movablerect.h @@ -10,7 +10,7 @@ class MovableRect : public QGraphicsRectItem { public: - MovableRect(bool *enabled, const QRectF &rect, const QRgb &color); + MovableRect(const QRectF &rect, const QSize &cellSize, const QRgb &color); QRectF boundingRect() const override { qreal penWidth = 4; return QRectF(-penWidth, @@ -29,13 +29,9 @@ public: } void updateLocation(int x, int y); - void setActive(bool active); - bool getActive() const { return this->active; } - protected: - bool *enabled = nullptr; - bool active = true; QRectF baseRect; + QSize cellSize; QRgb color; void updateVisibility(); @@ -48,7 +44,7 @@ class ResizableRect : public QObject, public MovableRect { Q_OBJECT public: - ResizableRect(QObject *parent, bool *enabled, int width, int height, QRgb color); + ResizableRect(QObject *parent, const QSize &cellSize, const QSize &size, const QRgb &color); QRectF boundingRect() const override { return QRectF(this->rect() + QMargins(lineWidth, lineWidth, lineWidth, lineWidth)); diff --git a/include/ui/newlayoutform.h b/include/ui/newlayoutform.h index 3e77af18..99640444 100644 --- a/include/ui/newlayoutform.h +++ b/include/ui/newlayoutform.h @@ -24,7 +24,7 @@ public: void setSettings(const Layout::Settings &settings); Layout::Settings settings() const; - void setDisabled(bool disabled); + void setDimensionsDisabled(bool disabled); bool validate(); diff --git a/include/ui/numericsorttableitem.h b/include/ui/numericsorttableitem.h new file mode 100644 index 00000000..e33f48cb --- /dev/null +++ b/include/ui/numericsorttableitem.h @@ -0,0 +1,20 @@ +#ifndef NUMERICSORTTABLEITEM_H +#define NUMERICSORTTABLEITEM_H + +#include +#include + +class NumericSortTableItem : public QTableWidgetItem +{ +public: + explicit NumericSortTableItem(const QString &text) : QTableWidgetItem(text) {}; + +protected: + virtual bool operator<(const QTableWidgetItem &other) const override { + QCollator collator; + collator.setNumericMode(true); + return collator.compare(text(), other.text()) < 0; + } +}; + +#endif // NUMERICSORTTABLEITEM_H diff --git a/include/ui/overlay.h b/include/ui/overlay.h index 86fe0963..9be73ec8 100644 --- a/include/ui/overlay.h +++ b/include/ui/overlay.h @@ -8,6 +8,8 @@ #include #include +#ifdef QT_QML_LIB + class OverlayItem { public: OverlayItem() {} @@ -123,4 +125,17 @@ private: QRectF *clippingRect; }; +#else + +class Overlay +{ +public: + Overlay() {} + ~Overlay() {} + + void renderItems(QPainter *) {} +}; + +#endif // QT_QML_LIB + #endif // OVERLAY_H diff --git a/include/ui/palettecolorsearch.h b/include/ui/palettecolorsearch.h new file mode 100644 index 00000000..967f6efa --- /dev/null +++ b/include/ui/palettecolorsearch.h @@ -0,0 +1,67 @@ +#ifndef PALETTECOLORSEARCH_H +#define PALETTECOLORSEARCH_H + +#include +#include +#include + +class Tileset; +class Project; + +namespace Ui { +class PaletteColorSearch; +} + +class PaletteColorSearch : public QDialog +{ + Q_OBJECT + +public: + explicit PaletteColorSearch(Project *project, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, + QWidget *parent = nullptr); + ~PaletteColorSearch(); + + void setPaletteId(int paletteId); + int currentPaletteId() const; + + void setColorId(int colorId); + int currentColorId() const; + + void setTilesets(const Tileset *primaryTileset, const Tileset *secondaryTileset); + const Tileset* currentTileset() const; + +signals: + void metatileSelected(uint16_t metatileId); + void paletteIdChanged(int paletteId); + +private: + struct RowData { + QString tilesetName; + QString pairedTilesetName; + QString metatileId; + QIcon metatileIcon; + }; + + enum ResultsColumn { + TilesetName, + Metatile, + }; + + Ui::PaletteColorSearch *ui; + Project *m_project; + const Tileset *m_primaryTileset; + const Tileset *m_secondaryTileset; + + QMap> m_resultsCache; + + void addTableEntry(const RowData &rowData); + QList search(int colorId) const; + QList search(int colorId, const Tileset *tileset, const Tileset *pairedTileset) const; + void refresh(); + void updateResults(); + void cellDoubleClicked(int row, int col); +}; + +#endif // PALETTECOLORSEARCH_H diff --git a/include/ui/paletteeditor.h b/include/ui/paletteeditor.h index 63581eb1..2d26cf71 100644 --- a/include/ui/paletteeditor.h +++ b/include/ui/paletteeditor.h @@ -2,10 +2,12 @@ #define PALETTEEDITOR_H #include +#include #include "colorinputwidget.h" #include "project.h" #include "history.h" +#include "palettecolorsearch.h" namespace Ui { class PaletteEditor; @@ -24,25 +26,40 @@ class PaletteEditor : public QMainWindow { public: explicit PaletteEditor(Project*, Tileset*, Tileset*, int paletteId, QWidget *parent = nullptr); ~PaletteEditor(); + void setPaletteId(int); + int currentPaletteId() const; + void setTilesets(Tileset*, Tileset*); + bool showingUnusedColors() const; + +signals: + void metatileSelected(uint16_t metatileId); + private: Ui::PaletteEditor *ui; - Project *project = nullptr; - QList colorInputs; - + Project *project; Tileset *primaryTileset; Tileset *secondaryTileset; - QList> palettesHistory; + QList colorInputs; + QMap> palettesHistory; + QMap> unusedColorCache; + QPointer colorSearchWindow; - Tileset* getTileset(int paletteId); + Tileset* getTileset(int paletteId) const; void refreshColorInputs(); + void refreshPaletteId(); void commitEditHistory(); void commitEditHistory(int paletteId); + void updateEditHistoryActions(); void restoreWindowState(); + void invalidateCache(); void closeEvent(QCloseEvent*); + void setColorInputTitles(bool show); + QSet getUnusedColorIds(); + void openColorSearch(); void setRgb(int index, QRgb rgb); void setPalette(int paletteId, const QList &palette); @@ -50,14 +67,13 @@ private: void setBitDepth(int bits); int bitDepth = 24; - static const int numColors = 16; + static const int numColors = Tileset::numColorsPerPalette(); signals: void closed(); void changedPaletteColor(); void changedPalette(int); private slots: - void on_spinBox_PaletteId_valueChanged(int arg1); void on_actionUndo_triggered(); void on_actionRedo_triggered(); void on_actionImport_Palette_triggered(); diff --git a/include/ui/preferenceeditor.h b/include/ui/preferenceeditor.h index 64647f0e..24b6253f 100644 --- a/include/ui/preferenceeditor.h +++ b/include/ui/preferenceeditor.h @@ -2,6 +2,7 @@ #define PREFERENCES_H #include +#include "config.h" class NoScrollComboBox; class QAbstractButton; @@ -23,7 +24,8 @@ public: signals: void preferencesSaved(); void themeChanged(const QString &theme); - void scriptSettingsChanged(bool on); + void scriptSettingsChanged(ScriptAutocompleteMode mode); + void reloadProjectRequested(); private: Ui::PreferenceEditor *ui; diff --git a/include/ui/resizelayoutpopup.h b/include/ui/resizelayoutpopup.h index 89ce2e99..b187044f 100644 --- a/include/ui/resizelayoutpopup.h +++ b/include/ui/resizelayoutpopup.h @@ -3,10 +3,10 @@ #include "maplayout.h" #include "project.h" +#include "checkeredbgscene.h" #include #include -#include #include #include #include @@ -16,39 +16,12 @@ namespace Ui { class ResizeLayoutPopup; } - - -/// Custom scene that paints its background a gray checkered pattern. -/// Additionally there is a definable "valid" area which will paint the checkerboard green inside. -class CheckeredBgScene : public QGraphicsScene { - Q_OBJECT - -public: - CheckeredBgScene(QObject *parent = nullptr); - void setValidRect(int x, int y, int width, int height) { - this->validRect = QRect(x * this->gridSize, y * this->gridSize, width * this->gridSize, height * this->gridSize); - } - void setValidRect(QRect rect) { - this->validRect = rect; - } - QRect getValidRect() { return this->validRect; } - -protected: - void drawBackground(QPainter *painter, const QRectF &rect) override; - -private: - int gridSize = 16; // virtual pixels - QRect validRect = QRect(); -}; - - - /// PixmapItem subclass which allows for creating a boundary which determine whether /// the pixmap paints normally or with a black tint. -/// This item is movable and snaps on a 16x16 grid. +/// This item is movable and snaps on a 'cellSize' grid. class BoundedPixmapItem : public QGraphicsPixmapItem { public: - BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent = nullptr); + BoundedPixmapItem(const QPixmap &pixmap, const QSize &cellSize, QGraphicsItem *parent = nullptr); void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override; void setBoundary(ResizableRect *rect) { this->boundary = rect; } @@ -59,6 +32,7 @@ protected: private: ResizableRect *boundary = nullptr; QPointF clickedPos = QPointF(); + QSize cellSize; }; diff --git a/include/ui/selectablepixmapitem.h b/include/ui/selectablepixmapitem.h index 2681e485..a267291e 100644 --- a/include/ui/selectablepixmapitem.h +++ b/include/ui/selectablepixmapitem.h @@ -7,16 +7,27 @@ class SelectablePixmapItem : public QObject, public QGraphicsPixmapItem { Q_OBJECT public: - SelectablePixmapItem(int cellWidth, int cellHeight): SelectablePixmapItem(cellWidth, cellHeight, INT_MAX, INT_MAX) {} - SelectablePixmapItem(int cellWidth, int cellHeight, int maxSelectionWidth, int maxSelectionHeight) { - this->cellWidth = cellWidth; - this->cellHeight = cellHeight; - this->maxSelectionWidth = maxSelectionWidth; - this->maxSelectionHeight = maxSelectionHeight; - } - virtual QPoint getSelectionDimensions(); + SelectablePixmapItem(const QSize &size, const QSize &maxSelectionSize = QSize(INT_MAX, INT_MAX)) + : SelectablePixmapItem(size.width(), size.height(), maxSelectionSize.width(), maxSelectionSize.height()) {} + SelectablePixmapItem(int cellWidth, int cellHeight, int maxSelectionWidth = INT_MAX, int maxSelectionHeight = INT_MAX) + : cellWidth(cellWidth), + cellHeight(cellHeight), + maxSelectionWidth(maxSelectionWidth), + maxSelectionHeight(maxSelectionHeight), + selectionInitialX(0), + selectionInitialY(0), + selectionOffsetX(0), + selectionOffsetY(0) + {} + virtual QSize getSelectionDimensions() const { return QSize(abs(this->selectionOffsetX) + 1, abs(this->selectionOffsetY) + 1); } virtual void draw() = 0; + virtual void setMaxSelectionSize(const QSize &size) { setMaxSelectionSize(size.width(), size.height()); } + virtual void setMaxSelectionSize(int width, int height); + QSize maxSelectionSize() { return QSize(this->maxSelectionWidth, this->maxSelectionHeight); } + + void setSelectionStyle(Qt::PenStyle style); + protected: int cellWidth; int cellHeight; @@ -28,16 +39,28 @@ protected: int selectionOffsetY; QPoint getSelectionStart(); - void select(int, int, int, int); - void updateSelection(int, int); - QPoint getCellPos(QPointF); + void select(const QPoint &pos, const QSize &size = QSize(1,1)); + void select(int x, int y, int width = 1, int height = 1) { select(QPoint(x, y), QSize(width, height)); } + void updateSelection(const QPoint &pos); + QPoint getCellPos(const QPointF &itemPos); + int getBoundedWidth(int width) const { return qBound(1, width, this->maxSelectionWidth); } + int getBoundedHeight(int height) const { return qBound(1, height, this->maxSelectionHeight); } virtual void mousePressEvent(QGraphicsSceneMouseEvent*) override; virtual void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + virtual void drawSelectionRect(const QPoint &, const QSize &, Qt::PenStyle style = Qt::SolidLine); virtual void drawSelection(); + virtual int cellsWide() const { return this->cellWidth ? (pixmap().width() / this->cellWidth) : 0; } + virtual int cellsTall() const { return this->cellHeight ? (pixmap().height() / this->cellHeight) : 0; } signals: - void selectionChanged(int, int, int, int); + void selectionChanged(const QPoint&, const QSize&); + +private: + QPoint prevCellPos = QPoint(-1,-1); + Qt::PenStyle selectionStyle = Qt::SolidLine; + + void setSelection(const QPoint &pos, const QSize &size); }; #endif // SELECTABLEPIXMAPITEM_H diff --git a/include/ui/shortcutseditor.h b/include/ui/shortcutseditor.h index e44c8896..6f136773 100644 --- a/include/ui/shortcutseditor.h +++ b/include/ui/shortcutseditor.h @@ -40,6 +40,7 @@ private: QHash> multiKeyEdits_objects; void parseObjectList(const QObjectList &objectList); + void parseObject(const QObject *object, QMap *objects_labels, QMap *objects_prefixes); QString getLabel(const QObject *object) const; bool stringPropertyIsNotEmpty(const QObject *object, const char *name) const; void populateMainContainer(); @@ -48,7 +49,6 @@ private: void addNewMultiKeyEdit(const QObject *object, const QString &shortcutContext); QList siblings(MultiKeyEdit *multiKeyEdit) const; void promptUserOnDuplicateFound(MultiKeyEdit *current, MultiKeyEdit *sender); - void removeKeySequence(const QKeySequence &keySequence, MultiKeyEdit *multiKeyEdit); void saveShortcuts(); void resetShortcuts(); diff --git a/include/ui/tilemaptileselector.h b/include/ui/tilemaptileselector.h index 155957a6..558b6dc6 100644 --- a/include/ui/tilemaptileselector.h +++ b/include/ui/tilemaptileselector.h @@ -136,7 +136,7 @@ public: this->palette = PaletteUtil::parse(palFilepath, &err); } this->setPixmap(QPixmap::fromImage(this->tileset)); - this->numTilesWide = this->tileset.width() / 8; + this->numTilesWide = this->tileset.width() / this->cellWidth; this->selectedTile = 0x00; setAcceptHoverEvents(true); } diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index b6a60a61..fc974d10 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -2,12 +2,15 @@ #define TILESETEDITOR_H #include +#include +#include #include "project.h" #include "history.h" #include "paletteeditor.h" #include "tileseteditormetatileselector.h" #include "tileseteditortileselector.h" #include "metatilelayersitem.h" +#include "metatileimageexporter.h" class NoScrollComboBox; class Layout; @@ -18,6 +21,7 @@ class TilesetEditor; class MetatileHistoryItem { public: + MetatileHistoryItem() {}; MetatileHistoryItem(uint16_t metatileId, Metatile *prevMetatile, Metatile *newMetatile, QString prevLabel, QString newLabel) { this->metatileId = metatileId; this->prevMetatile = prevMetatile; @@ -25,15 +29,24 @@ public: this->prevLabel = prevLabel; this->newLabel = newLabel; } + MetatileHistoryItem(uint16_t metatileIdA, uint16_t metatileIdB) { + this->metatileId = metatileIdA; + this->swapMetatileId = metatileIdB; + this->isSwap = true; + } ~MetatileHistoryItem() { delete this->prevMetatile; delete this->newMetatile; } - uint16_t metatileId; - Metatile *prevMetatile; - Metatile *newMetatile; + + uint16_t metatileId = 0; + Metatile *prevMetatile = nullptr; + Metatile *newMetatile = nullptr; QString prevLabel; QString newLabel; + + uint16_t swapMetatileId = 0; + bool isSwap = false; }; class TilesetEditor : public QMainWindow @@ -53,27 +66,19 @@ public: QObjectList shortcutableObjects() const; + void setPaletteId(int paletteId); + int paletteId() const; + public slots: void applyUserShortcuts(); void onSelectedMetatileChanged(uint16_t); private slots: void onWindowActivated(); - void onHoveredMetatileChanged(uint16_t); void onHoveredMetatileCleared(); - void onHoveredTileChanged(uint16_t); void onHoveredTileCleared(); - void onSelectedTilesChanged(); - void onMetatileLayerTileChanged(int, int); - void onMetatileLayerSelectionChanged(QPoint, int, int); + void onMetatileLayerSelectionChanged(const QPoint&, const QSize&); void onPaletteEditorChangedPaletteColor(); - void onPaletteEditorChangedPalette(int); - - void on_spinBox_paletteSelector_valueChanged(int arg1); - - void on_actionImport_Primary_Tiles_triggered(); - - void on_actionImport_Secondary_Tiles_triggered(); void on_actionChange_Metatiles_Count_triggered(); @@ -89,17 +94,7 @@ private slots: void on_actionUndo_triggered(); void on_actionRedo_triggered(); - void on_lineEdit_metatileLabel_editingFinished(); - - void on_actionExport_Primary_Tiles_Image_triggered(); - void on_actionExport_Secondary_Tiles_Image_triggered(); - void on_actionExport_Primary_Metatiles_Image_triggered(); - void on_actionExport_Secondary_Metatiles_Image_triggered(); - - void on_actionImport_Primary_Metatiles_triggered(); - void on_actionImport_Secondary_Metatiles_triggered(); - - void on_copyButton_metatileLabel_clicked(); + void on_copyButton_MetatileLabel_clicked(); void on_actionCut_triggered(); void on_actionCopy_triggered(); @@ -107,6 +102,10 @@ private slots: void on_horizontalSlider_MetatilesZoom_valueChanged(int value); void on_horizontalSlider_TilesZoom_valueChanged(int value); +protected: + void keyPressEvent(QKeyEvent *event) override; + void closeEvent(QCloseEvent*) override; + private: void initAttributesUi(); void initMetatileSelector(); @@ -122,16 +121,18 @@ private: void drawSelectedTiles(); void redrawTileSelector(); void redrawMetatileSelector(); - void importTilesetTiles(Tileset*, bool); - void importTilesetMetatiles(Tileset*, bool); + void importTilesetTiles(Tileset*); + void importAdvanceMapMetatiles(Tileset*); + void exportTilesImage(Tileset*); + void exportPorytilesLayerImages(Tileset*); + void exportMetatilesImage(); void refresh(); void commitMetatileLabel(); - void closeEvent(QCloseEvent*); void countMetatileUsage(); void countTileUsage(); void copyMetatile(bool cut); - void pasteMetatile(const Metatile * toPaste, QString label); - bool replaceMetatile(uint16_t metatileId, const Metatile * src, QString label); + void pasteMetatile(const Metatile &toPaste, QString label); + bool replaceMetatile(uint16_t metatileId, const Metatile &src, QString label); void commitMetatileChange(Metatile * prevMetatile); void commitMetatileAndLabelChange(Metatile * prevMetatile, QString prevLabel); uint32_t attributeNameToValue(Metatile::Attr attribute, const QString &text, bool *ok); @@ -142,24 +143,36 @@ private: void commitEncounterType(); void commitTerrainType(); void commitLayerType(); + void commit(MetatileHistoryItem *item); + void updateEditHistoryActions(); void setRawAttributesVisible(bool visible); - void setXFlip(bool enabled); - void setYFlip(bool enabled); + void refreshTileFlips(); + void refreshPaletteId(); + void paintSelectedLayerTiles(const QPoint &pos, bool paletteOnly = false); + void setMetatileLayerOrientation(Qt::Orientation orientation); + void commitMetatileSwap(uint16_t metatileIdA, uint16_t metatileIdB); + bool swapMetatiles(uint16_t metatileIdA, uint16_t metatileIdB); + void applyMetatileSwapToLayouts(uint16_t metatileIdA, uint16_t metatileIdB); + void applyMetatileSwapsToLayouts(); + void rebuildMetatilePropertiesFrame(); + void addWidgetToMetatileProperties(QWidget *w, int *row, int rowSpan); + void updateLayerTileStatus(); + void showTileStatus(const Tile &tile); + void showTileStatus(uint16_t tileId); + void updateMetatileStatus(); + void showMetatileStatus(uint16_t metatileId); Ui::TilesetEditor *ui; History metatileHistory; TilesetEditorMetatileSelector *metatileSelector = nullptr; TilesetEditorTileSelector *tileSelector = nullptr; MetatileLayersItem *metatileLayersItem = nullptr; - PaletteEditor *paletteEditor = nullptr; + QPointer paletteEditor = nullptr; Project *project = nullptr; Layout *layout = nullptr; Metatile *metatile = nullptr; Metatile *copiedMetatile = nullptr; QString copiedMetatileLabel; - int paletteId; - bool tileXFlip; - bool tileYFlip; bool hasUnsavedChanges; Tileset *primaryTileset = nullptr; Tileset *secondaryTileset = nullptr; @@ -170,6 +183,9 @@ private: QGraphicsScene *metatileLayersScene = nullptr; bool lockSelection = false; QSet metatileReloadQueue; + MetatileImageExporter::Settings *metatileImageExportSettings = nullptr; + QList> metatileIdSwaps; + int numLayerViewRows; bool save(); diff --git a/include/ui/tileseteditormetatileselector.h b/include/ui/tileseteditormetatileselector.h index 8a3fdfa7..3b38d9b2 100644 --- a/include/ui/tileseteditormetatileselector.h +++ b/include/ui/tileseteditormetatileselector.h @@ -9,7 +9,7 @@ class Layout; class TilesetEditorMetatileSelector: public SelectablePixmapItem { Q_OBJECT public: - TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout); + TilesetEditorMetatileSelector(int numMetatilesWide, Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout); Layout *layout = nullptr; void draw() override; @@ -18,11 +18,16 @@ public: bool select(uint16_t metatileId); void setTilesets(Tileset*, Tileset*); - uint16_t getSelectedMetatileId(); - void updateSelectedMetatile(); - QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId); - QImage buildPrimaryMetatilesImage(); - QImage buildSecondaryMetatilesImage(); + uint16_t getSelectedMetatileId() const { return this->selectedMetatileId; } + QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId) const; + + void setSwapMode(bool enabled); + void addToSwapSelection(uint16_t metatileId); + void removeFromSwapSelection(uint16_t metatileId); + void clearSwapSelection(); + + bool hasCursor() const { return this->prevCellPos != QPoint(-1,-1); } + uint16_t metatileIdUnderCursor() const { return this->lastHoveredMetatileId; } QVector usedMetatiles; bool selectorShowUnused = false; @@ -38,32 +43,37 @@ protected: void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; private: + const int numMetatilesWide; QImage baseImage; QPixmap basePixmap; Tileset *primaryTileset = nullptr; Tileset *secondaryTileset = nullptr; - uint16_t selectedMetatileId; - int numMetatilesWide; - int numMetatilesHigh; + uint16_t selectedMetatileId = 0; + QPoint prevCellPos = QPoint(-1,-1); + + QList swapMetatileIds; + uint16_t lastHoveredMetatileId = 0; + bool inSwapMode = false; + void updateBasePixmap(); - uint16_t getMetatileId(int x, int y); - QPoint getMetatileIdCoords(uint16_t); - bool shouldAcceptEvent(QGraphicsSceneMouseEvent*); - int numRows(int numMetatiles); - int numRows(); + uint16_t posToMetatileId(int x, int y, bool *ok = nullptr) const; + uint16_t posToMetatileId(const QPoint &pos, bool *ok = nullptr) const; + QPoint metatileIdToPos(uint16_t metatileId, bool *ok = nullptr) const; + bool isValidMetatileId(uint16_t metatileId) const; + int numRows(int numMetatiles) const; + int numRows() const; void drawGrid(); void drawDivider(); void drawFilters(); void drawUnused(); void drawCounts(); - QImage buildAllMetatilesImage(); - QImage buildImage(int metatileIdStart, int numMetatiles); int numPrimaryMetatilesRounded() const; signals: void hoveredMetatileChanged(uint16_t); void hoveredMetatileCleared(); void selectedMetatileChanged(uint16_t); + void swapRequested(uint16_t, uint16_t); }; #endif // TILESETEDITORMETATILESELECTOR_H diff --git a/include/ui/tileseteditortileselector.h b/include/ui/tileseteditortileselector.h index aa2a1923..c98239be 100644 --- a/include/ui/tileseteditortileselector.h +++ b/include/ui/tileseteditortileselector.h @@ -7,8 +7,8 @@ class TilesetEditorTileSelector: public SelectablePixmapItem { Q_OBJECT public: - TilesetEditorTileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, int numLayers) - : SelectablePixmapItem(16, 16, numLayers * 2, 2) { + TilesetEditorTileSelector(Tileset *primaryTileset, Tileset *secondaryTileset) + : SelectablePixmapItem(16, 16, Metatile::tileWidth(), Metatile::tileWidth()) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; this->numTilesWide = 16; @@ -18,15 +18,16 @@ public: this->paletteChanged = false; setAcceptHoverEvents(true); } - QPoint getSelectionDimensions(); - void draw(); + QSize getSelectionDimensions() const override; + void setMaxSelectionSize(int width, int height) override; + void draw() override; void select(uint16_t metatileId); void highlight(uint16_t metatileId); void setTilesets(Tileset*, Tileset*); void setPaletteId(int); void setTileFlips(bool, bool); QList getSelectedTiles(); - void setExternalSelection(int, int, QList, QList); + void setExternalSelection(int, int, const QList&); QPoint getTileCoordsOnWidget(uint16_t); QImage buildPrimaryTilesIndexedImage(); QImage buildSecondaryTilesIndexedImage(); @@ -36,18 +37,19 @@ public: bool showDivider = false; protected: - void mousePressEvent(QGraphicsSceneMouseEvent*); - void mouseMoveEvent(QGraphicsSceneMouseEvent*); - void mouseReleaseEvent(QGraphicsSceneMouseEvent*); - void hoverMoveEvent(QGraphicsSceneHoverEvent*); - void hoverLeaveEvent(QGraphicsSceneHoverEvent*); + void mousePressEvent(QGraphicsSceneMouseEvent*) override; + void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; + void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; + void hoverMoveEvent(QGraphicsSceneHoverEvent*) override; + void hoverLeaveEvent(QGraphicsSceneHoverEvent*) override; private: + QPixmap basePixmap; bool externalSelection; int externalSelectionWidth; int externalSelectionHeight; QList externalSelectedTiles; - QList externalSelectedPos; + QPoint prevCellPos = QPoint(-1,-1); Tileset *primaryTileset; Tileset *secondaryTileset; @@ -61,9 +63,9 @@ private: uint16_t getTileId(int x, int y); QPoint getTileCoords(uint16_t); QList getCurPaletteTable(); - QList buildSelectedTiles(int, int, QList); + QList buildSelectedTiles(int, int, const QList&); QImage buildImage(int tileIdStart, int numTiles); - + void updateBasePixmap(); void drawUnused(); signals: diff --git a/include/ui/uintspinbox.h b/include/ui/uintspinbox.h index bc217ec2..aa8bcb77 100644 --- a/include/ui/uintspinbox.h +++ b/include/ui/uintspinbox.h @@ -21,6 +21,7 @@ public: uint32_t value() const { return m_value; } uint32_t minimum() const { return m_minimum; } uint32_t maximum() const { return m_maximum; } + uint32_t singleStep() const { return m_singleStep; } QString prefix() const { return m_prefix; } int displayIntegerBase() const { return m_displayIntegerBase; } bool hasPadding() const { return m_hasPadding; } @@ -28,6 +29,7 @@ public: void setMinimum(uint32_t min); void setMaximum(uint32_t max); void setRange(uint32_t min, uint32_t max); + void setSingleStep(uint32_t val); void setPrefix(const QString &prefix); void setDisplayIntegerBase(int base); void setHasPadding(bool enabled); @@ -36,6 +38,7 @@ private: uint32_t m_minimum; uint32_t m_maximum; uint32_t m_value; + uint32_t m_singleStep; QString m_prefix; int m_displayIntegerBase; bool m_hasPadding; diff --git a/include/ui/updatepromoter.h b/include/ui/updatepromoter.h index de73bcdd..76157c4d 100644 --- a/include/ui/updatepromoter.h +++ b/include/ui/updatepromoter.h @@ -1,6 +1,8 @@ #ifndef UPDATEPROMOTER_H #define UPDATEPROMOTER_H +#ifdef QT_NETWORK_LIB + #include "network.h" #include @@ -47,4 +49,6 @@ signals: void changedPreferences(); }; +#endif // QT_NETWORK_LIB + #endif // UPDATEPROMOTER_H diff --git a/include/ui/wildmonsearch.h b/include/ui/wildmonsearch.h index ccf0fe94..4fe608b8 100644 --- a/include/ui/wildmonsearch.h +++ b/include/ui/wildmonsearch.h @@ -2,24 +2,11 @@ #define WILDMONSEARCH_H #include -#include -#include + +#include "numericsorttableitem.h" class Project; -class NumericSortTableItem : public QTableWidgetItem -{ -public: - explicit NumericSortTableItem(const QString &text) : QTableWidgetItem(text) {}; - -protected: - virtual bool operator<(const QTableWidgetItem &other) const override { - QCollator collator; - collator.setNumericMode(true); - return collator.compare(text(), other.text()) < 0; - } -}; - namespace Ui { class WildMonSearch; } diff --git a/porymap.pro b/porymap.pro index 4045e230..50aa83e2 100644 --- a/porymap.pro +++ b/porymap.pro @@ -4,13 +4,23 @@ # #------------------------------------------------- -QT += core gui qml network +QT += core gui qtHaveModule(charts) { QT += charts } else { warning("Qt module 'charts' not found, disabling chart features.") } +qtHaveModule(qml) { + QT += qml +} else { + warning("Qt module 'qml' not found, disabling plug-in features.") +} +qtHaveModule(network) { + QT += network +} else { + warning("Qt module 'network' not found, disabling network features.") +} greaterThan(QT_MAJOR_VERSION, 4): QT += widgets @@ -66,6 +76,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/scriptapi/apiutility.cpp \ src/scriptapi/scripting.cpp \ src/ui/aboutporymap.cpp \ + src/ui/checkeredbgscene.cpp \ src/ui/colorinputwidget.cpp \ src/ui/connectionslistitem.cpp \ src/ui/customattributesdialog.cpp \ @@ -115,6 +126,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/montabwidget.cpp \ src/ui/encountertablemodel.cpp \ src/ui/encountertabledelegates.cpp \ + src/ui/palettecolorsearch.cpp \ src/ui/paletteeditor.cpp \ src/ui/selectablepixmapitem.cpp \ src/ui/tileseteditor.cpp \ @@ -124,6 +136,7 @@ SOURCES += src/core/advancemapparser.cpp \ src/ui/regionmapeditor.cpp \ src/ui/newmapdialog.cpp \ src/ui/mapimageexporter.cpp \ + src/ui/metatileimageexporter.cpp \ src/ui/newtilesetdialog.cpp \ src/ui/flowlayout.cpp \ src/ui/mapruler.cpp \ @@ -180,6 +193,7 @@ HEADERS += include/core/advancemapparser.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ + include/ui/checkeredbgscene.h \ include/ui/connectionslistitem.h \ include/ui/customattributesdialog.h \ include/ui/customattributestable.h \ @@ -231,6 +245,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/encountertablemodel.h \ include/ui/encountertabledelegates.h \ include/ui/adjustingstackedwidget.h \ + include/ui/palettecolorsearch.h \ include/ui/paletteeditor.h \ include/ui/selectablepixmapitem.h \ include/ui/tileseteditor.h \ @@ -240,6 +255,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/regionmapeditor.h \ include/ui/newmapdialog.h \ include/ui/mapimageexporter.h \ + include/ui/metatileimageexporter.h \ include/ui/newtilesetdialog.h \ include/ui/overlay.h \ include/ui/flowlayout.h \ @@ -283,12 +299,14 @@ FORMS += forms/mainwindow.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ + forms/palettecolorsearch.ui \ forms/paletteeditor.ui \ forms/regionmapeditor.ui \ forms/newmapdialog.ui \ forms/aboutporymap.ui \ forms/newtilesetdialog.ui \ forms/mapimageexporter.ui \ + forms/metatileimageexporter.ui \ forms/shortcutseditor.ui \ forms/preferenceeditor.ui \ forms/regionmappropertiesdialog.ui \ diff --git a/resources/icons/swap_cursor.ico b/resources/icons/swap_cursor.ico new file mode 100644 index 00000000..d352b1f5 Binary files /dev/null and b/resources/icons/swap_cursor.ico differ diff --git a/resources/images.qrc b/resources/images.qrc index 2891ff3c..856a3fbe 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -44,6 +44,7 @@ icons/refresh.ico icons/shift_cursor.ico icons/shift.ico + icons/swap_cursor.ico icons/tall_grass.ico icons/warning.ico icons/minimap.ico diff --git a/src/config.cpp b/src/config.cpp index 4f5188bd..4322e4cd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -4,6 +4,7 @@ #include "map.h" #include "validator.h" #include "utility.h" +#include "metatile.h" #include #include #include @@ -283,7 +284,7 @@ int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &valu logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) { @@ -293,10 +294,13 @@ uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString & logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue)); result = defaultValue; } - return qMin(max, qMax(min, result)); + return qBound(min, result, max); } QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) { + if (value.isEmpty()) + return QColor(); + QColor color = QColor("#" + value); if (!color.isValid()) { logWarn(QString("Invalid config value for %1: '%2'. Must be a color in the format 'RRGGBB'. Using default value '%3'.").arg(key).arg(value).arg(defaultValue.name())); @@ -305,12 +309,72 @@ QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &val return color; } +QString KeyValueConfigBase::toConfigColor(const QColor &color) { + return color.isValid() ? color.name().remove("#") : QString(); // Our text config treats '#' as the start of a comment. +} + PorymapConfig porymapConfig; PorymapConfig::PorymapConfig() : KeyValueConfigBase(QStringLiteral("porymap.cfg")) { reset(); } +void PorymapConfig::reset() { + setRoot(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + this->recentProjects.clear(); + this->projectManuallyClosed = false; + this->reopenOnLaunch = true; + this->mapListTab = 0; + this->mapListEditGroupsEnabled = false; + this->mapListHideEmptyEnabled.clear(); + 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; + this->tilesetEditorMetatilesZoom = 30; + this->tilesetEditorTilesZoom = 30; + this->tilesetEditorLayerOrientation = Qt::Vertical; + this->showPlayerView = false; + this->showCursorTile = true; + this->showBorder = true; + this->showGrid = false; + this->showTilesetEditorMetatileGrid = false; + this->showTilesetEditorLayerGrid = true; + this->showTilesetEditorDivider = false; + this->showTilesetEditorRawAttributes = false; + this->showPaletteEditorUnusedColors = false; + this->monitorFiles = true; + this->tilesetCheckerboardFill = true; + this->newMapHeaderSectionExpanded = false; + this->theme = "default"; + this->wildMonChartTheme = ""; + this->textEditorOpenFolder = ""; + this->textEditorGotoLine = ""; + this->paletteEditorBitDepth = 24; + this->projectSettingsTab = 0; + this->scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly; + this->warpBehaviorWarningDisabled = false; + this->eventDeleteWarningDisabled = false; + this->eventOverlayEnabled = false; + this->checkForUpdates = true; + this->lastUpdateCheckTime = QDateTime(); + this->lastUpdateCheckVersion = porymapVersion; + this->rateLimitTimes.clear(); + this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; + this->shownInGameReloadMessage = false; + this->gridSettings = GridSettings(); + this->gridSettings.width = Metatile::pixelWidth(); + this->gridSettings.height = Metatile::pixelHeight(); + this->statusBarLogTypes = { LogType::LOG_ERROR, LogType::LOG_WARN }; + this->applicationFont = QFont(); + this->mapListFont = PorymapConfig::defaultMapListFont(); +} + void PorymapConfig::parseConfigKeyValue(QString key, QString value) { if (key == "recent_project") { this->recentProjects = value.split(",", Qt::SkipEmptyParts); @@ -391,6 +455,9 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetEditorMetatilesZoom = getConfigInteger(key, value, 10, 100, 30); } else if (key == "tileset_editor_tiles_zoom") { this->tilesetEditorTilesZoom = getConfigInteger(key, value, 10, 100, 30); + } else if (key == "tileset_editor_layer_orientation") { + // Being explicit here to avoid casting out-of-range values. + this->tilesetEditorLayerOrientation = (getConfigInteger(key, value) == static_cast(Qt::Horizontal)) ? Qt::Horizontal : Qt::Vertical; } else if (key == "show_player_view") { this->showPlayerView = getConfigBool(key, value); } else if (key == "show_cursor_tile") { @@ -407,6 +474,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->showTilesetEditorDivider = getConfigBool(key, value); } else if (key == "show_tileset_editor_raw_attributes") { this->showTilesetEditorRawAttributes = getConfigBool(key, value); + } else if (key == "show_palette_editor_unused_colors") { + this->showPaletteEditorUnusedColors = getConfigBool(key, value); } else if (key == "monitor_files") { this->monitorFiles = getConfigBool(key, value); } else if (key == "tileset_checkerboard_fill") { @@ -428,8 +497,13 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { } } else if (key == "project_settings_tab") { this->projectSettingsTab = getConfigInteger(key, value, 0); +#ifdef CONFIG_BACKWARDS_COMPATABILITY + // Old setting replaced by script_autocomplete_mode } else if (key == "load_all_event_scripts") { - this->loadAllEventScripts = getConfigBool(key, value); + this->scriptAutocompleteMode = getConfigBool(key, value) ? ScriptAutocompleteMode::All : ScriptAutocompleteMode::MapOnly; +#endif + } else if (key == "script_autocomplete_mode") { + this->scriptAutocompleteMode = static_cast(getConfigInteger(key, value, ScriptAutocompleteMode::MapOnly, ScriptAutocompleteMode::All)); } else if (key == "warp_behavior_warning_disabled") { this->warpBehaviorWarningDisabled = getConfigBool(key, value); } else if (key == "event_delete_warning_disabled") { @@ -534,6 +608,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("metatiles_zoom", QString::number(this->metatilesZoom)); map.insert("tileset_editor_metatiles_zoom", QString::number(this->tilesetEditorMetatilesZoom)); map.insert("tileset_editor_tiles_zoom", QString::number(this->tilesetEditorTilesZoom)); + map.insert("tileset_editor_layer_orientation", QString::number(this->tilesetEditorLayerOrientation)); map.insert("show_player_view", this->showPlayerView ? "1" : "0"); map.insert("show_cursor_tile", this->showCursorTile ? "1" : "0"); map.insert("show_border", this->showBorder ? "1" : "0"); @@ -542,6 +617,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("show_tileset_editor_layer_grid", this->showTilesetEditorLayerGrid ? "1" : "0"); map.insert("show_tileset_editor_divider", this->showTilesetEditorDivider ? "1" : "0"); map.insert("show_tileset_editor_raw_attributes", this->showTilesetEditorRawAttributes ? "1" : "0"); + map.insert("show_palette_editor_unused_colors", this->showPaletteEditorUnusedColors ? "1" : "0"); map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); map.insert("new_map_header_section_expanded", this->newMapHeaderSectionExpanded ? "1" : "0"); @@ -551,7 +627,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("text_editor_goto_line", this->textEditorGotoLine); map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth)); map.insert("project_settings_tab", QString::number(this->projectSettingsTab)); - map.insert("load_all_event_scripts", QString::number(this->loadAllEventScripts)); + map.insert("script_autocomplete_mode", QString::number(this->scriptAutocompleteMode)); map.insert("warp_behavior_warning_disabled", QString::number(this->warpBehaviorWarningDisabled)); map.insert("event_delete_warning_disabled", QString::number(this->eventDeleteWarningDisabled)); map.insert("event_overlay_enabled", QString::number(this->eventOverlayEnabled)); @@ -571,7 +647,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("grid_x", QString::number(this->gridSettings.offsetX)); map.insert("grid_y", QString::number(this->gridSettings.offsetY)); map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style)); - map.insert("grid_color", this->gridSettings.color.name().remove("#")); // Our text config treats '#' as the start of a comment. + map.insert("grid_color", toConfigColor(this->gridSettings.color)); QStringList logTypesStrings; for (const auto &type : this->statusBarLogTypes) { @@ -898,8 +974,13 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->tilesetsHaveCallback = getConfigBool(key, value); } else if (key == "tilesets_have_is_compressed") { this->tilesetsHaveIsCompressed = getConfigBool(key, value); +#ifdef CONFIG_BACKWARDS_COMPATABILITY + // Old setting replaced by transparency_color } else if (key == "set_transparent_pixels_black") { - this->setTransparentPixelsBlack = getConfigBool(key, value); + this->transparencyColor = getConfigBool(key, value) ? QColor(Qt::black) : QColor(); +#endif + } else if (key == "transparency_color") { + this->transparencyColor = getConfigColor(key, value); } else if (key == "preserve_matching_only_data") { this->preserveMatchingOnlyData = getConfigBool(key, value); } else if (key == "event_icon_path_object") { @@ -940,6 +1021,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->maxEventsPerGroup = getConfigInteger(key, value, 1, INT_MAX, 255); } else if (key == "forced_major_version") { this->forcedMajorVersion = getConfigInteger(key, value); + } else if (key == "metatile_selector_width") { + this->metatileSelectorWidth = getConfigInteger(key, value, 1, INT_MAX, 8); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->filepath()).arg(key)); } @@ -1005,7 +1088,7 @@ QMap ProjectConfig::getKeyValueMap() { } map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback)); map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed)); - map.insert("set_transparent_pixels_black", QString::number(this->setTransparentPixelsBlack)); + map.insert("transparency_color", toConfigColor(this->transparencyColor)); map.insert("preserve_matching_only_data", QString::number(this->preserveMatchingOnlyData)); map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize)); map.insert("metatile_behavior_mask", Util::toHexString(this->metatileBehaviorMask)); @@ -1049,6 +1132,7 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("warp_behaviors", warpBehaviorStrs.join(",")); map.insert("max_events_per_group", QString::number(this->maxEventsPerGroup)); map.insert("forced_major_version", QString::number(this->forcedMajorVersion)); + map.insert("metatile_selector_width", QString::number(this->metatileSelectorWidth)); return map; } @@ -1182,7 +1266,7 @@ int ProjectConfig::getNumLayersInMetatile() { } int ProjectConfig::getNumTilesInMetatile() { - return this->tripleLayerMetatilesEnabled ? 12 : 8; + return getNumLayersInMetatile() * Metatile::tilesPerLayer(); } void ProjectConfig::setEventIconPath(Event::Group group, const QString &path) { diff --git a/src/core/advancemapparser.cpp b/src/core/advancemapparser.cpp index 6a8428ec..04348e96 100644 --- a/src/core/advancemapparser.cpp +++ b/src/core/advancemapparser.cpp @@ -73,15 +73,21 @@ Layout *AdvanceMapParser::parseLayout(const QString &filepath, bool *error, cons const QList tilesets = project->tilesetLabelsOrdered; - if (mapPrimaryTilesetNum > tilesets.size()) - mapLayout->tileset_primary_label = project->getDefaultPrimaryTilesetLabel(); - else - mapLayout->tileset_primary_label = tilesets.at(mapPrimaryTilesetNum); + const QString defaultPrimaryTileset = project->getDefaultPrimaryTilesetLabel(); + QString primaryTilesetLabel = tilesets.value(mapPrimaryTilesetNum, defaultPrimaryTileset); + if (!project->primaryTilesetLabels.contains(primaryTilesetLabel)) { + // AdvanceMap's primary tileset value points to a secondary tileset. Ignore it. + primaryTilesetLabel = defaultPrimaryTileset; + } + const QString defaultSecondaryTileset = project->getDefaultSecondaryTilesetLabel(); + QString secondaryTilesetLabel = tilesets.value(mapSecondaryTilesetNum, defaultSecondaryTileset); + if (!project->secondaryTilesetLabels.contains(secondaryTilesetLabel)) { + // AdvanceMap's secondary tileset value points to a primary tileset. Ignore it. + secondaryTilesetLabel = defaultSecondaryTileset; + } - if (mapSecondaryTilesetNum > tilesets.size()) - mapLayout->tileset_secondary_label = project->getDefaultSecondaryTilesetLabel(); - else - mapLayout->tileset_secondary_label = tilesets.at(mapSecondaryTilesetNum); + mapLayout->tileset_primary_label = primaryTilesetLabel; + mapLayout->tileset_secondary_label = secondaryTilesetLabel; mapLayout->blockdata = blockdata; @@ -131,7 +137,7 @@ QList AdvanceMapParser::parseMetatiles(const QString &filepath, bool } int attrSize = Metatile::getDefaultAttributesSize(version); - int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary(); + int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesSecondary(); int numMetatiles = static_cast(in.at(0)) | (static_cast(in.at(1)) << 8) | (static_cast(in.at(2)) << 16) | diff --git a/src/core/events.cpp b/src/core/events.cpp index a4ab33a9..194a7ab1 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -3,6 +3,7 @@ #include "eventframes.h" #include "project.h" #include "config.h" +#include "metatile.h" Event* Event::create(Event::Type type) { switch (type) { @@ -23,6 +24,14 @@ Event::~Event() { delete this->eventFrame; } +int Event::getPixelX() const { + return (this->x * Metatile::pixelWidth()) - qMax(0, (this->pixmap.width() - Metatile::pixelWidth()) / 2); +} + +int Event::getPixelY() const { + return (this->y * Metatile::pixelHeight()) - qMax(0, this->pixmap.height() - Metatile::pixelHeight()); +} + EventFrame *Event::getEventFrame() { if (!this->eventFrame) createEventFrame(); return this->eventFrame; @@ -54,6 +63,16 @@ void Event::modify() { this->map->modify(); } +QString Event::groupToJsonKey(Event::Group group) { + static const QMap map = { + {Event::Group::Object, "object_events"}, + {Event::Group::Warp, "warp_events"}, + {Event::Group::Coord, "coord_events"}, + {Event::Group::Bg, "bg_events"}, + }; + return map.value(group); +} + const QMap groupToStringMap = { {Event::Group::Object, "Object"}, {Event::Group::Warp, "Warp"}, diff --git a/src/core/map.cpp b/src/core/map.cpp index 6ced8d4c..1f3b5916 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -4,6 +4,7 @@ #include "scripting.h" #include "utility.h" #include "editcommands.h" +#include "project.h" #include #include @@ -15,9 +16,6 @@ Map::Map(QObject *parent) : QObject(parent) { m_editHistory = new QUndoStack(this); - m_scriptFileWatcher = new QFileSystemWatcher(this); - connect(m_scriptFileWatcher, &QFileSystemWatcher::fileChanged, this, &Map::invalidateScripts); - resetEvents(); m_header = new MapHeader(this); @@ -33,8 +31,6 @@ Map::Map(const Map &other, QObject *parent) : Map(parent) { *m_header = *other.m_header; m_layout = other.m_layout; m_isPersistedToFile = false; - m_metatileLayerOrder = other.m_metatileLayerOrder; - m_metatileLayerOpacity = other.m_metatileLayerOpacity; // Copy events for (auto i = other.m_events.constBegin(); i != other.m_events.constEnd(); i++) { @@ -67,44 +63,28 @@ QString Map::mapConstantFromName(const QString &name) { return projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) + Util::toDefineCase(name); } -int Map::getWidth() const { - return m_layout ? m_layout->getWidth() : 0; -} - -int Map::getHeight() const { - return m_layout ? m_layout->getHeight() : 0; -} - -int Map::getBorderWidth() const { - return m_layout ? m_layout->getBorderWidth() : 0; -} - -int Map::getBorderHeight() const { - return m_layout ? m_layout->getBorderHeight() : 0; -} - // 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, Layout * fromLayout) const { int x = 0, y = 0; - int w = getWidth(), h = getHeight(); + int w = pixelWidth(), h = pixelHeight(); - QMargins viewDistance = Project::getMetatileViewDistance(); + QMargins viewDistance = Project::getPixelViewDistance(); if (direction == "up") { h = qMin(h, viewDistance.top()); - y = getHeight() - h; + y = pixelHeight() - h; } else if (direction == "down") { h = qMin(h, viewDistance.bottom()); } else if (direction == "left") { w = qMin(w, viewDistance.left()); - x = getWidth() - w; + x = pixelWidth() - w; } else if (direction == "right") { w = qMin(w, viewDistance.right()); } else if (MapConnection::isDiving(direction)) { if (fromLayout) { - w = qMin(w, fromLayout->getWidth()); - h = qMin(h, fromLayout->getHeight()); + w = qMin(w, fromLayout->pixelWidth()); + h = qMin(h, fromLayout->pixelHeight()); } } else { // Unknown direction @@ -127,7 +107,7 @@ QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) { fromLayout = nullptr; QPixmap connectionPixmap = m_layout->render(true, fromLayout, bounds); - return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); + return connectionPixmap.copy(bounds); } void Map::openScript(const QString &label) { @@ -143,7 +123,12 @@ void Map::setSharedScriptsMap(const QString &sharedScriptsMap) { void Map::invalidateScripts() { m_scriptsLoaded = false; - m_scriptFileWatcher->removePaths(m_scriptFileWatcher->files()); + + // m_scriptFileWatcher is a QPointer so clearing it shouldn't be necessary, + // but it's possible that Map::getScriptLabels will be called before events are processed. + delete m_scriptFileWatcher; + m_scriptFileWatcher = nullptr; + emit scriptsModified(); } @@ -158,14 +143,32 @@ QStringList Map::getScriptLabels(Event::Group group) { .arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/")) .arg(m_name) .arg(error)); + + // Setting this flag here (and below) lets us skip some steps and logging if we already know it failed. + // Script labels may be re-requested often, so we don't want to fill the log with warnings. m_loggedScriptsFileError = true; } - if (!m_scriptFileWatcher->files().contains(scriptsFilepath) && !m_scriptFileWatcher->addPath(scriptsFilepath) && !m_loggedScriptsFileError) { - logWarn(QString("Failed to add scripts file '%1' to file watcher for %2.") - .arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/")) - .arg(m_name)); - m_loggedScriptsFileError = true; + if (porymapConfig.monitorFiles && !m_loggedScriptsFileError) { + if (!m_scriptFileWatcher) { + // Only create the file watcher when it's first needed (even an empty QFileSystemWatcher will consume system resources). + // The other option would be for Porymap to have a single global QFileSystemWatcher, but that has complications of its own. + m_scriptFileWatcher = new QFileSystemWatcher(this); + connect(m_scriptFileWatcher, &QFileSystemWatcher::fileChanged, this, &Map::invalidateScripts); + } + // m_scriptFileWatcher can stil be nullptr here if the inotify limit was reached on Linux. + // Porymap isn't using enough resources in general for this to be a problem, but the user may have lowered the inotify limit. + if (!m_scriptFileWatcher) { + logWarn(QString("Failed to add scripts file '%1' to file watcher for %2: Reached system resource limit.") + .arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/")) + .arg(m_name)); + m_loggedScriptsFileError = true; + } else if (!m_scriptFileWatcher->files().contains(scriptsFilepath) && !m_scriptFileWatcher->addPath(scriptsFilepath)) { + logWarn(QString("Failed to add scripts file '%1' to file watcher for %2.") + .arg(Util::stripPrefix(scriptsFilepath, projectConfig.projectDir() + "/")) + .arg(m_name)); + m_loggedScriptsFileError = true; + } } m_scriptsLoaded = true; @@ -278,14 +281,23 @@ int Map::getNumEvents(Event::Group group) const { if (group == Event::Group::None) { // Total number of events int numEvents = 0; - for (auto i = m_events.constBegin(); i != m_events.constEnd(); i++) { - numEvents += i.value().length(); + for (auto it = m_events.constBegin(); it != m_events.constEnd(); it++) { + numEvents += it.value().length(); } return numEvents; } return m_events[group].length(); } +bool Map::hasEvents() const { + for (auto it = m_events.constBegin(); it != m_events.constEnd(); it++) { + if (!it.value().isEmpty()) { + return true; + } + } + return false; +} + void Map::removeEvent(Event *event) { for (auto i = m_events.begin(); i != m_events.end(); i++) { i.value().removeAll(event); diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index d37a8c74..e1478fad 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -72,20 +72,20 @@ QPixmap MapConnection::render() const { // For right/down connections this is offset by the dimensions of the parent map. // For left/up connections this is offset by the dimensions of the target map. // If 'clipped' is true, only the rendered dimensions of the target map will be used, rather than its full dimensions. -QPoint MapConnection::relativePos(bool clipped) const { +QPoint MapConnection::relativePixelPos(bool clipped) const { int x = 0, y = 0; if (m_direction == "right") { - if (m_parentMap) x = m_parentMap->getWidth(); - y = m_offset; + if (m_parentMap) x = m_parentMap->pixelWidth(); + y = m_offset * Metatile::pixelHeight(); } else if (m_direction == "down") { - x = m_offset; - if (m_parentMap) y = m_parentMap->getHeight(); + x = m_offset * Metatile::pixelWidth(); + if (m_parentMap) y = m_parentMap->pixelHeight(); } else if (m_direction == "left") { - if (targetMap()) x = !clipped ? -targetMap()->getWidth() : -targetMap()->getConnectionRect(m_direction).width(); - y = m_offset; + if (targetMap()) x = !clipped ? -targetMap()->pixelWidth() : -targetMap()->getConnectionRect(m_direction).width(); + y = m_offset * Metatile::pixelHeight(); } else if (m_direction == "up") { - x = m_offset; - if (targetMap()) y = !clipped ? -targetMap()->getHeight() : -targetMap()->getConnectionRect(m_direction).height(); + x = m_offset * Metatile::pixelWidth(); + if (targetMap()) y = !clipped ? -targetMap()->pixelHeight() : -targetMap()->getConnectionRect(m_direction).height(); } return QPoint(x, y); } diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 4108400f..44616ab6 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -5,6 +5,11 @@ #include "scripting.h" #include "imageproviders.h" #include "utility.h" +#include "project.h" +#include "layoutpixmapitem.h" + +QList Layout::s_globalMetatileLayerOrder; +QList Layout::s_globalMetatileLayerOpacity; Layout::Layout(const Layout &other) : Layout() { copyFrom(&other); @@ -55,6 +60,10 @@ bool Layout::isWithinBounds(int x, int y) const { return (x >= 0 && x < this->getWidth() && y >= 0 && y < this->getHeight()); } +bool Layout::isWithinBounds(const QPoint &pos) const { + return isWithinBounds(pos.x(), pos.y()); +} + bool Layout::isWithinBounds(const QRect &rect) const { return rect.left() >= 0 && rect.right() < this->getWidth() && rect.top() >= 0 && rect.bottom() < this->getHeight(); } @@ -85,15 +94,14 @@ QMargins Layout::getBorderMargins() const { return distance; } -// Get a rectangle that represents (in pixels) the layout's map area and the visible area of its border. -// At maximum, this is equal to the map size plus the border margins. -// If the border is large (and so beyond player the view) it may be smaller than that. +// Get a rectangle that represents (in pixels) the layout's map area + the distance the player can see. +// Note that this may be smaller than the map area + the size of the border for layouts with large border dimensions. QRect Layout::getVisibleRect() const { - QRect area = QRect(0, 0, this->width * 16, this->height * 16); - return area += (Project::getMetatileViewDistance() * 16); + QRect area = QRect(0, 0, this->pixelWidth(), this->pixelHeight()); + return area += Project::getPixelViewDistance(); } -bool Layout::getBlock(int x, int y, Block *out) { +bool Layout::getBlock(int x, int y, Block *out) const { if (isWithinBounds(x, y)) { int i = y * getWidth() + x; *out = this->blockdata.value(i); @@ -128,6 +136,20 @@ void Layout::setBlockdata(Blockdata newBlockdata, bool enableScriptCallback) { } } +uint16_t Layout::getMetatileId(int x, int y) const { + Block block; + return getBlock(x, y, &block) ? block.metatileId() : 0; +} + +bool Layout::setMetatileId(int x, int y, uint16_t metatileId, bool enableScriptCallback) { + Block block; + if (!getBlock(x, y, &block)) { + return false; + } + setBlock(x, y, Block(metatileId, block.collision(), block.elevation()), enableScriptCallback); + return true; +} + void Layout::clearBorderCache() { this->cached_border.clear(); } @@ -337,15 +359,13 @@ void Layout::magicFillCollisionElevation(int initialX, int initialY, uint16_t co } } -QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { +QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, const QRect &bounds) { bool changed_any = false; - int width_ = getWidth(); - int height_ = getHeight(); - if (this->image.isNull() || this->image.width() != width_ * 16 || this->image.height() != height_ * 16) { - this->image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (this->image.isNull() || this->image.width() != pixelWidth() || this->image.height() != pixelHeight()) { + this->image = QImage(pixelWidth(), pixelHeight(), QImage::Format_RGBA8888); changed_any = true; } - if (this->blockdata.isEmpty() || !width_ || !height_) { + if (this->blockdata.isEmpty() || this->width == 0 || this->height == 0) { this->pixmap = this->pixmap.fromImage(this->image); return this->pixmap; } @@ -361,9 +381,9 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { if (!ignoreCache && !layoutBlockChanged(i, this->blockdata, this->cached_blockdata)) { continue; } - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - if (bounds.isValid() && !bounds.contains(map_x, map_y)) { + int x = this->width ? ((i % this->width) * Metatile::pixelWidth()) : 0; + int y = this->width ? ((i / this->width) * Metatile::pixelHeight()) : 0; + if (bounds.isValid() && !bounds.contains(x, y)) { continue; } @@ -376,14 +396,12 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { metatileId, fromLayout ? fromLayout->tileset_primary : this->tileset_primary, fromLayout ? fromLayout->tileset_secondary : this->tileset_secondary, - metatileLayerOrder, - metatileLayerOpacity + metatileLayerOrder(), + metatileLayerOpacity() ); imageCache.insert(metatileId, metatileImage); } - - QPoint metatileOrigin = QPoint(map_x * 16, map_y * 16); - painter.drawImage(metatileOrigin, metatileImage); + painter.drawImage(x, y, metatileImage); changed_any = true; } painter.end(); @@ -397,13 +415,11 @@ QPixmap Layout::render(bool ignoreCache, Layout *fromLayout, QRect bounds) { QPixmap Layout::renderCollision(bool ignoreCache) { bool changed_any = false; - int width_ = getWidth(); - int height_ = getHeight(); - if (collision_image.isNull() || collision_image.width() != width_ * 16 || collision_image.height() != height_ * 16) { - collision_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (collision_image.isNull() || collision_image.width() != pixelWidth() || collision_image.height() != pixelHeight()) { + collision_image = QImage(pixelWidth(), pixelHeight(), QImage::Format_RGBA8888); changed_any = true; } - if (this->blockdata.isEmpty() || !width_ || !height_) { + if (this->blockdata.isEmpty() || this->width == 0 || this->height == 0) { collision_pixmap = collision_pixmap.fromImage(collision_image); return collision_pixmap; } @@ -415,10 +431,9 @@ QPixmap Layout::renderCollision(bool ignoreCache) { changed_any = true; Block block = this->blockdata.at(i); QImage collision_metatile_image = getCollisionMetatileImage(block); - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - QPoint metatile_origin = QPoint(map_x * 16, map_y * 16); - painter.drawImage(metatile_origin, collision_metatile_image); + int x = this->width ? ((i % this->width) * Metatile::pixelWidth()) : 0; + int y = this->width ? ((i / this->width) * Metatile::pixelHeight()) : 0; + painter.drawImage(x, y, collision_metatile_image); } painter.end(); cacheCollision(); @@ -430,14 +445,14 @@ QPixmap Layout::renderCollision(bool ignoreCache) { QPixmap Layout::renderBorder(bool ignoreCache) { bool changed_any = false, border_resized = false; - int width_ = getBorderWidth(); - int height_ = getBorderHeight(); + int pixelWidth = this->border_width * Metatile::pixelWidth(); + int pixelHeight = this->border_height * Metatile::pixelHeight(); if (this->border_image.isNull()) { - this->border_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + this->border_image = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888); changed_any = true; } - if (this->border_image.width() != width_ * 16 || this->border_image.height() != height_ * 16) { - this->border_image = QImage(width_ * 16, height_ * 16, QImage::Format_RGBA8888); + if (this->border_image.width() != pixelWidth || this->border_image.height() != pixelHeight) { + this->border_image = QImage(pixelWidth, pixelHeight, QImage::Format_RGBA8888); border_resized = true; } if (this->border.isEmpty()) { @@ -453,10 +468,10 @@ QPixmap Layout::renderBorder(bool ignoreCache) { changed_any = true; Block block = this->border.at(i); uint16_t metatileId = block.metatileId(); - QImage metatile_image = getMetatileImage(metatileId, this->tileset_primary, this->tileset_secondary, metatileLayerOrder, metatileLayerOpacity); - int map_y = width_ ? i / width_ : 0; - int map_x = width_ ? i % width_ : 0; - painter.drawImage(QPoint(map_x * 16, map_y * 16), metatile_image); + QImage metatile_image = getMetatileImage(metatileId, this); + int x = this->border_width ? ((i % this->border_width) * Metatile::pixelWidth()) : 0; + int y = this->border_width ? ((i / this->border_width) * Metatile::pixelHeight()) : 0; + painter.drawImage(x, y, metatile_image); } painter.end(); if (changed_any) { @@ -531,6 +546,7 @@ bool Layout::loadBorder(const QString &root) { logError(QString("Failed to load border for %1 from '%2': %3").arg(this->name).arg(path).arg(error)); return false; } + this->border = blockdata; // 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG if (this->border_width <= 0) { @@ -540,10 +556,6 @@ bool Layout::loadBorder(const QString &root) { this->border_height = DEFAULT_BORDER_HEIGHT; } - this->border = blockdata; - this->lastCommitBlocks.border = blockdata; - this->lastCommitBlocks.borderDimensions = QSize(this->border_width, this->border_height); - int expectedSize = this->border_width * this->border_height; if (this->border.count() != expectedSize) { logWarn(QString("%1 border blockdata length %2 does not match dimensions %3x%4 (should be %5). Resizing border blockdata.") @@ -554,6 +566,10 @@ bool Layout::loadBorder(const QString &root) { .arg(expectedSize)); this->border.resize(expectedSize); } + + this->lastCommitBlocks.border = this->border; + this->lastCommitBlocks.borderDimensions = QSize(this->border_width, this->border_height); + return true; } @@ -570,10 +586,7 @@ bool Layout::loadBlockdata(const QString &root) { logError(QString("Failed to load blockdata for %1 from '%2': %3").arg(this->name).arg(path).arg(error)); return false; } - this->blockdata = blockdata; - this->lastCommitBlocks.blocks = blockdata; - this->lastCommitBlocks.layoutDimensions = QSize(this->width, this->height); int expectedSize = this->width * this->height; if (expectedSize <= 0) { @@ -589,6 +602,10 @@ bool Layout::loadBlockdata(const QString &root) { .arg(expectedSize)); this->blockdata.resize(expectedSize); } + + this->lastCommitBlocks.blocks = this->blockdata; + this->lastCommitBlocks.layoutDimensions = QSize(this->width, this->height); + return true; } diff --git a/src/core/metatile.cpp b/src/core/metatile.cpp index 073fd867..00fff2ba 100644 --- a/src/core/metatile.cpp +++ b/src/core/metatile.cpp @@ -36,8 +36,10 @@ int Metatile::getIndexInTileset(int metatileId) { } QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) { - int x = static_cast(pixelCoord.x()) / 16; - int y = static_cast(pixelCoord.y()) / 16; + int x = static_cast(pixelCoord.x()) / pixelWidth(); + int y = static_cast(pixelCoord.y()) / pixelHeight(); + if (pixelCoord.x() < 0) x--; + if (pixelCoord.y() < 0) y--; return QPoint(x, y); } @@ -46,13 +48,18 @@ QString Metatile::getMetatileIdString(uint16_t metatileId) { return Util::toHexString(metatileId, numMetatileIdChars); }; -QString Metatile::getMetatileIdStrings(const QList metatileIds) { +QString Metatile::getMetatileIdStrings(const QList &metatileIds) { QStringList metatiles; for (auto metatileId : metatileIds) metatiles << Metatile::getMetatileIdString(metatileId); return metatiles.join(","); }; +QString Metatile::getLayerName(int layerNum) { + static const QStringList layerTitles = { "Bottom", "Middle", "Top"}; + return layerTitles.value(layerNum); +} + // Read and pack together this metatile's attributes. uint32_t Metatile::getAttributes() const { uint32_t data = 0; diff --git a/src/core/network.cpp b/src/core/network.cpp index 4f2a1c19..ce0d5124 100644 --- a/src/core/network.cpp +++ b/src/core/network.cpp @@ -1,3 +1,4 @@ +#ifdef QT_NETWORK_LIB #include "network.h" #include "config.h" @@ -148,3 +149,5 @@ void NetworkAccessManager::processReply(QNetworkReply * reply, NetworkReplyData cacheEntry->data = data->m_body = reply->readAll(); } + +#endif // QT_NETWORK_LIB diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index cc7efb55..0aa1f7d6 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -8,6 +8,7 @@ #include #include +#include Tileset::Tileset(const Tileset &other) @@ -20,15 +21,15 @@ Tileset::Tileset(const Tileset &other) metatile_attrs_label(other.metatile_attrs_label), metatile_attrs_path(other.metatile_attrs_path), tilesImagePath(other.tilesImagePath), - tilesImage(other.tilesImage.copy()), palettePaths(other.palettePaths), metatileLabels(other.metatileLabels), palettes(other.palettes), palettePreviews(other.palettePreviews), + m_tilesImage(other.m_tilesImage.copy()), m_hasUnsavedTilesImage(other.m_hasUnsavedTilesImage) { - for (auto tile : other.tiles) { - tiles.append(tile.copy()); + for (auto tile : other.m_tiles) { + m_tiles.append(tile.copy()); } for (auto *metatile : other.m_metatiles) { @@ -46,15 +47,15 @@ Tileset &Tileset::operator=(const Tileset &other) { metatile_attrs_label = other.metatile_attrs_label; metatile_attrs_path = other.metatile_attrs_path; tilesImagePath = other.tilesImagePath; - tilesImage = other.tilesImage.copy(); + m_tilesImage = other.m_tilesImage.copy(); palettePaths = other.palettePaths; metatileLabels = other.metatileLabels; palettes = other.palettes; palettePreviews = other.palettePreviews; - tiles.clear(); - for (auto tile : other.tiles) { - tiles.append(tile.copy()); + m_tiles.clear(); + for (auto tile : other.m_tiles) { + m_tiles.append(tile.copy()); } clearMetatiles(); @@ -94,7 +95,50 @@ void Tileset::resizeMetatiles(int newNumMetatiles) { } } +uint16_t Tileset::firstMetatileId() const { + return this->is_secondary ? Project::getNumMetatilesPrimary() : 0; +} + +uint16_t Tileset::lastMetatileId() const { + return qMax(1, firstMetatileId() + m_metatiles.length()) - 1; +} + +int Tileset::maxMetatiles() const { + return this->is_secondary ? Project::getNumMetatilesSecondary() : Project::getNumMetatilesPrimary(); +} + +uint16_t Tileset::firstTileId() const { + return this->is_secondary ? Project::getNumTilesPrimary() : 0; +} + +uint16_t Tileset::lastTileId() const { + return qMax(1, firstMetatileId() + m_tiles.length()) - 1; +} + +int Tileset::maxTiles() const { + return this->is_secondary ? Project::getNumTilesSecondary() : Project::getNumTilesPrimary(); +} + +Tileset* Tileset::getPaletteTileset(int paletteId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getPaletteTileset(paletteId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +const Tileset* Tileset::getPaletteTileset(int paletteId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + if (paletteId < Project::getNumPalettesPrimary()) { + return primaryTileset; + } else if (paletteId < Project::getNumPalettesTotal()) { + return secondaryTileset; + } else { + return nullptr; + } +} + Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getTileTileset(tileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +// Get the tileset *expected* to contain the given 'tileId'. Note that this does not mean the tile actually exists in that tileset. +const Tileset* Tileset::getTileTileset(int tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { if (tileId < Project::getNumTilesPrimary()) { return primaryTileset; } else if (tileId < Project::getNumTilesTotal()) { @@ -105,6 +149,11 @@ Tileset* Tileset::getTileTileset(int tileId, Tileset *primaryTileset, Tileset *s } Tileset* Tileset::getMetatileTileset(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { + return const_cast(getMetatileTileset(metatileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +// Get the tileset *expected* to contain the given 'metatileId'. Note that this does not mean the metatile actually exists in that tileset. +const Tileset* Tileset::getMetatileTileset(int metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { if (metatileId < Project::getNumMetatilesPrimary()) { return primaryTileset; } else if (metatileId < Project::getNumMetatilesTotal()) { @@ -115,7 +164,11 @@ Tileset* Tileset::getMetatileTileset(int metatileId, Tileset *primaryTileset, Ti } Metatile* Tileset::getMetatile(int metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset); + return const_cast(getMetatile(metatileId, static_cast(primaryTileset), static_cast(secondaryTileset))); +} + +const Metatile* Tileset::getMetatile(int metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + const Tileset *tileset = Tileset::getMetatileTileset(metatileId, primaryTileset, secondaryTileset); if (!tileset) { return nullptr; } @@ -200,46 +253,48 @@ QString Tileset::getMetatileLabelPrefix() QString Tileset::getMetatileLabelPrefix(const QString &name) { // Default is "gTileset_Name" --> "METATILE_Name_" - const QString tilesetPrefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); const QString labelPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix); - return QString("%1%2_").arg(labelPrefix).arg(QString(name).replace(tilesetPrefix, "")); + return QString("%1%2_").arg(labelPrefix).arg(Tileset::stripPrefix(name)); } -bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - if (metatileId >= Project::getNumMetatilesTotal()) - return false; - - if (metatileId < Project::getNumMetatilesPrimary() && metatileId >= primaryTileset->numMetatiles()) - return false; - - if (metatileId >= Project::getNumMetatilesPrimary() + secondaryTileset->numMetatiles()) - return false; - - return true; +bool Tileset::metatileIsValid(uint16_t metatileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + return (primaryTileset && primaryTileset->containsMetatileId(metatileId)) + || (secondaryTileset && secondaryTileset->containsMetatileId(metatileId)); } -QList> Tileset::getBlockPalettes(Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) { +QList> Tileset::getBlockPalettes(const Tileset *primaryTileset, const Tileset *secondaryTileset, bool useTruePalettes) { QList> palettes; - auto primaryPalettes = useTruePalettes ? primaryTileset->palettes : primaryTileset->palettePreviews; + + QList> primaryPalettes; + if (primaryTileset) { + primaryPalettes = useTruePalettes ? primaryTileset->palettes : primaryTileset->palettePreviews; + } for (int i = 0; i < Project::getNumPalettesPrimary(); i++) { - palettes.append(primaryPalettes.at(i)); + palettes.append(primaryPalettes.value(i)); + } + + QList> secondaryPalettes; + if (secondaryTileset) { + secondaryPalettes = useTruePalettes ? secondaryTileset->palettes : secondaryTileset->palettePreviews; } - auto secondaryPalettes = useTruePalettes ? secondaryTileset->palettes : secondaryTileset->palettePreviews; for (int i = Project::getNumPalettesPrimary(); i < Project::getNumPalettesTotal(); i++) { - palettes.append(secondaryPalettes.at(i)); + palettes.append(secondaryPalettes.value(i)); } + return palettes; } -QList Tileset::getPalette(int paletteId, Tileset *primaryTileset, Tileset *secondaryTileset, bool useTruePalettes) { +QList Tileset::getPalette(int paletteId, const Tileset *primaryTileset, const Tileset *secondaryTileset, bool useTruePalettes) { QList paletteTable; - Tileset *tileset = paletteId < Project::getNumPalettesPrimary() + const Tileset *tileset = paletteId < Project::getNumPalettesPrimary() ? primaryTileset : secondaryTileset; - auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews; + if (!tileset) { + return paletteTable; + } - if (paletteId < 0 || paletteId >= palettes.length()){ - logError(QString("Invalid tileset palette id '%1' requested.").arg(paletteId)); + auto palettes = useTruePalettes ? tileset->palettes : tileset->palettePreviews; + if (paletteId < 0 || paletteId >= palettes.length()) { return paletteTable; } @@ -316,7 +371,7 @@ bool Tileset::appendToGraphics(const QString &filepath, const QString &friendlyN dataString.append(QString("\t.incbin \"%1\"\n").arg(tilesPath)); } else { // Append to C file - dataString.append(QString("const u16 gTilesetPalettes_%1[][16] =\n{\n").arg(friendlyName)); + dataString.append(QString("const u16 gTilesetPalettes_%1[][%2] =\n{\n").arg(friendlyName).arg(Tileset::numColorsPerPalette())); for (int i = 0; i < Project::getNumPalettesTotal(); i++) dataString.append(QString(" INCBIN_U16(\"%1%2%3\"),\n").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0')).arg(palettesExt)); dataString.append("};\n"); @@ -373,8 +428,7 @@ QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary) : projectConfig.getFilePath(ProjectFilePath::data_primary_tilesets_folders); static const QRegularExpression re("([a-z])([A-Z0-9])"); - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); - return basePath + tilesetName.replace(prefix, "").replace(re, "\\1_\\2").toLower(); + return basePath + Tileset::stripPrefix(tilesetName).replace(re, "\\1_\\2").toLower(); } // Get the expected positions of the members in struct Tileset. @@ -400,17 +454,25 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) bool Tileset::loadMetatiles() { clearMetatiles(); - QFile metatiles_file(this->metatiles_path); - if (!metatiles_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + QFile file(this->metatiles_path); + if (!file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading: %2").arg(this->metatiles_path).arg(file.errorString())); return false; } - QByteArray data = metatiles_file.readAll(); + QByteArray data = file.readAll(); int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - int bytesPerMetatile = 2 * tilesPerMetatile; - int num_metatiles = data.length() / bytesPerMetatile; - for (int i = 0; i < num_metatiles; i++) { + int bytesPerMetatile = Tile::sizeInBytes() * tilesPerMetatile; + int numMetatiles = data.length() / bytesPerMetatile; + if (numMetatiles > maxMetatiles()) { + logWarn(QString("%1 metatile count %2 exceeds limit of %3. Additional metatiles will be ignored.") + .arg(this->name) + .arg(numMetatiles) + .arg(maxMetatiles())); + numMetatiles = maxMetatiles(); + } + + for (int i = 0; i < numMetatiles; i++) { auto metatile = new Metatile; int index = i * bytesPerMetatile; for (int j = 0; j < tilesPerMetatile; j++) { @@ -424,9 +486,9 @@ bool Tileset::loadMetatiles() { } bool Tileset::saveMetatiles() { - QFile metatiles_file(this->metatiles_path); - if (!metatiles_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - logError(QString("Could not open '%1' for writing: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + QFile file(this->metatiles_path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing: %2").arg(this->metatiles_path).arg(file.errorString())); return false; } @@ -434,31 +496,40 @@ bool Tileset::saveMetatiles() { int numTiles = projectConfig.getNumTilesInMetatile(); for (const auto &metatile : m_metatiles) { for (int i = 0; i < numTiles; i++) { - uint16_t tile = metatile->tiles.at(i).rawValue(); + uint16_t tile = metatile->tiles.value(i).rawValue(); data.append(static_cast(tile)); data.append(static_cast(tile >> 8)); } } - metatiles_file.write(data); + file.write(data); return true; } bool Tileset::loadMetatileAttributes() { - QFile attrs_file(this->metatile_attrs_path); - if (!attrs_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + QFile file(this->metatile_attrs_path); + if (!file.open(QIODevice::ReadOnly)) { + logError(QString("Could not open '%1' for reading: %2").arg(this->metatile_attrs_path).arg(file.errorString())); return false; } - QByteArray data = attrs_file.readAll(); + QByteArray data = file.readAll(); int attrSize = projectConfig.metatileAttributesSize; int numMetatiles = m_metatiles.length(); int numMetatileAttrs = data.length() / attrSize; - if (numMetatiles != numMetatileAttrs) { - logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(numMetatiles).arg(numMetatileAttrs).arg(this->name)); + if (numMetatileAttrs > numMetatiles) { + logWarn(QString("%1 metatile attributes count %2 exceeds metatile count of %3. Additional attributes will be ignored.") + .arg(this->name) + .arg(numMetatileAttrs) + .arg(numMetatiles)); + numMetatileAttrs = numMetatiles; + } else if (numMetatileAttrs < numMetatiles) { + logWarn(QString("%1 metatile attributes count %2 is fewer than the metatile count of %3. Missing attributes will default to 0.") + .arg(this->name) + .arg(numMetatileAttrs) + .arg(numMetatiles)); } - for (int i = 0; i < qMin(numMetatiles, numMetatileAttrs); i++) { + for (int i = 0; i < numMetatileAttrs; i++) { uint32_t attributes = 0; for (int j = 0; j < attrSize; j++) attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); @@ -468,9 +539,9 @@ bool Tileset::loadMetatileAttributes() { } bool Tileset::saveMetatileAttributes() { - QFile attrs_file(this->metatile_attrs_path); - if (!attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - logError(QString("Could not open '%1' for writing: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + QFile file(this->metatile_attrs_path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + logError(QString("Could not open '%1' for writing: %2").arg(this->metatile_attrs_path).arg(file.errorString())); return false; } @@ -480,45 +551,74 @@ bool Tileset::saveMetatileAttributes() { for (int i = 0; i < projectConfig.metatileAttributesSize; i++) data.append(static_cast(attributes >> (8 * i))); } - attrs_file.write(data); + file.write(data); return true; } bool Tileset::loadTilesImage(QImage *importedImage) { QImage image; + bool imported = false; if (importedImage) { image = *importedImage; - m_hasUnsavedTilesImage = true; + imported = true; } else if (QFile::exists(this->tilesImagePath)) { // No image provided, load from file path. image = QImage(this->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); - } else { - // Use default image - image = QImage(8, 8, QImage::Format_Indexed8); } - // Validate image contains 16 colors. + if (image.isNull()) { + logWarn(QString("Failed to load tiles image for %1. Using default tiles image.").arg(this->name)); + image = QImage(Tile::pixelWidth(), Tile::pixelHeight(), QImage::Format_Indexed8); + image.fill(0); + } + + // Validate image dimensions + if (image.width() % Tile::pixelWidth() || image.height() % Tile::pixelHeight()) { + logError(QString("%1 tiles image has invalid dimensions %2x%3. Dimensions must be a multiple of %4x%5.") + .arg(this->name) + .arg(image.width()) + .arg(image.height()) + .arg(Tile::pixelWidth()) + .arg(Tile::pixelHeight())); + return false; + } + + // Validate the number of colors in the image. int colorCount = image.colorCount(); - if (colorCount > 16) { + if (colorCount > Tileset::numColorsPerPalette()) { flattenTo4bppImage(&image); - } else if (colorCount < 16) { + } else if (colorCount < Tileset::numColorsPerPalette()) { QVector colorTable = image.colorTable(); - for (int i = colorTable.length(); i < 16; i++) { + for (int i = colorTable.length(); i < Tileset::numColorsPerPalette(); i++) { colorTable.append(0); } image.setColorTable(colorTable); } + m_tilesImage = image; - QList tiles; - int w = 8; - int h = 8; - for (int y = 0; y < image.height(); y += h) - for (int x = 0; x < image.width(); x += w) { - QImage tile = image.copy(x, y, w, h); - tiles.append(tile); + // Cut up the full tiles image into individual tile images. + m_tiles.clear(); + for (int y = 0; y < image.height(); y += Tile::pixelHeight()) + for (int x = 0; x < image.width(); x += Tile::pixelWidth()) { + m_tiles.append(image.copy(x, y, Tile::pixelWidth(), Tile::pixelHeight())); } - this->tilesImage = image; - this->tiles = tiles; + + if (m_tiles.length() > maxTiles()) { + logWarn(QString("%1 tile count of %2 exceeds limit of %3. Additional tiles will not be displayed.") + .arg(this->name) + .arg(m_tiles.length()) + .arg(maxTiles())); + + // Just resize m_tiles so that numTiles() reports the correct tile count. + // We'll leave m_tilesImage alone (it doesn't get displayed, and we don't want to delete the user's image data). + m_tiles = m_tiles.mid(0, maxTiles()); + } + + if (imported) { + // Only set this flag once we've successfully loaded the tiles image. + m_hasUnsavedTilesImage = true; + } + return true; } @@ -528,7 +628,7 @@ bool Tileset::saveTilesImage() { if (!m_hasUnsavedTilesImage) return true; - if (!this->tilesImage.save(this->tilesImagePath, "PNG")) { + if (!m_tilesImage.save(this->tilesImagePath, "PNG")) { logError(QString("Failed to save tiles image '%1'").arg(this->tilesImagePath)); return false; } @@ -553,8 +653,9 @@ bool Tileset::loadPalettes() { // Either the palette failed to load, or no palette exists. // We expect tilesets to have a certain number of palettes, // so fill this palette with dummy colors. - for (int j = 0; j < 16; j++) { - palette.append(qRgb(j * 16, j * 16, j * 16)); + for (int j = 0; j < Tileset::numColorsPerPalette(); j++) { + int colorComponent = j * (256 / Tileset::numColorsPerPalette()); + palette.append(qRgb(colorComponent, colorComponent, colorComponent)); } } this->palettes.append(palette); @@ -567,7 +668,7 @@ bool Tileset::savePalettes() { bool success = true; int numPalettes = qMin(this->palettePaths.length(), this->palettes.length()); for (int i = 0; i < numPalettes; i++) { - if (!PaletteUtil::writeJASC(this->palettePaths.at(i), this->palettes.at(i).toVector(), 0, 16)) + if (!PaletteUtil::writeJASC(this->palettePaths.at(i), this->palettes.at(i).toVector(), 0, Tileset::numColorsPerPalette())) success = false; } return success; @@ -575,19 +676,105 @@ bool Tileset::savePalettes() { bool Tileset::load() { bool success = true; + if (!loadPalettes()) success = false; + if (!loadTilesImage()) success = false; if (!loadMetatiles()) success = false; if (!loadMetatileAttributes()) success = false; - if (!loadTilesImage()) success = false; - if (!loadPalettes()) success = false; return success; } // Because metatile labels are global (and handled by the project) we don't save them here. bool Tileset::save() { bool success = true; + if (!savePalettes()) success = false; + if (!saveTilesImage()) success = false; if (!saveMetatiles()) success = false; if (!saveMetatileAttributes()) success = false; - if (!saveTilesImage()) success = false; - if (!savePalettes()) success = false; return success; } + +QString Tileset::stripPrefix(const QString &fullName) { + return QString(fullName).replace(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix), ""); +} + +// Find which of the specified color IDs in 'searchColors' are not used by any of this tileset's metatiles. +// The 'pairedTileset' may be used to get the tile images for any tiles that don't belong to this tileset. +// If 'searchColors' is empty, it will for search for all unused colors. +QSet Tileset::getUnusedColorIds(int paletteId, const Tileset *pairedTileset, const QSet &searchColors) const { + QSet unusedColors = searchColors; + if (unusedColors.isEmpty()) { + // Search for all colors + for (int i = 0; i < Tileset::numColorsPerPalette(); i++) { + unusedColors.insert(i); + } + } + const Tileset *primaryTileset = this->is_secondary ? pairedTileset : this; + const Tileset *secondaryTileset = this->is_secondary ? this : pairedTileset; + QSet seenTileIds; + for (const auto &metatile : m_metatiles) + for (const auto &tile : metatile->tiles) { + if (tile.palette != paletteId) + continue; + + // Save time by ignoring tiles we've already inspected. + if (seenTileIds.contains(tile.tileId)) + continue; + seenTileIds.insert(tile.tileId); + + QImage image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); + if (image.isNull() || image.sizeInBytes() < Tile::numPixels()) + continue; + + const uchar * pixels = image.constBits(); + for (int i = 0; i < Tile::numPixels(); i++) { + auto it = unusedColors.constFind(pixels[i]); + if (it != unusedColors.constEnd()) { + unusedColors.erase(it); + if (unusedColors.isEmpty()) { + return {}; + } + } + } + } + return unusedColors; +} + +// Returns the list of metatile IDs representing all the metatiles in this tileset that use the specified color ID. +QList Tileset::findMetatilesUsingColor(int paletteId, int colorId, const Tileset *pairedTileset) const { + const Tileset *primaryTileset = this->is_secondary ? pairedTileset : this; + const Tileset *secondaryTileset = this->is_secondary ? this : pairedTileset; + QSet metatileIdSet; + QHash tileContainsColor; + uint16_t metatileIdBase = firstMetatileId(); + for (int i = 0; i < m_metatiles.length(); i++) { + uint16_t metatileId = i + metatileIdBase; + for (const auto &tile : m_metatiles.at(i)->tiles) { + if (tile.palette != paletteId) + continue; + + // Save time on tiles we've already inspected by getting the cached result. + auto tileIt = tileContainsColor.constFind(tile.tileId); + if (tileIt != tileContainsColor.constEnd()) { + if (tileIt.value()) metatileIdSet.insert(metatileId); + continue; + } + tileContainsColor[tile.tileId] = false; + + QImage image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); + if (image.isNull() || image.sizeInBytes() < Tile::numPixels()) + continue; + + const uchar * pixels = image.constBits(); + for (int j = 0; j < Tile::numPixels(); j++) { + if (pixels[j] == colorId) { + metatileIdSet.insert(metatileId); + tileContainsColor[tile.tileId] = true; + break; + } + } + } + } + QList metatileIds(metatileIdSet.constBegin(), metatileIdSet.constEnd()); + std::sort(metatileIds.begin(), metatileIds.end()); + return metatileIds; +} diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 0ecfd13e..92cd1a4a 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -14,7 +14,7 @@ void Util::numericalModeSort(QStringList &list) { std::sort(list.begin(), list.end(), collator); } -int Util::roundUp(int numToRound, int multiple) { +int Util::roundUpToMultiple(int numToRound, int multiple) { if (multiple <= 0) return numToRound; @@ -112,3 +112,16 @@ void Util::setErrorStylesheet(QLineEdit *lineEdit, bool isError) { static const QString stylesheet = QStringLiteral("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); lineEdit->setStyleSheet(isError ? stylesheet : ""); } + +void Util::show(QWidget *w) { + if (!w) return; + + if (!w->isVisible()) { + w->show(); + } else if (w->isMinimized()) { + w->showNormal(); + } else { + w->raise(); + w->activateWindow(); + } +} diff --git a/src/editor.cpp b/src/editor.cpp index 5fb3ef80..6bce4288 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -30,7 +30,7 @@ Editor::Editor(Ui::MainWindow* ui) { this->ui = ui; this->settings = new Settings(); - this->cursorMapTileRect = new CursorTileRect(&this->settings->cursorTileRectEnabled, qRgb(255, 255, 255)); + this->cursorMapTileRect = new CursorTileRect(Metatile::pixelSize(), qRgb(255, 255, 255)); this->map_ruler = new MapRuler(4); connect(this->map_ruler, &MapRuler::statusChanged, this, &Editor::mapRulerStatusChanged); @@ -125,7 +125,7 @@ void Editor::closeProject() { delete this->project; } -bool Editor::getEditingLayout() { +bool Editor::getEditingLayout() const { return this->editMode == EditMode::Metatiles || this->editMode == EditMode::Collision; } @@ -155,7 +155,6 @@ void Editor::setEditMode(EditMode editMode) { break; } - map_item->setEditsEnabled(this->editMode != EditMode::Connections); map_item->draw(); collision_item->draw(); @@ -169,18 +168,26 @@ void Editor::setEditMode(EditMode editMode) { editStack = &this->layout->editHistory; } - this->cursorMapTileRect->setActive(editingLayout); - this->playerViewRect->setActive(editingLayout); this->editGroup.setActiveStack(editStack); this->ui->toolButton_Fill->setEnabled(editingLayout); this->ui->toolButton_Dropper->setEnabled(editingLayout); this->ui->pushButton_ChangeDimensions->setEnabled(editingLayout); this->ui->checkBox_smartPaths->setEnabled(editingLayout); - if (this->editMode == EditMode::Events || oldEditMode == EditMode::Events) { + if (this->editMode != oldEditMode) { + // When switching to or from the Connections tab we sync up the two separate map graphics views. + if (this->editMode == EditMode::Connections) { + ui->graphicsView_Connections->centerOn(ui->graphicsView_Map); + } else if (oldEditMode == EditMode::Connections) { + ui->graphicsView_Map->centerOn(ui->graphicsView_Connections); + } + // When switching to or from the Events tab the opacity of the events changes. Redraw the events to reflect that change. - redrawAllEvents(); + if (this->editMode == EditMode::Events || oldEditMode == EditMode::Events) { + redrawAllEvents(); + } } + if (this->editMode == EditMode::Events){ updateWarpEventWarnings(); } @@ -204,18 +211,40 @@ void Editor::setEditAction(EditAction editAction) { this->map_ruler->setEnabled(false); } + updateCursorRectVisibility(); + + // The tile cursor can only grow while painting metatiles this->cursorMapTileRect->setSingleTileMode(!(editAction == EditAction::Paint && this->editMode == EditMode::Metatiles)); + auto dragMode = (editAction == EditAction::Move) ? QGraphicsView::ScrollHandDrag : QGraphicsView::NoDrag; + ui->graphicsView_Map->setDragMode(dragMode); + ui->graphicsView_Connections->setDragMode(dragMode); + // Update cursor - static const QMap cursors = { - {EditAction::Paint, QCursor(QPixmap(":/icons/pencil_cursor.ico"), 10, 10)}, - {EditAction::Select, QCursor()}, - {EditAction::Fill, QCursor(QPixmap(":/icons/fill_color_cursor.ico"), 10, 10)}, - {EditAction::Pick, QCursor(QPixmap(":/icons/pipette_cursor.ico"), 10, 10)}, - {EditAction::Move, QCursor(QPixmap(":/icons/move.ico"), 7, 7)}, - {EditAction::Shift, QCursor(QPixmap(":/icons/shift_cursor.ico"), 10, 10)}, - }; - this->settings->mapCursor = cursors.value(editAction); + if (this->settings->betterCursors) { + static const QMap cursors = { + {EditAction::Paint, QCursor(QPixmap(":/icons/pencil_cursor.ico"), 10, 10)}, + {EditAction::Fill, QCursor(QPixmap(":/icons/fill_color_cursor.ico"), 10, 10)}, + {EditAction::Pick, QCursor(QPixmap(":/icons/pipette_cursor.ico"), 10, 10)}, + {EditAction::Shift, QCursor(QPixmap(":/icons/shift_cursor.ico"), 10, 10)}, + }; + + // Paint tools don't apply on the Connections tab, so don't show the cursor. + // We specifically unset the cursor for Move rather than explicitly set Qt::OpenHandCursor + // because otherwise the cursor may persist outside the map after the tool changes. + if (this->editMode == EditMode::Connections || editAction == EditAction::Move) { + if (this->map_item) + this->map_item->unsetCursor(); + if (this->collision_item) + this->collision_item->unsetCursor(); + } else { + auto cursor = cursors.value(editAction); + if (this->map_item) + this->map_item->setCursor(cursor); + if (this->collision_item) + this->collision_item->setCursor(cursor); + } + } emit editActionSet(editAction); } @@ -498,7 +527,7 @@ void Editor::configureEncounterJSON(QWidget *window) { auto createNewSlot = [&fieldSlots, &tempFields, &updateTotal](int index, EncounterField ¤tField) { QLabel *indexLabel = new QLabel(QString("Index: %1").arg(QString::number(index))); QSpinBox *chanceSpinner = new QSpinBox; - int chance = currentField.encounterRates.at(index); + int chance = currentField.encounterRates.value(index); chanceSpinner->setMinimum(1); chanceSpinner->setMaximum(9999); chanceSpinner->setValue(chance); @@ -1091,8 +1120,7 @@ void Editor::onHoveredMetatileSelectionCleared() { } void Editor::onSelectedMetatilesChanged() { - QPoint size = this->metatile_selector_item->getSelectionDimensions(); - this->cursorMapTileRect->updateSelectionSize(size.x(), size.y()); + this->cursorMapTileRect->updateSelectionSize(this->metatile_selector_item->getSelectionDimensions()); this->redrawCurrentMetatilesSelection(); } @@ -1136,15 +1164,20 @@ void Editor::scaleMapView(int s) { ui->graphicsView_Connections->setTransform(transform); } -void Editor::setPlayerViewRect(const QRectF &rect) { - delete this->playerViewRect; - this->playerViewRect = new MovableRect(&this->settings->playerViewRectEnabled, rect, qRgb(255, 255, 255)); - this->playerViewRect->setActive(getEditingLayout()); - if (ui->graphicsView_Map->scene()) - ui->graphicsView_Map->scene()->update(); +bool Editor::isMouseInMap() const { + return (this->map_item && this->map_item->has_mouse) || (this->collision_item && this->collision_item->has_mouse); } -void Editor::updateCursorRectPos(int x, int y) { +void Editor::setPlayerViewRect(const QRectF &rect) { + delete this->playerViewRect; + this->playerViewRect = new MovableRect(rect, Metatile::pixelSize(), qRgb(255, 255, 255)); + updateCursorRectVisibility(); +} + +void Editor::setCursorRectPos(const QPoint &pos) { + int x = qBound(0, pos.x(), this->layout ? this->layout->getWidth() - 1 : 0); + int y = qBound(0, pos.y(), this->layout ? this->layout->getHeight() - 1 : 0); + if (this->playerViewRect) this->playerViewRect->updateLocation(x, y); if (this->cursorMapTileRect) @@ -1153,23 +1186,67 @@ void Editor::updateCursorRectPos(int x, int y) { ui->graphicsView_Map->scene()->update(); } -void Editor::setCursorRectVisible(bool visible) { - if (this->playerViewRect) - this->playerViewRect->setVisible(visible); - if (this->cursorMapTileRect) - this->cursorMapTileRect->setVisible(visible); - if (ui->graphicsView_Map->scene()) +void Editor::updateCursorRectVisibility() { + bool mouseInMap = isMouseInMap(); + bool changed = false; + + if (this->playerViewRect) { + bool visible = this->settings->playerViewRectEnabled + && mouseInMap + && this->editMode != EditMode::Connections; + + if (visible != this->playerViewRect->isVisible()) { + this->playerViewRect->setVisible(visible); + changed = true; + } + } + + if (this->cursorMapTileRect) { + auto editAction = getEditAction(); + bool visible = this->settings->cursorTileRectEnabled + && mouseInMap + // Only show the tile cursor for tools that apply at a specific tile + && editAction != EditAction::Select + && editAction != EditAction::Move; + + if (visible != this->cursorMapTileRect->isVisible()) { + this->cursorMapTileRect->setVisible(visible); + changed = true; + } + } + + // TODO: Investigate whether it'd be worth limiting the scene update to the old and new areas of the cursor rectangles. + if (ui->graphicsView_Map->scene() && changed) { ui->graphicsView_Map->scene()->update(); + } } -void Editor::onHoveredMapMetatileChanged(const QPoint &pos) { - int x = pos.x(); - int y = pos.y(); - if (!layout || !layout->isWithinBounds(x, y)) +void Editor::onMapHoverEntered(const QPoint &pos) { + updateCursorRectVisibility(); + onMapHoverChanged(pos); +} + +void Editor::onMapHoverChanged(const QPoint &pos) { + this->setCursorRectPos(pos); + if (!layout || !layout->isWithinBounds(pos)) return; - this->updateCursorRectPos(x, y); - if (this->getEditingLayout()) { + setStatusFromMapPos(pos); + Scripting::cb_BlockHoverChanged(pos.x(), pos.y()); +} + +void Editor::onMapHoverCleared() { + updateCursorRectVisibility(); + if (getEditingLayout()) { + ui->statusBar->clearMessage(); + } + Scripting::cb_BlockHoverCleared(); +} + +void Editor::setStatusFromMapPos(const QPoint &pos) { + int x = pos.x(); + int y = pos.y(); + if (this->editMode == EditMode::Metatiles) { int blockIndex = y * layout->getWidth() + x; int metatileId = layout->blockdata.at(blockIndex).metatileId(); this->ui->statusBar->showMessage(QString("X: %1, Y: %2, %3, Scale = %4x") @@ -1177,49 +1254,20 @@ void Editor::onHoveredMapMetatileChanged(const QPoint &pos) { .arg(y) .arg(getMetatileDisplayMessage(metatileId)) .arg(QString::number(zoomLevels[this->scaleIndex], 'g', 2))); - } - else if (this->editMode == EditMode::Events) { + } else if (this->editMode == EditMode::Collision) { + int blockIndex = y * layout->getWidth() + x; + uint16_t collision = layout->blockdata.at(blockIndex).collision(); + uint16_t elevation = layout->blockdata.at(blockIndex).elevation(); + this->ui->statusBar->showMessage(QString("X: %1, Y: %2, %3") + .arg(x) + .arg(y) + .arg(this->getMovementPermissionText(collision, elevation))); + } else if (this->editMode == EditMode::Events) { this->ui->statusBar->showMessage(QString("X: %1, Y: %2, Scale = %3x") .arg(x) .arg(y) .arg(QString::number(zoomLevels[this->scaleIndex], 'g', 2))); } - - Scripting::cb_BlockHoverChanged(x, y); -} - -void Editor::onHoveredMapMetatileCleared() { - this->setCursorRectVisible(false); - if (map_item->getEditsEnabled()) { - this->ui->statusBar->clearMessage(); - } - Scripting::cb_BlockHoverCleared(); -} - -void Editor::onHoveredMapMovementPermissionChanged(int x, int y) { - if (!layout || !layout->isWithinBounds(x, y)) - return; - - this->updateCursorRectPos(x, y); - if (this->getEditingLayout()) { - int blockIndex = y * layout->getWidth() + x; - uint16_t collision = layout->blockdata.at(blockIndex).collision(); - uint16_t elevation = layout->blockdata.at(blockIndex).elevation(); - QString message = QString("X: %1, Y: %2, %3") - .arg(x) - .arg(y) - .arg(this->getMovementPermissionText(collision, elevation)); - this->ui->statusBar->showMessage(message); - } - Scripting::cb_BlockHoverChanged(x, y); -} - -void Editor::onHoveredMapMovementPermissionCleared() { - this->setCursorRectVisible(false); - if (this->getEditingLayout()) { - this->ui->statusBar->clearMessage(); - } - Scripting::cb_BlockHoverCleared(); } QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation) { @@ -1311,12 +1359,13 @@ bool Editor::setLayout(QString layoutId) { map_ruler->setMapDimensions(QSize(this->layout->getWidth(), this->layout->getHeight())); connect(this->layout, &Layout::dimensionsChanged, map_ruler, &MapRuler::setMapDimensions); - ui->comboBox_PrimaryTileset->blockSignals(true); - ui->comboBox_SecondaryTileset->blockSignals(true); + QString prevPrimaryTileset = ui->comboBox_PrimaryTileset->currentText(); + QString prevSecondaryTileset = ui->comboBox_SecondaryTileset->currentText(); + + const QSignalBlocker b_PrimaryTilest(ui->comboBox_PrimaryTileset); + const QSignalBlocker b_SecondaryTilest(ui->comboBox_SecondaryTileset); ui->comboBox_PrimaryTileset->setTextItem(this->layout->tileset_primary_label); ui->comboBox_SecondaryTileset->setTextItem(this->layout->tileset_secondary_label); - ui->comboBox_PrimaryTileset->blockSignals(false); - ui->comboBox_SecondaryTileset->blockSignals(false); const QSignalBlocker b0(this->ui->comboBox_LayoutSelector); int index = this->ui->comboBox_LayoutSelector->findText(layoutId); @@ -1325,17 +1374,25 @@ bool Editor::setLayout(QString layoutId) { if (this->layout->name != prevLayoutName) Scripting::cb_LayoutOpened(this->layout->name); + if (this->layout->tileset_primary_label != prevPrimaryTileset) + Scripting::cb_TilesetUpdated(this->layout->tileset_primary_label); + if (this->layout->tileset_secondary_label != prevSecondaryTileset) + Scripting::cb_TilesetUpdated(this->layout->tileset_secondary_label); return true; } +bool Editor::canPaintMetatiles() const { + return this->editMode == EditMode::Metatiles && this->mapEditAction != EditAction::Select && this->mapEditAction != EditAction::Move; +} + void Editor::onMapStartPaint(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *) { - if (!this->getEditingLayout()) { + if (!canPaintMetatiles()) { return; } QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - if (event->buttons() & Qt::RightButton && (mapEditAction == EditAction::Paint || mapEditAction == EditAction::Fill)) { + if (event->buttons() & Qt::RightButton && (this->mapEditAction == EditAction::Paint || this->mapEditAction == EditAction::Fill)) { this->cursorMapTileRect->initRightClickSelectionAnchor(pos.x(), pos.y()); } else { this->cursorMapTileRect->initAnchor(pos.x(), pos.y()); @@ -1343,7 +1400,7 @@ void Editor::onMapStartPaint(QGraphicsSceneMouseEvent *event, LayoutPixmapItem * } void Editor::onMapEndPaint(QGraphicsSceneMouseEvent *, LayoutPixmapItem *) { - if (!this->getEditingLayout()) { + if (!canPaintMetatiles()) { return; } this->cursorMapTileRect->stopRightClickSelectionAnchor(); @@ -1368,25 +1425,31 @@ void Editor::setSmartPathCursorMode(QGraphicsSceneMouseEvent *event) } } -void Editor::setStraightPathCursorMode(QGraphicsSceneMouseEvent *event) { +void Editor::adjustStraightPathPos(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item, QPoint *pos) const { if (event->modifiers() & Qt::ControlModifier) { - this->cursorMapTileRect->setStraightPathMode(true); - } else { - this->cursorMapTileRect->setStraightPathMode(false); + item->lockNondominantAxis(event); + *pos = item->adjustCoords(*pos); } } void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *item) { - if (!item->getEditsEnabled()) { + auto editAction = getEditAction(); + if (editAction == EditAction::Move) { + event->ignore(); return; } QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - if (this->getEditingLayout()) { - if (mapEditAction == EditAction::Paint) { + if (this->editMode == EditMode::Metatiles || this->editMode == EditMode::Collision) { + if (editAction == EditAction::Paint) { if (event->buttons() & Qt::RightButton) { - item->updateMetatileSelection(event); + if (this->editMode == EditMode::Collision) { + auto collisionItem = dynamic_cast(item); + if (collisionItem) collisionItem->updateMovementPermissionSelection(event); + } else { + item->updateMetatileSelection(event); + } } else if (event->buttons() & Qt::MiddleButton) { if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); @@ -1396,42 +1459,40 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i } else { if (event->type() == QEvent::GraphicsSceneMouseRelease) { // Update the tile rectangle at the end of a click-drag selection - this->updateCursorRectPos(pos.x(), pos.y()); - } - this->setSmartPathCursorMode(event); - this->setStraightPathCursorMode(event); - if (this->cursorMapTileRect->getStraightPathMode()) { - item->lockNondominantAxis(event); - pos = item->adjustCoords(pos); + setCursorRectPos(pos); } + setSmartPathCursorMode(event); + adjustStraightPathPos(event, item, &pos); item->paint(event); } - } else if (mapEditAction == EditAction::Select) { + setStatusFromMapPos(pos); + } else if (editAction == EditAction::Select) { item->select(event); - } else if (mapEditAction == EditAction::Fill) { + } else if (editAction == EditAction::Fill) { if (event->buttons() & Qt::RightButton) { - item->updateMetatileSelection(event); + if (this->editMode == EditMode::Metatiles) { + item->updateMetatileSelection(event); + } else { + item->pick(event); + } } else if (event->modifiers() & Qt::ControlModifier) { item->magicFill(event); } else { item->floodFill(event); } - } else if (mapEditAction == EditAction::Pick) { - if (event->buttons() & Qt::RightButton) { + setStatusFromMapPos(pos); + } else if (editAction == EditAction::Pick) { + if (this->editMode == EditMode::Metatiles && (event->buttons() & Qt::RightButton)) { item->updateMetatileSelection(event); } else if (event->type() != QEvent::GraphicsSceneMouseRelease) { item->pick(event); } - } else if (mapEditAction == EditAction::Shift) { - this->setStraightPathCursorMode(event); - if (this->cursorMapTileRect->getStraightPathMode()) { - item->lockNondominantAxis(event); - pos = item->adjustCoords(pos); - } + } else if (editAction == EditAction::Shift) { + adjustStraightPathPos(event, item, &pos); item->shift(event); } } else if (this->editMode == EditMode::Events) { - if (eventEditAction == EditAction::Paint && event->type() == QEvent::GraphicsSceneMousePress) { + if (editAction == EditAction::Paint && event->type() == QEvent::GraphicsSceneMousePress) { // Right-clicking while in paint mode will change mode to select. if (event->buttons() & Qt::RightButton) { setEditAction(EditAction::Select); @@ -1446,12 +1507,12 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i if (event && event->getPixmapItem()) event->getPixmapItem()->moveTo(pos); } - } else if (eventEditAction == EditAction::Select && event->type() == QEvent::GraphicsSceneMousePress) { + } else if (editAction == EditAction::Select && event->type() == QEvent::GraphicsSceneMousePress) { if (!(event->modifiers() & Qt::ControlModifier) && this->selectedEvents.length() > 1) { // User is clearing group selection by clicking on the background selectMapEvent(this->selectedEvents.first()); } - } else if (eventEditAction == EditAction::Shift) { + } else if (editAction == EditAction::Shift) { static QPoint selection_origin; if (event->type() == QEvent::GraphicsSceneMouseRelease) { @@ -1473,52 +1534,6 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i } } -void Editor::mouseEvent_collision(QGraphicsSceneMouseEvent *event, CollisionPixmapItem *item) { - if (!item->getEditsEnabled()) { - return; - } - - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - - if (mapEditAction == EditAction::Paint) { - if (event->buttons() & Qt::RightButton) { - item->updateMovementPermissionSelection(event); - } else if (event->buttons() & Qt::MiddleButton) { - if (event->modifiers() & Qt::ControlModifier) { - item->magicFill(event); - } else { - item->floodFill(event); - } - } else { - this->setStraightPathCursorMode(event); - if (this->cursorMapTileRect->getStraightPathMode()) { - item->lockNondominantAxis(event); - pos = item->adjustCoords(pos); - } - item->paint(event); - } - } else if (mapEditAction == EditAction::Select) { - item->select(event); - } else if (mapEditAction == EditAction::Fill) { - if (event->buttons() & Qt::RightButton) { - item->pick(event); - } else if (event->modifiers() & Qt::ControlModifier) { - item->magicFill(event); - } else { - item->floodFill(event); - } - } else if (mapEditAction == EditAction::Pick) { - item->pick(event); - } else if (mapEditAction == EditAction::Shift) { - this->setStraightPathCursorMode(event); - if (this->cursorMapTileRect->getStraightPathMode()) { - item->lockNondominantAxis(event); - pos = item->adjustCoords(pos); - } - item->shift(event); - } -} - // On project close we want to leave the editor view empty. // Otherwise a map is normally only cleared when a new one is being displayed. void Editor::clearMap() { @@ -1604,7 +1619,7 @@ void Editor::displayMetatileSelector() { scene_metatiles = new QGraphicsScene; if (!metatile_selector_item) { - metatile_selector_item = new MetatileSelector(8, this->layout); + metatile_selector_item = new MetatileSelector(projectConfig.metatileSelectorWidth, this->layout); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionChanged, this, &Editor::onHoveredMetatileSelectionChanged); connect(metatile_selector_item, &MetatileSelector::hoveredMetatileSelectionCleared, @@ -1614,13 +1629,6 @@ void Editor::displayMetatileSelector() { metatile_selector_item->select(0); } else { metatile_selector_item->setLayout(this->layout); - if (metatile_selector_item->primaryTileset - && metatile_selector_item->primaryTileset != this->layout->tileset_primary) - emit tilesetUpdated(this->layout->tileset_primary->name); - if (metatile_selector_item->secondaryTileset - && metatile_selector_item->secondaryTileset != this->layout->tileset_secondary) - emit tilesetUpdated(this->layout->tileset_secondary->name); - metatile_selector_item->setTilesets(this->layout->tileset_primary, this->layout->tileset_secondary); } scene_metatiles->addItem(metatile_selector_item); @@ -1640,8 +1648,9 @@ void Editor::displayMapMetatiles() { connect(map_item, &LayoutPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); connect(map_item, &LayoutPixmapItem::startPaint, this, &Editor::onMapStartPaint); connect(map_item, &LayoutPixmapItem::endPaint, this, &Editor::onMapEndPaint); - connect(map_item, &LayoutPixmapItem::hoveredMapMetatileChanged, this, &Editor::onHoveredMapMetatileChanged); - connect(map_item, &LayoutPixmapItem::hoveredMapMetatileCleared, this, &Editor::onHoveredMapMetatileCleared); + connect(map_item, &LayoutPixmapItem::hoverEntered, this, &Editor::onMapHoverEntered); + connect(map_item, &LayoutPixmapItem::hoverChanged, this, &Editor::onMapHoverChanged); + connect(map_item, &LayoutPixmapItem::hoverCleared, this, &Editor::onMapHoverCleared); map_item->draw(true); scene->addItem(map_item); @@ -1662,11 +1671,10 @@ void Editor::displayMapMovementPermissions() { collision_item = new CollisionPixmapItem(this->layout, ui->spinBox_SelectedCollision, ui->spinBox_SelectedElevation, this->metatile_selector_item, this->settings, &this->collisionOpacity); - connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_collision); - connect(collision_item, &CollisionPixmapItem::hoveredMapMovementPermissionChanged, - this, &Editor::onHoveredMapMovementPermissionChanged); - connect(collision_item, &CollisionPixmapItem::hoveredMapMovementPermissionCleared, - this, &Editor::onHoveredMapMovementPermissionCleared); + connect(collision_item, &CollisionPixmapItem::mouseEvent, this, &Editor::mouseEvent_map); + connect(collision_item, &CollisionPixmapItem::hoverEntered, this, &Editor::onMapHoverEntered); + connect(collision_item, &CollisionPixmapItem::hoverChanged, this, &Editor::onMapHoverChanged); + connect(collision_item, &CollisionPixmapItem::hoverCleared, this, &Editor::onMapHoverCleared); collision_item->draw(true); scene->addItem(collision_item); @@ -1739,8 +1747,8 @@ void Editor::displayMovementPermissionSelector() { this, &Editor::onHoveredMovementPermissionChanged); connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared, this, &Editor::onHoveredMovementPermissionCleared); - connect(movement_permissions_selector_item, &SelectablePixmapItem::selectionChanged, [this](int x, int y, int, int) { - this->setCollisionTabSpinBoxes(x, y); + connect(movement_permissions_selector_item, &SelectablePixmapItem::selectionChanged, [this](const QPoint &pos, const QSize&) { + this->setCollisionTabSpinBoxes(pos.x(), pos.y()); }); movement_permissions_selector_item->select(projectConfig.defaultCollision, projectConfig.defaultElevation); } @@ -1880,8 +1888,8 @@ void Editor::displayMapBorder() { for (int y = -borderMargins.top(); y < this->layout->getHeight() + borderMargins.bottom(); y += this->layout->getBorderHeight()) for (int x = -borderMargins.left(); x < this->layout->getWidth() + borderMargins.right(); x += this->layout->getBorderWidth()) { QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap); - item->setX(x * 16); - item->setY(y * 16); + item->setX(x * Metatile::pixelWidth()); + item->setY(y * Metatile::pixelHeight()); item->setZValue(ZValue::MapBorder); scene->addItem(item); borderItems.append(item); @@ -1929,8 +1937,8 @@ void Editor::displayMapGrid() { // elements of the scripting API, so they're painted manually in MapView::drawForeground. this->mapGrid = new QGraphicsItemGroup(); - const int pixelMapWidth = this->layout->getWidth() * 16; - const int pixelMapHeight = this->layout->getHeight() * 16; + const int pixelMapWidth = this->layout->pixelWidth(); + const int pixelMapHeight = this->layout->pixelHeight(); // The grid can be moved with a user-specified x/y offset. The grid's dash patterns will only wrap in full pattern increments, // so we draw an additional row/column outside the map that can be revealed as the offset changes. @@ -2025,12 +2033,6 @@ void Editor::updateCustomMapAttributes() map->modify(); } -Tileset* Editor::getCurrentMapPrimaryTileset() -{ - QString tilesetLabel = this->layout->tileset_primary_label; - return project->getTileset(tilesetLabel); -} - void Editor::redrawAllEvents() { if (this->map) redrawEvents(this->map->getEvents()); } @@ -2089,7 +2091,7 @@ void Editor::onEventDragged(Event *event, const QPoint &oldPosition, const QPoin if (!this->map || !this->map_item) return; - this->map_item->hoveredMapMetatileChanged(newPosition); + this->map_item->hoverChanged(newPosition); // Drag all the other selected events (if any) with it QList draggedEvents; @@ -2196,7 +2198,7 @@ bool Editor::canAddEvents(const QList &events) { } void Editor::duplicateSelectedEvents() { - if (this->selectedEvents.isEmpty() || !project || !map || !current_view || this->getEditingLayout()) + if (this->selectedEvents.isEmpty() || !project || !map || !current_view || this->editMode != EditMode::Events) return; QList duplicatedEvents; @@ -2318,21 +2320,29 @@ void Editor::openMapScripts() const { openInTextEditor(map->getScriptsFilepath()); } -void Editor::openScript(const QString &scriptLabel) const { +bool Editor::openScript(const QString &scriptLabel) const { // Find the location of scriptLabel. - QStringList scriptPaths(map->getScriptsFilepath()); - scriptPaths << project->getEventScriptsFilepaths(); - int lineNum = 0; - QString scriptPath = scriptPaths.first(); - for (const auto &path : scriptPaths) { - lineNum = ParseUtil::getScriptLineNumber(path, scriptLabel); - if (lineNum != 0) { - scriptPath = path; - break; - } - } + // First, try the current map's scripts file. + if (openScriptInFile(scriptLabel, map->getScriptsFilepath())) + return true; - openInTextEditor(scriptPath, lineNum); + // Script is not in the current map's scripts file. + // Search all possible script files. + const QStringList paths = project->getAllEventScriptsFilepaths(); + for (const auto &path : paths) { + if (openScriptInFile(scriptLabel, path)) + return true; + } + return false; +} + +bool Editor::openScriptInFile(const QString &scriptLabel, const QString &filepath) const { + int lineNum = ParseUtil::getScriptLineNumber(filepath, scriptLabel); + if (lineNum == 0) + return false; + + openInTextEditor(filepath, lineNum); + return true; } void Editor::openMapJson(const QString &mapName) const { @@ -2443,7 +2453,7 @@ void Editor::setCollisionGraphics() { // Use the image sheet to create an icon for each collision/elevation combination. // Any icons for combinations that aren't provided by the image sheet are also created now using default graphics. - const int w = 16, h = 16; + const int w = Metatile::pixelWidth(), h = Metatile::pixelHeight(); imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows); for (int collision = 0; collision <= Block::getMaxCollision(); collision++) { // If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet. diff --git a/src/log.cpp b/src/log.cpp index 0c23470c..7eaaec26 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -196,8 +196,9 @@ void logInit() { clearLogDisplays(); }); + Log::initialized = true; + if (cleanupLargeLog()) { logWarn(QString("Previous log file %1 was cleared due to being over 20MB in size.").arg(Log::path)); } - Log::initialized = true; } diff --git a/src/main.cpp b/src/main.cpp index d85c1b38..dfd3bff2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,8 @@ int main(int argc, char *argv[]) { QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round); + QCoreApplication::setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true); + QApplication a(argc, argv); a.setStyle("fusion"); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 8259c315..3519827b 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -54,6 +54,9 @@ #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) #define RELEASE_PLATFORM #endif +#if defined(QT_NETWORK_LIB) && defined(RELEASE_PLATFORM) +#define USE_UPDATE_PROMOTER +#endif @@ -72,7 +75,8 @@ MainWindow::MainWindow(QWidget *parent) : ui->setupUi(this); logInit(); - logInfo(QString("Launching Porymap v%1").arg(QCoreApplication::applicationVersion())); + logInfo(QString("Launching Porymap v%1 (%2)").arg(QCoreApplication::applicationVersion()).arg(QStringLiteral(PORYMAP_LATEST_COMMIT))); + logInfo(QString("Using Qt v%2 (%3)").arg(QStringLiteral(QT_VERSION_STR)).arg(QSysInfo::buildCpuArchitecture())); } void MainWindow::initialize() { @@ -155,14 +159,33 @@ void MainWindow::initWindow() { this->initMapList(); this->initShortcuts(); -#ifndef RELEASE_PLATFORM + QStringList missingModules; + +#ifndef USE_UPDATE_PROMOTER ui->actionCheck_for_Updates->setVisible(false); +#ifdef RELEASE_PLATFORM + // Only report the network module missing if we would + // have otherwise used it (we don't on non-release platforms). + missingModules.append(" 'network'"); +#endif #endif #ifndef QT_CHARTS_LIB ui->pushButton_SummaryChart->setVisible(false); + missingModules.append(" 'charts'"); #endif +#ifndef QT_QML_LIB + ui->actionCustom_Scripts->setVisible(false); + missingModules.append(" 'qml'"); +#endif + + if (!missingModules.isEmpty()) { + logWarn(QString("Qt module%1%2 not found. Some features will be disabled.") + .arg(missingModules.length() > 1 ? "s" : "") + .arg(missingModules.join(","))); + } + setWindowDisabled(true); } @@ -320,17 +343,19 @@ void MainWindow::initExtraSignals() { connect(ui->action_NewMap, &QAction::triggered, this, &MainWindow::openNewMapDialog); connect(ui->action_NewLayout, &QAction::triggered, this, &MainWindow::openNewLayoutDialog); connect(ui->actionDuplicate_Current_Map_Layout, &QAction::triggered, this, &MainWindow::openDuplicateMapOrLayoutDialog); - connect(ui->comboBox_LayoutSelector->lineEdit(), &QLineEdit::editingFinished, this, &MainWindow::onLayoutSelectorEditingFinished); + connect(ui->comboBox_LayoutSelector, &NoScrollComboBox::editingFinished, this, &MainWindow::onLayoutSelectorEditingFinished); connect(ui->checkBox_smartPaths, &QCheckBox::toggled, this, &MainWindow::setSmartPathsEnabled); connect(ui->checkBox_ToggleBorder, &QCheckBox::toggled, this, &MainWindow::setBorderVisibility); connect(ui->checkBox_MirrorConnections, &QCheckBox::toggled, this, &MainWindow::setMirrorConnectionsEnabled); + connect(ui->comboBox_PrimaryTileset, &NoScrollComboBox::editingFinished, [this] { setPrimaryTileset(ui->comboBox_PrimaryTileset->currentText()); }); + connect(ui->comboBox_SecondaryTileset, &NoScrollComboBox::editingFinished, [this] { setSecondaryTileset(ui->comboBox_SecondaryTileset->currentText()); }); } void MainWindow::on_actionCheck_for_Updates_triggered() { checkForUpdates(true); } -#ifdef RELEASE_PLATFORM +#ifdef USE_UPDATE_PROMOTER void MainWindow::checkForUpdates(bool requestedByUser) { if (!this->networkAccessManager) this->networkAccessManager = new NetworkAccessManager(this); @@ -345,7 +370,7 @@ void MainWindow::checkForUpdates(bool requestedByUser) { if (requestedByUser) { - openSubWindow(this->updatePromoter); + Util::show(this->updatePromoter); } else { // This is an automatic update check. Only run if we haven't done one in the last 5 minutes QDateTime lastCheck = porymapConfig.lastUpdateCheckTime; @@ -367,7 +392,6 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged); connect(this->editor, &Editor::wildMonTableEdited, [this] { markMapEdited(this->editor->map); }); connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged); - connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(this->editor, &Editor::editActionSet, this, &MainWindow::setEditActionUi); connect(ui->newEventToolButton, &NewEventToolButton::newEventAdded, this->editor, &Editor::addNewEvent); connect(ui->toolButton_deleteEvent, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents); @@ -420,7 +444,7 @@ void MainWindow::initEditor() { } void MainWindow::openEditHistory() { - openSubWindow(this->undoView); + Util::show(this->undoView); } void MainWindow::initMiscHeapObjects() { @@ -937,19 +961,6 @@ void MainWindow::refreshRecentProjectsMenu() { clearAction->setEnabled(!recentProjects.isEmpty()); } -void MainWindow::openSubWindow(QWidget * window) { - if (!window) return; - - if (!window->isVisible()) { - window->show(); - } else if (window->isMinimized()) { - window->showNormal(); - } else { - window->raise(); - window->activateWindow(); - } -} - void MainWindow::showFileWatcherWarning() { if (!porymapConfig.monitorFiles || !isProjectOpen()) return; @@ -1409,7 +1420,7 @@ bool MainWindow::setProjectUI() { ui->newEventToolButton->setEventTypeVisible(Event::Type::SecretBase, projectConfig.eventSecretBaseEnabled); ui->newEventToolButton->setEventTypeVisible(Event::Type::CloneObject, projectConfig.eventCloneObjectEnabled); - this->editor->setPlayerViewRect(QRectF(0, 0, 16, 16).marginsAdded(projectConfig.playerViewDistance)); + this->editor->setPlayerViewRect(QRectF(QPoint(0,0), Metatile::pixelSize()).marginsAdded(projectConfig.playerViewDistance)); editor->setCollisionGraphics(); ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); @@ -1475,6 +1486,7 @@ void MainWindow::clearProjectUI() { ui->comboBox_LayoutSelector->clear(); this->mapHeaderForm->clear(); + ui->label_NoEvents->setText(""); prefab.clearPrefabUi(); @@ -1801,7 +1813,7 @@ void MainWindow::redrawMetatileSelection() { void MainWindow::scrollMetatileSelectorToSelection() { // Internal selections or 1x1 external selections can be scrolled to - if (!editor->metatile_selector_item->isInternalSelection() && editor->metatile_selector_item->getSelectionDimensions() != QPoint(1, 1)) + if (!editor->metatile_selector_item->isInternalSelection() && editor->metatile_selector_item->getSelectionDimensions() != QSize(1, 1)) return; MetatileSelection selection = editor->metatile_selector_item->getMetatileSelection(); @@ -1809,8 +1821,8 @@ void MainWindow::scrollMetatileSelectorToSelection() { return; QPoint pos = editor->metatile_selector_item->getMetatileIdCoordsOnWidget(selection.metatileItems.first().metatileId); - QPoint size = editor->metatile_selector_item->getSelectionDimensions(); - pos += QPoint(size.x() - 1, size.y() - 1) * 16 / 2; // We want to focus on the center of the whole selection + QSize size = editor->metatile_selector_item->getSelectionDimensions(); + pos += QPoint((size.width() - 1) * Metatile::pixelWidth(), (size.height() - 1) * Metatile::pixelHeight()) / 2; // We want to focus on the center of the whole selection pos *= getMetatilesZoomScale(); auto viewport = ui->scrollArea_MetatileSelector->viewport(); @@ -1821,7 +1833,9 @@ void MainWindow::currentMetatilesSelectionChanged() { redrawMetatileSelection(); if (this->tilesetEditor) { MetatileSelection selection = editor->metatile_selector_item->getMetatileSelection(); - this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + if (!selection.metatileItems.isEmpty()) { + this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + } } // Don't scroll to internal selections here, it will disrupt the user while they make their selection. @@ -1982,8 +1996,8 @@ void MainWindow::copy() { } copyObject["metatile_selection"] = metatiles; copyObject["collision_selection"] = collisions; - copyObject["width"] = editor->metatile_selector_item->getSelectionDimensions().x(); - copyObject["height"] = editor->metatile_selector_item->getSelectionDimensions().y(); + copyObject["width"] = editor->metatile_selector_item->getSelectionDimensions().width(); + copyObject["height"] = editor->metatile_selector_item->getSelectionDimensions().height(); setClipboardData(copyObject); logInfo("Copied metatile selection to clipboard"); } @@ -2176,7 +2190,6 @@ void MainWindow::on_mapViewTab_tabBarClicked(int index) prefab.updatePrefabUi(this->editor->layout); } } - editor->setCursorRectVisible(false); } void MainWindow::on_mainTabBar_tabBarClicked(int index) @@ -2240,11 +2253,7 @@ void MainWindow::on_actionPlayer_View_Rectangle_triggered() bool enabled = ui->actionPlayer_View_Rectangle->isChecked(); porymapConfig.showPlayerView = enabled; this->editor->settings->playerViewRectEnabled = enabled; - if ((this->editor->map_item && this->editor->map_item->has_mouse) - || (this->editor->collision_item && this->editor->collision_item->has_mouse)) { - this->editor->playerViewRect->setVisible(enabled && this->editor->playerViewRect->getActive()); - ui->graphicsView_Map->scene()->update(); - } + this->editor->updateCursorRectVisibility(); } void MainWindow::on_actionCursor_Tile_Outline_triggered() @@ -2252,11 +2261,7 @@ void MainWindow::on_actionCursor_Tile_Outline_triggered() bool enabled = ui->actionCursor_Tile_Outline->isChecked(); porymapConfig.showCursorTile = enabled; this->editor->settings->cursorTileRectEnabled = enabled; - if ((this->editor->map_item && this->editor->map_item->has_mouse) - || (this->editor->collision_item && this->editor->collision_item->has_mouse)) { - this->editor->cursorMapTileRect->setVisible(enabled && this->editor->cursorMapTileRect->getActive()); - ui->graphicsView_Map->scene()->update(); - } + this->editor->updateCursorRectVisibility(); } void MainWindow::on_actionShow_Events_In_Map_View_triggered() { @@ -2274,7 +2279,7 @@ void MainWindow::on_actionGrid_Settings_triggered() { connect(this->gridSettingsDialog, &GridSettingsDialog::changedGridSettings, this->editor, &Editor::updateMapGrid); connect(this->gridSettingsDialog, &GridSettingsDialog::accepted, [this] { porymapConfig.gridSettings = this->editor->gridSettings; }); } - openSubWindow(this->gridSettingsDialog); + Util::show(this->gridSettingsDialog); } void MainWindow::on_actionShortcuts_triggered() @@ -2282,7 +2287,7 @@ void MainWindow::on_actionShortcuts_triggered() if (!shortcutsEditor) initShortcutsEditor(); - openSubWindow(shortcutsEditor); + Util::show(shortcutsEditor); } void MainWindow::initShortcutsEditor() { @@ -2292,27 +2297,40 @@ void MainWindow::initShortcutsEditor() { connectSubEditorsToShortcutsEditor(); - shortcutsEditor->setShortcutableObjects(shortcutableObjects()); + auto objectList = shortcutableObjects(); + for (auto *menu : findChildren()) { + if (!menu->objectName().isEmpty()) + objectList.append(qobject_cast(menu)); + } + shortcutsEditor->setShortcutableObjects(objectList); } void MainWindow::connectSubEditorsToShortcutsEditor() { /* Initialize sub-editors so that their children are added to MainWindow's object tree and will * be returned by shortcutableObjects() to be passed to ShortcutsEditor. */ - if (!tilesetEditor) + if (!this->tilesetEditor) { initTilesetEditor(); - connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved, - tilesetEditor, &TilesetEditor::applyUserShortcuts); + } + if (this->tilesetEditor) { + connect(this->shortcutsEditor, &ShortcutsEditor::shortcutsSaved, + this->tilesetEditor, &TilesetEditor::applyUserShortcuts); + } - if (!regionMapEditor) + if (!this->regionMapEditor){ initRegionMapEditor(true); - if (regionMapEditor) - connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved, - regionMapEditor, &RegionMapEditor::applyUserShortcuts); + } + if (this->regionMapEditor) { + connect(this->shortcutsEditor, &ShortcutsEditor::shortcutsSaved, + this->regionMapEditor, &RegionMapEditor::applyUserShortcuts); + } - if (!customScriptsEditor) + if (!this->customScriptsEditor) { initCustomScriptsEditor(); - connect(shortcutsEditor, &ShortcutsEditor::shortcutsSaved, - customScriptsEditor, &CustomScriptsEditor::applyUserShortcuts); + } + if (this->customScriptsEditor) { + connect(this->shortcutsEditor, &ShortcutsEditor::shortcutsSaved, + this->customScriptsEditor, &CustomScriptsEditor::applyUserShortcuts); + } } void MainWindow::resetMapViewScale() { @@ -2495,6 +2513,14 @@ void MainWindow::updateSelectedEvents() { } else { ui->tabWidget_EventType->hide(); + + if (this->editor->map && this->editor->map->isInheritingEvents()) { + QString message = QString("NOTE: This map inherits events from %1." + "
Adding any events will separate it from that map.").arg(this->editor->map->sharedEventsMap()); + ui->label_NoEvents->setText(message); + } else { + ui->label_NoEvents->setText(QStringLiteral("There are no events on the current map.")); + } ui->label_NoEvents->show(); } } @@ -2588,19 +2614,6 @@ void MainWindow::on_toolButton_Move_clicked() { editor->setEditAction(Editor: void MainWindow::on_toolButton_Shift_clicked() { editor->setEditAction(Editor::EditAction::Shift); } void MainWindow::setEditActionUi(Editor::EditAction editAction) { - if (editAction == Editor::EditAction::Move) { - ui->graphicsView_Map->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - ui->graphicsView_Map->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QScroller::grabGesture(ui->graphicsView_Map, QScroller::LeftMouseButtonGesture); - ui->graphicsView_Map->setViewportUpdateMode(QGraphicsView::ViewportUpdateMode::FullViewportUpdate); - } else { - ui->graphicsView_Map->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->graphicsView_Map->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - QScroller::ungrabGesture(ui->graphicsView_Map); - ui->graphicsView_Map->setViewportUpdateMode(QGraphicsView::ViewportUpdateMode::MinimalViewportUpdate); - } - ui->graphicsView_Map->setFocus(); - ui->toolButton_Paint->setChecked(editAction == Editor::EditAction::Paint); ui->toolButton_Select->setChecked(editAction == Editor::EditAction::Select); ui->toolButton_Fill->setChecked(editAction == Editor::EditAction::Fill); @@ -2719,7 +2732,7 @@ void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { } } - openSubWindow(this->mapImageExporter); + Util::show(this->mapImageExporter); } void MainWindow::on_pushButton_AddConnection_clicked() { @@ -2747,7 +2760,7 @@ void MainWindow::on_pushButton_SummaryChart_clicked() { connect(this->editor, &Editor::wildMonTableClosed, this->wildMonChart, &WildMonChart::clearTable); connect(this->editor, &Editor::wildMonTableEdited, this->wildMonChart, &WildMonChart::refresh); } - openSubWindow(this->wildMonChart); + Util::show(this->wildMonChart); } void MainWindow::on_toolButton_WildMonSearch_clicked() { @@ -2756,7 +2769,7 @@ void MainWindow::on_toolButton_WildMonSearch_clicked() { connect(this->wildMonSearch, &WildMonSearch::openWildMonTableRequested, this, &MainWindow::openWildMonTable); connect(this->editor, &Editor::wildMonTableEdited, this->wildMonSearch, &WildMonSearch::refresh); } - openSubWindow(this->wildMonSearch); + Util::show(this->wildMonSearch); } void MainWindow::openWildMonTable(const QString &mapName, const QString &groupName, const QString &fieldName) { @@ -2781,26 +2794,38 @@ void MainWindow::on_button_OpenEmergeMap_clicked() { userSetMap(ui->comboBox_EmergeMap->currentText()); } -void MainWindow::on_comboBox_PrimaryTileset_currentTextChanged(const QString &tilesetLabel) -{ - if (editor->project->primaryTilesetLabels.contains(tilesetLabel) && editor->layout) { +void MainWindow::setPrimaryTileset(const QString &tilesetLabel) { + if (!this->editor->layout || this->editor->layout->tileset_primary_label == tilesetLabel) + return; + + if (editor->project->primaryTilesetLabels.contains(tilesetLabel)) { editor->updatePrimaryTileset(tilesetLabel); redrawMapScene(); updateTilesetEditor(); prefab.updatePrefabUi(editor->layout); markLayoutEdited(); } + + // Restore valid text if input was invalid, or sync combo box with new valid setting. + const QSignalBlocker b(ui->comboBox_PrimaryTileset); + ui->comboBox_PrimaryTileset->setTextItem(this->editor->layout->tileset_primary_label); } -void MainWindow::on_comboBox_SecondaryTileset_currentTextChanged(const QString &tilesetLabel) -{ - if (editor->project->secondaryTilesetLabels.contains(tilesetLabel) && editor->layout) { +void MainWindow::setSecondaryTileset(const QString &tilesetLabel) { + if (!this->editor->layout || this->editor->layout->tileset_secondary_label == tilesetLabel) + return; + + if (editor->project->secondaryTilesetLabels.contains(tilesetLabel)) { editor->updateSecondaryTileset(tilesetLabel); redrawMapScene(); updateTilesetEditor(); prefab.updatePrefabUi(editor->layout); markLayoutEdited(); } + + // Restore valid text if input was invalid, or sync combo box with new valid setting. + const QSignalBlocker b(ui->comboBox_SecondaryTileset); + ui->comboBox_SecondaryTileset->setTextItem(this->editor->layout->tileset_secondary_label); } void MainWindow::on_pushButton_ChangeDimensions_clicked() { @@ -2864,10 +2889,12 @@ void MainWindow::on_actionTileset_Editor_triggered() initTilesetEditor(); } - openSubWindow(this->tilesetEditor); + Util::show(this->tilesetEditor); MetatileSelection selection = this->editor->metatile_selector_item->getMetatileSelection(); - this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + if (!selection.metatileItems.isEmpty()) { + this->tilesetEditor->selectMetatile(selection.metatileItems.first().metatileId); + } } void MainWindow::initTilesetEditor() { @@ -2914,7 +2941,7 @@ void MainWindow::on_actionAbout_Porymap_triggered() { if (!this->aboutWindow) this->aboutWindow = new AboutPorymap(this); - openSubWindow(this->aboutWindow); + Util::show(this->aboutWindow); } void MainWindow::on_actionOpen_Log_File_triggered() { @@ -2927,6 +2954,11 @@ void MainWindow::on_actionOpen_Config_Folder_triggered() { QDesktopServices::openUrl(QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation))); } +void MainWindow::on_actionOpen_Manual_triggered() { + static const QUrl url("https://huderlem.github.io/porymap/"); + QDesktopServices::openUrl(url); +} + void MainWindow::on_actionPreferences_triggered() { if (!preferenceEditor) { preferenceEditor = new PreferenceEditor(this); @@ -2938,16 +2970,19 @@ void MainWindow::on_actionPreferences_triggered() { // require us to repopulate the EventFrames and redraw event pixmaps, respectively. connect(preferenceEditor, &PreferenceEditor::preferencesSaved, editor, &Editor::updateEvents); connect(preferenceEditor, &PreferenceEditor::scriptSettingsChanged, editor->project, &Project::readEventScriptLabels); + connect(preferenceEditor, &PreferenceEditor::reloadProjectRequested, this, &MainWindow::on_action_Reload_Project_triggered); } - openSubWindow(preferenceEditor); + Util::show(preferenceEditor); } void MainWindow::togglePreferenceSpecificUi() { ui->actionOpen_Project_in_Text_Editor->setEnabled(!porymapConfig.textEditorOpenFolder.isEmpty()); +#ifdef USE_UPDATE_PROMOTER if (this->updatePromoter) this->updatePromoter->updatePreferences(); +#endif } void MainWindow::openProjectSettingsEditor(int tab) { @@ -2957,7 +2992,7 @@ void MainWindow::openProjectSettingsEditor(int tab) { this, &MainWindow::on_action_Reload_Project_triggered); } this->projectSettingsEditor->setTab(tab); - openSubWindow(this->projectSettingsEditor); + Util::show(this->projectSettingsEditor); } void MainWindow::on_actionProject_Settings_triggered() { @@ -2992,16 +3027,20 @@ void MainWindow::onWarpBehaviorWarningClicked() { } void MainWindow::on_actionCustom_Scripts_triggered() { - if (!this->customScriptsEditor) + if (!this->customScriptsEditor) { initCustomScriptsEditor(); - - openSubWindow(this->customScriptsEditor); + } + if (this->customScriptsEditor) { + Util::show(this->customScriptsEditor); + } } void MainWindow::initCustomScriptsEditor() { +#ifdef QT_QML_LIB this->customScriptsEditor = new CustomScriptsEditor(this); connect(this->customScriptsEditor, &CustomScriptsEditor::reloadScriptEngine, this, &MainWindow::reloadScriptEngine); +#endif } void MainWindow::reloadScriptEngine() { @@ -3075,7 +3114,7 @@ void MainWindow::on_actionRegion_Map_Editor_triggered() { } } - openSubWindow(this->regionMapEditor); + Util::show(this->regionMapEditor); } void MainWindow::on_pushButton_CreatePrefab_clicked() { @@ -3133,7 +3172,7 @@ bool MainWindow::closeSupplementaryWindows() { if (widget != this && widget->isWindow()) { // Make sure the window is raised and activated before closing in case it has a confirmation prompt. if (widget->isVisible()) { - openSubWindow(widget); + Util::show(widget); } if (!widget->close()) { QString message = QStringLiteral("Aborted project close"); diff --git a/src/project.cpp b/src/project.cpp index 1fab6300..af2bf0c2 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -32,9 +32,7 @@ int Project::num_pals_total = 13; Project::Project(QObject *parent) : QObject(parent) -{ - QObject::connect(&this->fileWatcher, &QFileSystemWatcher::fileChanged, this, &Project::recordFileChange); -} +{ } Project::~Project() { @@ -186,7 +184,10 @@ int Project::getSupportedMajorVersion(QString *errorOut) { bool Project::load() { this->parser.setUpdatesSplashScreen(true); + resetFileWatcher(); resetFileCache(); + QPixmapCache::clear(); + this->disabledSettingsNames.clear(); bool success = readGlobalConstants() && readMapLayouts() @@ -225,6 +226,7 @@ bool Project::load() { initNewLayoutSettings(); initNewMapSettings(); applyParsedLimits(); + logFileWatchStatus(); } this->parser.setUpdatesSplashScreen(false); return success; @@ -232,7 +234,6 @@ bool Project::load() { void Project::resetFileCache() { this->parser.clearFileCache(); - this->failedFileWatchPaths.clear(); const QSet filepaths = { // Whenever we load a tileset we'll need to parse some data from these files, so we cache them to avoid the overhead of opening the files. @@ -274,6 +275,17 @@ void Project::clearTilesetCache() { this->tilesetCache.clear(); } +void Project::cacheTileset(const QString &name, Tileset *tileset) { + auto it = this->tilesetCache.constFind(name); + if (it != this->tilesetCache.constEnd() && it.value() && tileset != it.value()) { + // Callers of this function should ensure this doesn't happen, + // but in case it does we should avoid leaking memory. + logWarn(QString("New tileset %1 overwrote existing tileset.").arg(name)); + delete it.value(); + } + this->tilesetCache.insert(name, tileset); +} + Map* Project::loadMap(const QString &mapName) { if (mapName == getDynamicMapName()) { // Silently ignored, caller is expected to handle this if they want this to be an error. @@ -348,10 +360,10 @@ QSet Project::getTopLevelMapFields() const { "show_map_name", "battle_scene", "connections", - "object_events", - "warp_events", - "coord_events", - "bg_events", + Event::groupToJsonKey(Event::Group::Object), + Event::groupToJsonKey(Event::Group::Warp), + Event::groupToJsonKey(Event::Group::Coord), + Event::groupToJsonKey(Event::Group::Bg), "shared_events_map", "shared_scripts_map", }; @@ -450,10 +462,10 @@ bool Project::loadMapData(Map* map) { static const QMap defaultEventTypes = { // Map of the expected keys for each event group, and the default type of that group. // If the default type is Type::None then each event must specify its type, or its an error. - {"object_events", Event::Type::Object}, - {"warp_events", Event::Type::Warp}, - {"coord_events", Event::Type::None}, - {"bg_events", Event::Type::None}, + {Event::groupToJsonKey(Event::Group::Object), Event::Type::Object}, + {Event::groupToJsonKey(Event::Group::Warp), Event::Type::Warp}, + {Event::groupToJsonKey(Event::Group::Coord), Event::Type::None}, + {Event::groupToJsonKey(Event::Group::Bg), Event::Type::None}, }; for (auto i = defaultEventTypes.constBegin(); i != defaultEventTypes.constEnd(); i++) { QString eventGroupKey = i.key(); @@ -749,16 +761,21 @@ bool Project::saveMapLayouts() { } bool Project::watchFile(const QString &filename) { + if (!porymapConfig.monitorFiles) + return true; + + if (!this->fileWatcher) { + // Only create the file watcher when it's first needed (even an empty QFileSystemWatcher will consume system resources). + this->fileWatcher = new QFileSystemWatcher(this); + QObject::connect(this->fileWatcher, &QFileSystemWatcher::fileChanged, this, &Project::recordFileChange); + } + QString filepath = filename.startsWith(this->root) ? filename : QString("%1/%2").arg(this->root).arg(filename); - if (!this->fileWatcher.addPath(filepath) && !this->fileWatcher.files().contains(filepath)) { + if (!this->fileWatcher->addPath(filepath) && !this->fileWatcher->files().contains(filepath)) { // We failed to watch the file, and this wasn't a file we were already watching. - // Log a warning, but only if A. we actually care that we failed, because 'monitor files' is enabled, - // B. we haven't logged a warning for this file yet, and C. we would have otherwise been able to watch it, because the file exists. - if (porymapConfig.monitorFiles && !this->failedFileWatchPaths.contains(filepath) && QFileInfo::exists(filepath)) { + // Record the filepath for logging later, assuming we should have been able to watch the file. + if (QFileInfo::exists(filepath)) { this->failedFileWatchPaths.insert(filepath); - logWarn(QString("Failed to add '%1' to file watcher. Currently watching %2 files.") - .arg(Util::stripPrefix(filepath, this->root)) - .arg(this->fileWatcher.files().length())); } return false; } @@ -774,8 +791,11 @@ bool Project::watchFiles(const QStringList &filenames) { } bool Project::stopFileWatch(const QString &filename) { + if (!this->fileWatcher) + return true; + QString filepath = filename.startsWith(this->root) ? filename : QString("%1/%2").arg(this->root).arg(filename); - return this->fileWatcher.removePath(filepath); + return this->fileWatcher->removePath(filepath); } void Project::ignoreWatchedFileTemporarily(const QString &filepath) { @@ -794,8 +814,8 @@ void Project::recordFileChange(const QString &filepath) { // Note: As a safety measure, many applications save an open file by writing a new file and then deleting the old one. // In your slot function, you can check watcher.files().contains(path). // If it returns false, check whether the file still exists and then call addPath() to continue watching it. - if (!this->fileWatcher.files().contains(filepath) && QFileInfo::exists(filepath)) { - this->fileWatcher.addPath(filepath); + if (this->fileWatcher && !this->fileWatcher->files().contains(filepath) && QFileInfo::exists(filepath)) { + this->fileWatcher->addPath(filepath); } if (this->modifiedFiles.contains(filepath)) { @@ -815,6 +835,38 @@ void Project::recordFileChange(const QString &filepath) { emit fileChanged(filepath); } +// When calling 'watchFile' we record failures rather than log them immediately. +// We do this primarily to condense the warning if we fail to monitor any files. +void Project::logFileWatchStatus() { + if (!this->fileWatcher) + return; + + int numSuccessful = this->fileWatcher->files().length(); + int numAttempted = numSuccessful + this->failedFileWatchPaths.count(); + if (numAttempted == 0) + return; + + if (numSuccessful == 0) { + // We failed to watch every file we tried. As of writing this happens if Porymap is running + // on Windows and the project files are in WSL2. Rather than filling the log by + // outputting a warning for every file, just log that we failed to monitor any of them. + logWarn(QString("Failed to monitor project files")); + return; + } else { + logInfo(QString("Successfully monitoring %1/%2 project files").arg(numSuccessful).arg(numAttempted)); + } + + for (const auto &failedPath : this->failedFileWatchPaths) { + logWarn(QString("Failed to monitor project file '%1'").arg(failedPath)); + } +} + +void Project::resetFileWatcher() { + this->failedFileWatchPaths.clear(); + delete this->fileWatcher; + this->fileWatcher = nullptr; +} + bool Project::saveMapGroups() { QString mapGroupsFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_map_groups)); QFile mapGroupsFile(mapGroupsFilepath); @@ -1135,13 +1187,44 @@ bool Project::loadLayoutTilesets(Layout *layout) { logError(QString("Failed to load %1: missing secondary tileset label.").arg(layout->name)); return false; } + if (!this->primaryTilesetLabels.contains(layout->tileset_primary_label)) { + logError(QString("Failed to load %1: unknown primary tileset label '%2'.") + .arg(layout->name) + .arg(layout->tileset_primary_label)); + return false; + } + if (!this->secondaryTilesetLabels.contains(layout->tileset_secondary_label)) { + logError(QString("Failed to load %1: unknown secondary tileset label '%2'.") + .arg(layout->name) + .arg(layout->tileset_secondary_label)); + return false; + } layout->tileset_primary = getTileset(layout->tileset_primary_label); layout->tileset_secondary = getTileset(layout->tileset_secondary_label); return layout->tileset_primary && layout->tileset_secondary; } -Tileset* Project::loadTileset(QString label, Tileset *tileset) { +Tileset* Project::getTileset(const QString &label, bool forceLoad) { + if (!this->tilesetLabelsOrdered.contains(label)) { + logError(QString("Unknown tileset name '%1'.").arg(label)); + return nullptr; + } + + Tileset *tileset = nullptr; + + auto it = this->tilesetCache.constFind(label); + if (it != this->tilesetCache.constEnd()) { + tileset = it.value(); + if (!forceLoad) { + return tileset; + } + } else { + // Create a cache entry even if we don't end up loading the tileset successfully. + // This will prevent repeated file reads if the tileset fails to load. + cacheTileset(label, nullptr); + } + auto memberMap = Tileset::getHeaderMemberMap(this->usingAsmTilesets); if (this->usingAsmTilesets) { // Read asm tileset header. Backwards compatibility @@ -1186,7 +1269,7 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { return nullptr; } - tilesetCache.insert(label, tileset); + cacheTileset(tileset->name, tileset); return tileset; } @@ -1216,7 +1299,7 @@ void Project::setNewLayoutBorder(Layout *layout) { } else { // Fill the border with the default metatiles from the config. for (int i = 0; i < width * height; i++) { - layout->border.append(projectConfig.newMapBorderMetatileIds.at(i)); + layout->border.append(projectConfig.newMapBorderMetatileIds.value(i)); } } @@ -1313,43 +1396,27 @@ bool Project::saveMap(Map *map, bool skipLayout) { mapObj["connections"] = OrderedJson(); } - if (map->sharedEventsMap().isEmpty()) { - // Object events - OrderedJson::array objectEventsArr; - for (const auto &event : map->getEvents(Event::Group::Object)){ - objectEventsArr.push_back(event->buildEventJson(this)); - } - mapObj["object_events"] = objectEventsArr; - - - // Warp events - OrderedJson::array warpEventsArr; - for (const auto &event : map->getEvents(Event::Group::Warp)) { - warpEventsArr.push_back(event->buildEventJson(this)); - } - mapObj["warp_events"] = warpEventsArr; - - // Coord events - OrderedJson::array coordEventsArr; - for (const auto &event : map->getEvents(Event::Group::Coord)) { - coordEventsArr.push_back(event->buildEventJson(this)); - } - mapObj["coord_events"] = coordEventsArr; - - // Bg Events - OrderedJson::array bgEventsArr; - for (const auto &event : map->getEvents(Event::Group::Bg)) { - bgEventsArr.push_back(event->buildEventJson(this)); - } - mapObj["bg_events"] = bgEventsArr; - } else { + if (map->isInheritingEvents()) { mapObj["shared_events_map"] = map->sharedEventsMap(); } - - if (!map->sharedScriptsMap().isEmpty()) { + if (map->isInheritingScripts()) { mapObj["shared_scripts_map"] = map->sharedScriptsMap(); } + if (!map->isInheritingEvents()) { + auto buildEventsJson = [this, map](Event::Group group, OrderedJson::object *json) { + OrderedJson::array arr; + for (const auto &event : map->getEvents(group)){ + arr.push_back(event->buildEventJson(this)); + } + (*json)[Event::groupToJsonKey(group)] = arr; + }; + buildEventsJson(Event::Group::Object, &mapObj); + buildEventsJson(Event::Group::Warp, &mapObj); + buildEventsJson(Event::Group::Coord, &mapObj); + buildEventsJson(Event::Group::Bg, &mapObj); + } + // Update the global heal locations array using the Map's heal location events. // This won't get saved to disc until Project::saveHealLocations is called. QList hlEvents; @@ -1478,7 +1545,7 @@ void Project::readTilesetPaths(Tileset* tileset) { tileset->metatile_attrs_path = defaultPath + "/metatile_attributes.bin"; if (tileset->palettePaths.isEmpty()) { QString palettes_dir_path = defaultPath + "/palettes/"; - for (int i = 0; i < 16; i++) { + for (int i = 0; i < Tileset::maxPalettes(); i++) { tileset->palettePaths.append(palettes_dir_path + QString("%1").arg(i, 2, 10, QLatin1Char('0')) + ".pal"); } } @@ -1519,9 +1586,8 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa tileset->loadTilesImage(&tilesImage); // Create default metatiles - const int numMetatiles = tileset->is_secondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); const int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - for (int i = 0; i < numMetatiles; ++i) { + for (int i = 0; i < tileset->maxMetatiles(); ++i) { auto metatile = new Metatile(); for(int j = 0; j < tilesPerMetatile; ++j){ Tile tile = Tile(); @@ -1541,9 +1607,9 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa } // Create default palettes - for(int i = 0; i < 16; ++i) { + for(int i = 0; i < Tileset::maxPalettes(); ++i) { QList currentPal; - for(int i = 0; i < 16;++i) { + for(int i = 0; i < Tileset::numColorsPerPalette();++i) { currentPal.append(qRgb(0,0,0)); } tileset->palettes.append(currentPal); @@ -1579,15 +1645,14 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa metatilesFilepath.append(projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles)); } ignoreWatchedFilesTemporarily({headersFilepath, graphicsFilepath, metatilesFilepath}); - name.remove(0, prefix.length()); // Strip prefix from name to get base name for use in other symbols. - tileset->appendToHeaders(headersFilepath, name, this->usingAsmTilesets); - tileset->appendToGraphics(graphicsFilepath, name, this->usingAsmTilesets); - tileset->appendToMetatiles(metatilesFilepath, name, this->usingAsmTilesets); + QString baseName = Tileset::stripPrefix(name); + tileset->appendToHeaders(headersFilepath, baseName, this->usingAsmTilesets); + tileset->appendToGraphics(graphicsFilepath, baseName, this->usingAsmTilesets); + tileset->appendToMetatiles(metatilesFilepath, baseName, this->usingAsmTilesets); tileset->save(); - this->tilesetCache.insert(tileset->name, tileset); - + cacheTileset(tileset->name, tileset); emit tilesetCreated(tileset); return tileset; } @@ -1636,26 +1701,13 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) { QString metatileLabelPrefix = tileset->getMetatileLabelPrefix(); // Reverse map for faster lookup by metatile id + tileset->metatileLabels.clear(); for (auto it = this->metatileLabelsMap[tileset->name].constBegin(); it != this->metatileLabelsMap[tileset->name].constEnd(); it++) { QString labelName = it.key(); tileset->metatileLabels[it.value()] = labelName.replace(metatileLabelPrefix, ""); } } -Tileset* Project::getTileset(QString label, bool forceLoad) { - Tileset *existingTileset = nullptr; - if (tilesetCache.contains(label)) { - existingTileset = tilesetCache.value(label); - } - - if (existingTileset && !forceLoad) { - return existingTileset; - } else { - Tileset *tileset = loadTileset(label, existingTileset); - return tileset; - } -} - bool Project::saveTextFile(const QString &path, const QString &text) { QFile file(path); if (!file.open(QIODevice::WriteOnly)) { @@ -2228,6 +2280,7 @@ bool Project::readTilesetLabels() { this->primaryTilesetLabels.clear(); this->secondaryTilesetLabels.clear(); this->tilesetLabelsOrdered.clear(); + clearTilesetCache(); QString filename = projectConfig.getFilePath(ProjectFilePath::tilesets_headers); QFileInfo fileInfo(this->root + "/" + filename); @@ -2309,7 +2362,7 @@ bool Project::readFieldmapProperties() { logWarn(QString("Value for '%1' not found. Using default (%2) instead.").arg(name).arg(*dest)); } }; - loadDefine(numPalsTotalName, &Project::num_pals_total, 2, INT_MAX); // In reality the max would be 16, but as far as Porymap is concerned it doesn't matter. + loadDefine(numPalsTotalName, &Project::num_pals_total, 2, Tileset::maxPalettes()); loadDefine(numTilesTotalName, &Project::num_tiles_total, 2, 1024); // 1024 is fixed because we store tile IDs in a 10-bit field. loadDefine(numPalsPrimaryName, &Project::num_pals_primary, 1, Project::num_pals_total - 1); loadDefine(numTilesPrimaryName, &Project::num_tiles_primary, 1, Project::num_tiles_total - 1); @@ -2361,12 +2414,11 @@ bool Project::readFieldmapProperties() { // We can determine whether triple-layer metatiles are in-use by reading this constant. // If the constant is missing (or is using a value other than 8 or 12) the user must tell // us whether they're using triple-layer metatiles under Project Settings. - static const int numTilesPerLayer = 4; int numTilesPerMetatile = it.value(); - if (numTilesPerMetatile == 2 * numTilesPerLayer) { + if (numTilesPerMetatile == 2 * Metatile::tilesPerLayer()) { projectConfig.tripleLayerMetatilesEnabled = false; this->disabledSettingsNames.insert(numTilesPerMetatileName); - } else if (numTilesPerMetatile == 3 * numTilesPerLayer) { + } else if (numTilesPerMetatile == 3 * Metatile::tilesPerLayer()) { projectConfig.tripleLayerMetatilesEnabled = true; this->disabledSettingsNames.insert(numTilesPerMetatileName); } @@ -2423,6 +2475,7 @@ bool Project::readFieldmapMasks() { projectConfig.blockCollisionMask = blockMask; if (readBlockMask(elevationMaskName, &blockMask)) projectConfig.blockElevationMask = blockMask; + Block::setLayout(); // Read RSE metatile attribute masks auto it = defines.find(behaviorMaskName); @@ -2602,7 +2655,10 @@ void Project::setRegionMapEntries(const QHash &entries QHash Project::getRegionMapEntries() const { QHash entries; for (auto it = this->locationData.constBegin(); it != this->locationData.constEnd(); it++) { - entries[it.key()] = it.value().map; + const MapSectionEntry regionMapData = it.value().map; + if (regionMapData.valid) { + entries[it.key()] = regionMapData; + } } return entries; } @@ -2928,13 +2984,19 @@ bool Project::readGlobalConstants() { bool Project::readEventScriptLabels() { this->globalScriptLabels.clear(); - if (porymapConfig.loadAllEventScripts) { - for (const auto &filePath : getEventScriptsFilepaths()) - this->globalScriptLabels << ParseUtil::getGlobalScriptLabels(filePath); - - this->globalScriptLabels.sort(Qt::CaseInsensitive); - this->globalScriptLabels.removeDuplicates(); + QStringList paths; + if (porymapConfig.scriptAutocompleteMode == ScriptAutocompleteMode::All) { + paths = getAllEventScriptsFilepaths(); + } else if (porymapConfig.scriptAutocompleteMode == ScriptAutocompleteMode::MapAndCommon) { + paths = getCommonEventScriptsFilepaths(); } + + for (const auto &path : paths) { + this->globalScriptLabels << ParseUtil::getGlobalScriptLabels(path); + } + this->globalScriptLabels.sort(Qt::CaseInsensitive); + this->globalScriptLabels.removeDuplicates(); + emit eventScriptLabelsRead(); return true; } @@ -2970,30 +3032,46 @@ QString Project::getScriptDefaultString(bool usePoryScript, QString mapName) con return QString("%1_MapScripts::\n\t.byte 0\n").arg(mapName); } -QStringList Project::getEventScriptsFilepaths() const { - QStringList filePaths(QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts))); - const QString scriptsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_scripts_folders)); - const QString mapsDir = QDir::cleanPath(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_map_folders)); +QStringList Project::getAllEventScriptsFilepaths() const { + return getMapScriptsFilepaths() + getCommonEventScriptsFilepaths(); +} - if (projectConfig.usePoryScript) { - QDirIterator it_pory_shared(scriptsDir, {"*.pory"}, QDir::Files); - while (it_pory_shared.hasNext()) - filePaths << it_pory_shared.next(); +// Get the paths for all "scripts.inc" / "scripts.pory" files in the data/maps/*/ folders. +QStringList Project::getMapScriptsFilepaths() const { + return findScriptsFiles(projectConfig.getFilePath(ProjectFilePath::data_map_folders), {"scripts"}); +} - QDirIterator it_pory_maps(mapsDir, {"scripts.pory"}, QDir::Files, QDirIterator::Subdirectories); - while (it_pory_maps.hasNext()) - filePaths << it_pory_maps.next(); +// Get the paths for all "*.inc" / "*.pory" files in the data/scripts/ folder, + data/event_scripts.s. +QStringList Project::getCommonEventScriptsFilepaths() const { + QStringList paths = { QDir::cleanPath(this->root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts)) }; + return paths + findScriptsFiles(projectConfig.getFilePath(ProjectFilePath::data_scripts_folders)); +} + +QStringList Project::findScriptsFiles(const QString &searchDir, const QStringList &fileNames) const { + QStringList paths; + + QString dir = searchDir; + if (!dir.startsWith(this->root)) { + dir = QDir::cleanPath(this->root + "/" + dir); } - QDirIterator it_inc_shared(scriptsDir, {"*.inc"}, QDir::Files); - while (it_inc_shared.hasNext()) - filePaths << it_inc_shared.next(); + QStringList filters; + for (const auto &s : fileNames) { + filters.append(s + getScriptFileExtension(false)); + if (projectConfig.usePoryScript) { + filters.append(s + getScriptFileExtension(true)); + } + } - QDirIterator it_inc_maps(mapsDir, {"scripts.inc"}, QDir::Files, QDirIterator::Subdirectories); - while (it_inc_maps.hasNext()) - filePaths << it_inc_maps.next(); - - return filePaths; + // TODO: Filter out .inc files that are generated by a .pory file. + // They won't cause problems for the user, but it'll create extra parsing work later. + if (!filters.isEmpty()) { + QDirIterator it(dir, filters, QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); + while (it.hasNext()) { + paths.append(it.next()); + } + } + return paths; } void Project::loadEventPixmap(Event *event, bool forceLoad) { @@ -3380,14 +3458,25 @@ QString Project::getEmptySpeciesName() { return projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_species_empty); } -// Get the distance in metatiles (rounded up) that the player is able to see in each direction in-game. +// Get the distance in pixels that the player is able to see in each direction in-game, +// rounded up to a multiple of a metatile's pixel size. +QMargins Project::getPixelViewDistance() { + QMargins viewDistance = projectConfig.playerViewDistance; + viewDistance.setTop(Util::roundUpToMultiple(viewDistance.top(), Metatile::pixelHeight())); + viewDistance.setBottom(Util::roundUpToMultiple(viewDistance.bottom(), Metatile::pixelHeight())); + viewDistance.setLeft(Util::roundUpToMultiple(viewDistance.left(), Metatile::pixelWidth())); + viewDistance.setRight(Util::roundUpToMultiple(viewDistance.right(), Metatile::pixelWidth())); + return viewDistance; +} + +// Get the distance in metatiles that the player is able to see in each direction in-game. // For the default view distance (i.e. assuming the player is centered in a 240x160 pixel GBA screen) this is 7x5 metatiles. QMargins Project::getMetatileViewDistance() { - QMargins viewDistance = projectConfig.playerViewDistance; - viewDistance.setTop(qCeil(viewDistance.top() / 16.0)); - viewDistance.setBottom(qCeil(viewDistance.bottom() / 16.0)); - viewDistance.setLeft(qCeil(viewDistance.left() / 16.0)); - viewDistance.setRight(qCeil(viewDistance.right() / 16.0)); + QMargins viewDistance = getPixelViewDistance(); + viewDistance.setTop(viewDistance.top() / Metatile::pixelHeight()); + viewDistance.setBottom(viewDistance.bottom() / Metatile::pixelHeight()); + viewDistance.setLeft(viewDistance.left() / Metatile::pixelWidth()); + viewDistance.setRight(viewDistance.right() / Metatile::pixelWidth()); return viewDistance; } @@ -3417,15 +3506,14 @@ void Project::applyParsedLimits() { projectConfig.metatileEncounterTypeMask &= maxMask; projectConfig.metatileLayerTypeMask &= maxMask; - Block::setLayout(); Metatile::setLayout(this); - Project::num_metatiles_primary = qMin(qMax(Project::num_metatiles_primary, 1), Block::getMaxMetatileId() + 1); + Project::num_metatiles_primary = qBound(1, Project::num_metatiles_primary, Block::getMaxMetatileId() + 1); projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); - projectConfig.collisionSheetSize.setHeight(qMin(qMax(projectConfig.collisionSheetSize.height(), 1), Block::getMaxElevation() + 1)); - projectConfig.collisionSheetSize.setWidth(qMin(qMax(projectConfig.collisionSheetSize.width(), 1), Block::getMaxCollision() + 1)); + projectConfig.collisionSheetSize.setHeight(qBound(1, projectConfig.collisionSheetSize.height(), Block::getMaxElevation() + 1)); + projectConfig.collisionSheetSize.setWidth(qBound(1, projectConfig.collisionSheetSize.width(), Block::getMaxCollision() + 1)); } bool Project::hasUnsavedChanges() { @@ -3445,3 +3533,34 @@ bool Project::hasUnsavedChanges() { } return false; } + +// Searches the project's map layouts to find the names of the tilesets that the provided tileset gets paired with. +QSet Project::getPairedTilesetLabels(const Tileset *tileset) const { + QSet pairedLabels; + for (const auto &layout : this->mapLayouts) { + if (tileset->is_secondary) { + if (layout->tileset_secondary_label == tileset->name) { + pairedLabels.insert(layout->tileset_primary_label); + } + } else if (layout->tileset_primary_label == tileset->name) { + pairedLabels.insert(layout->tileset_secondary_label); + } + } + return pairedLabels; +} + +// Returns the set of IDs for the layouts that use the specified tilesets. +// nullptr for either tileset is treated as a wildcard (so 'getTilesetLayouts(nullptr, nullptr)' returns all layout IDs). +QSet Project::getTilesetLayoutIds(const Tileset *primaryTileset, const Tileset *secondaryTileset) const { + // Note: We're intentioanlly just returning the layout IDs, and not the pointer to the layout. + // The layout may not be loaded yet (which isn't obvious), and we should leave it up to the caller to request that. + QSet layoutIds; + for (const auto &layout : this->mapLayouts) { + if (primaryTileset && primaryTileset->name != layout->tileset_primary_label) + continue; + if (secondaryTileset && secondaryTileset->name != layout->tileset_secondary_label) + continue; + layoutIds.insert(layout->id); + } + return layoutIds; +} diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 08bfc9c5..dcd4fc33 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -1,9 +1,11 @@ +#ifdef QT_QML_LIB #include "mainwindow.h" #include "ui_mainwindow.h" #include "scripting.h" #include "editcommands.h" #include "config.h" #include "imageproviders.h" +#include "scriptutility.h" // TODO: "tilesetNeedsRedraw" is used when redrawing the map after // changing a metatile's tiles via script. It is unnecessarily @@ -26,7 +28,7 @@ void MainWindow::tryRedrawMapArea(bool forceRedraw) { if (this->tilesetEditor) this->tilesetEditor->updateTilesets(this->editor->layout->tileset_primary_label, this->editor->layout->tileset_secondary_label); if (this->editor->metatile_selector_item) - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); if (this->editor->selected_border_metatiles_item) this->editor->selected_border_metatiles_item->draw(); if (this->editor->current_metatile_selection_item) @@ -40,6 +42,17 @@ void MainWindow::tryRedrawMapArea(bool forceRedraw) { } } +void MainWindow::redrawResizedMapArea() { + // We're calling this on a timer because we want to defer the redraw + // until it's time to process events. This avoids some potential crashes, + // as redrawMapScene will destroy a handful of objects capable of triggering + // script API callbacks; we want to let them finish what they were doing + // before we destroying them. + // NOTE: This has the same problem as ScriptEditLayout, namely that it's + // doing more work than necessary and can be prohibitively slow. + QTimer::singleShot(0, this, &MainWindow::redrawMapScene); +} + void MainWindow::tryCommitMapChanges(bool commitChanges) { if (commitChanges) { Layout *layout = this->editor->layout; @@ -95,21 +108,15 @@ void MainWindow::setBlocksFromSelection(int x, int y, bool forceRedraw, bool com int MainWindow::getMetatileId(int x, int y) { if (!this->editor || !this->editor->layout) return 0; - Block block; - if (!this->editor->layout->getBlock(x, y, &block)) { - return 0; - } - return block.metatileId(); + return this->editor->layout->getMetatileId(x, y); } void MainWindow::setMetatileId(int x, int y, int metatileId, bool forceRedraw, bool commitChanges) { if (!this->editor || !this->editor->layout) return; - Block block; - if (!this->editor->layout->getBlock(x, y, &block)) { + if (!this->editor->layout->setMetatileId(x, y, metatileId)) { return; } - this->editor->layout->setBlock(x, y, Block(metatileId, block.collision(), block.elevation())); this->tryCommitMapChanges(commitChanges); this->tryRedrawMapArea(forceRedraw); } @@ -231,7 +238,7 @@ void MainWindow::setDimensions(int width, int height) { return; this->editor->layout->setDimensions(width, height); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } void MainWindow::setWidth(int width) { @@ -241,7 +248,7 @@ void MainWindow::setWidth(int width) { return; this->editor->layout->setDimensions(width, this->editor->layout->getHeight()); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } void MainWindow::setHeight(int height) { @@ -251,7 +258,7 @@ void MainWindow::setHeight(int height) { return; this->editor->layout->setDimensions(this->editor->layout->getWidth(), height); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } //===================== @@ -301,7 +308,7 @@ void MainWindow::setBorderDimensions(int width, int height) { return; this->editor->layout->setBorderDimensions(width, height); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } void MainWindow::setBorderWidth(int width) { @@ -311,7 +318,7 @@ void MainWindow::setBorderWidth(int width) { return; this->editor->layout->setBorderDimensions(width, this->editor->layout->getBorderHeight()); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } void MainWindow::setBorderHeight(int height) { @@ -321,7 +328,7 @@ void MainWindow::setBorderHeight(int height) { return; this->editor->layout->setBorderDimensions(this->editor->layout->getBorderWidth(), height); this->tryCommitMapChanges(true); - this->redrawMapScene(); + this->redrawResizedMapArea(); } //====================== @@ -332,7 +339,7 @@ void MainWindow::refreshAfterPaletteChange(Tileset *tileset) { if (this->tilesetEditor) { this->tilesetEditor->updateTilesets(this->editor->layout->tileset_primary_label, this->editor->layout->tileset_secondary_label); } - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); this->editor->selected_border_metatiles_item->draw(); this->editor->map_item->draw(true); this->editor->updateMapBorder(); @@ -345,10 +352,7 @@ void MainWindow::setTilesetPalette(Tileset *tileset, int paletteIndex, QList= tileset->palettes.size()) return; - if (colors.size() != 16) - return; - - for (int i = 0; i < 16; i++) { + for (int i = 0; i < qMin(colors.length(), Tileset::numColorsPerPalette()); i++) { if (colors[i].size() != 3) continue; tileset->palettes[paletteIndex][i] = qRgb(colors[i][0], colors[i][1], colors[i][2]); @@ -444,7 +448,7 @@ QJSValue MainWindow::getSecondaryTilesetPalettes() { } void MainWindow::refreshAfterPalettePreviewChange() { - this->editor->metatile_selector_item->draw(); + this->editor->metatile_selector_item->refresh(); this->editor->selected_border_metatiles_item->draw(); this->editor->map_item->draw(true); this->editor->updateMapBorder(); @@ -456,10 +460,7 @@ void MainWindow::setTilesetPalettePreview(Tileset *tileset, int paletteIndex, QL return; if (paletteIndex >= tileset->palettePreviews.size()) return; - if (colors.size() != 16) - return; - - for (int i = 0; i < 16; i++) { + for (int i = 0; i < qMin(colors.length(), Tileset::numColorsPerPalette()); i++) { if (colors[i].size() != 3) continue; tileset->palettePreviews[paletteIndex][i] = qRgb(colors[i][0], colors[i][1], colors[i][2]); @@ -545,13 +546,13 @@ int MainWindow::getNumSecondaryTilesetMetatiles() { int MainWindow::getNumPrimaryTilesetTiles() { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_primary) return 0; - return this->editor->layout->tileset_primary->tiles.length(); + return this->editor->layout->tileset_primary->numTiles(); } int MainWindow::getNumSecondaryTilesetTiles() { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_secondary) return 0; - return this->editor->layout->tileset_secondary->tiles.length(); + return this->editor->layout->tileset_secondary->numTiles(); } QString MainWindow::getPrimaryTileset() { @@ -566,14 +567,6 @@ QString MainWindow::getSecondaryTileset() { return this->editor->layout->tileset_secondary->name; } -void MainWindow::setPrimaryTileset(QString tileset) { - this->on_comboBox_PrimaryTileset_currentTextChanged(tileset); -} - -void MainWindow::setSecondaryTileset(QString tileset) { - this->on_comboBox_SecondaryTileset_currentTextChanged(tileset); -} - void MainWindow::saveMetatilesByMetatileId(int metatileId) { Tileset * tileset = Tileset::getMetatileTileset(metatileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); if (tileset) @@ -796,17 +789,45 @@ void MainWindow::setMetatileTile(int metatileId, int tileIndex, QJSValue tileObj QJSValue MainWindow::getTilePixels(int tileId) { if (tileId < 0 || !this->editor || !this->editor->layout) return QJSValue(); + QImage tileImage = getTileImage(tileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (tileImage.isNull() || tileImage.sizeInBytes() < 64) + if (tileImage.isNull() || tileImage.sizeInBytes() < Tile::numPixels()) return QJSValue(); + const uchar * pixels = tileImage.constBits(); - QJSValue pixelArray = Scripting::getEngine()->newArray(64); - for (int i = 0; i < 64; i++) { + QJSValue pixelArray = Scripting::getEngine()->newArray(Tile::numPixels()); + for (int i = 0; i < Tile::numPixels(); i++) { pixelArray.setProperty(i, pixels[i]); } return pixelArray; } +QList MainWindow::getMetatileLayerOrder() const { + if (!this->editor || !this->editor->layout) + return QList(); + return this->editor->layout->metatileLayerOrder(); +} + +void MainWindow::setMetatileLayerOrder(const QList &order) { + if (!this->editor || !this->editor->layout || !ScriptUtility::validateMetatileLayerOrder(order)) + return; + this->editor->layout->setMetatileLayerOrder(order); + this->refreshAfterPalettePreviewChange(); +} + +QList MainWindow::getMetatileLayerOpacity() const { + if (!this->editor || !this->editor->layout) + return QList(); + return this->editor->layout->metatileLayerOpacity(); +} + +void MainWindow::setMetatileLayerOpacity(const QList &opacities) { + if (!this->editor || !this->editor->layout) + return; + this->editor->layout->setMetatileLayerOpacity(opacities); + this->refreshAfterPalettePreviewChange(); +} + //===================== // Editing map header //===================== @@ -943,3 +964,5 @@ void MainWindow::setFloorNumber(int floorNumber) { this->editor->map->header()->setFloorNumber(floorNumber); } + +#endif // QT_QML_LIB diff --git a/src/scriptapi/apioverlay.cpp b/src/scriptapi/apioverlay.cpp index 4e183659..e2ca888f 100644 --- a/src/scriptapi/apioverlay.cpp +++ b/src/scriptapi/apioverlay.cpp @@ -1,6 +1,8 @@ +#ifdef QT_QML_LIB #include "mapview.h" #include "scripting.h" #include "imageproviders.h" +#include "editor.h" void MapView::updateScene() { if (this->scene()) { @@ -294,13 +296,11 @@ void MapView::addTileImage(int x, int y, const Tile &tile, bool setTransparency, void MapView::addMetatileImage(int x, int y, int metatileId, bool setTransparency, int layer) { if (!this->editor || !this->editor->layout || !this->editor->layout->tileset_primary || !this->editor->layout->tileset_secondary) return; - QImage image = getMetatileImage(static_cast(metatileId), - this->editor->layout->tileset_primary, - this->editor->layout->tileset_secondary, - this->editor->layout->metatileLayerOrder, - this->editor->layout->metatileLayerOpacity); + QImage image = getMetatileImage(static_cast(metatileId), this->editor->layout); if (setTransparency) image.setColor(0, qRgba(0, 0, 0, 0)); if (this->getOverlay(layer)->addImage(x, y, image)) this->updateScene(); } + +#endif // QT_QML_LIB diff --git a/src/scriptapi/apiutility.cpp b/src/scriptapi/apiutility.cpp index dfc65617..8f205de2 100644 --- a/src/scriptapi/apiutility.cpp +++ b/src/scriptapi/apiutility.cpp @@ -1,12 +1,10 @@ +#ifdef QT_QML_LIB +#include "scriptutility.h" #include "mainwindow.h" #include "ui_mainwindow.h" #include "scripting.h" #include "config.h" -ScriptUtility::ScriptUtility(MainWindow *mainWindow) { - this->window = mainWindow; -} - ScriptUtility::~ScriptUtility() { if (window && window->ui && window->ui->menuTools) { for (auto action : this->registeredActions) { @@ -201,46 +199,36 @@ QList ScriptUtility::getCustomScripts() { } QList ScriptUtility::getMetatileLayerOrder() { - if (!window || !window->editor || !window->editor->layout) - return QList(); - return window->editor->layout->metatileLayerOrder; + return Layout::globalMetatileLayerOrder(); } -void ScriptUtility::setMetatileLayerOrder(QList order) { - if (!window || !window->editor || !window->editor->layout) - return; - +bool ScriptUtility::validateMetatileLayerOrder(const QList &order) { const int numLayers = 3; - int size = order.size(); - if (size < numLayers) { - logError(QString("Metatile layer order has insufficient elements (%1), needs at least %2.").arg(size).arg(numLayers)); - return; - } - bool invalid = false; - for (int i = 0; i < numLayers; i++) { + bool valid = true; + for (int i = 0; i < qMin(order.length(), numLayers); i++) { int layer = order.at(i); if (layer < 0 || layer >= numLayers) { logError(QString("'%1' is not a valid metatile layer order value, must be in range 0-%2.").arg(layer).arg(numLayers - 1)); - invalid = true; + valid = false; } } - if (invalid) return; + return valid; +} - window->editor->layout->metatileLayerOrder = order; - window->refreshAfterPalettePreviewChange(); +void ScriptUtility::setMetatileLayerOrder(const QList &order) { + if (!validateMetatileLayerOrder(order)) + return; + Layout::setGlobalMetatileLayerOrder(order); + if (window) window->refreshAfterPalettePreviewChange(); } QList ScriptUtility::getMetatileLayerOpacity() { - if (!window || !window->editor || !window->editor->layout) - return QList(); - return window->editor->layout->metatileLayerOpacity; + return Layout::globalMetatileLayerOpacity(); } -void ScriptUtility::setMetatileLayerOpacity(QList order) { - if (!window || !window->editor || !window->editor->layout) - return; - window->editor->layout->metatileLayerOpacity = order; - window->refreshAfterPalettePreviewChange(); +void ScriptUtility::setMetatileLayerOpacity(const QList &opacities) { + Layout::setGlobalMetatileLayerOpacity(opacities); + if (window) window->refreshAfterPalettePreviewChange(); } QList ScriptUtility::getMapNames() { @@ -328,3 +316,5 @@ bool ScriptUtility::isPrimaryTileset(QString tilesetName) { bool ScriptUtility::isSecondaryTileset(QString tilesetName) { return getSecondaryTilesetNames().contains(tilesetName); } + +#endif // QT_QML_LIB diff --git a/src/scriptapi/scripting.cpp b/src/scriptapi/scripting.cpp index 93823de2..ff614c1a 100644 --- a/src/scriptapi/scripting.cpp +++ b/src/scriptapi/scripting.cpp @@ -1,8 +1,10 @@ +#if __has_include() #include #include "scripting.h" #include "log.h" #include "config.h" +#include "mainwindow.h" const QMap callbackFunctions = { {OnProjectOpened, "onProjectOpened"}, @@ -41,7 +43,7 @@ Scripting::Scripting(MainWindow *mainWindow) { const QStringList paths = userConfig.getCustomScriptPaths(); const QList enabled = userConfig.getCustomScriptsEnabled(); for (int i = 0; i < paths.length(); i++) { - if (enabled.at(i)) + if (enabled.value(i, true)) this->filepaths.append(paths.at(i)); } this->loadModules(this->filepaths); @@ -99,15 +101,12 @@ void Scripting::populateGlobalObject(MainWindow *mainWindow) { constants.setProperty("version", version); // Get basic tileset information - int numTilesPrimary = Project::getNumTilesPrimary(); - int numMetatilesPrimary = Project::getNumMetatilesPrimary(); - int numPalettesPrimary = Project::getNumPalettesPrimary(); - constants.setProperty("max_primary_tiles", numTilesPrimary); - constants.setProperty("max_secondary_tiles", Project::getNumTilesTotal() - numTilesPrimary); - constants.setProperty("max_primary_metatiles", numMetatilesPrimary); - constants.setProperty("max_secondary_metatiles", Project::getNumMetatilesTotal() - numMetatilesPrimary); - constants.setProperty("num_primary_palettes", numPalettesPrimary); - constants.setProperty("num_secondary_palettes", Project::getNumPalettesTotal() - numPalettesPrimary); + constants.setProperty("max_primary_tiles", Project::getNumTilesPrimary()); + constants.setProperty("max_secondary_tiles", Project::getNumTilesSecondary()); + constants.setProperty("max_primary_metatiles", Project::getNumMetatilesPrimary()); + constants.setProperty("max_secondary_metatiles", Project::getNumMetatilesSecondary()); + constants.setProperty("num_primary_palettes", Project::getNumPalettesPrimary()); + constants.setProperty("num_secondary_palettes", Project::getNumPalettesSecondary()); constants.setProperty("layers_per_metatile", projectConfig.getNumLayersInMetatile()); constants.setProperty("tiles_per_metatile", projectConfig.getNumTilesInMetatile()); @@ -301,7 +300,7 @@ void Scripting::cb_MapShifted(int xDelta, int yDelta) { instance->invokeCallback(OnMapShifted, args); } -void Scripting::cb_TilesetUpdated(QString tilesetName) { +void Scripting::cb_TilesetUpdated(const QString &tilesetName) { if (!instance) return; QJSValueList args { @@ -425,3 +424,6 @@ const QImage * Scripting::getImage(const QString &inputFilepath, bool useCache) instance->imageCache.insert(inputFilepath, image); return image; } + + +#endif // __has_include() diff --git a/src/ui/bordermetatilespixmapitem.cpp b/src/ui/bordermetatilespixmapitem.cpp index e793e466..97096423 100644 --- a/src/ui/bordermetatilespixmapitem.cpp +++ b/src/ui/bordermetatilespixmapitem.cpp @@ -20,9 +20,9 @@ void BorderMetatilesPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) Blockdata oldBorder = layout->border; - for (int i = 0; i < selection.dimensions.x() && (i + pos.x()) < width; i++) { - for (int j = 0; j < selection.dimensions.y() && (j + pos.y()) < height; j++) { - MetatileSelectionItem item = selection.metatileItems.at(j * selection.dimensions.x() + i); + for (int i = 0; i < selection.dimensions.width() && (i + pos.x()) < width; i++) { + for (int j = 0; j < selection.dimensions.height() && (j + pos.y()) < height; j++) { + MetatileSelectionItem item = selection.metatileItems.value(j * selection.dimensions.width() + i); layout->setBorderMetatileId(pos.x() + i, pos.y() + j, item.metatileId, true); } } @@ -39,21 +39,15 @@ void BorderMetatilesPixmapItem::draw() { int width = layout->getBorderWidth(); int height = layout->getBorderHeight(); - QImage image(16 * width, 16 * height, QImage::Format_RGBA8888); + QImage image(width * Metatile::pixelWidth(), height * Metatile::pixelHeight(), QImage::Format_RGBA8888); QPainter painter(&image); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { - int x = i * 16; - int y = j * 16; - QImage metatile_image = getMetatileImage( - layout->getBorderMetatileId(i, j), - layout->tileset_primary, - layout->tileset_secondary, - layout->metatileLayerOrder, - layout->metatileLayerOpacity); - QPoint metatile_origin = QPoint(x, y); - painter.drawImage(metatile_origin, metatile_image); + QImage metatile_image = getMetatileImage(layout->getBorderMetatileId(i, j), layout); + int x = i * Metatile::pixelWidth(); + int y = j * Metatile::pixelHeight(); + painter.drawImage(x, y, metatile_image); } } diff --git a/src/ui/checkeredbgscene.cpp b/src/ui/checkeredbgscene.cpp new file mode 100644 index 00000000..9a32dff7 --- /dev/null +++ b/src/ui/checkeredbgscene.cpp @@ -0,0 +1,32 @@ +#include "checkeredbgscene.h" + +#include + +void CheckeredBgScene::drawBackground(QPainter *painter, const QRectF &rect) { + QRect r = rect.toRect(); + int w = this->gridSize.width(); + int h = this->gridSize.height(); + int xMin = r.left() - (r.left() % w) - w; + int yMin = r.top() - (r.top() % h) - h; + int xMax = r.right() - (r.right() % w) + w; + int yMax = r.bottom() - (r.bottom() % h) + h; + + // draw grid from top to bottom of scene + QColor paintColor; + for (int x = xMin; x <= xMax; x += w) { + for (int y = yMin; y <= yMax; y += h) { + if ((x/w + y/h) % 2) { + if (this->validRect.contains(x, y)) + paintColor = QColor(132, 217, 165); // green light color + else + paintColor = 0xbcbcbc; // normal light color + } else { + if (this->validRect.contains(x, y)) + paintColor = QColor(76, 178, 121); // green dark color + else + paintColor = 0x969696; // normal dark color + } + painter->fillRect(QRect(x, y, w, h), paintColor); + } + } +} diff --git a/src/ui/collisionpixmapitem.cpp b/src/ui/collisionpixmapitem.cpp index e0587b08..ce06fb7c 100644 --- a/src/ui/collisionpixmapitem.cpp +++ b/src/ui/collisionpixmapitem.cpp @@ -2,52 +2,6 @@ #include "editcommands.h" #include "metatile.h" -void CollisionPixmapItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - if (pos != this->previousPos) { - this->previousPos = pos; - emit this->hoveredMapMovementPermissionChanged(pos.x(), pos.y()); - } - if (this->settings->betterCursors && this->getEditsEnabled()) { - setCursor(this->settings->mapCursor); - } -} - -void CollisionPixmapItem::hoverEnterEvent(QGraphicsSceneHoverEvent * event) { - this->has_mouse = true; - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - emit this->hoveredMapMovementPermissionChanged(pos.x(), pos.y()); -} - -void CollisionPixmapItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *) { - emit this->hoveredMapMovementPermissionCleared(); - if (this->settings->betterCursors && this->getEditsEnabled()){ - unsetCursor(); - } - this->has_mouse = false; -} - -void CollisionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - this->paint_tile_initial_x = this->straight_path_initial_x = pos.x(); - this->paint_tile_initial_y = this->straight_path_initial_y = pos.y(); - emit mouseEvent(event, this); -} - -void CollisionPixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - if (pos != this->previousPos) { - this->previousPos = pos; - emit this->hoveredMapMovementPermissionChanged(pos.x(), pos.y()); - } - emit mouseEvent(event, this); -} - -void CollisionPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - this->lockedAxis = CollisionPixmapItem::Axis::None; - emit mouseEvent(event, this); -} - void CollisionPixmapItem::draw(bool ignoreCache) { if (this->layout) { this->layout->setCollisionItem(this); diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index c6e606b0..db05ffab 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -100,9 +100,9 @@ void ConnectionPixmapItem::updatePos() { void ConnectionPixmapItem::updateOrigin() { if (this->connection->isVertical()) { this->originX = 0; - this->originY = this->connection->relativePos(true).y() * this->mHeight; + this->originY = this->connection->relativePixelPos(true).y(); } else if (this->connection->isHorizontal()) { - this->originX = this->connection->relativePos(true).x() * this->mWidth; + this->originX = this->connection->relativePixelPos(true).x(); this->originY = 0; } updatePos(); @@ -126,7 +126,11 @@ void ConnectionPixmapItem::setSelected(bool selected) { emit selectionChanged(selected); } -void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { +void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { + if (!this->getEditable()) { + event->ignore(); + return; + } this->setSelected(true); } diff --git a/src/ui/currentselectedmetatilespixmapitem.cpp b/src/ui/currentselectedmetatilespixmapitem.cpp index e8b16f49..221c48e0 100644 --- a/src/ui/currentselectedmetatilespixmapitem.cpp +++ b/src/ui/currentselectedmetatilespixmapitem.cpp @@ -3,26 +3,21 @@ #include QPixmap drawMetatileSelection(MetatileSelection selection, Layout *layout) { - int width = selection.dimensions.x() * 16; - int height = selection.dimensions.y() * 16; + int width = selection.dimensions.width() * Metatile::pixelWidth(); + int height = selection.dimensions.height() * Metatile::pixelHeight(); QImage image(width, height, QImage::Format_RGBA8888); image.fill(QColor(0, 0, 0, 0)); QPainter painter(&image); - for (int i = 0; i < selection.dimensions.x(); i++) { - for (int j = 0; j < selection.dimensions.y(); j++) { - int x = i * 16; - int y = j * 16; + for (int i = 0; i < selection.dimensions.width(); i++) { + for (int j = 0; j < selection.dimensions.height(); j++) { + int x = i * Metatile::pixelWidth(); + int y = j * Metatile::pixelHeight(); QPoint metatile_origin = QPoint(x, y); - int index = j * selection.dimensions.x() + i; - MetatileSelectionItem item = selection.metatileItems.at(index); + int index = j * selection.dimensions.width() + i; + MetatileSelectionItem item = selection.metatileItems.value(index); if (item.enabled) { - QImage metatile_image = getMetatileImage( - item.metatileId, - layout->tileset_primary, - layout->tileset_secondary, - layout->metatileLayerOrder, - layout->metatileLayerOpacity); + QImage metatile_image = getMetatileImage(item.metatileId, layout); painter.drawImage(metatile_origin, metatile_image); } } diff --git a/src/ui/cursortilerect.cpp b/src/ui/cursortilerect.cpp index 511f0fda..d8c50a04 100644 --- a/src/ui/cursortilerect.cpp +++ b/src/ui/cursortilerect.cpp @@ -1,94 +1,71 @@ #include "cursortilerect.h" +#include "layoutpixmapitem.h" #include "log.h" -CursorTileRect::CursorTileRect(bool *enabled, QRgb color) -{ - this->enabled = enabled; - this->active = true; - this->color = color; - this->width = 16; - this->height = 16; - this->smartPathMode = false; - this->straightPathMode = false; - this->singleTileMode = false; - this->anchored = false; - this->rightClickSelectionAnchored = false; - this->anchorCoordX = 0; - this->anchorCoordY = 0; - this->selectionWidth = 1; - this->selectionHeight = 1; +CursorTileRect::CursorTileRect(const QSize &tileSize, const QRgb &color, QGraphicsItem *parent) + : QGraphicsItem(parent), + m_tileSize(tileSize), + m_selectionSize(QSize(1,1)), + m_anchorCoord(QPoint(0,0)), + m_color(color) +{ } + +// Size of the cursor may be explicitly enforced depending on settings. +QSize CursorTileRect::size() const { + if (m_singleTileMode) + return m_tileSize; + + if (smartPathInEffect()) + return m_tileSize * 2; + + return QSize(m_tileSize.width() * m_selectionSize.width(), + m_tileSize.height() * m_selectionSize.height()); } -void CursorTileRect::setActive(bool active) -{ - this->active = active; +void CursorTileRect::initAnchor(int coordX, int coordY) { + m_anchorCoord = QPoint(coordX, coordY); + m_anchored = true; } -bool CursorTileRect::getActive() -{ - return active; +void CursorTileRect::stopAnchor() { + m_anchored = false; } -void CursorTileRect::initAnchor(int coordX, int coordY) -{ - this->anchorCoordX = coordX; - this->anchorCoordY = coordY; - this->anchored = true; +void CursorTileRect::initRightClickSelectionAnchor(int coordX, int coordY) { + m_anchorCoord = QPoint(coordX, coordY); + m_rightClickSelectionAnchored = true; } -void CursorTileRect::stopAnchor() -{ - this->anchored = false; +void CursorTileRect::stopRightClickSelectionAnchor() { + m_rightClickSelectionAnchored = false; } -void CursorTileRect::initRightClickSelectionAnchor(int coordX, int coordY) -{ - this->anchorCoordX = coordX; - this->anchorCoordY = coordY; - this->rightClickSelectionAnchored = true; +void CursorTileRect::updateSelectionSize(const QSize &size) { + m_selectionSize = size.expandedTo(QSize(1,1)); // Enforce minimum of 1x1 cell + prepareGeometryChange(); + update(); } -void CursorTileRect::stopRightClickSelectionAnchor() -{ - this->rightClickSelectionAnchored = false; +bool CursorTileRect::smartPathInEffect() const { + return !m_rightClickSelectionAnchored && m_smartPathMode && LayoutPixmapItem::isSmartPathSize(m_selectionSize); } -void CursorTileRect::updateSelectionSize(int width, int height) -{ - this->selectionWidth = width; - this->selectionHeight = height; - this->width = width * 16; - this->height = height * 16; - this->prepareGeometryChange(); - this->update(); -} +void CursorTileRect::updateLocation(int coordX, int coordY) { + if (!m_singleTileMode) { + if (m_rightClickSelectionAnchored) { + coordX = qMin(coordX, m_anchorCoord.x()); + coordY = qMin(coordY, m_anchorCoord.y()); + } else if (m_anchored && !smartPathInEffect()) { + int xDiff = coordX - m_anchorCoord.x(); + int yDiff = coordY - m_anchorCoord.y(); + if (xDiff < 0 && xDiff % m_selectionSize.width() != 0) xDiff -= m_selectionSize.width(); + if (yDiff < 0 && yDiff % m_selectionSize.height() != 0) yDiff -= m_selectionSize.height(); -bool CursorTileRect::smartPathInEffect() -{ - return !this->rightClickSelectionAnchored && this->smartPathMode && this->selectionHeight == 3 && this->selectionWidth == 3; -} - -void CursorTileRect::updateLocation(int coordX, int coordY) -{ - if (!this->singleTileMode) { - if (this->rightClickSelectionAnchored) { - coordX = qMin(coordX, this->anchorCoordX); - coordY = qMin(coordY, this->anchorCoordY); - } - else if (this->anchored && !this->smartPathInEffect()) { - int xDiff = coordX - this->anchorCoordX; - int yDiff = coordY - this->anchorCoordY; - if (xDiff < 0 && xDiff % this->selectionWidth != 0) xDiff -= this->selectionWidth; - if (yDiff < 0 && yDiff % this->selectionHeight != 0) yDiff -= this->selectionHeight; - - coordX = this->anchorCoordX + (xDiff / this->selectionWidth) * this->selectionWidth; - coordY = this->anchorCoordY + (yDiff / this->selectionHeight) * this->selectionHeight; + coordX = m_anchorCoord.x() + (xDiff / m_selectionSize.width()) * m_selectionSize.width(); + coordY = m_anchorCoord.y() + (yDiff / m_selectionSize.height()) * m_selectionSize.height(); } } - coordX = qMax(coordX, 0); - coordY = qMax(coordY, 0); - this->setX(coordX * 16); - this->setY(coordY * 16); - this->setVisible(*this->enabled && this->active); + this->setX(coordX * m_tileSize.width()); + this->setY(coordY * m_tileSize.height()); } diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 10d060c2..0b3dca1e 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -21,7 +21,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) : const QStringList paths = userConfig.getCustomScriptPaths(); const QList enabled = userConfig.getCustomScriptsEnabled(); for (int i = 0; i < paths.length(); i++) - this->displayScript(paths.at(i), enabled.at(i)); + this->displayScript(paths.at(i), enabled.value(i, true)); connect(ui->button_Help, &QAbstractButton::clicked, this, &CustomScriptsEditor::openManual); connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript); diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 6457e564..a1af9e97 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -201,8 +201,8 @@ void EventFrame::populateScriptDropdown(NoScrollComboBox * combo, Project * proj QStringList scripts = map->getScriptLabels(this->event->getEventGroup()); populateDropdown(combo, scripts); - // Depending on the settings, the autocomplete may also contain all global scripts. - if (project && porymapConfig.loadAllEventScripts) { + // Depending on the settings, the autocomplete may also contain scripts from outside the map. + if (project && porymapConfig.scriptAutocompleteMode != ScriptAutocompleteMode::MapOnly) { project->insertGlobalScriptLabels(scripts); } diff --git a/src/ui/graphicsview.cpp b/src/ui/graphicsview.cpp index c8fc4495..667e0aab 100644 --- a/src/ui/graphicsview.cpp +++ b/src/ui/graphicsview.cpp @@ -33,7 +33,7 @@ void MapView::drawForeground(QPainter *painter, const QRectF&) { painter->save(); if (editor->layout) { // We're clipping here to hide parts of the grid that are outside the map. - const QRectF mapRect(-0.5, -0.5, editor->layout->getWidth() * 16 + 1.5, editor->layout->getHeight() * 16 + 1.5); + const QRectF mapRect(-0.5, -0.5, editor->layout->pixelWidth() + 1.5, editor->layout->pixelHeight() + 1.5); painter->setClipping(true); painter->setClipRect(mapRect); } diff --git a/src/ui/imageproviders.cpp b/src/ui/imageproviders.cpp index 3f7b8fb8..5efa1e91 100644 --- a/src/ui/imageproviders.cpp +++ b/src/ui/imageproviders.cpp @@ -1,6 +1,5 @@ #include "config.h" #include "imageproviders.h" -#include "log.h" #include "editor.h" #include @@ -9,39 +8,63 @@ QImage getCollisionMetatileImage(Block block) { } QImage getCollisionMetatileImage(int collision, int elevation) { - const QImage * image = Editor::collisionIcons.at(collision).at(elevation); + const QImage * image = Editor::collisionIcons.value(collision).value(elevation); return image ? *image : QImage(); } +QImage getMetatileImage(uint16_t metatileId, const Layout *layout, bool useTruePalettes) { + Metatile* metatile = Tileset::getMetatile(metatileId, + layout ? layout->tileset_primary : nullptr, + layout ? layout->tileset_secondary : nullptr); + return getMetatileImage(metatile, layout, useTruePalettes); +} + +QImage getMetatileImage(const Metatile *metatile, const Layout *layout, bool useTruePalettes) { + if (!layout) { + return getMetatileImage(metatile, nullptr, nullptr, {}, {}, useTruePalettes); + } + return getMetatileImage(metatile, + layout->tileset_primary, + layout->tileset_secondary, + layout->metatileLayerOrder(), + layout->metatileLayerOpacity(), + useTruePalettes); +} + QImage getMetatileImage( uint16_t metatileId, - Tileset *primaryTileset, - Tileset *secondaryTileset, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, const QList &layerOrder, const QList &layerOpacity, bool useTruePalettes) { - Metatile* metatile = Tileset::getMetatile(metatileId, primaryTileset, secondaryTileset); - if (!metatile) { - QImage metatile_image(16, 16, QImage::Format_RGBA8888); - metatile_image.fill(Qt::magenta); - return metatile_image; - } - return getMetatileImage(metatile, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes); + return getMetatileImage(Tileset::getMetatile(metatileId, primaryTileset, secondaryTileset), + primaryTileset, + secondaryTileset, + layerOrder, + layerOpacity, + useTruePalettes); +} + +// The color to use when we want to show some portion of the image request was invalid. +// Normally this is Qt::magenta, but we'll use Qt::transparent if we think the image allows it. +QColor getInvalidImageColor() { + return (projectConfig.transparencyColor == QColor(Qt::transparent)) ? QColor(Qt::transparent) : QColor(Qt::magenta); } QImage getMetatileImage( - Metatile *metatile, - Tileset *primaryTileset, - Tileset *secondaryTileset, + const Metatile *metatile, + const Tileset *primaryTileset, + const Tileset *secondaryTileset, const QList &layerOrder, const QList &layerOpacity, bool useTruePalettes) { - QImage metatile_image(16, 16, QImage::Format_RGBA8888); + QImage metatileImage(Metatile::pixelSize(), QImage::Format_RGBA8888); if (!metatile) { - metatile_image.fill(Qt::magenta); - return metatile_image; + metatileImage.fill(getInvalidImageColor()); + return metatileImage; } QList> palettes = Tileset::getBlockPalettes(primaryTileset, secondaryTileset, useTruePalettes); @@ -50,22 +73,20 @@ QImage getMetatileImage( // tile pixels line up across layers we will still have something to render. // The GBA renders transparent pixels using palette 0 color 0. We have this color, // but all 3 games actually overwrite it with black when loading the tileset palettes, - // so we have a setting to choose between these two behaviors. - metatile_image.fill(projectConfig.setTransparentPixelsBlack ? QColor("black") : QColor(palettes.value(0).value(0))); + // so we have a setting to specify an override transparency color. + metatileImage.fill(projectConfig.transparencyColor.isValid() ? projectConfig.transparencyColor : QColor(palettes.value(0).value(0))); + + QPainter painter(&metatileImage); - QPainter metatile_painter(&metatile_image); - const int numLayers = 3; // When rendering, metatiles always have 3 layers uint32_t layerType = metatile->layerType(); - for (int layer = 0; layer < numLayers; layer++) - for (int y = 0; y < 2; y++) - for (int x = 0; x < 2; x++) { - int l = layerOrder.size() >= numLayers ? layerOrder[layer] : layer; - + for (const auto &layer : layerOrder) + for (int y = 0; y < Metatile::tileHeight(); y++) + for (int x = 0; x < Metatile::tileWidth(); x++) { // Get the tile to render next Tile tile; - int tileOffset = (y * 2) + x; + int tileOffset = (y * Metatile::tileWidth()) + x; if (projectConfig.tripleLayerMetatilesEnabled) { - tile = metatile->tiles.value(tileOffset + (l * 4)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); } else { // "Vanilla" metatiles only have 8 tiles, but render 12. // The remaining 4 tiles are rendered using user-specified tiles depending on layer type. @@ -73,97 +94,80 @@ QImage getMetatileImage( { default: case Metatile::LayerType::Normal: - if (l == 0) + if (layer == 0) tile = Tile(projectConfig.unusedTileNormal); else // Tiles are on layers 1 and 2 - tile = metatile->tiles.value(tileOffset + ((l - 1) * 4)); + tile = metatile->tiles.value(tileOffset + ((layer - 1) * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Covered: - if (l == 2) + if (layer == 2) tile = Tile(projectConfig.unusedTileCovered); else // Tiles are on layers 0 and 1 - tile = metatile->tiles.value(tileOffset + (l * 4)); + tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer())); break; case Metatile::LayerType::Split: - if (l == 1) + if (layer == 1) tile = Tile(projectConfig.unusedTileSplit); else // Tiles are on layers 0 and 2 - tile = metatile->tiles.value(tileOffset + ((l == 0 ? 0 : 1) * 4)); + tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * Metatile::tilesPerLayer())); break; } } - QImage tile_image = getTileImage(tile.tileId, primaryTileset, secondaryTileset); - if (tile_image.isNull()) { - // Some metatiles specify tiles that are outside the valid range. - // The way the GBA will render these depends on what's in memory (which Porymap can't know) - // so we treat them as if they were transparent. - continue; - } + QImage tileImage = getColoredTileImage(tile.tileId, primaryTileset, secondaryTileset, palettes.value(tile.palette)); - // Colorize the metatile tiles with its palette. - if (tile.palette < palettes.length()) { - const QList palette = palettes.value(tile.palette); - for (int j = 0; j < palette.length(); j++) { - tile_image.setColor(j, palette.value(j)); - } - } else { - logWarn(QString("Tile '%1' is referring to invalid palette number: '%2'").arg(tile.tileId).arg(tile.palette)); - } - - QPoint origin = QPoint(x*8, y*8); - float opacity = layerOpacity.size() >= numLayers ? layerOpacity[l] : 1.0; + float opacity = layerOpacity.value(layer, 1.0); if (opacity < 1.0) { int alpha = 255 * opacity; - for (int c = 0; c < tile_image.colorCount(); c++) { - QColor color(tile_image.color(c)); + for (int c = 0; c < tileImage.colorCount(); c++) { + QColor color(tileImage.color(c)); color.setAlpha(alpha); - tile_image.setColor(c, color.rgba()); + tileImage.setColor(c, color.rgba()); } } // Color 0 is displayed as transparent. - QColor color(tile_image.color(0)); - color.setAlpha(0); - tile_image.setColor(0, color.rgba()); - tile.flip(&tile_image); - metatile_painter.drawImage(origin, tile_image); - } - metatile_painter.end(); + if (tileImage.colorCount()) { + QColor color(tileImage.color(0)); + color.setAlpha(0); + tileImage.setColor(0, color.rgba()); + } - return metatile_image; + tile.flip(&tileImage); + painter.drawImage(x * Tile::pixelWidth(), y * Tile::pixelHeight(), tileImage); + } + painter.end(); + + return metatileImage; } -QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { - Tileset *tileset = Tileset::getTileTileset(tileId, primaryTileset, secondaryTileset); - int index = Tile::getIndexInTileset(tileId); - if (!tileset) { - return QImage(); - } - return tileset->tiles.value(index, QImage()); +QImage getTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { + const Tileset *tileset = Tileset::getTileTileset(tileId, primaryTileset, secondaryTileset); + return tileset ? tileset->tileImage(tileId) : QImage(); } -QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList &palette) { +QImage getColoredTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset, const QList &palette) { QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset); if (tileImage.isNull()) { - tileImage = QImage(8, 8, QImage::Format_RGBA8888); - QPainter painter(&tileImage); - painter.fillRect(0, 0, 8, 8, palette.at(0)); + // Some tiles specify tile IDs or palette IDs that are outside the valid range. + // The way the GBA will render these depends on what's in memory (which Porymap can't know) + // so we render them using the invalid color + tileImage = QImage(Tile::pixelSize(), QImage::Format_RGBA8888); + tileImage.fill(getInvalidImageColor()); } else { - for (int i = 0; i < 16; i++) { - tileImage.setColor(i, palette.at(i)); + for (int i = 0; i < Tileset::numColorsPerPalette(); i++) { + tileImage.setColor(i, palette.value(i, getInvalidImageColor().rgb())); } } - return tileImage; } -QImage getPalettedTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, int paletteId, bool useTruePalettes) { +QImage getPalettedTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset, int paletteId, bool useTruePalettes) { QList palette = Tileset::getPalette(paletteId, primaryTileset, secondaryTileset, useTruePalettes); return getColoredTileImage(tileId, primaryTileset, secondaryTileset, palette); } -QImage getGreyscaleTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset) { +QImage getGreyscaleTileImage(uint16_t tileId, const Tileset *primaryTileset, const Tileset *secondaryTileset) { return getColoredTileImage(tileId, primaryTileset, secondaryTileset, greyscalePalette); } @@ -175,3 +179,94 @@ void flattenTo4bppImage(QImage * image) { for (int i = 0; i < image->sizeInBytes(); i++, pixel++) *pixel %= 16; } + +// Constructs a grid image of the metatiles in the specified ID range. +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, + uint16_t metatileIdStart, + uint16_t metatileIdEnd, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity, + const QSize &metatileSize, + bool useTruePalettes) +{ + if (metatileIdEnd < metatileIdStart || numMetatilesWide <= 0) + return QImage(); + + int numMetatilesToDraw = metatileIdEnd - metatileIdStart + 1; + + // Round up image height for incomplete last row. + int numMetatilesTall = Util::roundUpToMultiple(numMetatilesToDraw, numMetatilesWide) / numMetatilesWide; + + QImage image(numMetatilesWide * metatileSize.width(), numMetatilesTall * metatileSize.height(), QImage::Format_RGBA8888); + image.fill(getInvalidImageColor()); + + QPainter painter(&image); + for (int i = 0; i < numMetatilesToDraw; i++) { + uint16_t metatileId = i + metatileIdStart; + QImage metatileImage = getMetatileImage(metatileId, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes) + .scaled(metatileSize); + + int x = (i % numMetatilesWide) * metatileSize.width(); + int y = (i / numMetatilesWide) * metatileSize.height(); + painter.drawImage(x, y, metatileImage); + } + painter.end(); + return image; +} + +// Constructs a grid image of the metatiles in the primary and secondary tileset, +// rounding as necessary to keep the two tilesets on separate rows. +// The unused metatiles (if any) between the primary and secondary tilesets are skipped. +QImage getMetatileSheetImage(const Tileset *primaryTileset, + const Tileset *secondaryTileset, + int numMetatilesWide, + const QList &layerOrder, + const QList &layerOpacity, + const QSize &metatileSize, + bool useTruePalettes) +{ + auto createSheetImage = [=](uint16_t start, const Tileset *tileset) { + uint16_t end = start; + if (tileset) { + if (tileset->numMetatiles() == 0) + return QImage(); + end += tileset->numMetatiles() - 1; + } + return getMetatileSheetImage(primaryTileset, + secondaryTileset, + start, + end, + numMetatilesWide, + layerOrder, + layerOpacity, + metatileSize, + useTruePalettes); + }; + + QImage primaryImage = createSheetImage(0, primaryTileset); + QImage secondaryImage = createSheetImage(Project::getNumMetatilesPrimary(), secondaryTileset); + + QImage image(qMax(primaryImage.width(), secondaryImage.width()), primaryImage.height() + secondaryImage.height(), QImage::Format_RGBA8888); + image.fill(getInvalidImageColor()); + + QPainter painter(&image); + painter.drawImage(0, 0, primaryImage); + painter.drawImage(0, primaryImage.height(), secondaryImage); + painter.end(); + + return image; +} + +QImage getMetatileSheetImage(const Layout *layout, int numMetatilesWide, bool useTruePalettes) { + if (!layout) + return QImage(); + return getMetatileSheetImage(layout->tileset_primary, + layout->tileset_secondary, + numMetatilesWide, + layout->metatileLayerOrder(), + layout->metatileLayerOpacity(), + Metatile::pixelSize(), + useTruePalettes); +} diff --git a/src/ui/layoutpixmapitem.cpp b/src/ui/layoutpixmapitem.cpp index 2fb89171..95571661 100644 --- a/src/ui/layoutpixmapitem.cpp +++ b/src/ui/layoutpixmapitem.cpp @@ -25,15 +25,15 @@ void LayoutPixmapItem::paint(QGraphicsSceneMouseEvent *event) { // Paint onto the map. bool shiftPressed = event->modifiers() & Qt::ShiftModifier; - QPoint selectionDimensions = this->metatileSelector->getSelectionDimensions(); + QSize selectionDimensions = this->metatileSelector->getSelectionDimensions(); if (settings->smartPathsEnabled) { - if (!shiftPressed && selectionDimensions.x() == 3 && selectionDimensions.y() == 3) { + if (!shiftPressed && isSmartPathSize(selectionDimensions)) { paintSmartPath(pos.x(), pos.y()); } else { paintNormal(pos.x(), pos.y()); } } else { - if (shiftPressed && selectionDimensions.x() == 3 && selectionDimensions.y() == 3) { + if (shiftPressed && isSmartPathSize(selectionDimensions)) { paintSmartPath(pos.x(), pos.y()); } else { paintNormal(pos.x(), pos.y()); @@ -91,7 +91,7 @@ void LayoutPixmapItem::shift(int xDelta, int yDelta, bool fromScriptCall) { destY %= this->layout->getHeight(); int blockIndex = j * this->layout->getWidth() + i; - Block srcBlock = oldMetatiles.at(blockIndex); + Block srcBlock = oldMetatiles.value(blockIndex); this->layout->setBlock(destX, destY, srcBlock); } @@ -110,28 +110,28 @@ void LayoutPixmapItem::paintNormal(int x, int y, bool fromScriptCall) { // This allows painting via dragging the mouse to tile the painted region. int xDiff = x - initialX; int yDiff = y - initialY; - if (xDiff < 0 && xDiff % selection.dimensions.x() != 0) xDiff -= selection.dimensions.x(); - if (yDiff < 0 && yDiff % selection.dimensions.y() != 0) yDiff -= selection.dimensions.y(); + if (xDiff < 0 && xDiff % selection.dimensions.width() != 0) xDiff -= selection.dimensions.width(); + if (yDiff < 0 && yDiff % selection.dimensions.height() != 0) yDiff -= selection.dimensions.height(); - x = initialX + (xDiff / selection.dimensions.x()) * selection.dimensions.x(); - y = initialY + (yDiff / selection.dimensions.y()) * selection.dimensions.y(); + x = initialX + (xDiff / selection.dimensions.width()) * selection.dimensions.width(); + y = initialY + (yDiff / selection.dimensions.height()) * selection.dimensions.height(); // for edit history Blockdata oldMetatiles = !fromScriptCall ? this->layout->blockdata : Blockdata(); - for (int i = 0; i < selection.dimensions.x() && i + x < this->layout->getWidth(); i++) - for (int j = 0; j < selection.dimensions.y() && j + y < this->layout->getHeight(); j++) { + for (int i = 0; i < selection.dimensions.width() && i + x < this->layout->getWidth(); i++) + for (int j = 0; j < selection.dimensions.height() && j + y < this->layout->getHeight(); j++) { int actualX = i + x; int actualY = j + y; Block block; if (this->layout->getBlock(actualX, actualY, &block)) { - int index = j * selection.dimensions.x() + i; - MetatileSelectionItem item = selection.metatileItems.at(index); + int index = j * selection.dimensions.width() + i; + MetatileSelectionItem item = selection.metatileItems.value(index); if (!item.enabled) continue; block.setMetatileId(item.metatileId); if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) { - CollisionSelectionItem collisionItem = selection.collisionItems.at(index); + CollisionSelectionItem collisionItem = selection.collisionItems.value(index); block.setCollision(collisionItem.collision); block.setElevation(collisionItem.elevation); } @@ -177,8 +177,11 @@ bool isSmartPathTile(QList metatileItems, uint16_t metati return false; } -bool isValidSmartPathSelection(MetatileSelection selection) { - if (selection.dimensions.x() != 3 || selection.dimensions.y() != 3) +bool LayoutPixmapItem::isValidSmartPathSelection(MetatileSelection selection) { + if (!isSmartPathSize(selection.dimensions)) + return false; + + if (selection.metatileItems.length() != (LayoutPixmapItem::smartPathWidth * LayoutPixmapItem::smartPathHeight)) return false; for (int i = 0; i < selection.metatileItems.length(); i++) { @@ -195,13 +198,13 @@ void LayoutPixmapItem::paintSmartPath(int x, int y, bool fromScriptCall) { return; // Shift to the middle tile of the smart path selection. - uint16_t openMetatileId = selection.metatileItems.at(4).metatileId; + uint16_t openMetatileId = selection.metatileItems.at(smartPathMiddleIndex).metatileId; uint16_t openCollision = 0; uint16_t openElevation = 0; bool setCollisions = false; if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) { - openCollision = selection.collisionItems.at(4).collision; - openElevation = selection.collisionItems.at(4).elevation; + openCollision = selection.collisionItems.at(smartPathMiddleIndex).collision; + openElevation = selection.collisionItems.at(smartPathMiddleIndex).elevation; setCollisions = true; } @@ -356,7 +359,7 @@ void LayoutPixmapItem::updateMetatileSelection(QGraphicsSceneMouseEvent *event) metatiles.append(block.metatileId()); } int blockIndex = y * this->layout->getWidth() + x; - block = this->layout->blockdata.at(blockIndex); + block = this->layout->blockdata.value(blockIndex); auto collision = block.collision(); auto elevation = block.elevation(); collisions.append(QPair(collision, elevation)); @@ -377,7 +380,7 @@ void LayoutPixmapItem::floodFill(QGraphicsSceneMouseEvent *event) { int metatileId = selection.metatileItems.first().metatileId; if (selection.metatileItems.count() > 1 || (this->layout->getBlock(pos.x(), pos.y(), &block) && block.metatileId() != metatileId)) { bool smartPathsEnabled = event->modifiers() & Qt::ShiftModifier; - if ((this->settings->smartPathsEnabled || smartPathsEnabled) && selection.dimensions.x() == 3 && selection.dimensions.y() == 3) + if ((this->settings->smartPathsEnabled || smartPathsEnabled) && isSmartPathSize(selection.dimensions)) this->floodFillSmartPath(pos.x(), pos.y()); else this->floodFill(pos.x(), pos.y()); @@ -398,7 +401,7 @@ void LayoutPixmapItem::magicFill(QGraphicsSceneMouseEvent *event) { } void LayoutPixmapItem::magicFill(int x, int y, uint16_t metatileId, bool fromScriptCall) { - QPoint selectionDimensions(1, 1); + QSize selectionDimensions(1, 1); QList selectedMetatiles = QList({MetatileSelectionItem{ true, metatileId }}); this->magicFill(x, y, selectionDimensions, selectedMetatiles, QList(), fromScriptCall); } @@ -411,9 +414,9 @@ void LayoutPixmapItem::magicFill(int x, int y, bool fromScriptCall) { void LayoutPixmapItem::magicFill( int initialX, int initialY, - QPoint selectionDimensions, - QList selectedMetatiles, - QList selectedCollisions, + const QSize &selectionDimensions, + const QList &selectedMetatiles, + const QList &selectedCollisions, bool fromScriptCall) { Block block; if (this->layout->getBlock(initialX, initialY, &block)) { @@ -430,12 +433,12 @@ void LayoutPixmapItem::magicFill( if (this->layout->getBlock(x, y, &block) && block.metatileId() == metatileId) { int xDiff = x - initialX; int yDiff = y - initialY; - int i = xDiff % selectionDimensions.x(); - int j = yDiff % selectionDimensions.y(); - if (i < 0) i = selectionDimensions.x() + i; - if (j < 0) j = selectionDimensions.y() + j; - int index = j * selectionDimensions.x() + i; - if (selectedMetatiles.at(index).enabled) { + int i = xDiff % selectionDimensions.width(); + int j = yDiff % selectionDimensions.height(); + if (i < 0) i = selectionDimensions.width() + i; + if (j < 0) j = selectionDimensions.height() + j; + int index = j * selectionDimensions.width() + i; + if (index < selectedMetatiles.length() && selectedMetatiles.at(index).enabled) { block.setMetatileId(selectedMetatiles.at(index).metatileId); if (setCollisions) { CollisionSelectionItem item = selectedCollisions.at(index); @@ -460,7 +463,7 @@ void LayoutPixmapItem::floodFill(int initialX, int initialY, bool fromScriptCall } void LayoutPixmapItem::floodFill(int initialX, int initialY, uint16_t metatileId, bool fromScriptCall) { - QPoint selectionDimensions(1, 1); + QSize selectionDimensions(1, 1); QList selectedMetatiles = QList({MetatileSelectionItem{true, metatileId}}); this->floodFill(initialX, initialY, selectionDimensions, selectedMetatiles, QList(), fromScriptCall); } @@ -468,9 +471,9 @@ void LayoutPixmapItem::floodFill(int initialX, int initialY, uint16_t metatileId void LayoutPixmapItem::floodFill( int initialX, int initialY, - QPoint selectionDimensions, - QList selectedMetatiles, - QList selectedCollisions, + const QSize &selectionDimensions, + const QList &selectedMetatiles, + const QList &selectedCollisions, bool fromScriptCall) { bool setCollisions = selectedCollisions.length() == selectedMetatiles.length(); Blockdata oldMetatiles = !fromScriptCall ? this->layout->blockdata : Blockdata(); @@ -490,17 +493,17 @@ void LayoutPixmapItem::floodFill( visited.insert(x + y * this->layout->getWidth()); int xDiff = x - initialX; int yDiff = y - initialY; - int i = xDiff % selectionDimensions.x(); - int j = yDiff % selectionDimensions.y(); - if (i < 0) i = selectionDimensions.x() + i; - if (j < 0) j = selectionDimensions.y() + j; - int index = j * selectionDimensions.x() + i; - uint16_t metatileId = selectedMetatiles.at(index).metatileId; + int i = xDiff % selectionDimensions.width(); + int j = yDiff % selectionDimensions.height(); + if (i < 0) i = selectionDimensions.width() + i; + if (j < 0) j = selectionDimensions.height() + j; + int index = j * selectionDimensions.width() + i; + uint16_t metatileId = selectedMetatiles.value(index).metatileId; uint16_t old_metatileId = block.metatileId(); - if (selectedMetatiles.at(index).enabled && (selectedMetatiles.count() != 1 || old_metatileId != metatileId)) { + if (selectedMetatiles.value(index).enabled && (selectedMetatiles.count() != 1 || old_metatileId != metatileId)) { block.setMetatileId(metatileId); if (setCollisions) { - CollisionSelectionItem item = selectedCollisions.at(index); + CollisionSelectionItem item = selectedCollisions.value(index); block.setCollision(item.collision); block.setElevation(item.elevation); } @@ -535,12 +538,12 @@ void LayoutPixmapItem::floodFillSmartPath(int initialX, int initialY, bool fromS return; // Shift to the middle tile of the smart path selection. - uint16_t openMetatileId = selection.metatileItems.at(4).metatileId; + uint16_t openMetatileId = selection.metatileItems.at(smartPathMiddleIndex).metatileId; uint16_t openCollision = 0; uint16_t openElevation = 0; bool setCollisions = false; if (selection.hasCollision && selection.collisionItems.length() == selection.metatileItems.length()) { - CollisionSelectionItem item = selection.collisionItems.at(4); + CollisionSelectionItem item = selection.collisionItems.at(smartPathMiddleIndex); openCollision = item.collision; openElevation = item.elevation; setCollisions = true; @@ -695,25 +698,19 @@ void LayoutPixmapItem::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); if (pos != this->metatilePos) { this->metatilePos = pos; - emit this->hoveredMapMetatileChanged(pos); - } - if (this->settings->betterCursors && this->editsEnabled) { - setCursor(this->settings->mapCursor); + emit this->hoverChanged(pos); } } -void LayoutPixmapItem::hoverEnterEvent(QGraphicsSceneHoverEvent * event) { +void LayoutPixmapItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event) { this->has_mouse = true; - QPoint pos = Metatile::coordFromPixmapCoord(event->pos()); - emit this->hoveredMapMetatileChanged(pos); + this->metatilePos = Metatile::coordFromPixmapCoord(event->pos()); + emit this->hoverEntered(this->metatilePos); } void LayoutPixmapItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *) { - emit this->hoveredMapMetatileCleared(); - if (this->settings->betterCursors && this->editsEnabled) { - unsetCursor(); - } this->has_mouse = false; + emit this->hoverCleared(); } void LayoutPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { @@ -730,7 +727,7 @@ void LayoutPixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { return; this->metatilePos = pos; - emit hoveredMapMetatileChanged(pos); + emit hoverChanged(pos); emit mouseEvent(event, this); } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index 59efbe4b..30c6ff72 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -46,7 +46,7 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - m_scene = new QGraphicsScene(this); + m_scene = new CheckeredBgScene(QSize(8,8), this); m_preview = m_scene->addPixmap(QPixmap()); ui->graphicsView_Preview->setScene(m_scene); @@ -333,8 +333,8 @@ QGifImage* MapImageExporter::createTimelapseGifImage(QProgressDialog *progress) if (currentHistoryAppliesToFrame(step.historyStack) || step.historyStack->index() == step.initialStackIndex) { // Either this is relevant edit history, or it's the final frame (which is always rendered). Record the size of the map at this point. QMargins margins = getMargins(m_map); - canvasSize = canvasSize.expandedTo(QSize(m_layout->getWidth() * 16 + margins.left() + margins.right(), - m_layout->getHeight() * 16 + margins.top() + margins.bottom())); + canvasSize = canvasSize.expandedTo(QSize(m_layout->pixelWidth() + margins.left() + margins.right(), + m_layout->pixelHeight() + margins.top() + margins.bottom())); } if (step.historyStack->canUndo()){ step.historyStack->undo(); @@ -434,8 +434,8 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { if (!connection->isCardinal()) continue; Map *connectedMap = connection->targetMap(); if (!connectedMap) continue; - QPoint pos = connection->relativePos(); - unvisited.append(StitchedMap{cur.x + (pos.x() * 16), cur.y + (pos.y() * 16), connectedMap}); + QPoint pos = connection->relativePixelPos(); + unvisited.append(StitchedMap{cur.x + pos.x(), cur.y + pos.y(), connectedMap}); } } if (stitchedMaps.isEmpty()) @@ -447,7 +447,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress) { // Determine the overall dimensions of the stitched maps. QRect dimensions = QRect(0, 0, m_map->getWidth(), m_map->getHeight()) + getMargins(m_map); for (const StitchedMap &map : stitchedMaps) { - dimensions |= (QRect(map.x, map.y, map.map->getWidth() * 16, map.map->getHeight() * 16) + getMargins(map.map)); + dimensions |= (QRect(map.x, map.y, map.map->pixelWidth(), map.map->pixelHeight()) + getMargins(map.map)); } QPixmap stitchedPixmap(dimensions.width(), dimensions.height()); @@ -602,7 +602,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap() { QMargins MapImageExporter::getMargins(const Map *map) { QMargins margins; if (m_settings.showBorder) { - margins = m_project->getMetatileViewDistance() * 16; + margins = m_project->getPixelViewDistance(); } else if (map && connectionsEnabled()) { for (const auto &connection : map->getConnections()) { const QString dir = connection->direction(); @@ -612,10 +612,10 @@ QMargins MapImageExporter::getMargins(const Map *map) { if (!targetMap) continue; QRect rect = targetMap->getConnectionRect(dir); - if (dir == "up") margins.setTop(rect.height() * 16); - else if (dir == "down") margins.setBottom(rect.height() * 16); - else if (dir == "left") margins.setLeft(rect.width() * 16); - else if (dir == "right") margins.setRight(rect.width() * 16); + if (dir == "up") margins.setTop(rect.height()); + else if (dir == "down") margins.setBottom(rect.height()); + else if (dir == "left") margins.setLeft(rect.width()); + else if (dir == "right") margins.setRight(rect.width()); } } if (m_settings.showGrid) { @@ -652,7 +652,7 @@ void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { // Skip border painting if it would be fully covered by the rest of the map if (layout->isWithinBounds(QRect(x, y, layout->getBorderWidth(), layout->getBorderHeight()))) continue; - painter->drawPixmap(x * 16, y * 16, layout->border_pixmap); + painter->drawPixmap(x * Metatile::pixelWidth(), y * Metatile::pixelHeight(), layout->border_pixmap); } painter->restore(); @@ -665,7 +665,7 @@ void MapImageExporter::paintConnections(QPainter *painter, const Map *map) { for (const auto &connection : map->getConnections()) { if (!m_settings.showConnections.contains(connection->direction())) continue; - painter->drawImage(connection->relativePos(true) * 16, connection->render().toImage()); + painter->drawImage(connection->relativePixelPos(true), connection->render().toImage()); } } @@ -693,12 +693,12 @@ void MapImageExporter::paintGrid(QPainter *painter, const Layout *layout) { if (!m_settings.showGrid) return; - int w = layout->getWidth() * 16; - int h = layout->getHeight() * 16; - for (int x = 0; x <= w; x += 16) { + int w = layout->pixelWidth(); + int h = layout->pixelHeight(); + for (int x = 0; x <= w; x += Metatile::pixelWidth()) { painter->drawLine(x, 0, x, h); } - for (int y = 0; y <= h; y += 16) { + for (int y = 0; y <= h; y += Metatile::pixelHeight()) { painter->drawLine(0, y, w, y); } } diff --git a/src/ui/mapruler.cpp b/src/ui/mapruler.cpp index 908e0a46..da83821c 100644 --- a/src/ui/mapruler.cpp +++ b/src/ui/mapruler.cpp @@ -37,9 +37,11 @@ QPainterPath MapRuler::shape() const { ruler.addRect(xRuler); ruler.addRect(yRuler); ruler = ruler.simplified(); - for (int x = 16; x < pixWidth(); x += 16) + int w = Metatile::pixelWidth(); + int h = Metatile::pixelHeight(); + for (int x = w; x < pixWidth(); x += w) ruler.addRect(x, xRuler.y(), 0, thickness); - for (int y = 16; y < pixHeight(); y += 16) + for (int y = h; y < pixHeight(); y += h) ruler.addRect(yRuler.x(), y, thickness, 0); if (deltaX() && deltaY()) ruler.addPolygon(QVector({ cornerTick.p1(), cornerTick.p2() })); @@ -131,7 +133,9 @@ QPoint MapRuler::snapToWithinBounds(QPoint pos) const { void MapRuler::updateGeometry() { prepareGeometryChange(); - setPos(QPoint(left() * 16 + 8, top() * 16 + 8)); + int w = Metatile::pixelWidth(); + int h = Metatile::pixelHeight(); + setPos(QPoint(left() * w + w/2, top() * h + h/2)); /* Determine what quadrant the end point is in relative to the anchor point. The anchor * point is the top-left corner of the metatile the ruler starts in, so a zero-length * ruler is considered to be in the bottom-right quadrant from the anchor point. */ diff --git a/src/ui/metatileimageexporter.cpp b/src/ui/metatileimageexporter.cpp new file mode 100644 index 00000000..51b953df --- /dev/null +++ b/src/ui/metatileimageexporter.cpp @@ -0,0 +1,376 @@ +#include "metatileimageexporter.h" +#include "ui_metatileimageexporter.h" +#include "filedialog.h" +#include "imageproviders.h" +#include "utility.h" +#include "project.h" +#include "metatile.h" + +#include + +MetatileImageExporter::MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings) : + QDialog(parent), + ui(new Ui::MetatileImageExporter), + m_primaryTileset(primaryTileset), + m_secondaryTileset(secondaryTileset), + m_savedSettings(savedSettings) +{ + ui->setupUi(this); + m_transparencyButtons = { + ui->radioButton_TransparencyNormal, + ui->radioButton_TransparencyBlack, + ui->radioButton_TransparencyFirst, + }; + + m_scene = new CheckeredBgScene(QSize(8,8), this); + m_preview = m_scene->addPixmap(QPixmap()); + ui->graphicsView_Preview->setScene(m_scene); + + if (projectConfig.tripleLayerMetatilesEnabled) { + // When triple-layer metatiles are enabled there is no unused layer, + // so this setting becomes pointless. Disable it. + ui->checkBox_Placeholders->setVisible(false); + } + + uint16_t maxMetatileId = Block::getMaxMetatileId(); + ui->spinBox_MetatileStart->setMaximum(maxMetatileId); + ui->spinBox_MetatileEnd->setMaximum(maxMetatileId); + ui->spinBox_WidthMetatiles->setRange(1, maxMetatileId); + ui->spinBox_WidthPixels->setRange(1 * Metatile::pixelWidth(), maxMetatileId * Metatile::pixelWidth()); + + if (m_primaryTileset) { + ui->comboBox_PrimaryTileset->setTextItem(m_primaryTileset->name); + } + if (m_secondaryTileset) { + ui->comboBox_SecondaryTileset->setTextItem(m_secondaryTileset->name); + } + + if (m_savedSettings) { + populate(*m_savedSettings); + } else { + populate({}); + } + + connect(ui->listWidget_Layers, &ReorderableListWidget::itemChanged, this, &MetatileImageExporter::updatePreview); + connect(ui->listWidget_Layers, &ReorderableListWidget::reordered, this, &MetatileImageExporter::updatePreview); + + connect(ui->pushButton_Save, &QPushButton::pressed, [this] { if (saveImage()) close(); }); + connect(ui->pushButton_Close, &QPushButton::pressed, this, &MetatileImageExporter::close); + connect(ui->pushButton_Reset, &QPushButton::pressed, this, &MetatileImageExporter::reset); + + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncPixelWidth); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncMetatileWidth); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::syncPixelWidth); // Round pixel width to multiple of 16 + connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileEnd); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileStart); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate); + connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview); + + // If we used toggled instead of clicked we'd get two preview updates instead of one when the setting changes. + connect(ui->radioButton_TransparencyNormal, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyBlack, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + connect(ui->radioButton_TransparencyFirst, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview); + + connect(ui->checkBox_Placeholders, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::tryEnforceMetatileRange); + connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::tryEnforceMetatileRange); + connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview); + + ui->graphicsView_Preview->setFocus(); +} + +MetatileImageExporter::~MetatileImageExporter() { + delete ui; +} + +// Allow the window to open before displaying the preview. +// Metatile sheet image creation is generally quick, so this only +// really matters so that the graphics view can adjust the scale properly. +void MetatileImageExporter::showEvent(QShowEvent *event) { + QDialog::showEvent(event); + if (!event->spontaneous()) { + QTimer::singleShot(0, this, &MetatileImageExporter::updatePreview); + } +} + +void MetatileImageExporter::closeEvent(QCloseEvent *event) { + if (m_savedSettings) { + m_savedSettings->metatileStart = ui->spinBox_MetatileStart->value(); + m_savedSettings->metatileEnd = ui->spinBox_MetatileEnd->value(); + m_savedSettings->numMetatilesWide = ui->spinBox_WidthMetatiles->value(); + m_savedSettings->usePrimaryTileset = ui->checkBox_PrimaryTileset->isChecked(); + m_savedSettings->useSecondaryTileset = ui->checkBox_SecondaryTileset->isChecked(); + m_savedSettings->renderPlaceholders = ui->checkBox_Placeholders->isChecked(); + for (int i = 0; i < m_transparencyButtons.length(); i++) { + if (m_transparencyButtons.at(i)->isChecked()) { + m_savedSettings->transparencyMode = i; + break; + } + } + m_savedSettings->layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + int layerNum = item->data(Qt::UserRole).toInt(); + m_savedSettings->layerOrder[layerNum] = (item->checkState() == Qt::Checked); + } + } + QDialog::closeEvent(event); +} + +void MetatileImageExporter::populate(const Settings &settings) { + const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart); + ui->spinBox_MetatileStart->setValue(settings.metatileStart); + + const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileStart); + ui->spinBox_MetatileEnd->setValue(settings.metatileEnd); + + const QSignalBlocker b_WidthMetatiles(ui->spinBox_MetatileStart); + ui->spinBox_WidthMetatiles->setValue(settings.numMetatilesWide); + + const QSignalBlocker b_WidthPixels(ui->spinBox_MetatileStart); + ui->spinBox_WidthPixels->setValue(settings.numMetatilesWide * Metatile::pixelWidth()); + + const QSignalBlocker b_PrimaryTileset(ui->spinBox_MetatileStart); + ui->checkBox_PrimaryTileset->setChecked(settings.usePrimaryTileset); + + const QSignalBlocker b_SecondaryTileset(ui->spinBox_MetatileStart); + ui->checkBox_SecondaryTileset->setChecked(settings.useSecondaryTileset); + + const QSignalBlocker b_Placeholders(ui->spinBox_MetatileStart); + ui->checkBox_Placeholders->setChecked(settings.renderPlaceholders); + + if (m_transparencyButtons.value(settings.transparencyMode)) { + auto button = m_transparencyButtons[settings.transparencyMode]; + const QSignalBlocker b_Transparency(button); + button->setChecked(true); + } + + // Build layer list from settings + const QSignalBlocker b_Layers(ui->listWidget_Layers); + ui->listWidget_Layers->clear(); + for (auto it = settings.layerOrder.cbegin(); it != settings.layerOrder.cend(); it++) { + int layerNum = it.key(); + bool enabled = it.value(); + + auto item = new QListWidgetItem(Metatile::getLayerName(layerNum)); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren); + item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked); + item->setData(Qt::UserRole, layerNum); // Save the original index to identify the layer + ui->listWidget_Layers->addItem(item); + } + // Don't give extra unnecessary space to the list + ui->listWidget_Layers->setFixedHeight(ui->listWidget_Layers->sizeHintForRow(0) * ui->listWidget_Layers->count() + 4); + + tryEnforceMetatileRange(); +} + +void MetatileImageExporter::applySettings(const Settings &settings) { + populate(settings); + updatePreview(); +} + +void MetatileImageExporter::reset() { + applySettings({}); +} + +QImage MetatileImageExporter::getImage() { + tryUpdatePreview(); + return m_preview->pixmap().toImage(); +} + +bool MetatileImageExporter::saveImage(QString filepath) { + tryUpdatePreview(); + if (filepath.isEmpty()) { + QString defaultFilepath = QString("%1/%2").arg(FileDialog::getDirectory()).arg(getDefaultFileName()); + filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, QStringLiteral("Image Files (*.png *.jpg *.bmp)")); + if (filepath.isEmpty()) { + return false; + } + } + return m_preview->pixmap().save(filepath); +} + +QString MetatileImageExporter::getDefaultFileName() const { + if (m_layerOrder.length() == 1) { + // Exporting a metatile layer image is an expected use case for Porytiles, which expects certain file names. + // We can make the process a little easier by setting the default file name to those expected file names. + static const QStringList layerFilenames = { "bottom", "middle", "top" }; + return layerFilenames.at(m_layerOrder.constFirst()) + ".png"; + } + + QString defaultFilename; + if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_primaryTileset->name))); + } + if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_secondaryTileset->name))); + } + if (!m_layerOrder.isEmpty() && m_layerOrder != QList({0,1,2})) { + for (int i = m_layerOrder.length() - 1; i >= 0; i--) { + defaultFilename.append(Metatile::getLayerName(m_layerOrder.at(i))); + } + defaultFilename.append("_"); + } + defaultFilename.append("Metatile"); + + uint16_t start = ui->spinBox_MetatileStart->value(); + uint16_t end = ui->spinBox_MetatileEnd->value(); + if (start != end) { + defaultFilename.append("s"); + } + if (!ui->checkBox_PrimaryTileset->isChecked() && !ui->checkBox_SecondaryTileset->isChecked()) { + defaultFilename.append(QString("_%1").arg(Metatile::getMetatileIdString(start))); + if (start != end) { + defaultFilename.append(QString("-%1").arg(Metatile::getMetatileIdString(end))); + } + } + return QString("%1.png").arg(defaultFilename); +} + +void MetatileImageExporter::queuePreviewUpdate() { + m_previewUpdateQueued = true; +} + +// For updating only when a change has been recorded. +// Useful for something that might happen often, like an input widget losing focus. +void MetatileImageExporter::tryUpdatePreview() { + if (m_preview->pixmap().isNull() || m_previewUpdateQueued) { + updatePreview(); + } +} + +void MetatileImageExporter::updatePreview() { + copyRenderSettings(); + + m_layerOrder.clear(); + for (int i = 0; i < ui->listWidget_Layers->count(); i++) { + auto item = ui->listWidget_Layers->item(i); + if (item->checkState() == Qt::Checked) { + int layerNum = item->data(Qt::UserRole).toInt(); + m_layerOrder.prepend(qBound(0, layerNum, 2)); + } + } + + QImage previewImage; + if (ui->checkBox_PrimaryTileset->isChecked() && ui->checkBox_SecondaryTileset->isChecked()) { + // Special behavior to combine the two tilesets while skipping the unused region between tilesets. + previewImage = getMetatileSheetImage(m_primaryTileset, + m_secondaryTileset, + ui->spinBox_WidthMetatiles->value(), + m_layerOrder); + } else { + previewImage = getMetatileSheetImage(m_primaryTileset, + m_secondaryTileset, + ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value(), + ui->spinBox_WidthMetatiles->value(), + m_layerOrder); + } + + m_preview->setPixmap(QPixmap::fromImage(previewImage)); + m_scene->setSceneRect(m_scene->itemsBoundingRect()); + m_previewUpdateQueued = false; + + restoreRenderSettings(); +} + +void MetatileImageExporter::validateMetatileStart() { + const QSignalBlocker b(ui->spinBox_MetatileStart); + ui->spinBox_MetatileStart->setValue(qMin(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +void MetatileImageExporter::validateMetatileEnd() { + const QSignalBlocker b(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileEnd->setValue(qMax(ui->spinBox_MetatileStart->value(), + ui->spinBox_MetatileEnd->value())); +} + +void MetatileImageExporter::updateMetatileRange() { + uint16_t min; + uint16_t max; + if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) { + if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + // Both tilesets enforced + min = qMin(m_primaryTileset->firstMetatileId(), m_secondaryTileset->firstMetatileId()); + max = qMax(m_primaryTileset->lastMetatileId(), m_secondaryTileset->lastMetatileId()); + } else { + // Primary enforced + min = m_primaryTileset->firstMetatileId(); + max = m_primaryTileset->lastMetatileId(); + } + } else if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) { + // Secondary enforced + min = m_secondaryTileset->firstMetatileId(); + max = m_secondaryTileset->lastMetatileId(); + } else { + // No tilesets enforced + return; + } + + const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart); + const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileEnd); + ui->spinBox_MetatileStart->setValue(min); + ui->spinBox_MetatileEnd->setValue(max); +} + +void MetatileImageExporter::tryEnforceMetatileRange() { + // Users can either specify which tileset(s) to render, or specify a range of metatiles, but not both. + if (ui->checkBox_PrimaryTileset->isChecked() || ui->checkBox_SecondaryTileset->isChecked()) { + updateMetatileRange(); + ui->groupBox_MetatileRange->setDisabled(true); + } else { + ui->groupBox_MetatileRange->setDisabled(false); + } +} + +void MetatileImageExporter::syncPixelWidth() { + const QSignalBlocker b(ui->spinBox_WidthPixels); + ui->spinBox_WidthPixels->setValue(ui->spinBox_WidthMetatiles->value() * Metatile::pixelWidth()); +} + +void MetatileImageExporter::syncMetatileWidth() { + const QSignalBlocker b(ui->spinBox_WidthMetatiles); + ui->spinBox_WidthMetatiles->setValue(Util::roundUpToMultiple(ui->spinBox_WidthPixels->value(), Metatile::pixelWidth()) / Metatile::pixelWidth()); +} + +// These settings control some rendering behavior that make metatiles render accurately to their in-game appearance, +// which may be undesirable when exporting metatile images for editing. +// The settings are buried in getMetatileImage at the moment, to change them we'll temporarily overwrite them. +void MetatileImageExporter::copyRenderSettings() { + m_savedConfig.transparencyColor = projectConfig.transparencyColor; + m_savedConfig.unusedTileNormal = projectConfig.unusedTileNormal; + m_savedConfig.unusedTileCovered = projectConfig.unusedTileCovered; + m_savedConfig.unusedTileSplit = projectConfig.unusedTileSplit; + + if (ui->radioButton_TransparencyNormal->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::transparent); + } else if (ui->radioButton_TransparencyBlack->isChecked()) { + projectConfig.transparencyColor = QColor(Qt::black); + } else { + projectConfig.transparencyColor = QColor(); + } + + if (!ui->checkBox_Placeholders->isChecked()) { + projectConfig.unusedTileNormal = 0; + projectConfig.unusedTileCovered = 0; + projectConfig.unusedTileSplit = 0; + } +} + +void MetatileImageExporter::restoreRenderSettings() { + projectConfig.transparencyColor = m_savedConfig.transparencyColor; + projectConfig.unusedTileNormal = m_savedConfig.unusedTileNormal; + projectConfig.unusedTileCovered = m_savedConfig.unusedTileCovered; + projectConfig.unusedTileSplit = m_savedConfig.unusedTileSplit; +} diff --git a/src/ui/metatilelayersitem.cpp b/src/ui/metatilelayersitem.cpp index fabb49a4..c540f4c4 100644 --- a/src/ui/metatilelayersitem.cpp +++ b/src/ui/metatilelayersitem.cpp @@ -3,40 +3,89 @@ #include "imageproviders.h" #include -static const QList tilePositions = { - QPoint(0, 0), - QPoint(1, 0), - QPoint(0, 1), - QPoint(1, 1), - QPoint(2, 0), - QPoint(3, 0), - QPoint(2, 1), - QPoint(3, 1), - QPoint(4, 0), - QPoint(5, 0), - QPoint(4, 1), - QPoint(5, 1), -}; +MetatileLayersItem::MetatileLayersItem(Metatile *metatile, Tileset *primaryTileset, Tileset *secondaryTileset, Qt::Orientation orientation) + : SelectablePixmapItem(16, 16, Metatile::tileWidth(), Metatile::tileHeight()), + metatile(metatile), + primaryTileset(primaryTileset), + secondaryTileset(secondaryTileset) +{ + setAcceptHoverEvents(true); + setOrientation(orientation); +} + +void MetatileLayersItem::setOrientation(Qt::Orientation orientation) { + if (this->orientation == orientation) + return; + this->orientation = orientation; + int maxWidth = Metatile::tileWidth(); + int maxHeight = Metatile::tileHeight(); + + // Generate a table of tile positions that allows us to map between + // the index of a tile in the metatile and its position in this layer view. + this->tilePositions.clear(); + if (this->orientation == Qt::Horizontal) { + // Tiles are laid out horizontally, with the bottom layer on the left: + // 0 1 4 5 8 9 + // 2 3 6 7 10 11 + for (int layer = 0; layer < projectConfig.getNumLayersInMetatile(); layer++) + for (int y = 0; y < Metatile::tileHeight(); y++) + for (int x = 0; x < Metatile::tileWidth(); x++) { + this->tilePositions.append(QPoint(x + layer * Metatile::tileWidth(), y)); + } + maxWidth *= projectConfig.getNumLayersInMetatile(); + } else if (this->orientation == Qt::Vertical) { + // Tiles are laid out vertically, with the bottom layer on the bottom: + // 8 9 + // 10 11 + // 4 5 + // 6 7 + // 0 1 + // 2 3 + for (int layer = projectConfig.getNumLayersInMetatile() - 1; layer >= 0; layer--) + for (int y = 0; y < Metatile::tileHeight(); y++) + for (int x = 0; x < Metatile::tileWidth(); x++) { + this->tilePositions.append(QPoint(x, y + layer * Metatile::tileHeight())); + } + maxHeight *= projectConfig.getNumLayersInMetatile(); + } + setMaxSelectionSize(maxWidth, maxHeight); + update(); + if (!this->pixmap().isNull()) { + draw(); + } +} void MetatileLayersItem::draw() { - const int numLayers = projectConfig.getNumLayersInMetatile(); - QPixmap pixmap(numLayers * 32, 32); + QPixmap pixmap(this->cellWidth * this->maxSelectionWidth, this->cellHeight * this->maxSelectionHeight); QPainter painter(&pixmap); // Draw tile images int numTiles = qMin(projectConfig.getNumTilesInMetatile(), this->metatile ? this->metatile->tiles.length() : 0); for (int i = 0; i < numTiles; i++) { Tile tile = this->metatile->tiles.at(i); - QImage tileImage = getPalettedTileImage(tile.tileId, this->primaryTileset, this->secondaryTileset, tile.palette, true).scaled(16, 16); + QImage tileImage = getPalettedTileImage(tile.tileId, + this->primaryTileset, + this->secondaryTileset, + tile.palette, + true + ).scaled(this->cellWidth, this->cellHeight); tile.flip(&tileImage); - painter.drawImage(tilePositions.at(i) * 16, tileImage); + QPoint pos = tileIndexToPos(i); + painter.drawImage(pos.x() * this->cellWidth, pos.y() * this->cellHeight, tileImage); } if (this->showGrid) { // Draw grid painter.setPen(Qt::white); - for (int i = 1; i < numLayers; i++) { - int x = i * 32; - painter.drawLine(x, 0, x, 32); + const int layerWidth = this->cellWidth * Metatile::tileWidth(); + const int layerHeight = this->cellHeight * Metatile::tileHeight(); + for (int i = 1; i < projectConfig.getNumLayersInMetatile(); i++) { + if (this->orientation == Qt::Vertical) { + int y = i * layerHeight; + painter.drawLine(0, y, layerWidth, y); + } else if (this->orientation == Qt::Horizontal) { + int x = i * layerWidth; + painter.drawLine(x, 0, x, layerHeight); + } } } @@ -45,95 +94,88 @@ void MetatileLayersItem::draw() { void MetatileLayersItem::setMetatile(Metatile *metatile) { this->metatile = metatile; - this->clearLastModifiedCoords(); - this->clearLastHoveredCoords(); + draw(); } void MetatileLayersItem::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; this->draw(); - this->clearLastModifiedCoords(); - this->clearLastHoveredCoords(); +} + +void MetatileLayersItem::updateSelection() { + drawSelection(); + emit selectedTilesChanged(getSelectionStart(), getSelectionDimensions()); } void MetatileLayersItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { + const QPoint pos = getBoundedPos(event->pos()); + setCursorCellPos(pos); + if (event->buttons() & Qt::RightButton) { SelectablePixmapItem::mousePressEvent(event); - QPoint selectionOrigin = this->getSelectionStart(); - QPoint dimensions = this->getSelectionDimensions(); - emit this->selectedTilesChanged(selectionOrigin, dimensions.x(), dimensions.y()); - this->drawSelection(); + updateSelection(); + } else if (event->modifiers() & Qt::ControlModifier) { + emit paletteChanged(pos); } else { - const QPoint pos = this->getBoundedPos(event->pos()); - this->prevChangedPos = pos; - this->clearLastHoveredCoords(); - emit this->tileChanged(pos.x(), pos.y()); + emit tileChanged(pos); } } void MetatileLayersItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + const QPoint pos = getBoundedPos(event->pos()); + if (!setCursorCellPos(pos)) + return; + if (event->buttons() & Qt::RightButton) { SelectablePixmapItem::mouseMoveEvent(event); - QPoint selectionOrigin = this->getSelectionStart(); - QPoint dimensions = this->getSelectionDimensions(); - emit this->selectedTilesChanged(selectionOrigin, dimensions.x(), dimensions.y()); - this->drawSelection(); + updateSelection(); + } else if (event->modifiers() & Qt::ControlModifier) { + emit paletteChanged(pos); } else { - const QPoint pos = this->getBoundedPos(event->pos()); - if (prevChangedPos != pos) { - this->prevChangedPos = pos; - this->clearLastHoveredCoords(); - emit this->tileChanged(pos.x(), pos.y()); - } + emit tileChanged(pos); } } void MetatileLayersItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { if (event->buttons() & Qt::RightButton) { SelectablePixmapItem::mouseReleaseEvent(event); - QPoint selectionOrigin = this->getSelectionStart(); - QPoint dimensions = this->getSelectionDimensions(); - emit this->selectedTilesChanged(selectionOrigin, dimensions.x(), dimensions.y()); + updateSelection(); } - this->draw(); + // Clear selection rectangle + draw(); } void MetatileLayersItem::hoverMoveEvent(QGraphicsSceneHoverEvent * event) { - const QPoint pos = this->getBoundedPos(event->pos()); - if (pos == this->prevHoveredPos) - return; - this->prevHoveredPos = pos; + setCursorCellPos(getBoundedPos(event->pos())); +} - int tileIndex = tilePositions.indexOf(pos); - if (tileIndex < 0 || tileIndex >= this->metatile->tiles.length()) - return; +bool MetatileLayersItem::setCursorCellPos(const QPoint &pos) { + if (this->cursorCellPos == pos) + return false; + this->cursorCellPos = pos; - emit this->hoveredTileChanged(this->metatile->tiles.at(tileIndex).tileId); + emit this->hoveredTileChanged(tileUnderCursor()); + return true; } void MetatileLayersItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *) { - this->clearLastHoveredCoords(); + this->cursorCellPos = QPoint(-1,-1); emit this->hoveredTileCleared(); } -void MetatileLayersItem::clearLastModifiedCoords() { - this->prevChangedPos = QPoint(-1, -1); -} - -void MetatileLayersItem::clearLastHoveredCoords() { - this->prevHoveredPos = QPoint(-1, -1); +Tile MetatileLayersItem::tileUnderCursor() const { + int tileIndex = posToTileIndex(this->cursorCellPos); + if (tileIndex < 0 || !this->metatile || tileIndex >= this->metatile->tiles.length()) { + return Tile(); + } + return this->metatile->tiles.at(tileIndex); } QPoint MetatileLayersItem::getBoundedPos(const QPointF &pos) { - int x, y; - int maxX = (projectConfig.getNumLayersInMetatile() * 2) - 1; - x = static_cast(pos.x()) / 16; - y = static_cast(pos.y()) / 16; - if (x < 0) x = 0; - if (y < 0) y = 0; - if (x > maxX) x = maxX; - if (y > 1) y = 1; - return QPoint(x, y); + int x = static_cast(pos.x()) / this->cellWidth; + int y = static_cast(pos.y()) / this->cellHeight; + return QPoint(qBound(0, x, this->maxSelectionWidth - 1), + qBound(0, y, this->maxSelectionHeight - 1)); } diff --git a/src/ui/metatileselector.cpp b/src/ui/metatileselector.cpp index 012b6609..00725b9a 100644 --- a/src/ui/metatileselector.cpp +++ b/src/ui/metatileselector.cpp @@ -3,40 +3,20 @@ #include "project.h" #include -QPoint MetatileSelector::getSelectionDimensions() { +QSize MetatileSelector::getSelectionDimensions() const { if (this->prefabSelection || this->externalSelection) return selection.dimensions; return SelectablePixmapItem::getSelectionDimensions(); } int MetatileSelector::numPrimaryMetatilesRounded() const { - // We round up the number of primary metatiles to keep the tilesets on separate rows. - return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; + if (!primaryTileset()) + return 0; + return Util::roundUpToMultiple(primaryTileset()->numMetatiles(), this->numMetatilesWide); } void MetatileSelector::updateBasePixmap() { - int primaryLength = this->numPrimaryMetatilesRounded(); - int length_ = primaryLength + this->secondaryTileset->numMetatiles(); - int height_ = length_ / this->numMetatilesWide; - if (length_ % this->numMetatilesWide != 0) { - height_++; - } - QImage image(this->numMetatilesWide * 16, height_ * 16, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < length_; i++) { - int tile = i; - if (i >= primaryLength) { - tile += Project::getNumMetatilesPrimary() - primaryLength; - } - QImage metatile_image = getMetatileImage(tile, this->primaryTileset, this->secondaryTileset, layout->metatileLayerOrder, layout->metatileLayerOpacity); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * 16, map_y * 16); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - this->basePixmap = QPixmap::fromImage(image); + this->basePixmap = QPixmap::fromImage(getMetatileSheetImage(this->layout, this->numMetatilesWide)); } void MetatileSelector::draw() { @@ -53,17 +33,20 @@ void MetatileSelector::drawSelection() { } bool MetatileSelector::select(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) return false; + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) { + return false; + } this->externalSelection = false; this->prefabSelection = false; this->selection = MetatileSelection{ - QPoint(1, 1), + QSize(1, 1), false, QList({MetatileSelectionItem{true, metatileId}}), QList(), }; - QPoint coords = this->getMetatileIdCoords(metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + SelectablePixmapItem::select(pos); this->updateSelectedMetatiles(); return true; } @@ -73,9 +56,8 @@ void MetatileSelector::selectFromMap(uint16_t metatileId, uint16_t collision, ui this->setExternalSelection(1, 1, {metatileId}, {movePermissions}); } -void MetatileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { - this->primaryTileset = primaryTileset; - this->secondaryTileset = secondaryTileset; +void MetatileSelector::setLayout(Layout *layout) { + this->layout = layout; if (this->externalSelection) this->updateExternalSelectedMetatiles(); else @@ -85,11 +67,11 @@ void MetatileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTi draw(); } -MetatileSelection MetatileSelector::getMetatileSelection() { - return selection; +void MetatileSelector::refresh() { + setLayout(this->layout); } -void MetatileSelector::setExternalSelection(int width, int height, QList metatiles, QList> collisions) { +void MetatileSelector::setExternalSelection(int width, int height, const QList &metatiles, const QList> &collisions) { this->prefabSelection = false; this->externalSelection = true; this->externalSelectionWidth = width; @@ -98,19 +80,19 @@ void MetatileSelector::setExternalSelection(int width, int height, QListselection.metatileItems.clear(); this->selection.collisionItems.clear(); this->selection.hasCollision = true; - this->selection.dimensions = QPoint(width, height); - for (int i = 0; i < metatiles.length(); i++) { - auto collision = collisions.at(i); - this->selection.collisionItems.append(CollisionSelectionItem{true, collision.first, collision.second}); + this->selection.dimensions = QSize(width, height); + for (int i = 0; i < qMin(metatiles.length(), collisions.length()); i++) { uint16_t metatileId = metatiles.at(i); + uint16_t collision = collisions.at(i).first; + uint16_t elevation = collisions.at(i).second; + this->selection.collisionItems.append(CollisionSelectionItem{true, collision, elevation}); this->externalSelectedMetatiles.append(metatileId); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + if (!this->layout->metatileIsValid(metatileId)) metatileId = 0; this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } if (this->selection.metatileItems.length() == 1) { - QPoint coords = this->getMetatileIdCoords(this->selection.metatileItems.first().metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + SelectablePixmapItem::select(metatileIdToPos(this->selection.metatileItems.first().metatileId)); } this->draw(); @@ -127,7 +109,9 @@ void MetatileSelector::setPrefabSelection(MetatileSelection selection) { } bool MetatileSelector::positionIsValid(const QPoint &pos) const { - return Tileset::metatileIsValid(getMetatileId(pos.x(), pos.y()), this->primaryTileset, this->secondaryTileset); + bool ok; + posToMetatileId(pos, &ok); + return ok; } void MetatileSelector::mousePressEvent(QGraphicsSceneMouseEvent *event) { @@ -168,8 +152,14 @@ void MetatileSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { } void MetatileSelector::hoverChanged() { - uint16_t metatileId = this->getMetatileId(this->cellPos.x(), this->cellPos.y()); - emit this->hoveredMetatileSelectionChanged(metatileId); + bool ok; + uint16_t metatileId = posToMetatileId(this->cellPos, &ok); + if (ok) { + emit this->hoveredMetatileSelectionChanged(metatileId); + } else { + emit this->hoveredMetatileSelectionCleared(); + this->cellPos = QPoint(-1, -1); + } } void MetatileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { @@ -185,11 +175,9 @@ void MetatileSelector::updateSelectedMetatiles() { this->selection.hasCollision = false; this->selection.dimensions = this->getSelectionDimensions(); QPoint origin = this->getSelectionStart(); - for (int j = 0; j < this->selection.dimensions.y(); j++) { - for (int i = 0; i < this->selection.dimensions.x(); i++) { - uint16_t metatileId = this->getMetatileId(origin.x() + i, origin.y() + j); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - metatileId = 0; + for (int j = 0; j < this->selection.dimensions.height(); j++) { + for (int i = 0; i < this->selection.dimensions.width(); i++) { + uint16_t metatileId = posToMetatileId(origin.x() + i, origin.y() + j); this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } } @@ -198,46 +186,62 @@ void MetatileSelector::updateSelectedMetatiles() { void MetatileSelector::updateExternalSelectedMetatiles() { this->selection.metatileItems.clear(); - this->selection.dimensions = QPoint(this->externalSelectionWidth, this->externalSelectionHeight); + this->selection.dimensions = QSize(this->externalSelectionWidth, this->externalSelectionHeight); for (int i = 0; i < this->externalSelectedMetatiles.count(); ++i) { uint16_t metatileId = this->externalSelectedMetatiles.at(i); - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) + if (!this->layout->metatileIsValid(metatileId)) metatileId = 0; this->selection.metatileItems.append(MetatileSelectionItem{true, metatileId}); } emit selectedMetatilesChanged(); } -uint16_t MetatileSelector::getMetatileId(int x, int y) const { +uint16_t MetatileSelector::posToMetatileId(const QPoint &pos, bool *ok) const { + return posToMetatileId(pos.x(), pos.y(), ok); +} + +uint16_t MetatileSelector::posToMetatileId(int x, int y, bool *ok) const { + if (ok) *ok = true; int index = y * this->numMetatilesWide + x; - int numPrimary = this->numPrimaryMetatilesRounded(); - if (index < numPrimary) { - return static_cast(index); - } else { - return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); - } -} - -QPoint MetatileSelector::getMetatileIdCoords(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - { - // Invalid metatile id. - return QPoint(0, 0); + uint16_t metatileId = static_cast(index); + if (primaryTileset() && primaryTileset()->containsMetatileId(metatileId)) { + return metatileId; } - int index = metatileId < Project::getNumMetatilesPrimary() - ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); - return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + // There's some extra handling here because we round the tilesets to keep them on separate rows. + // This means if the maximum number of primary metatiles is not divisible by the metatile width + // then the metatiles we used to round the primary tileset would have the index of valid secondary metatiles. + // These need to be ignored, or they'll appear to be duplicates of the subseqeunt secondary metatiles. + int numPrimaryRounded = numPrimaryMetatilesRounded(); + int firstSecondaryRow = numPrimaryRounded / this->numMetatilesWide; + metatileId = static_cast(Project::getNumMetatilesPrimary() + index - numPrimaryRounded); + if (secondaryTileset() && secondaryTileset()->containsMetatileId(metatileId) && y >= firstSecondaryRow) { + return metatileId; + } + + if (ok) *ok = false; + return 0; } -QPoint MetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); +QPoint MetatileSelector::metatileIdToPos(uint16_t metatileId, bool *ok) const { + if (primaryTileset() && primaryTileset()->containsMetatileId(metatileId)) { + if (ok) *ok = true; + int index = metatileId; + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + if (secondaryTileset() && secondaryTileset()->containsMetatileId(metatileId)) { + if (ok) *ok = true; + int index = metatileId - Project::getNumMetatilesPrimary() + numPrimaryMetatilesRounded(); + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + + if (ok) *ok = false; + return QPoint(0,0); +} + +QPoint MetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) const { + QPoint pos = metatileIdToPos(metatileId); pos.rx() = (pos.x() * this->cellWidth) + (this->cellWidth / 2); pos.ry() = (pos.y() * this->cellHeight) + (this->cellHeight / 2); return pos; } - -void MetatileSelector::setLayout(Layout *layout) { - this->layout = layout; -} diff --git a/src/ui/montabwidget.cpp b/src/ui/montabwidget.cpp index 3a04c1b7..e2291f11 100644 --- a/src/ui/montabwidget.cpp +++ b/src/ui/montabwidget.cpp @@ -11,6 +11,9 @@ static WildMonInfo encounterClipboard; MonTabWidget::MonTabWidget(Editor *editor, QWidget *parent) : QTabWidget(parent) { this->editor = editor; + connect(this, &MonTabWidget::edited, this->editor, &Editor::saveEncounterTabData); + connect(this, &MonTabWidget::edited, this->editor, &Editor::wildMonTableEdited); + populate(); this->tabBar()->installEventFilter(new WheelFilter(this)); } @@ -64,7 +67,7 @@ void MonTabWidget::paste(int index) { WildMonInfo newInfo = getDefaultMonInfo(this->editor->project->wildMonFields.at(index)); combineEncounters(newInfo, encounterClipboard); populateTab(index, newInfo); - emit editor->wildMonTableEdited(); + emit edited(); } void MonTabWidget::actionCopyTab(int index) { @@ -88,15 +91,12 @@ void MonTabWidget::actionAddDeleteTab(int index) { if (activeTabs[index]) { // delete tab deactivateTab(index); - editor->saveEncounterTabData(); - } - else { + } else { // add tab populateTab(index, getDefaultMonInfo(editor->project->wildMonFields.at(index))); - editor->saveEncounterTabData(); setCurrentIndex(index); } - emit editor->wildMonTableEdited(); + emit edited(); } void MonTabWidget::clearTableAt(int tabIndex) { @@ -123,8 +123,7 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { QTableView *speciesTable = tableAt(tabIndex); EncounterTableModel *model = new EncounterTableModel(monInfo, editor->project->wildMonFields[tabIndex], this); - connect(model, &EncounterTableModel::edited, editor, &Editor::saveEncounterTabData); - connect(model, &EncounterTableModel::edited, editor, &Editor::wildMonTableEdited); + connect(model, &EncounterTableModel::edited, this, &MonTabWidget::edited); speciesTable->setModel(model); speciesTable->setItemDelegateForColumn(EncounterTableModel::ColumnType::Species, new SpeciesComboDelegate(editor->project, this)); diff --git a/src/ui/movablerect.cpp b/src/ui/movablerect.cpp index e02222df..1e751aa9 100644 --- a/src/ui/movablerect.cpp +++ b/src/ui/movablerect.cpp @@ -5,43 +5,32 @@ #include "movablerect.h" #include "utility.h" -MovableRect::MovableRect(bool *enabled, const QRectF &rect, const QRgb &color) +MovableRect::MovableRect(const QRectF &rect, const QSize &cellSize, const QRgb &color) : QGraphicsRectItem(rect), - enabled(enabled), baseRect(rect), + cellSize(cellSize), color(color) -{ - updateVisibility(); -} +{ } /// Center rect on grid position (x, y) void MovableRect::updateLocation(int x, int y) { - setRect(this->baseRect.x() + (x * 16), - this->baseRect.y() + (y * 16), + setRect(this->baseRect.x() + (x * this->cellSize.width()), + this->baseRect.y() + (y * this->cellSize.height()), this->baseRect.width(), this->baseRect.height()); - updateVisibility(); } -void MovableRect::setActive(bool active) { - this->active = active; - updateVisibility(); -} - -void MovableRect::updateVisibility() { - setVisible(*this->enabled && this->active); -} /****************************************************************************** ************************************************************************ ******************************************************************************/ -ResizableRect::ResizableRect(QObject *parent, bool *enabled, int width, int height, QRgb color) +ResizableRect::ResizableRect(QObject *parent, const QSize &cellSize, const QSize &size, const QRgb &color) : QObject(parent), - MovableRect(enabled, QRect(0, 0, width * 16, height * 16), color) + MovableRect(QRect(0, 0, size.width(), size.height()), cellSize, color) { - setAcceptHoverEvents(true); - setFlags(this->flags() | QGraphicsItem::ItemIsMovable); + setAcceptHoverEvents(true); + setFlags(this->flags() | QGraphicsItem::ItemIsMovable); } ResizableRect::Edge ResizableRect::detectEdge(int x, int y) { @@ -126,8 +115,8 @@ void ResizableRect::mousePressEvent(QGraphicsSceneMouseEvent *event) { } void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - int dx = Util::roundUp(event->scenePos().x() - this->clickedPos.x(), 16); - int dy = Util::roundUp(event->scenePos().y() - this->clickedPos.y(), 16); + int dx = Util::roundUpToMultiple(event->scenePos().x() - this->clickedPos.x(), this->cellSize.width()); + int dy = Util::roundUpToMultiple(event->scenePos().y() - this->clickedPos.y(), this->cellSize.height()); QRect resizedRect = this->clickedRect; @@ -161,20 +150,20 @@ void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { break; } - // Lower limits: smallest possible size is 16x16 square - if (resizedRect.width() < 16) { + // Lower limits: smallest possible size is 1 cell + if (resizedRect.width() < this->cellSize.width()) { if (dx < 0) { // right sided adjustment made - resizedRect.setWidth(16); + resizedRect.setWidth(this->cellSize.width()); } else { // left sided adjustment slightly more complicated - int dxMax = this->clickedRect.right() - this->clickedRect.left() - 16; + int dxMax = this->clickedRect.right() - this->clickedRect.left() - this->cellSize.width(); resizedRect.adjust(dxMax - dx, 0, 0, 0); } } - if (resizedRect.height() < 16) { + if (resizedRect.height() < this->cellSize.height()) { if (dy < 0) { // bottom - resizedRect.setHeight(16); + resizedRect.setHeight(this->cellSize.height()); } else { // top - int dyMax = this->clickedRect.bottom() - this->clickedRect.top() - 16; + int dyMax = this->clickedRect.bottom() - this->clickedRect.top() - this->cellSize.height(); resizedRect.adjust(0, dyMax - dy, 0, 0); } } diff --git a/src/ui/multikeyedit.cpp b/src/ui/multikeyedit.cpp index cf11210f..ec76e918 100644 --- a/src/ui/multikeyedit.cpp +++ b/src/ui/multikeyedit.cpp @@ -137,14 +137,6 @@ void MultiKeyEdit::addNewKeySequenceEdit() { connect(lineEdit, &QLineEdit::customContextMenuRequested, this, &MultiKeyEdit::customContextMenuRequested); - // Gross way to connect the line edit's clear button. - auto actions = lineEdit->findChildren(); - if (!actions.isEmpty()) { - connect(actions.first(), &QAction::triggered, this, [this, keySequenceEdit]() { - removeOne(keySequenceEdit->keySequence()); - }); - } - layout()->addWidget(keySequenceEdit); this->keySequenceEdits.append(keySequenceEdit); } diff --git a/src/ui/newlayoutdialog.cpp b/src/ui/newlayoutdialog.cpp index 785f0389..98d16e62 100644 --- a/src/ui/newlayoutdialog.cpp +++ b/src/ui/newlayoutdialog.cpp @@ -62,10 +62,10 @@ void NewLayoutDialog::refresh() { if (this->layoutToCopy) { // If we're importing a layout then some settings will be enforced. ui->newLayoutForm->setSettings(this->layoutToCopy->settings()); - ui->newLayoutForm->setDisabled(true); + ui->newLayoutForm->setDimensionsDisabled(true); } else { ui->newLayoutForm->setSettings(*settings); - ui->newLayoutForm->setDisabled(false); + ui->newLayoutForm->setDimensionsDisabled(false); } ui->lineEdit_Name->setText(settings->name); diff --git a/src/ui/newlayoutform.cpp b/src/ui/newlayoutform.cpp index bc750ddd..11bdc0be 100644 --- a/src/ui/newlayoutform.cpp +++ b/src/ui/newlayoutform.cpp @@ -40,10 +40,9 @@ void NewLayoutForm::initUi(Project *project) { } } -void NewLayoutForm::setDisabled(bool disabled) { +void NewLayoutForm::setDimensionsDisabled(bool disabled) { ui->groupBox_MapDimensions->setDisabled(disabled); ui->groupBox_BorderDimensions->setDisabled(disabled); - ui->groupBox_Tilesets->setDisabled(disabled); } void NewLayoutForm::setSettings(const Layout::Settings &settings) { @@ -111,7 +110,7 @@ bool NewLayoutForm::validatePrimaryTileset(bool allowEmpty) { if (name.isEmpty()) { if (!allowEmpty) errorText = QString("The Primary Tileset cannot be empty."); } else if (ui->comboBox_PrimaryTileset->findText(name) < 0) { - errorText = QString("The Primary Tileset '%1' does not exist.").arg(ui->label_PrimaryTileset->text()).arg(name); + errorText = QString("The Primary Tileset '%1' does not exist.").arg(name); } bool isValid = errorText.isEmpty(); diff --git a/src/ui/overlay.cpp b/src/ui/overlay.cpp index d1ba4ef8..4fccb530 100644 --- a/src/ui/overlay.cpp +++ b/src/ui/overlay.cpp @@ -1,3 +1,4 @@ +#ifdef QT_QML_LIB #include "overlay.h" #include "scripting.h" #include "log.h" @@ -256,3 +257,6 @@ bool Overlay::addImage(int x, int y, QImage image) { this->items.append(new OverlayPixmap(x, y, QPixmap::fromImage(image))); return true; } + + +#endif // QT_QML_LIB diff --git a/src/ui/palettecolorsearch.cpp b/src/ui/palettecolorsearch.cpp new file mode 100644 index 00000000..e1f47a90 --- /dev/null +++ b/src/ui/palettecolorsearch.cpp @@ -0,0 +1,207 @@ +#include "palettecolorsearch.h" +#include "ui_palettecolorsearch.h" +#include "project.h" +#include "tileset.h" +#include "imageproviders.h" +#include "eventfilters.h" +#include "log.h" +#include "numericsorttableitem.h" + +enum ResultsDataRole { + PairedTilesetName = Qt::UserRole, +}; + +PaletteColorSearch::PaletteColorSearch(Project *project, const Tileset *primaryTileset, const Tileset *secondaryTileset, QWidget *parent) : + QDialog(parent), + ui(new Ui::PaletteColorSearch), + m_project(project), + m_primaryTileset(primaryTileset), + m_secondaryTileset(secondaryTileset) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + + ui->buttonBox->setVisible(isModal()); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close); + + // Rather than try to keep track of metatile/tile changes that affect which colors are used, + // we'll just refresh when the window is activated. + ActiveWindowFilter *filter = new ActiveWindowFilter(this); + connect(filter, &ActiveWindowFilter::activated, this, &PaletteColorSearch::refresh); + this->installEventFilter(filter); + + ui->spinBox_ColorId->setRange(0, Tileset::numColorsPerPalette() - 1); + connect(ui->spinBox_ColorId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::updateResults); + + ui->spinBox_PaletteId->setRange(0, Project::getNumPalettesTotal() - 1); + connect(ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::updateResults); + connect(ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteColorSearch::paletteIdChanged); + + // Set up table header + static const QStringList labels = {"Tileset", "Metatile"}; + ui->table_Results->setHorizontalHeaderLabels(labels); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::TilesetName, QHeaderView::ResizeToContents); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Metatile, QHeaderView::Stretch); + + // Table is read-only + ui->table_Results->setEditTriggers(QAbstractItemView::NoEditTriggers); + ui->table_Results->setSelectionMode(QAbstractItemView::NoSelection); + + connect(ui->table_Results, &QTableWidget::cellDoubleClicked, this, &PaletteColorSearch::cellDoubleClicked); +} + +PaletteColorSearch::~PaletteColorSearch() { + delete ui; +} + +void PaletteColorSearch::setPaletteId(int paletteId) { + ui->spinBox_PaletteId->setValue(paletteId); +} + +int PaletteColorSearch::currentPaletteId() const { + return ui->spinBox_PaletteId->value(); +} + +void PaletteColorSearch::setColorId(int colorId) { + ui->spinBox_ColorId->setValue(colorId); +} + +int PaletteColorSearch::currentColorId() const { + return ui->spinBox_ColorId->value(); +} + +void PaletteColorSearch::setTilesets(const Tileset *primaryTileset, const Tileset *secondaryTileset) { + m_primaryTileset = primaryTileset; + m_secondaryTileset = secondaryTileset; + refresh(); +} + +const Tileset* PaletteColorSearch::currentTileset() const { + return Tileset::getPaletteTileset(currentPaletteId(), m_primaryTileset, m_secondaryTileset); +} + +void PaletteColorSearch::addTableEntry(const RowData &rowData) { + int row = ui->table_Results->rowCount(); + ui->table_Results->insertRow(row); + + auto tilesetNameItem = new NumericSortTableItem(rowData.tilesetName); + tilesetNameItem->setData(ResultsDataRole::PairedTilesetName, rowData.pairedTilesetName); + + ui->table_Results->setItem(row, ResultsColumn::TilesetName, tilesetNameItem); + ui->table_Results->setItem(row, ResultsColumn::Metatile, new QTableWidgetItem(rowData.metatileIcon, rowData.metatileId)); +} + +QList PaletteColorSearch::search(int colorId) const { + QList results; + + // Check our current tilesets for color usage. + results.append(search(colorId, m_primaryTileset, m_secondaryTileset)); + results.append(search(colorId, m_secondaryTileset, m_primaryTileset)); + + // The current palette comes from either the primary or secondary tileset. + // We need to check all the other tilesets that are paired with the tileset that owns this palette. + const Tileset *paletteTileset = currentTileset(); + QSet tilesetsToSearch = m_project->getPairedTilesetLabels(paletteTileset); + + // We exclude the currently-loaded pair (we already checked them, and because they're being + // edited in the Tileset Editor they may differ from their copies saved in the layout). + tilesetsToSearch.remove(m_primaryTileset->name); + tilesetsToSearch.remove(m_secondaryTileset->name); + + for (const auto &label : tilesetsToSearch) { + Tileset *searchTileset = m_project->getTileset(label); + if (searchTileset) { + results.append(search(colorId, searchTileset, paletteTileset)); + } + } + return results; +} + +QList PaletteColorSearch::search(int colorId, const Tileset *tileset, const Tileset *pairedTileset) const { + QList results; + QList metatileIds = tileset->findMetatilesUsingColor(currentPaletteId(), colorId, pairedTileset); + auto primaryTileset = tileset->is_secondary ? pairedTileset : tileset; + auto secondaryTileset = tileset->is_secondary ? tileset : pairedTileset; + for (const auto &metatileId : metatileIds) { + QImage metatileImage = getMetatileImage(metatileId, primaryTileset, secondaryTileset); + RowData rowData = { + .tilesetName = tileset->name, + .pairedTilesetName = pairedTileset->name, + .metatileId = Metatile::getMetatileIdString(metatileId), + .metatileIcon = QIcon(QPixmap::fromImage(metatileImage)), + }; + results.append(rowData); + } + return results; +} + +void PaletteColorSearch::refresh() { + m_resultsCache.clear(); + updateResults(); +} + +void PaletteColorSearch::updateResults() { + const Tileset *tileset = currentTileset(); + int paletteId = currentPaletteId(); + int colorId = currentColorId(); + + // Update color icon + QRgb color = tileset->palettePreviews.value(paletteId).value(colorId); + ui->frame_Color->setStyleSheet(QString("background-color: rgb(%1, %2, %3);").arg(qRed(color)).arg(qGreen(color)).arg(qBlue(color))); + + // Update title + ui->label_Title->setText(QString("Searching for usage of %1's palette %2.").arg(tileset->name).arg(paletteId)); + + // Update table + ui->table_Results->clearContents(); + ui->table_Results->setRowCount(0); + + QString cacheKey = QString("%1#%2").arg(paletteId).arg(colorId); + auto it = m_resultsCache.constFind(cacheKey); + bool inCache = (it != m_resultsCache.constEnd()); + const QList results = inCache ? it.value() : search(colorId); + + if (results.isEmpty()) { + static const RowData noResults = { + .tilesetName = QStringLiteral("This color is unused."), + .pairedTilesetName = "", + .metatileId = QStringLiteral("--"), + .metatileIcon = QIcon(), + }; + addTableEntry(noResults); + } else { + for (const auto &entry : results) { + addTableEntry(entry); + } + } + + ui->table_Results->sortByColumn(ResultsColumn::TilesetName, Qt::AscendingOrder); + + if (!inCache) m_resultsCache.insert(cacheKey, results); +} + +// Double-clicking row data selects the corresponding metatile in the Tileset Editor. +void PaletteColorSearch::cellDoubleClicked(int row, int) { + auto tilesetNameItem = ui->table_Results->item(row, ResultsColumn::TilesetName); + auto metatileItem = ui->table_Results->item(row, ResultsColumn::Metatile); + if (!tilesetNameItem || !metatileItem) + return; + + // The Tileset Editor (as of writing) has no way to change the selected tilesets independently of + // the main editor's layout, so if the metatile is not in the current tileset we do nothing. + // To compare the tileset names, rather than sort out which was the primary or secondary we + // just make sure it's the same set of names. + QSet currentTilesets; + currentTilesets.insert(m_primaryTileset->name); + currentTilesets.insert(m_secondaryTileset->name); + + QSet metatileTilesets; + metatileTilesets.insert(tilesetNameItem->text()); + metatileTilesets.insert(tilesetNameItem->data(ResultsDataRole::PairedTilesetName).toString()); + if (currentTilesets != metatileTilesets) + return; + + bool ok; + uint16_t metatileId = metatileItem->text().toUInt(&ok, 0); + if (ok) emit metatileSelected(metatileId); +} diff --git a/src/ui/paletteeditor.cpp b/src/ui/paletteeditor.cpp index 1078ecc6..ebc66cde 100644 --- a/src/ui/paletteeditor.cpp +++ b/src/ui/paletteeditor.cpp @@ -5,34 +5,29 @@ #include "log.h" #include "filedialog.h" #include "message.h" +#include "eventfilters.h" +#include "utility.h" PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset *secondaryTileset, int paletteId, QWidget *parent) : QMainWindow(parent), - ui(new Ui::PaletteEditor) + ui(new Ui::PaletteEditor), + project(project), + primaryTileset(primaryTileset), + secondaryTileset(secondaryTileset) { - this->project = project; - this->primaryTileset = primaryTileset; - this->secondaryTileset = secondaryTileset; + setAttribute(Qt::WA_DeleteOnClose); this->ui->setupUi(this); - this->ui->spinBox_PaletteId->setMinimum(0); - this->ui->spinBox_PaletteId->setMaximum(Project::getNumPalettesTotal() - 1); - this->colorInputs.clear(); const int numColorsPerRow = 4; for (int i = 0; i < this->numColors; i++) { - auto colorInput = new ColorInputWidget(QString("Color %1").arg(i)); + auto colorInput = new ColorInputWidget; connect(colorInput, &ColorInputWidget::colorChanged, [this, i](QRgb color) { setRgb(i, color); }); connect(colorInput, &ColorInputWidget::editingFinished, [this] { commitEditHistory(); }); this->colorInputs.append(colorInput); ui->layout_Colors->addWidget(colorInput, i / numColorsPerRow, i % numColorsPerRow); } - // Setup edit-undo history for each of the palettes. - for (int i = 0; i < Project::getNumPalettesTotal(); i++) { - this->palettesHistory.append(History()); - } - int bitDepth = porymapConfig.paletteEditorBitDepth; if (bitDepth == 15) { this->ui->bit_depth_15->setChecked(true); @@ -42,23 +37,50 @@ PaletteEditor::PaletteEditor(Project *project, Tileset *primaryTileset, Tileset setBitDepth(bitDepth); // Connect bit depth buttons - connect(this->ui->bit_depth_15, &QRadioButton::toggled, [this](bool checked){ if (checked) this->setBitDepth(15); }); - connect(this->ui->bit_depth_24, &QRadioButton::toggled, [this](bool checked){ if (checked) this->setBitDepth(24); }); + connect(this->ui->bit_depth_15, &QRadioButton::toggled, [this](bool checked){ if (checked) setBitDepth(15); }); + connect(this->ui->bit_depth_24, &QRadioButton::toggled, [this](bool checked){ if (checked) setBitDepth(24); }); - this->setPaletteId(paletteId); - this->commitEditHistory(); - this->restoreWindowState(); + this->ui->actionShow_Unused_Colors->setChecked(porymapConfig.showPaletteEditorUnusedColors); + connect(this->ui->actionShow_Unused_Colors, &QAction::toggled, this, &PaletteEditor::setColorInputTitles); + + connect(this->ui->toolButton_ColorSearch, &QToolButton::clicked, this, &PaletteEditor::openColorSearch); + connect(this->ui->actionFind_Color_Usage, &QAction::triggered, this, &PaletteEditor::openColorSearch); + + // Rather than try to keep track of metatile/tile changes that affect which colors are used, + // we'll just refresh when the window is activated. + ActiveWindowFilter *filter = new ActiveWindowFilter(this); + connect(filter, &ActiveWindowFilter::activated, this, &PaletteEditor::invalidateCache); + this->installEventFilter(filter); + + this->ui->spinBox_PaletteId->setRange(0, Project::getNumPalettesTotal() - 1); + this->ui->spinBox_PaletteId->setValue(paletteId); + connect(this->ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteEditor::refreshPaletteId); + connect(this->ui->spinBox_PaletteId, QOverload::of(&QSpinBox::valueChanged), this, &PaletteEditor::changedPalette); + + ui->actionRedo->setShortcuts({ui->actionRedo->shortcut(), QKeySequence("Ctrl+Shift+Z")}); + + refreshPaletteId(); + restoreWindowState(); } -PaletteEditor::~PaletteEditor() -{ +PaletteEditor::~PaletteEditor() { delete ui; } -Tileset* PaletteEditor::getTileset(int paletteId) { - return (paletteId < Project::getNumPalettesPrimary()) - ? this->primaryTileset - : this->secondaryTileset; +int PaletteEditor::currentPaletteId() const { + return ui->spinBox_PaletteId->value(); +} + +void PaletteEditor::setPaletteId(int paletteId) { + ui->spinBox_PaletteId->setValue(paletteId); +} + +bool PaletteEditor::showingUnusedColors() const { + return ui->actionShow_Unused_Colors->isChecked(); +} + +Tileset* PaletteEditor::getTileset(int paletteId) const { + return Tileset::getPaletteTileset(paletteId, this->primaryTileset, this->secondaryTileset); } void PaletteEditor::setBitDepth(int bits) { @@ -70,93 +92,104 @@ void PaletteEditor::setBitDepth(int bits) { } void PaletteEditor::setRgb(int colorIndex, QRgb rgb) { - const int paletteId = this->ui->spinBox_PaletteId->value(); - + const int paletteId = currentPaletteId(); Tileset *tileset = getTileset(paletteId); tileset->palettes[paletteId][colorIndex] = rgb; tileset->palettePreviews[paletteId][colorIndex] = rgb; - emit changedPaletteColor(); } void PaletteEditor::setPalette(int paletteId, const QList &palette) { Tileset *tileset = getTileset(paletteId); for (int i = 0; i < this->numColors; i++) { - tileset->palettes[paletteId][i] = palette.at(i); - tileset->palettePreviews[paletteId][i] = palette.at(i); + tileset->palettes[paletteId][i] = palette.value(i); + tileset->palettePreviews[paletteId][i] = palette.value(i); } refreshColorInputs(); emit changedPaletteColor(); } void PaletteEditor::refreshColorInputs() { - const int paletteId = ui->spinBox_PaletteId->value(); + const int paletteId = currentPaletteId(); Tileset *tileset = getTileset(paletteId); - for (int i = 0; i < this->numColors; i++) { + for (int i = 0; i < this->colorInputs.length(); i++) { auto colorInput = this->colorInputs.at(i); const QSignalBlocker b(colorInput); - colorInput->setColor(tileset->palettes.at(paletteId).at(i)); + colorInput->setColor(tileset->palettes.value(paletteId).value(i)); } + setColorInputTitles(showingUnusedColors()); } -void PaletteEditor::setPaletteId(int paletteId) { - const QSignalBlocker b(ui->spinBox_PaletteId); - this->ui->spinBox_PaletteId->setValue(paletteId); - this->refreshColorInputs(); +void PaletteEditor::refreshPaletteId() { + refreshColorInputs(); + + int paletteId = currentPaletteId(); + + if (!this->palettesHistory[paletteId].current()) { + // The original colors are saved as an initial commit. + commitEditHistory(paletteId); + } else { + updateEditHistoryActions(); + } + if (this->colorSearchWindow) { + this->colorSearchWindow->setPaletteId(paletteId); + } } void PaletteEditor::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; - this->refreshColorInputs(); -} - -void PaletteEditor::on_spinBox_PaletteId_valueChanged(int paletteId) { - this->refreshColorInputs(); - if (!this->palettesHistory[paletteId].current()) { - this->commitEditHistory(paletteId); + invalidateCache(); + if (this->colorSearchWindow) { + this->colorSearchWindow->setTilesets(primaryTileset, secondaryTileset); } - emit this->changedPalette(paletteId); + refreshColorInputs(); } void PaletteEditor::commitEditHistory() { - commitEditHistory(ui->spinBox_PaletteId->value()); + commitEditHistory(currentPaletteId()); } void PaletteEditor::commitEditHistory(int paletteId) { QList colors; - for (int i = 0; i < this->numColors; i++) { + for (int i = 0; i < this->colorInputs.length(); i++) { colors.append(this->colorInputs.at(i)->color()); } - PaletteHistoryItem *commit = new PaletteHistoryItem(colors); - this->palettesHistory[paletteId].push(commit); + this->palettesHistory[paletteId].push(new PaletteHistoryItem(colors)); + updateEditHistoryActions(); } void PaletteEditor::restoreWindowState() { logInfo("Restoring palette editor geometry from previous session."); QMap geometry = porymapConfig.getPaletteEditorGeometry(); - this->restoreGeometry(geometry.value("palette_editor_geometry")); - this->restoreState(geometry.value("palette_editor_state")); + restoreGeometry(geometry.value("palette_editor_geometry")); + restoreState(geometry.value("palette_editor_state")); } -void PaletteEditor::on_actionUndo_triggered() -{ - int paletteId = this->ui->spinBox_PaletteId->value(); - PaletteHistoryItem *prev = this->palettesHistory[paletteId].back(); - if (prev) - setPalette(paletteId, prev->colors); +void PaletteEditor::updateEditHistoryActions() { + int paletteId = currentPaletteId(); + // We have an initial commit that shouldn't be available to Undo, so we ignore that. + ui->actionUndo->setEnabled(this->palettesHistory[paletteId].index() > 0); + ui->actionRedo->setEnabled(this->palettesHistory[paletteId].canRedo()); } -void PaletteEditor::on_actionRedo_triggered() -{ - int paletteId = this->ui->spinBox_PaletteId->value(); - PaletteHistoryItem *next = this->palettesHistory[paletteId].next(); - if (next) - setPalette(paletteId, next->colors); +void PaletteEditor::on_actionUndo_triggered() { + int paletteId = currentPaletteId(); + PaletteHistoryItem *commit = this->palettesHistory[paletteId].back(); + if (!commit) return; + setPalette(paletteId, commit->colors); + updateEditHistoryActions(); } -void PaletteEditor::on_actionImport_Palette_triggered() -{ +void PaletteEditor::on_actionRedo_triggered() { + int paletteId = currentPaletteId(); + PaletteHistoryItem *commit = this->palettesHistory[paletteId].next(); + if (!commit) return; + setPalette(paletteId, commit->colors); + updateEditHistoryActions(); +} + +void PaletteEditor::on_actionImport_Palette_triggered() { QString filepath = FileDialog::getOpenFileName(this, "Import Tileset Palette", "", "Palette Files (*.pal *.act *tpl *gpl)"); if (filepath.isEmpty()) { return; @@ -171,14 +204,92 @@ void PaletteEditor::on_actionImport_Palette_triggered() palette.append(0); } - const int paletteId = ui->spinBox_PaletteId->value(); + const int paletteId = currentPaletteId(); setPalette(paletteId, palette); commitEditHistory(paletteId); } +void PaletteEditor::openColorSearch() { + if (!this->colorSearchWindow) { + this->colorSearchWindow = new PaletteColorSearch(this->project, this->primaryTileset, this->secondaryTileset, this); + this->colorSearchWindow->setPaletteId(currentPaletteId()); + connect(this->colorSearchWindow, &PaletteColorSearch::metatileSelected, this, &PaletteEditor::metatileSelected); + connect(this->colorSearchWindow, &PaletteColorSearch::paletteIdChanged, this, &PaletteEditor::setPaletteId); + } + Util::show(this->colorSearchWindow); +} + +void PaletteEditor::invalidateCache() { + this->unusedColorCache.clear(); + if (showingUnusedColors()) { + setColorInputTitles(true); + } +} + +QSet PaletteEditor::getUnusedColorIds() { + const int paletteId = currentPaletteId(); + + if (this->unusedColorCache.contains(paletteId)) { + return this->unusedColorCache.value(paletteId); + } + this->unusedColorCache[paletteId] = {}; + + // Check our current tilesets for color usage. + QSet unusedColorIds = this->primaryTileset->getUnusedColorIds(paletteId, this->secondaryTileset); + if (unusedColorIds.isEmpty()) + return {}; + unusedColorIds = this->secondaryTileset->getUnusedColorIds(paletteId, this->primaryTileset, unusedColorIds); + if (unusedColorIds.isEmpty()) + return {}; + + // The current palette comes from either the primary or secondary tileset. + // We need to check all the other tilesets that are paired with the tileset that owns this palette. + Tileset *paletteTileset = getTileset(paletteId); + QSet tilesetsToSearch = this->project->getPairedTilesetLabels(paletteTileset); + + // We exclude the currently-loaded pair (we already checked them, and because they're being + // edited in the Tileset Editor they may differ from their copies saved in the layout). + tilesetsToSearch.remove(this->primaryTileset->name); + tilesetsToSearch.remove(this->secondaryTileset->name); + + for (const auto &label : tilesetsToSearch) { + Tileset *searchTileset = this->project->getTileset(label); + if (!searchTileset) continue; + unusedColorIds = searchTileset->getUnusedColorIds(paletteId, paletteTileset, unusedColorIds); + if (unusedColorIds.isEmpty()) + return {}; + } + + this->unusedColorCache[paletteId] = unusedColorIds; + return unusedColorIds; +} + +void PaletteEditor::setColorInputTitles(bool showUnused) { + porymapConfig.showPaletteEditorUnusedColors = showUnused; + + QSet unusedColorIds = showUnused ? getUnusedColorIds() : QSet(); + ui->label_AllColorsUsed->setVisible(showUnused && unusedColorIds.isEmpty()); + for (int i = 0; i < this->colorInputs.length(); i++) { + QString title = QString("Color %1").arg(i); + if (unusedColorIds.contains(i)) { + title.append(QStringLiteral(" (Unused)")); + } + this->colorInputs.at(i)->setTitle(title); + } +} + void PaletteEditor::closeEvent(QCloseEvent*) { porymapConfig.setPaletteEditorGeometry( - this->saveGeometry(), - this->saveState() + saveGeometry(), + saveState() ); + + // Opening the color search window then closing the Palette Editor sets + // focus to the main editor window instead of the parent (Tileset Editor). + // Make sure the parent is active when we close. + auto p = dynamic_cast(parent()); + if (p && p->isVisible()) { + p->raise(); + p->activateWindow(); + } } diff --git a/src/ui/prefab.cpp b/src/ui/prefab.cpp index 32a6c86a..7a1e96d2 100644 --- a/src/ui/prefab.cpp +++ b/src/ui/prefab.cpp @@ -58,7 +58,7 @@ void Prefab::loadPrefabs() { QString secondaryTileset = ParseUtil::jsonToQString(prefabObj["secondary_tileset"]); MetatileSelection selection; - selection.dimensions = QPoint(width, height); + selection.dimensions = QSize(width, height); selection.hasCollision = true; for (int j = 0; j < width * height; j++) { selection.metatileItems.append(MetatileSelectionItem{false, 0}); @@ -112,15 +112,15 @@ void Prefab::savePrefabs() { for (auto item : this->items) { OrderedJson::object prefabObj; prefabObj["name"] = item.name; - prefabObj["width"] = item.selection.dimensions.x(); - prefabObj["height"] = item.selection.dimensions.y(); + prefabObj["width"] = item.selection.dimensions.width(); + prefabObj["height"] = item.selection.dimensions.height(); prefabObj["primary_tileset"] = item.primaryTileset; prefabObj["secondary_tileset"] = item.secondaryTileset; OrderedJson::array metatiles; - for (int y = 0; y < item.selection.dimensions.y(); y++) { - for (int x = 0; x < item.selection.dimensions.x(); x++) { - int index = y * item.selection.dimensions.x() + x; - auto metatileItem = item.selection.metatileItems.at(index); + for (int y = 0; y < item.selection.dimensions.height(); y++) { + for (int x = 0; x < item.selection.dimensions.width(); x++) { + int index = y * item.selection.dimensions.width() + x; + auto metatileItem = item.selection.metatileItems.value(index); if (metatileItem.enabled) { OrderedJson::object metatileObj; metatileObj["x"] = x; diff --git a/src/ui/prefabcreationdialog.cpp b/src/ui/prefabcreationdialog.cpp index ef6e6a0f..34b11127 100644 --- a/src/ui/prefabcreationdialog.cpp +++ b/src/ui/prefabcreationdialog.cpp @@ -23,13 +23,13 @@ PrefabCreationDialog::PrefabCreationDialog(QWidget *parent, MetatileSelector *me QObject::connect(this->ui->graphicsView_Prefab, &ClickableGraphicsView::clicked, [=](QMouseEvent *event){ auto pos = event->pos(); - int selectionWidth = this->selection.dimensions.x() * 16; - int selectionHeight = this->selection.dimensions.y() * 16; + int selectionWidth = this->selection.dimensions.width() * Metatile::pixelWidth(); + int selectionHeight = this->selection.dimensions.height() * Metatile::pixelHeight(); if (pos.x() < 0 || pos.x() >= selectionWidth || pos.y() < 0 || pos.y() >= selectionHeight) return; - int metatileX = pos.x() / 16; - int metatileY = pos.y() / 16; - int index = metatileY * this->selection.dimensions.x() + metatileX; + int metatileX = pos.x() / Metatile::pixelWidth(); + int metatileY = pos.y() / Metatile::pixelHeight(); + int index = metatileY * this->selection.dimensions.width() + metatileX; bool toggledState = !this->selection.metatileItems[index].enabled; this->selection.metatileItems[index].enabled = toggledState; if (this->selection.hasCollision) { diff --git a/src/ui/preferenceeditor.cpp b/src/ui/preferenceeditor.cpp index bda0ec98..5765ba01 100644 --- a/src/ui/preferenceeditor.cpp +++ b/src/ui/preferenceeditor.cpp @@ -1,7 +1,7 @@ #include "preferenceeditor.h" #include "ui_preferenceeditor.h" -#include "config.h" #include "noscrollcombobox.h" +#include "message.h" #include #include @@ -75,7 +75,14 @@ void PreferenceEditor::updateFields() { ui->checkBox_OpenRecentProject->setChecked(porymapConfig.reopenOnLaunch); ui->checkBox_CheckForUpdates->setChecked(porymapConfig.checkForUpdates); ui->checkBox_DisableEventWarning->setChecked(porymapConfig.eventDeleteWarningDisabled); - ui->checkBox_AutocompleteAllScripts->setChecked(porymapConfig.loadAllEventScripts); + + if (porymapConfig.scriptAutocompleteMode == ScriptAutocompleteMode::MapOnly) { + ui->radioButton_AutocompleteMapScripts->setChecked(true); + } else if (porymapConfig.scriptAutocompleteMode == ScriptAutocompleteMode::MapAndCommon) { + ui->radioButton_AutocompleteCommonScripts->setChecked(true); + } else if (porymapConfig.scriptAutocompleteMode == ScriptAutocompleteMode::All) { + ui->radioButton_AutocompleteAllScripts->setChecked(true); + } auto logTypeEnd = porymapConfig.statusBarLogTypes.end(); ui->checkBox_StatusErrors->setChecked(porymapConfig.statusBarLogTypes.find(LogType::LOG_ERROR) != logTypeEnd); @@ -87,20 +94,28 @@ void PreferenceEditor::updateFields() { } void PreferenceEditor::saveFields() { + bool needsProjectReload = false; + bool changedTheme = false; if (themeSelector->currentText() != porymapConfig.theme) { porymapConfig.theme = themeSelector->currentText(); changedTheme = true; } - bool loadAllEventScripts = ui->checkBox_AutocompleteAllScripts->isChecked(); - if (loadAllEventScripts != porymapConfig.loadAllEventScripts) { - porymapConfig.loadAllEventScripts = loadAllEventScripts; - emit scriptSettingsChanged(loadAllEventScripts); + + auto scriptAutocompleteMode = ScriptAutocompleteMode::MapOnly; + if (ui->radioButton_AutocompleteCommonScripts->isChecked()) { + scriptAutocompleteMode = ScriptAutocompleteMode::MapAndCommon; + } else if (ui->radioButton_AutocompleteAllScripts->isChecked()) { + scriptAutocompleteMode = ScriptAutocompleteMode::All; } + if (scriptAutocompleteMode != porymapConfig.scriptAutocompleteMode) { + porymapConfig.scriptAutocompleteMode = scriptAutocompleteMode; + emit scriptSettingsChanged(scriptAutocompleteMode); + } + porymapConfig.eventSelectionShapeMode = ui->radioButton_OnSprite->isChecked() ? QGraphicsPixmapItem::MaskShape : QGraphicsPixmapItem::BoundingRectShape; porymapConfig.textEditorOpenFolder = ui->lineEdit_TextEditorOpenFolder->text(); porymapConfig.textEditorGotoLine = ui->lineEdit_TextEditorGotoLine->text(); - porymapConfig.monitorFiles = ui->checkBox_MonitorProjectFiles->isChecked(); porymapConfig.reopenOnLaunch = ui->checkBox_OpenRecentProject->isChecked(); porymapConfig.checkForUpdates = ui->checkBox_CheckForUpdates->isChecked(); porymapConfig.eventDeleteWarningDisabled = ui->checkBox_DisableEventWarning->isChecked(); @@ -110,6 +125,11 @@ void PreferenceEditor::saveFields() { if (ui->checkBox_StatusWarnings->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_WARN); if (ui->checkBox_StatusInformation->isChecked()) porymapConfig.statusBarLogTypes.insert(LogType::LOG_INFO); + if (porymapConfig.monitorFiles != ui->checkBox_MonitorProjectFiles->isChecked()) { + porymapConfig.monitorFiles = ui->checkBox_MonitorProjectFiles->isChecked(); + needsProjectReload = true; + } + if (porymapConfig.applicationFont != this->applicationFont) { porymapConfig.applicationFont = this->applicationFont; changedTheme = true; @@ -119,13 +139,19 @@ void PreferenceEditor::saveFields() { changedTheme = true; } - porymapConfig.save(); - - emit preferencesSaved(); - if (changedTheme) { emit themeChanged(porymapConfig.theme); } + + porymapConfig.save(); + emit preferencesSaved(); + + if (needsProjectReload) { + auto message = QStringLiteral("Some changes will only take effect after reloading the project. Reload the project now?"); + if (QuestionMessage::show(message, this) == QMessageBox::Yes) { + emit reloadProjectRequested(); + } + } } void PreferenceEditor::dialogButtonClicked(QAbstractButton *button) { diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index c4becaa5..dadaefd7 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -161,6 +161,7 @@ void ProjectSettingsEditor::initUi() { ui->spinBox_PlayerViewDistance_North->setMaximum(INT_MAX); ui->spinBox_PlayerViewDistance_East->setMaximum(INT_MAX); ui->spinBox_PlayerViewDistance_South->setMaximum(INT_MAX); + ui->spinBox_MetatileSelectorWidth->setMaximum(maxMetatileId + 1); // The values for some of the settings we provide in this window can be determined using constants in the user's projects. // If the user has these constants we disable these settings in the UI -- they can modify them using their constants. @@ -232,7 +233,7 @@ QList ProjectSettingsEditor::getBorderMetatileIds(bool customSize) { // Custom border size, read metatiles from line edit for (auto s : ui->lineEdit_BorderMetatiles->text().split(",")) { uint16_t metatileId = s.toUInt(nullptr, 0); - metatileIds.append(qMin(metatileId, static_cast(Project::getNumMetatilesTotal() - 1))); + metatileIds.append(qMin(metatileId, Block::getMaxMetatileId())); } } else { // Default border size, read metatiles from spin boxes @@ -475,7 +476,7 @@ void ProjectSettingsEditor::refresh() { ui->checkBox_PreserveMatchingOnlyData->setChecked(projectConfig.preserveMatchingOnlyData); // Radio buttons - if (projectConfig.setTransparentPixelsBlack) + if (projectConfig.transparencyColor == QColor(Qt::black)) ui->radioButton_RenderBlack->setChecked(true); else ui->radioButton_RenderFirstPalColor->setChecked(true); @@ -503,6 +504,7 @@ void ProjectSettingsEditor::refresh() { ui->spinBox_PlayerViewDistance_North->setValue(projectConfig.playerViewDistance.top()); ui->spinBox_PlayerViewDistance_East->setValue(projectConfig.playerViewDistance.right()); ui->spinBox_PlayerViewDistance_South->setValue(projectConfig.playerViewDistance.bottom()); + ui->spinBox_MetatileSelectorWidth->setValue(projectConfig.metatileSelectorWidth); // Set (and sync) border metatile IDs this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds); @@ -574,7 +576,7 @@ void ProjectSettingsEditor::save() { projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked(); projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked(); porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked(); - projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); + projectConfig.transparencyColor = ui->radioButton_RenderBlack->isChecked() ? QColor(Qt::black) : QColor(); projectConfig.preserveMatchingOnlyData = ui->checkBox_PreserveMatchingOnlyData->isChecked(); // Save spin box settings @@ -598,6 +600,7 @@ void ProjectSettingsEditor::save() { ui->spinBox_PlayerViewDistance_North->value(), ui->spinBox_PlayerViewDistance_East->value(), ui->spinBox_PlayerViewDistance_South->value()); + projectConfig.metatileSelectorWidth = ui->spinBox_MetatileSelectorWidth->value(); // Save line edit settings projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text(); diff --git a/src/ui/regionmapentriespixmapitem.cpp b/src/ui/regionmapentriespixmapitem.cpp index 8ac888a0..5bf9a08f 100644 --- a/src/ui/regionmapentriespixmapitem.cpp +++ b/src/ui/regionmapentriespixmapitem.cpp @@ -17,12 +17,12 @@ void RegionMapEntriesPixmapItem::draw() { entry_w = entry.width, entry_h = entry.height; } - QImage image(region_map->tilemapWidth() * 8, region_map->tilemapHeight() * 8, QImage::Format_RGBA8888); + QImage image(region_map->tilemapWidth() * this->cellWidth, region_map->tilemapHeight() * this->cellHeight, QImage::Format_RGBA8888); QPainter painter(&image); for (int i = 0; i < region_map->tilemapSize(); i++) { QImage bottom_img = this->tile_selector->tileImg(region_map->getTile(i)); - QImage top_img(8, 8, QImage::Format_RGBA8888); + QImage top_img(this->cellWidth, this->cellHeight, QImage::Format_RGBA8888); int x = i % region_map->tilemapWidth(); int y = i / region_map->tilemapWidth(); bool insideEntry = false; @@ -40,7 +40,7 @@ void RegionMapEntriesPixmapItem::draw() { } else { top_img.fill(Qt::black); } - QPoint pos = QPoint(x * 8, y * 8); + QPoint pos = QPoint(x * this->cellWidth, y * this->cellHeight); painter.setOpacity(1); painter.drawImage(pos, bottom_img); painter.save(); diff --git a/src/ui/regionmaplayoutpixmapitem.cpp b/src/ui/regionmaplayoutpixmapitem.cpp index eda35152..1895f8c8 100644 --- a/src/ui/regionmaplayoutpixmapitem.cpp +++ b/src/ui/regionmaplayoutpixmapitem.cpp @@ -8,7 +8,7 @@ void RegionMapLayoutPixmapItem::draw() { QPainter painter(&image); for (int i = 0; i < region_map->tilemapSize(); i++) { QImage bottom_img = this->tile_selector->tileImg(region_map->getTile(i)); - QImage top_img(8, 8, QImage::Format_RGBA8888); + QImage top_img(this->cellWidth, this->cellHeight, QImage::Format_RGBA8888); if (region_map->squareHasMap(i)) { top_img.fill(Qt::gray); } else { @@ -16,7 +16,7 @@ void RegionMapLayoutPixmapItem::draw() { } int x = i % region_map->tilemapWidth(); int y = i / region_map->tilemapWidth(); - QPoint pos = QPoint(x * 8, y * 8); + QPoint pos = QPoint(x * this->cellWidth, y * this->cellHeight); painter.setOpacity(1); painter.drawImage(pos, bottom_img); painter.save(); diff --git a/src/ui/resizelayoutpopup.cpp b/src/ui/resizelayoutpopup.cpp index 189ba4af..afbf63fb 100644 --- a/src/ui/resizelayoutpopup.cpp +++ b/src/ui/resizelayoutpopup.cpp @@ -7,42 +7,9 @@ #include "ui_resizelayoutpopup.h" -CheckeredBgScene::CheckeredBgScene(QObject *parent) : QGraphicsScene(parent) { } - -void CheckeredBgScene::drawBackground(QPainter *painter, const QRectF &rect) { - QRect r = rect.toRect(); - int xMin = r.left() - r.left() % this->gridSize - this->gridSize; - int yMin = r.top() - r.top() % this->gridSize - this->gridSize; - int xMax = r.right() - r.right() % this->gridSize + this->gridSize; - int yMax = r.bottom() - r.bottom() % this->gridSize + this->gridSize; - - // draw grid 16x16 from top to bottom of scene - QColor paintColor(0x00ff00); - for (int x = xMin, xTile = 0; x <= xMax; x += this->gridSize, xTile++) { - for (int y = yMin, yTile = 0; y <= yMax; y += this->gridSize, yTile++) { - if (!((xTile ^ yTile) & 1)) { // tile numbers have same parity (evenness) - if (this->validRect.contains(x, y)) - paintColor = QColor(132, 217, 165); // green light color - else - paintColor = 0xbcbcbc; // normal light color - } - else { - if (this->validRect.contains(x, y)) - paintColor = QColor(76, 178, 121); // green dark color - else - paintColor = 0x969696; // normal dark color - } - painter->fillRect(QRect(x, y, this->gridSize, this->gridSize), paintColor); - } - } -} - -/****************************************************************************** - ************************************************************************ - ******************************************************************************/ - -BoundedPixmapItem::BoundedPixmapItem(const QPixmap &pixmap, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { +BoundedPixmapItem::BoundedPixmapItem(const QPixmap &pixmap, const QSize &cellSize, QGraphicsItem *parent) : QGraphicsPixmapItem(pixmap, parent) { setFlags(this->flags() | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemSendsGeometryChanges | QGraphicsItem::ItemIsSelectable); + this->cellSize = cellSize; } void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { @@ -61,7 +28,8 @@ void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem QVariant BoundedPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) { if (change == ItemPositionChange && scene()) { QPointF newPos = value.toPointF(); - return QPointF(Util::roundUp(newPos.x(), 16), Util::roundUp(newPos.y(), 16)); + return QPointF(Util::roundUpToMultiple(newPos.x(), this->cellSize.width()), + Util::roundUpToMultiple(newPos.y(), this->cellSize.height())); } else return QGraphicsItem::itemChange(change, value); @@ -83,7 +51,7 @@ ResizeLayoutPopup::ResizeLayoutPopup(QWidget *parent, Layout *layout, Project *p this->setWindowFlags(this->windowFlags() | Qt::FramelessWindowHint); this->setWindowModality(Qt::ApplicationModal); - this->scene = new CheckeredBgScene(this); + this->scene = new CheckeredBgScene(Metatile::pixelSize(), this); this->ui->graphicsView->setScene(this->scene); this->ui->graphicsView->setRenderHints(QPainter::Antialiasing); this->ui->graphicsView->setViewportUpdateMode(QGraphicsView::FullViewportUpdate); @@ -124,11 +92,13 @@ void ResizeLayoutPopup::setupLayoutView() { this->ui->spinBox_borderHeight->setValue(this->layout->getBorderHeight()); // Layout stuff - this->layoutPixmap = new BoundedPixmapItem(this->layout->pixmap); + this->layoutPixmap = new BoundedPixmapItem(this->layout->pixmap, Metatile::pixelSize()); this->scene->addItem(layoutPixmap); int maxWidth = this->project->getMaxMapWidth(); int maxHeight = this->project->getMaxMapHeight(); - QGraphicsRectItem *cover = new QGraphicsRectItem(-maxWidth * 8, -maxHeight * 8, maxWidth * 16, maxHeight * 16); + int maxPixelWidth = maxWidth * Metatile::pixelWidth() * 2; // *2 to allow reaching max dimension by expanding from 0,0 in either direction + int maxPixelHeight = maxHeight * Metatile::pixelHeight() * 2; + QGraphicsRectItem *cover = new QGraphicsRectItem(-(maxPixelWidth / 2), -(maxPixelHeight / 2), maxPixelWidth, maxPixelHeight); this->scene->addItem(cover); this->ui->spinBox_width->setMinimum(1); @@ -136,14 +106,14 @@ void ResizeLayoutPopup::setupLayoutView() { this->ui->spinBox_height->setMinimum(1); this->ui->spinBox_height->setMaximum(maxHeight); - static bool layoutSizeRectVisible = true; - - this->outline = new ResizableRect(this, &layoutSizeRectVisible, this->layout->getWidth(), this->layout->getHeight(), qRgb(255, 0, 255)); + this->outline = new ResizableRect(this, Metatile::pixelSize(), this->layout->pixelSize(), qRgb(255, 0, 255)); this->outline->setZValue(Editor::ZValue::ResizeLayoutPopup); // Ensure on top of view this->outline->setLimit(cover->rect().toAlignedRect()); connect(outline, &ResizableRect::rectUpdated, [=](QRect rect){ // Note: this extra limit check needs access to the project values, so it is done here and not ResizableRect::mouseMoveEvent - int size = this->project->getMapDataSize(rect.width() / 16, rect.height() / 16); + int metatilesWide = rect.width() / Metatile::pixelWidth(); + int metatilesTall = rect.height() / Metatile::pixelHeight(); + int size = this->project->getMapDataSize(metatilesWide, metatilesTall); int maxSize = this->project->getMaxMapDataSize(); if (size > maxSize) { QSize addition = this->project->getMapSizeAddition(); @@ -153,8 +123,8 @@ void ResizeLayoutPopup::setupLayoutView() { .arg(addition.width()) .arg(addition.height()) .arg(maxSize) - .arg(rect.width() / 16) - .arg(rect.height() / 16) + .arg(metatilesWide) + .arg(metatilesTall) .arg(size), this); // adjust rect to last accepted size @@ -162,8 +132,12 @@ void ResizeLayoutPopup::setupLayoutView() { } this->scene->setValidRect(rect); this->outline->setRect(rect); - this->ui->spinBox_width->setValue(rect.width() / 16); - this->ui->spinBox_height->setValue(rect.height() / 16); + + // Rect may have changed, ensure spinners reflect final rect size. + const QSignalBlocker b_Width(this->ui->spinBox_width); + const QSignalBlocker b_Height(this->ui->spinBox_height); + this->ui->spinBox_width->setValue(rect.width() / Metatile::pixelWidth()); + this->ui->spinBox_height->setValue(rect.height() / Metatile::pixelHeight()); }); scene->addItem(outline); @@ -173,7 +147,7 @@ void ResizeLayoutPopup::setupLayoutView() { this->scale = 1.0; QRectF rect = this->outline->rect(); - const int marginSize = 10 * 16; // Leave a margin of 10 metatiles around the map + const int marginSize = 10 * Metatile::pixelWidth(); // Leave a margin of 10 metatiles around the map rect += QMargins(marginSize, marginSize, marginSize, marginSize); this->ui->graphicsView->fitInView(rect, Qt::KeepAspectRatio); } @@ -181,25 +155,23 @@ void ResizeLayoutPopup::setupLayoutView() { void ResizeLayoutPopup::on_spinBox_width_valueChanged(int value) { if (!this->outline) return; QRectF rect = this->outline->rect(); - this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), value * 16, rect.height())); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), value * Metatile::pixelWidth(), rect.height())); } void ResizeLayoutPopup::on_spinBox_height_valueChanged(int value) { if (!this->outline) return; QRectF rect = this->outline->rect(); - this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), rect.width(), value * 16)); + this->outline->updatePosFromRect(QRect(rect.x(), rect.y(), rect.width(), value * Metatile::pixelHeight())); } /// Result is the number of metatiles to add (or subtract) to each side of the map after dimension changes QMargins ResizeLayoutPopup::getResult() { QMargins result = QMargins(); - - result.setLeft(this->layoutPixmap->x() - this->outline->rect().left()); - result.setTop(this->layoutPixmap->y() - this->outline->rect().top()); - result.setRight(this->outline->rect().right() - (this->layoutPixmap->x() + this->layoutPixmap->pixmap().width())); - result.setBottom(this->outline->rect().bottom() - (this->layoutPixmap->y() + this->layoutPixmap->pixmap().height())); - - return result / 16; + result.setLeft((this->layoutPixmap->x() - this->outline->rect().left()) / Metatile::pixelWidth()); + result.setTop((this->layoutPixmap->y() - this->outline->rect().top()) / Metatile::pixelHeight()); + result.setRight((this->outline->rect().right() - (this->layoutPixmap->x() + this->layoutPixmap->pixmap().width())) / Metatile::pixelWidth()); + result.setBottom((this->outline->rect().bottom() - (this->layoutPixmap->y() + this->layoutPixmap->pixmap().height())) / Metatile::pixelHeight()); + return result; } QSize ResizeLayoutPopup::getBorderResult() { diff --git a/src/ui/selectablepixmapitem.cpp b/src/ui/selectablepixmapitem.cpp index e15824ad..267fbd1f 100644 --- a/src/ui/selectablepixmapitem.cpp +++ b/src/ui/selectablepixmapitem.cpp @@ -1,11 +1,6 @@ #include "selectablepixmapitem.h" #include -QPoint SelectablePixmapItem::getSelectionDimensions() -{ - return QPoint(abs(this->selectionOffsetX) + 1, abs(this->selectionOffsetY) + 1); -} - QPoint SelectablePixmapItem::getSelectionStart() { int x = this->selectionInitialX; @@ -15,47 +10,60 @@ QPoint SelectablePixmapItem::getSelectionStart() return QPoint(x, y); } -void SelectablePixmapItem::select(int x, int y, int width, int height) -{ - this->selectionInitialX = x; - this->selectionInitialY = y; - this->selectionOffsetX = qMax(0, qMin(width, this->maxSelectionWidth)); - this->selectionOffsetY = qMax(0, qMin(height, this->maxSelectionHeight)); - this->draw(); - emit this->selectionChanged(x, y, width, height); +void SelectablePixmapItem::setSelection(const QPoint &pos, const QSize &size) { + this->selectionInitialX = pos.x(); + this->selectionInitialY = pos.y(); + this->selectionOffsetX = getBoundedWidth(size.width()) - 1; + this->selectionOffsetY = getBoundedHeight(size.height()) - 1; +} + +void SelectablePixmapItem::select(const QPoint &pos, const QSize &size) { + setSelection(pos, size); + draw(); + emit selectionChanged(pos, getSelectionDimensions()); } void SelectablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - this->selectionInitialX = pos.x(); - this->selectionInitialY = pos.y(); - this->selectionOffsetX = 0; - this->selectionOffsetY = 0; - this->updateSelection(pos.x(), pos.y()); + QPoint pos = getCellPos(event->pos()); + setSelection(pos, QSize(1,1)); + this->prevCellPos = pos; + updateSelection(pos); } void SelectablePixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - this->updateSelection(pos.x(), pos.y()); + QPoint pos = getCellPos(event->pos()); + if (pos == this->prevCellPos) + return; + this->prevCellPos = pos; + updateSelection(pos); } void SelectablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - this->updateSelection(pos.x(), pos.y()); + updateSelection(getCellPos(event->pos())); } -void SelectablePixmapItem::updateSelection(int x, int y) -{ +void SelectablePixmapItem::setMaxSelectionSize(int width, int height) { + this->maxSelectionWidth = qMax(width, 1); + this->maxSelectionHeight = qMax(height, 1); + + // Update the selection if we shrank below the current selection size. + QSize size = getSelectionDimensions(); + if (size.width() > this->maxSelectionWidth || size.height() > this->maxSelectionHeight) { + setSelection(getSelectionStart(), size); + draw(); + // 'draw' is allowed to change the selection position/size, + // so call these again rather than keep values from above. + emit selectionChanged(getSelectionStart(), getSelectionDimensions()); + } +} + +void SelectablePixmapItem::updateSelection(const QPoint &pos) { // Snap to a valid position inside the selection area. - int width = pixmap().width() / this->cellWidth; - int height = pixmap().height() / this->cellHeight; - if (x < 0) x = 0; - if (x >= width) x = width - 1; - if (y < 0) y = 0; - if (y >= height) y = height - 1; + int x = qBound(0, pos.x(), cellsWide() - 1); + int y = qBound(0, pos.y(), cellsTall() - 1); this->selectionOffsetX = x - this->selectionInitialX; this->selectionOffsetY = y - this->selectionInitialY; @@ -76,25 +84,30 @@ void SelectablePixmapItem::updateSelection(int x, int y) this->selectionOffsetY = -this->maxSelectionHeight + 1; } - this->draw(); - emit this->selectionChanged(x, y, width, height); + draw(); + emit selectionChanged(QPoint(x, y), getSelectionDimensions()); } -QPoint SelectablePixmapItem::getCellPos(QPointF pos) -{ - if (pos.x() < 0) pos.setX(0); - if (pos.y() < 0) pos.setY(0); - if (pos.x() >= this->pixmap().width()) pos.setX(this->pixmap().width() - 1); - if (pos.y() >= this->pixmap().height()) pos.setY(this->pixmap().height() - 1); - return QPoint(static_cast(pos.x()) / this->cellWidth, - static_cast(pos.y()) / this->cellHeight); +void SelectablePixmapItem::setSelectionStyle(Qt::PenStyle style) { + this->selectionStyle = style; + draw(); } -void SelectablePixmapItem::drawSelection() -{ - QPoint origin = this->getSelectionStart(); - QPoint dimensions = this->getSelectionDimensions(); - QRect selectionRect(origin.x() * this->cellWidth, origin.y() * this->cellHeight, dimensions.x() * this->cellWidth, dimensions.y() * this->cellHeight); +QPoint SelectablePixmapItem::getCellPos(const QPointF &itemPos) { + if (this->cellWidth == 0 || this->cellHeight == 0 || pixmap().isNull()) + return QPoint(0,0); + + int x = qBound(0, static_cast(itemPos.x()), pixmap().width() - 1); + int y = qBound(0, static_cast(itemPos.y()), pixmap().height() - 1); + return QPoint(x / this->cellWidth, y / this->cellHeight); +} + +void SelectablePixmapItem::drawSelection() { + drawSelectionRect(getSelectionStart(), getSelectionDimensions(), this->selectionStyle); +} + +void SelectablePixmapItem::drawSelectionRect(const QPoint &origin, const QSize &dimensions, Qt::PenStyle style) { + QRect selectionRect(origin.x() * this->cellWidth, origin.y() * this->cellHeight, dimensions.width() * this->cellWidth, dimensions.height() * this->cellHeight); // If a selection is fully outside the bounds of the selectable area, don't draw anything. // This prevents the border of the selection rectangle potentially being visible on an otherwise invisible selection. @@ -102,12 +115,27 @@ void SelectablePixmapItem::drawSelection() if (!selectionRect.intersects(pixmap.rect())) return; + auto fillPen = QPen(QColor(Qt::white)); + auto borderPen = QPen(QColor(Qt::black)); + borderPen.setStyle(style); + QPainter painter(&pixmap); - painter.setPen(QColor(0xff, 0xff, 0xff)); - painter.drawRect(selectionRect.x(), selectionRect.y(), selectionRect.width() - 1, selectionRect.height() - 1); - painter.setPen(QColor(0, 0, 0)); - painter.drawRect(selectionRect.x() - 1, selectionRect.y() - 1, selectionRect.width() + 1, selectionRect.height() + 1); - painter.drawRect(selectionRect.x() + 1, selectionRect.y() + 1, selectionRect.width() - 3, selectionRect.height() - 3); + if (style == Qt::SolidLine) { + painter.setPen(fillPen); + painter.drawRect(selectionRect - QMargins(1,1,1,1)); + painter.setPen(borderPen); + painter.drawRect(selectionRect); + painter.drawRect(selectionRect - QMargins(2,2,2,2)); + } else { + // Having separately sized rectangles with anything but a + // solid line looks a little wonky because the dashes wont align. + // For non-solid styles we'll draw a base white rectangle, then draw + // a styled black rectangle on top + painter.setPen(fillPen); + painter.drawRect(selectionRect); + painter.setPen(borderPen); + painter.drawRect(selectionRect); + } this->setPixmap(pixmap); } diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp index d0ef943d..f2f8083b 100644 --- a/src/ui/shortcutseditor.cpp +++ b/src/ui/shortcutseditor.cpp @@ -12,6 +12,7 @@ #include #include #include +#include ShortcutsEditor::ShortcutsEditor(QWidget *parent) : @@ -78,11 +79,43 @@ void ShortcutsEditor::resetShortcuts() { } } +void ShortcutsEditor::parseObject(const QObject *object, QMap *objects_labels, QMap *objects_prefixes) { + auto menu = dynamic_cast(object); + if (menu) { + // If a menu is provided we'll use it to create prefixes for any of the menu's actions, + // and automatically insert its actions in the shortcut list (if they weren't present already). + // The prefixing assumes the provided object list is in inheritance order. + // These prefixes are important for differentiating actions that may have the same display text + // but appear in different menus. + for (const auto &action : menu->actions()) { + if (!menu->title().isEmpty()) { + auto prefix = QString("%1%2 > ") + .arg(objects_prefixes->value(menu->menuAction())) // If this is a sub-menu, it may itself have a prefix. + .arg(menu->title()); + objects_prefixes->insert(action, prefix); + } + parseObject(action, objects_labels, objects_prefixes); + } + } else if (object && !object->objectName().isEmpty() && !object->objectName().startsWith("_q_")) { + QString label = getLabel(object); + if (!label.isEmpty()) { + objects_labels->insert(object, label); + } + } +} + void ShortcutsEditor::parseObjectList(const QObjectList &objectList) { - for (auto *object : objectList) { - const auto label = getLabel(object); - if (!label.isEmpty() && !object->objectName().isEmpty() && !object->objectName().startsWith("_q_")) - labels_objects.insert(label, object); + QMap objects_labels; + QMap objects_prefixes; + for (const auto &object : objectList) { + parseObject(object, &objects_labels, &objects_prefixes); + } + + // Sort alphabetically by label + this->labels_objects.clear(); + for (auto it = objects_labels.constBegin(); it != objects_labels.constEnd(); it++) { + QString fullLabel = objects_prefixes.value(it.key()) + it.value(); + this->labels_objects.insert(fullLabel, it.key()); } } @@ -151,9 +184,12 @@ void ShortcutsEditor::checkForDuplicates(const QKeySequence &keySequence) { if (!sender_multiKeyEdit) return; - for (auto *sibling_multiKeyEdit : siblings(sender_multiKeyEdit)) - if (sibling_multiKeyEdit->contains(keySequence)) + for (auto *sibling_multiKeyEdit : siblings(sender_multiKeyEdit)) { + if (sibling_multiKeyEdit->contains(keySequence)) { promptUserOnDuplicateFound(sender_multiKeyEdit, sibling_multiKeyEdit); + break; + } + } } QList ShortcutsEditor::siblings(MultiKeyEdit *multiKeyEdit) const { @@ -164,26 +200,25 @@ QList ShortcutsEditor::siblings(MultiKeyEdit *multiKeyEdit) cons void ShortcutsEditor::promptUserOnDuplicateFound(MultiKeyEdit *sender, MultiKeyEdit *sibling) { const auto duplicateKeySequence = sender->keySequences().last(); - const auto siblingLabel = getLabel(multiKeyEdits_objects.value(sibling)); - const auto message = QString( - "Shortcut '%1' is already used by '%2', would you like to replace it?") - .arg(duplicateKeySequence.toString()).arg(siblingLabel); + const auto siblingLabel = this->labels_objects.key(multiKeyEdits_objects.value(sibling)); + if (siblingLabel.isEmpty()) + return; + const auto message = QString("Shortcut '%1' is already used by '%2', would you like to replace it?") + .arg(duplicateKeySequence.toString()) + .arg(siblingLabel); - const auto result = QMessageBox::question( - this, QApplication::applicationName(), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + // QKeySequenceEdit::keySequenceChanged fires when editing finishes on a QKeySequenceEdit, + // even if no change occurs. Displaying our question prompt will cause the edit to lose focus + // and fire another signal, which would cause another "duplicate shortcut" prompt to appear. + // For this reason we need to block their signals before the message is displayed. + const QSignalBlocker b_Sender(sender); + const QSignalBlocker b_Sibling(sibling); - if (result == QMessageBox::Yes) - removeKeySequence(duplicateKeySequence, sibling); - else - removeKeySequence(duplicateKeySequence, sender); - - activateWindow(); -} - -void ShortcutsEditor::removeKeySequence(const QKeySequence &keySequence, MultiKeyEdit *multiKeyEdit) { - multiKeyEdit->blockSignals(true); - multiKeyEdit->removeOne(keySequence); - multiKeyEdit->blockSignals(false); + if (QuestionMessage::show(message, this) == QMessageBox::Yes) { + sibling->removeOne(duplicateKeySequence); + } else { + sender->removeOne(duplicateKeySequence); + } } void ShortcutsEditor::dialogButtonClicked(QAbstractButton *button) { diff --git a/src/ui/tilemaptileselector.cpp b/src/ui/tilemaptileselector.cpp index 45771ad2..35dd294a 100644 --- a/src/ui/tilemaptileselector.cpp +++ b/src/ui/tilemaptileselector.cpp @@ -7,9 +7,9 @@ void TilemapTileSelector::draw() { this->pixelWidth = width_; size_t height_ = this->tileset.height(); this->pixelHeight = height_; - size_t ntiles_ = (width_/8) * (height_/8); + size_t ntiles_ = (width_/this->cellWidth) * (height_/this->cellHeight); - this->numTilesWide = width_ / 8; + this->numTilesWide = width_ / this->cellWidth; this->numTiles = ntiles_; this->setPixmap(QPixmap::fromImage(this->setPalette(this->tile_palette))); @@ -92,7 +92,7 @@ QImage TilemapTileSelector::tileImg(shared_ptr tile) { QImage tilesetImage = setPalette(tile->palette()); // take a tile from the tileset - QImage img = tilesetImage.copy(pos.x() * 8, pos.y() * 8, 8, 8); + QImage img = tilesetImage.copy(pos.x() * this->cellWidth, pos.y() * this->cellHeight, this->cellWidth, this->cellHeight); // QImage::flip was introduced in 6.9.0 to replace QImage::mirrored. #if (QT_VERSION >= QT_VERSION_CHECK(6, 9, 0)) diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 6fe96f0f..38a41bf2 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -24,37 +24,56 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) hasUnsavedChanges(false) { setAttribute(Qt::WA_DeleteOnClose); - setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); ui->setupUi(this); - connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::setXFlip); - connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::setYFlip); - this->tileXFlip = ui->checkBox_xFlip->isChecked(); - this->tileYFlip = ui->checkBox_yFlip->isChecked(); - this->paletteId = ui->spinBox_paletteSelector->value(); + ui->spinBox_paletteSelector->setRange(0, Project::getNumPalettesTotal() - 1); - connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); + auto validator = new IdentifierValidator(this); + validator->setAllowEmpty(true); + ui->lineEdit_MetatileLabel->setValidator(validator); ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); - ui->spinBox_paletteSelector->setMinimum(0); - ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); - - auto validator = new IdentifierValidator(this); - validator->setAllowEmpty(true); - ui->lineEdit_metatileLabel->setValidator(validator); - ActiveWindowFilter *filter = new ActiveWindowFilter(this); connect(filter, &ActiveWindowFilter::activated, this, &TilesetEditor::onWindowActivated); this->installEventFilter(filter); + setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); + + connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::refreshTileFlips); + connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::refreshTileFlips); + + connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); + + connect(ui->actionImport_Primary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->secondaryTileset); }); + + connect(ui->actionImport_Primary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->primaryTileset); }); + connect(ui->actionImport_Secondary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->secondaryTileset); }); + + connect(ui->actionExport_Primary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->primaryTileset); }); + connect(ui->actionExport_Secondary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->secondaryTileset); }); + + connect(ui->actionExport_Primary_Porytiles_Layer_Images, &QAction::triggered, [this] { exportPorytilesLayerImages(this->primaryTileset); }); + connect(ui->actionExport_Secondary_Porytiles_Layer_Images, &QAction::triggered, [this] { exportPorytilesLayerImages(this->secondaryTileset); }); + + connect(ui->actionExport_Metatiles_Image, &QAction::triggered, [this] { exportMetatilesImage(); }); + + connect(ui->spinBox_paletteSelector, QOverload::of(&QSpinBox::valueChanged), this, &TilesetEditor::refreshPaletteId); + + connect(ui->actionLayer_Arrangement_Horizontal, &QAction::triggered, [this] { setMetatileLayerOrientation(Qt::Horizontal); }); + connect(ui->actionLayer_Arrangement_Vertical, &QAction::triggered, [this] { setMetatileLayerOrientation(Qt::Vertical); }); + + connect(ui->lineEdit_MetatileLabel, &QLineEdit::editingFinished, this, &TilesetEditor::commitMetatileLabel); + initAttributesUi(); initMetatileSelector(); initMetatileLayersItem(); initTileSelector(); initSelectedTileItem(); initShortcuts(); + setMetatileLayerOrientation(porymapConfig.tilesetEditorLayerOrientation); this->metatileSelector->select(0); restoreWindowState(); } @@ -74,6 +93,7 @@ TilesetEditor::~TilesetEditor() delete selectedTileScene; delete metatileLayersScene; delete copiedMetatile; + delete metatileImageExportSettings; this->metatileHistory.clear(); } @@ -114,97 +134,90 @@ void TilesetEditor::setTilesets(QString primaryTilesetLabel, QString secondaryTi this->metatileReloadQueue.clear(); Tileset *primaryTileset = project->getTileset(primaryTilesetLabel); Tileset *secondaryTileset = project->getTileset(secondaryTilesetLabel); - if (this->primaryTileset) delete this->primaryTileset; - if (this->secondaryTileset) delete this->secondaryTileset; + delete this->primaryTileset; + delete this->secondaryTileset; this->primaryTileset = new Tileset(*primaryTileset); this->secondaryTileset = new Tileset(*secondaryTileset); - if (paletteEditor) paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); - this->initMetatileHistory(); + if (this->paletteEditor) this->paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); + initMetatileHistory(); } void TilesetEditor::initAttributesUi() { - connect(ui->comboBox_metatileBehaviors, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitMetatileBehavior); - connect(ui->comboBox_encounterType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitEncounterType); - connect(ui->comboBox_terrainType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitTerrainType); - connect(ui->comboBox_layerType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitLayerType); + connect(ui->comboBox_MetatileBehaviors, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitMetatileBehavior); + connect(ui->comboBox_EncounterType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitEncounterType); + connect(ui->comboBox_TerrainType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitTerrainType); + connect(ui->comboBox_LayerType, &NoScrollComboBox::editingFinished, this, &TilesetEditor::commitLayerType); // Behavior if (projectConfig.metatileBehaviorMask) { for (auto i = project->metatileBehaviorMapInverse.constBegin(); i != project->metatileBehaviorMapInverse.constEnd(); i++) { - this->ui->comboBox_metatileBehaviors->addItem(i.value(), i.key()); + this->ui->comboBox_MetatileBehaviors->addItem(i.value(), i.key()); } - this->ui->comboBox_metatileBehaviors->setMinimumContentsLength(0); + this->ui->comboBox_MetatileBehaviors->setMinimumContentsLength(0); } else { - this->ui->comboBox_metatileBehaviors->setVisible(false); - this->ui->label_metatileBehavior->setVisible(false); + this->ui->frame_MetatileBehavior->setVisible(false); } // Terrain Type if (projectConfig.metatileTerrainTypeMask) { for (auto i = project->terrainTypeToName.constBegin(); i != project->terrainTypeToName.constEnd(); i++) { - this->ui->comboBox_terrainType->addItem(i.value(), i.key()); + this->ui->comboBox_TerrainType->addItem(i.value(), i.key()); } - this->ui->comboBox_terrainType->setMinimumContentsLength(0); + this->ui->comboBox_TerrainType->setMinimumContentsLength(0); } else { - this->ui->comboBox_terrainType->setVisible(false); - this->ui->label_terrainType->setVisible(false); + this->ui->frame_TerrainType->setVisible(false); } // Encounter Type if (projectConfig.metatileEncounterTypeMask) { for (auto i = project->encounterTypeToName.constBegin(); i != project->encounterTypeToName.constEnd(); i++) { - this->ui->comboBox_encounterType->addItem(i.value(), i.key()); + this->ui->comboBox_EncounterType->addItem(i.value(), i.key()); } - this->ui->comboBox_encounterType->setMinimumContentsLength(0); + this->ui->comboBox_EncounterType->setMinimumContentsLength(0); } else { - this->ui->comboBox_encounterType->setVisible(false); - this->ui->label_encounterType->setVisible(false); + this->ui->frame_EncounterType->setVisible(false); } // Layer Type if (!projectConfig.tripleLayerMetatilesEnabled) { - this->ui->comboBox_layerType->addItem("Normal - Middle/Top", Metatile::LayerType::Normal); - this->ui->comboBox_layerType->addItem("Covered - Bottom/Middle", Metatile::LayerType::Covered); - this->ui->comboBox_layerType->addItem("Split - Bottom/Top", Metatile::LayerType::Split); - this->ui->comboBox_layerType->setEditable(false); - this->ui->comboBox_layerType->setMinimumContentsLength(0); + this->ui->comboBox_LayerType->addItem("Normal - Middle/Top", Metatile::LayerType::Normal); + this->ui->comboBox_LayerType->addItem("Covered - Bottom/Middle", Metatile::LayerType::Covered); + this->ui->comboBox_LayerType->addItem("Split - Bottom/Top", Metatile::LayerType::Split); + this->ui->comboBox_LayerType->setEditable(false); + this->ui->comboBox_LayerType->setMinimumContentsLength(0); if (!projectConfig.metatileLayerTypeMask) { // User doesn't have triple layer metatiles, but has no layer type attribute. // Porymap is still using the layer type value to render metatiles, and with // no mask set every metatile will be "Middle/Top", so just display the combo // box but prevent the user from changing the value. - this->ui->comboBox_layerType->setEnabled(false); + this->ui->comboBox_LayerType->setEnabled(false); } } else { - this->ui->comboBox_layerType->setVisible(false); - this->ui->label_layerType->setVisible(false); + this->ui->frame_LayerType->setVisible(false); this->ui->label_BottomTop->setText("Bottom/Middle/Top"); } // Raw attributes value - ui->spinBox_rawAttributesValue->setMaximum(Metatile::getMaxAttributesMask()); + ui->spinBox_RawAttributesValue->setMaximum(Metatile::getMaxAttributesMask()); setRawAttributesVisible(ui->actionShow_Raw_Metatile_Attributes->isChecked()); - connect(ui->spinBox_rawAttributesValue, &UIntHexSpinBox::editingFinished, this, &TilesetEditor::onRawAttributesEdited); + connect(ui->spinBox_RawAttributesValue, &UIntHexSpinBox::editingFinished, this, &TilesetEditor::onRawAttributesEdited); connect(ui->actionShow_Raw_Metatile_Attributes, &QAction::toggled, this, &TilesetEditor::setRawAttributesVisible); - - this->ui->frame_Properties->adjustSize(); } void TilesetEditor::setRawAttributesVisible(bool visible) { porymapConfig.showTilesetEditorRawAttributes = visible; - ui->label_rawAttributesValue->setVisible(visible); - ui->spinBox_rawAttributesValue->setVisible(visible); + ui->frame_RawAttributesValue->setVisible(visible); + rebuildMetatilePropertiesFrame(); } void TilesetEditor::initMetatileSelector() { - this->metatileSelector = new TilesetEditorMetatileSelector(this->primaryTileset, this->secondaryTileset, this->layout); - connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileChanged, - this, &TilesetEditor::onHoveredMetatileChanged); - connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileCleared, - this, &TilesetEditor::onHoveredMetatileCleared); - connect(this->metatileSelector, &TilesetEditorMetatileSelector::selectedMetatileChanged, - this, &TilesetEditor::onSelectedMetatileChanged); + this->metatileSelector = new TilesetEditorMetatileSelector(projectConfig.metatileSelectorWidth, this->primaryTileset, this->secondaryTileset, this->layout); + connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileChanged, this, &TilesetEditor::showMetatileStatus); + connect(this->metatileSelector, &TilesetEditorMetatileSelector::hoveredMetatileCleared, this, &TilesetEditor::onHoveredMetatileCleared); + connect(this->metatileSelector, &TilesetEditorMetatileSelector::selectedMetatileChanged, this, &TilesetEditor::onSelectedMetatileChanged); + connect(this->metatileSelector, &TilesetEditorMetatileSelector::swapRequested, this, &TilesetEditor::commitMetatileSwap); + connect(ui->actionSwap_Metatiles, &QAction::toggled, this->metatileSelector, &TilesetEditorMetatileSelector::setSwapMode); bool showGrid = porymapConfig.showTilesetEditorMetatileGrid; this->ui->actionMetatile_Grid->setChecked(showGrid); @@ -220,17 +233,89 @@ void TilesetEditor::initMetatileSelector() this->ui->horizontalSlider_MetatilesZoom->setValue(porymapConfig.tilesetEditorMetatilesZoom); } +void TilesetEditor::setMetatileLayerOrientation(Qt::Orientation orientation) { + // Sync settings + bool horizontal = (orientation == Qt::Horizontal); + porymapConfig.tilesetEditorLayerOrientation = orientation; + const QSignalBlocker b_Horizontal(ui->actionLayer_Arrangement_Horizontal); + const QSignalBlocker b_Vertical(ui->actionLayer_Arrangement_Vertical); + ui->actionLayer_Arrangement_Horizontal->setChecked(horizontal); + ui->actionLayer_Arrangement_Vertical->setChecked(!horizontal); + + this->metatileLayersItem->setOrientation(orientation); + + int numTilesWide = Metatile::tileWidth(); + int numTilesTall = Metatile::tileHeight(); + int numLayers = projectConfig.getNumLayersInMetatile(); + if (horizontal) { + numTilesWide *= numLayers; + } else { + numTilesTall *= numLayers; + } + this->tileSelector->setMaxSelectionSize(numTilesWide, numTilesTall); + + const int scale = 2; + int w = Tile::pixelWidth() * numTilesWide * scale + 2; + int h = Tile::pixelHeight() * numTilesTall * scale + 2; + ui->graphicsView_selectedTile->setFixedSize(w, h); + ui->graphicsView_MetatileLayers->setFixedSize(w, h); + + drawSelectedTiles(); + + // If the layers are laid out vertically then the orientation is obvious, no need to label them. + // This also lets us give the vertical space of the label over to the layer view. + ui->label_BottomTop->setVisible(horizontal); + + rebuildMetatilePropertiesFrame(); +} + +// We rearrange the metatile properties panel depending on the orientation and size of the metatile layer view. +// If triple layer metatiles are in-use then layer type field is hidden, so there's an awkward amount of space +// next to the layer view, especially in the vertical orientation. +// We shift 1-2 widgets up to fill this space next to the layer view. This gets a little complicated because which +// widgets are available to move changes depending on the user's settings. +void TilesetEditor::rebuildMetatilePropertiesFrame() { + if (porymapConfig.tilesetEditorLayerOrientation == Qt::Horizontal) { + this->numLayerViewRows = 1; + } else { + this->numLayerViewRows = projectConfig.tripleLayerMetatilesEnabled ? 4 : 2; + } + + for (const auto &frame : ui->gridLayout_MetatileProperties->findChildren()) { + ui->gridLayout_MetatileProperties->removeWidget(frame); + } + ui->gridLayout_MetatileProperties->addWidget(ui->frame_Layers, 0, 0, this->numLayerViewRows, 1); + + int row = 0; + addWidgetToMetatileProperties(ui->frame_LayerType, &row, 2); + if (porymapConfig.tilesetEditorLayerOrientation == Qt::Horizontal) { + // When the layer view's orientation is horizontal we only allow the + // layer type selector to share the row with the layer view. + row = this->numLayerViewRows; + } + addWidgetToMetatileProperties(ui->frame_MetatileBehavior, &row, 2); + addWidgetToMetatileProperties(ui->frame_EncounterType, &row, 2); + addWidgetToMetatileProperties(ui->frame_TerrainType, &row, 2); + addWidgetToMetatileProperties(ui->frame_RawAttributesValue, &row, 2); + addWidgetToMetatileProperties(ui->frame_MetatileLabel, &row, 2); +} + +void TilesetEditor::addWidgetToMetatileProperties(QWidget *w, int *row, int rowSpan) { + if (w->isVisibleTo(ui->frame_Properties)) { + int col = (*row < this->numLayerViewRows) ? 1 : 0; // Shift widget over if it shares the row with the layer view + ui->gridLayout_MetatileProperties->addWidget(w, *row, col, rowSpan, -1); + *row += rowSpan; + } +} + void TilesetEditor::initMetatileLayersItem() { Metatile *metatile = Tileset::getMetatile(this->getSelectedMetatileId(), this->primaryTileset, this->secondaryTileset); this->metatileLayersItem = new MetatileLayersItem(metatile, this->primaryTileset, this->secondaryTileset); - connect(this->metatileLayersItem, &MetatileLayersItem::tileChanged, - this, &TilesetEditor::onMetatileLayerTileChanged); - connect(this->metatileLayersItem, &MetatileLayersItem::selectedTilesChanged, - this, &TilesetEditor::onMetatileLayerSelectionChanged); - connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileChanged, - this, &TilesetEditor::onHoveredTileChanged); - connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileCleared, - this, &TilesetEditor::onHoveredTileCleared); + connect(this->metatileLayersItem, &MetatileLayersItem::tileChanged, [this](const QPoint &pos) { paintSelectedLayerTiles(pos); }); + connect(this->metatileLayersItem, &MetatileLayersItem::paletteChanged, [this](const QPoint &pos) { paintSelectedLayerTiles(pos, true); }); + connect(this->metatileLayersItem, &MetatileLayersItem::selectedTilesChanged, this, &TilesetEditor::onMetatileLayerSelectionChanged); + connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileChanged, [this](const Tile &tile) { showTileStatus(tile); }); + connect(this->metatileLayersItem, &MetatileLayersItem::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); bool showGrid = porymapConfig.showTilesetEditorLayerGrid; this->ui->actionLayer_Grid->setChecked(showGrid); @@ -238,18 +323,16 @@ void TilesetEditor::initMetatileLayersItem() { this->metatileLayersScene = new QGraphicsScene; this->metatileLayersScene->addItem(this->metatileLayersItem); - this->ui->graphicsView_metatileLayers->setScene(this->metatileLayersScene); + this->ui->graphicsView_MetatileLayers->setScene(this->metatileLayersScene); } -void TilesetEditor::initTileSelector() -{ - this->tileSelector = new TilesetEditorTileSelector(this->primaryTileset, this->secondaryTileset, projectConfig.getNumLayersInMetatile()); - connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileChanged, - this, &TilesetEditor::onHoveredTileChanged); - connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileCleared, - this, &TilesetEditor::onHoveredTileCleared); - connect(this->tileSelector, &TilesetEditorTileSelector::selectedTilesChanged, - this, &TilesetEditor::onSelectedTilesChanged); +void TilesetEditor::initTileSelector() { + this->tileSelector = new TilesetEditorTileSelector(this->primaryTileset, this->secondaryTileset); + connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileChanged, [this](uint16_t tileId) { + showTileStatus(tileId); + }); + connect(this->tileSelector, &TilesetEditorTileSelector::hoveredTileCleared, this, &TilesetEditor::onHoveredTileCleared); + connect(this->tileSelector, &TilesetEditorTileSelector::selectedTilesChanged, this, &TilesetEditor::drawSelectedTiles); this->tileSelector->showDivider = this->ui->actionShow_Tileset_Divider->isChecked(); @@ -267,7 +350,6 @@ void TilesetEditor::initSelectedTileItem() { this->selectedTileScene = new QGraphicsScene; this->drawSelectedTiles(); this->ui->graphicsView_selectedTile->setScene(this->selectedTileScene); - this->ui->graphicsView_selectedTile->setFixedSize(this->selectedTilePixmapItem->pixmap().width() + 2, this->selectedTilePixmapItem->pixmap().height() + 2); } void TilesetEditor::initShortcuts() { @@ -330,13 +412,6 @@ void TilesetEditor::onWindowActivated() { } } -void TilesetEditor::initMetatileHistory() { - metatileHistory.clear(); - MetatileHistoryItem *commit = new MetatileHistoryItem(0, nullptr, new Metatile(), QString(), QString()); - metatileHistory.push(commit); - this->hasUnsavedChanges = false; -} - void TilesetEditor::reset() { this->setTilesets(this->primaryTileset->name, this->secondaryTileset->name); if (this->paletteEditor) @@ -374,19 +449,20 @@ void TilesetEditor::drawSelectedTiles() { return; } + const int imgTileWidth = 16; + const int imgTileHeight = 16; this->selectedTileScene->clear(); QList tiles = this->tileSelector->getSelectedTiles(); - QPoint dimensions = this->tileSelector->getSelectionDimensions(); - QImage selectionImage(16 * dimensions.x(), 16 * dimensions.y(), QImage::Format_RGBA8888); + QSize dimensions = this->tileSelector->getSelectionDimensions(); + QImage selectionImage(imgTileWidth * dimensions.width(), imgTileHeight * dimensions.height(), QImage::Format_RGBA8888); QPainter painter(&selectionImage); int tileIndex = 0; - for (int j = 0; j < dimensions.y(); j++) { - for (int i = 0; i < dimensions.x(); i++) { - auto tile = tiles.at(tileIndex); - QImage tileImage = getPalettedTileImage(tile.tileId, this->primaryTileset, this->secondaryTileset, tile.palette, true).scaled(16, 16); + for (int y = 0; y < dimensions.height(); y++) { + for (int x = 0; x < dimensions.width(); x++) { + auto tile = tiles.value(tileIndex++); + QImage tileImage = getPalettedTileImage(tile.tileId, this->primaryTileset, this->secondaryTileset, tile.palette, true).scaled(imgTileWidth, imgTileHeight); tile.flip(&tileImage); - tileIndex++; - painter.drawImage(i * 16, j * 16, tileImage); + painter.drawImage(x * imgTileWidth, y * imgTileHeight, tileImage); } } @@ -395,10 +471,15 @@ void TilesetEditor::drawSelectedTiles() { QSize size(this->selectedTilePixmapItem->pixmap().width(), this->selectedTilePixmapItem->pixmap().height()); this->ui->graphicsView_selectedTile->setSceneRect(0, 0, size.width(), size.height()); - this->ui->graphicsView_selectedTile->setFixedSize(size.width() + 2, size.height() + 2); } -void TilesetEditor::onHoveredMetatileChanged(uint16_t metatileId) { +void TilesetEditor::updateMetatileStatus() { + if (this->metatileSelector->hasCursor()) { + showMetatileStatus(this->metatileSelector->metatileIdUnderCursor()); + } +} + +void TilesetEditor::showMetatileStatus(uint16_t metatileId) { QString label = Tileset::getMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset); QString message = QString("Metatile: %1").arg(Metatile::getMetatileIdString(metatileId)); if (label.size() != 0) { @@ -413,6 +494,7 @@ void TilesetEditor::onHoveredMetatileCleared() { void TilesetEditor::onSelectedMetatileChanged(uint16_t metatileId) { this->metatile = Tileset::getMetatile(metatileId, this->primaryTileset, this->secondaryTileset); + if (!this->metatile) return; // The scripting API allows users to change metatiles in the project, and these changes are saved to disk. // The Tileset Editor (if open) needs to reflect these changes when the metatile is next displayed. @@ -423,12 +505,10 @@ void TilesetEditor::onSelectedMetatileChanged(uint16_t metatileId) { } this->metatileLayersItem->setMetatile(metatile); - this->metatileLayersItem->draw(); - this->ui->graphicsView_metatileLayers->setFixedSize(this->metatileLayersItem->pixmap().width() + 2, this->metatileLayersItem->pixmap().height() + 2); MetatileLabelPair labels = Tileset::getMetatileLabelPair(metatileId, this->primaryTileset, this->secondaryTileset); - this->ui->lineEdit_metatileLabel->setText(labels.owned); - this->ui->lineEdit_metatileLabel->setPlaceholderText(labels.shared); + this->ui->lineEdit_MetatileLabel->setText(labels.owned); + this->ui->lineEdit_MetatileLabel->setPlaceholderText(labels.shared); refreshMetatileAttributes(); } @@ -437,136 +517,126 @@ void TilesetEditor::queueMetatileReload(uint16_t metatileId) { this->metatileReloadQueue.insert(metatileId); } -void TilesetEditor::onHoveredTileChanged(uint16_t tile) { - this->ui->statusbar->showMessage(QString("Tile: %1").arg(Util::toHexString(tile, 3))); +void TilesetEditor::updateLayerTileStatus() { + if (this->metatileLayersItem->hasCursor()) { + showTileStatus(this->metatileLayersItem->tileUnderCursor()); + } +} + +void TilesetEditor::showTileStatus(const Tile &tile) { + this->ui->statusbar->showMessage(QString("Tile: %1, Palette: %2%3%4") + .arg(Util::toHexString(tile.tileId, 3)) + .arg(QString::number(tile.palette)) + .arg(tile.xflip ? ", X-flipped" : "") + .arg(tile.yflip ? ", Y-flipped" : "") + ); +} + +void TilesetEditor::showTileStatus(uint16_t tileId) { + this->ui->statusbar->showMessage(QString("Tile: %1").arg(Util::toHexString(tileId, 3))); } void TilesetEditor::onHoveredTileCleared() { this->ui->statusbar->clearMessage(); } -void TilesetEditor::onSelectedTilesChanged() { - this->drawSelectedTiles(); -} +void TilesetEditor::paintSelectedLayerTiles(const QPoint &pos, bool paletteOnly) { + if (!this->metatile) return; -void TilesetEditor::onMetatileLayerTileChanged(int x, int y) { - static const QList tileCoords = QList{ - QPoint(0, 0), - QPoint(1, 0), - QPoint(0, 1), - QPoint(1, 1), - QPoint(2, 0), - QPoint(3, 0), - QPoint(2, 1), - QPoint(3, 1), - QPoint(4, 0), - QPoint(5, 0), - QPoint(4, 1), - QPoint(5, 1), - }; + bool changed = false; Metatile *prevMetatile = new Metatile(*this->metatile); - QPoint dimensions = this->tileSelector->getSelectionDimensions(); + QSize dimensions = this->tileSelector->getSelectionDimensions(); QList tiles = this->tileSelector->getSelectedTiles(); - int selectedTileIndex = 0; + int srcTileIndex = 0; int maxTileIndex = projectConfig.getNumTilesInMetatile(); - for (int j = 0; j < dimensions.y(); j++) { - for (int i = 0; i < dimensions.x(); i++) { - int tileIndex = ((x + i) / 2 * 4) + ((y + j) * 2) + ((x + i) % 2); - if (tileIndex < maxTileIndex - && tileCoords.at(tileIndex).x() >= x - && tileCoords.at(tileIndex).y() >= y){ - Tile &tile = this->metatile->tiles[tileIndex]; - tile.tileId = tiles.at(selectedTileIndex).tileId; - tile.xflip = tiles.at(selectedTileIndex).xflip; - tile.yflip = tiles.at(selectedTileIndex).yflip; - tile.palette = tiles.at(selectedTileIndex).palette; - if (this->tileSelector->showUnused) { - this->tileSelector->usedTiles[tile.tileId] += 1; - this->tileSelector->usedTiles[prevMetatile->tiles[tileIndex].tileId] -= 1; + for (int y = 0; y < dimensions.height(); y++) { + for (int x = 0; x < dimensions.width(); x++) { + int destTileIndex = this->metatileLayersItem->posToTileIndex(pos.x() + x, pos.y() + y); + if (destTileIndex < maxTileIndex) { + Tile &destTile = this->metatile->tiles[destTileIndex]; + const Tile srcTile = tiles.value(srcTileIndex++); + if (paletteOnly) { + if (srcTile.palette == destTile.palette) + continue; // Ignore no-ops for edit history + destTile.palette = srcTile.palette; + } else { + if (srcTile == destTile) + continue; // Ignore no-ops for edit history + + // Update tile usage count + if (this->tileSelector->showUnused && destTile.tileId != srcTile.tileId) { + this->tileSelector->usedTiles[srcTile.tileId] += 1; + this->tileSelector->usedTiles[destTile.tileId] -= 1; + } + destTile = srcTile; } + changed = true; } - selectedTileIndex++; } } + if (!changed) { + delete prevMetatile; + return; + } this->metatileSelector->drawSelectedMetatile(); this->metatileLayersItem->draw(); + updateLayerTileStatus(); this->tileSelector->draw(); this->commitMetatileChange(prevMetatile); } -void TilesetEditor::onMetatileLayerSelectionChanged(QPoint selectionOrigin, int width, int height) { +void TilesetEditor::onMetatileLayerSelectionChanged(const QPoint &selectionOrigin, const QSize &size) { QList tiles; - QList tileIdxs; - int x = selectionOrigin.x(); - int y = selectionOrigin.y(); - int maxTileIndex = projectConfig.getNumTilesInMetatile(); - for (int j = 0; j < height; j++) { - for (int i = 0; i < width; i++) { - int tileIndex = ((x + i) / 2 * 4) + ((y + j) * 2) + ((x + i) % 2); - if (tileIndex < maxTileIndex) { - tiles.append(this->metatile->tiles.at(tileIndex)); - tileIdxs.append(tileIndex); - } + for (int y = 0; y < size.height(); y++) { + for (int x = 0; x < size.width(); x++) { + int tileIndex = this->metatileLayersItem->posToTileIndex(selectionOrigin.x() + x, selectionOrigin.y() + y); + tiles.append(this->metatile ? this->metatile->tiles.value(tileIndex) : Tile()); } } - this->tileSelector->setExternalSelection(width, height, tiles, tileIdxs); - if (width == 1 && height == 1) { - ui->spinBox_paletteSelector->setValue(tiles[0].palette); - this->tileSelector->highlight(static_cast(tiles[0].tileId)); + this->tileSelector->setExternalSelection(size.width(), size.height(), tiles); + if (size == QSize(1,1)) { + setPaletteId(tiles[0].palette); + this->tileSelector->highlight(tiles[0].tileId); this->redrawTileSelector(); } - this->metatileLayersItem->clearLastModifiedCoords(); } -void TilesetEditor::on_spinBox_paletteSelector_valueChanged(int paletteId) -{ - this->ui->spinBox_paletteSelector->blockSignals(true); - this->ui->spinBox_paletteSelector->setValue(paletteId); - this->ui->spinBox_paletteSelector->blockSignals(false); - this->paletteId = paletteId; - this->tileSelector->setPaletteId(paletteId); +void TilesetEditor::setPaletteId(int paletteId) { + ui->spinBox_paletteSelector->setValue(paletteId); +} + +int TilesetEditor::paletteId() const { + return ui->spinBox_paletteSelector->value(); +} + +void TilesetEditor::refreshPaletteId() { + this->tileSelector->setPaletteId(paletteId()); this->drawSelectedTiles(); if (this->paletteEditor) { - this->paletteEditor->setPaletteId(paletteId); + this->paletteEditor->setPaletteId(paletteId()); } - this->metatileLayersItem->clearLastModifiedCoords(); } -void TilesetEditor::setXFlip(bool enabled) -{ - this->tileXFlip = enabled; - this->tileSelector->setTileFlips(this->tileXFlip, this->tileYFlip); +void TilesetEditor::refreshTileFlips() { + this->tileSelector->setTileFlips(ui->checkBox_xFlip->isChecked(), ui->checkBox_yFlip->isChecked()); this->drawSelectedTiles(); - this->metatileLayersItem->clearLastModifiedCoords(); -} - -void TilesetEditor::setYFlip(bool enabled) -{ - this->tileYFlip = enabled; - this->tileSelector->setTileFlips(this->tileXFlip, this->tileYFlip); - this->drawSelectedTiles(); - this->metatileLayersItem->clearLastModifiedCoords(); } void TilesetEditor::setMetatileLabel(QString label) { - this->ui->lineEdit_metatileLabel->setText(label); + this->ui->lineEdit_MetatileLabel->setText(label); commitMetatileLabel(); } -void TilesetEditor::on_lineEdit_metatileLabel_editingFinished() -{ - commitMetatileLabel(); -} +void TilesetEditor::commitMetatileLabel() { + if (!this->metatile) return; -void TilesetEditor::commitMetatileLabel() -{ // Only commit if the field has changed. uint16_t metatileId = this->getSelectedMetatileId(); QString oldLabel = Tileset::getOwnedMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset); - QString newLabel = this->ui->lineEdit_metatileLabel->text(); + QString newLabel = this->ui->lineEdit_MetatileLabel->text(); if (oldLabel != newLabel) { Metatile *prevMetatile = new Metatile(*this->metatile); Tileset::setMetatileLabel(metatileId, newLabel, this->primaryTileset, this->secondaryTileset); @@ -574,17 +644,17 @@ void TilesetEditor::commitMetatileLabel() } } -void TilesetEditor::commitMetatileAndLabelChange(Metatile * prevMetatile, QString prevLabel) -{ - metatileHistory.push(new MetatileHistoryItem(this->getSelectedMetatileId(), - prevMetatile, new Metatile(*this->metatile), - prevLabel, this->ui->lineEdit_metatileLabel->text())); - this->hasUnsavedChanges = true; +void TilesetEditor::commitMetatileAndLabelChange(Metatile * prevMetatile, QString prevLabel) { + if (!this->metatile) return; + + commit(new MetatileHistoryItem(this->getSelectedMetatileId(), + prevMetatile, new Metatile(*this->metatile), + prevLabel, this->ui->lineEdit_MetatileLabel->text())); } void TilesetEditor::commitMetatileChange(Metatile * prevMetatile) { - this->commitMetatileAndLabelChange(prevMetatile, this->ui->lineEdit_metatileLabel->text()); + this->commitMetatileAndLabelChange(prevMetatile, this->ui->lineEdit_MetatileLabel->text()); } uint32_t TilesetEditor::attributeNameToValue(Metatile::Attr attribute, const QString &text, bool *ok) { @@ -603,15 +673,14 @@ uint32_t TilesetEditor::attributeNameToValue(Metatile::Attr attribute, const QSt } } else if (attribute == Metatile::Attr::LayerType) { // The layer type text is not editable, it uses special display names. Just get the index of the display name. - int i = ui->comboBox_layerType->findText(text); + int i = ui->comboBox_LayerType->findText(text); if (i >= 0) return i; } return text.toUInt(ok, 0); } void TilesetEditor::commitAttributeFromComboBox(Metatile::Attr attribute, NoScrollComboBox *combo) { - if (!this->metatile) - return; + if (!this->metatile) return; bool ok; uint32_t newValue = this->attributeNameToValue(attribute, combo->currentText(), &ok); @@ -621,8 +690,8 @@ void TilesetEditor::commitAttributeFromComboBox(Metatile::Attr attribute, NoScro this->commitMetatileChange(prevMetatile); // When an attribute changes we also need to update the raw value display. - const QSignalBlocker b_RawAttributesValue(ui->spinBox_rawAttributesValue); - ui->spinBox_rawAttributesValue->setValue(this->metatile->getAttributes()); + const QSignalBlocker b_RawAttributesValue(ui->spinBox_RawAttributesValue); + ui->spinBox_RawAttributesValue->setValue(this->metatile->getAttributes()); } // Update the text in the combo box to reflect the final value. @@ -632,7 +701,9 @@ void TilesetEditor::commitAttributeFromComboBox(Metatile::Attr attribute, NoScro } void TilesetEditor::onRawAttributesEdited() { - uint32_t newAttributes = ui->spinBox_rawAttributesValue->value(); + if (!this->metatile) return; + + uint32_t newAttributes = ui->spinBox_RawAttributesValue->value(); if (newAttributes != this->metatile->getAttributes()) { Metatile *prevMetatile = new Metatile(*this->metatile); this->metatile->setAttributes(newAttributes); @@ -644,34 +715,34 @@ void TilesetEditor::onRawAttributesEdited() { void TilesetEditor::refreshMetatileAttributes() { if (!this->metatile) return; - const QSignalBlocker b_MetatileBehaviors(ui->comboBox_metatileBehaviors); - const QSignalBlocker b_EncounterType(ui->comboBox_encounterType); - const QSignalBlocker b_TerrainType(ui->comboBox_terrainType); - const QSignalBlocker b_LayerType(ui->comboBox_layerType); - const QSignalBlocker b_RawAttributesValue(ui->spinBox_rawAttributesValue); - ui->comboBox_metatileBehaviors->setHexItem(this->metatile->behavior()); - ui->comboBox_encounterType->setHexItem(this->metatile->encounterType()); - ui->comboBox_terrainType->setHexItem(this->metatile->terrainType()); - ui->comboBox_layerType->setHexItem(this->metatile->layerType()); - ui->spinBox_rawAttributesValue->setValue(this->metatile->getAttributes()); + const QSignalBlocker b_MetatileBehaviors(ui->comboBox_MetatileBehaviors); + const QSignalBlocker b_EncounterType(ui->comboBox_EncounterType); + const QSignalBlocker b_TerrainType(ui->comboBox_TerrainType); + const QSignalBlocker b_LayerType(ui->comboBox_LayerType); + const QSignalBlocker b_RawAttributesValue(ui->spinBox_RawAttributesValue); + ui->comboBox_MetatileBehaviors->setHexItem(this->metatile->behavior()); + ui->comboBox_EncounterType->setHexItem(this->metatile->encounterType()); + ui->comboBox_TerrainType->setHexItem(this->metatile->terrainType()); + ui->comboBox_LayerType->setHexItem(this->metatile->layerType()); + ui->spinBox_RawAttributesValue->setValue(this->metatile->getAttributes()); this->metatileSelector->drawSelectedMetatile(); } void TilesetEditor::commitMetatileBehavior() { - commitAttributeFromComboBox(Metatile::Attr::Behavior, ui->comboBox_metatileBehaviors); + commitAttributeFromComboBox(Metatile::Attr::Behavior, ui->comboBox_MetatileBehaviors); } void TilesetEditor::commitEncounterType() { - commitAttributeFromComboBox(Metatile::Attr::EncounterType, ui->comboBox_encounterType); + commitAttributeFromComboBox(Metatile::Attr::EncounterType, ui->comboBox_EncounterType); } void TilesetEditor::commitTerrainType() { - commitAttributeFromComboBox(Metatile::Attr::TerrainType, ui->comboBox_terrainType); + commitAttributeFromComboBox(Metatile::Attr::TerrainType, ui->comboBox_TerrainType); }; void TilesetEditor::commitLayerType() { - commitAttributeFromComboBox(Metatile::Attr::LayerType, ui->comboBox_layerType); + commitAttributeFromComboBox(Metatile::Attr::LayerType, ui->comboBox_LayerType); this->metatileSelector->drawSelectedMetatile(); // Changing the layer type can affect how fully transparent metatiles appear } @@ -681,6 +752,7 @@ bool TilesetEditor::save() { this->lockSelection = true; bool success = this->project->saveTilesets(this->primaryTileset, this->secondaryTileset); + applyMetatileSwapsToLayouts(); emit this->tilesetsSaved(this->primaryTileset->name, this->secondaryTileset->name); if (this->paletteEditor) { this->paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); @@ -694,17 +766,8 @@ bool TilesetEditor::save() { return success; } -void TilesetEditor::on_actionImport_Primary_Tiles_triggered() -{ - this->importTilesetTiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Tiles_triggered() -{ - this->importTilesetTiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { +void TilesetEditor::importTilesetTiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptor = primary ? "primary" : "secondary"; QString descriptorCaps = primary ? "Primary" : "Secondary"; @@ -725,20 +788,22 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { } else { logError(QString("Failed to open image file: '%1'").arg(filepath)); } - if (image.width() == 0 || image.height() == 0 || image.width() % 8 != 0 || image.height() % 8 != 0) { + if (image.width() == 0 || image.height() == 0 || image.width() % Tile::pixelWidth() != 0 || image.height() % Tile::pixelHeight() != 0) { ErrorMessage::show(QStringLiteral("Failed to import tiles."), - QString("The image dimensions (%1 x %2) are invalid. Width and height must be multiples of 8 pixels.") + QString("The image dimensions (%1x%2) are invalid. The dimensions must be a multiple of %3x%4 pixels.") .arg(image.width()) - .arg(image.height()), + .arg(image.height()) + .arg(Tile::pixelWidth()) + .arg(Tile::pixelHeight()), this); return; } // Validate total number of tiles in image. - int numTilesWide = image.width() / 8; - int numTilesHigh = image.height() / 8; + int numTilesWide = image.width() / Tile::pixelWidth(); + int numTilesHigh = image.height() / Tile::pixelHeight(); int totalTiles = numTilesHigh * numTilesWide; - int maxAllowedTiles = primary ? Project::getNumTilesPrimary() : Project::getNumTilesTotal() - Project::getNumTilesPrimary(); + int maxAllowedTiles = primary ? Project::getNumTilesPrimary() : Project::getNumTilesSecondary(); if (totalTiles > maxAllowedTiles) { ErrorMessage::show(QStringLiteral("Failed to import tiles."), QString("The maximum number of tiles allowed in the %1 tileset is %2, but the provided image contains %3 total tiles.") @@ -774,13 +839,21 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { image = image.convertToFormat(QImage::Format::Format_Indexed8, colorTable); } - tileset->loadTilesImage(&image); + if (!tileset->loadTilesImage(&image)) { + RecentErrorMessage::show(QStringLiteral("Failed to import tiles."), this); + return; + } this->refresh(); this->hasUnsavedChanges = true; } void TilesetEditor::closeEvent(QCloseEvent *event) { + // If focus is still on any input widgets, a user may have made changes + // but the widget hasn't had a chance to fire the 'editingFinished' signal. + // Make sure they lose focus before we close so that changes aren't missed. + setFocus(); + if (this->hasUnsavedChanges) { auto result = SaveChangesMessage::show(QStringLiteral("Tileset"), this); if (result == QMessageBox::Yes) { @@ -822,7 +895,7 @@ void TilesetEditor::on_actionChange_Metatiles_Count_triggered() primarySpinBox->setMinimum(1); secondarySpinBox->setMinimum(1); primarySpinBox->setMaximum(Project::getNumMetatilesPrimary()); - secondarySpinBox->setMaximum(Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()); + secondarySpinBox->setMaximum(Project::getNumMetatilesSecondary()); primarySpinBox->setValue(this->primaryTileset->numMetatiles()); secondarySpinBox->setValue(this->secondaryTileset->numMetatiles()); form.addRow(new QLabel("Primary Tileset"), primarySpinBox); @@ -836,8 +909,15 @@ void TilesetEditor::on_actionChange_Metatiles_Count_triggered() if (dialog.exec() == QDialog::Accepted) { this->primaryTileset->resizeMetatiles(primarySpinBox->value()); this->secondaryTileset->resizeMetatiles(secondarySpinBox->value()); - this->metatileSelector->updateSelectedMetatile(); - this->refresh(); + + // Our selected metatile ID may have become invalid. Make sure it's in-bounds. + uint16_t metatileId = this->metatileSelector->getSelectedMetatileId(); + Tileset *tileset = Tileset::getMetatileTileset(metatileId, this->primaryTileset, this->secondaryTileset); + if (tileset && !tileset->containsMetatileId(metatileId)) { + this->metatileSelector->select(qBound(tileset->firstMetatileId(), metatileId, tileset->lastMetatileId())); + } + + refresh(); this->hasUnsavedChanges = true; } } @@ -846,21 +926,12 @@ void TilesetEditor::on_actionChange_Palettes_triggered() { if (!this->paletteEditor) { this->paletteEditor = new PaletteEditor(this->project, this->primaryTileset, - this->secondaryTileset, this->paletteId, this); - connect(this->paletteEditor, &PaletteEditor::changedPaletteColor, - this, &TilesetEditor::onPaletteEditorChangedPaletteColor); - connect(this->paletteEditor, &PaletteEditor::changedPalette, - this, &TilesetEditor::onPaletteEditorChangedPalette); - } - - if (!this->paletteEditor->isVisible()) { - this->paletteEditor->show(); - } else if (this->paletteEditor->isMinimized()) { - this->paletteEditor->showNormal(); - } else { - this->paletteEditor->raise(); - this->paletteEditor->activateWindow(); + this->secondaryTileset, this->paletteId(), this); + connect(this->paletteEditor, &PaletteEditor::changedPaletteColor, this, &TilesetEditor::onPaletteEditorChangedPaletteColor); + connect(this->paletteEditor, &PaletteEditor::changedPalette, this, &TilesetEditor::setPaletteId); + connect(this->paletteEditor, &PaletteEditor::metatileSelected, this, &TilesetEditor::selectMetatile); } + Util::show(this->paletteEditor); } void TilesetEditor::onPaletteEditorChangedPaletteColor() { @@ -868,27 +939,22 @@ void TilesetEditor::onPaletteEditorChangedPaletteColor() { this->hasUnsavedChanges = true; } -void TilesetEditor::onPaletteEditorChangedPalette(int paletteId) { - this->on_spinBox_paletteSelector_valueChanged(paletteId); -} - -bool TilesetEditor::replaceMetatile(uint16_t metatileId, const Metatile * src, QString newLabel) -{ +bool TilesetEditor::replaceMetatile(uint16_t metatileId, const Metatile &src, QString newLabel) { Metatile * dest = Tileset::getMetatile(metatileId, this->primaryTileset, this->secondaryTileset); QString oldLabel = Tileset::getOwnedMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset); - if (!dest || !src || (*dest == *src && oldLabel == newLabel)) + if (!dest || (*dest == src && oldLabel == newLabel)) return false; Tileset::setMetatileLabel(metatileId, newLabel, this->primaryTileset, this->secondaryTileset); if (metatileId == this->getSelectedMetatileId()) - this->ui->lineEdit_metatileLabel->setText(newLabel); + this->ui->lineEdit_MetatileLabel->setText(newLabel); // Update tile usage if any tiles changed if (this->tileSelector && this->tileSelector->showUnused) { int numTiles = projectConfig.getNumTilesInMetatile(); for (int i = 0; i < numTiles; i++) { - if (src->tiles[i].tileId != dest->tiles[i].tileId) { - this->tileSelector->usedTiles[src->tiles[i].tileId] += 1; + if (src.tiles[i].tileId != dest->tiles[i].tileId) { + this->tileSelector->usedTiles[src.tiles[i].tileId] += 1; this->tileSelector->usedTiles[dest->tiles[i].tileId] -= 1; } } @@ -896,38 +962,61 @@ bool TilesetEditor::replaceMetatile(uint16_t metatileId, const Metatile * src, Q } this->metatile = dest; - *this->metatile = *src; + *this->metatile = src; this->metatileSelector->select(metatileId); this->metatileSelector->drawMetatile(metatileId); + updateMetatileStatus(); this->metatileLayersItem->draw(); - this->metatileLayersItem->clearLastModifiedCoords(); - this->metatileLayersItem->clearLastHoveredCoords(); + updateLayerTileStatus(); return true; } -void TilesetEditor::on_actionUndo_triggered() -{ - MetatileHistoryItem *commit = this->metatileHistory.current(); - if (!commit) return; - Metatile *prev = commit->prevMetatile; - if (!prev) return; - this->metatileHistory.back(); - this->replaceMetatile(commit->metatileId, prev, commit->prevLabel); +void TilesetEditor::initMetatileHistory() { + this->metatileHistory.clear(); + updateEditHistoryActions(); + this->hasUnsavedChanges = false; } -void TilesetEditor::on_actionRedo_triggered() -{ +void TilesetEditor::commit(MetatileHistoryItem *item) { + this->metatileHistory.push(item); + updateEditHistoryActions(); + this->hasUnsavedChanges = true; +} + +void TilesetEditor::updateEditHistoryActions() { + ui->actionUndo->setEnabled(this->metatileHistory.canUndo()); + ui->actionRedo->setEnabled(this->metatileHistory.canRedo()); +} + +void TilesetEditor::on_actionUndo_triggered() { + MetatileHistoryItem *commit = this->metatileHistory.current(); + if (!commit) return; + this->metatileHistory.back(); + + if (commit->isSwap) { + swapMetatiles(commit->swapMetatileId, commit->metatileId); + } else if (commit->prevMetatile) { + replaceMetatile(commit->metatileId, *commit->prevMetatile, commit->prevLabel); + }; + updateEditHistoryActions(); +} + +void TilesetEditor::on_actionRedo_triggered() { MetatileHistoryItem *commit = this->metatileHistory.next(); if (!commit) return; - this->replaceMetatile(commit->metatileId, commit->newMetatile, commit->newLabel); + + if (commit->isSwap) { + swapMetatiles(commit->metatileId, commit->swapMetatileId); + } else if (commit->newMetatile) { + replaceMetatile(commit->metatileId, *commit->newMetatile, commit->newLabel); + } + updateEditHistoryActions(); } void TilesetEditor::on_actionCut_triggered() { - Metatile * empty = new Metatile(projectConfig.getNumTilesInMetatile()); this->copyMetatile(true); - this->pasteMetatile(empty, ""); - delete empty; + this->pasteMetatile(Metatile(projectConfig.getNumTilesInMetatile()), ""); } void TilesetEditor::on_actionCopy_triggered() @@ -937,7 +1026,9 @@ void TilesetEditor::on_actionCopy_triggered() void TilesetEditor::on_actionPaste_triggered() { - this->pasteMetatile(this->copiedMetatile, this->copiedMetatileLabel); + if (this->copiedMetatile) { + this->pasteMetatile(*this->copiedMetatile, this->copiedMetatileLabel); + } } void TilesetEditor::copyMetatile(bool cut) { @@ -950,14 +1041,17 @@ void TilesetEditor::copyMetatile(bool cut) { else *this->copiedMetatile = *toCopy; + ui->actionPaste->setEnabled(true); + // Don't try to copy the label unless it's a cut, these should be unique to each metatile. this->copiedMetatileLabel = cut ? Tileset::getOwnedMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset) : QString(); } -void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel) -{ +void TilesetEditor::pasteMetatile(const Metatile &toPaste, QString newLabel) { + if (!this->metatile) return; + Metatile *prevMetatile = new Metatile(*this->metatile); - QString prevLabel = this->ui->lineEdit_metatileLabel->text(); + QString prevLabel = this->ui->lineEdit_MetatileLabel->text(); if (newLabel.isNull()) newLabel = prevLabel; // Don't change the label if one wasn't copied uint16_t metatileId = this->getSelectedMetatileId(); if (!this->replaceMetatile(metatileId, toPaste, newLabel)) { @@ -968,62 +1062,70 @@ void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel) this->commitMetatileAndLabelChange(prevMetatile, prevLabel); } -void TilesetEditor::on_actionExport_Primary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->primaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Tiles Image", defaultFilepath, "Image Files (*.png)"); +void TilesetEditor::exportTilesImage(Tileset *tileset) { + bool primary = !tileset->is_secondary; + QString defaultFilepath = QString("%1/%2_Tiles_Pal%3.png").arg(FileDialog::getDirectory()).arg(tileset->name).arg(this->paletteId()); + QString filepath = FileDialog::getSaveFileName(this, QString("Export %1 Tiles Image").arg(primary ? "Primary" : "Secondary"), defaultFilepath, "Image Files (*.png)"); if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildPrimaryTilesIndexedImage(); + QImage image = primary ? this->tileSelector->buildPrimaryTilesIndexedImage() : this->tileSelector->buildSecondaryTilesIndexedImage(); exportIndexed4BPPPng(image, filepath); } } -void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered() -{ - QString defaultName = QString("%1_Tiles_Pal%2").arg(this->secondaryTileset->name).arg(this->paletteId); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Tiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->tileSelector->buildSecondaryTilesIndexedImage(); - exportIndexed4BPPPng(image, filepath); +// There are many more options for exporting metatile images than tile images, so we open a separate dialog to ask the user for settings. +void TilesetEditor::exportMetatilesImage() { + if (!this->metatileImageExportSettings) { + this->metatileImageExportSettings = new MetatileImageExporter::Settings; + } + auto dialog = new MetatileImageExporter(this, this->primaryTileset, this->secondaryTileset, this->metatileImageExportSettings); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->open(); +} + +void TilesetEditor::exportPorytilesLayerImages(Tileset *tileset) { + QString dir = FileDialog::getExistingDirectory(this, QStringLiteral("Choose Folder to Export Images")); + if (dir.isEmpty()) { + return; + } + + MetatileImageExporter layerExporter(this, this->primaryTileset, this->secondaryTileset); + MetatileImageExporter::Settings settings = {}; + settings.usePrimaryTileset = !tileset->is_secondary; + settings.useSecondaryTileset = tileset->is_secondary; + + QMap images; + QStringList pathCollisions; + for (int i = 0; i < 3; i++) { + settings.layerOrder.clear(); + settings.layerOrder[i] = true; + layerExporter.applySettings(settings); + + QString filename = layerExporter.getDefaultFileName(); + QString path = QString("%1/%2").arg(dir).arg(filename); + if (QFileInfo::exists(path)) { + pathCollisions.append(filename); + } + images[path] = layerExporter.getImage(); + } + + if (!pathCollisions.isEmpty()) { + QString message = QString("The following files will be overwritten, are you sure you want to export?\n\n%1").arg(pathCollisions.join("\n")); + auto reply = QuestionMessage::show(message, this); + if (reply != QMessageBox::Yes) { + return; + } + } + + for (auto it = images.constBegin(); it != images.constEnd(); it++) { + QString path = it.key(); + if (!it.value().save(path)) { + logError(QString("Failed to save Porytiles layer image '%1'.").arg(path)); + } } } -void TilesetEditor::on_actionExport_Primary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->primaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Primary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildPrimaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionExport_Secondary_Metatiles_Image_triggered() -{ - QString defaultName = QString("%1_Metatiles").arg(this->secondaryTileset->name); - QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName); - QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Metatiles Image", defaultFilepath, "Image Files (*.png)"); - if (!filepath.isEmpty()) { - QImage image = this->metatileSelector->buildSecondaryMetatilesImage(); - image.save(filepath, "PNG"); - } -} - -void TilesetEditor::on_actionImport_Primary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->primaryTileset, true); -} - -void TilesetEditor::on_actionImport_Secondary_Metatiles_triggered() -{ - this->importTilesetMetatiles(this->secondaryTileset, false); -} - -void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) -{ +void TilesetEditor::importAdvanceMapMetatiles(Tileset *tileset) { + bool primary = !tileset->is_secondary; QString descriptorCaps = primary ? "Primary" : "Secondary"; QString filepath = FileDialog::getOpenFileName(this, QString("Import %1 Tileset Metatiles from Advance Map 1.92").arg(descriptorCaps), "", "Advance Map 1.92 Metatile Files (*.bvd)"); @@ -1041,7 +1143,7 @@ void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) // TODO: This is crude because it makes a history entry for every newly-imported metatile. // Revisit this when tiles and num metatiles are added to tileset editory history. - int metatileIdBase = primary ? 0 : Project::getNumMetatilesPrimary(); + uint16_t metatileIdBase = tileset->firstMetatileId(); for (int i = 0; i < metatiles.length(); i++) { if (i >= tileset->numMetatiles()) { break; @@ -1050,15 +1152,13 @@ void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) uint16_t metatileId = static_cast(metatileIdBase + i); QString prevLabel = Tileset::getOwnedMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset); Metatile *prevMetatile = new Metatile(*tileset->metatileAt(i)); - MetatileHistoryItem *commit = new MetatileHistoryItem(metatileId, - prevMetatile, new Metatile(*metatiles.at(i)), - prevLabel, prevLabel); - metatileHistory.push(commit); + commit(new MetatileHistoryItem(metatileId, + prevMetatile, new Metatile(*metatiles.at(i)), + prevLabel, prevLabel)); } tileset->setMetatiles(metatiles); this->refresh(); - this->hasUnsavedChanges = true; } void TilesetEditor::on_actionShow_Unused_toggled(bool checked) { @@ -1143,63 +1243,41 @@ void TilesetEditor::countMetatileUsage() { } void TilesetEditor::countTileUsage() { - // check primary tiles this->tileSelector->usedTiles.resize(Project::getNumTilesTotal()); this->tileSelector->usedTiles.fill(0); - QSet primaryTilesets; - QSet secondaryTilesets; + auto countTilesetTileUsage = [this](Tileset *searchTileset) { + // Count usage of our search tileset's tiles (in itself, and in any tilesets it gets paired with). + QSet tilesetNames = this->project->getPairedTilesetLabels(searchTileset); + QSet tilesets; - for (const auto &layoutId : this->project->layoutIds()) { - Layout *layout = this->project->getLayout(layoutId); - if (layout->tileset_primary_label == this->primaryTileset->name - || layout->tileset_secondary_label == this->secondaryTileset->name) { - // need to check metatiles - this->project->loadLayoutTilesets(layout); - if (layout->tileset_primary && layout->tileset_secondary) { - primaryTilesets.insert(layout->tileset_primary); - secondaryTilesets.insert(layout->tileset_secondary); + // For the currently-loaded tilesets, make sure we use the Tileset Editor's versions + // (which may contain unsaved changes) and not the versions from the project. + tilesetNames.remove(this->primaryTileset->name); + tilesetNames.remove(this->secondaryTileset->name); + tilesets.insert(this->primaryTileset); + tilesets.insert(this->secondaryTileset); + + for (const auto &tilesetName : tilesetNames) { + Tileset *tileset = this->project->getTileset(tilesetName); + if (tileset) tilesets.insert(tileset); + } + for (const auto &tileset : tilesets) { + for (const auto &metatile : tileset->metatiles()) { + for (const auto &tile : metatile->tiles) { + if (searchTileset->containsTileId(tile.tileId)) { + this->tileSelector->usedTiles[tile.tileId]++; + } + } } } - } + }; - // check primary tilesets that are used with this secondary tileset for - // reference to secondary tiles in primary metatiles - for (const auto &tileset : primaryTilesets) { - for (const auto &metatile : tileset->metatiles()) { - for (const auto &tile : metatile->tiles) { - if (tile.tileId >= Project::getNumTilesPrimary()) - this->tileSelector->usedTiles[tile.tileId]++; - } - } - } - - // do the opposite for primary tiles in secondary metatiles - for (Tileset *tileset : secondaryTilesets) { - for (const auto &metatile : tileset->metatiles()) { - for (const auto &tile : metatile->tiles) { - if (tile.tileId < Project::getNumTilesPrimary()) - this->tileSelector->usedTiles[tile.tileId]++; - } - } - } - - // check this primary tileset metatiles - for (const auto &metatile : this->primaryTileset->metatiles()) { - for (const auto &tile : metatile->tiles) { - this->tileSelector->usedTiles[tile.tileId]++; - } - } - - // and the secondary metatiles - for (const auto &metatile : this->secondaryTileset->metatiles()) { - for (const auto &tile : metatile->tiles) { - this->tileSelector->usedTiles[tile.tileId]++; - } - } + countTilesetTileUsage(this->primaryTileset); + countTilesetTileUsage(this->secondaryTileset); } -void TilesetEditor::on_copyButton_metatileLabel_clicked() { +void TilesetEditor::on_copyButton_MetatileLabel_clicked() { uint16_t metatileId = this->getSelectedMetatileId(); QString label = Tileset::getMetatileLabel(metatileId, this->primaryTileset, this->secondaryTileset); if (label.isEmpty()) return; @@ -1207,7 +1285,7 @@ void TilesetEditor::on_copyButton_metatileLabel_clicked() { if (tileset) label.prepend(tileset->getMetatileLabelPrefix()); QGuiApplication::clipboard()->setText(label); - QToolTip::showText(this->ui->copyButton_metatileLabel->mapToGlobal(QPoint(0, 0)), "Copied!"); + QToolTip::showText(this->ui->copyButton_MetatileLabel->mapToGlobal(QPoint(0, 0)), "Copied!"); } void TilesetEditor::on_horizontalSlider_MetatilesZoom_valueChanged(int value) { @@ -1262,3 +1340,126 @@ void TilesetEditor::redrawTileSelector() { this->ui->scrollArea_Tiles->ensureVisible(pos.x(), pos.y(), viewport->width() / 2, viewport->height() / 2); } } + +void TilesetEditor::commitMetatileSwap(uint16_t metatileIdA, uint16_t metatileIdB) { + if (swapMetatiles(metatileIdA, metatileIdB)) { + commit(new MetatileHistoryItem(metatileIdA, metatileIdB)); + } +} + +bool TilesetEditor::swapMetatiles(uint16_t metatileIdA, uint16_t metatileIdB) { + this->metatileSelector->clearSwapSelection(); + + QList metatiles; + for (const auto &metatileId : {metatileIdA, metatileIdB}) { + Metatile *metatile = Tileset::getMetatile(metatileId, this->primaryTileset, this->secondaryTileset); + if (metatile) { + metatiles.append(metatile); + } else { + logError(QString("Failed to load metatile %1 for swap.").arg(Metatile::getMetatileIdString(metatileId))); + } + } + if (metatiles.length() < 2) + return false; + + // Swap the metatile data in the tileset + Metatile tempMetatile = *metatiles.at(0); + QString tempLabel = Tileset::getOwnedMetatileLabel(metatileIdA, this->primaryTileset, this->secondaryTileset); + replaceMetatile(metatileIdA, *metatiles.at(1), Tileset::getOwnedMetatileLabel(metatileIdB, this->primaryTileset, this->secondaryTileset)); + replaceMetatile(metatileIdB, tempMetatile, tempLabel); + + // Record this swap so that we can update the layouts later. + // If this is the inverse of the most recent swap (e.g. from Undo), we instead remove that swap to save time. + if (!this->metatileIdSwaps.isEmpty()) { + auto recentSwapPair = this->metatileIdSwaps.constLast(); + if (recentSwapPair.first == metatileIdB && recentSwapPair.second == metatileIdA) { + this->metatileIdSwaps.removeLast(); + return true; + } + } + this->metatileIdSwaps.append(QPair(metatileIdA, metatileIdB)); + return true; +} + +// If any metatiles swapped positions, apply the swap to all relevant layouts. +// We only do this once changes in the Tileset Editor are saved. +void TilesetEditor::applyMetatileSwapsToLayouts() { + if (this->metatileIdSwaps.isEmpty()) + return; + + QProgressDialog progress("", "", 0, this->metatileIdSwaps.length(), this); + progress.setAutoClose(true); + progress.setWindowModality(Qt::WindowModal); + progress.setModal(true); + progress.setMinimumDuration(1000); + progress.setValue(progress.minimum()); + + for (const auto &swapPair : this->metatileIdSwaps) { + progress.setLabelText(QString("Swapping metatiles %1 and %2 in map layouts...") + .arg(Metatile::getMetatileIdString(swapPair.first)) + .arg(Metatile::getMetatileIdString(swapPair.second))); + applyMetatileSwapToLayouts(swapPair.first, swapPair.second); + progress.setValue(progress.value() + 1); + } + this->metatileIdSwaps.clear(); +} + +void TilesetEditor::applyMetatileSwapToLayouts(uint16_t metatileIdA, uint16_t metatileIdB) { + struct TilesetPair { + Tileset* primary = nullptr; + Tileset* secondary = nullptr; + }; + TilesetPair tilesets; + + // Get which tilesets our swapped metatiles belong to. + auto addSourceTileset = [this](uint16_t metatileId, TilesetPair *tilesets) { + if (this->primaryTileset->containsMetatileId(metatileId)) { + tilesets->primary = this->primaryTileset; + } else if (this->secondaryTileset->containsMetatileId(metatileId)) { + tilesets->secondary = this->secondaryTileset; + } else { + // Invalid metatile, shouldn't happen + this->metatileSelector->removeFromSwapSelection(metatileId); + } + }; + addSourceTileset(metatileIdA, &tilesets); + addSourceTileset(metatileIdB, &tilesets); + if (!tilesets.primary && !tilesets.secondary) { + return; + } + + // In each layout that uses the appropriate tileset(s), swap the two metatiles. + QSet layoutIds = this->project->getTilesetLayoutIds(tilesets.primary, tilesets.secondary); + for (const auto &layoutId : layoutIds) { + Layout *layout = this->project->loadLayout(layoutId); + if (!layout) continue; + // Perform swap(s) in layout's main data. + for (int y = 0; y < layout->height; y++) + for (int x = 0; x < layout->width; x++) { + uint16_t metatileId = layout->getMetatileId(x, y); + if (metatileId == metatileIdA) { + layout->setMetatileId(x, y, metatileIdB); + } else if (metatileId == metatileIdB) { + layout->setMetatileId(x, y, metatileIdA); + } else continue; + layout->hasUnsavedDataChanges = true; + } + // Perform swap(s) in layout's border data. + for (auto &borderBlock : layout->border) { + if (borderBlock.metatileId() == metatileIdA) { + borderBlock.setMetatileId(metatileIdB); + } else if (borderBlock.metatileId() == metatileIdB) { + borderBlock.setMetatileId(metatileIdA); + } else continue; + layout->hasUnsavedDataChanges = true; + } + } +} + +void TilesetEditor::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Escape && ui->actionSwap_Metatiles->isChecked()) { + ui->actionSwap_Metatiles->setChecked(false); + } else { + QMainWindow::keyPressEvent(event); + } +} diff --git a/src/ui/tileseteditormetatileselector.cpp b/src/ui/tileseteditormetatileselector.cpp index 4e09cf34..1329a49b 100644 --- a/src/ui/tileseteditormetatileselector.cpp +++ b/src/ui/tileseteditormetatileselector.cpp @@ -3,17 +3,20 @@ #include "project.h" #include -TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout) - : SelectablePixmapItem(32, 32, 1, 1) { +// TODO: This class has a decent bit of overlap with the MetatileSelector class. +// They should be refactored to inherit from a single parent class. + +TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(int numMetatilesWide, Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout) + : SelectablePixmapItem(32, 32, 1, 1), + numMetatilesWide(qMax(numMetatilesWide, 1)) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; - this->numMetatilesWide = 8; this->layout = layout; setAcceptHoverEvents(true); this->usedMetatiles.resize(Project::getNumMetatilesTotal()); } -int TilesetEditorMetatileSelector::numRows(int numMetatiles) { +int TilesetEditorMetatileSelector::numRows(int numMetatiles) const { int numMetatilesHigh = numMetatiles / this->numMetatilesWide; if (numMetatiles % this->numMetatilesWide != 0) { // Round up height for incomplete last row @@ -22,67 +25,29 @@ int TilesetEditorMetatileSelector::numRows(int numMetatiles) { return numMetatilesHigh; } -int TilesetEditorMetatileSelector::numRows() { +int TilesetEditorMetatileSelector::numRows() const { return this->numRows(this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); } int TilesetEditorMetatileSelector::numPrimaryMetatilesRounded() const { - // We round up the number of primary metatiles to keep the tilesets on separate rows. - return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide; -} - -QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() { - return this->buildImage(0, this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildPrimaryMetatilesImage() { - return this->buildImage(0, this->primaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() { - return this->buildImage(Project::getNumMetatilesPrimary(), this->secondaryTileset->numMetatiles()); -} - -QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) { - int numMetatilesHigh = this->numRows(numMetatiles); - int numPrimary = this->numPrimaryMetatilesRounded(); - int maxPrimary = Project::getNumMetatilesPrimary(); - bool includesPrimary = metatileIdStart < maxPrimary; - - QImage image(this->numMetatilesWide * this->cellWidth, numMetatilesHigh * this->cellHeight, QImage::Format_RGBA8888); - image.fill(Qt::magenta); - QPainter painter(&image); - for (int i = 0; i < numMetatiles; i++) { - int metatileId = i + metatileIdStart; - if (includesPrimary && metatileId >= numPrimary) - metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset - QImage metatile_image = getMetatileImage( - metatileId, - this->primaryTileset, - this->secondaryTileset, - this->layout->metatileLayerOrder, - this->layout->metatileLayerOpacity, - true) - .scaled(this->cellWidth, this->cellHeight); - int map_y = i / this->numMetatilesWide; - int map_x = i % this->numMetatilesWide; - QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight); - painter.drawImage(metatile_origin, metatile_image); - } - painter.end(); - return image; + if (!this->primaryTileset) + return 0; + return Util::roundUpToMultiple(this->primaryTileset->numMetatiles(), this->numMetatilesWide); } void TilesetEditorMetatileSelector::drawMetatile(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) + return; QPainter painter(&this->baseImage); QImage metatile_image = getMetatileImage( metatileId, this->primaryTileset, this->secondaryTileset, - this->layout->metatileLayerOrder, - this->layout->metatileLayerOpacity, + this->layout->metatileLayerOrder(), + this->layout->metatileLayerOpacity(), true) .scaled(this->cellWidth, this->cellHeight); painter.drawImage(QPoint(pos.x() * this->cellWidth, pos.y() * this->cellHeight), metatile_image); @@ -97,7 +62,13 @@ void TilesetEditorMetatileSelector::drawSelectedMetatile() { } void TilesetEditorMetatileSelector::updateBasePixmap() { - this->baseImage = buildAllMetatilesImage(); + this->baseImage = getMetatileSheetImage(this->primaryTileset, + this->secondaryTileset, + this->numMetatilesWide, + this->layout->metatileLayerOrder(), + this->layout->metatileLayerOpacity(), + QSize(this->cellWidth, this->cellHeight), + true); this->basePixmap = QPixmap::fromImage(this->baseImage); } @@ -110,13 +81,25 @@ void TilesetEditorMetatileSelector::draw() { drawDivider(); drawFilters(); - drawSelection(); + if (this->inSwapMode) { + QSet metatileIds(this->swapMetatileIds.constBegin(), this->swapMetatileIds.constEnd()); + metatileIds.insert(this->lastHoveredMetatileId); + for (const auto &metatileId : metatileIds) { + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (ok) drawSelectionRect(pos, QSize(1,1), Qt::DashLine); + } + } else if (isValidMetatileId(this->selectedMetatileId)) { + drawSelection(); + } } bool TilesetEditorMetatileSelector::select(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) return false; - QPoint coords = this->getMetatileIdCoords(metatileId); - SelectablePixmapItem::select(coords.x(), coords.y(), 0, 0); + bool ok; + QPoint pos = metatileIdToPos(metatileId, &ok); + if (!ok) + return false; + SelectablePixmapItem::select(pos); this->selectedMetatileId = metatileId; emit selectedMetatileChanged(metatileId); return true; @@ -130,78 +113,156 @@ void TilesetEditorMetatileSelector::setTilesets(Tileset *primaryTileset, Tileset draw(); } -void TilesetEditorMetatileSelector::updateSelectedMetatile() { - QPoint origin = this->getSelectionStart(); - uint16_t metatileId = this->getMetatileId(origin.x(), origin.y()); - if (Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - this->selectedMetatileId = metatileId; - else - this->selectedMetatileId = Project::getNumMetatilesPrimary() + this->secondaryTileset->numMetatiles() - 1; - emit selectedMetatileChanged(this->selectedMetatileId); -} +void TilesetEditorMetatileSelector::addToSwapSelection(uint16_t metatileId) { + if (this->swapMetatileIds.contains(metatileId)) { + return; + } + if (this->swapMetatileIds.length() >= 2) { + this->swapMetatileIds.clear(); + } -uint16_t TilesetEditorMetatileSelector::getSelectedMetatileId() { - return this->selectedMetatileId; -} + this->swapMetatileIds.append(metatileId); + draw(); -uint16_t TilesetEditorMetatileSelector::getMetatileId(int x, int y) { - int index = y * this->numMetatilesWide + x; - int numPrimary = numPrimaryMetatilesRounded(); - if (index < numPrimary) { - return static_cast(index); - } else { - return static_cast(Project::getNumMetatilesPrimary() + index - numPrimary); + if (this->swapMetatileIds.length() == 2) { + emit swapRequested(this->swapMetatileIds.at(0), this->swapMetatileIds.at(1)); } } -bool TilesetEditorMetatileSelector::shouldAcceptEvent(QGraphicsSceneMouseEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - return Tileset::metatileIsValid(getMetatileId(pos.x(), pos.y()), this->primaryTileset, this->secondaryTileset); +void TilesetEditorMetatileSelector::removeFromSwapSelection(uint16_t metatileId) { + if (this->swapMetatileIds.removeOne(metatileId)) { + draw(); + } +} + +void TilesetEditorMetatileSelector::clearSwapSelection() { + if (this->swapMetatileIds.isEmpty()) + return; + this->swapMetatileIds.clear(); + draw(); } void TilesetEditorMetatileSelector::mousePressEvent(QGraphicsSceneMouseEvent *event) { - if (!shouldAcceptEvent(event)) return; + QPoint cellPos = getCellPos(event->pos()); + + bool ok; + uint16_t metatileId = posToMetatileId(cellPos, &ok); + if (!ok) return; + SelectablePixmapItem::mousePressEvent(event); - this->updateSelectedMetatile(); + this->selectedMetatileId = this->lastHoveredMetatileId = metatileId; + emit selectedMetatileChanged(this->selectedMetatileId); + this->prevCellPos = cellPos; } void TilesetEditorMetatileSelector::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - if (!shouldAcceptEvent(event)) return; + QPoint cellPos = getCellPos(event->pos()); + if (cellPos == this->prevCellPos) return; + + bool ok; + uint16_t metatileId = posToMetatileId(cellPos, &ok); + if (!ok) return; + SelectablePixmapItem::mouseMoveEvent(event); - this->updateSelectedMetatile(); + this->selectedMetatileId = this->lastHoveredMetatileId = metatileId; + emit selectedMetatileChanged(this->selectedMetatileId); emit hoveredMetatileChanged(this->selectedMetatileId); + this->prevCellPos = cellPos; } void TilesetEditorMetatileSelector::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { - if (!shouldAcceptEvent(event)) return; + QPoint cellPos = getCellPos(event->pos()); + + bool ok; + uint16_t metatileId = posToMetatileId(cellPos, &ok); + if (!ok) return; + + if (this->inSwapMode) { + if (this->swapMetatileIds.contains(metatileId)) { + this->removeFromSwapSelection(metatileId); + } else { + this->addToSwapSelection(metatileId); + } + } + SelectablePixmapItem::mouseReleaseEvent(event); - this->updateSelectedMetatile(); + this->selectedMetatileId = this->lastHoveredMetatileId = metatileId; + emit selectedMetatileChanged(this->selectedMetatileId); + this->prevCellPos = cellPos; } void TilesetEditorMetatileSelector::hoverMoveEvent(QGraphicsSceneHoverEvent *event) { - QPoint pos = this->getCellPos(event->pos()); - uint16_t metatileId = this->getMetatileId(pos.x(), pos.y()); - emit this->hoveredMetatileChanged(metatileId); + QPoint cellPos = getCellPos(event->pos()); + if (cellPos == this->prevCellPos) return; + + bool ok; + uint16_t metatileId = posToMetatileId(cellPos, &ok); + if (ok) { + this->lastHoveredMetatileId = metatileId; + emit this->hoveredMetatileChanged(metatileId); + if (this->inSwapMode) draw(); + } else { + emit this->hoveredMetatileCleared(); + } + this->prevCellPos = cellPos; } void TilesetEditorMetatileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { emit this->hoveredMetatileCleared(); + this->prevCellPos = QPoint(-1,-1); } -QPoint TilesetEditorMetatileSelector::getMetatileIdCoords(uint16_t metatileId) { - if (!Tileset::metatileIsValid(metatileId, this->primaryTileset, this->secondaryTileset)) - { - // Invalid metatile id. - return QPoint(0, 0); +uint16_t TilesetEditorMetatileSelector::posToMetatileId(const QPoint &pos, bool *ok) const { + return posToMetatileId(pos.x(), pos.y(), ok); +} + +uint16_t TilesetEditorMetatileSelector::posToMetatileId(int x, int y, bool *ok) const { + if (ok) *ok = true; + int index = y * this->numMetatilesWide + x; + uint16_t metatileId = static_cast(index); + if (this->primaryTileset && this->primaryTileset->containsMetatileId(metatileId)) { + return metatileId; } - int index = metatileId < Project::getNumMetatilesPrimary() - ? metatileId - : metatileId - Project::getNumMetatilesPrimary() + this->numPrimaryMetatilesRounded(); - return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + + // There's some extra handling here because we round the tilesets to keep them on separate rows. + // This means if the maximum number of primary metatiles is not divisible by the metatile width + // then the metatiles we used to round the primary tileset would have the index of valid secondary metatiles. + // These need to be ignored, or they'll appear to be duplicates of the subseqeunt secondary metatiles. + int numPrimaryRounded = numPrimaryMetatilesRounded(); + int firstSecondaryRow = numPrimaryRounded / this->numMetatilesWide; + metatileId = static_cast(Project::getNumMetatilesPrimary() + index - numPrimaryRounded); + if (this->secondaryTileset && this->secondaryTileset->containsMetatileId(metatileId) && y >= firstSecondaryRow) { + return metatileId; + } + + if (ok) *ok = false; + return 0; } -QPoint TilesetEditorMetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) { - QPoint pos = getMetatileIdCoords(metatileId); +QPoint TilesetEditorMetatileSelector::metatileIdToPos(uint16_t metatileId, bool *ok) const { + if (this->primaryTileset && this->primaryTileset->containsMetatileId(metatileId)) { + if (ok) *ok = true; + int index = metatileId; + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + if (this->secondaryTileset && this->secondaryTileset->containsMetatileId(metatileId)) { + if (ok) *ok = true; + int index = metatileId - Project::getNumMetatilesPrimary() + numPrimaryMetatilesRounded(); + return QPoint(index % this->numMetatilesWide, index / this->numMetatilesWide); + } + + if (ok) *ok = false; + return QPoint(0,0); +} + +bool TilesetEditorMetatileSelector::isValidMetatileId(uint16_t metatileId) const { + bool ok; + metatileIdToPos(metatileId, &ok); + return ok; +} + +QPoint TilesetEditorMetatileSelector::getMetatileIdCoordsOnWidget(uint16_t metatileId) const { + QPoint pos = metatileIdToPos(metatileId); pos.rx() = (pos.x() * this->cellWidth) + (this->cellWidth / 2); pos.ry() = (pos.y() * this->cellHeight) + (this->cellHeight / 2); return pos; @@ -328,3 +389,19 @@ void TilesetEditorMetatileSelector::drawCounts() { this->setPixmap(metatilesPixmap); } + +void TilesetEditorMetatileSelector::setSwapMode(bool enabled) { + if (enabled == this->inSwapMode) + return; + this->inSwapMode = enabled; + this->swapMetatileIds.clear(); + if (porymapConfig.prettyCursors) { + if (enabled) { + static const QCursor cursor = QCursor(QPixmap(":/icons/swap_cursor.ico"), 10, 10); + setCursor(cursor); + } else { + unsetCursor(); + } + } + draw(); +} diff --git a/src/ui/tileseteditortileselector.cpp b/src/ui/tileseteditortileselector.cpp index 01e4008c..026b3d58 100644 --- a/src/ui/tileseteditortileselector.cpp +++ b/src/ui/tileseteditortileselector.cpp @@ -4,61 +4,76 @@ #include #include -QPoint TilesetEditorTileSelector::getSelectionDimensions() { +QSize TilesetEditorTileSelector::getSelectionDimensions() const { if (this->externalSelection) { - return QPoint(this->externalSelectionWidth, this->externalSelectionHeight); + return QSize(this->externalSelectionWidth, this->externalSelectionHeight); } else { return SelectablePixmapItem::getSelectionDimensions(); } } -void TilesetEditorTileSelector::draw() { - if (!this->primaryTileset || !this->secondaryTileset) { - this->setPixmap(QPixmap()); +void TilesetEditorTileSelector::setMaxSelectionSize(int width, int height) { + width = qMax(1, width); + height = qMax(1, height); + SelectablePixmapItem::setMaxSelectionSize(width, height); + if (this->externalSelection) { + if (this->externalSelectionWidth > this->maxSelectionWidth || this->externalSelectionHeight > this->maxSelectionHeight) { + // Crop external selection to new max size. + QList cropped; + int croppedWidth = qMin(this->externalSelectionWidth, this->maxSelectionWidth); + int croppedHeight = qMin(this->externalSelectionHeight, this->maxSelectionHeight); + for (int y = 0; y < croppedHeight; y++) + for (int x = 0; x < croppedWidth; x++) { + int index = y * this->externalSelectionWidth + x; + cropped.append(this->externalSelectedTiles.value(index)); + } + this->externalSelectionWidth = croppedWidth; + this->externalSelectionHeight = croppedHeight; + this->externalSelectedTiles = cropped; + } + } else { + updateSelectedTiles(); + } +} + +void TilesetEditorTileSelector::updateBasePixmap() { + if (!this->primaryTileset || !this->secondaryTileset || this->numTilesWide == 0) { + this->basePixmap = QPixmap(); + return; } int totalTiles = Project::getNumTilesTotal(); - int primaryLength = this->primaryTileset->tiles.length(); - int secondaryLength = this->secondaryTileset->tiles.length(); int height = totalTiles / this->numTilesWide; - QList palette = Tileset::getPalette(this->paletteId, this->primaryTileset, this->secondaryTileset, true); - QImage image(this->numTilesWide * 16, height * 16, QImage::Format_RGBA8888); + QImage image(this->numTilesWide * this->cellWidth, height * this->cellHeight, QImage::Format_RGBA8888); QPainter painter(&image); - for (uint16_t tile = 0; tile < totalTiles; tile++) { - QImage tileImage; - if (tile < primaryLength) { - tileImage = getPalettedTileImage(tile, this->primaryTileset, this->secondaryTileset, this->paletteId, true).scaled(16, 16); - } else if (tile < Project::getNumTilesPrimary()) { - tileImage = QImage(16, 16, QImage::Format_RGBA8888); - tileImage.fill(palette.at(0)); - } else if (tile < Project::getNumTilesPrimary() + secondaryLength) { - tileImage = getPalettedTileImage(tile, this->primaryTileset, this->secondaryTileset, this->paletteId, true).scaled(16, 16); - } else { - tileImage = QImage(16, 16, QImage::Format_RGBA8888); - QPainter painter(&tileImage); - painter.fillRect(0, 0, 16, 16, palette.at(0)); - } - - int y = tile / this->numTilesWide; - int x = tile % this->numTilesWide; - QPoint origin = QPoint(x * 16, y * 16); - painter.drawImage(origin, tileImage); + for (uint16_t tileId = 0; tileId < totalTiles; tileId++) { + QImage tileImage = getPalettedTileImage(tileId, this->primaryTileset, this->secondaryTileset, this->paletteId, true) + .scaled(this->cellWidth, this->cellHeight); + int x = (tileId % this->numTilesWide) * this->cellWidth; + int y = (tileId / this->numTilesWide) * this->cellHeight; + painter.drawImage(x, y, tileImage); } + painter.end(); + + this->basePixmap = QPixmap::fromImage(image); +} + +void TilesetEditorTileSelector::draw() { + if (this->basePixmap.isNull()) + updateBasePixmap(); + + QPixmap pixmap = this->basePixmap; if (this->showDivider) { - int row = this->primaryTileset->tiles.length() / this->numTilesWide; - if (this->primaryTileset->tiles.length() % this->numTilesWide != 0) { - // Round up height for incomplete last row - row++; - } - const int y = row * 16; + QPainter painter(&pixmap); + int row = Util::roundUpToMultiple(Project::getNumTilesPrimary(), this->numTilesWide) / this->numTilesWide; + const int y = row * this->cellHeight; painter.setPen(Qt::white); - painter.drawLine(0, y, this->numTilesWide * 16, y); + painter.drawLine(0, y, this->numTilesWide * this->cellWidth, y); } - painter.end(); - this->setPixmap(QPixmap::fromImage(image)); + setPixmap(pixmap); if (!this->externalSelection || (this->externalSelectionWidth == 1 && this->externalSelectionHeight == 1)) { this->drawSelection(); @@ -82,12 +97,14 @@ void TilesetEditorTileSelector::highlight(uint16_t tile) { void TilesetEditorTileSelector::setTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { this->primaryTileset = primaryTileset; this->secondaryTileset = secondaryTileset; + this->updateBasePixmap(); this->draw(); } void TilesetEditorTileSelector::setPaletteId(int paletteId) { this->paletteId = paletteId; this->paletteChanged = true; + this->updateBasePixmap(); this->draw(); } @@ -101,9 +118,9 @@ void TilesetEditorTileSelector::updateSelectedTiles() { this->externalSelection = false; this->selectedTiles.clear(); QPoint origin = this->getSelectionStart(); - QPoint dimensions = this->getSelectionDimensions(); - for (int j = 0; j < dimensions.y(); j++) { - for (int i = 0; i < dimensions.x(); i++) { + QSize dimensions = this->getSelectionDimensions(); + for (int j = 0; j < dimensions.height(); j++) { + for (int i = 0; i < dimensions.width(); i++) { uint16_t metatileId = this->getTileId(origin.x() + i, origin.y() + j); this->selectedTiles.append(metatileId); } @@ -114,41 +131,32 @@ QList TilesetEditorTileSelector::getSelectedTiles() { if (this->externalSelection) { return buildSelectedTiles(this->externalSelectionWidth, this->externalSelectionHeight, this->externalSelectedTiles); } else { - QPoint dimensions = this->getSelectionDimensions(); + QSize dimensions = this->getSelectionDimensions(); QList tiles; for (int i = 0; i < this->selectedTiles.length(); i++) { uint16_t tile = this->selectedTiles.at(i); tiles.append(Tile(tile, false, false, this->paletteId)); } - return buildSelectedTiles(dimensions.x(), dimensions.y(), tiles); + return buildSelectedTiles(dimensions.width(), dimensions.height(), tiles); } } -QList TilesetEditorTileSelector::buildSelectedTiles(int width, int height, QList selected) { +QList TilesetEditorTileSelector::buildSelectedTiles(int width, int height, const QList &selected) { QList tiles; QList> tileMatrix; for (int j = 0; j < height; j++) { QList row; - QList layerRow; for (int i = 0; i < width; i++) { int index = i + j * width; - Tile tile = selected.at(index); + Tile tile = selected.value(index); tile.xflip ^= this->xFlip; tile.yflip ^= this->yFlip; if (this->paletteChanged) tile.palette = this->paletteId; if (this->xFlip) - layerRow.prepend(tile); + row.prepend(tile); else - layerRow.append(tile); - - // If we've completed a layer row, or its the last tile of an incompletely - // selected layer, then append the layer row to the full row - // If not an external selection, treat the whole row as 1 "layer" - if (i == width - 1 || (this->externalSelection && (this->externalSelectedPos.at(index) % 4) & 1)) { - row.append(layerRow); - layerRow.clear(); - } + row.append(tile); } if (this->yFlip) tileMatrix.prepend(row); @@ -163,15 +171,14 @@ QList TilesetEditorTileSelector::buildSelectedTiles(int width, int height, return tiles; } -void TilesetEditorTileSelector::setExternalSelection(int width, int height, QList tiles, QList tileIdxs) { +void TilesetEditorTileSelector::setExternalSelection(int width, int height, const QList &tiles) { + width = qBound(1, width, this->maxSelectionWidth); + height = qBound(1, height, this->maxSelectionHeight); this->externalSelection = true; this->paletteChanged = false; this->externalSelectionWidth = width; this->externalSelectionHeight = height; - this->externalSelectedTiles.clear(); - this->externalSelectedTiles.append(tiles); - this->externalSelectedPos.clear(); - this->externalSelectedPos.append(tileIdxs); + this->externalSelectedTiles = tiles.mid(0, width * height); this->draw(); emit selectedTilesChanged(); } @@ -181,16 +188,20 @@ uint16_t TilesetEditorTileSelector::getTileId(int x, int y) { } void TilesetEditorTileSelector::mousePressEvent(QGraphicsSceneMouseEvent *event) { + this->prevCellPos = getCellPos(event->pos()); SelectablePixmapItem::mousePressEvent(event); this->updateSelectedTiles(); emit selectedTilesChanged(); } void TilesetEditorTileSelector::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { + QPoint pos = getCellPos(event->pos()); + if (this->prevCellPos == pos) + return; + this->prevCellPos = pos; + SelectablePixmapItem::mouseMoveEvent(event); this->updateSelectedTiles(); - - QPoint pos = this->getCellPos(event->pos()); uint16_t tile = this->getTileId(pos.x(), pos.y()); emit hoveredTileChanged(tile); emit selectedTilesChanged(); @@ -213,7 +224,7 @@ void TilesetEditorTileSelector::hoverLeaveEvent(QGraphicsSceneHoverEvent*) { } QPoint TilesetEditorTileSelector::getTileCoords(uint16_t tile) { - if (tile >= Project::getNumTilesTotal()) + if (tile >= Project::getNumTilesTotal() || this->numTilesWide == 0) { // Invalid tile. return QPoint(0, 0); @@ -233,28 +244,30 @@ QImage TilesetEditorTileSelector::buildPrimaryTilesIndexedImage() { if (!this->primaryTileset) return QImage(); - return buildImage(0, this->primaryTileset->tiles.length()); + return buildImage(0, this->primaryTileset->numTiles()); } QImage TilesetEditorTileSelector::buildSecondaryTilesIndexedImage() { if (!this->secondaryTileset) return QImage(); - return buildImage(Project::getNumTilesPrimary(), this->secondaryTileset->tiles.length()); + return buildImage(Project::getNumTilesPrimary(), this->secondaryTileset->numTiles()); } QImage TilesetEditorTileSelector::buildImage(int tileIdStart, int numTiles) { + if (this->numTilesWide == 0) + return QImage(); + int height = qCeil(numTiles / static_cast(this->numTilesWide)); - QImage image(this->numTilesWide * 8, height * 8, QImage::Format_RGBA8888); + QImage image(this->numTilesWide * Tile::pixelWidth(), height * Tile::pixelHeight(), QImage::Format_RGBA8888); image.fill(0); QPainter painter(&image); for (int i = 0; i < numTiles; i++) { QImage tileImage = getGreyscaleTileImage(tileIdStart + i, this->primaryTileset, this->secondaryTileset); - int y = i / this->numTilesWide; - int x = i % this->numTilesWide; - QPoint origin = QPoint(x * 8, y * 8); - painter.drawImage(origin, tileImage); + int x = (i % this->numTilesWide) * Tile::pixelWidth(); + int y = (i / this->numTilesWide) * Tile::pixelHeight(); + painter.drawImage(x, y, tileImage); } painter.end(); @@ -305,7 +318,7 @@ void TilesetEditorTileSelector::drawUnused() { for (int tile = 0; tile < this->usedTiles.size(); tile++) { if (!this->usedTiles[tile]) { - unusedPainter.drawPixmap((tile % 16) * 16, (tile / 16) * 16, redX); + unusedPainter.drawPixmap((tile % this->cellWidth) * this->cellWidth, (tile / this->cellWidth) * this->cellHeight, redX); } } diff --git a/src/ui/uintspinbox.cpp b/src/ui/uintspinbox.cpp index 53f6df78..7155aa67 100644 --- a/src/ui/uintspinbox.cpp +++ b/src/ui/uintspinbox.cpp @@ -2,24 +2,24 @@ #include UIntSpinBox::UIntSpinBox(QWidget *parent) - : QAbstractSpinBox(parent) + : QAbstractSpinBox(parent), + m_minimum(0), + m_maximum(99), + m_value(m_minimum), + m_singleStep(1), + m_displayIntegerBase(10), + m_hasPadding(false), + m_numChars(2) { // Don't let scrolling hijack focus. setFocusPolicy(Qt::StrongFocus); - m_minimum = 0; - m_maximum = 99; - m_value = m_minimum; - m_displayIntegerBase = 10; - m_numChars = 2; - m_hasPadding = false; - this->updateEdit(); connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(onEditFinished())); }; void UIntSpinBox::setValue(uint32_t val) { - val = qMax(m_minimum, qMin(m_maximum, val)); + val = qBound(m_minimum, val, m_maximum); if (m_value != val) { m_value = val; emit valueChanged(m_value); @@ -69,6 +69,12 @@ void UIntSpinBox::setRange(uint32_t min, uint32_t max) { this->updateEdit(); } +void UIntSpinBox::setSingleStep(uint32_t val) { + if (m_singleStep != val) { + m_singleStep = val; + } +} + void UIntSpinBox::setPrefix(const QString &prefix) { if (m_prefix != prefix) { m_prefix = prefix; @@ -113,11 +119,16 @@ void UIntSpinBox::onEditFinished() { // Valid input newValue = this->valueFromText(input); } else if (state == QValidator::Intermediate) { - // User has deleted all the number text. - // If they did this by selecting all text and then hitting delete - // make sure to put the cursor back in front of the prefix. - newValue = m_minimum; - this->lineEdit()->setCursorPosition(m_prefix.length()); + if (input == m_prefix) { + // User has deleted all the number text. + // If they did this by selecting all text and then hitting delete + // make sure to put the cursor back in front of the prefix. + newValue = m_minimum; + this->lineEdit()->setCursorPosition(m_prefix.length()); + } else { + // Other intermediate inputs (values outside of acceptable range) should be ignored. + return; + } } if (newValue != m_value) { m_value = newValue; @@ -127,6 +138,7 @@ void UIntSpinBox::onEditFinished() { } void UIntSpinBox::stepBy(int steps) { + steps *= m_singleStep; auto newValue = m_value; if (steps < 0 && newValue + steps > newValue) { newValue = 0; @@ -160,10 +172,14 @@ QValidator::State UIntSpinBox::validate(QString &input, int &pos) const { bool ok; uint32_t num = copy.toUInt(&ok, m_displayIntegerBase); - if (!ok || num < m_minimum || num > m_maximum) + if (!ok) return QValidator::Invalid; input += copy.toUpper(); + + if (num < m_minimum || num > m_maximum) + return QValidator::Intermediate; + return QValidator::Acceptable; } diff --git a/src/ui/updatepromoter.cpp b/src/ui/updatepromoter.cpp index 8afc8d9c..4b66fbff 100644 --- a/src/ui/updatepromoter.cpp +++ b/src/ui/updatepromoter.cpp @@ -1,3 +1,4 @@ +#ifdef QT_NETWORK_LIB #include "updatepromoter.h" #include "ui_updatepromoter.h" #include "log.h" @@ -188,3 +189,5 @@ void UpdatePromoter::dialogButtonClicked(QAbstractButton *button) { QDesktopServices::openUrl(this->downloadUrl); } } + +#endif // QT_NETWORK_LIB diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index fd2d9216..bc45415a 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -1,4 +1,4 @@ -#if __has_include() +#ifdef QT_CHARTS_LIB #include "wildmonchart.h" #include "ui_wildmonchart.h" #include "config.h" @@ -367,7 +367,7 @@ QChart* WildMonChart::createLevelDistributionChart() { series->attachAxis(axisY); // We round the y-axis max up to a multiple of 5. - axisY->setMax(Util::roundUp(qCeil(axisY->max()), 5)); + axisY->setMax(Util::roundUpToMultiple(qCeil(axisY->max()), 5)); return chart; } @@ -392,10 +392,10 @@ void WildMonChart::updateTheme() { saveSpeciesColors(static_cast(chart->series().at(0))->barSets()); chart = ui->chartView_LevelDistribution->chart(); - if (chart) { - chart->setTheme(theme); - applySpeciesColors(static_cast(chart->series().at(0))->barSets()); - } + if (!chart || chart->series().isEmpty()) + return; + chart->setTheme(theme); + applySpeciesColors(static_cast(chart->series().at(0))->barSets()); } void WildMonChart::saveSpeciesColors(const QList &barSets) { @@ -466,4 +466,4 @@ void WildMonChart::closeEvent(QCloseEvent *event) { QWidget::closeEvent(event); } -#endif // __has_include() +#endif // QT_CHARTS_LIB diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp index 9957f33a..c8053bdb 100644 --- a/src/ui/wildmonsearch.cpp +++ b/src/ui/wildmonsearch.cpp @@ -122,7 +122,10 @@ void WildMonSearch::updateResults(const QString &species) { if (ui->comboBox_Search->findText(species) < 0) return; // Not a species name, no need to search wild encounter data. - const QList results = this->resultsCache.value(species, search(species)); + auto it = this->resultsCache.constFind(species); + bool inCache = (it != this->resultsCache.constEnd()); + const QList results = inCache ? it.value() : search(species); + if (results.isEmpty()) { static const RowData noResults = { .mapName = "", @@ -140,7 +143,7 @@ void WildMonSearch::updateResults(const QString &species) { ui->table_Results->setSortingEnabled(true); - this->resultsCache.insert(species, results); + if (!inCache) this->resultsCache.insert(species, results); } // Double-clicking row data opens the corresponding map/table on the Wild Pokémon tab.