diff --git a/CHANGELOG.md b/CHANGELOG.md index 984f3879..50007ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Add support for defining project values with `enum` where `#define` was expected. - Add a setting to specify the tile values to use for the unused metatile layer. - Add a setting to specify the maximum number of events in a group. A warning will be shown if too many events are added. +- Add a setting to customize the size and position of the player view distance. - Add `onLayoutOpened` to the scripting API. ### Changed @@ -35,7 +36,6 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions. - A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening. - The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire". -- The max encounter rate is now read from the project, rather than assuming the default value from RSE. - It's now possible to cancel quitting if there are unsaved changes in sub-windows. - The triple-layer metatiles setting can now be set automatically using a project constant. - `Export Map Stitch Image` and `Export Map Timelapse Image` now show a preview of the full image/gif, not just the current map. @@ -50,6 +50,10 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - `Script` dropdowns now autocomplete only with scripts from the current map, rather than every script in the project. The old behavior is available via a new setting. - The options for `Encounter Type` and `Terrain Type` in the Tileset Editor are not hardcoded anymore, they're now read from the project. - The `symbol_wild_encounters` setting was replaced; this value is now read from the project. +- The max encounter rate is now read from the project, rather than assuming the default value from RSE. +- `MAP_OFFSET_W` and `MAP_OFFSET_H` (used to limit the maximum map size) are now read from the project. +- The rendered area of the map border is now limited to the maximum player view distance (prior to this it included two extra rows on the top and bottom). +- An error message will now be shown when Porymap is unable to save changes (e.g. if Porymap doesn't have write permissions for your project). - A project may now be opened even if it has no maps or map groups. A minimum of one map layout is required. - The file extensions that are expected for `.png` and `.pal` data files and the extensions outputted when creating a new tileset can now be customized. - Miscellaneous performance improvements, especially for opening projects. @@ -58,6 +62,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d - Fix `Add Region Map...` not updating the region map settings file. - Fix some crashes on invalid region map tilesets. - Improve error reporting for invalid region map editor settings. +- Fix the region map editor's palette resetting between region maps. +- Fix the region map editor's h-flip and v-flip settings being swapped. - Fix config files being written before the project is opened successfully. - Fix the map and other project info still displaying if a new project fails to open. - Fix unsaved changes being ignored when quitting (such as with Cmd+Q on macOS). diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index caac85c2..93715d86 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -97,6 +97,8 @@ In addition to these files, there are some specific symbol and macro names that ``define_pals_total``, ``NUM_PALS_TOTAL``, ``define_tiles_per_metatile``, ``NUM_TILES_PER_METATILE``, to determine if triple-layer metatiles are in use. Values other than 8 or 12 are ignored ``define_map_size``, ``MAX_MAP_DATA_SIZE``, to limit map dimensions + ``define_map_offset_width``, ``MAP_OFFSET_W``, to limit map dimensions + ``define_map_offset_height``, ``MAP_OFFSET_H``, to limit map dimensions ``define_mask_metatile``, ``MAPGRID_METATILE_ID_MASK``, optionally read to get settings on ``Maps`` tab ``define_mask_collision``, ``MAPGRID_COLLISION_MASK``, optionally read to get settings on ``Maps`` tab ``define_mask_elevation``, ``MAPGRID_ELEVATION_MASK``, optionally read to get settings on ``Maps`` tab diff --git a/forms/connectionslistitem.ui b/forms/connectionslistitem.ui index bf04e8be..116da983 100644 --- a/forms/connectionslistitem.ui +++ b/forms/connectionslistitem.ui @@ -6,7 +6,7 @@ 0 0 - 178 + 188 157 @@ -20,7 +20,7 @@ .ConnectionsListItem { border-width: 1px; } - QFrame::StyledPanel + QFrame::Shape::StyledPanel @@ -65,7 +65,7 @@ - Remove this connection. + <html><head/><body><p>Remove this connection.</p></body></html> ... @@ -79,28 +79,28 @@ - Where the connected map should be positioned relative to the current map. + <html><head/><body><p>Where the connected map should be positioned relative to the current map.</p></body></html> - The name of the map to connect to the current map. + <html><head/><body><p>The name of the map to connect to the current map.</p></body></html> - The number of spaces to move the connected map perpendicular to its connected direction. + <html><head/><body><p>The number of spaces to move the connected map perpendicular to its connected direction.</p></body></html> - Open the connected map. + <html><head/><body><p>Open the connected map.</p></body></html> ... diff --git a/forms/customattributesdialog.ui b/forms/customattributesdialog.ui index b1f1ee4b..90dfba6e 100644 --- a/forms/customattributesdialog.ui +++ b/forms/customattributesdialog.ui @@ -33,7 +33,7 @@ - The key name for the new JSON field + <html><head/><body><p>The key name for the new JSON field</p></body></html> true @@ -50,7 +50,7 @@ - The data type for the new JSON field + <html><head/><body><p>The data type for the new JSON field</p></body></html> @@ -70,7 +70,7 @@ - The value for the new JSON field + <html><head/><body><p>The value for the new JSON field</p></body></html> diff --git a/forms/customscriptseditor.ui b/forms/customscriptseditor.ui index e2efa2af..7db3b208 100644 --- a/forms/customscriptseditor.ui +++ b/forms/customscriptseditor.ui @@ -60,7 +60,7 @@ - Create a new Porymap script file with a default template + <html><head/><body><p>Create a new Porymap script file with a default template</p></body></html> Create New Script... @@ -74,7 +74,7 @@ - Add an existing script file to the list below + <html><head/><body><p>Add an existing script file to the list below</p></body></html> Load Script... @@ -88,7 +88,7 @@ - Refresh all loaded scripts to account for any recent edits + <html><head/><body><p>Refresh all loaded scripts to account for any recent edits</p></body></html> Refresh Scripts diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 86f50047..bf0d2608 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -260,9 +260,6 @@ false - - - 0 @@ -351,12 +348,6 @@ false - - QGraphicsView::ViewportAnchor::AnchorUnderMouse - - - QGraphicsView::ViewportAnchor::AnchorUnderMouse - @@ -2386,7 +2377,7 @@ - If enabled, connections will automatically be updated on the connected map. + <html><head/><body><p>If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider.</p></body></html> Mirror to Connecting Maps @@ -2435,8 +2426,11 @@ + + false + - Open the selected Dive Map + <html><head/><body><p>Open the selected Dive Map</p></body></html> ... @@ -2450,7 +2444,7 @@ - If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider. + <html><head/><body><p>If enabled, the connected Emerge and/or Dive maps will be displayed with an opacity set using the slider.</p></body></html> Show Emerge/Dive Maps @@ -2569,8 +2563,11 @@ + + false + - Open the selected Emerge Map + <html><head/><body><p>Open the selected Emerge Map</p></body></html> ... @@ -2589,7 +2586,7 @@ Qt::Orientation::Horizontal - + 0 @@ -2863,6 +2860,7 @@ + @@ -2887,8 +2885,10 @@ + + @@ -3076,45 +3076,6 @@ Ctrl+T - - - true - - - - :/icons/sort_alphabet.ico:/icons/sort_alphabet.ico - - - Sort by &Location - - - - - true - - - - :/icons/sort_number.ico:/icons/sort_number.ico - - - Sort by &Group - - - Sort by Group - - - - - true - - - - :/icons/sort_map.ico:/icons/sort_map.ico - - - Sort by &Layout - - About Porymap... @@ -3310,9 +3271,14 @@ MapView - QWidget + QGraphicsView
mapview.h
+ + ConnectionsView + QGraphicsView +
graphicsview.h
+
MapTree QTreeView @@ -3321,7 +3287,7 @@ NoScrollGraphicsView QGraphicsView -
mapview.h
+
graphicsview.h
MapListToolBar diff --git a/forms/mapheaderform.ui b/forms/mapheaderform.ui index 8faba290..08552e2c 100644 --- a/forms/mapheaderform.ui +++ b/forms/mapheaderform.ui @@ -7,7 +7,7 @@ 0 0 407 - 349 + 380
@@ -224,7 +224,11 @@
- + + + <html><head/><body><p>The name that will be displayed in-game for this Location. This name will be shared with any other map that has the same Location.</p></body></html> + +
diff --git a/forms/maplisttoolbar.ui b/forms/maplisttoolbar.ui index 54eb48d0..07878f0a 100644 --- a/forms/maplisttoolbar.ui +++ b/forms/maplisttoolbar.ui @@ -32,7 +32,7 @@ - Add a new folder to the list. + <html><head/><body><p>Add a new folder to the list.</p></body></html> @@ -73,7 +73,7 @@ - Expand all folders in the list. + <html><head/><body><p>Expand all folders in the list.</p></body></html> @@ -93,7 +93,7 @@ - Collapse all folders in the list. + <html><head/><body><p>Collapse all folders in the list.</p></body></html> @@ -113,7 +113,7 @@ - If enabled, folders may be renamed and items in the list may be rearranged. + <html><head/><body><p>If enabled, folders may be renamed and items in the list may be rearranged.</p></body></html> diff --git a/forms/newmapconnectiondialog.ui b/forms/newmapconnectiondialog.ui index 9b3a3b6e..85aeec72 100644 --- a/forms/newmapconnectiondialog.ui +++ b/forms/newmapconnectiondialog.ui @@ -17,10 +17,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -45,7 +45,7 @@ - The name of the map to connect to the current map. + <html><head/><body><p>The name of the map to connect to the current map.</p></body></html> @@ -59,7 +59,7 @@ - Where the connected map should be positioned relative to the current map. + <html><head/><body><p>Where the connected map should be positioned relative to the current map.</p></body></html> @@ -82,10 +82,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/forms/preferenceeditor.ui b/forms/preferenceeditor.ui index 2ce37cbe..83de6f18 100644 --- a/forms/preferenceeditor.ui +++ b/forms/preferenceeditor.ui @@ -40,7 +40,7 @@ - If checked, a prompt to reload your project will appear if relevant project files are edited + <html><head/><body><p>If checked, a prompt to reload your project will appear if relevant project files are edited</p></body></html> Monitor project files @@ -50,7 +50,7 @@ - If checked, Porymap will automatically open your most recently opened project on startup + <html><head/><body><p>If checked, Porymap will automatically open your most recently opened project on startup</p></body></html> Open recent project on launch @@ -60,7 +60,7 @@ - If checked, Porymap will automatically alert you on startup if a new release is available + <html><head/><body><p>If checked, Porymap will automatically alert you on startup if a new release is available</p></body></html> Automatically check for updates @@ -112,7 +112,7 @@ - If checked, no warning will be shown when deleting an event that has an associated #define that may also be deleted. + <html><head/><body><p>If checked, no warning will be shown when deleting an event that has an associated #define that may also be deleted.</p></body></html> Disable warning when deleting events with IDs @@ -138,7 +138,7 @@ - If enabled, an event can be selected by clicking directly on the opaque pixels of its sprite. This may be preferable when events are overlapping. + <html><head/><body><p>If enabled, an event can be selected by clicking directly on the opaque pixels of its sprite. This may be preferable when events are overlapping.</p></body></html> Select by clicking on sprite @@ -148,7 +148,7 @@ - If enabled, an event can be selected by clicking anywhere within its sprite dimensions. This may be preferable for events with small or mostly transparent sprites. + <html><head/><body><p>If enabled, an event can be selected by clicking anywhere within its sprite dimensions. This may be preferable for events with small or mostly transparent sprites.</p></body></html> Select by clicking within bounding rectangle @@ -231,7 +231,7 @@ - The shell command for your preferred text editor (possibly an absolute path if the program doesn't exist in your PATH). + <html><head/><body><p>The shell command for your preferred text editor (possibly an absolute path if the program doesn't exist in your PATH).</p></body></html> e.g. code %D @@ -264,7 +264,7 @@ - The shell command for your preferred text editor to open a file to a specific line number (possibly an absolute path if the program doesn't exist in your PATH). + <html><head/><body><p>The shell command for your preferred text editor to open a file to a specific line number (possibly an absolute path if the program doesn't exist in your PATH).</p></body></html> e.g. code --goto %F:%L diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui index 179392dd..4cf06515 100644 --- a/forms/projectsettingseditor.ui +++ b/forms/projectsettingseditor.ui @@ -6,8 +6,8 @@ 0 0 - 631 - 600 + 642 + 609 @@ -38,8 +38,8 @@ 0 0 - 559 - 589 + 570 + 692 @@ -52,7 +52,7 @@ - Whether map script files should prefer using .pory + <html><head/><body><p>Whether map script files should prefer using .pory</p></body></html> Use Poryscript @@ -61,6 +61,9 @@ + + <html><head/><body><p>If enabled, Porymap will display wild encounter data on the Wild Pokémon tab.</p></body></html> + Show Wild Encounter Tables @@ -99,7 +102,7 @@ - Restore the data in the prefabs file to the version defaults. Will create a new file if one doesn't exist. + <html><head/><body><p>Restore the data in the prefabs file to the version defaults. Will create a new file if one doesn't exist.</p></body></html> Import Defaults @@ -109,7 +112,7 @@ - The file that will be used to populate the Prefabs tab + <html><head/><body><p>The file that will be used to populate the Prefabs tab</p></body></html> prefabs.json @@ -148,7 +151,7 @@ - The image sheet that will be used to represent elevation and collision on the Collision tab + <html><head/><body><p>The image sheet that will be used to represent elevation and collision on the Collision tab</p></body></html> true @@ -176,7 +179,7 @@ - The maximum collision value represented with an icon on the image sheet + <html><head/><body><p>The maximum collision value represented with an icon on the image sheet</p></body></html> @@ -197,7 +200,7 @@ - The maximum elevation value represented with an icon on the image sheet + <html><head/><body><p>The maximum elevation value represented with an icon on the image sheet</p></body></html> @@ -270,7 +273,7 @@ - The icon that will be displayed on the Wild Pokémon tab for the above species + <html><head/><body><p>The icon that will be displayed on the Wild Pokémon tab for the above species</p></body></html> true @@ -280,6 +283,91 @@ + + + + Player View Distance + + + + + + + + North + + + + + + + South + + + + + + + <html><head/><body><p>The distance (in pixels) that a player is able to see North of their character's position in-game. By default this is the distance from the center 16x16 to the edge of the 160 pixel tall GBA screen.</p></body></html> + + + 0 + + + + + + + <html><head/><body><p>The distance (in pixels) that a player is able to see South of their character's position in-game. By default this is the distance from the center 16x16 to the edge of the 160 pixel tall GBA screen.</p></body></html> + + + 0 + + + + + + + + + + + West + + + + + + + East + + + + + + + <html><head/><body><p>The distance (in pixels) that a player is able to see West of their character's position in-game. By default this is the distance from the center 16x16 to the edge of the 240 pixel wide GBA screen.</p></body></html> + + + 0 + + + + + + + <html><head/><body><p>The distance (in pixels) that a player is able to see East of their character's position in-game. By default this is the distance from the center 16x16 to the edge of the 240 pixel wide GBA screen.</p></body></html> + + + 0 + + + + + + + + @@ -378,8 +466,8 @@ 0 0 - 559 - 560 + 570 + 622 @@ -389,37 +477,6 @@ Map Data Defaults - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - The default metatile value that will be used to fill new maps - - - 0x - - - 16 - - - @@ -427,6 +484,50 @@ + + + + Width + + + + + + + <html><head/><body><p>The default elevation that will be used to fill new maps</p></body></html> + + + + + + + <html><head/><body><p>Whether a separate text.inc or text.pory file will be created for new maps, alongside the scripts file</p></body></html> + + + Create separate text file + + + + + + + <html><head/><body><p>The default layout width for new maps</p></body></html> + + + 1 + + + + + + + <html><head/><body><p>The default layout height for new maps</p></body></html> + + + 1 + + + @@ -434,13 +535,10 @@ - - - - Whether a separate text.inc or text.pory file will be created for new maps, alongside the scripts file - + + - Create separate text file + Collision @@ -451,6 +549,20 @@ + + + + <html><head/><body><p>The default metatile value that will be used to fill new maps</p></body></html> + + + + + + + <html><head/><body><p>The default collision that will be used to fill new maps</p></body></html> + + + @@ -470,7 +582,7 @@ - A comma-separated list of metatile values that will be used to fill new map borders + <html><head/><body><p>A comma-separated list of metatile values that will be used to fill new map borders</p></body></html> @@ -491,54 +603,30 @@ 0 - + - The default metatile value that will be used for the top-left border metatile on new maps. - - - 0x - - - 16 + <html><head/><body><p>The default metatile value that will be used for the top-left border metatile on new maps.</p></body></html> - + - The default metatile value that will be used for the top-right border metatile on new maps. - - - 0x - - - 16 + <html><head/><body><p>The default metatile value that will be used for the top-right border metatile on new maps.</p></body></html> - + - The default metatile value that will be used for the bottom-left border metatile on new maps. - - - 0x - - - 16 + <html><head/><body><p>The default metatile value that will be used for the bottom-left border metatile on new maps.</p></body></html> - + - The default metatile value that will be used for the bottom-right border metatile on new maps. - - - 0x - - - 16 + <html><head/><body><p>The default metatile value that will be used for the bottom-right border metatile on new maps.</p></body></html> @@ -546,24 +634,28 @@ - - - - The default elevation that will be used to fill new maps - + + + + + 0 + + + 0 + + + 0 + + + 0 + + - - + + - Collision - - - - - - - The default collision that will be used to fill new maps + Height @@ -614,7 +706,7 @@ - The mask used to read/write metatile IDs in map data. + <html><head/><body><p>The mask used to read/write metatile IDs in map data.</p></body></html> @@ -628,7 +720,7 @@ - The mask used to read/write collision values in map data. + <html><head/><body><p>The mask used to read/write collision values in map data.</p></body></html> @@ -642,7 +734,7 @@ - The mask used to read/write elevation values in map data. + <html><head/><body><p>The mask used to read/write elevation values in map data.</p></body></html> @@ -671,7 +763,7 @@ - Whether "Allow Running", "Allow Biking" and "Allow Dig & Escape Rope" are default options for Map Headers + <html><head/><body><p>Whether &quot;Allow Running&quot;, &quot;Allow Biking&quot; and &quot;Allow Dig &amp; Escape Rope&quot; are default options for Map Headers</p></body></html> Enable 'Allow Running/Biking/Escaping' @@ -681,7 +773,7 @@ - Whether "Floor Number" is a default option for Map Headers + <html><head/><body><p>Whether &quot;Floor Number&quot; is a default option for Map Headers</p></body></html> Enable 'Floor Number' @@ -691,7 +783,7 @@ - Whether the dimensions of the border can be changed. If not set, all borders are 2x2 + <html><head/><body><p>Whether the dimensions of the border can be changed. If not set, all borders are 2x2</p></body></html> Enable Custom Border Size @@ -751,7 +843,7 @@ 0 0 - 559 + 570 798 @@ -770,7 +862,11 @@ - + + + <html><head/><body><p>The default primary tileset to use for new maps/layouts.</p></body></html> + + @@ -780,7 +876,11 @@ - + + + <html><head/><body><p>The default secondary tileset to use for new maps/layouts.</p></body></html> + + @@ -794,7 +894,7 @@ - Fully transparent pixels will be rendered as black pixels (the Pokémon games do this by default) + <html><head/><body><p>Fully transparent pixels will be rendered as black pixels (the Pokémon games do this by default)</p></body></html> Render as black @@ -804,7 +904,7 @@ - Fully transparent pixels will be rendered using the first palette color (this the default behavior for the GBA) + <html><head/><body><p>Fully transparent pixels will be rendered using the first palette color (this the default behavior for the GBA)</p></body></html> Render using first palette color @@ -830,7 +930,7 @@ - This raw tile value will be used to fill the unused bottom layer of Normal metatiles + <html><head/><body><p>This raw tile value will be used to fill the unused bottom layer of Normal metatiles</p></body></html> @@ -844,7 +944,7 @@ - This raw tile value will be used to fill the unused top layer of Covered metatiles + <html><head/><body><p>This raw tile value will be used to fill the unused top layer of Covered metatiles</p></body></html> @@ -858,7 +958,7 @@ - This raw tile value will be used to fill the unused middle layer of Split metatiles + <html><head/><body><p>This raw tile value will be used to fill the unused middle layer of Split metatiles</p></body></html> @@ -902,22 +1002,19 @@ - The mask used to read/write Layer Type from the metatile's attributes data. If 0, this attribute is disabled. + <html><head/><body><p>The mask used to read/write Layer Type from the metatile's attributes data. If 0, this attribute is disabled.</p></body></html> - The mask used to read/write Metatile Behavior from the metatile's attributes data. If 0, this attribute is disabled. + <html><head/><body><p>The mask used to read/write Metatile Behavior from the metatile's attributes data. If 0, this attribute is disabled.</p></body></html> - - The number of bytes used per metatile for metatile attributes - Attributes size (in bytes) @@ -948,6 +1045,9 @@ + + <html><head/><body><p>If checked, metatiles will be interpreted as having 3 layers of 4 tiles each (12 tiles total) as opposed to the default 2 layers of 4 tiles each (8 total).</p></body></html> + Enable Triple Layer Metatiles @@ -956,7 +1056,7 @@ - The mask used to read/write Terrain Type from the metatile's attributes data. If 0, this attribute is disabled. + <html><head/><body><p>The mask used to read/write Terrain Type from the metatile's attributes data. If 0, this attribute is disabled.</p></body></html> @@ -983,7 +1083,7 @@ - The mask used to read/write Encounter Type from the metatile's attributes data. If 0, this attribute is disabled. + <html><head/><body><p>The mask used to read/write Encounter Type from the metatile's attributes data. If 0, this attribute is disabled.</p></body></html> @@ -996,6 +1096,9 @@ + + <html><head/><body><p>The number of bytes each metatile has for metatile attributes. This is the metadata about each metatile like behvior, layer type, etc.</p></body></html> + false @@ -1036,7 +1139,7 @@ - Whether the C data outputted for new tilesets will include the "callback" field + <html><head/><body><p>Whether the C data outputted for new tilesets will include the &quot;callback&quot; field</p></body></html> Output 'callback' field @@ -1046,7 +1149,7 @@ - Whether the C data outputted for new tilesets will include the "isCompressed" field + <html><head/><body><p>Whether the C data outputted for new tilesets will include the &quot;isCompressed&quot; field</p></body></html> Output 'isCompressed' field @@ -1093,7 +1196,7 @@ 0 0 - 559 + 570 840 @@ -1121,7 +1224,7 @@ - The icon that will be used to represent Warp events + <html><head/><body><p>The icon that will be used to represent Warp events</p></body></html> true @@ -1131,7 +1234,7 @@ - The icon that will be used to represent Heal Location events + <html><head/><body><p>The icon that will be used to represent Heal Location events</p></body></html> true @@ -1155,7 +1258,7 @@ - The icon that will be used to represent Object events that don't have their own sprite + <html><head/><body><p>The icon that will be used to represent Object events that don't have their own sprite</p></body></html> true @@ -1172,7 +1275,7 @@ - The icon that will be used to represent Trigger events + <html><head/><body><p>The icon that will be used to represent Trigger events</p></body></html> true @@ -1182,7 +1285,7 @@ - The icon that will be used to represent BG events + <html><head/><body><p>The icon that will be used to represent BG events</p></body></html> true @@ -1256,7 +1359,7 @@ - Remove the current text from the list + <html><head/><body><p>Remove the current text from the list</p></body></html> ... @@ -1280,7 +1383,7 @@ - If checked, Warp Events will not display a warning about incompatible metatile behaviors + <html><head/><body><p>If checked, Warp Events will not display a warning about incompatible metatile behaviors</p></body></html> Disable Warning @@ -1291,9 +1394,9 @@ - + - Metatile Behaviors on this list won't trigger warnings for Warp Events + <html><head/><body><p>Metatile Behaviors on this list won't trigger warnings for Warp Events</p></body></html> true @@ -1309,7 +1412,7 @@ - Add the current text to the list + <html><head/><body><p>Add the current text to the list</p></body></html> ... @@ -1475,8 +1578,8 @@ 0 0 - 559 - 490 + 570 + 499 @@ -1522,8 +1625,8 @@ 0 0 - 533 - 428 + 544 + 437 @@ -1564,8 +1667,8 @@ 0 0 - 559 - 490 + 570 + 499 @@ -1611,8 +1714,8 @@ 0 0 - 533 - 428 + 544 + 437 @@ -1661,6 +1764,11 @@ QSpinBox
noscrollspinbox.h
+ + NoScrollTextEdit + QTextEdit +
noscrolltextedit.h
+
UIntSpinBox QAbstractSpinBox diff --git a/forms/regionmappropertiesdialog.ui b/forms/regionmappropertiesdialog.ui index 80e7020a..88b465f4 100644 --- a/forms/regionmappropertiesdialog.ui +++ b/forms/regionmappropertiesdialog.ui @@ -21,7 +21,7 @@
- QFormLayout::AllNonFixedFieldsGrow + QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow @@ -33,7 +33,7 @@ - A nickname for this region map that will differentiate it from others (should be unique). + <html><head/><body><p>A nickname for this region map that will differentiate it from others (should be unique).</p></body></html> @@ -131,7 +131,7 @@
- The height of the tilemap + <html><head/><body><p>The height of the tilemap</p></body></html> 255 @@ -148,10 +148,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Raised + QFrame::Shadow::Raised @@ -218,10 +218,10 @@ <html><head/><body><p>Path to the tilemap binary relative to the project root.</p></body></html> - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Raised + QFrame::Shadow::Raised @@ -269,10 +269,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -392,10 +392,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Raised + QFrame::Shadow::Raised @@ -487,7 +487,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -517,7 +517,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -590,7 +590,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -617,7 +617,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -646,7 +646,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -659,10 +659,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui index 488e066e..8d6668e4 100644 --- a/forms/wildmonchart.ui +++ b/forms/wildmonchart.ui @@ -145,7 +145,7 @@ false - QComboBox::AdjustToMinimumContentsLength + QComboBox::AdjustToMinimumContentsLengthWithIcon 8 diff --git a/include/config.h b/include/config.h index 6746a0f4..67a8b507 100644 --- a/include/config.h +++ b/include/config.h @@ -14,19 +14,20 @@ #include #include "events.h" +#include "gridsettings.h" -static const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION); +extern const QVersionNumber porymapVersion; -// In both versions the default new map border is a generic tree -#define DEFAULT_BORDER_RSE (QList{0x1D4, 0x1D5, 0x1DC, 0x1DD}) -#define DEFAULT_BORDER_FRLG (QList{0x14, 0x15, 0x1C, 0x1D}) +// Distance in pixels from the edge of a GBA screen (240x160) to the center 16x16 pixels. +#define GBA_H_DIST_TO_CENTER ((240-16)/2) +#define GBA_V_DIST_TO_CENTER ((160-16)/2) #define CONFIG_BACKWARDS_COMPATABILITY class KeyValueConfigBase { public: - void save(); + bool save(); void load(); virtual ~KeyValueConfigBase(); virtual void reset() = 0; @@ -36,9 +37,11 @@ protected: virtual QMap getKeyValueMap() = 0; virtual void init() = 0; virtual void setUnreadKeys() = 0; - bool getConfigBool(QString key, QString value); - int getConfigInteger(QString key, QString value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0); - uint32_t getConfigUint32(QString key, QString value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0); + + 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); }; class PorymapConfig: public KeyValueConfigBase @@ -92,6 +95,7 @@ public: this->rateLimitTimes.clear(); this->eventSelectionShapeMode = QGraphicsPixmapItem::MaskShape; this->shownInGameReloadMessage = false; + this->gridSettings = GridSettings(); } void addRecentProject(QString project); void setRecentProjects(QStringList projects); @@ -156,6 +160,7 @@ public: QByteArray newMapDialogGeometry; QByteArray newLayoutDialogGeometry; bool shownInGameReloadMessage; + GridSettings gridSettings; protected: virtual QString getConfigFilepath() override; @@ -214,6 +219,8 @@ enum ProjectIdentifier { define_pals_total, define_tiles_per_metatile, define_map_size, + define_map_offset_width, + define_map_offset_height, define_mask_metatile, define_mask_collision, define_mask_elevation, @@ -317,6 +324,7 @@ public: this->defaultMetatileId = 1; this->defaultElevation = 3; this->defaultCollision = 0; + this->defaultMapSize = QSize(20,20); this->defaultPrimaryTileset = "gTileset_General"; this->prefabFilepath = QString(); this->prefabImportPrompted = false; @@ -328,8 +336,8 @@ public: this->eventIconPaths.clear(); this->pokemonIconPaths.clear(); this->collisionSheetPath = QString(); - this->collisionSheetWidth = 2; - this->collisionSheetHeight = 16; + this->collisionSheetSize = QSize(2, 16); + this->playerViewDistance = QMargins(GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER, GBA_H_DIST_TO_CENTER, GBA_V_DIST_TO_CENTER); this->blockMetatileIdMask = 0x03FF; this->blockCollisionMask = 0x0C00; this->blockElevationMask = 0xF000; @@ -382,6 +390,7 @@ public: uint16_t defaultMetatileId; uint16_t defaultElevation; uint16_t defaultCollision; + QSize defaultMapSize; QList newMapBorderMetatileIds; QString defaultPrimaryTileset; QString defaultSecondaryTileset; @@ -404,8 +413,8 @@ public: uint16_t unusedTileSplit; bool mapAllowFlagsEnabled; QString collisionSheetPath; - int collisionSheetWidth; - int collisionSheetHeight; + QSize collisionSheetSize; + QMargins playerViewDistance; QList warpBehaviors; int maxEventsPerGroup; diff --git a/include/core/map.h b/include/core/map.h index 1f8b5da5..84791fa3 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -22,10 +22,6 @@ #define MAX_BORDER_WIDTH 255 #define MAX_BORDER_HEIGHT 255 -// Number of metatiles to draw out from edge of map. Could allow modification of this in the future. -// porymap will reflect changes to it, but the value is hard-coded in the projects at the moment -#define BORDER_DISTANCE 7 - class LayoutPixmapItem; class CollisionPixmapItem; class BorderMetatilesPixmapItem; @@ -89,6 +85,7 @@ public: void deleteConnections(); QList getConnections() const { return m_connections; } + MapConnection* getConnection(const QString &direction) const; void removeConnection(MapConnection *); void addConnection(MapConnection *); void loadConnection(MapConnection *); diff --git a/include/core/maplayout.h b/include/core/maplayout.h index 40a3a035..6e82604a 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -96,8 +96,8 @@ public: int getHeight() const { return height; } int getBorderWidth() const { return border_width; } int getBorderHeight() const { return border_height; } - int getBorderDrawWidth() const; - int getBorderDrawHeight() const; + QMargins getBorderMargins() const; + QRect getVisibleRect() const; bool isWithinBounds(int x, int y) const; bool isWithinBounds(const QRect &rect) const; @@ -116,9 +116,12 @@ public: void clearBorderCache(); void cacheBorder(); - void setClean(); bool hasUnsavedChanges() const; + bool save(const QString &root); + bool saveBorder(const QString &root); + bool saveBlockdata(const QString &root); + bool layoutBlockChanged(int i, const Blockdata &cache); uint16_t getBorderMetatileId(int x, int y); @@ -143,6 +146,7 @@ public: private: void setNewDimensionsBlockdata(int newWidth, int newHeight); void setNewBorderDimensionsBlockdata(int newWidth, int newHeight); + bool writeBlockdata(const QString &path, const Blockdata &blockdata) const; static int getBorderDrawDistance(int dimension, qreal minimum); diff --git a/include/core/paletteutil.h b/include/core/paletteutil.h index ce221026..34e9ae3f 100644 --- a/include/core/paletteutil.h +++ b/include/core/paletteutil.h @@ -7,7 +7,7 @@ namespace PaletteUtil { QList parse(QString filepath, bool *error); - void writeJASC(QString filepath, QVector colors, int offset, int nColors); + bool writeJASC(const QString &filepath, const QVector &colors, int offset, int nColors); } #endif // PALETTEUTIL_H diff --git a/include/core/tileset.h b/include/core/tileset.h index 32d18858..a05afdc3 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -55,17 +55,17 @@ public: static QString getExpectedDir(QString tilesetName, bool isSecondary); QString getExpectedDir(); - void load(); - void loadMetatiles(); - void loadMetatileAttributes(); - void loadTilesImage(QImage *importedImage = nullptr); - void loadPalettes(); + bool load(); + bool loadMetatiles(); + bool loadMetatileAttributes(); + bool loadTilesImage(QImage *importedImage = nullptr); + bool loadPalettes(); - void save(); - void saveMetatileAttributes(); - void saveMetatiles(); - void saveTilesImage(); - void savePalettes(); + bool save(); + bool saveMetatileAttributes(); + bool saveMetatiles(); + bool saveTilesImage(); + bool savePalettes(); bool appendToHeaders(QString root, QString friendlyName, bool usingAsm); bool appendToGraphics(QString root, QString friendlyName, bool usingAsm); diff --git a/include/core/utility.h b/include/core/utility.h index 6613ee71..09caebce 100644 --- a/include/core/utility.h +++ b/include/core/utility.h @@ -9,6 +9,7 @@ namespace Util { int roundUp(int numToRound, int multiple); QString toDefineCase(QString input); QString toHexString(uint32_t value, int minLength = 0); + QString toHtmlParagraph(const QString &text); Qt::Orientations getOrientation(bool xflip, bool yflip); } diff --git a/include/editor.h b/include/editor.h index fe01014f..432084e8 100644 --- a/include/editor.h +++ b/include/editor.h @@ -57,8 +57,8 @@ public: GridSettings gridSettings; void setProject(Project * project); - void saveAll(); - void saveCurrent(); + bool saveAll(); + bool saveCurrent(); void saveEncounterTabData(); void closeProject(); @@ -92,14 +92,16 @@ public: void setConnectionsVisibility(bool visible); void updateDivingMapsVisibility(); void renderDivingConnections(); - void addConnection(MapConnection* connection); + void addNewConnection(const QString &mapName, const QString &direction); + void replaceConnection(const QString &mapName, const QString &direction); void removeConnection(MapConnection* connection); + void removeSelectedConnection(); void addNewWildMonGroup(QWidget *window); void deleteWildMonGroup(); void configureEncounterJSON(QWidget *); EncounterTableModel* getCurrentWildMonTable(); - void updateDiveMap(QString mapName); - void updateEmergeMap(QString mapName); + bool setDivingMapName(const QString &mapName, const QString &direction); + QString getDivingMapName(const QString &direction) const; void setSelectedConnection(MapConnection *connection); void updatePrimaryTileset(QString tilesetLabel, bool forceLoad = false); @@ -120,6 +122,7 @@ public: void updateEventPixmapItemZValue(EventPixmapItem *item); qreal getEventOpacity(const Event *event) const; + void setPlayerViewRect(const QRectF &rect); void updateCursorRectPos(int x, int y); void setCursorRectVisible(bool visible); @@ -215,7 +218,7 @@ private: EditMode editMode = EditMode::None; - void save(bool currentOnly); + bool save(bool currentOnly); void clearMap(); void clearMetatileSelector(); void clearMovementPermissionSelector(); @@ -234,8 +237,9 @@ private: void removeConnectionPixmap(MapConnection *connection); void displayConnection(MapConnection *connection); void displayDivingConnection(MapConnection *connection); - void setDivingMapName(QString mapName, QString direction); void removeDivingMapPixmap(MapConnection *connection); + void onDivingMapEditingFinished(NoScrollComboBox* combo, const QString &direction); + void updateDivingMapButton(QToolButton* button, const QString &mapName); void updateEncounterFields(EncounterFields newFields); QString getMovementPermissionText(uint16_t collision, uint16_t elevation); QString getMetatileDisplayMessage(uint16_t metatileId); diff --git a/include/mainwindow.h b/include/mainwindow.h index 4d33847d..683df61f 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -175,7 +175,7 @@ private slots: void on_action_Reload_Project_triggered(); void on_action_Close_Project_triggered(); void on_action_Save_Project_triggered(); - void save(bool currentOnly = false); + bool save(bool currentOnly = false); void openEventMap(Event *event); @@ -242,8 +242,6 @@ private slots: void on_pushButton_AddConnection_clicked(); void on_button_OpenDiveMap_clicked(); void on_button_OpenEmergeMap_clicked(); - void on_comboBox_DiveMap_currentTextChanged(const QString &mapName); - void on_comboBox_EmergeMap_currentTextChanged(const QString &mapName); void on_comboBox_PrimaryTileset_currentTextChanged(const QString &arg1); void on_comboBox_SecondaryTileset_currentTextChanged(const QString &arg1); void on_pushButton_ChangeDimensions_clicked(); diff --git a/include/project.h b/include/project.h index c5aed0a3..1031640b 100644 --- a/include/project.h +++ b/include/project.h @@ -108,10 +108,6 @@ public: bool loadBlockdata(Layout *); bool loadLayoutBorder(Layout *); - void saveTextFile(QString path, QString text); - void appendTextFile(QString path, QString text); - void deleteFile(QString path); - bool readMapGroups(); void addNewMapGroup(const QString &groupName); QString mapNameToMapGroup(const QString &mapName) const; @@ -168,25 +164,20 @@ public: bool loadLayout(Layout *); bool loadMapLayout(Map*); bool loadLayoutTilesets(Layout *); - void loadTilesetAssets(Tileset*); + bool loadTilesetAssets(Tileset*); void loadTilesetMetatileLabels(Tileset*); void readTilesetPaths(Tileset* tileset); - void saveAll(); - void saveGlobalData(); - void saveLayout(Layout *); - void saveLayoutBlockdata(Layout *); - void saveLayoutBorder(Layout *); - void writeBlockdata(QString, const Blockdata &); - void saveMap(Map *map, bool skipLayout = false); - void saveConfig(); - void saveMapLayouts(); - void saveMapGroups(); - void saveRegionMapSections(); - void saveWildMonData(); - void saveHealLocations(); - void saveTilesets(Tileset*, Tileset*); - void saveTilesetMetatileLabels(Tileset*, Tileset*); + bool saveAll(); + bool saveGlobalData(); + bool saveConfig(); + bool saveLayout(Layout *layout); + bool saveMap(Map *map, bool skipLayout = false); + bool saveTextFile(const QString &path, const QString &text); + bool saveRegionMapSections(); + bool saveTilesets(Tileset*, Tileset*); + bool saveTilesetMetatileLabels(Tileset*, Tileset*); + void appendTilesetLabel(const QString &label, const QString &isSecondaryStr); bool readTilesetLabels(); bool readTilesetMetatileLabels(); @@ -235,30 +226,34 @@ public: static QString getExistingFilepath(QString filepath); void applyParsedLimits(); - + void setRegionMapEntries(const QHash &entries); QHash getRegionMapEntries() const; QSet getTopLevelMapFields() const; + int getMapDataSize(int width, int height) const; + int getMaxMapDataSize() const { return this->maxMapDataSize; } + int getMaxMapWidth() const; + int getMaxMapHeight() const; + bool mapDimensionsValid(int width, int height) const; + bool calculateDefaultMapSize(); + QSize getDefaultMapSize() const { return this->defaultMapSize; } + QSize getMapSizeAddition() const { return this->mapSizeAddition; } + + int getMaxEvents(Event::Group group) const; + static QString getEmptyMapDefineName(); static QString getDynamicMapDefineName(); static QString getDynamicMapName(); static QString getEmptySpeciesName(); - static int getNumTilesPrimary(); - static int getNumTilesTotal(); - static int getNumMetatilesPrimary(); - static int getNumMetatilesTotal(); - static int getNumPalettesPrimary(); - static int getNumPalettesTotal(); - static int getMaxMapDataSize(); - static int getDefaultMapDimension(); - static int getMaxMapWidth(); - static int getMaxMapHeight(); - static int getMapDataSize(int width, int height); - static bool mapDimensionsValid(int width, int height); - bool calculateDefaultMapSize(); - int getMaxEvents(Event::Group group); + static QMargins getMetatileViewDistance(); + static int getNumTilesPrimary() { return num_tiles_primary; } + static int getNumTilesTotal() { return num_tiles_total; } + static int getNumMetatilesPrimary() { return num_metatiles_primary; } + static int getNumMetatilesTotal() { return Block::getMaxMetatileId() + 1; } + static int getNumPalettesPrimary(){ return num_pals_primary; } + static int getNumPalettesTotal() { return num_pals_total; } static QString getEmptyMapsecName(); static QString getMapGroupPrefix(); @@ -309,8 +304,6 @@ private: }; QHash locationData; - void updateLayout(Layout *); - void setNewLayoutBlockdata(Layout *layout); void setNewLayoutBorder(Layout *layout); @@ -318,17 +311,27 @@ private: void recordFileChange(const QString &filepath); void resetFileCache(); + bool saveMapLayouts(); + bool saveMapGroups(); + bool saveWildMonData(); + bool saveHealLocations(); + bool appendTextFile(const QString &path, const QString &text); + QString findSpeciesIconPath(const QStringList &names) const; - int maxEventsPerGroup; int maxObjectEvents; + int maxMapDataSize; + QSize defaultMapSize; + QSize mapSizeAddition; + + // TODO: These really shouldn't be static, they're specific to a single project. + // We're making an assumption here that we only have one project open at a single time + // (which is true, but then if that's the case we should have some global Project instance instead) static int num_tiles_primary; static int num_tiles_total; static int num_metatiles_primary; static int num_pals_primary; static int num_pals_total; - static int max_map_data_size; - static int default_map_dimension; signals: void fileChanged(const QString &filepath); diff --git a/include/ui/connectionpixmapitem.h b/include/ui/connectionpixmapitem.h index 26b83aa6..32e309f9 100644 --- a/include/ui/connectionpixmapitem.h +++ b/include/ui/connectionpixmapitem.h @@ -43,8 +43,6 @@ protected: virtual void mousePressEvent(QGraphicsSceneMouseEvent*) override; virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; virtual void mouseDoubleClickEvent(QGraphicsSceneMouseEvent*) override; - virtual void keyPressEvent(QKeyEvent*) override; - virtual void focusInEvent(QFocusEvent*) override; signals: void connectionItemDoubleClicked(MapConnection*); diff --git a/include/ui/connectionslistitem.h b/include/ui/connectionslistitem.h index b63922a9..1b9713cf 100644 --- a/include/ui/connectionslistitem.h +++ b/include/ui/connectionslistitem.h @@ -36,19 +36,17 @@ private: protected: virtual void mousePressEvent(QMouseEvent*) override; - virtual void focusInEvent(QFocusEvent*) override; - virtual void keyPressEvent(QKeyEvent*) override; + virtual bool eventFilter(QObject*, QEvent *event) override; signals: void selected(); void openMapClicked(MapConnection*); -private slots: - void on_comboBox_Direction_currentTextChanged(QString direction); - void on_comboBox_Map_currentTextChanged(QString mapName); - void on_spinBox_Offset_valueChanged(int offset); - void on_button_Delete_clicked(); - void on_button_OpenMap_clicked(); +private: + void commitDirection(); + void commitMap(const QString &mapName); + void commitMove(int offset); + void commitRemove(); }; #endif // CONNECTIONSLISTITEM_H diff --git a/include/ui/graphicsview.h b/include/ui/graphicsview.h index cac812b2..c587d2a9 100644 --- a/include/ui/graphicsview.h +++ b/include/ui/graphicsview.h @@ -32,4 +32,17 @@ signals: void clicked(QMouseEvent *event); }; +class ConnectionsView : public QGraphicsView +{ + Q_OBJECT +public: + ConnectionsView(QWidget *parent = nullptr) : QGraphicsView(parent) {} + +signals: + void pressedDelete(); + +protected: + virtual void keyPressEvent(QKeyEvent *event) override; +}; + #endif // GRAPHICSVIEW_H diff --git a/include/ui/mapheaderform.h b/include/ui/mapheaderform.h index 7f246d6d..4f3dc775 100644 --- a/include/ui/mapheaderform.h +++ b/include/ui/mapheaderform.h @@ -64,6 +64,8 @@ private: QPointer m_project = nullptr; bool m_allowProjectChanges = true; + void setText(QComboBox *combo, const QString &text) const; + void setText(QLineEdit *lineEdit, const QString &text) const; void setLocations(const QStringList &locations); void updateLocationName(); diff --git a/include/ui/movablerect.h b/include/ui/movablerect.h index 56798a0c..21edd21d 100644 --- a/include/ui/movablerect.h +++ b/include/ui/movablerect.h @@ -10,27 +10,28 @@ class MovableRect : public QGraphicsRectItem { public: - MovableRect(bool *enabled, int width, int height, QRgb color); + MovableRect(bool *enabled, const QRectF &rect, const QRgb &color); QRectF boundingRect() const override { qreal penWidth = 4; return QRectF(-penWidth, -penWidth, - 30 * 8 + penWidth * 2, - 20 * 8 + penWidth * 2); + this->rect().width() + penWidth * 2, + this->rect().height() + penWidth * 2); } void paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override { if (!(*enabled)) return; painter->setPen(this->color); - painter->drawRect(this->rect().x() - 2, this->rect().y() - 2, this->rect().width() + 3, this->rect().height() + 3); - painter->setPen(QColor(0, 0, 0)); - painter->drawRect(this->rect().x() - 3, this->rect().y() - 3, this->rect().width() + 5, this->rect().height() + 5); - painter->drawRect(this->rect().x() - 1, this->rect().y() - 1, this->rect().width() + 1, this->rect().height() + 1); + painter->drawRect(this->rect() + QMargins(1,1,1,1)); // Fill + painter->setPen(Qt::black); + painter->drawRect(this->rect() + QMargins(2,2,2,2)); // Outer border + painter->drawRect(this->rect()); // Inner border } void updateLocation(int x, int y); bool *enabled; protected: + QRectF baseRect; QRgb color; }; diff --git a/include/ui/newmapconnectiondialog.h b/include/ui/newmapconnectiondialog.h index 4781c971..db9eee49 100644 --- a/include/ui/newmapconnectiondialog.h +++ b/include/ui/newmapconnectiondialog.h @@ -20,13 +20,16 @@ public: virtual void accept() override; signals: - void accepted(MapConnection *result); + void newConnectionedAdded(const QString &mapName, const QString &direction); + void connectionReplaced(const QString &mapName, const QString &direction); private: Ui::NewMapConnectionDialog *ui; + Map *m_map; bool mapNameIsValid(); void setWarningVisible(bool visible); + bool askReplaceConnection(MapConnection *connection, const QString &newMapName); }; #endif // NEWMAPCONNECTIONDIALOG_H diff --git a/include/ui/noscrollcombobox.h b/include/ui/noscrollcombobox.h index 32966b3a..0ae2487c 100644 --- a/include/ui/noscrollcombobox.h +++ b/include/ui/noscrollcombobox.h @@ -18,6 +18,9 @@ public: void setLineEdit(QLineEdit *edit); void setFocusedScrollingEnabled(bool enabled); +signals: + void editingFinished(); + private: void setItem(int index, const QString &text); diff --git a/include/ui/noscrolltextedit.h b/include/ui/noscrolltextedit.h new file mode 100644 index 00000000..dfc66789 --- /dev/null +++ b/include/ui/noscrolltextedit.h @@ -0,0 +1,25 @@ +#ifndef NOSCROLLTEXTEDIT_H +#define NOSCROLLTEXTEDIT_H + +#include +#include + +class NoScrollTextEdit : public QTextEdit +{ + Q_OBJECT +public: + explicit NoScrollTextEdit(const QString &text, QWidget *parent = nullptr) : QTextEdit(text, parent) { + setFocusPolicy(Qt::StrongFocus); + }; + explicit NoScrollTextEdit(QWidget *parent = nullptr) : NoScrollTextEdit(QString(), parent) {}; + + virtual void wheelEvent(QWheelEvent *event) override { + if (hasFocus()) { + QTextEdit::wheelEvent(event); + } else { + event->ignore(); + } + }; +}; + +#endif // NOSCROLLTEXTEDIT_H diff --git a/include/ui/tilemaptileselector.h b/include/ui/tilemaptileselector.h index 5c3b8dac..155957a6 100644 --- a/include/ui/tilemaptileselector.h +++ b/include/ui/tilemaptileselector.h @@ -149,10 +149,10 @@ public: void select(unsigned tileId); unsigned selectedTile = 0; - void selectVFlip(bool hFlip) { this->tile_hFlip = hFlip; } + void selectHFlip(bool hFlip) { this->tile_hFlip = hFlip; } bool tile_hFlip = false; - void selectHFlip(bool vFlip) { this->tile_vFlip = vFlip; } + void selectVFlip(bool vFlip) { this->tile_vFlip = vFlip; } bool tile_vFlip = false; void selectPalette(int palette) { diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index fdd4751c..b6a60a61 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -71,8 +71,6 @@ private slots: void on_spinBox_paletteSelector_valueChanged(int arg1); - void on_actionSave_Tileset_triggered(); - void on_actionImport_Primary_Tiles_triggered(); void on_actionImport_Secondary_Tiles_triggered(); @@ -173,6 +171,8 @@ private: bool lockSelection = false; QSet metatileReloadQueue; + bool save(); + signals: void tilesetsSaved(QString, QString); }; diff --git a/porymap.pro b/porymap.pro index 7374ac30..d8505e11 100644 --- a/porymap.pro +++ b/porymap.pro @@ -223,6 +223,7 @@ HEADERS += include/core/advancemapparser.h \ include/ui/newmapgroupdialog.h \ include/ui/noscrollcombobox.h \ include/ui/noscrollspinbox.h \ + include/ui/noscrolltextedit.h \ include/ui/montabwidget.h \ include/ui/encountertablemodel.h \ include/ui/encountertabledelegates.h \ diff --git a/src/config.cpp b/src/config.cpp index 5c5feb54..7e7422c7 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -18,6 +18,12 @@ #include #include +const QVersionNumber porymapVersion = QVersionNumber::fromString(PORYMAP_VERSION); + +// In both versions the default new map border is a generic tree +const QList defaultBorder_RSE = {0x1D4, 0x1D5, 0x1DC, 0x1DD}; +const QList defaultBorder_FRLG = {0x14, 0x15, 0x1C, 0x1D}; + const QList defaultWarpBehaviors_RSE = { 0x0E, // MB_MOSSDEEP_GYM_WARP 0x0F, // MB_MT_PYRE_HOLE @@ -89,6 +95,8 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_pals_total, {"define_pals_total", "NUM_PALS_TOTAL"}}, {ProjectIdentifier::define_tiles_per_metatile, {"define_tiles_per_metatile", "NUM_TILES_PER_METATILE"}}, {ProjectIdentifier::define_map_size, {"define_map_size", "MAX_MAP_DATA_SIZE"}}, + {ProjectIdentifier::define_map_offset_width, {"define_map_offset_width", "MAP_OFFSET_W"}}, + {ProjectIdentifier::define_map_offset_height, {"define_map_offset_height", "MAP_OFFSET_H"}}, {ProjectIdentifier::define_mask_metatile, {"define_mask_metatile", "MAPGRID_METATILE_ID_MASK"}}, {ProjectIdentifier::define_mask_collision, {"define_mask_collision", "MAPGRID_COLLISION_MASK"}}, {ProjectIdentifier::define_mask_elevation, {"define_mask_elevation", "MAPGRID_ELEVATION_MASK"}}, @@ -233,7 +241,7 @@ void KeyValueConfigBase::load() { file.close(); } -void KeyValueConfigBase::save() { +bool KeyValueConfigBase::save() { QString text = ""; QMap map = this->getKeyValueMap(); for (QMap::iterator it = map.begin(); it != map.end(); it++) { @@ -241,15 +249,17 @@ void KeyValueConfigBase::save() { } QFile file(this->getConfigFilepath()); - if (file.open(QIODevice::WriteOnly)) { - file.write(text.toUtf8()); - file.close(); - } else { + if (!file.open(QIODevice::WriteOnly)) { logError(QString("Could not open config file '%1' for writing: ").arg(this->getConfigFilepath()) + file.errorString()); + return false; } + + file.write(text.toUtf8()); + file.close(); + return true; } -bool KeyValueConfigBase::getConfigBool(QString key, QString value) { +bool KeyValueConfigBase::getConfigBool(const QString &key, const QString &value) { bool ok; int result = value.toInt(&ok, 0); if (!ok || (result != 0 && result != 1)) { @@ -258,26 +268,35 @@ bool KeyValueConfigBase::getConfigBool(QString key, QString value) { return (result != 0); } -int KeyValueConfigBase::getConfigInteger(QString key, QString value, int min, int max, int defaultValue) { +int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &value, int min, int max, int defaultValue) { bool ok; int result = value.toInt(&ok, 0); if (!ok) { - logWarn(QString("Invalid config value for %1: '%2'. Must be an integer.").arg(key).arg(value)); - return defaultValue; + 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)); } -uint32_t KeyValueConfigBase::getConfigUint32(QString key, QString value, uint32_t min, uint32_t max, uint32_t defaultValue) { +uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) { bool ok; uint32_t result = value.toUInt(&ok, 0); if (!ok) { - logWarn(QString("Invalid config value for %1: '%2'. Must be an integer.").arg(key).arg(value)); - return defaultValue; + 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)); } +QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) { + 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())); + color = defaultValue; + } + return color; +} + PorymapConfig porymapConfig; QString PorymapConfig::getConfigFilepath() { @@ -445,6 +464,18 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { } } else if (key == "shown_in_game_reload_message") { this->shownInGameReloadMessage = getConfigBool(key, value); + } else if (key == "grid_width") { + this->gridSettings.width = getConfigUint32(key, value); + } else if (key == "grid_height") { + this->gridSettings.height = getConfigUint32(key, value); + } else if (key == "grid_x") { + this->gridSettings.offsetX = getConfigInteger(key, value, 0, 999); + } else if (key == "grid_y") { + this->gridSettings.offsetY = getConfigInteger(key, value, 0, 999); + } else if (key == "grid_style") { + this->gridSettings.style = GridSettings::getStyleFromName(value); + } else if (key == "grid_color") { + this->gridSettings.color = getConfigColor(key, value); } else { logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); } @@ -522,6 +553,12 @@ QMap PorymapConfig::getKeyValueMap() { } map.insert("event_selection_shape_mode", (this->eventSelectionShapeMode == QGraphicsPixmapItem::MaskShape) ? "mask" : "bounding_rect"); map.insert("shown_in_game_reload_message", this->shownInGameReloadMessage ? "1" : "0"); + map.insert("grid_width", QString::number(this->gridSettings.width)); + map.insert("grid_height", QString::number(this->gridSettings.height)); + 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. return map; } @@ -737,6 +774,10 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { this->defaultElevation = getConfigUint32(key, value, 0, Block::maxValue); } else if (key == "default_collision") { this->defaultCollision = getConfigUint32(key, value, 0, Block::maxValue); + } else if (key == "default_map_width") { + this->defaultMapSize.setWidth(getConfigInteger(key, value, 1)); + } else if (key == "default_map_height") { + this->defaultMapSize.setHeight(getConfigInteger(key, value, 1)); } else if (key == "new_map_border_metatiles") { this->newMapBorderMetatileIds.clear(); QList metatileIds = value.split(","); @@ -826,9 +867,17 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) { } else if (key == "collision_sheet_path") { this->collisionSheetPath = value; } else if (key == "collision_sheet_width") { - this->collisionSheetWidth = getConfigUint32(key, value, 1, Block::maxValue); + this->collisionSheetSize.setWidth(getConfigInteger(key, value, 1, Block::maxValue)); } else if (key == "collision_sheet_height") { - this->collisionSheetHeight = getConfigUint32(key, value, 1, Block::maxValue); + this->collisionSheetSize.setHeight(getConfigInteger(key, value, 1, Block::maxValue)); + } else if (key == "player_view_north") { + this->playerViewDistance.setTop(getConfigInteger(key, value, 0, INT_MAX, GBA_V_DIST_TO_CENTER)); + } else if (key == "player_view_south") { + this->playerViewDistance.setBottom(getConfigInteger(key, value, 0, INT_MAX, GBA_V_DIST_TO_CENTER)); + } else if (key == "player_view_west") { + this->playerViewDistance.setLeft(getConfigInteger(key, value, 0, INT_MAX, GBA_H_DIST_TO_CENTER)); + } else if (key == "player_view_east") { + this->playerViewDistance.setRight(getConfigInteger(key, value, 0, INT_MAX, GBA_H_DIST_TO_CENTER)); } else if (key == "warp_behaviors") { this->warpBehaviors.clear(); value.remove(" "); @@ -862,7 +911,7 @@ void ProjectConfig::setUnreadKeys() { if (!readKeys.contains("enable_event_clone_object")) this->eventCloneObjectEnabled = isPokefirered; if (!readKeys.contains("enable_floor_number")) this->floorNumberEnabled = isPokefirered; if (!readKeys.contains("create_map_text_file")) this->createMapTextFileEnabled = (this->baseGameVersion != BaseGameVersion::pokeemerald); - if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? DEFAULT_BORDER_FRLG : DEFAULT_BORDER_RSE; + if (!readKeys.contains("new_map_border_metatiles")) this->newMapBorderMetatileIds = isPokefirered ? defaultBorder_FRLG : defaultBorder_RSE; if (!readKeys.contains("default_secondary_tileset")) this->defaultSecondaryTileset = isPokefirered ? "gTileset_PalletTown" : "gTileset_Petalburg"; if (!readKeys.contains("metatile_attributes_size")) this->metatileAttributesSize = Metatile::getDefaultAttributesSize(this->baseGameVersion); if (!readKeys.contains("metatile_behavior_mask")) this->metatileBehaviorMask = Metatile::getDefaultAttributesMask(this->baseGameVersion, Metatile::Attr::Behavior); @@ -890,6 +939,8 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("default_metatile", Metatile::getMetatileIdString(this->defaultMetatileId)); map.insert("default_elevation", QString::number(this->defaultElevation)); map.insert("default_collision", QString::number(this->defaultCollision)); + map.insert("default_map_width", QString::number(this->defaultMapSize.width())); + map.insert("default_map_height", QString::number(this->defaultMapSize.height())); map.insert("new_map_border_metatiles", Metatile::getMetatileIdStrings(this->newMapBorderMetatileIds)); map.insert("default_primary_tileset", this->defaultPrimaryTileset); map.insert("default_secondary_tileset", this->defaultSecondaryTileset); @@ -927,8 +978,12 @@ QMap ProjectConfig::getKeyValueMap() { map.insert("ident/"+defaultIdentifiers.value(i.key()).first, i.value()); } map.insert("collision_sheet_path", this->collisionSheetPath); - map.insert("collision_sheet_width", QString::number(this->collisionSheetWidth)); - map.insert("collision_sheet_height", QString::number(this->collisionSheetHeight)); + map.insert("collision_sheet_width", QString::number(this->collisionSheetSize.width())); + map.insert("collision_sheet_height", QString::number(this->collisionSheetSize.height())); + map.insert("player_view_north", QString::number(this->playerViewDistance.top())); + map.insert("player_view_south", QString::number(this->playerViewDistance.bottom())); + map.insert("player_view_west", QString::number(this->playerViewDistance.left())); + map.insert("player_view_east", QString::number(this->playerViewDistance.right())); QStringList warpBehaviorStrs; for (const auto &value : this->warpBehaviors) warpBehaviorStrs.append("0x" + QString("%1").arg(value, 2, 16, QChar('0')).toUpper()); diff --git a/src/core/events.cpp b/src/core/events.cpp index ad50a60e..c902db32 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -777,6 +777,7 @@ Event *HealLocationEvent::duplicate() const { copy->setX(this->getX()); copy->setY(this->getY()); copy->setIdName(this->getIdName()); + copy->setHostMapName(this->getHostMapName()); copy->setRespawnMapName(this->getRespawnMapName()); copy->setRespawnNPC(this->getRespawnNPC()); diff --git a/src/core/map.cpp b/src/core/map.cpp index caff07db..3cd087e1 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -83,16 +83,17 @@ QRect Map::getConnectionRect(const QString &direction, Layout * fromLayout) cons int x = 0, y = 0; int w = getWidth(), h = getHeight(); + QMargins viewDistance = Project::getMetatileViewDistance(); if (direction == "up") { - h = qMin(h, BORDER_DISTANCE); + h = qMin(h, viewDistance.top()); y = getHeight() - h; } else if (direction == "down") { - h = qMin(h, BORDER_DISTANCE); + h = qMin(h, viewDistance.bottom()); } else if (direction == "left") { - w = qMin(w, BORDER_DISTANCE); + w = qMin(w, viewDistance.left()); x = getWidth() - w; } else if (direction == "right") { - w = qMin(w, BORDER_DISTANCE); + w = qMin(w, viewDistance.right()); } else if (MapConnection::isDiving(direction)) { if (fromLayout) { w = qMin(w, fromLayout->getWidth()); @@ -331,6 +332,15 @@ void Map::removeConnection(MapConnection *connection) { emit connectionRemoved(connection); } +// Return the first map connection that has the given direction. +MapConnection* Map::getConnection(const QString &direction) const { + for (const auto &connection : m_connections) { + if (connection->direction() == direction) + return connection; + } + return nullptr; +} + void Map::commit(QUndoCommand *cmd) { m_editHistory->push(cmd); } diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 45b35f91..fb0f714c 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -63,17 +63,11 @@ bool Layout::isWithinBorderBounds(int x, int y) const { return (x >= 0 && x < this->getBorderWidth() && y >= 0 && y < this->getBorderHeight()); } -int Layout::getBorderDrawWidth() const { - return getBorderDrawDistance(border_width, BORDER_DISTANCE); -} - -int Layout::getBorderDrawHeight() const { - return getBorderDrawDistance(border_height, BORDER_DISTANCE); -} - -// We need to draw sufficient border blocks to fill the area that gets loaded around the player in-game (BORDER_DISTANCE). -// Note that this is not the same as the player's view distance. -// The result will be some multiple of the input dimension, because we only draw the border in increments of its full width/height. +// Calculate the distance away from the layout's edge that we need to start drawing border blocks. +// We need to fulfill two requirements here: +// - We should draw enough to fill the player's in-game view +// - The value should be some multiple of the border's dimension +// (otherwise the border won't be positioned the same as it would in-game). int Layout::getBorderDrawDistance(int dimension, qreal minimum) { if (dimension >= minimum) return dimension; @@ -81,6 +75,23 @@ int Layout::getBorderDrawDistance(int dimension, qreal minimum) { // Get first multiple of dimension >= the minimum return dimension * qCeil(minimum / qMax(dimension, 1)); } +QMargins Layout::getBorderMargins() const { + QMargins minimum = Project::getMetatileViewDistance(); + QMargins distance; + distance.setTop(getBorderDrawDistance(this->border_height, minimum.top())); + distance.setBottom(getBorderDrawDistance(this->border_height, minimum.bottom())); + distance.setLeft(getBorderDrawDistance(this->border_width, minimum.left())); + distance.setRight(getBorderDrawDistance(this->border_width, minimum.right())); + 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. +QRect Layout::getVisibleRect() const { + QRect area = QRect(0, 0, this->width * 16, this->height * 16); + return area += (Project::getMetatileViewDistance() * 16); +} bool Layout::getBlock(int x, int y, Block *out) { if (isWithinBounds(x, y)) { @@ -467,11 +478,50 @@ QPixmap Layout::getLayoutItemPixmap() { return this->layoutItem ? this->layoutItem->pixmap() : QPixmap(); } -void Layout::setClean() { - this->editHistory.setClean(); - this->hasUnsavedDataChanges = false; -} - bool Layout::hasUnsavedChanges() const { return !this->editHistory.isClean() || this->hasUnsavedDataChanges || !this->newFolderPath.isEmpty(); } + +bool Layout::save(const QString &root) { + if (!this->newFolderPath.isEmpty()) { + // Layout directory doesn't exist yet, create it now. + const QString fullPath = QString("%1/%2").arg(root).arg(this->newFolderPath); + if (!QDir::root().mkpath(fullPath)) { + logError(QString("Failed to create directory for new layout: '%1'").arg(fullPath)); + return false; + } + this->newFolderPath = QString(); + } + + bool success = true; + if (!saveBorder(root)) success = false; + if (!saveBlockdata(root)) success = false; + if (!success) + return false; + + this->editHistory.setClean(); + this->hasUnsavedDataChanges = false; + return true; +} + +bool Layout::saveBorder(const QString &root) { + QString path = QString("%1/%2").arg(root).arg(this->border_path); + return writeBlockdata(path, this->border); +} + +bool Layout::saveBlockdata(const QString &root) { + QString path = QString("%1/%2").arg(root).arg(this->blockdata_path); + return writeBlockdata(path, this->blockdata); +} + +bool Layout::writeBlockdata(const QString &path, const Blockdata &blockdata) const { + QFile file(path); + if (!file.open(QIODevice::WriteOnly)) { + logError(QString("Could not open '%1' for writing: %2").arg(path).arg(file.errorString())); + return false; + } + + QByteArray data = blockdata.serialize(); + file.write(data); + return true; +} diff --git a/src/core/paletteutil.cpp b/src/core/paletteutil.cpp index 929336b2..76a3d0c4 100644 --- a/src/core/paletteutil.cpp +++ b/src/core/paletteutil.cpp @@ -38,14 +38,14 @@ QList PaletteUtil::parse(QString filepath, bool *error) { return QList(); } -void PaletteUtil::writeJASC(QString filepath, QVector palette, int offset, int nColors) { +bool PaletteUtil::writeJASC(const QString &filepath, const QVector &palette, int offset, int nColors) { if (!nColors) { - logWarn(QString("Cannot save a palette with no colors.")); - return; + logError(QString("Cannot save a palette with no colors.")); + return false; } if (offset > palette.size() || offset + nColors > palette.size()) { - logWarn("Palette offset out of range for color table."); - return; + logError("Palette offset out of range for color table."); + return false; } QString text = "JASC-PAL\r\n0100\r\n"; @@ -59,11 +59,13 @@ void PaletteUtil::writeJASC(QString filepath, QVector palette, int offset, } QFile file(filepath); - if (file.open(QIODevice::WriteOnly)) { - file.write(text.toUtf8()); - } else { - logWarn(QString("Could not write to file '%1': ").arg(filepath) + file.errorString()); + if (!file.open(QIODevice::WriteOnly)) { + logError(QString("Could not write to file '%1': ").arg(filepath) + file.errorString()); + return false; } + + file.write(text.toUtf8()); + return true; } QList parsePal(QString filepath, bool *error) { diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index f6ce6a2e..0ad43d1a 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -402,13 +402,13 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) return map; } -void Tileset::loadMetatiles() { +bool Tileset::loadMetatiles() { clearMetatiles(); QFile metatiles_file(this->metatiles_path); if (!metatiles_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading.").arg(this->metatiles_path)); - return; + logError(QString("Could not open '%1' for reading: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + return false; } QByteArray data = metatiles_file.readAll(); @@ -425,13 +425,14 @@ void Tileset::loadMetatiles() { } m_metatiles.append(metatile); } + return true; } -void Tileset::saveMetatiles() { +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.").arg(this->metatiles_path)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(this->metatiles_path).arg(metatiles_file.errorString())); + return false; } QByteArray data; @@ -444,13 +445,14 @@ void Tileset::saveMetatiles() { } } metatiles_file.write(data); + return true; } -void Tileset::loadMetatileAttributes() { +bool Tileset::loadMetatileAttributes() { QFile attrs_file(this->metatile_attrs_path); if (!attrs_file.open(QIODevice::ReadOnly)) { - logError(QString("Could not open '%1' for reading.").arg(this->metatile_attrs_path)); - return; + logError(QString("Could not open '%1' for reading: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + return false; } QByteArray data = attrs_file.readAll(); @@ -467,13 +469,14 @@ void Tileset::loadMetatileAttributes() { attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); m_metatiles.at(i)->setAttributes(attributes); } + return true; } -void Tileset::saveMetatileAttributes() { +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.").arg(this->metatile_attrs_path)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(this->metatile_attrs_path).arg(attrs_file.errorString())); + return false; } QByteArray data; @@ -483,9 +486,10 @@ void Tileset::saveMetatileAttributes() { data.append(static_cast(attributes >> (8 * i))); } attrs_file.write(data); + return true; } -void Tileset::loadTilesImage(QImage *importedImage) { +bool Tileset::loadTilesImage(QImage *importedImage) { QImage image; if (importedImage) { image = *importedImage; @@ -520,23 +524,25 @@ void Tileset::loadTilesImage(QImage *importedImage) { } this->tilesImage = image; this->tiles = tiles; + return true; } -void Tileset::saveTilesImage() { +bool Tileset::saveTilesImage() { // Only write the tiles image if it was changed. // Porymap will only ever change an existing tiles image by importing a new one. if (!m_hasUnsavedTilesImage) - return; + return true; if (!this->tilesImage.save(this->tilesImagePath, "PNG")) { logError(QString("Failed to save tiles image '%1'").arg(this->tilesImagePath)); - return; + return false; } m_hasUnsavedTilesImage = false; + return true; } -void Tileset::loadPalettes() { +bool Tileset::loadPalettes() { this->palettes.clear(); this->palettePreviews.clear(); @@ -559,26 +565,34 @@ void Tileset::loadPalettes() { this->palettes.append(palette); this->palettePreviews.append(palette); } + return true; } -void Tileset::savePalettes() { +bool Tileset::savePalettes() { + bool success = true; int numPalettes = qMin(this->palettePaths.length(), this->palettes.length()); for (int i = 0; i < numPalettes; i++) { - 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, 16)) + success = false; } + return success; } -void Tileset::load() { - loadMetatiles(); - loadMetatileAttributes(); - loadTilesImage(); - loadPalettes(); +bool Tileset::load() { + bool success = true; + 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. -void Tileset::save() { - saveMetatiles(); - saveMetatileAttributes(); - saveTilesImage(); - savePalettes(); +bool Tileset::save() { + bool success = true; + if (!saveMetatiles()) success = false; + if (!saveMetatileAttributes()) success = false; + if (!saveTilesImage()) success = false; + if (!savePalettes()) success = false; + return success; } diff --git a/src/core/utility.cpp b/src/core/utility.cpp index 55830b8a..7c7e5435 100644 --- a/src/core/utility.cpp +++ b/src/core/utility.cpp @@ -43,6 +43,10 @@ QString Util::toHexString(uint32_t value, int minLength) { return "0x" + QString("%1").arg(value, minLength, 16, QChar('0')).toUpper(); } +QString Util::toHtmlParagraph(const QString &text) { + return QString("

%1

").arg(text); +} + Qt::Orientations Util::getOrientation(bool xflip, bool yflip) { Qt::Orientations flags; if (xflip) flags |= Qt::Orientation::Horizontal; diff --git a/src/editor.cpp b/src/editor.cpp index ec87c6f3..c63034e2 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -30,7 +30,6 @@ Editor::Editor(Ui::MainWindow* ui) { this->ui = ui; this->settings = new Settings(); - this->playerViewRect = new MovableRect(&this->settings->playerViewRectEnabled, 30 * 8, 20 * 8, qRgb(255, 255, 255)); this->cursorMapTileRect = new CursorTileRect(&this->settings->cursorTileRectEnabled, qRgb(255, 255, 255)); this->map_ruler = new MapRuler(4); connect(this->map_ruler, &MapRuler::statusChanged, this, &Editor::mapRulerStatusChanged); @@ -54,6 +53,19 @@ Editor::Editor(Ui::MainWindow* ui) connect(ui->actionOpen_Project_in_Text_Editor, &QAction::triggered, this, &Editor::openProjectInTextEditor); connect(ui->checkBox_ToggleGrid, &QCheckBox::toggled, this, &Editor::toggleGrid); connect(ui->mapCustomAttributesFrame->table(), &CustomAttributesTable::edited, this, &Editor::updateCustomMapAttributes); + + connect(ui->comboBox_DiveMap, &NoScrollComboBox::editingFinished, [this] { + onDivingMapEditingFinished(this->ui->comboBox_DiveMap, "dive"); + }); + connect(ui->comboBox_EmergeMap, &NoScrollComboBox::editingFinished, [this] { + onDivingMapEditingFinished(this->ui->comboBox_EmergeMap, "emerge"); + }); + connect(ui->comboBox_DiveMap, &NoScrollComboBox::currentTextChanged, [this] { + updateDivingMapButton(this->ui->button_OpenDiveMap, this->ui->comboBox_DiveMap->currentText()); + }); + connect(ui->comboBox_EmergeMap, &NoScrollComboBox::currentTextChanged, [this] { + updateDivingMapButton(this->ui->button_OpenEmergeMap, this->ui->comboBox_EmergeMap->currentText()); + }); } Editor::~Editor() @@ -68,30 +80,33 @@ Editor::~Editor() closeProject(); } -void Editor::saveCurrent() { - save(true); +bool Editor::saveCurrent() { + return save(true); } -void Editor::saveAll() { - save(false); +bool Editor::saveAll() { + return save(false); } -void Editor::save(bool currentOnly) { +bool Editor::save(bool currentOnly) { if (!this->project) - return; + return true; saveEncounterTabData(); + bool success = true; if (currentOnly) { if (this->map) { - this->project->saveMap(this->map); + success = this->project->saveMap(this->map); } else if (this->layout) { - this->project->saveLayout(this->layout); + success = this->project->saveLayout(this->layout); } - this->project->saveGlobalData(); + if (!this->project->saveGlobalData()) + success = false; } else { - this->project->saveAll(); + success = this->project->saveAll(); } + return success; } void Editor::setProject(Project * project) { @@ -805,23 +820,41 @@ void Editor::displayConnection(MapConnection *connection) { } } -void Editor::addConnection(MapConnection *connection) { - if (!connection) +void Editor::addNewConnection(const QString &mapName, const QString &direction) { + if (!this->map) return; + MapConnection *connection = new MapConnection(mapName, direction); + // Mark this connection to be selected once its display elements have been created. // It's possible this is a Dive/Emerge connection, but that's ok (no selection will occur). - connection_to_select = connection; + this->connection_to_select = connection; this->map->commit(new MapConnectionAdd(this->map, connection)); } +void Editor::replaceConnection(const QString &mapName, const QString &direction) { + if (!this->map) + return; + + MapConnection *connection = this->map->getConnection(direction); + if (!connection || connection->targetMapName() == mapName) + return; + + this->map->commit(new MapConnectionChangeMap(connection, mapName)); +} + void Editor::removeConnection(MapConnection *connection) { - if (!connection) + if (!this->map || !connection) return; this->map->commit(new MapConnectionRemove(this->map, connection)); } +void Editor::removeSelectedConnection() { + if (selected_connection_item) + removeConnection(selected_connection_item->connection); +} + void Editor::removeConnectionPixmap(MapConnection *connection) { if (!connection) return; @@ -914,21 +947,18 @@ void Editor::removeDivingMapPixmap(MapConnection *connection) { updateDivingMapsVisibility(); } -void Editor::updateDiveMap(QString mapName) { - setDivingMapName(mapName, "dive"); -} +bool Editor::setDivingMapName(const QString &mapName, const QString &direction) { + if (!mapName.isEmpty() && !this->project->mapNames.contains(mapName)) + return false; + if (!MapConnection::isDiving(direction)) + return false; -void Editor::updateEmergeMap(QString mapName) { - setDivingMapName(mapName, "emerge"); -} - -void Editor::setDivingMapName(QString mapName, QString direction) { auto pixmapItem = diving_map_items.value(direction); MapConnection *connection = pixmapItem ? pixmapItem->connection() : nullptr; if (connection) { if (mapName == connection->targetMapName()) - return; // No change + return true; // No change // Update existing connection if (mapName.isEmpty()) { @@ -938,8 +968,25 @@ void Editor::setDivingMapName(QString mapName, QString direction) { } } else if (!mapName.isEmpty()) { // Create new connection - addConnection(new MapConnection(mapName, direction)); + addNewConnection(mapName, direction); } + return true; +} + +QString Editor::getDivingMapName(const QString &direction) const { + auto pixmapItem = diving_map_items.value(direction); + return (pixmapItem && pixmapItem->connection()) ? pixmapItem->connection()->targetMapName() : QString(); +} + +void Editor::onDivingMapEditingFinished(NoScrollComboBox *combo, const QString &direction) { + if (!setDivingMapName(combo->currentText(), direction)) { + // If user input was invalid, restore the combo to the previously-valid text. + combo->setCurrentText(getDivingMapName(direction)); + } +} + +void Editor::updateDivingMapButton(QToolButton* button, const QString &mapName) { + if (this->project) button->setDisabled(!this->project->mapNames.contains(mapName)); } void Editor::updateDivingMapsVisibility() { @@ -1061,6 +1108,13 @@ 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)); + if (ui->graphicsView_Map->scene()) + ui->graphicsView_Map->scene()->update(); +} + void Editor::updateCursorRectPos(int x, int y) { if (this->playerViewRect) this->playerViewRect->updateLocation(x, y); @@ -1562,14 +1616,8 @@ void Editor::displayMapMetatiles() { map_item->draw(true); scene->addItem(map_item); - int tw = 16; - int th = 16; - scene->setSceneRect( - -BORDER_DISTANCE * tw, - -BORDER_DISTANCE * th, - map_item->pixmap().width() + BORDER_DISTANCE * 2 * tw, - map_item->pixmap().height() + BORDER_DISTANCE * 2 * th - ); + // Scene rect is the map plus a margin that gives enough space to scroll and see the edge of the player view rectangle. + scene->setSceneRect(this->layout->getVisibleRect() + QMargins(3,3,3,3)); } void Editor::clearMapMovementPermissions() { @@ -1727,8 +1775,6 @@ void Editor::clearMapConnections() { } connection_items.clear(); - const QSignalBlocker blocker1(ui->comboBox_DiveMap); - const QSignalBlocker blocker2(ui->comboBox_EmergeMap); ui->comboBox_DiveMap->setCurrentText(""); ui->comboBox_EmergeMap->setCurrentText(""); @@ -1765,18 +1811,13 @@ void Editor::clearConnectionMask() { } } -// Hides connected map tiles that cannot be seen from the current map (beyond BORDER_DISTANCE). +// Hides connected map tiles that cannot be seen from the current map void Editor::maskNonVisibleConnectionTiles() { clearConnectionMask(); QPainterPath mask; mask.addRect(scene->itemsBoundingRect().toRect()); - mask.addRect( - -BORDER_DISTANCE * 16, - -BORDER_DISTANCE * 16, - (layout->getWidth() + BORDER_DISTANCE * 2) * 16, - (layout->getHeight() + BORDER_DISTANCE * 2) * 16 - ); + mask.addRect(layout->getVisibleRect()); // Mask the tiles with the current theme's background color. QPen pen(ui->graphicsView_Map->palette().color(QPalette::Active, QPalette::Base)); @@ -1799,13 +1840,10 @@ void Editor::clearMapBorder() { void Editor::displayMapBorder() { clearMapBorder(); - int borderWidth = this->layout->getBorderWidth(); - int borderHeight = this->layout->getBorderHeight(); - int borderHorzDist = this->layout->getBorderDrawWidth(); - int borderVertDist = this->layout->getBorderDrawHeight(); QPixmap pixmap = this->layout->renderBorder(); - for (int y = -borderVertDist; y < this->layout->getHeight() + borderVertDist; y += borderHeight) - for (int x = -borderHorzDist; x < this->layout->getWidth() + borderHorzDist; x += borderWidth) { + const QMargins borderMargins = layout->getBorderMargins(); + 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); @@ -2345,8 +2383,8 @@ void Editor::setCollisionGraphics() { // Users are not required to provide an image that gives an icon for every elevation/collision combination. // Instead they tell us how many are provided in their image by specifying the number of columns and rows. - const int imgColumns = projectConfig.collisionSheetWidth; - const int imgRows = projectConfig.collisionSheetHeight; + const int imgColumns = projectConfig.collisionSheetSize.width(); + const int imgRows = projectConfig.collisionSheetSize.height(); // Create a pixmap for the selector on the Collision tab. If a project was previously opened we'll also need to refresh the selector. this->collisionSheetPixmap = QPixmap::fromImage(imgSheet).scaled(MovementPermissionsSelector::CellWidth * imgColumns, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e44b967d..0b918385 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -261,6 +261,10 @@ void MainWindow::initCustomUI() { // Create map header data widget this->mapHeaderForm = new MapHeaderForm(); ui->layout_HeaderData->addWidget(this->mapHeaderForm); + + // Center zooming on the mouse + ui->graphicsView_Map->setTransformationAnchor(QGraphicsView::ViewportAnchor::AnchorUnderMouse); + ui->graphicsView_Map->setResizeAnchor(QGraphicsView::ViewportAnchor::AnchorUnderMouse); } void MainWindow::initExtraSignals() { @@ -348,6 +352,7 @@ void MainWindow::initEditor() { connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated); connect(ui->newEventToolButton, &NewEventToolButton::newEventAdded, this->editor, &Editor::addNewEvent); connect(ui->toolButton_deleteEvent, &QAbstractButton::clicked, this->editor, &Editor::deleteSelectedEvents); + connect(ui->graphicsView_Connections, &ConnectionsView::pressedDelete, this->editor, &Editor::removeSelectedConnection); this->loadUserSettings(); @@ -583,6 +588,8 @@ void MainWindow::loadUserSettings() { ui->checkBox_ToggleBorder->setChecked(porymapConfig.showBorder); ui->actionShow_Events_In_Map_View->setChecked(porymapConfig.eventOverlayEnabled); + this->editor->gridSettings = porymapConfig.gridSettings; + setTheme(porymapConfig.theme); setDivingMapsVisible(porymapConfig.showDiveEmergeMaps); } @@ -1202,6 +1209,8 @@ 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)); + editor->setCollisionGraphics(); ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); ui->spinBox_SelectedCollision->setMaximum(Block::getMaxCollision()); @@ -1244,10 +1253,7 @@ void MainWindow::clearProjectUI() { const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); ui->comboBox_SecondaryTileset->clear(); - const QSignalBlocker b_DiveMap(ui->comboBox_DiveMap); ui->comboBox_DiveMap->clear(); - - const QSignalBlocker b_EmergeMap(ui->comboBox_EmergeMap); ui->comboBox_EmergeMap->clear(); const QSignalBlocker b_LayoutSelector(ui->comboBox_LayoutSelector); @@ -1408,8 +1414,6 @@ void MainWindow::onNewMapCreated(Map *newMap, const QString &groupName) { // (other combo boxes like for warp destinations are repopulated when the map changes). int mapIndex = this->editor->project->mapNames.indexOf(newMap->name()); if (mapIndex >= 0) { - const QSignalBlocker b_DiveMap(ui->comboBox_DiveMap); - const QSignalBlocker b_EmergeMap(ui->comboBox_EmergeMap); ui->comboBox_DiveMap->insertItem(mapIndex, newMap->name()); ui->comboBox_EmergeMap->insertItem(mapIndex, newMap->name()); } @@ -1665,16 +1669,15 @@ void MainWindow::on_action_Save_triggered() { save(true); } -void MainWindow::save(bool currentOnly) { - if (currentOnly) { - this->editor->saveCurrent(); - } else { - this->editor->saveAll(); +bool MainWindow::save(bool currentOnly) { + bool success = currentOnly ? this->editor->saveCurrent() : this->editor->saveAll(); + if (!success) { + RecentErrorMessage::show(QStringLiteral("Failed to save some project changes."), this); } updateWindowTitle(); updateMapList(); - if (!porymapConfig.shownInGameReloadMessage) { + if (success && !porymapConfig.shownInGameReloadMessage) { // Show a one-time warning that the user may need to reload their map to see their new changes. InfoMessage::show(QStringLiteral("Reload your map in-game!\n\nIf your game is currently saved on a map you have edited, " "the changes may not appear until you leave the map and return."), @@ -1683,6 +1686,7 @@ void MainWindow::save(bool currentOnly) { } saveGlobalConfigs(); + return success; } void MainWindow::duplicate() { @@ -2018,6 +2022,7 @@ void MainWindow::on_actionGrid_Settings_triggered() { if (!this->gridSettingsDialog) { this->gridSettingsDialog = new GridSettingsDialog(&this->editor->gridSettings, this); connect(this->gridSettingsDialog, &GridSettingsDialog::changedGridSettings, this->editor, &Editor::updateMapGrid); + connect(this->gridSettingsDialog, &GridSettingsDialog::accepted, [this] { porymapConfig.gridSettings = this->editor->gridSettings; }); } openSubWindow(this->gridSettingsDialog); } @@ -2620,7 +2625,8 @@ void MainWindow::on_pushButton_AddConnection_clicked() { return; auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); - connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); + connect(dialog, &NewMapConnectionDialog::newConnectionedAdded, this->editor, &Editor::addNewConnection); + connect(dialog, &NewMapConnectionDialog::connectionReplaced, this->editor, &Editor::replaceConnection); dialog->open(); } @@ -2673,17 +2679,6 @@ void MainWindow::on_button_OpenEmergeMap_clicked() { userSetMap(ui->comboBox_EmergeMap->currentText()); } -void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { - // Include empty names as an update (user is deleting the connection) - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) - editor->updateDiveMap(mapName); -} - -void MainWindow::on_comboBox_EmergeMap_currentTextChanged(const QString &mapName) { - if (mapName.isEmpty() || editor->project->mapNames.contains(mapName)) - editor->updateEmergeMap(mapName); -} - void MainWindow::on_comboBox_PrimaryTileset_currentTextChanged(const QString &tilesetLabel) { if (editor->project->primaryTilesetLabels.contains(tilesetLabel) && editor->layout) { @@ -3079,7 +3074,8 @@ bool MainWindow::closeProject() { auto reply = msgBox.exec(); if (reply == QMessageBox::Yes) { - save(); + if (!save()) + return false; } else if (reply == QMessageBox::No) { logWarn("Closing project with unsaved changes."); } else if (reply == QMessageBox::Cancel) { diff --git a/src/project.cpp b/src/project.cpp index 300af774..43713c9d 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -29,8 +29,6 @@ int Project::num_tiles_total = 1024; int Project::num_metatiles_primary = 512; int Project::num_pals_primary = 6; int Project::num_pals_total = 13; -int Project::max_map_data_size = 10240; // 0x2800 -int Project::default_map_dimension = 20; Project::Project(QObject *parent) : QObject(parent), @@ -458,7 +456,7 @@ Layout *Project::loadLayout(QString layoutId) { } if (!loadLayout(layout)) { - logError(QString("Failed to load layout '%1'").arg(layoutId)); + // Error should already be logged. return nullptr; } return layout; @@ -599,12 +597,12 @@ bool Project::readMapLayouts() { return true; } -void Project::saveMapLayouts() { +bool Project::saveMapLayouts() { QString layoutsFilepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::json_layouts); QFile layoutsFile(layoutsFilepath); if (!layoutsFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(layoutsFilepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(layoutsFilepath).arg(layoutsFile.errorString())); + return false; } OrderedJson::object layoutsObj; @@ -638,6 +636,7 @@ void Project::saveMapLayouts() { OrderedJsonDoc jsonDoc(&layoutJson); jsonDoc.dump(&layoutsFile); layoutsFile.close(); + return true; } void Project::ignoreWatchedFileTemporarily(QString filepath) { @@ -663,12 +662,12 @@ void Project::recordFileChange(const QString &filepath) { emit fileChanged(filepath); } -void Project::saveMapGroups() { +bool Project::saveMapGroups() { QString mapGroupsFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_map_groups)); QFile mapGroupsFile(mapGroupsFilepath); if (!mapGroupsFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(mapGroupsFilepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(mapGroupsFilepath).arg(mapGroupsFile.errorString())); + return false; } OrderedJson::object mapGroupsObj; @@ -698,14 +697,15 @@ void Project::saveMapGroups() { OrderedJsonDoc jsonDoc(&mapGroupJson); jsonDoc.dump(&mapGroupsFile); mapGroupsFile.close(); + return true; } -void Project::saveRegionMapSections() { +bool Project::saveRegionMapSections() { const QString filepath = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_map_entries)); QFile file(filepath); if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Could not open '%1' for writing").arg(filepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(filepath).arg(file.errorString())); + return false; } OrderedJson::array mapSectionArray; @@ -739,16 +739,17 @@ void Project::saveRegionMapSections() { OrderedJsonDoc jsonDoc(&json); jsonDoc.dump(&file); file.close(); + return true; } -void Project::saveWildMonData() { - if (!this->wildEncountersLoaded) return; +bool Project::saveWildMonData() { + if (!this->wildEncountersLoaded) return true; QString wildEncountersJsonFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_wild_encounters)); QFile wildEncountersFile(wildEncountersJsonFilepath); if (!wildEncountersFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(wildEncountersJsonFilepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(wildEncountersJsonFilepath).arg(wildEncountersFile.errorString())); + return false; } OrderedJson::object wildEncountersObject; @@ -834,6 +835,7 @@ void Project::saveWildMonData() { OrderedJsonDoc jsonDoc(&encounterJson); jsonDoc.dump(&wildEncountersFile); wildEncountersFile.close(); + return true; } // For a map with a constant of 'MAP_FOO', returns a unique 'HEAL_LOCATION_FOO'. @@ -850,25 +852,19 @@ QString Project::getNewHealLocationName(const Map* map) const { return toUniqueIdentifier(projectConfig.getIdentifier(ProjectIdentifier::define_heal_locations_prefix) + idName); } -void Project::saveHealLocations() { +bool Project::saveHealLocations() { const QString filepath = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(ProjectFilePath::json_heal_locations)); QFile file(filepath); if (!file.open(QIODevice::WriteOnly)) { - logError(QString("Could not open '%1' for writing").arg(filepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(filepath).arg(file.errorString())); + return false; } // Build the JSON data for output. QMap> idNameToJson; - for (auto i = this->healLocations.constBegin(); i != this->healLocations.constEnd(); i++) { - const QString mapConstant = i.key(); - for (const auto &event : i.value()) { - // Heal location events don't need to track the "map" field, we're already tracking it either with - // the keys in the healLocations map or by virtue of the event being added to a particular Map object. - // The global JSON data needs this field, so we add it back here. - auto eventJson = event->buildEventJson(this); - eventJson["map"] = mapConstant; - idNameToJson[event->getIdName()].append(eventJson); + for (const auto &events : this->healLocations) { + for (const auto &event : events) { + idNameToJson[event->getIdName()].append(event->buildEventJson(this)); } } @@ -883,8 +879,8 @@ void Project::saveHealLocations() { } } // Save any heal locations that weren't covered above (should be any new data). - for (auto i = idNameToJson.constBegin(); i != idNameToJson.constEnd(); i++) { - for (const auto &object : i.value()) { + for (const auto &objects : idNameToJson) { + for (const auto &object : objects) { eventJsonArr.push_back(object); } } @@ -898,17 +894,21 @@ void Project::saveHealLocations() { OrderedJsonDoc jsonDoc(&json); jsonDoc.dump(&file); file.close(); + return true; } -void Project::saveTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { - saveTilesetMetatileLabels(primaryTileset, secondaryTileset); - if (primaryTileset) - primaryTileset->save(); - if (secondaryTileset) - secondaryTileset->save(); +bool Project::saveTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { + bool success = saveTilesetMetatileLabels(primaryTileset, secondaryTileset); + if (primaryTileset && !primaryTileset->save()) + success = false; + if (secondaryTileset && !secondaryTileset->save()) + success = false; + return success; } void Project::updateTilesetMetatileLabels(Tileset *tileset) { + if (!tileset) return; + // Erase old labels, then repopulate with new labels const QString prefix = tileset->getMetatileLabelPrefix(); this->metatileLabelsMap[tileset->name].clear(); @@ -943,11 +943,11 @@ QString Project::buildMetatileLabelsText(const QMap defines) return output; } -void Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *secondaryTileset) { +bool Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *secondaryTileset) { // Skip writing the file if there are no labels in both the new and old sets - if (metatileLabelsMap[primaryTileset->name].size() == 0 && primaryTileset->metatileLabels.size() == 0 - && metatileLabelsMap[secondaryTileset->name].size() == 0 && secondaryTileset->metatileLabels.size() == 0) - return; + if ((!primaryTileset || (metatileLabelsMap[primaryTileset->name].size() == 0 && primaryTileset->metatileLabels.size() == 0)) + && (!secondaryTileset || (metatileLabelsMap[secondaryTileset->name].size() == 0 && secondaryTileset->metatileLabels.size() == 0))) + return true; updateTilesetMetatileLabels(primaryTileset); updateTilesetMetatileLabels(secondaryTileset); @@ -974,42 +974,23 @@ void Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *second QString filename = projectConfig.getFilePath(ProjectFilePath::constants_metatile_labels); ignoreWatchedFileTemporarily(root + "/" + filename); - saveTextFile(root + "/" + filename, outputText); + return saveTextFile(root + "/" + filename, outputText); } bool Project::loadLayoutTilesets(Layout *layout) { layout->tileset_primary = getTileset(layout->tileset_primary_label); - if (!layout->tileset_primary) { - QString defaultTileset = this->getDefaultPrimaryTilesetLabel(); - layout->tileset_primary_label = defaultTileset; - layout->tileset_primary = getTileset(layout->tileset_primary_label); - if (!layout->tileset_primary) { - logError(QString("%1 has invalid primary tileset '%2'.").arg(layout->name).arg(layout->tileset_primary_label)); - return false; - } - logWarn(QString("%1 has invalid primary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_primary_label).arg(defaultTileset)); - } - layout->tileset_secondary = getTileset(layout->tileset_secondary_label); - if (!layout->tileset_secondary) { - QString defaultTileset = this->getDefaultSecondaryTilesetLabel(); - layout->tileset_secondary_label = defaultTileset; - layout->tileset_secondary = getTileset(layout->tileset_secondary_label); - if (!layout->tileset_secondary) { - logError(QString("%1 has invalid secondary tileset '%2'.").arg(layout->name).arg(layout->tileset_secondary_label)); - return false; - } - logWarn(QString("%1 has invalid secondary tileset '%2'. Using default '%3'").arg(layout->name).arg(layout->tileset_secondary_label).arg(defaultTileset)); - } - return true; + return layout->tileset_primary && layout->tileset_secondary; } Tileset* Project::loadTileset(QString label, Tileset *tileset) { auto memberMap = Tileset::getHeaderMemberMap(this->usingAsmTilesets); if (this->usingAsmTilesets) { // Read asm tileset header. Backwards compatibility - const QStringList values = parser.getLabelValues(parser.parseAsm(projectConfig.getFilePath(ProjectFilePath::tilesets_headers_asm)), label); + const QString path = projectConfig.getFilePath(ProjectFilePath::tilesets_headers_asm); + const QStringList values = parser.getLabelValues(parser.parseAsm(path), label); if (values.isEmpty()) { + logError(QString("Failed to find header data in '%1' for tileset '%2'.").arg(path).arg(label)); return nullptr; } if (tileset == nullptr) { @@ -1023,8 +1004,10 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { tileset->metatile_attrs_label = values.value(memberMap.key("metatileAttributes")); } else { // Read C tileset header - auto structs = parser.readCStructs(projectConfig.getFilePath(ProjectFilePath::tilesets_headers), label, memberMap); + const QString path = projectConfig.getFilePath(ProjectFilePath::tilesets_headers); + auto structs = parser.readCStructs(path, label, memberMap); if (!structs.contains(label)) { + logError(QString("Failed to find header data in '%1' for tileset '%2'.").arg(path).arg(label)); return nullptr; } if (tileset == nullptr) { @@ -1039,7 +1022,11 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { tileset->metatile_attrs_label = tilesetAttributes.value("metatileAttributes"); } - loadTilesetAssets(tileset); + if (!loadTilesetAssets(tileset)) { + // Error should already be logged. + delete tileset; + return nullptr; + } tilesetCache.insert(label, tileset); return tileset; @@ -1128,38 +1115,22 @@ void Project::setNewLayoutBorder(Layout *layout) { layout->lastCommitBlocks.borderDimensions = QSize(width, height); } -void Project::saveLayoutBorder(Layout *layout) { - QString path = QString("%1/%2").arg(root).arg(layout->border_path); - writeBlockdata(path, layout->border); -} - -void Project::saveLayoutBlockdata(Layout *layout) { - QString path = QString("%1/%2").arg(root).arg(layout->blockdata_path); - writeBlockdata(path, layout->blockdata); -} - -void Project::writeBlockdata(QString path, const Blockdata &blockdata) { - QFile file(path); - if (file.open(QIODevice::WriteOnly)) { - QByteArray data = blockdata.serialize(); - file.write(data); - } else { - logError(QString("Failed to open blockdata file for writing: '%1'").arg(path)); - } -} - -void Project::saveAll() { +bool Project::saveAll() { + bool success = true; for (auto map : this->maps) { - saveMap(map, true); // Avoid double-saving the layouts + if (!saveMap(map, true)) // Avoid double-saving the layouts + success = false; } for (auto layout : this->mapLayouts) { - saveLayout(layout); + if (!saveLayout(layout)) + success = false; } - saveGlobalData(); + if (!saveGlobalData()) success = false; + return success; } -void Project::saveMap(Map *map, bool skipLayout) { - if (!map || !isMapLoaded(map)) return; +bool Project::saveMap(Map *map, bool skipLayout) { + if (!map || !isMapLoaded(map)) return true; // Create/Modify a few collateral files for brand new maps. const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_map_folders) + map->name(); @@ -1167,7 +1138,7 @@ void Project::saveMap(Map *map, bool skipLayout) { if (!map->isPersistedToFile()) { if (!QDir::root().mkpath(fullPath)) { logError(QString("Failed to create directory for new map: '%1'").arg(fullPath)); - return; + return false; } // Create file data/maps//scripts.inc @@ -1191,8 +1162,8 @@ void Project::saveMap(Map *map, bool skipLayout) { QString mapFilepath = fullPath + "/map.json"; QFile mapFile(mapFilepath); if (!mapFile.open(QIODevice::WriteOnly)) { - logError(QString("Error: Could not open %1 for writing").arg(mapFilepath)); - return; + logError(QString("Could not open '%1' for writing: %2").arg(mapFilepath).arg(mapFile.errorString())); + return false; } OrderedJson::object mapObj; @@ -1288,72 +1259,61 @@ void Project::saveMap(Map *map, bool skipLayout) { jsonDoc.dump(&mapFile); mapFile.close(); - if (!skipLayout) saveLayout(map->layout()); - // Try to record the MAPSEC name in case this is a new name. addNewMapsec(map->header()->location()); - map->setClean(); + + if (!skipLayout && !saveLayout(map->layout())) + return false; + return true; } -void Project::saveLayout(Layout *layout) { +bool Project::saveLayout(Layout *layout) { if (!layout || !isLayoutLoaded(layout)) - return; + return true; - if (!layout->newFolderPath.isEmpty()) { - // Layout directory doesn't exist yet, create it now. - const QString fullPath = QString("%1/%2").arg(this->root).arg(layout->newFolderPath); - if (!QDir::root().mkpath(fullPath)) { - logError(QString("Failed to create directory for new layout: '%1'").arg(fullPath)); - return; - } - layout->newFolderPath = QString(); - } - - saveLayoutBorder(layout); - saveLayoutBlockdata(layout); + if (!layout->save(this->root)) + return false; // Update global data structures with current map data. - updateLayout(layout); - - layout->setClean(); -} - -void Project::updateLayout(Layout *layout) { if (!this->layoutIdsMaster.contains(layout->id)) { this->layoutIdsMaster.append(layout->id); } if (this->mapLayoutsMaster.contains(layout->id)) { this->mapLayoutsMaster[layout->id]->copyFrom(layout); - } - else { + } else { this->mapLayoutsMaster.insert(layout->id, layout->copy()); } + return true; } -void Project::saveGlobalData() { - saveMapLayouts(); - saveMapGroups(); - saveRegionMapSections(); - saveHealLocations(); - saveWildMonData(); - saveConfig(); +bool Project::saveGlobalData() { + bool success = true; + if (!saveMapLayouts()) success = false; + if (!saveMapGroups()) success = false; + if (!saveRegionMapSections()) success = false; + if (!saveHealLocations()) success = false; + if (!saveWildMonData()) success = false; + if (!saveConfig()) success = false; + if (!success) + return false; + this->hasUnsavedDataChanges = false; + return true; } -void Project::saveConfig() { - projectConfig.save(); - userConfig.save(); +bool Project::saveConfig() { + bool success = true; + if (!projectConfig.save()) success = false; + if (!userConfig.save()) success = false; + return success; } -void Project::loadTilesetAssets(Tileset* tileset) { - if (tileset->name.isNull()) { - return; - } +bool Project::loadTilesetAssets(Tileset* tileset) { readTilesetPaths(tileset); loadTilesetMetatileLabels(tileset); - tileset->load(); + return tileset->load(); } void Project::readTilesetPaths(Tileset* tileset) { @@ -1547,6 +1507,8 @@ bool Project::readTilesetMetatileLabels() { } void Project::loadTilesetMetatileLabels(Tileset* tileset) { + if (!tileset || tileset->name.isEmpty()) return; + QString metatileLabelPrefix = tileset->getMetatileLabelPrefix(); // Reverse map for faster lookup by metatile id @@ -1588,29 +1550,24 @@ Tileset* Project::getTileset(QString label, bool forceLoad) { } } -void Project::saveTextFile(QString path, QString text) { +bool Project::saveTextFile(const QString &path, const QString &text) { QFile file(path); - if (file.open(QIODevice::WriteOnly)) { - file.write(text.toUtf8()); - } else { + if (!file.open(QIODevice::WriteOnly)) { logError(QString("Could not open '%1' for writing: ").arg(path) + file.errorString()); + return false; } + file.write(text.toUtf8()); + return true; } -void Project::appendTextFile(QString path, QString text) { +bool Project::appendTextFile(const QString &path, const QString &text) { QFile file(path); - if (file.open(QIODevice::Append)) { - file.write(text.toUtf8()); - } else { + if (!file.open(QIODevice::Append)) { logError(QString("Could not open '%1' for appending: ").arg(path) + file.errorString()); + return false; } -} - -void Project::deleteFile(QString path) { - QFile file(path); - if (file.exists() && !file.remove()) { - logError(QString("Could not delete file '%1': ").arg(path) + file.errorString()); - } + file.write(text.toUtf8()); + return true; } bool Project::readWildMonData() { @@ -1700,19 +1657,22 @@ bool Project::readWildMonData() { // Each element describes a type of wild encounter Porymap can expect to find, and we represent this data with an EncounterField. // They should contain a name ("type"), the number of encounter slots and the ratio at which they occur ("encounter_rates"), // and whether the encounters are divided into groups (like fishing rods). - for (const OrderedJson &fieldJson : mainArrayObject.take("fields").array_items()) { + OrderedJson::array fieldsArray = mainArrayObject.take("fields").array_items(); + for (const OrderedJson &fieldJson : fieldsArray) { OrderedJson::object fieldObject = fieldJson.object_items(); EncounterField encounterField; encounterField.name = fieldObject.take("type").string_value(); - for (auto val : fieldObject.take("encounter_rates").array_items()) { + OrderedJson::array encounterRatesArray = fieldObject.take("encounter_rates").array_items(); + for (const auto &val : encounterRatesArray) { encounterField.encounterRates.append(val.int_value()); } // Each element of the "groups" array is an object with the group name as the key (e.g. "old_rod") // and an array of slot numbers indicating which encounter slots in this encounter type belong to that group. - for (auto groupPair : fieldObject.take("groups").object_items()) { + OrderedJson::object groups = fieldObject.take("groups").object_items(); + for (auto groupPair : groups) { const QString groupName = groupPair.first; for (auto slotNum : groupPair.second.array_items()) { encounterField.groups[groupName].append(slotNum.int_value()); @@ -1728,7 +1688,8 @@ bool Project::readWildMonData() { // Each element is an object that will tell us which map it's associated with, // its symbol name (which we will display in the Groups dropdown) and a list of // pokémon associated with any of the encounter types described by the data we parsed above. - for (const auto &encounterJson : mainArrayObject.take("encounters").array_items()) { + OrderedJson::array encountersArray = mainArrayObject.take("encounters").array_items(); + for (const auto &encounterJson : encountersArray) { OrderedJson::object encounterObj = encounterJson.object_items(); WildPokemonHeader header; @@ -1750,7 +1711,8 @@ bool Project::readWildMonData() { encounterRateFrequencyMaps[field][monInfo.encounterRate]++; // Read wild pokémon list - for (const auto &monJson : encounterFieldObj.take("mons").array_items()) { + OrderedJson::array monsArray = encounterFieldObj.take("mons").array_items(); + for (const auto &monJson : monsArray) { OrderedJson::object monObj = monJson.object_items(); WildPokemon newMon; @@ -2036,8 +1998,8 @@ void Project::initNewMapSettings() { this->newMapSettings.layout.folderName = this->newMapSettings.name; this->newMapSettings.layout.name = QString(); this->newMapSettings.layout.id = Layout::layoutConstantFromName(this->newMapSettings.name); - this->newMapSettings.layout.width = getDefaultMapDimension(); - this->newMapSettings.layout.height = getDefaultMapDimension(); + this->newMapSettings.layout.width = this->defaultMapSize.width(); + this->newMapSettings.layout.height = this->defaultMapSize.height(); this->newMapSettings.layout.borderWidth = DEFAULT_BORDER_WIDTH; this->newMapSettings.layout.borderHeight = DEFAULT_BORDER_HEIGHT; this->newMapSettings.layout.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); @@ -2059,8 +2021,8 @@ void Project::initNewMapSettings() { void Project::initNewLayoutSettings() { this->newLayoutSettings.name = QString(); this->newLayoutSettings.id = Layout::layoutConstantFromName(this->newLayoutSettings.name); - this->newLayoutSettings.width = getDefaultMapDimension(); - this->newLayoutSettings.height = getDefaultMapDimension(); + this->newLayoutSettings.width = this->defaultMapSize.width(); + this->newLayoutSettings.height = this->defaultMapSize.height(); this->newLayoutSettings.borderWidth = DEFAULT_BORDER_WIDTH; this->newLayoutSettings.borderHeight = DEFAULT_BORDER_HEIGHT; this->newLayoutSettings.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); @@ -2153,7 +2115,12 @@ bool Project::readFieldmapProperties() { const QString numPalsTotalName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_total); const QString maxMapSizeName = projectConfig.getIdentifier(ProjectIdentifier::define_map_size); const QString numTilesPerMetatileName = projectConfig.getIdentifier(ProjectIdentifier::define_tiles_per_metatile); - const QSet names = { + const QString mapOffsetWidthName = projectConfig.getIdentifier(ProjectIdentifier::define_map_offset_width); + const QString mapOffsetHeightName = projectConfig.getIdentifier(ProjectIdentifier::define_map_offset_height); + + const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); + fileWatcher.addPath(root + "/" + filename); + const QMap defines = parser.readCDefinesByName(filename, { numTilesPrimaryName, numTilesTotalName, numMetatilesPrimaryName, @@ -2161,24 +2128,23 @@ bool Project::readFieldmapProperties() { numPalsTotalName, maxMapSizeName, numTilesPerMetatileName, - }; - const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap); - fileWatcher.addPath(root + "/" + filename); - const QMap defines = parser.readCDefinesByName(filename, names); + mapOffsetWidthName, + mapOffsetHeightName, + }); auto loadDefine = [defines](const QString name, int * dest, int min, int max) { auto it = defines.find(name); if (it != defines.end()) { *dest = it.value(); if (*dest < min) { - logWarn(QString("Value for tileset property '%1' (%2) is below the minimum (%3). Defaulting to minimum.").arg(name).arg(*dest).arg(min)); + logWarn(QString("Value for '%1' (%2) is below the minimum (%3). Defaulting to minimum.").arg(name).arg(*dest).arg(min)); *dest = min; } else if (*dest > max) { - logWarn(QString("Value for tileset property '%1' (%2) is above the maximum (%3). Defaulting to maximum.").arg(name).arg(*dest).arg(max)); + logWarn(QString("Value for '%1' (%2) is above the maximum (%3). Defaulting to maximum.").arg(name).arg(*dest).arg(max)); *dest = max; } } else { - logWarn(QString("Value for tileset property '%1' not found. Using default (%2) instead.").arg(name).arg(*dest)); + 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. @@ -2190,25 +2156,42 @@ bool Project::readFieldmapProperties() { // we don't actually know what the maximum number of metatiles is. loadDefine(numMetatilesPrimaryName, &Project::num_metatiles_primary, 1, 0xFFFF - 1); + int w = 15, h = 14; // Default values of MAP_OFFSET_W, MAP_OFFSET_H + loadDefine(mapOffsetWidthName, &w, 0, INT_MAX); + loadDefine(mapOffsetHeightName, &h, 0, INT_MAX); + this->mapSizeAddition = QSize(w, h); + + this->maxMapDataSize = 10240; // Default value of MAX_MAP_DATA_SIZE + this->defaultMapSize = projectConfig.defaultMapSize; auto it = defines.find(maxMapSizeName); if (it != defines.end()) { int min = getMapDataSize(1, 1); if (it.value() >= min) { - Project::max_map_data_size = it.value(); - calculateDefaultMapSize(); + this->maxMapDataSize = it.value(); + if (getMapDataSize(this->defaultMapSize.width(), this->defaultMapSize.height()) > this->maxMapDataSize) { + // The specified map size is too small to use the default map dimensions. + // Calculate the largest square map size that we can use instead. + int dimension = qFloor((qSqrt(4 * this->maxMapDataSize + 1) - (w + h)) / 2); + logWarn(QString("Value for '%1' (%2) is too small to support the default %3x%4 map. Default changed to %5x%5.") + .arg(maxMapSizeName) + .arg(it.value()) + .arg(this->defaultMapSize.width()) + .arg(this->defaultMapSize.height()) + .arg(dimension)); + this->defaultMapSize = QSize(dimension, dimension); + } } else { - // must be large enough to support a 1x1 map - logWarn(QString("Value for map property '%1' is %2, must be at least %3. Using default (%4) instead.") + logWarn(QString("Value for '%1' (%2) is too small to support a 1x1 map. Must be at least %3. Using default (%4) instead.") .arg(maxMapSizeName) .arg(it.value()) .arg(min) - .arg(Project::max_map_data_size)); + .arg(this->maxMapDataSize)); } } else { - logWarn(QString("Value for map property '%1' not found. Using default (%2) instead.") + logWarn(QString("Value for '%1' not found. Using default (%2) instead.") .arg(maxMapSizeName) - .arg(Project::max_map_data_size)); + .arg(this->maxMapDataSize)); } it = defines.find(numTilesPerMetatileName); @@ -3185,91 +3168,28 @@ QPixmap Project::getSpeciesIcon(const QString &species) { return pixmap; } -int Project::getNumTilesPrimary() -{ - return Project::num_tiles_primary; +int Project::getMapDataSize(int width, int height) const { + return (width + this->mapSizeAddition.width()) + * (height + this->mapSizeAddition.height()); } -int Project::getNumTilesTotal() -{ - return Project::num_tiles_total; +int Project::getMaxMapWidth() const { + return (getMaxMapDataSize() / (1 + this->mapSizeAddition.height())) - this->mapSizeAddition.width(); } -int Project::getNumMetatilesPrimary() -{ - return Project::num_metatiles_primary; +int Project::getMaxMapHeight() const { + return (getMaxMapDataSize() / (1 + this->mapSizeAddition.width())) - this->mapSizeAddition.height(); } -int Project::getNumMetatilesTotal() -{ - return Block::getMaxMetatileId() + 1; -} - -int Project::getNumPalettesPrimary() -{ - return Project::num_pals_primary; -} - -int Project::getNumPalettesTotal() -{ - return Project::num_pals_total; -} - -int Project::getMaxMapDataSize() -{ - return Project::max_map_data_size; -} - -int Project::getMapDataSize(int width, int height) -{ - // + 15 and + 14 come from fieldmap.c in pokeruby/pokeemerald/pokefirered. - return (width + 15) * (height + 14); -} - -int Project::getDefaultMapDimension() -{ - return Project::default_map_dimension; -} - -int Project::getMaxMapWidth() -{ - return (getMaxMapDataSize() / (1 + 14)) - 15; -} - -int Project::getMaxMapHeight() -{ - return (getMaxMapDataSize() / (1 + 15)) - 14; -} - -bool Project::mapDimensionsValid(int width, int height) { +bool Project::mapDimensionsValid(int width, int height) const { return getMapDataSize(width, height) <= getMaxMapDataSize(); } -// Get largest possible square dimensions for a map up to maximum of 20x20 (arbitrary) -bool Project::calculateDefaultMapSize(){ - int max = getMaxMapDataSize(); - - if (max >= getMapDataSize(20, 20)) { - default_map_dimension = 20; - } else if (max >= getMapDataSize(1, 1)) { - // Below equation derived from max >= (x + 15) * (x + 14) - // x^2 + 29x + (210 - max), then complete the square and simplify - default_map_dimension = qFloor((qSqrt(4 * getMaxMapDataSize() + 1) - 29) / 2); - } else { - logError(QString("'%1' of %2 is too small to support a 1x1 map. Must be at least %3.") - .arg(projectConfig.getIdentifier(ProjectIdentifier::define_map_size)) - .arg(max) - .arg(getMapDataSize(1, 1))); - return false; - } - return true; -} - // Object events have their own limit specified by ProjectIdentifier::define_obj_event_count. // The default value for this is 64. All events (object events included) are also limited by // the data types of the event counters in the project. This would normally be u8, so the limit is 255. // We let the users tell us this limit in case they change these data types. -int Project::getMaxEvents(Event::Group group) { +int Project::getMaxEvents(Event::Group group) const { if (group == Event::Group::Object) return qMin(this->maxObjectEvents, projectConfig.maxEventsPerGroup); return projectConfig.maxEventsPerGroup; @@ -3291,6 +3211,17 @@ 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. +// 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)); + return viewDistance; +} + // If the provided filepath is an absolute path to an existing file, return filepath. // If not, and the provided filepath is a relative path from the project dir to an existing file, return the relative path. // Otherwise return empty string. @@ -3324,8 +3255,8 @@ void Project::applyParsedLimits() { projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId()); projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation()); projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision()); - projectConfig.collisionSheetHeight = qMin(qMax(projectConfig.collisionSheetHeight, 1), Block::getMaxElevation() + 1); - projectConfig.collisionSheetWidth = qMin(qMax(projectConfig.collisionSheetWidth, 1), Block::getMaxCollision() + 1); + projectConfig.collisionSheetSize.setHeight(qMin(qMax(projectConfig.collisionSheetSize.height(), 1), Block::getMaxElevation() + 1)); + projectConfig.collisionSheetSize.setWidth(qMin(qMax(projectConfig.collisionSheetSize.width(), 1), Block::getMaxCollision() + 1)); } bool Project::hasUnsavedChanges() { diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 0ee3316e..08bfc9c5 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -227,7 +227,7 @@ int MainWindow::getHeight() { void MainWindow::setDimensions(int width, int height) { if (!this->editor || !this->editor->layout) return; - if (!Project::mapDimensionsValid(width, height)) + if (this->editor->project && !this->editor->project->mapDimensionsValid(width, height)) return; this->editor->layout->setDimensions(width, height); this->tryCommitMapChanges(true); @@ -237,7 +237,7 @@ void MainWindow::setDimensions(int width, int height) { void MainWindow::setWidth(int width) { if (!this->editor || !this->editor->layout) return; - if (!Project::mapDimensionsValid(width, this->editor->layout->getHeight())) + if (this->editor->project && !this->editor->project->mapDimensionsValid(width, this->editor->layout->getHeight())) return; this->editor->layout->setDimensions(width, this->editor->layout->getHeight()); this->tryCommitMapChanges(true); @@ -247,7 +247,7 @@ void MainWindow::setWidth(int width) { void MainWindow::setHeight(int height) { if (!this->editor || !this->editor->layout) return; - if (!Project::mapDimensionsValid(this->editor->layout->getWidth(), height)) + if (this->editor->project && !this->editor->project->mapDimensionsValid(this->editor->layout->getWidth(), height)) return; this->editor->layout->setDimensions(this->editor->layout->getWidth(), height); this->tryCommitMapChanges(true); diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index f023aa22..c6e606b0 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -10,7 +10,6 @@ ConnectionPixmapItem::ConnectionPixmapItem(MapConnection* connection) connection(connection) { this->setEditable(true); - setFlag(ItemIsFocusable, true); this->basePixmap = pixmap(); updateOrigin(); render(false); @@ -119,10 +118,6 @@ bool ConnectionPixmapItem::getEditable() { } void ConnectionPixmapItem::setSelected(bool selected) { - if (selected && !hasFocus()) { - setFocus(Qt::OtherFocusReason); - } - if (this->selected == selected) return; this->selected = selected; @@ -132,7 +127,7 @@ void ConnectionPixmapItem::setSelected(bool selected) { } void ConnectionPixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *) { - setFocus(Qt::MouseFocusReason); + this->setSelected(true); } void ConnectionPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { @@ -143,18 +138,3 @@ void ConnectionPixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { void ConnectionPixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) { emit connectionItemDoubleClicked(this->connection); } - -void ConnectionPixmapItem::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) { - emit deleteRequested(this->connection); - } else { - QGraphicsPixmapItem::keyPressEvent(event); - } -} - -void ConnectionPixmapItem::focusInEvent(QFocusEvent* event) { - if (!this->getEditable()) - return; - this->setSelected(true); - QGraphicsPixmapItem::focusInEvent(event); -} diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index 5b21a12c..86df9525 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -7,44 +7,55 @@ ConnectionsListItem::ConnectionsListItem(QWidget *parent, MapConnection * connection, const QStringList &mapNames) : QFrame(parent), - ui(new Ui::ConnectionsListItem) + ui(new Ui::ConnectionsListItem), + connection(connection), + map(connection->parentMap()) { ui->setupUi(this); setFocusPolicy(Qt::StrongFocus); - const QSignalBlocker blocker1(ui->comboBox_Direction); - const QSignalBlocker blocker2(ui->comboBox_Map); - const QSignalBlocker blocker3(ui->spinBox_Offset); - - ui->comboBox_Direction->setEditable(false); + // Direction + const QSignalBlocker b_Direction(ui->comboBox_Direction); ui->comboBox_Direction->setMinimumContentsLength(0); ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); + ui->comboBox_Direction->installEventFilter(this); + connect(ui->comboBox_Direction, &NoScrollComboBox::editingFinished, this, &ConnectionsListItem::commitDirection); + + // Map + const QSignalBlocker b_Map(ui->comboBox_Map); ui->comboBox_Map->setMinimumContentsLength(6); ui->comboBox_Map->addItems(mapNames); ui->comboBox_Map->setFocusedScrollingEnabled(false); // Scrolling could cause rapid changes to many different maps ui->comboBox_Map->setInsertPolicy(QComboBox::NoInsert); + ui->comboBox_Map->installEventFilter(this); - ui->spinBox_Offset->setMinimum(INT_MIN); - ui->spinBox_Offset->setMaximum(INT_MAX); + connect(ui->comboBox_Map, &QComboBox::currentTextChanged, this, &ConnectionsListItem::commitMap); // Invalid map names are not considered a change. If editing finishes with an invalid name, restore the previous name. connect(ui->comboBox_Map->lineEdit(), &QLineEdit::editingFinished, [this] { - const QSignalBlocker blocker(ui->comboBox_Map); - if (ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) + const QSignalBlocker b(ui->comboBox_Map); + if (this->connection && ui->comboBox_Map->findText(ui->comboBox_Map->currentText()) < 0) ui->comboBox_Map->setTextItem(this->connection->targetMapName()); }); - // Distinguish between move actions for the edit history - connect(ui->spinBox_Offset, &QSpinBox::editingFinished, [this] { this->actionId++; }); + // Offset + const QSignalBlocker b_Offset(ui->spinBox_Offset); + ui->spinBox_Offset->setMinimum(INT_MIN); + ui->spinBox_Offset->setMaximum(INT_MAX); + ui->spinBox_Offset->installEventFilter(this); + + connect(ui->spinBox_Offset, &QSpinBox::editingFinished, [this] { this->actionId++; }); // Distinguish between move actions for the edit history + connect(ui->spinBox_Offset, QOverload::of(&QSpinBox::valueChanged), this, &ConnectionsListItem::commitMove); // If the connection changes externally we want to update to reflect the change. connect(connection, &MapConnection::offsetChanged, this, &ConnectionsListItem::updateUI); connect(connection, &MapConnection::directionChanged, this, &ConnectionsListItem::updateUI); connect(connection, &MapConnection::targetMapNameChanged, this, &ConnectionsListItem::updateUI); - this->connection = connection; - this->map = connection->parentMap(); + connect(ui->button_Delete, &QToolButton::clicked, this, &ConnectionsListItem::commitRemove); + connect(ui->button_OpenMap, &QToolButton::clicked, [this] { emit openMapClicked(this->connection); }); + this->updateUI(); } @@ -66,13 +77,19 @@ void ConnectionsListItem::updateUI() { ui->spinBox_Offset->setValue(this->connection->offset()); } +bool ConnectionsListItem::eventFilter(QObject*, QEvent *event) { + if (event->type() == QEvent::FocusIn) + this->setSelected(true); + return false; +} + void ConnectionsListItem::setSelected(bool selected) { if (selected == this->isSelected) return; this->isSelected = selected; - this->setStyleSheet(selected ? ".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }" - : ".ConnectionsListItem { border-width: 1px; }"); + this->setStyleSheet(selected ? QStringLiteral(".ConnectionsListItem { border: 1px solid rgb(255, 0, 255); }") + : QStringLiteral(".ConnectionsListItem { border-width: 1px; }")); if (selected) emit this->selected(); } @@ -81,42 +98,34 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { this->setSelected(true); } -void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(QString direction) { - this->setSelected(true); - if (this->map) +void ConnectionsListItem::commitDirection() { + const QString direction = ui->comboBox_Direction->currentText(); + if (!this->connection || this->connection->direction() == direction) + return; + + if (MapConnection::isDiving(direction)) { + // Diving maps are displayed separately, no support right now for replacing a list item with a diving map. + // For now just restore the original direction. + ui->comboBox_Direction->setCurrentText(this->connection->direction()); + return; + } + + if (this->map) { this->map->commit(new MapConnectionChangeDirection(this->connection, direction)); + } } -void ConnectionsListItem::on_comboBox_Map_currentTextChanged(QString mapName) { - this->setSelected(true); +void ConnectionsListItem::commitMap(const QString &mapName) { if (this->map && ui->comboBox_Map->findText(mapName) >= 0) this->map->commit(new MapConnectionChangeMap(this->connection, mapName)); } -void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { - this->setSelected(true); +void ConnectionsListItem::commitMove(int offset) { if (this->map) this->map->commit(new MapConnectionMove(this->connection, offset, this->actionId)); } -void ConnectionsListItem::on_button_Delete_clicked() { +void ConnectionsListItem::commitRemove() { if (this->map) this->map->commit(new MapConnectionRemove(this->map, this->connection)); } - -void ConnectionsListItem::on_button_OpenMap_clicked() { - emit openMapClicked(this->connection); -} - -void ConnectionsListItem::focusInEvent(QFocusEvent* event) { - this->setSelected(true); - QFrame::focusInEvent(event); -} - -void ConnectionsListItem::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) { - on_button_Delete_clicked(); - } else { - QFrame::keyPressEvent(event); - } -} diff --git a/src/ui/customattributestable.cpp b/src/ui/customattributestable.cpp index 28153b4d..ba9409c8 100644 --- a/src/ui/customattributestable.cpp +++ b/src/ui/customattributestable.cpp @@ -1,6 +1,7 @@ #include "customattributestable.h" #include "parseutil.h" #include "noscrollspinbox.h" +#include "utility.h" #include #include @@ -96,7 +97,7 @@ int CustomAttributesTable::addAttribute(const QString &key, const QJsonValue &va keyItem->setFlags(Qt::ItemIsEnabled); keyItem->setData(DataRole::JsonType, type); // Record the type for writing to the file keyItem->setTextAlignment(Qt::AlignCenter); - keyItem->setToolTip(key); // Display name as tool tip in case it's too long to see in the cell + keyItem->setToolTip(Util::toHtmlParagraph(key)); // Display name as tool tip in case it's too long to see in the cell this->setItem(rowIndex, Column::Key, keyItem); // Add value to table diff --git a/src/ui/divingmappixmapitem.cpp b/src/ui/divingmappixmapitem.cpp index e20b8f25..b774b1e9 100644 --- a/src/ui/divingmappixmapitem.cpp +++ b/src/ui/divingmappixmapitem.cpp @@ -38,9 +38,5 @@ void DivingMapPixmapItem::onTargetMapChanged() { } void DivingMapPixmapItem::setComboText(const QString &text) { - if (!m_combo) - return; - - const QSignalBlocker blocker(m_combo); - m_combo->setCurrentText(text); + if (m_combo) m_combo->setCurrentText(text); } diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index 86461b83..07b77c0b 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -239,15 +239,16 @@ void ObjectFrame::setup() { // sprite combo QFormLayout *l_form_sprite = new QFormLayout(); this->combo_sprite = new NoScrollComboBox(this); - this->combo_sprite->setToolTip("The sprite graphics to use for this object."); + static const QString combo_sprite_toolTip = Util::toHtmlParagraph("The sprite graphics to use for this object."); + this->combo_sprite->setToolTip(combo_sprite_toolTip); l_form_sprite->addRow("Sprite", this->combo_sprite); this->layout_contents->addLayout(l_form_sprite); // movement QFormLayout *l_form_movement = new QFormLayout(); this->combo_movement = new NoScrollComboBox(this); - this->combo_movement->setToolTip("The object's natural movement behavior when\n" - "the player is not interacting with it."); + static const QString combo_movement_toolTip = Util::toHtmlParagraph("The object's natural movement behavior when the player is not interacting with it."); + this->combo_movement->setToolTip(combo_movement_toolTip); l_form_movement->addRow("Movement", this->combo_movement); this->layout_contents->addLayout(l_form_movement); @@ -256,15 +257,15 @@ void ObjectFrame::setup() { this->spinner_radius_x = new NoScrollSpinBox(this); this->spinner_radius_x->setMinimum(0); this->spinner_radius_x->setMaximum(255); - this->spinner_radius_x->setToolTip("The maximum number of metatiles this object\n" - "is allowed to move left or right during its\n" - "normal movement behavior actions."); + static const QString spinner_radius_x_toolTip = Util::toHtmlParagraph("The maximum number of metatiles this object is allowed to move left " + "or right during its normal movement behavior actions."); + this->spinner_radius_x->setToolTip(spinner_radius_x_toolTip); this->spinner_radius_y = new NoScrollSpinBox(this); this->spinner_radius_y->setMinimum(0); this->spinner_radius_y->setMaximum(255); - this->spinner_radius_y->setToolTip("The maximum number of metatiles this object\n" - "is allowed to move up or down during its\n" - "normal movement behavior actions."); + static const QString spinner_radius_y_toolTip = Util::toHtmlParagraph("The maximum number of metatiles this object is allowed to move up " + "or down during its normal movement behavior actions."); + this->spinner_radius_y->setToolTip(spinner_radius_y_toolTip); l_form_radii_xy->addRow("Movement Radius X", this->spinner_radius_x); l_form_radii_xy->addRow("Movement Radius Y", this->spinner_radius_y); this->layout_contents->addLayout(l_form_radii_xy); @@ -272,11 +273,13 @@ void ObjectFrame::setup() { // script QFormLayout *l_form_script = new QFormLayout(); this->combo_script = new NoScrollComboBox(this); - this->combo_script->setToolTip("The script which is executed with this event."); + static const QString combo_script_toolTip = Util::toHtmlParagraph("The script that is executed with this event."); + this->combo_script->setToolTip(combo_script_toolTip); // Add button next to combo which opens combo's current script. this->button_script = new QToolButton(this); - this->button_script->setToolTip("Go to this script definition in text editor."); + static const QString button_script_toolTip = Util::toHtmlParagraph("Go to this script definition in text editor."); + this->button_script->setToolTip(button_script_toolTip); this->button_script->setFixedSize(this->combo_script->height(), this->combo_script->height()); this->button_script->setIcon(QFileIconProvider().icon(QFileIconProvider::File)); @@ -291,24 +294,25 @@ void ObjectFrame::setup() { // event flag QFormLayout *l_form_flag = new QFormLayout(); this->combo_flag = new NoScrollComboBox(this); - this->combo_flag->setToolTip("The flag which hides the object when set."); + static const QString combo_flag_toolTip = Util::toHtmlParagraph("The flag that hides the object when set."); + this->combo_flag->setToolTip(combo_flag_toolTip); l_form_flag->addRow("Event Flag", this->combo_flag); this->layout_contents->addLayout(l_form_flag); // trainer type QFormLayout *l_form_trainer = new QFormLayout(); this->combo_trainer_type = new NoScrollComboBox(this); - this->combo_trainer_type->setToolTip("The trainer type of this object event.\n" - "If it is not a trainer, use NONE. SEE ALL DIRECTIONS\n" - "should only be used with a sight radius of 1."); + static const QString combo_trainer_type_toolTip = Util::toHtmlParagraph("The trainer type of this object event. If it is not a trainer, use NONE. " + "SEE ALL DIRECTIONS should only be used with a sight radius of 1."); + this->combo_trainer_type->setToolTip(combo_trainer_type_toolTip); l_form_trainer->addRow("Trainer Type", this->combo_trainer_type); this->layout_contents->addLayout(l_form_trainer); // sight radius / berry tree id QFormLayout *l_form_radius_treeid = new QFormLayout(); this->combo_radius_treeid = new NoScrollComboBox(this); - this->combo_radius_treeid->setToolTip("The maximum sight range of a trainer,\n" - "OR the unique id of the berry tree."); + static const QString combo_radius_treeid_toolTip = Util::toHtmlParagraph("The maximum sight range of a trainer, OR the unique id of the berry tree."); + this->combo_radius_treeid->setToolTip(combo_radius_treeid_toolTip); l_form_radius_treeid->addRow("Sight Radius / Berry Tree ID", this->combo_radius_treeid); this->layout_contents->addLayout(l_form_radius_treeid); @@ -470,14 +474,16 @@ void CloneObjectFrame::setup() { // clone map id combo QFormLayout *l_form_dest_map = new QFormLayout(); this->combo_target_map = new NoScrollComboBox(this); - this->combo_target_map->setToolTip("The name of the map that the object being cloned is on."); + static const QString combo_target_map_toolTip = Util::toHtmlParagraph("The name of the map that the object being cloned is on."); + this->combo_target_map->setToolTip(combo_target_map_toolTip); l_form_dest_map->addRow("Target Map", this->combo_target_map); this->layout_contents->addLayout(l_form_dest_map); // clone local id combo QFormLayout *l_form_dest_id = new QFormLayout(); this->combo_target_id = new NoScrollComboBox(this); - this->combo_target_id->setToolTip("The Local ID name or number of the object being cloned."); + static const QString combo_target_id_toolTip = Util::toHtmlParagraph("The Local ID name or number of the object being cloned."); + this->combo_target_id->setToolTip(combo_target_id_toolTip); l_form_dest_id->addRow("Target Local ID", this->combo_target_id); this->layout_contents->addLayout(l_form_dest_id); @@ -577,21 +583,21 @@ void WarpFrame::setup() { // desination map combo QFormLayout *l_form_dest_map = new QFormLayout(); this->combo_dest_map = new NoScrollComboBox(this); - this->combo_dest_map->setToolTip("The destination map name of the warp."); + static const QString combo_dest_map_toolTip = Util::toHtmlParagraph("The destination map name of the warp."); + this->combo_dest_map->setToolTip(combo_dest_map_toolTip); l_form_dest_map->addRow("Destination Map", this->combo_dest_map); this->layout_contents->addLayout(l_form_dest_map); // desination warp id QFormLayout *l_form_dest_warp = new QFormLayout(); this->combo_dest_warp = new NoScrollComboBox(this); - this->combo_dest_warp->setToolTip("The warp id on the destination map."); + static const QString combo_dest_warp_toolTip = Util::toHtmlParagraph("The warp id on the destination map."); + this->combo_dest_warp->setToolTip(combo_dest_warp_toolTip); l_form_dest_warp->addRow("Destination Warp", this->combo_dest_warp); this->layout_contents->addLayout(l_form_dest_warp); // warning - static const QString warningText = "Warning:\n" - "This warp event is not positioned on a metatile with a warp behavior.\n" - "Click this warning for more details."; + auto warningText = QStringLiteral("Warning:\nThis warp event is not positioned on a metatile with a warp behavior.\nClick this warning for more details."); QVBoxLayout *l_vbox_warning = new QVBoxLayout(); this->warning = new QPushButton(warningText, this); this->warning->setFlat(true); @@ -683,22 +689,25 @@ void TriggerFrame::setup() { // script combo QFormLayout *l_form_script = new QFormLayout(); this->combo_script = new NoScrollComboBox(this); - this->combo_script->setToolTip("The script which is executed with this event."); + static const QString combo_script_toolTip = Util::toHtmlParagraph("The script that is executed with this event."); + this->combo_script->setToolTip(combo_script_toolTip); l_form_script->addRow("Script", this->combo_script); this->layout_contents->addLayout(l_form_script); // var combo QFormLayout *l_form_var = new QFormLayout(); this->combo_var = new NoScrollComboBox(this); - this->combo_var->setToolTip("The variable by which the script is triggered.\n" - "The script is triggered when this variable's value matches 'Var Value'."); + static const QString combo_var_toolTip = Util::toHtmlParagraph("The variable by which the script is triggered. " + "The script is triggered when this variable's value matches 'Var Value'."); + this->combo_var->setToolTip(combo_var_toolTip); l_form_var->addRow("Var", this->combo_var); this->layout_contents->addLayout(l_form_var); // var value combo QFormLayout *l_form_var_val = new QFormLayout(); this->combo_var_value = new NoScrollComboBox(this); - this->combo_var_value->setToolTip("The variable's value which triggers the script."); + static const QString combo_var_value_toolTip = Util::toHtmlParagraph("The variable's value that triggers the script."); + this->combo_var_value->setToolTip(combo_var_value_toolTip); l_form_var_val->addRow("Var Value", this->combo_var_value); this->layout_contents->addLayout(l_form_var_val); @@ -767,7 +776,8 @@ void WeatherTriggerFrame::setup() { // weather combo QFormLayout *l_form_weather = new QFormLayout(); this->combo_weather = new NoScrollComboBox(this); - this->combo_weather->setToolTip("The weather that starts when the player steps on this spot."); + static const QString combo_weather_toolTip = Util::toHtmlParagraph("The weather that starts when the player steps on this spot."); + this->combo_weather->setToolTip(combo_weather_toolTip); l_form_weather->addRow("Weather", this->combo_weather); this->layout_contents->addLayout(l_form_weather); @@ -815,15 +825,16 @@ void SignFrame::setup() { // facing dir combo QFormLayout *l_form_facing_dir = new QFormLayout(); this->combo_facing_dir = new NoScrollComboBox(this); - this->combo_facing_dir->setToolTip("The direction which the player must be facing\n" - "to be able to interact with this event."); + static const QString combo_facing_dir_toolTip = Util::toHtmlParagraph("The direction that the player must be facing to be able to interact with this event."); + this->combo_facing_dir->setToolTip(combo_facing_dir_toolTip); l_form_facing_dir->addRow("Player Facing Direction", this->combo_facing_dir); this->layout_contents->addLayout(l_form_facing_dir); // script combo QFormLayout *l_form_script = new QFormLayout(); this->combo_script = new NoScrollComboBox(this); - this->combo_script->setToolTip("The script which is executed with this event."); + static const QString combo_script_toolTip = Util::toHtmlParagraph("The script that is executed with this event."); + this->combo_script->setToolTip(combo_script_toolTip); l_form_script->addRow("Script", this->combo_script); this->layout_contents->addLayout(l_form_script); @@ -882,14 +893,16 @@ void HiddenItemFrame::setup() { // item combo QFormLayout *l_form_item = new QFormLayout(); this->combo_item = new NoScrollComboBox(this); - this->combo_item->setToolTip("The item to be given."); + static const QString combo_item_toolTip = Util::toHtmlParagraph("The item to be given."); + this->combo_item->setToolTip(combo_item_toolTip); l_form_item->addRow("Item", this->combo_item); this->layout_contents->addLayout(l_form_item); // flag combo QFormLayout *l_form_flag = new QFormLayout(); this->combo_flag = new NoScrollComboBox(this); - this->combo_flag->setToolTip("The flag which is set when the hidden item is picked up."); + static const QString combo_flag_toolTip = Util::toHtmlParagraph("The flag that is set when the hidden item is picked up."); + this->combo_flag->setToolTip(combo_flag_toolTip); l_form_flag->addRow("Flag", this->combo_flag); this->layout_contents->addLayout(l_form_flag); @@ -898,7 +911,8 @@ void HiddenItemFrame::setup() { QFormLayout *l_form_quantity = new QFormLayout(hideable_quantity); l_form_quantity->setContentsMargins(0, 0, 0, 0); this->spinner_quantity = new NoScrollSpinBox(hideable_quantity); - this->spinner_quantity->setToolTip("The number of items received when the hidden item is picked up."); + static const QString spinner_quantity_toolTip = Util::toHtmlParagraph("The number of items received when the hidden item is picked up."); + this->spinner_quantity->setToolTip(spinner_quantity_toolTip); this->spinner_quantity->setMinimum(0x01); this->spinner_quantity->setMaximum(0xFF); l_form_quantity->addRow("Quantity", this->spinner_quantity); @@ -909,7 +923,8 @@ void HiddenItemFrame::setup() { QFormLayout *l_form_itemfinder = new QFormLayout(hideable_itemfinder); l_form_itemfinder->setContentsMargins(0, 0, 0, 0); this->check_itemfinder = new QCheckBox(hideable_itemfinder); - this->check_itemfinder->setToolTip("If checked, hidden item can only be picked up using the Itemfinder"); + static const QString check_itemfinder_toolTip = Util::toHtmlParagraph("If checked, hidden item can only be picked up using the Itemfinder"); + this->check_itemfinder->setToolTip(check_itemfinder_toolTip); l_form_itemfinder->addRow("Requires Itemfinder", this->check_itemfinder); this->layout_contents->addWidget(hideable_itemfinder); @@ -996,9 +1011,9 @@ void SecretBaseFrame::setup() { // item combo QFormLayout *l_form_base_id = new QFormLayout(); this->combo_base_id = new NoScrollComboBox(this); - this->combo_base_id->setToolTip("The secret base id which is inside this secret\n" - "base entrance. Secret base ids are meant to be\n" - "unique to each and every secret base entrance."); + static const QString combo_base_id_toolTip = Util::toHtmlParagraph("The secret base id that is inside this secret base entrance. " + "Secret base ids are meant to be unique to each and every secret base entrance."); + this->combo_base_id->setToolTip(combo_base_id_toolTip); l_form_base_id->addRow("Secret Base", this->combo_base_id); this->layout_contents->addLayout(l_form_base_id); @@ -1048,7 +1063,8 @@ void HealLocationFrame::setup() { // ID QFormLayout *l_form_id = new QFormLayout(); this->line_edit_id = new QLineEdit(this); - this->line_edit_id->setToolTip("The unique identifier for this heal location."); + static const QString line_edit_id_toolTip = Util::toHtmlParagraph("The unique identifier for this heal location."); + this->line_edit_id->setToolTip(line_edit_id_toolTip); this->line_edit_id->setPlaceholderText(projectConfig.getIdentifier(ProjectIdentifier::define_heal_locations_prefix) + "MY_MAP"); l_form_id->addRow("ID", this->line_edit_id); this->layout_contents->addLayout(l_form_id); @@ -1058,7 +1074,8 @@ void HealLocationFrame::setup() { QFormLayout *l_form_respawn_map = new QFormLayout(hideable_respawn_map); l_form_respawn_map->setContentsMargins(0, 0, 0, 0); this->combo_respawn_map = new NoScrollComboBox(hideable_respawn_map); - this->combo_respawn_map->setToolTip("The map where the player will respawn after whiteout."); + static const QString combo_respawn_map_toolTip = Util::toHtmlParagraph("The map where the player will respawn after whiteout."); + this->combo_respawn_map->setToolTip(combo_respawn_map_toolTip); l_form_respawn_map->addRow("Respawn Map", this->combo_respawn_map); this->layout_contents->addWidget(hideable_respawn_map); @@ -1067,8 +1084,9 @@ void HealLocationFrame::setup() { QFormLayout *l_form_respawn_npc = new QFormLayout(hideable_respawn_npc); l_form_respawn_npc->setContentsMargins(0, 0, 0, 0); this->combo_respawn_npc = new NoScrollComboBox(hideable_respawn_npc); - this->combo_respawn_npc->setToolTip("The Local ID name or number of the NPC the player\n" - "interacts with upon respawning after whiteout."); + static const QString combo_respawn_npc_toolTip = Util::toHtmlParagraph("The Local ID name or number of the NPC the player " + "interacts with upon respawning after whiteout."); + this->combo_respawn_npc->setToolTip(combo_respawn_npc_toolTip); l_form_respawn_npc->addRow("Respawn NPC", this->combo_respawn_npc); this->layout_contents->addWidget(hideable_respawn_npc); diff --git a/src/ui/graphicsview.cpp b/src/ui/graphicsview.cpp index 6c4ccdb3..c8fc4495 100644 --- a/src/ui/graphicsview.cpp +++ b/src/ui/graphicsview.cpp @@ -64,3 +64,12 @@ Overlay * MapView::getOverlay(int layer) { } return overlay; } + +void ConnectionsView::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) { + emit pressedDelete(); + event->accept(); + } else { + QGraphicsView::keyPressEvent(event); + } +} diff --git a/src/ui/gridsettings.cpp b/src/ui/gridsettings.cpp index d3346f11..87e0896a 100644 --- a/src/ui/gridsettings.cpp +++ b/src/ui/gridsettings.cpp @@ -1,8 +1,6 @@ #include "ui_gridsettingsdialog.h" #include "gridsettings.h" -// TODO: Save settings in config - const QMap GridSettings::styleToName = { {Style::Solid, "Solid"}, {Style::LargeDashes, "Large Dashes"}, diff --git a/src/ui/mapheaderform.cpp b/src/ui/mapheaderform.cpp index 65fe6ead..a2a0b8b5 100644 --- a/src/ui/mapheaderform.cpp +++ b/src/ui/mapheaderform.cpp @@ -174,19 +174,29 @@ void MapHeaderForm::updateLocationName() { } // Set data in UI -void MapHeaderForm::setSong(const QString &song) { ui->comboBox_Song->setCurrentText(song); } -void MapHeaderForm::setLocation(const QString &location) { ui->comboBox_Location->setCurrentText(location); } -void MapHeaderForm::setLocationName(const QString &locationName) { ui->lineEdit_LocationName->setText(locationName); } +void MapHeaderForm::setSong(const QString &song) { setText(ui->comboBox_Song, song); } +void MapHeaderForm::setLocation(const QString &location) { setText(ui->comboBox_Location, location); } +void MapHeaderForm::setLocationName(const QString &locationName) { setText(ui->lineEdit_LocationName, locationName); } void MapHeaderForm::setRequiresFlash(bool requiresFlash) { ui->checkBox_RequiresFlash->setChecked(requiresFlash); } -void MapHeaderForm::setWeather(const QString &weather) { ui->comboBox_Weather->setCurrentText(weather); } -void MapHeaderForm::setType(const QString &type) { ui->comboBox_Type->setCurrentText(type); } -void MapHeaderForm::setBattleScene(const QString &battleScene) { ui->comboBox_BattleScene->setCurrentText(battleScene); } +void MapHeaderForm::setWeather(const QString &weather) { setText(ui->comboBox_Weather, weather); } +void MapHeaderForm::setType(const QString &type) { setText(ui->comboBox_Type, type); } +void MapHeaderForm::setBattleScene(const QString &battleScene) { setText(ui->comboBox_BattleScene, battleScene); } void MapHeaderForm::setShowsLocationName(bool showsLocationName) { ui->checkBox_ShowLocationName->setChecked(showsLocationName); } void MapHeaderForm::setAllowsRunning(bool allowsRunning) { ui->checkBox_AllowRunning->setChecked(allowsRunning); } void MapHeaderForm::setAllowsBiking(bool allowsBiking) { ui->checkBox_AllowBiking->setChecked(allowsBiking); } void MapHeaderForm::setAllowsEscaping(bool allowsEscaping) { ui->checkBox_AllowEscaping->setChecked(allowsEscaping); } void MapHeaderForm::setFloorNumber(int floorNumber) { ui->spinBox_FloorNumber->setValue(floorNumber); } +// If we always call setText / setCurrentText the user's cursor may move to the end of the text while they're typing. +void MapHeaderForm::setText(QComboBox *combo, const QString &text) const { + if (combo->currentText() != text) + combo->setCurrentText(text); +} +void MapHeaderForm::setText(QLineEdit *lineEdit, const QString &text) const { + if (lineEdit->text() != text) + lineEdit->setText(text); +} + // Read data from UI QString MapHeaderForm::song() const { return ui->comboBox_Song->currentText(); } QString MapHeaderForm::location() const { return ui->comboBox_Location->currentText(); } diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index f036d215..732d3e83 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -55,10 +55,7 @@ MapImageExporter::MapImageExporter(QWidget *parent, Project *project, Map *map, connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); connect(ui->pushButton_Cancel, &QPushButton::pressed, this, &MapImageExporter::close); - // Update the map selector when the text changes. - // We don't use QComboBox::currentTextChanged to avoid unnecessary re-rendering. - connect(ui->comboBox_MapSelection, QOverload::of(&QComboBox::currentIndexChanged), this, &MapImageExporter::updateMapSelection); - connect(ui->comboBox_MapSelection->lineEdit(), &QLineEdit::editingFinished, this, &MapImageExporter::updateMapSelection); + connect(ui->comboBox_MapSelection, &NoScrollComboBox::editingFinished, this, &MapImageExporter::updateMapSelection); connect(ui->checkBox_Objects, &QCheckBox::toggled, this, &MapImageExporter::setShowObjects); connect(ui->checkBox_Warps, &QCheckBox::toggled, this, &MapImageExporter::setShowWarps); @@ -605,9 +602,7 @@ QPixmap MapImageExporter::getFormattedMapPixmap() { QMargins MapImageExporter::getMargins(const Map *map) { QMargins margins; if (m_settings.showBorder) { - // The border may technically extend beyond BORDER_DISTANCE, but when the border is painted - // we will be limiting it to the visible sight range. - margins = QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16; + margins = m_project->getMetatileViewDistance() * 16; } else if (map && connectionsEnabled()) { for (const auto &connection : map->getConnections()) { const QString dir = connection->direction(); @@ -648,15 +643,12 @@ void MapImageExporter::paintBorder(QPainter *painter, Layout *layout) { layout->renderBorder(true); // Clip parts of the border that would be beyond player visibility. - QRect visibleArea(0, 0, layout->getWidth() * 16, layout->getHeight() * 16); - visibleArea += (QMargins(BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE, BORDER_DISTANCE) * 16); painter->save(); - painter->setClipRect(visibleArea); + painter->setClipRect(layout->getVisibleRect()); - int borderHorzDist = layout->getBorderDrawWidth(); - int borderVertDist = layout->getBorderDrawHeight(); - for (int y = -borderVertDist; y < layout->getHeight() + borderVertDist; y += layout->getBorderHeight()) - for (int x = -borderHorzDist; x < layout->getWidth() + borderHorzDist; x += layout->getBorderWidth()) { + const QMargins borderMargins = layout->getBorderMargins(); + for (int y = -borderMargins.top(); y < layout->getHeight() + borderMargins.bottom(); y += layout->getBorderHeight()) + for (int x = -borderMargins.left(); x < layout->getWidth() + borderMargins.right(); x += layout->getBorderWidth()) { // 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; diff --git a/src/ui/maplisttoolbar.cpp b/src/ui/maplisttoolbar.cpp index 2584c4fd..f56be39b 100644 --- a/src/ui/maplisttoolbar.cpp +++ b/src/ui/maplisttoolbar.cpp @@ -93,7 +93,7 @@ void MapListToolBar::setEmptyFoldersVisible(bool visible) { } // Update tool tip to reflect what will happen if the button is pressed. - const QString toolTip = QString("%1 empty folders in the list.").arg(visible ? "Hide" : "Show"); + const QString toolTip = Util::toHtmlParagraph(QString("%1 empty folders in the list.").arg(visible ? "Hide" : "Show")); ui->button_ToggleEmptyFolders->setToolTip(toolTip); const QSignalBlocker b(ui->button_ToggleEmptyFolders); @@ -121,7 +121,9 @@ void MapListToolBar::applyFilter(const QString &filterText) { return; const QSignalBlocker b(ui->lineEdit_filterBox); - ui->lineEdit_filterBox->setText(filterText); + if (ui->lineEdit_filterBox->text() != filterText) { + ui->lineEdit_filterBox->setText(filterText); + } // The clear button does not properly disappear when filterText is empty. // It seems like this is because blocking the QLineEdit's signals prevents diff --git a/src/ui/movablerect.cpp b/src/ui/movablerect.cpp index d867c598..afdf19a9 100644 --- a/src/ui/movablerect.cpp +++ b/src/ui/movablerect.cpp @@ -5,17 +5,21 @@ #include "movablerect.h" #include "utility.h" -MovableRect::MovableRect(bool *enabled, int width, int height, QRgb color) - : QGraphicsRectItem(0, 0, width, height) +MovableRect::MovableRect(bool *enabled, const QRectF &rect, const QRgb &color) + : QGraphicsRectItem(rect), + enabled(enabled), + baseRect(rect), + color(color) { - this->enabled = enabled; - this->color = color; this->setVisible(*enabled); } /// Center rect on grid position (x, y) void MovableRect::updateLocation(int x, int y) { - this->setRect((x * 16) - this->rect().width() / 2 + 8, (y * 16) - this->rect().height() / 2 + 8, this->rect().width(), this->rect().height()); + this->setRect(this->baseRect.x() + (x * 16), + this->baseRect.y() + (y * 16), + this->baseRect.width(), + this->baseRect.height()); this->setVisible(*this->enabled); } @@ -25,7 +29,7 @@ void MovableRect::updateLocation(int x, int y) { ResizableRect::ResizableRect(QObject *parent, bool *enabled, int width, int height, QRgb color) : QObject(parent), - MovableRect(enabled, width * 16, height * 16, color) + MovableRect(enabled, QRect(0, 0, width * 16, height * 16), color) { setAcceptHoverEvents(true); setFlags(this->flags() | QGraphicsItem::ItemIsMovable); diff --git a/src/ui/newlayoutform.cpp b/src/ui/newlayoutform.cpp index aa8178ce..22edd753 100644 --- a/src/ui/newlayoutform.cpp +++ b/src/ui/newlayoutform.cpp @@ -18,8 +18,8 @@ NewLayoutForm::NewLayoutForm(QWidget *parent) connect(ui->spinBox_MapWidth, QOverload::of(&QSpinBox::valueChanged), [=](int){ validateMapDimensions(); }); connect(ui->spinBox_MapHeight, QOverload::of(&QSpinBox::valueChanged), [=](int){ validateMapDimensions(); }); - connect(ui->comboBox_PrimaryTileset->lineEdit(), &QLineEdit::editingFinished, [this]{ validatePrimaryTileset(true); }); - connect(ui->comboBox_SecondaryTileset->lineEdit(), &QLineEdit::editingFinished, [this]{ validateSecondaryTileset(true); }); + connect(ui->comboBox_PrimaryTileset, &NoScrollComboBox::editingFinished, [this]{ validatePrimaryTileset(true); }); + connect(ui->comboBox_SecondaryTileset, &NoScrollComboBox::editingFinished, [this]{ validateSecondaryTileset(true); }); } NewLayoutForm::~NewLayoutForm() @@ -86,17 +86,14 @@ bool NewLayoutForm::validateMapDimensions() { int size = m_project->getMapDataSize(ui->spinBox_MapWidth->value(), ui->spinBox_MapHeight->value()); int maxSize = m_project->getMaxMapDataSize(); - // TODO: Get from project - const int additionalWidth = 15; - const int additionalHeight = 14; - QString errorText; if (size > maxSize) { + QSize addition = m_project->getMapSizeAddition(); errorText = QString("The specified width and height are too large.\n" "The maximum map width and height is the following: (width + %1) * (height + %2) <= %3\n" "The specified map width and height was: (%4 + %1) * (%5 + %2) = %6") - .arg(additionalWidth) - .arg(additionalHeight) + .arg(addition.width()) + .arg(addition.height()) .arg(maxSize) .arg(ui->spinBox_MapWidth->value()) .arg(ui->spinBox_MapHeight->value()) diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp index def341fd..b9938f3e 100644 --- a/src/ui/newmapconnectiondialog.cpp +++ b/src/ui/newmapconnectiondialog.cpp @@ -1,14 +1,15 @@ #include "newmapconnectiondialog.h" #include "ui_newmapconnectiondialog.h" +#include "message.h" NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const QStringList &mapNames) : QDialog(parent), - ui(new Ui::NewMapConnectionDialog) + ui(new Ui::NewMapConnectionDialog), + m_map(map) { ui->setupUi(this); setAttribute(Qt::WA_DeleteOnClose); - ui->comboBox_Direction->setEditable(false); ui->comboBox_Direction->addItems(MapConnection::cardinalDirections); ui->comboBox_Map->addItems(mapNames); @@ -16,7 +17,7 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const // Choose default direction QMap directionCounts; - for (auto connection : map->getConnections()) { + for (auto connection : m_map->getConnections()) { directionCounts[connection->direction()]++; } QString defaultDirection; @@ -33,7 +34,7 @@ NewMapConnectionDialog::NewMapConnectionDialog(QWidget *parent, Map* map, const QString defaultMapName; if (mapNames.isEmpty()) { defaultMapName = QString(); - } else if (mapNames.first() == map->name() && mapNames.length() > 1) { + } else if (mapNames.first() == m_map->name() && mapNames.length() > 1) { // Prefer not to connect the map to itself defaultMapName = mapNames.at(1); } else { @@ -62,11 +63,43 @@ void NewMapConnectionDialog::setWarningVisible(bool visible) { adjustSize(); } +bool NewMapConnectionDialog::askReplaceConnection(MapConnection *connection, const QString &newMapName) { + QString message = QString("%1 already has a %2 connection to '%3'. Replace it with a %2 connection to '%4'?") + .arg(m_map->name()) + .arg(connection->direction()) + .arg(connection->targetMapName()) + .arg(newMapName); + return QuestionMessage::show(message, this) == QMessageBox::Yes; +} + void NewMapConnectionDialog::accept() { if (!mapNameIsValid()) { setWarningVisible(true); return; } - emit accepted(new MapConnection(ui->comboBox_Map->currentText(), ui->comboBox_Direction->currentText())); + + const QString direction = ui->comboBox_Direction->currentText(); + const QString targetMapName = ui->comboBox_Map->currentText(); + + // This is a very niche use case. Normally the user should add Dive/Emerge map connections using the line edits at the top of + // the Connections tab, but because we allow custom direction names in this dialog's Direction drop-down, a user could type + // in "dive" or "emerge" and we have to decide what to do. If there's no existing Dive/Emerge map we can just add it normally + // as if they had typed in the regular line edits. If there's already an existing connection we need to replace it. + if (MapConnection::isDiving(direction)) { + MapConnection *connection = m_map->getConnection(direction); + if (connection) { + if (connection->targetMapName() != targetMapName) { + if (!askReplaceConnection(connection, targetMapName)) + return; // Canceled + emit connectionReplaced(targetMapName, direction); + } + // Replaced the diving connection (or no-op, if adding a diving connection with the same map name) + QDialog::accept(); + return; + } + // Adding a new diving connection that doesn't exist yet, proceed normally. + } + + emit newConnectionedAdded(targetMapName, direction); QDialog::accept(); } diff --git a/src/ui/noscrollcombobox.cpp b/src/ui/noscrollcombobox.cpp index 21de55a8..191c5c45 100644 --- a/src/ui/noscrollcombobox.cpp +++ b/src/ui/noscrollcombobox.cpp @@ -23,6 +23,11 @@ NoScrollComboBox::NoScrollComboBox(QWidget *parent) static const QRegularExpression re("[^\\s]*"); QValidator *validator = new QRegularExpressionValidator(re, this); this->setValidator(validator); + + // QComboBox (as of writing) has no 'editing finished' signal to capture + // changes made either through the text edit or the drop-down. + connect(this, QOverload::of(&QComboBox::activated), this, &NoScrollComboBox::editingFinished); + connect(this->lineEdit(), &QLineEdit::editingFinished, this, &NoScrollComboBox::editingFinished); } // On macOS QComboBox::setEditable and QComboBox::setLineEdit will override our changes to the focus policy, so we enforce it here. diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 365567f3..ccbf1ec3 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -136,6 +136,12 @@ void ProjectSettingsEditor::initUi() { ui->spinBox_UnusedTileCovered->setMaximum(Tile::maxValue); ui->spinBox_UnusedTileSplit->setMaximum(Tile::maxValue); ui->spinBox_MaxEvents->setMaximum(INT_MAX); + ui->spinBox_MapWidth->setMaximum(INT_MAX); + ui->spinBox_MapHeight->setMaximum(INT_MAX); + ui->spinBox_PlayerViewDistance_West->setMaximum(INT_MAX); + ui->spinBox_PlayerViewDistance_North->setMaximum(INT_MAX); + ui->spinBox_PlayerViewDistance_East->setMaximum(INT_MAX); + ui->spinBox_PlayerViewDistance_South->setMaximum(INT_MAX); // 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. @@ -167,7 +173,10 @@ void ProjectSettingsEditor::initUi() { bool ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &identifier, const QString &filepath) { if (project && project->disabledSettingsNames.contains(identifier)) { widget->setEnabled(false); - widget->setToolTip(QString("This value has been set using '%1' in %2").arg(identifier).arg(filepath)); + QString toolTip = QString("This value has been set using '%1' in %2").arg(identifier).arg(filepath); + if (!widget->toolTip().isEmpty()) + toolTip.prepend(QString("%1\n\n").arg(widget->toolTip())); + widget->setToolTip(Util::toHtmlParagraph(toolTip)); return true; } return false; @@ -456,8 +465,10 @@ void ProjectSettingsEditor::refresh() { ui->spinBox_Elevation->setValue(projectConfig.defaultElevation); ui->spinBox_Collision->setValue(projectConfig.defaultCollision); ui->spinBox_FillMetatile->setValue(projectConfig.defaultMetatileId); - ui->spinBox_MaxElevation->setValue(projectConfig.collisionSheetHeight - 1); - ui->spinBox_MaxCollision->setValue(projectConfig.collisionSheetWidth - 1); + ui->spinBox_MapWidth->setValue(projectConfig.defaultMapSize.width()); + ui->spinBox_MapHeight->setValue(projectConfig.defaultMapSize.height()); + ui->spinBox_MaxElevation->setValue(projectConfig.collisionSheetSize.height() - 1); + ui->spinBox_MaxCollision->setValue(projectConfig.collisionSheetSize.width() - 1); ui->spinBox_BehaviorMask->setValue(projectConfig.metatileBehaviorMask & ui->spinBox_BehaviorMask->maximum()); ui->spinBox_EncounterTypeMask->setValue(projectConfig.metatileEncounterTypeMask & ui->spinBox_EncounterTypeMask->maximum()); ui->spinBox_LayerTypeMask->setValue(projectConfig.metatileLayerTypeMask & ui->spinBox_LayerTypeMask->maximum()); @@ -469,6 +480,10 @@ void ProjectSettingsEditor::refresh() { ui->spinBox_UnusedTileCovered->setValue(projectConfig.unusedTileCovered); ui->spinBox_UnusedTileSplit->setValue(projectConfig.unusedTileSplit); ui->spinBox_MaxEvents->setValue(projectConfig.maxEventsPerGroup); + ui->spinBox_PlayerViewDistance_West->setValue(projectConfig.playerViewDistance.left()); + ui->spinBox_PlayerViewDistance_North->setValue(projectConfig.playerViewDistance.top()); + ui->spinBox_PlayerViewDistance_East->setValue(projectConfig.playerViewDistance.right()); + ui->spinBox_PlayerViewDistance_South->setValue(projectConfig.playerViewDistance.bottom()); // Set (and sync) border metatile IDs this->setBorderMetatileIds(false, projectConfig.newMapBorderMetatileIds); @@ -532,8 +547,8 @@ void ProjectSettingsEditor::save() { projectConfig.defaultElevation = ui->spinBox_Elevation->value(); projectConfig.defaultCollision = ui->spinBox_Collision->value(); projectConfig.defaultMetatileId = ui->spinBox_FillMetatile->value(); - projectConfig.collisionSheetHeight = ui->spinBox_MaxElevation->value() + 1; - projectConfig.collisionSheetWidth = ui->spinBox_MaxCollision->value() + 1; + projectConfig.defaultMapSize = QSize(ui->spinBox_MapWidth->value(), ui->spinBox_MapHeight->value()); + projectConfig.collisionSheetSize = QSize(ui->spinBox_MaxElevation->value() + 1, ui->spinBox_MaxCollision->value() + 1); projectConfig.metatileBehaviorMask = ui->spinBox_BehaviorMask->value(); projectConfig.metatileTerrainTypeMask = ui->spinBox_TerrainTypeMask->value(); projectConfig.metatileEncounterTypeMask = ui->spinBox_EncounterTypeMask->value(); @@ -545,6 +560,10 @@ void ProjectSettingsEditor::save() { projectConfig.unusedTileCovered = ui->spinBox_UnusedTileCovered->value(); projectConfig.unusedTileSplit = ui->spinBox_UnusedTileSplit->value(); projectConfig.maxEventsPerGroup = ui->spinBox_MaxEvents->value(); + projectConfig.playerViewDistance = QMargins(ui->spinBox_PlayerViewDistance_West->value(), + ui->spinBox_PlayerViewDistance_North->value(), + ui->spinBox_PlayerViewDistance_East->value(), + ui->spinBox_PlayerViewDistance_South->value()); // Save line edit settings projectConfig.prefabFilepath = ui->lineEdit_PrefabsPath->text(); diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index b66d442b..493ea78e 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -774,7 +774,10 @@ void RegionMapEditor::displayRegionMapTileSelector() { this->mapsquare_selector_item = new TilemapTileSelector(this->region_map->pngPath(), this->region_map->tilemapFormat(), this->region_map->palPath()); - this->mapsquare_selector_item->draw(); + // Initialize with current settings + this->mapsquare_selector_item->selectHFlip(ui->checkBox_tileHFlip->isChecked()); + this->mapsquare_selector_item->selectVFlip(ui->checkBox_tileVFlip->isChecked()); + this->mapsquare_selector_item->selectPalette(ui->spinBox_tilePalette->value()); // This will also draw the selector this->scene_region_map_tiles->addItem(this->mapsquare_selector_item); diff --git a/src/ui/resizelayoutpopup.cpp b/src/ui/resizelayoutpopup.cpp index 2b4f6c0b..2dc78d47 100644 --- a/src/ui/resizelayoutpopup.cpp +++ b/src/ui/resizelayoutpopup.cpp @@ -146,15 +146,18 @@ void ResizeLayoutPopup::setupLayoutView() { // Upper limits: maximum metatiles in a map formula: // max = (width + 15) * (height + 14) // This limit can be found in fieldmap.c in pokeruby/pokeemerald/pokefirered. - int numMetatiles = editor->project->getMapDataSize(rect.width() / 16, rect.height() / 16); - int maxMetatiles = editor->project->getMaxMapDataSize(); - if (numMetatiles > maxMetatiles) { - QString errorText = QString("The maximum layout width and height is the following: (width + 15) * (height + 14) <= %1\n" - "The specified layout width and height was: (%2 + 15) * (%3 + 14) = %4") - .arg(maxMetatiles) + int size = editor->project->getMapDataSize(rect.width() / 16, rect.height() / 16); + int maxSize = editor->project->getMaxMapDataSize(); + if (size > maxSize) { + QSize addition = editor->project->getMapSizeAddition(); + QString errorText = QString("The maximum layout width and height is the following: (width + %1) * (height + %2) <= %3\n" + "The specified layout width and height was: (%4 + %1) * (%5 + %2) = %6") + .arg(addition.width()) + .arg(addition.height()) + .arg(maxSize) .arg(rect.width() / 16) .arg(rect.height() / 16) - .arg(numMetatiles); + .arg(size); QMessageBox warning; warning.setIcon(QMessageBox::Warning); warning.setText("The specified width and height are too large."); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index 3d610fb9..eedc4215 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -33,6 +33,8 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) this->tileYFlip = ui->checkBox_yFlip->isChecked(); this->paletteId = ui->spinBox_paletteSelector->value(); + connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save); + ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider); ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes); @@ -94,7 +96,7 @@ void TilesetEditor::updateTilesets(QString primaryTilesetLabel, QString secondar QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes); if (result == QMessageBox::Yes) - this->on_actionSave_Tileset_triggered(); + this->save(); } this->setTilesets(primaryTilesetLabel, secondaryTilesetLabel); this->refresh(); @@ -125,16 +127,10 @@ void TilesetEditor::setTilesets(QString primaryTilesetLabel, QString secondaryTi } void TilesetEditor::initAttributesUi() { - // Update the metatile's attributes values when the attribute combo boxes are edited. - // We avoid using the 'currentTextChanged' signal here, we want to know when we can clean up the input field and commit changes. - connect(ui->comboBox_metatileBehaviors->lineEdit(), &QLineEdit::editingFinished, this, &TilesetEditor::commitMetatileBehavior); - connect(ui->comboBox_encounterType->lineEdit(), &QLineEdit::editingFinished, this, &TilesetEditor::commitEncounterType); - connect(ui->comboBox_terrainType->lineEdit(), &QLineEdit::editingFinished, this, &TilesetEditor::commitTerrainType); - connect(ui->comboBox_layerType->lineEdit(), &QLineEdit::editingFinished, this, &TilesetEditor::commitLayerType); - connect(ui->comboBox_metatileBehaviors, QOverload::of(&QComboBox::activated), this, &TilesetEditor::commitMetatileBehavior); - connect(ui->comboBox_encounterType, QOverload::of(&QComboBox::activated), this, &TilesetEditor::commitEncounterType); - connect(ui->comboBox_terrainType, QOverload::of(&QComboBox::activated), this, &TilesetEditor::commitTerrainType); - connect(ui->comboBox_layerType, QOverload::of(&QComboBox::activated), 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) { @@ -688,19 +684,23 @@ void TilesetEditor::commitLayerType() { this->metatileSelector->drawSelectedMetatile(); // Changing the layer type can affect how fully transparent metatiles appear } -void TilesetEditor::on_actionSave_Tileset_triggered() -{ +bool TilesetEditor::save() { // Need this temporary flag to stop selection resetting after saving. // This is a workaround; redrawing the map's metatile selector shouldn't emit the same signal as when it's selected. this->lockSelection = true; - this->project->saveTilesets(this->primaryTileset, this->secondaryTileset); + + bool success = this->project->saveTilesets(this->primaryTileset, this->secondaryTileset); emit this->tilesetsSaved(this->primaryTileset->name, this->secondaryTileset->name); if (this->paletteEditor) { this->paletteEditor->setTilesets(this->primaryTileset, this->secondaryTileset); } - this->ui->statusbar->showMessage(QString("Saved primary and secondary Tilesets!"), 5000); - this->hasUnsavedChanges = false; + this->ui->statusbar->showMessage(success ? QStringLiteral("Saved primary and secondary Tilesets!") + : QStringLiteral("Failed to save tilesets! See log for details."), 5000); + if (success) { + this->hasUnsavedChanges = false; + } this->lockSelection = false; + return success; } void TilesetEditor::on_actionImport_Primary_Tiles_triggered() @@ -812,8 +812,11 @@ void TilesetEditor::closeEvent(QCloseEvent *event) QMessageBox::Yes); if (result == QMessageBox::Yes) { - this->on_actionSave_Tileset_triggered(); - event->accept(); + if (this->save()) { + event->accept(); + } else { + event->ignore(); + } } else if (result == QMessageBox::No) { this->reset(); event->accept(); @@ -1143,12 +1146,6 @@ void TilesetEditor::countMetatileUsage() { this->metatileSelector->usedMetatiles.fill(0); for (auto layout : this->project->mapLayouts) { - // It's possible for a layout's tileset labels to change if they are invalid, - // so we need to load all the tilesets even if they aren't the tileset we're looking for. - // Otherwise the metatile usage counts may change because the layouts with invalid tilesets - // were updated to use a tileset we were looking for. - this->project->loadLayoutTilesets(layout); - bool usesPrimary = (layout->tileset_primary_label == this->primaryTileset->name); bool usesSecondary = (layout->tileset_secondary_label == this->secondaryTileset->name); @@ -1187,10 +1184,10 @@ void TilesetEditor::countTileUsage() { QSet secondaryTilesets; for (auto &layout : this->project->mapLayouts) { - this->project->loadLayoutTilesets(layout); 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); diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp index 056e7565..9957f33a 100644 --- a/src/ui/wildmonsearch.cpp +++ b/src/ui/wildmonsearch.cpp @@ -125,6 +125,7 @@ void WildMonSearch::updateResults(const QString &species) { const QList results = this->resultsCache.value(species, search(species)); if (results.isEmpty()) { static const RowData noResults = { + .mapName = "", .groupName = QStringLiteral("Species not found."), .fieldName = QStringLiteral("--"), .levelRange = QStringLiteral("--"),