diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index e4f0479b..67bba635 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -45,7 +45,6 @@ The filepath that Porymap expects for each file can be overridden on the ``Files src/data/region_map/region_map_sections.json, yes, yes, ``json_region_map_entries``, src/data/region_map/porymap_config.json, yes, yes, ``json_region_porymap_cfg``, include/constants/global.h, yes, no, ``constants_global``, reads ``define_obj_event_count`` - include/constants/map_groups.h, no, yes, ``constants_map_groups``, include/constants/items.h, yes, no, ``constants_items``, for Hidden Item events include/constants/flags.h, yes, no, ``constants_flags``, for Object and Hidden Item events include/constants/vars.h, yes, no, ``constants_vars``, for Trigger events @@ -93,6 +92,7 @@ In addition to these files, there are some specific symbol and macro names that ``symbol_spawn_npcs``, ``u8 sWhiteoutRespawnHealerNpcIds``, the type and table name for Heal Location ``Respawn NPC`` values ``symbol_attribute_table``, ``sMetatileAttrMasks``, optionally read to get settings on ``Tilesets`` tab ``symbol_tilesets_prefix``, ``gTileset_``, for new tileset names and to extract base tileset names + ``symbol_dynamic_map_name``, ``Dynamic``, reserved map name to display for ``define_map_dynamic`` ``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events ``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level ``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 6a373edc..39f59836 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -95,11 +95,11 @@ - + - Areas + Locations - + 0 @@ -116,10 +116,10 @@ 0 - + - + 0 @@ -1155,7 +1155,7 @@ 2 - + Layout @@ -1740,7 +1740,7 @@ 0 0 100 - 16 + 30 @@ -1834,7 +1834,7 @@ 0 0 100 - 16 + 30 @@ -1928,7 +1928,7 @@ 0 0 100 - 16 + 30 @@ -2028,7 +2028,7 @@ 0 0 100 - 16 + 30 @@ -2122,7 +2122,7 @@ 0 0 100 - 16 + 30 @@ -2220,7 +2220,7 @@ 0 - + false @@ -2236,206 +2236,10 @@ QFrame::Shadow::Raised - - - QFormLayout::FieldGrowthPolicy::FieldsStayAtSizeHint - - - 12 - + 9 - - - - Song - - - - - - - <html><head/><body><p>The default background music for this map.</p></body></html> - - - true - - - - - - - Location - - - - - - - <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is display when the player enters it.</p></body></html> - - - true - - - - - - - Requires Flash - - - - - - - <html><head/><body><p>Whether or not the map is dark and requires Flash to illuminate.</p></body></html> - - - - - - - - - - Weather - - - - - - - <html><head/><body><p>The default weather for this map.</p></body></html> - - - true - - - - - - - Type - - - - - - - <html><head/><body><p>The map type is a general attribute, which is used for many different things. For example. it determines whether biking or running is allowed.</p></body></html> - - - true - - - - - - - Battle scene - - - - - - - <html><head/><body><p>Determines the type of battle scene graphics to use.</p></body></html> - - - true - - - - - - - Show Location Name - - - - - - - <html><head/><body><p>Whether or not to display the location name when the player enters the map.</p></body></html> - - - - - - - - - - Allow Running - - - - - - - <html><head/><body><p>Allows the player to use Running Shoes</p></body></html> - - - - - - - - - - Allow Biking - - - - - - - <html><head/><body><p>Allows the player to use a Bike</p></body></html> - - - - - - - - - - Allow Dig & Escape Rope - - - - - - - <html><head/><body><p>Allows the player to use Dig or Escape Rope</p></body></html> - - - - - - - - - - Floor Number - - - - - - - <html><head/><body><p>Floor number to be used for maps with elevators.</p></body></html> - - - -128 - - - 127 - - - @@ -2896,8 +2700,8 @@ 0 0 - 204 - 16 + 100 + 30 @@ -3126,7 +2930,9 @@ + + @@ -3295,7 +3101,7 @@ Ctrl+T - + true @@ -3304,7 +3110,7 @@ :/icons/sort_alphabet.ico:/icons/sort_alphabet.ico - Sort by &Area + Sort by &Location @@ -3482,6 +3288,11 @@ Grid Settings... + + + New Layout... + + diff --git a/forms/mapheaderform.ui b/forms/mapheaderform.ui new file mode 100644 index 00000000..8faba290 --- /dev/null +++ b/forms/mapheaderform.ui @@ -0,0 +1,245 @@ + + + MapHeaderForm + + + + 0 + 0 + 407 + 349 + + + + Form + + + + QFormLayout::FieldGrowthPolicy::FieldsStayAtSizeHint + + + + + Song + + + + + + + <html><head/><body><p>The default background music for this map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Location + + + + + + + <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is displayed when the player enters it.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Requires Flash + + + + + + + <html><head/><body><p>If checked, the player will need to use Flash to see fully on this map.</p></body></html> + + + + + + + + + + Weather + + + + + + + <html><head/><body><p>The default weather on this map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Type + + + + + + + <html><head/><body><p>The map type is a general attribute, which is used for many different things. For example, underground type maps will have a special transition effect when the player enters/exits the map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Battle Scene + + + + + + + <html><head/><body><p>This field is used to help determine what graphics to use in the background of battles on this map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Show Location Name + + + + + + + <html><head/><body><p>If checked, a map name popup will appear when the player enters this map. The name that appears on this popup depends on the Location field.</p></body></html> + + + + + + + + + + Allow Running + + + + + + + <html><head/><body><p>If checked, the player will be allowed to run on this map.</p></body></html> + + + + + + + + + + Allow Biking + + + + + + + <html><head/><body><p>If checked, the player will be allowed to get on their bike on this map.</p></body></html> + + + + + + + + + + Allow Dig & Escape Rope + + + + + + + <html><head/><body><p>If checked, the player will be allowed to use Dig or Escape Rope on this map.</p></body></html> + + + + + + + + + + Floor Number + + + + + + + <html><head/><body><p>Floor number to be used for maps with elevators.</p></body></html> + + + + + + + Location Name + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + +
diff --git a/forms/newlayoutdialog.ui b/forms/newlayoutdialog.ui new file mode 100644 index 00000000..980b5351 --- /dev/null +++ b/forms/newlayoutdialog.ui @@ -0,0 +1,174 @@ + + + NewLayoutDialog + + + + 0 + 0 + 264 + 173 + + + + New Layout Options + + + true + + + true + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + true + + + + + 0 + 0 + 240 + 109 + + + + + 10 + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + + + + <html><head/><body><p>The constant that will be used to refer to this layout. It cannot be the same as any other existing layout.</p></body></html> + + + LAYOUT_MY_NEW_LAYOUT + + + true + + + + + + + Layout Name + + + + + + + Layout ID + + + + + + + <html><head/><body><p>The name of the new layout. The name cannot be the same as any other existing layout.</p></body></html> + + + MyNewLayout + + + true + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + + + + + NewLayoutForm + QWidget +
newlayoutform.h
+ 1 +
+
+ + +
diff --git a/forms/newlayoutform.ui b/forms/newlayoutform.ui new file mode 100644 index 00000000..bc3e7502 --- /dev/null +++ b/forms/newlayoutform.ui @@ -0,0 +1,244 @@ + + + NewLayoutForm + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map Dimensions + + + + + + + 0 + 0 + + + + Width + + + + + + + <html><head/><body><p>Width (in metatiles) of the new map.</p></body></html> + + + 1 + + + + + + + + 0 + 0 + + + + Height + + + + + + + <html><head/><body><p>Height (in metatiles) of the new map.</p></body></html> + + + 1 + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + + + + Border Dimensions + + + + + + + 0 + 0 + + + + Width + + + + + + + <html><head/><body><p>Width (in metatiles) of the new map's border.</p></body></html> + + + 1 + + + + + + + <html><head/><body><p>Height (in metatiles) of the new map's border.</p></body></html> + + + 1 + + + + + + + + 0 + 0 + + + + Height + + + + + + + + + + Tilesets + + + + + + Primary + + + + + + + <html><head/><body><p>The primary tileset for the new map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + Secondary + + + + + + + <html><head/><body><p>The secondary tileset for the new map.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
+
+ + +
diff --git a/forms/newlocationdialog.ui b/forms/newlocationdialog.ui new file mode 100644 index 00000000..285e2704 --- /dev/null +++ b/forms/newlocationdialog.ui @@ -0,0 +1,89 @@ + + + NewLocationDialog + + + + 0 + 0 + 252 + 124 + + + + true + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Location ID + + + + + + + true + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + + + + + + + + Location Name + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + false + + + + + + + + diff --git a/forms/newmapdialog.ui b/forms/newmapdialog.ui new file mode 100644 index 00000000..604800f8 --- /dev/null +++ b/forms/newmapdialog.ui @@ -0,0 +1,253 @@ + + + NewMapDialog + + + + 0 + 0 + 559 + 614 + + + + New Map Options + + + true + + + true + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Plain + + + QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents + + + true + + + + + 0 + 0 + 535 + 550 + + + + + 10 + + + + + Map Name + + + + + + + + QLayout::SizeConstraint::SetMinAndMaxSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + Layout ID + + + + + + + Map Group + + + + + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + <html><head/><body><p>If checked, a Heal Location will be added to this map automatically.</p></body></html> + + + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + + + + <html><head/><body><p>The name of the group this map will be added to.</p></body></html> + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + Can Fly To + + + + + + + <html><head/><body><p>The name of the new map. The name cannot be the same as any other existing map.</p></body></html> + + + MyNewMap + + + true + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::Reset + + + + + + + + NewLayoutForm + QWidget +
newlayoutform.h
+ 1 +
+ + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + +
diff --git a/forms/newmapgroupdialog.ui b/forms/newmapgroupdialog.ui new file mode 100644 index 00000000..d475b665 --- /dev/null +++ b/forms/newmapgroupdialog.ui @@ -0,0 +1,79 @@ + + + NewMapGroupDialog + + + + 0 + 0 + 252 + 87 + + + + true + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map Group Name + + + + + + + true + + + + + + + false + + + color: rgb(255, 0, 0) + + + + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + false + + + + + + + + diff --git a/forms/newmappopup.ui b/forms/newmappopup.ui deleted file mode 100644 index 3a83073f..00000000 --- a/forms/newmappopup.ui +++ /dev/null @@ -1,504 +0,0 @@ - - - NewMapPopup - - - - 0 - 0 - 410 - 687 - - - - New Map Options - - - - - - - false - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - QLayout::SetDefaultConstraint - - - 12 - - - - - Name - - - - - - - <html><head/><body><p>The name of the new map. If the name is invalid (red), it will be replaced with the default name of a new map.</p></body></html> - - - true - - - - - - - Group - - - - - - - <html><head/><body><p>New map group.</p></body></html> - - - true - - - - - - - Map Width - - - - - - - <html><head/><body><p>Width (in blocks) of the new map.</p></body></html> - - - 200 - - - - - - - Map Height - - - - - - - <html><head/><body><p>Height (in blocks) of the new map.</p></body></html> - - - 200 - - - - - - - Border Width - - - - - - - <html><head/><body><p>Width (in blocks) of the new map's border.</p></body></html> - - - 255 - - - - - - - Border Height - - - - - - - <html><head/><body><p>Height (in blocks) of the new map's border.</p></body></html> - - - 255 - - - - - - - Primary Tileset - - - - - - - <html><head/><body><p>The primary tileset for the new map.</p></body></html> - - - true - - - - - - - Secondary Tileset - - - - - - - <html><head/><body><p>The secondary tileset for the new map.</p></body></html> - - - true - - - - - - - Type - - - - - - - <html><head/><body><p>The map type is a general attribute, which is used for many different things. For example. it determines whether biking or running is allowed.</p></body></html> - - - true - - - - - - - Location - - - - - - - <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is displayed when the player enters it.</p></body></html> - - - true - - - - - - - Song - - - - - - - <html><head/><body><p>The default background music for this map.</p></body></html> - - - true - - - - - - - Can Fly To - - - - - - - <html><head/><body><p>Whether to add a heal location to the new map.</p></body></html> - - - - - - - - - - Show Location Name - - - - - - - <html><head/><body><p>Whether or not to display the location name when the player enters the map.</p></body></html> - - - - - - - - - - Allow Running - - - - - - - <html><head/><body><p>Allows the player to use Running Shoes</p></body></html> - - - - - - - - - - Allow Biking - - - - - - - <html><head/><body><p>Allows the player to use a Bike</p></body></html> - - - - - - - - - - Allow Dig & Escape Rope - - - - - - - <html><head/><body><p>Allows the player to use Dig or Escape Rope</p></body></html> - - - - - - - - - - Floor Number - - - - - - - <html><head/><body><p>Floor number to be used for maps with elevators.</p></body></html> - - - 127 - - - - - - - false - - - - - - - Layout - - - - - - - - - Use Existing Layout - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - Accept - - - - - - - true - - - - 0 - 0 - - - - false - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - false - - - ! - - - - - - - - 0 - 0 - - - - color: rgb(255, 0, 0) - - - - - - - - - - - - - - - 0 - 0 - 410 - 22 - - - - - - - - NoScrollComboBox - QComboBox -
noscrollcombobox.h
-
-
- - -
diff --git a/forms/newtilesetdialog.ui b/forms/newtilesetdialog.ui index c582eba0..13996c73 100644 --- a/forms/newtilesetdialog.ui +++ b/forms/newtilesetdialog.ui @@ -6,238 +6,147 @@ 0 0 - 400 - 216 + 450 + 209 - - - 0 - 0 - - Add new Tileset - - - - 0 - 0 - 400 - 216 - - - - - 0 - 0 - - - - - 10 - - - 10 - - - 10 - - - 10 - - - 6 - - - - - Qt::Vertical + + true + + + + + + + 0 - - - 20 - 40 - + + 0 - - - - - - - 0 - 0 - + + 0 - - - 380 - 161 - + + 0 - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 380 - 155 - - - - - 6 + + + + Name - - 10 + + + + + + true - - 10 + + + + + + false - - 10 + + color: rgb(255, 0, 0) - - - - Name - - + + + + + + + + + Type + + + + + + + false + + + + Primary + - - - - true - - + + + Secondary + - - - - Type - - - - - - - - Primary - - - - - Secondary - - - - - - - - Path - - - - - - - false - - - true - - - - - - - Symbol Name - - - - - - - false - - - true - - - - - - - Checkerboard Fill - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - false - - - - - +
+
+ + + + Checkerboard Fill + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 1 + + + + +
+
+ + + + + false + + + color: rgb(255, 0, 0) + + + + + + true + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + false + + + +
+ + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
- - - buttonBox - accepted() - NewTilesetDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - NewTilesetDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - + diff --git a/forms/regionmapeditor.ui b/forms/regionmapeditor.ui index 17cff63e..66787b7b 100644 --- a/forms/regionmapeditor.ui +++ b/forms/regionmapeditor.ui @@ -18,10 +18,10 @@ - QFrame::NoFrame + QFrame::Shape::NoFrame - QFrame::Plain + QFrame::Shadow::Plain @@ -46,14 +46,14 @@ - QComboBox::AdjustToContents + QComboBox::SizeAdjustPolicy::AdjustToContents - Qt::Horizontal + Qt::Orientation::Horizontal @@ -83,7 +83,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus 10 @@ -95,10 +95,10 @@ 30 - Qt::Vertical + Qt::Orientation::Vertical - QSlider::NoTicks + QSlider::TickPosition::NoTicks 1 @@ -108,7 +108,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -123,7 +123,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal false @@ -160,7 +160,7 @@ 0 0 466 - 351 + 336 @@ -182,7 +182,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -207,17 +207,17 @@ false - QAbstractScrollArea::AdjustIgnored + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored - QGraphicsView::NoDrag + QGraphicsView::DragMode::NoDrag - Qt::Vertical + Qt::Orientation::Vertical @@ -230,7 +230,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -243,7 +243,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -281,7 +281,7 @@ 0 0 466 - 351 + 336 @@ -315,17 +315,17 @@ false - QAbstractScrollArea::AdjustIgnored + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored - QGraphicsView::NoDrag + QGraphicsView::DragMode::NoDrag - Qt::Horizontal + Qt::Orientation::Horizontal @@ -338,7 +338,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -351,7 +351,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -364,7 +364,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -402,7 +402,7 @@ 0 0 466 - 351 + 336 @@ -436,17 +436,17 @@ false - QAbstractScrollArea::AdjustIgnored + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored - QGraphicsView::NoDrag + QGraphicsView::DragMode::NoDrag - Qt::Horizontal + Qt::Orientation::Horizontal @@ -459,7 +459,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -472,7 +472,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -485,7 +485,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -529,19 +529,19 @@ - Qt::ScrollBarAlwaysOn + Qt::ScrollBarPolicy::ScrollBarAlwaysOn - Qt::ScrollBarAsNeeded + Qt::ScrollBarPolicy::ScrollBarAsNeeded - QAbstractScrollArea::AdjustIgnored + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored true - Qt::AlignHCenter|Qt::AlignTop + Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop @@ -552,7 +552,7 @@ 8 0 278 - 342 + 327 @@ -563,7 +563,7 @@ - QLayout::SetDefaultConstraint + QLayout::SizeConstraint::SetDefaultConstraint 0 @@ -583,7 +583,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -596,7 +596,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -618,20 +618,20 @@ - Qt::ScrollBarAlwaysOff + Qt::ScrollBarPolicy::ScrollBarAlwaysOff - Qt::ScrollBarAlwaysOff + Qt::ScrollBarPolicy::ScrollBarAlwaysOff - QAbstractScrollArea::AdjustIgnored + QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored - Qt::Horizontal + Qt::Orientation::Horizontal @@ -648,10 +648,10 @@ - QFrame::Panel + QFrame::Shape::Panel - QFrame::Sunken + QFrame::Shadow::Sunken @@ -681,7 +681,7 @@ - + 15 @@ -719,10 +719,10 @@ - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised 1 @@ -747,7 +747,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -760,10 +760,10 @@ - QLayout::SetNoConstraint + QLayout::SizeConstraint::SetNoConstraint - + 0 @@ -771,10 +771,10 @@ - <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is display when the player enters it.</p></body></html> + <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is displayed when the player enters it.</p></body></html> - - true + + false @@ -803,7 +803,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -816,10 +816,10 @@ - + - + @@ -862,10 +862,10 @@ - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised 1 @@ -874,15 +874,15 @@ - QLayout::SetNoConstraint + QLayout::SizeConstraint::SetNoConstraint - + - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised @@ -893,10 +893,10 @@ - + - + @@ -909,7 +909,7 @@ - + true @@ -922,25 +922,25 @@ <html><head/><body><p>The section of the region map which the map is grouped under. This also determines the name of the map that is display when the player enters it.</p></body></html> - - true + + false - - + + Dimensions - + - QFrame::StyledPanel + QFrame::Shape::StyledPanel - QFrame::Raised + QFrame::Shadow::Raised @@ -951,10 +951,10 @@ - + - + @@ -966,7 +966,7 @@ - + Location @@ -980,16 +980,6 @@ - - - - Map Name - - - - - - @@ -1002,7 +992,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -1037,7 +1027,7 @@ - Qt::StrongFocus + Qt::FocusPolicy::StrongFocus 10 @@ -1049,10 +1039,10 @@ 30 - Qt::Vertical + Qt::Orientation::Vertical - QSlider::NoTicks + QSlider::TickPosition::NoTicks 1 @@ -1062,7 +1052,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -1082,7 +1072,7 @@ 0 0 957 - 22 + 37 @@ -1180,9 +1170,14 @@ NoScrollComboBox - QWidget + QComboBox
noscrollcombobox.h
+ + NoScrollSpinBox + QSpinBox +
noscrollspinbox.h
+
diff --git a/include/config.h b/include/config.h index 1a588c0a..926e93c6 100644 --- a/include/config.h +++ b/include/config.h @@ -71,6 +71,7 @@ public: this->showTilesetEditorDivider = false; this->monitorFiles = true; this->tilesetCheckerboardFill = true; + this->newMapHeaderSectionExpanded = false; this->theme = "default"; this->wildMonChartTheme = ""; this->textEditorOpenFolder = ""; @@ -123,6 +124,7 @@ public: bool showTilesetEditorDivider; bool monitorFiles; bool tilesetCheckerboardFill; + bool newMapHeaderSectionExpanded; QString theme; QString wildMonChartTheme; QString textEditorOpenFolder; @@ -135,6 +137,8 @@ public: QVersionNumber lastUpdateCheckVersion; QMap rateLimitTimes; QByteArray wildMonChartGeometry; + QByteArray newMapDialogGeometry; + QByteArray newLayoutDialogGeometry; protected: virtual QString getConfigFilepath() override; @@ -187,6 +191,7 @@ enum ProjectIdentifier { symbol_spawn_npcs, symbol_attribute_table, symbol_tilesets_prefix, + symbol_dynamic_map_name, define_obj_event_count, define_min_level, define_max_level, @@ -256,7 +261,6 @@ enum ProjectFilePath { data_pokemon_gfx, data_heal_locations, constants_global, - constants_map_groups, constants_items, constants_flags, constants_vars, @@ -328,7 +332,7 @@ public: QString getCustomFilePath(ProjectFilePath pathId); QString getCustomFilePath(const QString &pathId); QString getFilePath(ProjectFilePath pathId); - void setIdentifier(ProjectIdentifier id, const QString &text); + void setIdentifier(ProjectIdentifier id, QString text); void setIdentifier(const QString &id, const QString &text); QString getCustomIdentifier(ProjectIdentifier id); QString getCustomIdentifier(const QString &id); diff --git a/include/core/advancemapparser.h b/include/core/advancemapparser.h new file mode 100644 index 00000000..c3cb76cb --- /dev/null +++ b/include/core/advancemapparser.h @@ -0,0 +1,18 @@ +#ifndef ADVANCEMAPPARSER_H +#define ADVANCEMAPPARSER_H + +#include +#include +#include + +class Project; +class Layout; +class Metatile; + +namespace AdvanceMapParser { + Layout *parseLayout(const QString &filepath, bool *error, const Project *project); + QList parseMetatiles(const QString &filepath, bool *error, bool primaryTileset); + QList parsePalette(const QString &filepath, bool *error); +}; + +#endif // ADVANCEMAPPARSER_H diff --git a/include/core/map.h b/include/core/map.h index acc52d90..9cc2a69b 100644 --- a/include/core/map.h +++ b/include/core/map.h @@ -7,6 +7,7 @@ #include "maplayout.h" #include "tileset.h" #include "events.h" +#include "mapheader.h" #include #include @@ -34,61 +35,56 @@ class Map : public QObject Q_OBJECT public: explicit Map(QObject *parent = nullptr); + explicit Map(const Map &other, QObject *parent = nullptr); ~Map(); public: - QString name; - QString constantName; + void setName(const QString &mapName) { m_name = mapName; } + QString name() const { return m_name; } - QString song; - QString layoutId; - QString location; - bool requiresFlash; - QString weather; - QString type; - bool show_location; - bool allowRunning; - bool allowBiking; - bool allowEscaping; - int floorNumber = 0; - QString battle_scene; + void setConstantName(const QString &constantName) { m_constantName = constantName; } + QString constantName() const { return m_constantName; } - QString sharedEventsMap = ""; - QString sharedScriptsMap = ""; - - QStringList scriptsFileLabels; - QMap customHeaders; - - Layout *layout = nullptr; - void setLayout(Layout *layout); - - bool isPersistedToFile = true; - bool hasUnsavedDataChanges = false; - - bool needsLayoutDir = true; - bool needsHealLocation = false; - bool scriptsLoaded = false; - - QMap> events; - QList ownedEvents; // for memory management - - QList metatileLayerOrder; - QList metatileLayerOpacity; - - void setName(QString mapName); static QString mapConstantFromName(QString mapName, bool includePrefix = true); - int getWidth(); - int getHeight(); - int getBorderWidth(); - int getBorderHeight(); + void setLayout(Layout *layout); + Layout* layout() const { return m_layout; } - QList getAllEvents() const; + void setLayoutId(const QString &layoutId) { m_layoutId = layoutId; } + QString layoutId() const { return m_layoutId; } + + int getWidth() const; + int getHeight() const; + int getBorderWidth() const; + int getBorderHeight() const; + + void setHeader(const MapHeader &header) { *m_header = header; } + MapHeader* header() const { return m_header; } + + void setSharedEventsMap(const QString &sharedEventsMap) { m_sharedEventsMap = sharedEventsMap; } + void setSharedScriptsMap(const QString &sharedScriptsMap) { m_sharedScriptsMap = sharedScriptsMap; } + + QString sharedEventsMap() const { return m_sharedEventsMap; } + QString sharedScriptsMap() const { return m_sharedScriptsMap; } + + void setNeedsHealLocation(bool needsHealLocation) { m_needsHealLocation = needsHealLocation; } + void setIsPersistedToFile(bool persistedToFile) { m_isPersistedToFile = persistedToFile; } + void setHasUnsavedDataChanges(bool unsavedDataChanges) { m_hasUnsavedDataChanges = unsavedDataChanges; } + + bool needsHealLocation() const { return m_needsHealLocation; } + bool isPersistedToFile() const { return m_isPersistedToFile; } + bool hasUnsavedDataChanges() const { return m_hasUnsavedDataChanges; } + + void resetEvents(); + QList getEvents(Event::Group group = Event::Group::None) const; + Event* getEvent(Event::Group group, int index) const; + int getNumEvents(Event::Group group = Event::Group::None) const; QStringList getScriptLabels(Event::Group group = Event::Group::None); QString getScriptsFilePath() const; void openScript(QString label); void removeEvent(Event *); void addEvent(Event *); + int getIndexOfEvent(Event *) const; void deleteConnections(); QList getConnections() const; @@ -98,18 +94,47 @@ public: QRect getConnectionRect(const QString &direction, Layout *fromLayout = nullptr); QPixmap renderConnection(const QString &direction, Layout *fromLayout = nullptr); - QUndoStack editHistory; + QUndoStack* editHistory() const { return m_editHistory; } + void commit(QUndoCommand*); void modify(); - void clean(); + void setClean(); bool hasUnsavedChanges() const; void pruneEditHistory(); + void setCustomAttributes(const QMap &attributes) { m_customAttributes = attributes; } + QMap customAttributes() const { return m_customAttributes; } + private: + QString m_name; + QString m_constantName; + QString m_layoutId; + QString m_sharedEventsMap = ""; + QString m_sharedScriptsMap = ""; + + QStringList m_scriptsFileLabels; + QMap m_customAttributes; + + MapHeader *m_header = nullptr; + Layout *m_layout = nullptr; + + bool m_isPersistedToFile = true; + bool m_hasUnsavedDataChanges = false; + bool m_needsHealLocation = false; + bool m_scriptsLoaded = false; + + QMap> m_events; + QSet m_ownedEvents; // for memory management + + QList m_metatileLayerOrder; + QList m_metatileLayerOpacity; + void trackConnection(MapConnection*); // MapConnections in 'ownedConnections' but not 'connections' persist in the edit history. - QList connections; - QSet ownedConnections; + QList m_connections; + QSet m_ownedConnections; + + QUndoStack *m_editHistory = nullptr; signals: void modified(); diff --git a/include/core/mapheader.h b/include/core/mapheader.h new file mode 100644 index 00000000..2058280a --- /dev/null +++ b/include/core/mapheader.h @@ -0,0 +1,83 @@ +#ifndef MAPHEADER_H +#define MAPHEADER_H + +#include + +class MapHeader : public QObject +{ + Q_OBJECT +public: + MapHeader(QObject *parent = nullptr) : QObject(parent) {}; + ~MapHeader() {}; + MapHeader(const MapHeader& other); + MapHeader& operator=(const MapHeader& other); + bool operator==(const MapHeader& other) const { + return m_song == other.m_song + && m_location == other.m_location + && m_requiresFlash == other.m_requiresFlash + && m_weather == other.m_weather + && m_type == other.m_type + && m_showsLocationName == other.m_showsLocationName + && m_allowsRunning == other.m_allowsRunning + && m_allowsBiking == other.m_allowsBiking + && m_allowsEscaping == other.m_allowsEscaping + && m_floorNumber == other.m_floorNumber + && m_battleScene == other.m_battleScene; + } + bool operator!=(const MapHeader& other) const { + return !(operator==(other)); + } + + void setSong(const QString &song); + void setLocation(const QString &location); + void setRequiresFlash(bool requiresFlash); + void setWeather(const QString &weather); + void setType(const QString &type); + void setShowsLocationName(bool showsLocationName); + void setAllowsRunning(bool allowsRunning); + void setAllowsBiking(bool allowsBiking); + void setAllowsEscaping(bool allowsEscaping); + void setFloorNumber(int floorNumber); + void setBattleScene(const QString &battleScene); + + QString song() const { return m_song; } + QString location() const { return m_location; } + bool requiresFlash() const { return m_requiresFlash; } + QString weather() const { return m_weather; } + QString type() const { return m_type; } + bool showsLocationName() const { return m_showsLocationName; } + bool allowsRunning() const { return m_allowsRunning; } + bool allowsBiking() const { return m_allowsBiking; } + bool allowsEscaping() const { return m_allowsEscaping; } + int floorNumber() const { return m_floorNumber; } + QString battleScene() const { return m_battleScene; } + +signals: + void songChanged(QString); + void locationChanged(QString); + void requiresFlashChanged(bool); + void weatherChanged(QString); + void typeChanged(QString); + void showsLocationNameChanged(bool); + void allowsRunningChanged(bool); + void allowsBikingChanged(bool); + void allowsEscapingChanged(bool); + void floorNumberChanged(int); + void battleSceneChanged(QString); + void modified(); + +private: + QString m_song; + QString m_location; + bool m_requiresFlash = false; + QString m_weather; + QString m_type; + bool m_showsLocationName = false; + bool m_allowsRunning = false; + bool m_allowsBiking = false; + bool m_allowsEscaping = false; + int m_floorNumber = 0; + QString m_battleScene; +}; + +#endif // MAPHEADER_H diff --git a/include/core/maplayout.h b/include/core/maplayout.h index b617002f..1579b4ad 100644 --- a/include/core/maplayout.h +++ b/include/core/maplayout.h @@ -18,8 +18,11 @@ class Layout : public QObject { Q_OBJECT public: Layout() {} + Layout(const Layout &other); static QString layoutConstantFromName(QString mapName); + static QString defaultSuffix(); + bool loaded = false; @@ -70,24 +73,28 @@ public: QUndoStack editHistory; // to simplify new layout settings transfer between functions - struct SimpleSettings { + struct Settings { QString id; QString name; + // The name of a new layout's folder in `data/layouts/` is not always the same as the layout's name + // (e.g. the majority of the default layouts use the name of their associated map). + QString folderName; int width; int height; - QString tileset_primary_label; - QString tileset_secondary_label; - QString from_id = QString(); + int borderWidth; + int borderHeight; + QString primaryTilesetLabel; + QString secondaryTilesetLabel; }; + Settings settings() const; -public: - Layout *copy(); - void copyFrom(Layout *other); + Layout *copy() const; + void copyFrom(const Layout *other); - int getWidth(); - int getHeight(); - int getBorderWidth(); - int getBorderHeight(); + int getWidth() const { return width; } + int getHeight() const { return height; } + int getBorderWidth() const { return border_width; } + int getBorderHeight() const { return border_height; } bool isWithinBounds(int x, int y); bool isWithinBorderBounds(int x, int y); diff --git a/include/core/mapparser.h b/include/core/mapparser.h deleted file mode 100644 index 4032154a..00000000 --- a/include/core/mapparser.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef MAPPARSER_H -#define MAPPARSER_H - -#include "maplayout.h" -#include "project.h" -#include -#include - -class MapParser -{ -public: - MapParser(); - Layout *parse(QString filepath, bool *error, Project *project); -}; - -#endif // MAPPARSER_H diff --git a/include/core/metatileparser.h b/include/core/metatileparser.h deleted file mode 100644 index b85e5b36..00000000 --- a/include/core/metatileparser.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#ifndef METATILEPARSER_H -#define METATILEPARSER_H - -#include "metatile.h" -#include - -namespace MetatileParser { - QList parse(QString filepath, bool *error, bool primaryTileset); -} - -#endif // METATILEPARSER_H diff --git a/include/core/parseutil.h b/include/core/parseutil.h index e3df8375..d184646b 100644 --- a/include/core/parseutil.h +++ b/include/core/parseutil.h @@ -5,6 +5,7 @@ #include "heallocation.h" #include "log.h" #include "orderedjson.h" +#include "orderedmap.h" #include #include @@ -57,7 +58,7 @@ public: QMap readCDefinesByRegex(const QString &filename, const QStringList ®exList); QMap readCDefinesByName(const QString &filename, const QStringList &names); QStringList readCDefineNames(const QString &filename, const QStringList ®exList); - QMap> readCStructs(const QString &, const QString & = "", const QHash = { }); + tsl::ordered_map> readCStructs(const QString &, const QString & = "", const QHash& = {}); QList getLabelMacros(const QList&, const QString&); QStringList getLabelValues(const QList&, const QString&); bool tryParseJsonFile(QJsonDocument *out, const QString &filepath); diff --git a/include/core/regionmap.h b/include/core/regionmap.h index 822df79b..18362663 100644 --- a/include/core/regionmap.h +++ b/include/core/regionmap.h @@ -33,7 +33,6 @@ struct LayoutSquare struct MapSectionEntry { - QString name = ""; int x = 0; int y = 0; int width = 1; diff --git a/include/core/tileset.h b/include/core/tileset.h index bb7631d7..999e731d 100644 --- a/include/core/tileset.h +++ b/include/core/tileset.h @@ -38,8 +38,6 @@ public: QList> palettes; QList> palettePreviews; - bool hasUnsavedTilesImage; - static Tileset* getMetatileTileset(int, Tileset*, Tileset*); static Tileset* getTileTileset(int, Tileset*, Tileset*); static Metatile* getMetatile(int, Tileset*, Tileset*); @@ -56,10 +54,25 @@ public: static QHash getHeaderMemberMap(bool usingAsm); static QString getExpectedDir(QString tilesetName, bool isSecondary); QString getExpectedDir(); + + void load(); + void loadMetatiles(); + void loadMetatileAttributes(); + void loadTilesImage(QImage *importedImage = nullptr); + void loadPalettes(); + + void save(); + void saveMetatileAttributes(); + void saveMetatiles(); + void saveTilesImage(); + void savePalettes(); + bool appendToHeaders(QString root, QString friendlyName, bool usingAsm); bool appendToGraphics(QString root, QString friendlyName, bool usingAsm); bool appendToMetatiles(QString root, QString friendlyName, bool usingAsm); + void setTilesImage(const QImage &image); + void setMetatiles(const QList &metatiles); void addMetatile(Metatile* metatile); @@ -72,6 +85,7 @@ public: private: QList m_metatiles; + bool m_hasUnsavedTilesImage = false; }; #endif // TILESET_H diff --git a/include/core/validator.h b/include/core/validator.h new file mode 100644 index 00000000..f5f2ef91 --- /dev/null +++ b/include/core/validator.h @@ -0,0 +1,66 @@ +#ifndef VALIDATOR_H +#define VALIDATOR_H + +/* + This file contains our subclasses of QValidator. + + - PrefixValidator is for input widgets that want to enforce a particular prefix. + It differs from a QRegularExpressionValidator with a prefix in the regex because + it will automatically enforce the prefix in fixup() if it isn't present. + It's preferable to QLineEdit's input mask because it won't affect cursor behavior. + + - IdentifierValidator is for validating that input text can be used for the name of an identifier in the project. + (i.e., starts with a letter or underscore, then may continue with letters, numbers, or underscores). + Unless a prefix is specified this is a normal QRegularExpressionValidator, we only have a subclass because we use it so often. + + - UppercaseValidator is just a validator that uppercases input text. +*/ + +#include + +class PrefixValidator : public QRegularExpressionValidator { + Q_OBJECT + +public: + explicit PrefixValidator(const QString &prefix, QObject *parent = nullptr) + : QRegularExpressionValidator(parent), m_prefix(prefix) {}; + explicit PrefixValidator(const QString &prefix, const QRegularExpression &re, QObject *parent = nullptr) + : QRegularExpressionValidator(re, parent), m_prefix(prefix) {}; + ~PrefixValidator() {}; + + virtual QValidator::State validate(QString &input, int &) const override; + virtual void fixup(QString &input) const override; + + QString prefix() const { return m_prefix; } + void setPrefix(const QString &prefix); + + bool isValid(QString &input) const; + +private: + QString m_prefix; + + bool missingPrefix(const QString &input) const; +}; + +class IdentifierValidator : public PrefixValidator { + Q_OBJECT + +public: + explicit IdentifierValidator(QObject *parent = nullptr) + : PrefixValidator("", re_identifier, parent) {}; + explicit IdentifierValidator(const QString &prefix, QObject *parent = nullptr) + : PrefixValidator(prefix, re_identifier, parent) {}; + ~IdentifierValidator() {}; + +private: + static const QRegularExpression re_identifier; +}; + +class UppercaseValidator : public QValidator { + virtual QValidator::State validate(QString &input, int &) const override { + input = input.toUpper(); + return QValidator::Acceptable; + } +}; + +#endif // VALIDATOR_H diff --git a/include/lib/collapsiblesection.h b/include/lib/collapsiblesection.h new file mode 100644 index 00000000..b74835f4 --- /dev/null +++ b/include/lib/collapsiblesection.h @@ -0,0 +1,69 @@ +/* + Elypson/qt-collapsible-section + (c) 2016 Michael A. Voelkel - michael.alexander.voelkel@gmail.com + + This file is part of Elypson/qt-collapsible section. + + Elypson/qt-collapsible-section is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Elypson/qt-collapsible-section is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Elypson/qt-collapsible-section. If not, see . + + + PORYMAP NOTE: Modified to support having the section expanded by default, to stop the contents + squashing during the collapse animation, and to add some guard rails against crashes. +*/ + +#ifndef COLLAPSIBLESECTION_H +#define COLLAPSIBLESECTION_H + +#include +#include +#include +#include +#include +#include +#include +#include + +class CollapsibleSection : public QWidget +{ + Q_OBJECT + +public: + explicit CollapsibleSection(const QString& title = "", const bool expanded = false, const int animationDuration = 0, QWidget* parent = 0); + + void setContentLayout(QLayout* contentLayout); + void setTitle(const QString &title); + bool isExpanded() const { return this->expanded; } + +public slots: + void toggle(bool collapsed); + +private: + QGridLayout* mainLayout; + QToolButton* toggleButton; + QFrame* headerLine; + QParallelAnimationGroup* toggleAnimation; + QSet sectionAnimations; + QPropertyAnimation* contentAnimation; + QScrollArea* contentArea; + int animationDuration; + int collapsedHeight; + bool expanded; + + void updateToggleButton(); + void updateAnimationTargets(); + int getContentHeight() const; + +}; + +#endif // COLLAPSIBLESECTION_H diff --git a/include/lib/fex/parser.h b/include/lib/fex/parser.h index b73dd81e..e69b2c40 100644 --- a/include/lib/fex/parser.h +++ b/include/lib/fex/parser.h @@ -9,6 +9,7 @@ #include "array_value.h" #include "define_statement.h" #include "lexer.h" +#include "orderedmap.h" namespace fex { @@ -19,9 +20,9 @@ namespace fex std::vector Parse(std::vector tokens); std::vector ParseTopLevelArrays(std::vector tokens); - std::map ParseTopLevelObjects(std::vector tokens); + tsl::ordered_map ParseTopLevelObjects(std::vector tokens); - std::map ReadDefines(const QString &filename, std::vector matching); + tsl::ordered_map ReadDefines(const QString &filename, std::vector matching); private: int EvaluateExpression(std::vector tokens); diff --git a/include/mainwindow.h b/include/mainwindow.h index a4d60697..1ee612bc 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -22,8 +22,6 @@ #include "mapimageexporter.h" #include "filterchildrenproxymodel.h" #include "maplistmodels.h" -#include "newmappopup.h" -#include "newtilesetdialog.h" #include "shortcutseditor.h" #include "preferenceeditor.h" #include "projectsettingseditor.h" @@ -32,6 +30,8 @@ #include "wildmonchart.h" #include "updatepromoter.h" #include "aboutporymap.h" +#include "mapheaderform.h" +#include "newlayoutdialog.h" @@ -180,36 +180,29 @@ private slots: void duplicate(); void setClipboardData(poryjson::Json::object); void setClipboardData(QImage); + void setClipboardData(const QString &text); void copy(); void paste(); void onLayoutChanged(Layout *layout); void onOpenConnectedMap(MapConnection*); void onTilesetsSaved(QString, QString); - void openNewMapPopupWindow(); - void onNewMapCreated(); + void onNewMapCreated(Map *newMap, const QString &groupName); + void onNewMapGroupCreated(const QString &groupName); + void onNewMapSectionCreated(const QString &idName); + void onMapSectionDisplayNameChanged(const QString &idName, const QString &displayName); + void onNewLayoutCreated(Layout *layout); + void onNewTilesetCreated(Tileset *tileset); void onMapLoaded(Map *map); - void importMapFromAdvanceMap1_92(); void onMapRulerStatusChanged(const QString &); void applyUserShortcuts(); void markMapEdited(); void markSpecificMapEdited(Map*); - void on_action_NewMap_triggered(); void on_actionNew_Tileset_triggered(); void on_action_Save_triggered(); void on_action_Exit_triggered(); - void on_comboBox_Song_currentTextChanged(const QString &arg1); - void on_comboBox_Location_currentTextChanged(const QString &arg1); - void on_comboBox_Weather_currentTextChanged(const QString &arg1); - void on_comboBox_Type_currentTextChanged(const QString &arg1); - void on_comboBox_BattleScene_currentTextChanged(const QString &arg1); - void on_comboBox_LayoutSelector_currentTextChanged(const QString &arg1); - void on_checkBox_ShowLocation_stateChanged(int selected); - void on_checkBox_AllowRunning_stateChanged(int selected); - void on_checkBox_AllowBiking_stateChanged(int selected); - void on_checkBox_AllowEscaping_stateChanged(int selected); - void on_spinBox_FloorNumber_valueChanged(int offset); + void on_comboBox_LayoutSelector_currentTextChanged(const QString &text); void on_actionShortcuts_triggered(); void on_actionZoom_In_triggered(); @@ -254,7 +247,6 @@ private slots: void on_comboBox_SecondaryTileset_currentTextChanged(const QString &arg1); void on_pushButton_ChangeDimensions_clicked(); void on_checkBox_smartPaths_stateChanged(int selected); - void on_checkBox_Visibility_stateChanged(int selected); void on_checkBox_ToggleBorder_stateChanged(int selected); void resetMapViewScale(); @@ -313,7 +305,6 @@ private: QPointer regionMapEditor = nullptr; QPointer shortcutsEditor = nullptr; QPointer mapImageExporter = nullptr; - QPointer newMapPrompt = nullptr; QPointer preferenceEditor = nullptr; QPointer projectSettingsEditor = nullptr; QPointer gridSettingsDialog = nullptr; @@ -321,8 +312,8 @@ private: QPointer groupListProxyModel = nullptr; QPointer mapGroupModel = nullptr; - QPointer areaListProxyModel = nullptr; - QPointer mapAreaModel = nullptr; + QPointer locationListProxyModel = nullptr; + QPointer mapLocationModel = nullptr; QPointer layoutListProxyModel = nullptr; QPointer layoutTreeModel = nullptr; @@ -337,10 +328,11 @@ private: QAction *copyAction = nullptr; QAction *pasteAction = nullptr; + MapHeaderForm *mapHeaderForm = nullptr; + QMap lastSelectedEvent; bool isProgrammaticEventTabChange; - bool newMapDefaultsSet = false; bool tilesetNeedsRedraw = false; @@ -360,8 +352,15 @@ private: bool setProjectUI(); void clearProjectUI(); + void openNewMapDialog(); + void openDuplicateMapDialog(const QString &mapName); + NewLayoutDialog* createNewLayoutDialog(const Layout *layoutToCopy = nullptr); + void openNewLayoutDialog(); + void openDuplicateLayoutDialog(const QString &layoutId); + void openNewMapGroupDialog(); + void openNewLocationDialog(); void openSubWindow(QWidget * window); - void scrollMapList(MapTree *list, QString itemName); + void scrollMapList(MapTree *list, const QString &itemName); void scrollMapListToCurrentMap(MapTree *list); void scrollMapListToCurrentLayout(MapTree *list); void resetMapListFilters(); @@ -369,7 +368,9 @@ private: QString getExistingDirectory(QString); bool openProject(QString dir, bool initial = false); bool closeProject(); + void showRecentError(const QString &baseMessage); void showProjectOpenFailure(); + void showMapsExcludedAlert(const QStringList &excludedMapNames); bool setInitialMap(); void saveGlobalConfigs(); @@ -377,9 +378,6 @@ private: void refreshRecentProjectsMenu(); void updateMapList(); - void mapListAddGroup(); - void mapListAddLayout(); - void mapListAddArea(); void openMapListItem(const QModelIndex &index); void saveMapListTab(int index); @@ -419,11 +417,10 @@ private: void scrollMetatileSelectorToSelection(); MapListToolBar* getCurrentMapListToolBar(); MapTree* getCurrentMapList(); - void refreshLocationsComboBox(); + void setLocationComboBoxes(const QStringList &locations); QObjectList shortcutableObjects() const; void addCustomHeaderValue(QString key, QJsonValue value, bool isNew = false); - int insertTilesetLabel(QStringList * list, QString label); void checkForUpdates(bool requestedByUser); void setDivingMapsVisible(bool visible); @@ -450,7 +447,7 @@ struct MapViewTab { struct MapListTab { enum { - Groups = 0, Areas, Layouts + Groups = 0, Locations, Layouts }; }; diff --git a/include/project.h b/include/project.h index efb7db7c..9fb4e2ed 100644 --- a/include/project.h +++ b/include/project.h @@ -18,9 +18,6 @@ #include #include -// The displayed name of the special map value used by warps with multiple potential destinations -static QString DYNAMIC_MAP_NAME = "Dynamic"; - class Project : public QObject { Q_OBJECT @@ -35,18 +32,18 @@ public: public: QString root; - QStringList groupNames; - QMap mapGroups; - QList groupedMapNames; QStringList mapNames; + QStringList groupNames; + QMap groupNameToMapNames; QList healLocations; QMap healLocationNameToValue; QMap mapConstantsToMapNames; QMap mapNamesToMapConstants; - QStringList mapLayoutsTable; - QStringList mapLayoutsTableMaster; + QMap mapNameToLayoutId; + QMap mapNameToMapSectionName; QString layoutsLabel; - QMap layoutIdsToNames; + QStringList layoutIds; + QStringList layoutIdsMaster; QMap mapLayouts; QMap mapLayoutsMaster; QMap eventGraphicsMap; @@ -65,6 +62,7 @@ public: QStringList bgEventFacingDirections; QStringList trainerTypes; QStringList globalScriptLabels; + QStringList mapSectionIdNamesSaveOrder; QStringList mapSectionIdNames; QMap regionMapEntries; QMap> metatileLabelsMap; @@ -77,11 +75,11 @@ public: QMap modifiedFileTimestamps; bool usingAsmTilesets; QSet disabledSettingsNames; + QSet topLevelMapFields; int pokemonMinLevel; int pokemonMaxLevel; int maxEncounterRate; bool wildEncountersLoaded; - bool saveEmptyMapsec; void set_root(QString); @@ -113,7 +111,7 @@ public: QStringList secondaryTilesetLabels; QStringList tilesetLabelsOrdered; - Blockdata readBlockdata(QString); + Blockdata readBlockdata(QString, bool *ok = nullptr); bool loadBlockdata(Layout *); bool loadLayoutBorder(Layout *); @@ -122,12 +120,29 @@ public: void deleteFile(QString path); bool readMapGroups(); - Map* addNewMapToGroup(QString, int, Map*, bool, bool); - QString getNewMapName(); - QString getProjectTitle(); + void addNewMapGroup(const QString &groupName); + QString mapNameToMapGroup(const QString &mapName); - QString readMapLayoutId(QString map_name); - QString readMapLocation(QString map_name); + struct NewMapSettings { + QString name; + QString group; + bool canFlyTo; + Layout::Settings layout; + MapHeader header; + }; + NewMapSettings newMapSettings; + Layout::Settings newLayoutSettings; + + void initNewMapSettings(); + void initNewLayoutSettings(); + + Map *createNewMap(const Project::NewMapSettings &mapSettings, const Map* toDuplicate = nullptr); + Layout *createNewLayout(const Layout::Settings &layoutSettings, const Layout* toDuplicate = nullptr); + Tileset *createNewTileset(QString name, bool secondary, bool checkerboardFill); + bool isIdentifierUnique(const QString &identifier) const; + bool isValidNewIdentifier(QString identifier) const; + QString toUniqueIdentifier(const QString &identifier) const; + QString getProjectTitle(); bool readWildMonData(); tsl::ordered_map> wildMonData; @@ -139,25 +154,24 @@ public: bool readSpeciesIconPaths(); QMap speciesToIconPath; - void addNewMapsec(const QString &name); - void removeMapsec(const QString &name); + void addNewMapsec(const QString &idName); + void removeMapsec(const QString &idName); + QString getMapsecDisplayName(const QString &idName) const { return this->mapSectionDisplayNames.value(idName); } + void setMapsecDisplayName(const QString &idName, const QString &displayName); bool hasUnsavedChanges(); bool hasUnsavedDataChanges = false; - QSet getTopLevelMapFields(); + void initTopLevelMapFields(); + bool readMapJson(const QString &mapName, QJsonDocument * out); bool loadMapData(Map*); bool readMapLayouts(); Layout *loadLayout(QString layoutId); - Layout *createNewLayout(Layout::SimpleSettings &layoutSettings); bool loadLayout(Layout *); bool loadMapLayout(Map*); bool loadLayoutTilesets(Layout *); void loadTilesetAssets(Tileset*); - void loadTilesetTiles(Tileset*, QImage); - void loadTilesetMetatiles(Tileset*); void loadTilesetMetatileLabels(Tileset*); - void loadTilesetPalettes(Tileset*); void readTilesetPaths(Tileset* tileset); void saveLayout(Layout *); @@ -172,15 +186,10 @@ public: void saveMapGroups(); void saveRegionMapSections(); void saveWildMonData(); - void saveMapConstantsHeader(); void saveHealLocations(Map*); void saveTilesets(Tileset*, Tileset*); void saveTilesetMetatileLabels(Tileset*, Tileset*); - void saveTilesetMetatileAttributes(Tileset*); - void saveTilesetMetatiles(Tileset*); - void saveTilesetTilesImage(Tileset*); - void saveTilesetPalettes(Tileset*); - void appendTilesetLabel(QString label, QString isSecondaryStr); + void appendTilesetLabel(const QString &label, const QString &isSecondaryStr); bool readTilesetLabels(); bool readTilesetMetatileLabels(); bool readRegionMapSections(); @@ -217,9 +226,8 @@ public: QString getScriptDefaultString(bool usePoryScript, QString mapName) const; QStringList getEventScriptsFilePaths() const; - QString getDefaultPrimaryTilesetLabel(); - QString getDefaultSecondaryTilesetLabel(); - QString getDynamicMapDefineName(); + QString getDefaultPrimaryTilesetLabel() const; + QString getDefaultSecondaryTilesetLabel() const; void updateTilesetMetatileLabels(Tileset *tileset); QString buildMetatileLabelsText(const QMap defines); QString findMetatileLabelsTileset(QString label); @@ -227,6 +235,9 @@ public: static QString getExistingFilepath(QString filepath); void applyParsedLimits(); + static QString getEmptyMapDefineName(); + static QString getDynamicMapDefineName(); + static QString getDynamicMapName(); static int getNumTilesPrimary(); static int getNumTilesTotal(); static int getNumMetatilesPrimary(); @@ -234,7 +245,7 @@ public: static int getNumPalettesPrimary(); static int getNumPalettesTotal(); static int getMaxMapDataSize(); - static int getDefaultMapSize(); + static int getDefaultMapDimension(); static int getMaxMapWidth(); static int getMaxMapHeight(); static int getMapDataSize(int width, int height); @@ -242,14 +253,17 @@ public: bool calculateDefaultMapSize(); static int getMaxObjectEvents(); static QString getEmptyMapsecName(); + static QString getMapGroupPrefix(); + + static void numericalModeSort(QStringList &list); private: + QMap mapSectionDisplayNames; + void updateLayout(Layout *); void setNewLayoutBlockdata(Layout *layout); void setNewLayoutBorder(Layout *layout); - void setNewMapEvents(Map *map); - void setNewMapConnections(Map *map); void saveHealLocationsData(Map *map); void saveHealLocationsConstants(); @@ -262,13 +276,20 @@ private: static int num_pals_primary; static int num_pals_total; static int max_map_data_size; - static int default_map_size; + static int default_map_dimension; static int max_object_events; signals: void fileChanged(QString filepath); - void mapSectionIdNamesChanged(); void mapLoaded(Map *map); + void mapCreated(Map *newMap, const QString &groupName); + void layoutCreated(Layout *newLayout); + void tilesetCreated(Tileset *newTileset); + void mapGroupAdded(const QString &groupName); + void mapSectionAdded(const QString &idName); + void mapSectionDisplayNameChanged(const QString &idName, const QString &displayName); + void mapSectionIdNamesChanged(const QStringList &idNames); + void mapsExcluded(const QStringList &excludedMapNames); }; #endif // PROJECT_H diff --git a/include/ui/aboutporymap.h b/include/ui/aboutporymap.h index 148fc8b1..6bd0ed32 100644 --- a/include/ui/aboutporymap.h +++ b/include/ui/aboutporymap.h @@ -2,7 +2,6 @@ #define ABOUTPORYMAP_H #include -#include #include namespace Ui { diff --git a/include/ui/colorinputwidget.h b/include/ui/colorinputwidget.h index cd871e0b..527794d9 100644 --- a/include/ui/colorinputwidget.h +++ b/include/ui/colorinputwidget.h @@ -2,7 +2,6 @@ #define COLORINPUTWIDGET_H #include -#include namespace Ui { class ColorInputWidget; diff --git a/include/ui/mapheaderform.h b/include/ui/mapheaderform.h new file mode 100644 index 00000000..8f9adda8 --- /dev/null +++ b/include/ui/mapheaderform.h @@ -0,0 +1,84 @@ +#ifndef MAPHEADERFORM_H +#define MAPHEADERFORM_H + +/* + This is the UI class used to edit the fields in a map's header. + It's intended to be used anywhere the UI needs to present an editor for a map's header, + e.g. for the current map in the main editor or in the new map dialog. +*/ + +#include +#include +#include "mapheader.h" + +class Project; + +namespace Ui { +class MapHeaderForm; +} + +class MapHeaderForm : public QWidget +{ + Q_OBJECT + +public: + explicit MapHeaderForm(QWidget *parent = nullptr); + ~MapHeaderForm(); + + void setProject(Project * project, bool allowProjectChanges = true); + void clear(); + + void setHeader(MapHeader *header); + void setHeaderData(const MapHeader &header); + MapHeader headerData() const; + + void setSong(const QString &song); + void setLocation(const QString &location); + void setLocationName(const QString &locationName); + void setRequiresFlash(bool requiresFlash); + void setWeather(const QString &weather); + void setType(const QString &type); + void setBattleScene(const QString &battleScene); + void setShowsLocationName(bool showsLocationName); + void setAllowsRunning(bool allowsRunning); + void setAllowsBiking(bool allowsBiking); + void setAllowsEscaping(bool allowsEscaping); + void setFloorNumber(int floorNumber); + + QString song() const; + QString location() const; + QString locationName() const; + bool requiresFlash() const; + QString weather() const; + QString type() const; + QString battleScene() const; + bool showsLocationName() const; + bool allowsRunning() const; + bool allowsBiking() const; + bool allowsEscaping() const; + int floorNumber() const; + +private: + Ui::MapHeaderForm *ui; + QPointer m_header = nullptr; + QPointer m_project = nullptr; + bool m_allowProjectChanges = true; + + void setLocations(const QStringList &locations); + void updateLocationName(); + + void onSongUpdated(const QString &song); + void onLocationChanged(const QString &location); + void onLocationNameChanged(const QString &locationName); + void onWeatherChanged(const QString &weather); + void onTypeChanged(const QString &type); + void onBattleSceneChanged(const QString &battleScene); + void onRequiresFlashChanged(int selected); + void onShowLocationNameChanged(int selected); + void onAllowRunningChanged(int selected); + void onAllowBikingChanged(int selected); + void onAllowEscapingChanged(int selected); + void onFloorNumberChanged(int offset); +}; + +#endif // MAPHEADERFORM_H diff --git a/include/ui/mapimageexporter.h b/include/ui/mapimageexporter.h index 37df3038..88a00447 100644 --- a/include/ui/mapimageexporter.h +++ b/include/ui/mapimageexporter.h @@ -45,22 +45,25 @@ public: private: Ui::MapImageExporter *ui; - Layout *layout = nullptr; - Map *map = nullptr; - Editor *editor = nullptr; - QGraphicsScene *scene = nullptr; + Layout *m_layout = nullptr; + Map *m_map = nullptr; + Editor *m_editor = nullptr; + QGraphicsScene *m_scene = nullptr; - QPixmap preview; + QPixmap m_preview; - ImageExporterSettings settings; - ImageExporterMode mode = ImageExporterMode::Normal; + ImageExporterSettings m_settings; + ImageExporterMode m_mode = ImageExporterMode::Normal; void updatePreview(); void scalePreview(); void updateShowBorderState(); void saveImage(); QPixmap getStitchedImage(QProgressDialog *progress, bool includeBorder); + QPixmap getFormattedMapPixmap(); QPixmap getFormattedMapPixmap(Map *map, bool ignoreBorder = false); + QPixmap getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder = false, bool ignoreGrid = false); + void paintGrid(QPixmap *pixmap, bool ignoreBorder = false); bool historyItemAppliesToFrame(const QUndoCommand *command); protected: diff --git a/include/ui/maplistmodels.h b/include/ui/maplistmodels.h index 17810c7e..b973f438 100644 --- a/include/ui/maplistmodels.h +++ b/include/ui/maplistmodels.h @@ -13,9 +13,8 @@ class Project; enum MapListUserRoles { - GroupRole = Qt::UserRole + 1, // Used to hold the map group number. - TypeRole, // Used to differentiate between the different layers of the map list tree view. - TypeRole2, // Used for various extra data needed. + NameRole = Qt::UserRole, // Holds the name of the item in the list + TypeRole, // Used to differentiate between the different layers of the map list tree view. }; @@ -55,22 +54,47 @@ private: }; - -class QRegularExpressionValidator; - class MapListModel : public QStandardItemModel { Q_OBJECT public: - MapListModel(QObject *parent = nullptr) : QStandardItemModel(parent) {}; + MapListModel(Project *project, QObject *parent = nullptr); ~MapListModel() { } - virtual QModelIndex indexOf(QString id) const = 0; + void setActiveItem(const QString &itemName) { this->activeItemName = itemName; } + + virtual QStandardItem *insertMapItem(const QString &mapName, const QString &folderName); + virtual QStandardItem *insertMapFolderItem(const QString &folderName); + + virtual QModelIndex indexOf(const QString &itemName) const; virtual void removeItemAt(const QModelIndex &index); - virtual QStandardItem *getItem(const QModelIndex &index) const = 0; + virtual QStandardItem *itemAt(const QModelIndex &index) const; + virtual QStandardItem *itemAt(const QString &itemName) const; + + virtual QVariant data(const QModelIndex &index, int role) const override; protected: - virtual void removeItem(QStandardItem *item) = 0; + Project *project; + QStandardItem *root = nullptr; + + QString activeItemName; + QString folderTypeName; + bool sortingEnabled = false; + bool editable = false; + + QIcon mapGrayIcon; + QIcon mapIcon; + QIcon mapEditedIcon; + QIcon mapOpenedIcon; + QIcon mapFolderIcon; + QIcon emptyMapFolderIcon; + + QMap mapFolderItems; + QMap mapItems; + + virtual QStandardItem *createMapItem(const QString &mapName, QStandardItem *map = nullptr); + virtual QStandardItem *createMapFolderItem(const QString &groupName, QStandardItem *fromItem = nullptr); + virtual void removeItem(QStandardItem *item) = 0; }; class MapGroupModel : public MapListModel { @@ -81,87 +105,37 @@ public: ~MapGroupModel() { } QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; - virtual QMimeData *mimeData(const QModelIndexList &indexes) const override; - virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; - - virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; - -public: - void setMap(QString mapName) { this->openMap = mapName; } - - QStandardItem *createGroupItem(QString groupName, int groupIndex, QStandardItem *fromItem = nullptr); - QStandardItem *createMapItem(QString mapName, QStandardItem *fromItem = nullptr); - - QStandardItem *insertGroupItem(QString groupName); - QStandardItem *insertMapItem(QString mapName, QString groupName); - - virtual QStandardItem *getItem(const QModelIndex &index) const override; - virtual QModelIndex indexOf(QString mapName) const override; - - void initialize(); + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; protected: - virtual void removeItem(QStandardItem *item) override; + void removeItem(QStandardItem *item) override; private: friend class MapTree; void updateProject(); -private: - Project *project; - QStandardItem *root = nullptr; - - QMap groupItems; - QMap mapItems; - - QString openMap; - signals: void dragMoveCompleted(); }; -class MapAreaModel : public MapListModel { +class MapLocationModel : public MapListModel { Q_OBJECT public: - MapAreaModel(Project *project, QObject *parent = nullptr); - ~MapAreaModel() {} + MapLocationModel(Project *project, QObject *parent = nullptr); + ~MapLocationModel() {} - QVariant data(const QModelIndex &index, int role) const override; - -public: - void setMap(QString mapName) { this->openMap = mapName; } - - QStandardItem *createAreaItem(QString areaName); - QStandardItem *createMapItem(QString mapName, int areaIndex, int mapIndex); - - QStandardItem *insertAreaItem(QString areaName); - QStandardItem *insertMapItem(QString mapName, QString areaName, int groupIndex); - - virtual QStandardItem *getItem(const QModelIndex &index) const override; - virtual QModelIndex indexOf(QString mapName) const override; - - void initialize(); + QStandardItem *createMapFolderItem(const QString &folderName, QStandardItem *folder) override; protected: - virtual void removeItem(QStandardItem *item) override; - -private: - Project *project; - QStandardItem *root = nullptr; - - QMap areaItems; - QMap mapItems; - - QString openMap; - -signals: - void edited(); + void removeItem(QStandardItem *item) override; }; @@ -174,35 +148,10 @@ public: ~LayoutTreeModel() {} QVariant data(const QModelIndex &index, int role) const override; - -public: - void setLayout(QString layoutId) { this->openLayout = layoutId; } - - QStandardItem *createLayoutItem(QString layoutId); - QStandardItem *createMapItem(QString mapName); - - QStandardItem *insertLayoutItem(QString layoutId); - QStandardItem *insertMapItem(QString mapName, QString layoutId); - - virtual QStandardItem *getItem(const QModelIndex &index) const override; - virtual QModelIndex indexOf(QString layoutName) const override; - - void initialize(); + QStandardItem *createMapFolderItem(const QString &folderName, QStandardItem *folder) override; protected: - virtual void removeItem(QStandardItem *item) override; - -private: - Project *project; - QStandardItem *root = nullptr; - - QMap layoutItems; - QMap mapItems; - - QString openLayout; - -signals: - void edited(); + void removeItem(QStandardItem *item) override; }; #endif // MAPLISTMODELS_H diff --git a/include/ui/message.h b/include/ui/message.h new file mode 100644 index 00000000..8e36372d --- /dev/null +++ b/include/ui/message.h @@ -0,0 +1,60 @@ +#pragma once +#ifndef MESSAGE_H +#define MESSAGE_H + +/* + These classes are thin wrappers around QMessageBox for convenience. + The base Message class is a regular window-modal QMessageBox with "porymap" as the window title. + + QMessageBox's static functions enforce application modality (among other things), which changes the style of the message boxes on macOS. + With these equivalent static functions we have more control over the appearance and behavior of the window, + and we keep the convenience of not needing to provide all the arguments. + + If more control is needed (like adding custom buttons to the window) use the constructors as you would for a normal QMessageBox. +*/ + +#include + +class Message : public QMessageBox { +public: + Message(QMessageBox::Icon icon, const QString &text, QMessageBox::StandardButtons buttons, QWidget *parent); +}; + +// Basic error message with an 'Ok' button. +class ErrorMessage : public Message { +public: + ErrorMessage(const QString &message, QWidget *parent); + static int show(const QString &message, QWidget *parent); +}; + +// Basic warning message with an 'Ok' button. +class WarningMessage : public Message { +public: + WarningMessage(const QString &message, QWidget *parent); + static int show(const QString &message, QWidget *parent); +}; + +// Basic informational message with a 'Close' button. +class InfoMessage : public Message { +public: + InfoMessage(const QString &message, QWidget *parent); + static int show(const QString &message, QWidget *parent); +}; + +// Basic question message with a 'Yes' and 'No' button. +class QuestionMessage : public Message { +public: + QuestionMessage(const QString &message, QWidget *parent); + static int show(const QString &message, QWidget *parent); +}; + +// Error message directing users to their log file. +// Shows the most recent error as detailed text. +class RecentErrorMessage : public ErrorMessage { +public: + RecentErrorMessage(const QString &message, QWidget *parent); + static int show(const QString &message, QWidget *parent); +}; + + +#endif // MESSAGE_H diff --git a/include/ui/newlayoutdialog.h b/include/ui/newlayoutdialog.h new file mode 100644 index 00000000..b1a2c47b --- /dev/null +++ b/include/ui/newlayoutdialog.h @@ -0,0 +1,49 @@ +#ifndef NEWLAYOUTDIALOG_H +#define NEWLAYOUTDIALOG_H + +#include +#include +#include "editor.h" +#include "project.h" +#include "map.h" +#include "mapheaderform.h" +#include "newlayoutform.h" +#include "lib/collapsiblesection.h" + +namespace Ui { +class NewLayoutDialog; +} + +class NewLayoutDialog : public QDialog +{ + Q_OBJECT +public: + explicit NewLayoutDialog(Project *project, QWidget *parent = nullptr); + explicit NewLayoutDialog(Project *project, const Layout *layoutToCopy, QWidget *parent = nullptr); + ~NewLayoutDialog(); + + virtual void accept() override; + +signals: + void applied(const QString &newLayoutId); + +private: + Ui::NewLayoutDialog *ui; + Project *project; + const Layout *layoutToCopy; + + // Each of these validation functions will allow empty names up until `OK` is selected, + // because clearing the text during editing is common and we don't want to flash errors for this. + bool validateLayoutID(bool allowEmpty = false); + bool validateName(bool allowEmpty = false); + + void refresh(); + void saveSettings(); + +private slots: + void dialogButtonClicked(QAbstractButton *button); + void on_lineEdit_Name_textChanged(const QString &); + void on_lineEdit_LayoutID_textChanged(const QString &); +}; + +#endif // NEWLAYOUTDIALOG_H diff --git a/include/ui/newlayoutform.h b/include/ui/newlayoutform.h new file mode 100644 index 00000000..3e77af18 --- /dev/null +++ b/include/ui/newlayoutform.h @@ -0,0 +1,40 @@ +#ifndef NEWLAYOUTFORM_H +#define NEWLAYOUTFORM_H + +#include + +#include "maplayout.h" + +class Project; + +namespace Ui { +class NewLayoutForm; +} + +class NewLayoutForm : public QWidget +{ + Q_OBJECT + +public: + explicit NewLayoutForm(QWidget *parent = nullptr); + ~NewLayoutForm(); + + void initUi(Project *project); + + void setSettings(const Layout::Settings &settings); + Layout::Settings settings() const; + + void setDisabled(bool disabled); + + bool validate(); + +private: + Ui::NewLayoutForm *ui; + Project *m_project; + + bool validateMapDimensions(); + bool validatePrimaryTileset(bool allowEmpty = false); + bool validateSecondaryTileset(bool allowEmpty = false); +}; + +#endif // NEWLAYOUTFORM_H diff --git a/include/ui/newlocationdialog.h b/include/ui/newlocationdialog.h new file mode 100644 index 00000000..33af5c9c --- /dev/null +++ b/include/ui/newlocationdialog.h @@ -0,0 +1,34 @@ +#ifndef NEWLOCATIONDIALOG_H +#define NEWLOCATIONDIALOG_H + +#include +#include +#include + +class Project; + +namespace Ui { +class NewLocationDialog; +} + +class NewLocationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewLocationDialog(Project *project = nullptr, QWidget *parent = nullptr); + ~NewLocationDialog(); + + virtual void accept() override; + +private: + Ui::NewLocationDialog *ui; + QPointer project = nullptr; + const QString namePrefix; + + bool validateIdName(bool allowEmpty = false); + void onIdNameChanged(const QString &name); + void dialogButtonClicked(QAbstractButton *button); +}; + +#endif // NEWLOCATIONDIALOG_H diff --git a/include/ui/newmapdialog.h b/include/ui/newmapdialog.h new file mode 100644 index 00000000..4b560bf1 --- /dev/null +++ b/include/ui/newmapdialog.h @@ -0,0 +1,51 @@ +#ifndef NEWMAPDIALOG_H +#define NEWMAPDIALOG_H + +#include +#include +#include "editor.h" +#include "project.h" +#include "map.h" +#include "mapheaderform.h" +#include "newlayoutform.h" +#include "lib/collapsiblesection.h" + +namespace Ui { +class NewMapDialog; +} + +class NewMapDialog : public QDialog +{ + Q_OBJECT +public: + explicit NewMapDialog(Project *project, QWidget *parent = nullptr); + explicit NewMapDialog(Project *project, const Map *mapToCopy = nullptr, QWidget *parent = nullptr); + explicit NewMapDialog(Project *project, int mapListTab, const QString &mapListItem, QWidget *parent = nullptr); + ~NewMapDialog(); + + virtual void accept() override; + +private: + Ui::NewMapDialog *ui; + Project *project; + CollapsibleSection *headerSection; + MapHeaderForm *headerForm; + const Map *mapToCopy; + + // Each of these validation functions will allow empty names up until `OK` is selected, + // because clearing the text during editing is common and we don't want to flash errors for this. + bool validateName(bool allowEmpty = false); + bool validateGroup(bool allowEmpty = false); + bool validateLayoutID(bool allowEmpty = false); + + void refresh(); + void saveSettings(); + +private slots: + void dialogButtonClicked(QAbstractButton *button); + void on_lineEdit_Name_textChanged(const QString &); + void on_comboBox_Group_currentTextChanged(const QString &text); + void on_comboBox_LayoutID_currentTextChanged(const QString &text); +}; + +#endif // NEWMAPDIALOG_H diff --git a/include/ui/newmapgroupdialog.h b/include/ui/newmapgroupdialog.h new file mode 100644 index 00000000..cbf8aa34 --- /dev/null +++ b/include/ui/newmapgroupdialog.h @@ -0,0 +1,36 @@ +#ifndef NEWMAPGROUPDIALOG_H +#define NEWMAPGROUPDIALOG_H + +/* + This is a generic dialog for requesting a new unique name from the user. +*/ + +#include +#include + +class Project; + +namespace Ui { +class NewMapGroupDialog; +} + +class NewMapGroupDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewMapGroupDialog(Project *project = nullptr, QWidget *parent = nullptr); + ~NewMapGroupDialog(); + + virtual void accept() override; + +private: + Ui::NewMapGroupDialog *ui; + Project *project = nullptr; + + bool validateName(bool allowEmpty = false); + void onNameChanged(const QString &name); + void dialogButtonClicked(QAbstractButton *button); +}; + +#endif // NEWMAPGROUPDIALOG_H diff --git a/include/ui/newmappopup.h b/include/ui/newmappopup.h deleted file mode 100644 index f160876a..00000000 --- a/include/ui/newmappopup.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef NEWMAPPOPUP_H -#define NEWMAPPOPUP_H - -#include -#include -#include "editor.h" -#include "project.h" -#include "map.h" - -namespace Ui { -class NewMapPopup; -} - -class NewMapPopup : public QMainWindow -{ - Q_OBJECT -public: - explicit NewMapPopup(QWidget *parent = nullptr, Project *project = nullptr); - ~NewMapPopup(); - Map *map; - int group; - bool existingLayout; - bool importedMap; - QString layoutId; - void init(); - void initUi(); - void init(int tabIndex, QString data); - void init(Layout *); - static void setDefaultSettings(Project *project); - -signals: - void applied(); - -private: - Ui::NewMapPopup *ui; - Project *project; - bool checkNewMapDimensions(); - bool checkNewMapGroup(); - void saveSettings(); - void useLayout(QString layoutId); - void useLayoutSettings(Layout *mapLayout); - - struct Settings { - QString group; - int width; - int height; - int borderWidth; - int borderHeight; - QString primaryTilesetLabel; - QString secondaryTilesetLabel; - QString type; - QString location; - QString song; - bool canFlyTo; - bool showLocationName; - bool allowRunning; - bool allowBiking; - bool allowEscaping; - int floorNumber; - }; - static struct Settings settings; - -private slots: - void on_checkBox_UseExistingLayout_stateChanged(int state); - void on_comboBox_Layout_currentTextChanged(const QString &text); - void on_pushButton_NewMap_Accept_clicked(); - void on_lineEdit_NewMap_Name_textChanged(const QString &); -}; - -#endif // NEWMAPPOPUP_H diff --git a/include/ui/newtilesetdialog.h b/include/ui/newtilesetdialog.h index c2563f21..2a5ad14e 100644 --- a/include/ui/newtilesetdialog.h +++ b/include/ui/newtilesetdialog.h @@ -2,7 +2,10 @@ #define NEWTILESETDIALOG_H #include -#include "project.h" +#include + +class Project; +class Tileset; namespace Ui { class NewTilesetDialog; @@ -15,20 +18,20 @@ class NewTilesetDialog : public QDialog public: explicit NewTilesetDialog(Project *project, QWidget *parent = nullptr); ~NewTilesetDialog(); - QString path; - QString fullSymbolName; - QString friendlyName; - bool isSecondary; - bool checkerboardFill; -private slots: - void NameOrSecondaryChanged(); - void SecondaryChanged(); - void FillChanged(); + virtual void accept() override; + +signals: + void applied(Tileset *tileset); private: Ui::NewTilesetDialog *ui; Project *project = nullptr; + const QString symbolPrefix; + + bool validateName(bool allowEmpty = false); + void onNameChanged(const QString &name); + void dialogButtonClicked(QAbstractButton *button); }; #endif // NEWTILESETDIALOG_H diff --git a/include/ui/regionmapeditor.h b/include/ui/regionmapeditor.h index c1941651..9a838827 100644 --- a/include/ui/regionmapeditor.h +++ b/include/ui/regionmapeditor.h @@ -116,6 +116,7 @@ private: void displayRegionMapEntryOptions(); void updateRegionMapEntryOptions(QString); void setRegionMap(RegionMap *map); + void setLocations(const QStringList &locations); void restoreWindowState(); void closeEvent(QCloseEvent* event); @@ -133,7 +134,7 @@ private slots: void on_tabWidget_Region_Map_currentChanged(int); void on_pushButton_RM_Options_delete_clicked(); void on_comboBox_RM_ConnectedMap_textActivated(const QString &); - void on_comboBox_RM_Entry_MapSection_textActivated(const QString &); + void on_comboBox_RM_Entry_MapSection_currentTextChanged(const QString &); void on_comboBox_regionSelector_textActivated(const QString &); void on_comboBox_layoutLayer_textActivated(const QString &); void on_spinBox_RM_Entry_x_valueChanged(int); @@ -148,7 +149,6 @@ private slots: void on_checkBox_tileVFlip_stateChanged(int); void on_verticalSlider_Zoom_Map_Image_valueChanged(int); void on_verticalSlider_Zoom_Image_Tiles_valueChanged(int); - void on_lineEdit_RM_MapName_textEdited(const QString &); void onHoveredRegionMapTileChanged(int x, int y); void onHoveredRegionMapTileCleared(); void mouseEvent_region_map(QGraphicsSceneMouseEvent *event, RegionMapPixmapItem *item); diff --git a/include/ui/tileseteditor.h b/include/ui/tileseteditor.h index 86d53cc2..53d5717f 100644 --- a/include/ui/tileseteditor.h +++ b/include/ui/tileseteditor.h @@ -121,9 +121,7 @@ private slots: void on_horizontalSlider_TilesZoom_valueChanged(int value); private: - void initUi(); void setAttributesUi(); - void setMetatileLabelValidator(); void initMetatileSelector(); void initTileSelector(); void initSelectedTileItem(); diff --git a/porymap.pro b/porymap.pro index 9555e7e5..8987b5cc 100644 --- a/porymap.pro +++ b/porymap.pro @@ -33,7 +33,8 @@ DEFINES += PORYMAP_LATEST_COMMIT=\\\"$$LATEST_COMMIT\\\" VERSION = 5.4.1 DEFINES += PORYMAP_VERSION=\\\"$$VERSION\\\" -SOURCES += src/core/block.cpp \ +SOURCES += src/core/advancemapparser.cpp \ + src/core/block.cpp \ src/core/bitpacker.cpp \ src/core/blockdata.cpp \ src/core/events.cpp \ @@ -42,20 +43,21 @@ SOURCES += src/core/block.cpp \ src/core/imageexport.cpp \ src/core/map.cpp \ src/core/mapconnection.cpp \ + src/core/mapheader.cpp \ src/core/maplayout.cpp \ - src/core/mapparser.cpp \ src/core/metatile.cpp \ - src/core/metatileparser.cpp \ src/core/network.cpp \ src/core/paletteutil.cpp \ src/core/parseutil.cpp \ src/core/tile.cpp \ src/core/tileset.cpp \ + src/core/validator.cpp \ src/core/regionmap.cpp \ src/core/wildmoninfo.cpp \ src/core/editcommands.cpp \ src/lib/fex/lexer.cpp \ src/lib/fex/parser.cpp \ + src/lib/collapsiblesection.cpp \ src/lib/orderedjson.cpp \ src/core/regionmapeditcommands.cpp \ src/scriptapi/apimap.cpp \ @@ -87,17 +89,23 @@ SOURCES += src/core/block.cpp \ src/ui/filterchildrenproxymodel.cpp \ src/ui/maplistmodels.cpp \ src/ui/maplisttoolbar.cpp \ + src/ui/message.cpp \ src/ui/graphicsview.cpp \ src/ui/imageproviders.cpp \ src/ui/layoutpixmapitem.cpp \ src/ui/prefabcreationdialog.cpp \ src/ui/regionmappixmapitem.cpp \ src/ui/citymappixmapitem.cpp \ + src/ui/mapheaderform.cpp \ src/ui/metatilelayersitem.cpp \ src/ui/metatileselector.cpp \ src/ui/movablerect.cpp \ src/ui/movementpermissionsselector.cpp \ src/ui/neweventtoolbutton.cpp \ + src/ui/newlayoutdialog.cpp \ + src/ui/newlayoutform.cpp \ + src/ui/newlocationdialog.cpp \ + src/ui/newmapgroupdialog.cpp \ src/ui/noscrollcombobox.cpp \ src/ui/noscrollspinbox.cpp \ src/ui/montabwidget.cpp \ @@ -110,7 +118,7 @@ SOURCES += src/core/block.cpp \ src/ui/tileseteditortileselector.cpp \ src/ui/tilemaptileselector.cpp \ src/ui/regionmapeditor.cpp \ - src/ui/newmappopup.cpp \ + src/ui/newmapdialog.cpp \ src/ui/mapimageexporter.cpp \ src/ui/newtilesetdialog.cpp \ src/ui/flowlayout.cpp \ @@ -133,7 +141,8 @@ SOURCES += src/core/block.cpp \ src/ui/updatepromoter.cpp \ src/ui/wildmonchart.cpp -HEADERS += include/core/block.h \ +HEADERS += include/core/advancemapparser.h \ + include/core/block.h \ include/core/bitpacker.h \ include/core/blockdata.h \ include/core/events.h \ @@ -143,15 +152,15 @@ HEADERS += include/core/block.h \ include/core/imageexport.h \ include/core/map.h \ include/core/mapconnection.h \ + include/core/mapheader.h \ include/core/maplayout.h \ - include/core/mapparser.h \ include/core/metatile.h \ - include/core/metatileparser.h \ include/core/network.h \ include/core/paletteutil.h \ include/core/parseutil.h \ include/core/tile.h \ include/core/tileset.h \ + include/core/validator.h \ include/core/regionmap.h \ include/core/wildmoninfo.h \ include/core/editcommands.h \ @@ -161,6 +170,7 @@ HEADERS += include/core/block.h \ include/lib/fex/define_statement.h \ include/lib/fex/lexer.h \ include/lib/fex/parser.h \ + include/lib/collapsiblesection.h \ include/lib/orderedmap.h \ include/lib/orderedjson.h \ include/ui/aboutporymap.h \ @@ -174,6 +184,7 @@ HEADERS += include/core/block.h \ include/ui/connectionpixmapitem.h \ include/ui/currentselectedmetatilespixmapitem.h \ include/ui/gridsettings.h \ + include/ui/mapheaderform.h \ include/ui/newmapconnectiondialog.h \ include/ui/prefabframe.h \ include/ui/projectsettingseditor.h \ @@ -186,6 +197,7 @@ HEADERS += include/core/block.h \ include/ui/filterchildrenproxymodel.h \ include/ui/maplistmodels.h \ include/ui/maplisttoolbar.h \ + include/ui/message.h \ include/ui/graphicsview.h \ include/ui/imageproviders.h \ include/ui/layoutpixmapitem.h \ @@ -199,6 +211,10 @@ HEADERS += include/core/block.h \ include/ui/movablerect.h \ include/ui/movementpermissionsselector.h \ include/ui/neweventtoolbutton.h \ + include/ui/newlayoutdialog.h \ + include/ui/newlayoutform.h \ + include/ui/newlocationdialog.h \ + include/ui/newmapgroupdialog.h \ include/ui/noscrollcombobox.h \ include/ui/noscrollspinbox.h \ include/ui/montabwidget.h \ @@ -212,7 +228,7 @@ HEADERS += include/core/block.h \ include/ui/tileseteditortileselector.h \ include/ui/tilemaptileselector.h \ include/ui/regionmapeditor.h \ - include/ui/newmappopup.h \ + include/ui/newmapdialog.h \ include/ui/mapimageexporter.h \ include/ui/newtilesetdialog.h \ include/ui/overlay.h \ @@ -241,14 +257,19 @@ FORMS += forms/mainwindow.ui \ forms/colorinputwidget.ui \ forms/connectionslistitem.ui \ forms/gridsettingsdialog.ui \ + forms/mapheaderform.ui \ forms/maplisttoolbar.ui \ + forms/newlayoutdialog.ui \ + forms/newlayoutform.ui \ + forms/newlocationdialog.ui \ forms/newmapconnectiondialog.ui \ + forms/newmapgroupdialog.ui \ forms/prefabcreationdialog.ui \ forms/prefabframe.ui \ forms/tileseteditor.ui \ forms/paletteeditor.ui \ forms/regionmapeditor.ui \ - forms/newmappopup.ui \ + forms/newmapdialog.ui \ forms/aboutporymap.ui \ forms/newtilesetdialog.ui \ forms/mapimageexporter.ui \ diff --git a/src/config.cpp b/src/config.cpp index f1a52ec0..ce78d8b9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -2,6 +2,7 @@ #include "log.h" #include "shortcut.h" #include "map.h" +#include "validator.h" #include #include #include @@ -81,6 +82,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::symbol_spawn_npcs, {"symbol_spawn_npcs", "u8 sWhiteoutRespawnHealerNpcIds"}}, {ProjectIdentifier::symbol_attribute_table, {"symbol_attribute_table", "sMetatileAttrMasks"}}, {ProjectIdentifier::symbol_tilesets_prefix, {"symbol_tilesets_prefix", "gTileset_"}}, + {ProjectIdentifier::symbol_dynamic_map_name, {"symbol_dynamic_map_name", "Dynamic"}}, // Defines {ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}}, {ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}}, @@ -152,7 +154,6 @@ const QMap> ProjectConfig::defaultPaths {ProjectFilePath::data_pokemon_gfx, { "data_pokemon_gfx", "src/data/graphics/pokemon.h"}}, {ProjectFilePath::data_heal_locations, { "data_heal_locations", "src/data/heal_locations.h"}}, {ProjectFilePath::constants_global, { "constants_global", "include/constants/global.h"}}, - {ProjectFilePath::constants_map_groups, { "constants_map_groups", "include/constants/map_groups.h"}}, {ProjectFilePath::constants_items, { "constants_items", "include/constants/items.h"}}, {ProjectFilePath::constants_flags, { "constants_flags", "include/constants/flags.h"}}, {ProjectFilePath::constants_vars, { "constants_vars", "include/constants/vars.h"}}, @@ -348,6 +349,10 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->customScriptsEditorState = bytesFromString(value); } else if (key == "wild_mon_chart_geometry") { this->wildMonChartGeometry = bytesFromString(value); + } else if (key == "new_map_dialog_geometry") { + this->newMapDialogGeometry = bytesFromString(value); + } else if (key == "new_layout_dialog_geometry") { + this->newLayoutDialogGeometry = bytesFromString(value); } else if (key == "metatiles_zoom") { this->metatilesZoom = getConfigInteger(key, value, 10, 100, 30); } else if (key == "collision_zoom") { @@ -374,6 +379,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) { this->monitorFiles = getConfigBool(key, value); } else if (key == "tileset_checkerboard_fill") { this->tilesetCheckerboardFill = getConfigBool(key, value); + } else if (key == "new_map_header_section_expanded") { + this->newMapHeaderSectionExpanded = getConfigBool(key, value); } else if (key == "theme") { this->theme = value; } else if (key == "wild_mon_chart_theme") { @@ -438,6 +445,8 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry)); map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState)); map.insert("wild_mon_chart_geometry", stringFromByteArray(this->wildMonChartGeometry)); + map.insert("new_map_dialog_geometry", stringFromByteArray(this->newMapDialogGeometry)); + map.insert("new_layout_dialog_geometry", stringFromByteArray(this->newLayoutDialogGeometry)); map.insert("mirror_connecting_maps", this->mirrorConnectingMaps ? "1" : "0"); map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0"); map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity)); @@ -457,6 +466,7 @@ QMap PorymapConfig::getKeyValueMap() { map.insert("show_tileset_editor_divider", this->showTilesetEditorDivider ? "1" : "0"); map.insert("monitor_files", this->monitorFiles ? "1" : "0"); map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0"); + map.insert("new_map_header_section_expanded", this->newMapHeaderSectionExpanded ? "1" : "0"); map.insert("theme", this->theme); map.insert("wild_mon_chart_theme", this->wildMonChartTheme); map.insert("text_editor_open_directory", this->textEditorOpenFolder); @@ -954,13 +964,23 @@ QString ProjectConfig::getFilePath(ProjectFilePath pathId) { } -void ProjectConfig::setIdentifier(ProjectIdentifier id, const QString &text) { - if (!defaultIdentifiers.contains(id)) return; - QString copy(text); - if (copy.isEmpty()) { +void ProjectConfig::setIdentifier(ProjectIdentifier id, QString text) { + if (!defaultIdentifiers.contains(id)) + return; + + if (text.isEmpty()) { this->identifiers.remove(id); } else { - this->identifiers[id] = copy; + const QString idName = defaultIdentifiers.value(id).first; + if (idName.startsWith("define_") || idName.startsWith("symbol_")) { + // Validate the input for the identifier, depending on the type. + IdentifierValidator validator; + if (!validator.isValid(text)) { + logError(QString("The name '%1' for project identifier '%2' is invalid. It must only contain word characters, and cannot start with a digit.").arg(text).arg(idName)); + return; + } + } + this->identifiers[id] = text; } } diff --git a/src/core/advancemapparser.cpp b/src/core/advancemapparser.cpp new file mode 100644 index 00000000..6a8428ec --- /dev/null +++ b/src/core/advancemapparser.cpp @@ -0,0 +1,217 @@ +#include "advancemapparser.h" +#include "log.h" +#include "project.h" +#include "maplayout.h" + +Layout *AdvanceMapParser::parseLayout(const QString &filepath, bool *error, const Project *project) +{ + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) { + *error = true; + logError(QString("Could not open Advance Map 1.92 Map .map file '%1': ").arg(filepath) + file.errorString()); + return nullptr; + } + + QByteArray in = file.readAll(); + file.close(); + + if (in.length() < 20 || in.length() % 2 != 0) { + *error = true; + logError(QString("Advance Map 1.92 Map .map file '%1' is an unexpected size.").arg(filepath)); + return nullptr; + } + + int borderWidth = static_cast(in.at(16)); // 0 in RSE .map files + int borderHeight = static_cast(in.at(17)); // 0 in RSE .map files + int numBorderTiles = borderWidth * borderHeight; // 0 if RSE + + int mapDataOffset = 20 + (numBorderTiles * 2); // FRLG .map files store border metatile data after the header + int mapWidth = static_cast(in.at(0)) | + (static_cast(in.at(1)) << 8) | + (static_cast(in.at(2)) << 16) | + (static_cast(in.at(3)) << 24); + int mapHeight = static_cast(in.at(4)) | + (static_cast(in.at(5)) << 8) | + (static_cast(in.at(6)) << 16) | + (static_cast(in.at(7)) << 24); + int mapPrimaryTilesetNum = static_cast(in.at(8)) | + (static_cast(in.at(9)) << 8) | + (static_cast(in.at(10)) << 16) | + (static_cast(in.at(11)) << 24); + int mapSecondaryTilesetNum = static_cast(in.at(12)) | + (static_cast(in.at(13)) << 8) | + (static_cast(in.at(14)) << 16) | + (static_cast(in.at(15)) << 24); + + int numMetatiles = mapWidth * mapHeight; + int expectedFileSize = 20 + (numBorderTiles * 2) + (numMetatiles * 2); + if (in.length() != expectedFileSize) { + *error = true; + logError(QString(".map file is an unexpected size. Expected %1 bytes, but it has %2 bytes.").arg(expectedFileSize).arg(in.length())); + return nullptr; + } + + Blockdata blockdata; + for (int i = mapDataOffset; (i + 1) < in.length(); i += 2) { + uint16_t word = static_cast((in[i] & 0xff) + ((in[i + 1] & 0xff) << 8)); + blockdata.append(word); + } + + Blockdata border; + if (numBorderTiles != 0) { + for (int i = 20; (i + 1) < mapDataOffset; i += 2) { + uint16_t word = static_cast((in[i] & 0xff) + ((in[i + 1] & 0xff) << 8)); + border.append(word); + } + } + + Layout *mapLayout = new Layout(); + mapLayout->width = mapWidth; + mapLayout->height = mapHeight; + mapLayout->border_width = (borderWidth == 0) ? DEFAULT_BORDER_WIDTH : borderWidth; + mapLayout->border_height = (borderHeight == 0) ? DEFAULT_BORDER_HEIGHT : borderHeight; + + const QList tilesets = project->tilesetLabelsOrdered; + + if (mapPrimaryTilesetNum > tilesets.size()) + mapLayout->tileset_primary_label = project->getDefaultPrimaryTilesetLabel(); + else + mapLayout->tileset_primary_label = tilesets.at(mapPrimaryTilesetNum); + + if (mapSecondaryTilesetNum > tilesets.size()) + mapLayout->tileset_secondary_label = project->getDefaultSecondaryTilesetLabel(); + else + mapLayout->tileset_secondary_label = tilesets.at(mapSecondaryTilesetNum); + + mapLayout->blockdata = blockdata; + + if (!border.isEmpty()) { + mapLayout->border = border; + } + + return mapLayout; +} + +QList AdvanceMapParser::parseMetatiles(const QString &filepath, bool *error, bool primaryTileset) +{ + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) { + *error = true; + logError(QString("Could not open Advance Map 1.92 Metatile .bvd file '%1': ").arg(filepath) + file.errorString()); + return { }; + } + + QByteArray in = file.readAll(); + file.close(); + + if (in.length() < 9 || in.length() % 2 != 0) { + *error = true; + logError(QString("Advance Map 1.92 Metatile .bvd file '%1' is an unexpected size.").arg(filepath)); + return { }; + } + + int projIdOffset = in.length() - 4; + int metatileSize = 16; + BaseGameVersion version; + if (in.at(projIdOffset + 0) == 'R' + && in.at(projIdOffset + 1) == 'S' + && in.at(projIdOffset + 2) == 'E' + && in.at(projIdOffset + 3) == ' ') { + // ruby and emerald are handled equally here. + version = BaseGameVersion::pokeemerald; + } else if (in.at(projIdOffset + 0) == 'F' + && in.at(projIdOffset + 1) == 'R' + && in.at(projIdOffset + 2) == 'L' + && in.at(projIdOffset + 3) == 'G') { + version = BaseGameVersion::pokefirered; + } else { + *error = true; + logError(QString("Detected unsupported game type from .bvd file. Last 4 bytes of file must be 'RSE ' or 'FRLG'.")); + return { }; + } + + int attrSize = Metatile::getDefaultAttributesSize(version); + int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary(); + int numMetatiles = static_cast(in.at(0)) | + (static_cast(in.at(1)) << 8) | + (static_cast(in.at(2)) << 16) | + (static_cast(in.at(3)) << 24); + if (numMetatiles > maxMetatiles) { + *error = true; + logError(QString(".bvd file contains data for %1 metatiles, but the maximum number of metatiles is %2.").arg(numMetatiles).arg(maxMetatiles)); + return { }; + } + if (numMetatiles < 1) { + *error = true; + logError(QString(".bvd file contains no data for metatiles.")); + return { }; + } + + int expectedFileSize = 4 + (metatileSize * numMetatiles) + (attrSize * numMetatiles) + 4; + if (in.length() != expectedFileSize) { + *error = true; + logError(QString(".bvd file is an unexpected size. Expected %1 bytes, but it has %2 bytes.").arg(expectedFileSize).arg(in.length())); + return { }; + } + + QList metatiles; + for (int i = 0; i < numMetatiles; i++) { + Metatile *metatile = new Metatile(); + QList tiles; + for (int j = 0; j < 8; j++) { + int metatileOffset = 4 + i * metatileSize + j * 2; + Tile tile(static_cast( + static_cast(in.at(metatileOffset)) | + (static_cast(in.at(metatileOffset + 1)) << 8))); + tiles.append(tile); + } + + // AdvanceMap .bvd files only contain 8 tiles of data per metatile. + // If the user has triple-layer metatiles enabled we need to fill the remaining 4 tiles ourselves. + if (projectConfig.tripleLayerMetatilesEnabled) { + Tile tile = Tile(); + for (int j = 0; j < 4; j++) + tiles.append(tile); + } + + int attrOffset = 4 + (numMetatiles * metatileSize) + (i * attrSize); + uint32_t attributes = 0; + for (int j = 0; j < attrSize; j++) + attributes |= static_cast(in.at(attrOffset + j)) << (8 * j); + metatile->setAttributes(attributes, version); + metatile->tiles = tiles; + metatiles.append(metatile); + } + + return metatiles; +} + +QList AdvanceMapParser::parsePalette(const QString &filepath, bool *error) { + QFile file(filepath); + if (!file.open(QIODevice::ReadOnly)) { + *error = true; + logError(QString("Could not open Advance Map 1.92 palette file '%1': ").arg(filepath) + file.errorString()); + return QList(); + } + + QByteArray in = file.readAll(); + file.close(); + + if (in.length() % 4 != 0) { + *error = true; + logError(QString("Advance Map 1.92 palette file '%1' had an unexpected format. File's length must be a multiple of 4, but the length is %2.").arg(filepath).arg(in.length())); + return QList(); + } + + QList palette; + int i = 0; + while (i < in.length()) { + unsigned char red = static_cast(in.at(i + 0)); + unsigned char green = static_cast(in.at(i + 1)); + unsigned char blue = static_cast(in.at(i + 2)); + palette.append(qRgb(red, green, blue)); + i += 4; + } + + return palette; +} diff --git a/src/core/events.cpp b/src/core/events.cpp index 89416eae..c0cf7b7a 100644 --- a/src/core/events.cpp +++ b/src/core/events.cpp @@ -44,7 +44,7 @@ void Event::setPixmapItem(DraggablePixmapItem *item) { } int Event::getEventIndex() { - return this->map->events.value(this->getEventGroup()).indexOf(this); + return this->map->getIndexOfEvent(this); } void Event::setDefaultValues(Project *) { @@ -392,7 +392,8 @@ OrderedJson::object CloneObjectEvent::buildEventJson(Project *project) { cloneJson["x"] = this->getX(); cloneJson["y"] = this->getY(); cloneJson["target_local_id"] = this->getTargetID(); - cloneJson["target_map"] = project->mapNamesToMapConstants.value(this->getTargetMap()); + const QString mapName = this->getTargetMap(); + cloneJson["target_map"] = project->mapNamesToMapConstants.value(mapName, mapName); this->addCustomValuesTo(&cloneJson); return cloneJson; @@ -404,17 +405,11 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) { this->setGfx(ParseUtil::jsonToQString(json["graphics_id"])); this->setTargetID(ParseUtil::jsonToInt(json["target_local_id"])); - // Ensure the target map constant is valid before adding it to the events. - const QString dynamicMapConstant = project->getDynamicMapDefineName(); - QString mapConstant = ParseUtil::jsonToQString(json["target_map"]); - if (project->mapConstantsToMapNames.contains(mapConstant)) { - this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant)); - } else if (mapConstant == dynamicMapConstant) { - this->setTargetMap(DYNAMIC_MAP_NAME); - } else { - logWarn(QString("Target Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant)); - this->setTargetMap(DYNAMIC_MAP_NAME); - } + // Log a warning if "target_map" isn't a known map ID, but don't overwrite user data. + const QString mapConstant = ParseUtil::jsonToQString(json["target_map"]); + if (!project->mapConstantsToMapNames.contains(mapConstant)) + logWarn(QString("Unknown Target Map constant '%1'.").arg(mapConstant)); + this->setTargetMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); this->readCustomValues(json); @@ -424,7 +419,7 @@ bool CloneObjectEvent::loadFromJson(QJsonObject json, Project *project) { void CloneObjectEvent::setDefaultValues(Project *project) { this->setGfx(project->gfxDefines.keys().value(0, "0")); this->setTargetID(1); - if (this->getMap()) this->setTargetMap(this->getMap()->name); + if (this->getMap()) this->setTargetMap(this->getMap()->name()); } const QSet expectedCloneObjectFields = { @@ -445,7 +440,7 @@ void CloneObjectEvent::loadPixmap(Project *project) { // Try to get the targeted object to clone int eventIndex = this->targetID - 1; Map *clonedMap = project->getMap(this->targetMap); - Event *clonedEvent = clonedMap ? clonedMap->events[Event::Group::Object].value(eventIndex, nullptr) : nullptr; + Event *clonedEvent = clonedMap ? clonedMap->getEvent(Event::Group::Object, eventIndex) : nullptr; if (clonedEvent && clonedEvent->getEventType() == Event::Type::Object) { // Get graphics data from cloned object @@ -502,7 +497,8 @@ OrderedJson::object WarpEvent::buildEventJson(Project *project) { warpJson["x"] = this->getX(); warpJson["y"] = this->getY(); warpJson["elevation"] = this->getElevation(); - warpJson["dest_map"] = project->mapNamesToMapConstants.value(this->getDestinationMap()); + const QString mapName = this->getDestinationMap(); + warpJson["dest_map"] = project->mapNamesToMapConstants.value(mapName, mapName); warpJson["dest_warp_id"] = this->getDestinationWarpID(); this->addCustomValuesTo(&warpJson); @@ -516,17 +512,11 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) { this->setElevation(ParseUtil::jsonToInt(json["elevation"])); this->setDestinationWarpID(ParseUtil::jsonToQString(json["dest_warp_id"])); - // Ensure the warp destination map constant is valid before adding it to the warps. - const QString dynamicMapConstant = project->getDynamicMapDefineName(); - QString mapConstant = ParseUtil::jsonToQString(json["dest_map"]); - if (project->mapConstantsToMapNames.contains(mapConstant)) { - this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant)); - } else if (mapConstant == dynamicMapConstant) { - this->setDestinationMap(DYNAMIC_MAP_NAME); - } else { - logWarn(QString("Destination Map constant '%1' is invalid. Using default '%2'.").arg(mapConstant).arg(dynamicMapConstant)); - this->setDestinationMap(DYNAMIC_MAP_NAME); - } + // Log a warning if "dest_map" isn't a known map ID, but don't overwrite user data. + const QString mapConstant = ParseUtil::jsonToQString(json["dest_map"]); + if (!project->mapConstantsToMapNames.contains(mapConstant)) + logWarn(QString("Unknown Destination Map constant '%1'.").arg(mapConstant)); + this->setDestinationMap(project->mapConstantsToMapNames.value(mapConstant, mapConstant)); this->readCustomValues(json); @@ -534,7 +524,7 @@ bool WarpEvent::loadFromJson(QJsonObject json, Project *project) { } void WarpEvent::setDefaultValues(Project *) { - if (this->getMap()) this->setDestinationMap(this->getMap()->name); + if (this->getMap()) this->setDestinationMap(this->getMap()->name()); this->setDestinationWarpID("0"); this->setElevation(0); } @@ -949,16 +939,16 @@ OrderedJson::object HealLocationEvent::buildEventJson(Project *) { void HealLocationEvent::setDefaultValues(Project *) { this->setElevation(projectConfig.defaultElevation); - if (!this->getMap()) + if (!this->map) return; + bool respawnEnabled = projectConfig.healLocationRespawnDataEnabled; - const QString mapConstant = Map::mapConstantFromName(this->getMap()->name, false); const QString prefix = projectConfig.getIdentifier(respawnEnabled ? ProjectIdentifier::define_spawn_prefix : ProjectIdentifier::define_heal_locations_prefix); - this->setLocationName(mapConstant); - this->setIdName(prefix + mapConstant); + this->setLocationName(this->map->constantName()); + this->setIdName(prefix + this->map->constantName()); if (respawnEnabled) { - this->setRespawnMap(this->getMap()->name); + this->setRespawnMap(this->map->name()); this->setRespawnNPC(1); } } diff --git a/src/core/heallocation.cpp b/src/core/heallocation.cpp index 7fb424da..d4f9a2c8 100644 --- a/src/core/heallocation.cpp +++ b/src/core/heallocation.cpp @@ -3,6 +3,8 @@ #include "events.h" #include "map.h" +// TODO: Remove + HealLocation::HealLocation(QString id, QString map, int i, int16_t x, int16_t y, QString respawnMap, uint8_t respawnNPC) { diff --git a/src/core/map.cpp b/src/core/map.cpp index 2a7d96dc..a42faa27 100644 --- a/src/core/map.cpp +++ b/src/core/map.cpp @@ -13,25 +13,51 @@ Map::Map(QObject *parent) : QObject(parent) { - editHistory.setClean(); + m_editHistory = new QUndoStack(this); + resetEvents(); + + m_header = new MapHeader(this); + connect(m_header, &MapHeader::modified, this, &Map::modified); +} + +Map::Map(const Map &other, QObject *parent) : Map(parent) { + m_name = other.m_name; + m_constantName = other.m_constantName; + m_layoutId = other.m_layoutId; + m_sharedEventsMap = other.m_sharedEventsMap; + m_sharedScriptsMap = other.m_sharedScriptsMap; + m_customAttributes = other.m_customAttributes; + *m_header = *other.m_header; + m_layout = other.m_layout; + m_isPersistedToFile = false; + m_metatileLayerOrder = other.m_metatileLayerOrder; + m_metatileLayerOpacity = other.m_metatileLayerOpacity; + + // Copy events + for (auto i = other.m_events.constBegin(); i != other.m_events.constEnd(); i++) { + QList newEvents; + for (const auto &event : i.value()) { + auto newEvent = event->duplicate(); + m_ownedEvents.insert(newEvent); + newEvents.append(newEvent); + } + m_events[i.key()] = newEvents; + } + + // Duplicating the map connections is probably not desirable, so we skip them. } Map::~Map() { - qDeleteAll(ownedEvents); - ownedEvents.clear(); + qDeleteAll(m_ownedEvents); + m_ownedEvents.clear(); deleteConnections(); } -void Map::setName(QString mapName) { - name = mapName; - constantName = mapConstantFromName(mapName); - scriptsLoaded = false; -} - +// Note: Map does not take ownership of layout void Map::setLayout(Layout *layout) { - this->layout = layout; + m_layout = layout; if (layout) { - this->layoutId = layout->id; + m_layoutId = layout->id; } } @@ -42,29 +68,23 @@ QString Map::mapConstantFromName(QString mapName, bool includePrefix) { const QString prefix = includePrefix ? projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) : ""; QString withMapAndUppercase = prefix + nameWithUnderscores.toUpper(); static const QRegularExpression underscores("_+"); - QString constantName = withMapAndUppercase.replace(underscores, "_"); - - // Handle special cases. - // SSTidal needs to be SS_TIDAL, rather than SSTIDAL - constantName = constantName.replace("SSTIDAL", "SS_TIDAL"); - - return constantName; + return withMapAndUppercase.replace(underscores, "_"); } -int Map::getWidth() { - return layout->getWidth(); +int Map::getWidth() const { + return m_layout->getWidth(); } -int Map::getHeight() { - return layout->getHeight(); +int Map::getHeight() const { + return m_layout->getHeight(); } -int Map::getBorderWidth() { - return layout->getBorderWidth(); +int Map::getBorderWidth() const { + return m_layout->getBorderWidth(); } -int Map::getBorderHeight() { - return layout->getBorderHeight(); +int Map::getBorderHeight() const { + return m_layout->getBorderHeight(); } // Get the portion of the map that can be rendered when rendered as a map connection. @@ -106,7 +126,7 @@ QPixmap Map::renderConnection(const QString &direction, Layout * fromLayout) { if (MapConnection::isDiving(direction)) fromLayout = nullptr; - QPixmap connectionPixmap = this->layout->render(true, fromLayout, bounds); + QPixmap connectionPixmap = m_layout->render(true, fromLayout, bounds); return connectionPixmap.copy(bounds.x() * 16, bounds.y() * 16, bounds.width() * 16, bounds.height() * 16); } @@ -114,18 +134,10 @@ void Map::openScript(QString label) { emit openScriptRequested(label); } -QList Map::getAllEvents() const { - QList all_events; - for (const auto &event_list : events) { - all_events << event_list; - } - return all_events; -} - QStringList Map::getScriptLabels(Event::Group group) { - if (!this->scriptsLoaded) { - this->scriptsFileLabels = ParseUtil::getGlobalScriptLabels(this->getScriptsFilePath()); - this->scriptsLoaded = true; + if (!m_scriptsLoaded) { + m_scriptsFileLabels = ParseUtil::getGlobalScriptLabels(getScriptsFilePath()); + m_scriptsLoaded = true; } QStringList scriptLabels; @@ -133,20 +145,20 @@ QStringList Map::getScriptLabels(Event::Group group) { // Get script labels currently in-use by the map's events if (group == Event::Group::None) { ScriptTracker scriptTracker; - for (Event *event : this->getAllEvents()) { + for (const auto &event : getEvents()) { event->accept(&scriptTracker); } scriptLabels = scriptTracker.getScripts(); } else { ScriptTracker scriptTracker; - for (Event *event : events.value(group)) { + for (const auto &event : m_events.value(group)) { event->accept(&scriptTracker); } scriptLabels = scriptTracker.getScripts(); } // Add scripts from map's scripts file, and empty names. - scriptLabels.append(this->scriptsFileLabels); + scriptLabels.append(m_scriptsFileLabels); scriptLabels.sort(Qt::CaseInsensitive); scriptLabels.prepend("0x0"); scriptLabels.prepend("NULL"); @@ -162,7 +174,7 @@ QString Map::getScriptsFilePath() const { auto path = QDir::cleanPath(QString("%1/%2/%3/scripts") .arg(projectConfig.projectDir) .arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)) - .arg(this->name)); + .arg(m_name)); auto extension = Project::getScriptFileExtension(usePoryscript); if (usePoryscript && !QFile::exists(path + extension)) extension = Project::getScriptFileExtension(false); @@ -170,37 +182,77 @@ QString Map::getScriptsFilePath() const { return path; } +void Map::resetEvents() { + m_events[Event::Group::Object].clear(); + m_events[Event::Group::Warp].clear(); + m_events[Event::Group::Coord].clear(); + m_events[Event::Group::Bg].clear(); + m_events[Event::Group::Heal].clear(); +} + +QList Map::getEvents(Event::Group group) const { + if (group == Event::Group::None) { + // Get all events + QList all_events; + for (const auto &event_list : m_events) { + all_events << event_list; + } + return all_events; + } + return m_events[group]; +} + +Event* Map::getEvent(Event::Group group, int index) const { + return m_events[group].value(index, nullptr); +} + +int Map::getNumEvents(Event::Group group) const { + if (group == Event::Group::None) { + // Total number of events + int numEvents = 0; + for (auto i = m_events.constBegin(); i != m_events.constEnd(); i++) { + numEvents += i.value().length(); + } + return numEvents; + } + return m_events[group].length(); +} + void Map::removeEvent(Event *event) { - for (Event::Group key : events.keys()) { - events[key].removeAll(event); + for (auto i = m_events.begin(); i != m_events.end(); i++) { + i.value().removeAll(event); } } void Map::addEvent(Event *event) { event->setMap(this); - events[event->getEventGroup()].append(event); - if (!ownedEvents.contains(event)) ownedEvents.append(event); + m_events[event->getEventGroup()].append(event); + if (!m_ownedEvents.contains(event)) m_ownedEvents.insert(event); +} + +int Map::getIndexOfEvent(Event *event) const { + return m_events.value(event->getEventGroup()).indexOf(event); } void Map::deleteConnections() { - qDeleteAll(this->ownedConnections); - this->ownedConnections.clear(); - this->connections.clear(); + qDeleteAll(m_ownedConnections); + m_ownedConnections.clear(); + m_connections.clear(); } QList Map::getConnections() const { - return this->connections; + return m_connections; } void Map::addConnection(MapConnection *connection) { - if (!connection || this->connections.contains(connection)) + if (!connection || m_connections.contains(connection)) return; // Maps should only have one Dive/Emerge connection at a time. // (Users can technically have more by editing their data manually, but we will only display one at a time) // Any additional connections being added (this can happen via mirroring) are tracked for deleting but otherwise ignored. if (MapConnection::isDiving(connection->direction())) { - for (auto i : this->connections) { + for (const auto &i : m_connections) { if (i->direction() == connection->direction()) { trackConnection(connection); return; @@ -218,8 +270,8 @@ void Map::loadConnection(MapConnection *connection) { if (!connection) return; - if (!this->connections.contains(connection)) - this->connections.append(connection); + if (!m_connections.contains(connection)) + m_connections.append(connection); trackConnection(connection); } @@ -227,12 +279,12 @@ void Map::loadConnection(MapConnection *connection) { void Map::trackConnection(MapConnection *connection) { connection->setParentMap(this, false); - if (!this->ownedConnections.contains(connection)) { - this->ownedConnections.insert(connection); + if (!m_ownedConnections.contains(connection)) { + m_ownedConnections.insert(connection); connect(connection, &MapConnection::parentMapChanged, [=](Map *, Map *after) { if (after != this && after != nullptr) { // MapConnection's parent has been reassigned, it's no longer our responsibility - this->ownedConnections.remove(connection); + m_ownedConnections.remove(connection); QObject::disconnect(connection, &MapConnection::parentMapChanged, this, nullptr); } }); @@ -241,23 +293,29 @@ void Map::trackConnection(MapConnection *connection) { // We retain ownership of this MapConnection until it's assigned to a new parent map. void Map::removeConnection(MapConnection *connection) { - if (!this->connections.removeOne(connection)) + if (!m_connections.removeOne(connection)) return; connection->setParentMap(nullptr, false); modify(); emit connectionRemoved(connection); } +void Map::commit(QUndoCommand *cmd) { + m_editHistory->push(cmd); +} + void Map::modify() { emit modified(); } -void Map::clean() { - this->hasUnsavedDataChanges = false; +void Map::setClean() { + m_editHistory->setClean(); + m_hasUnsavedDataChanges = false; + m_isPersistedToFile = true; } bool Map::hasUnsavedChanges() const { - return !editHistory.isClean() || this->layout->hasUnsavedChanges() || hasUnsavedDataChanges || !isPersistedToFile; + return !m_editHistory->isClean() || m_layout->hasUnsavedChanges() || m_hasUnsavedDataChanges || !m_isPersistedToFile; } void Map::pruneEditHistory() { @@ -271,11 +329,11 @@ void Map::pruneEditHistory() { ID_MapConnectionAdd, ID_MapConnectionRemove }; - for (int i = 0; i < this->editHistory.count(); i++) { + for (int i = 0; i < m_editHistory->count(); i++) { // Qt really doesn't expect editing commands in the stack to be valid (fair). // A better future design might be to have separate edit histories per map tab, // and dumping the entire Connections tab history with QUndoStack::clear. - auto command = const_cast(this->editHistory.command(i)); + auto command = const_cast(m_editHistory->command(i)); if (mapConnectionIds.contains(command->id())) command->setObsolete(true); } diff --git a/src/core/mapconnection.cpp b/src/core/mapconnection.cpp index b80b8d09..db2755e9 100644 --- a/src/core/mapconnection.cpp +++ b/src/core/mapconnection.cpp @@ -65,7 +65,7 @@ QPixmap MapConnection::getPixmap() { if (!map) return QPixmap(); - return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout : nullptr); + return map->renderConnection(m_direction, m_parentMap ? m_parentMap->layout() : nullptr); } void MapConnection::setParentMap(Map* map, bool mirror) { @@ -75,7 +75,7 @@ void MapConnection::setParentMap(Map* map, bool mirror) { if (mirror) { auto connection = findMirror(); if (connection) - connection->setTargetMapName(map ? map->name : QString(), false); + connection->setTargetMapName(map ? map->name() : QString(), false); } if (m_parentMap) @@ -91,7 +91,7 @@ void MapConnection::setParentMap(Map* map, bool mirror) { } QString MapConnection::parentMapName() const { - return m_parentMap ? m_parentMap->name : QString(); + return m_parentMap ? m_parentMap->name() : QString(); } void MapConnection::setTargetMapName(const QString &targetMapName, bool mirror) { diff --git a/src/core/mapheader.cpp b/src/core/mapheader.cpp new file mode 100644 index 00000000..689a52dc --- /dev/null +++ b/src/core/mapheader.cpp @@ -0,0 +1,121 @@ +#include "mapheader.h" + +MapHeader::MapHeader(const MapHeader& other) : MapHeader() { + m_song = other.m_song; + m_location = other.m_location; + m_requiresFlash = other.m_requiresFlash; + m_weather = other.m_weather; + m_type = other.m_type; + m_showsLocationName = other.m_showsLocationName; + m_allowsRunning = other.m_allowsRunning; + m_allowsBiking = other.m_allowsBiking; + m_allowsEscaping = other.m_allowsEscaping; + m_floorNumber = other.m_floorNumber; + m_battleScene = other.m_battleScene; +} + +MapHeader &MapHeader::operator=(const MapHeader &other) { + // We want to call each set function here to ensure any fieldChanged signals + // are sent as necessary. This does also mean the modified signal can be sent + // repeatedly (but for now at least that's not a big issue). + setSong(other.m_song); + setLocation(other.m_location); + setRequiresFlash(other.m_requiresFlash); + setWeather(other.m_weather); + setType(other.m_type); + setShowsLocationName(other.m_showsLocationName); + setAllowsRunning(other.m_allowsRunning); + setAllowsBiking(other.m_allowsBiking); + setAllowsEscaping(other.m_allowsEscaping); + setFloorNumber(other.m_floorNumber); + setBattleScene(other.m_battleScene); + return *this; +} + +void MapHeader::setSong(const QString &song) { + if (m_song == song) + return; + m_song = song; + emit songChanged(m_song); + emit modified(); +} + +void MapHeader::setLocation(const QString &location) { + if (m_location == location) + return; + m_location = location; + emit locationChanged(m_location); + emit modified(); +} + +void MapHeader::setRequiresFlash(bool requiresFlash) { + if (m_requiresFlash == requiresFlash) + return; + m_requiresFlash = requiresFlash; + emit requiresFlashChanged(m_requiresFlash); + emit modified(); +} + +void MapHeader::setWeather(const QString &weather) { + if (m_weather == weather) + return; + m_weather = weather; + emit weatherChanged(m_weather); + emit modified(); +} + +void MapHeader::setType(const QString &type) { + if (m_type == type) + return; + m_type = type; + emit typeChanged(m_type); + emit modified(); +} + +void MapHeader::setShowsLocationName(bool showsLocationName) { + if (m_showsLocationName == showsLocationName) + return; + m_showsLocationName = showsLocationName; + emit showsLocationNameChanged(m_showsLocationName); + emit modified(); +} + +void MapHeader::setAllowsRunning(bool allowsRunning) { + if (m_allowsRunning == allowsRunning) + return; + m_allowsRunning = allowsRunning; + emit allowsRunningChanged(m_allowsRunning); + emit modified(); +} + +void MapHeader::setAllowsBiking(bool allowsBiking) { + if (m_allowsBiking == allowsBiking) + return; + m_allowsBiking = allowsBiking; + emit allowsBikingChanged(m_allowsBiking); + emit modified(); +} + +void MapHeader::setAllowsEscaping(bool allowsEscaping) { + if (m_allowsEscaping == allowsEscaping) + return; + m_allowsEscaping = allowsEscaping; + emit allowsEscapingChanged(m_allowsEscaping); + emit modified(); +} + +void MapHeader::setFloorNumber(int floorNumber) { + if (m_floorNumber == floorNumber) + return; + m_floorNumber = floorNumber; + emit floorNumberChanged(m_floorNumber); + emit modified(); +} + +void MapHeader::setBattleScene(const QString &battleScene) { + if (m_battleScene == battleScene) + return; + m_battleScene = battleScene; + emit battleSceneChanged(m_battleScene); + emit modified(); +} diff --git a/src/core/maplayout.cpp b/src/core/maplayout.cpp index 34033ac5..94e6c291 100644 --- a/src/core/maplayout.cpp +++ b/src/core/maplayout.cpp @@ -5,15 +5,17 @@ #include "scripting.h" #include "imageproviders.h" +Layout::Layout(const Layout &other) : Layout() { + copyFrom(&other); +} - -Layout *Layout::copy() { +Layout *Layout::copy() const { Layout *layout = new Layout; layout->copyFrom(this); return layout; } -void Layout::copyFrom(Layout *other) { +void Layout::copyFrom(const Layout *other) { this->id = other->id; this->name = other->name; this->width = other->width; @@ -36,29 +38,20 @@ QString Layout::layoutConstantFromName(QString mapName) { QString nameWithUnderscores = mapName.replace(caseChange, "\\1_\\2"); QString withMapAndUppercase = "LAYOUT_" + nameWithUnderscores.toUpper(); static const QRegularExpression underscores("_+"); - QString constantName = withMapAndUppercase.replace(underscores, "_"); - - // Handle special cases. - // SSTidal should be SS_TIDAL, rather than SSTIDAL - constantName = constantName.replace("SSTIDAL", "SS_TIDAL"); - - return constantName; + return withMapAndUppercase.replace(underscores, "_"); } -int Layout::getWidth() { - return width; -} - -int Layout::getHeight() { - return height; -} - -int Layout::getBorderWidth() { - return border_width; -} - -int Layout::getBorderHeight() { - return border_height; +Layout::Settings Layout::settings() const { + Layout::Settings settings; + settings.id = this->id; + settings.name = this->name; + settings.width = this->width; + settings.height = this->height; + settings.borderWidth = this->border_width; + settings.borderHeight = this->border_height; + settings.primaryTilesetLabel = this->tileset_primary_label; + settings.secondaryTilesetLabel = this->tileset_secondary_label; + return settings; } bool Layout::isWithinBounds(int x, int y) { diff --git a/src/core/mapparser.cpp b/src/core/mapparser.cpp deleted file mode 100644 index 3d4258bd..00000000 --- a/src/core/mapparser.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "mapparser.h" -#include "config.h" -#include "log.h" -#include "project.h" - -MapParser::MapParser() -{ -} - -Layout *MapParser::parse(QString filepath, bool *error, Project *project) -{ - QFile file(filepath); - if (!file.open(QIODevice::ReadOnly)) { - *error = true; - logError(QString("Could not open Advance Map 1.92 Map .map file '%1': ").arg(filepath) + file.errorString()); - return nullptr; - } - - QByteArray in = file.readAll(); - file.close(); - - if (in.length() < 20 || in.length() % 2 != 0) { - *error = true; - logError(QString("Advance Map 1.92 Map .map file '%1' is an unexpected size.").arg(filepath)); - return nullptr; - } - - int borderWidth = static_cast(in.at(16)); // 0 in RSE .map files - int borderHeight = static_cast(in.at(17)); // 0 in RSE .map files - int numBorderTiles = borderWidth * borderHeight; // 0 if RSE - - int mapDataOffset = 20 + (numBorderTiles * 2); // FRLG .map files store border metatile data after the header - int mapWidth = static_cast(in.at(0)) | - (static_cast(in.at(1)) << 8) | - (static_cast(in.at(2)) << 16) | - (static_cast(in.at(3)) << 24); - int mapHeight = static_cast(in.at(4)) | - (static_cast(in.at(5)) << 8) | - (static_cast(in.at(6)) << 16) | - (static_cast(in.at(7)) << 24); - int mapPrimaryTilesetNum = static_cast(in.at(8)) | - (static_cast(in.at(9)) << 8) | - (static_cast(in.at(10)) << 16) | - (static_cast(in.at(11)) << 24); - int mapSecondaryTilesetNum = static_cast(in.at(12)) | - (static_cast(in.at(13)) << 8) | - (static_cast(in.at(14)) << 16) | - (static_cast(in.at(15)) << 24); - - int numMetatiles = mapWidth * mapHeight; - int expectedFileSize = 20 + (numBorderTiles * 2) + (numMetatiles * 2); - if (in.length() != expectedFileSize) { - *error = true; - logError(QString(".map file is an unexpected size. Expected %1 bytes, but it has %2 bytes.").arg(expectedFileSize).arg(in.length())); - return nullptr; - } - - Blockdata blockdata; - for (int i = mapDataOffset; (i + 1) < in.length(); i += 2) { - uint16_t word = static_cast((in[i] & 0xff) + ((in[i + 1] & 0xff) << 8)); - blockdata.append(word); - } - - Blockdata border; - if (numBorderTiles != 0) { - for (int i = 20; (i + 1) < mapDataOffset; i += 2) { - uint16_t word = static_cast((in[i] & 0xff) + ((in[i + 1] & 0xff) << 8)); - border.append(word); - } - } - - Layout *mapLayout = new Layout(); - mapLayout->width = mapWidth; - mapLayout->height = mapHeight; - mapLayout->border_width = (borderWidth == 0) ? DEFAULT_BORDER_WIDTH : borderWidth; - mapLayout->border_height = (borderHeight == 0) ? DEFAULT_BORDER_HEIGHT : borderHeight; - - QList tilesets = project->tilesetLabelsOrdered; - - if (mapPrimaryTilesetNum > tilesets.size()) - mapLayout->tileset_primary_label = tilesets.at(0); - else - mapLayout->tileset_primary_label = tilesets.at(mapPrimaryTilesetNum); - - if (mapSecondaryTilesetNum > tilesets.size()) - mapLayout->tileset_secondary_label = tilesets.at(1); - else - mapLayout->tileset_secondary_label = tilesets.at(mapSecondaryTilesetNum); - - mapLayout->blockdata = blockdata; - - if (!border.isEmpty()) { - mapLayout->border = border; - } - - return mapLayout; -} diff --git a/src/core/metatileparser.cpp b/src/core/metatileparser.cpp deleted file mode 100644 index 104b757a..00000000 --- a/src/core/metatileparser.cpp +++ /dev/null @@ -1,99 +0,0 @@ -#include "metatileparser.h" -#include "config.h" -#include "log.h" -#include "project.h" -#include - -QList MetatileParser::parse(QString filepath, bool *error, bool primaryTileset) -{ - QFile file(filepath); - if (!file.open(QIODevice::ReadOnly)) { - *error = true; - logError(QString("Could not open Advance Map 1.92 Metatile .bvd file '%1': ").arg(filepath) + file.errorString()); - return { }; - } - - QByteArray in = file.readAll(); - file.close(); - - if (in.length() < 9 || in.length() % 2 != 0) { - *error = true; - logError(QString("Advance Map 1.92 Metatile .bvd file '%1' is an unexpected size.").arg(filepath)); - return { }; - } - - int projIdOffset = in.length() - 4; - int metatileSize = 16; - BaseGameVersion version; - if (in.at(projIdOffset + 0) == 'R' - && in.at(projIdOffset + 1) == 'S' - && in.at(projIdOffset + 2) == 'E' - && in.at(projIdOffset + 3) == ' ') { - // ruby and emerald are handled equally here. - version = BaseGameVersion::pokeemerald; - } else if (in.at(projIdOffset + 0) == 'F' - && in.at(projIdOffset + 1) == 'R' - && in.at(projIdOffset + 2) == 'L' - && in.at(projIdOffset + 3) == 'G') { - version = BaseGameVersion::pokefirered; - } else { - *error = true; - logError(QString("Detected unsupported game type from .bvd file. Last 4 bytes of file must be 'RSE ' or 'FRLG'.")); - return { }; - } - - int attrSize = Metatile::getDefaultAttributesSize(version); - int maxMetatiles = primaryTileset ? Project::getNumMetatilesPrimary() : Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary(); - int numMetatiles = static_cast(in.at(0)) | - (static_cast(in.at(1)) << 8) | - (static_cast(in.at(2)) << 16) | - (static_cast(in.at(3)) << 24); - if (numMetatiles > maxMetatiles) { - *error = true; - logError(QString(".bvd file contains data for %1 metatiles, but the maximum number of metatiles is %2.").arg(numMetatiles).arg(maxMetatiles)); - return { }; - } - if (numMetatiles < 1) { - *error = true; - logError(QString(".bvd file contains no data for metatiles.")); - return { }; - } - - int expectedFileSize = 4 + (metatileSize * numMetatiles) + (attrSize * numMetatiles) + 4; - if (in.length() != expectedFileSize) { - *error = true; - logError(QString(".bvd file is an unexpected size. Expected %1 bytes, but it has %2 bytes.").arg(expectedFileSize).arg(in.length())); - return { }; - } - - QList metatiles; - for (int i = 0; i < numMetatiles; i++) { - Metatile *metatile = new Metatile(); - QList tiles; - for (int j = 0; j < 8; j++) { - int metatileOffset = 4 + i * metatileSize + j * 2; - Tile tile(static_cast( - static_cast(in.at(metatileOffset)) | - (static_cast(in.at(metatileOffset + 1)) << 8))); - tiles.append(tile); - } - - // AdvanceMap .bvd files only contain 8 tiles of data per metatile. - // If the user has triple-layer metatiles enabled we need to fill the remaining 4 tiles ourselves. - if (projectConfig.tripleLayerMetatilesEnabled) { - Tile tile = Tile(); - for (int j = 0; j < 4; j++) - tiles.append(tile); - } - - int attrOffset = 4 + (numMetatiles * metatileSize) + (i * attrSize); - uint32_t attributes = 0; - for (int j = 0; j < attrSize; j++) - attributes |= static_cast(in.at(attrOffset + j)) << (8 * j); - metatile->setAttributes(attributes, version); - metatile->tiles = tiles; - metatiles.append(metatile); - } - - return metatiles; -} diff --git a/src/core/paletteutil.cpp b/src/core/paletteutil.cpp index da281ce6..929336b2 100644 --- a/src/core/paletteutil.cpp +++ b/src/core/paletteutil.cpp @@ -1,4 +1,5 @@ #include "paletteutil.h" +#include "advancemapparser.h" #include "log.h" #include #include @@ -6,7 +7,6 @@ QList parsePal(QString filepath, bool *error); QList parseJASC(QString filepath, bool *error); -QList parseAdvanceMapPal(QString filepath, bool *error); QList parseAdobeColorTable(QString filepath, bool *error); QList parseTileLayerPro(QString filepath, bool *error); QList parseAdvancePaletteEditor(QString filepath, bool *error); @@ -81,7 +81,7 @@ QList parsePal(QString filepath, bool *error) { return parseJASC(filepath, error); } else { file.close(); - return parseAdvanceMapPal(filepath, error); + return AdvanceMapParser::parsePalette(filepath, error); } } @@ -152,38 +152,6 @@ QList parseJASC(QString filepath, bool *error) { return palette; } -QList parseAdvanceMapPal(QString filepath, bool *error) { - QFile file(filepath); - if (!file.open(QIODevice::ReadOnly)) { - *error = true; - logError(QString("Could not open Advance Map 1.92 palette file '%1': ").arg(filepath) + file.errorString()); - return QList(); - } - - QByteArray in = file.readAll(); - file.close(); - - if (in.length() % 4 != 0) { - *error = true; - logError(QString("Advance Map 1.92 palette file '%1' had an unexpected format. File's length must be a multiple of 4, but the length is %2.").arg(filepath).arg(in.length())); - return QList(); - } - - QList palette; - int i = 0; - while (i < in.length()) { - unsigned char red = static_cast(in.at(i)); - unsigned char green = static_cast(in.at(i + 1)); - unsigned char blue = static_cast(in.at(i + 2)); - palette.append(qRgb(clampColorValue(red), - clampColorValue(green), - clampColorValue(blue))); - i += 4; - } - - return palette; -} - QList parseAdobeColorTable(QString filepath, bool *error) { QFile file(filepath); if (!file.open(QIODevice::ReadOnly)) { diff --git a/src/core/parseutil.cpp b/src/core/parseutil.cpp index 2c357776..a4a2018d 100644 --- a/src/core/parseutil.cpp +++ b/src/core/parseutil.cpp @@ -593,13 +593,13 @@ bool ParseUtil::gameStringToBool(QString gameString, bool * ok) { return gameStringToInt(gameString, ok) != 0; } -QMap> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash memberMap) { +tsl::ordered_map> ParseUtil::readCStructs(const QString &filename, const QString &label, const QHash &memberMap) { QString filePath = this->root + "/" + filename; auto cParser = fex::Parser(); auto tokens = fex::Lexer().LexFile(filePath); - auto structs = cParser.ParseTopLevelObjects(tokens); - QMap> structMaps; - for (auto it = structs.begin(); it != structs.end(); it++) { + auto topLevelObjects = cParser.ParseTopLevelObjects(tokens); + tsl::ordered_map> structs; + for (auto it = topLevelObjects.begin(); it != topLevelObjects.end(); it++) { QString structLabel = QString::fromStdString(it->first); if (structLabel.isEmpty()) continue; if (!label.isEmpty() && label != structLabel) continue; // Speed up parsing if only looking for a particular symbol @@ -617,9 +617,9 @@ QMap> ParseUtil::readCStructs(const QString &fi } i++; } - structMaps.insert(structLabel, values); + structs[structLabel] = values; } - return structMaps; + return structs; } QList ParseUtil::getLabelMacros(const QList &list, const QString &label) { diff --git a/src/core/regionmap.cpp b/src/core/regionmap.cpp index 94650ccd..587be33c 100644 --- a/src/core/regionmap.cpp +++ b/src/core/regionmap.cpp @@ -397,10 +397,15 @@ void RegionMap::saveLayout() { case LayoutFormat::Binary: { QByteArray data; + int defaultValue = this->project->mapSectionIdNames.indexOf(this->default_map_section); for (int m = 0; m < this->layout_height; m++) { for (int n = 0; n < this->layout_width; n++) { int i = n + this->layout_width * m; - data.append(this->project->mapSectionIdNames.indexOf(this->layouts["main"][i].map_section)); + int mapSectionValue = this->project->mapSectionIdNames.indexOf(this->layouts["main"][i].map_section); + if (mapSectionValue < 0){ + mapSectionValue = defaultValue; + } + data.append(mapSectionValue); } } QFile bfile(fullPath(this->layout_path)); diff --git a/src/core/tileset.cpp b/src/core/tileset.cpp index be3a04d4..e79e7706 100644 --- a/src/core/tileset.cpp +++ b/src/core/tileset.cpp @@ -3,6 +3,8 @@ #include "project.h" #include "log.h" #include "config.h" +#include "imageproviders.h" +#include "validator.h" #include #include @@ -23,7 +25,7 @@ Tileset::Tileset(const Tileset &other) metatileLabels(other.metatileLabels), palettes(other.palettes), palettePreviews(other.palettePreviews), - hasUnsavedTilesImage(false) + m_hasUnsavedTilesImage(other.m_hasUnsavedTilesImage) { for (auto tile : other.tiles) { tiles.append(tile.copy()); @@ -179,10 +181,8 @@ bool Tileset::setMetatileLabel(int metatileId, QString label, Tileset *primaryTi if (!tileset) return false; - static const QRegularExpression expression("[_A-Za-z0-9]*$"); - QRegularExpressionValidator validator(expression); - int pos = 0; - if (validator.validate(label, pos) != QValidator::Acceptable) + IdentifierValidator validator; + if (!validator.isValid(label)) return false; tileset->metatileLabels[metatileId] = label; @@ -397,3 +397,184 @@ QHash Tileset::getHeaderMemberMap(bool usingAsm) map.insert(metatileAttrPosition, "metatileAttributes"); return map; } + +void 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; + } + + QByteArray data = metatiles_file.readAll(); + int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); + int bytesPerMetatile = 2 * tilesPerMetatile; + int num_metatiles = data.length() / bytesPerMetatile; + for (int i = 0; i < num_metatiles; i++) { + auto metatile = new Metatile; + int index = i * bytesPerMetatile; + for (int j = 0; j < tilesPerMetatile; j++) { + uint16_t tileRaw = static_cast(data[index++]); + tileRaw |= static_cast(data[index++]) << 8; + metatile->tiles.append(Tile(tileRaw)); + } + m_metatiles.append(metatile); + } +} + +void 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; + } + + QByteArray data; + int numTiles = projectConfig.getNumTilesInMetatile(); + for (const auto &metatile : m_metatiles) { + for (int i = 0; i < numTiles; i++) { + uint16_t tile = metatile->tiles.at(i).rawValue(); + data.append(static_cast(tile)); + data.append(static_cast(tile >> 8)); + } + } + metatiles_file.write(data); +} + +void 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; + } + + QByteArray data = attrs_file.readAll(); + int attrSize = projectConfig.metatileAttributesSize; + int numMetatiles = m_metatiles.length(); + int numMetatileAttrs = data.length() / attrSize; + if (numMetatiles != numMetatileAttrs) { + logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(numMetatiles).arg(numMetatileAttrs).arg(this->name)); + } + + for (int i = 0; i < qMin(numMetatiles, numMetatileAttrs); i++) { + uint32_t attributes = 0; + for (int j = 0; j < attrSize; j++) + attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); + m_metatiles.at(i)->setAttributes(attributes); + } +} + +void 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; + } + + QByteArray data; + for (const auto &metatile : m_metatiles) { + uint32_t attributes = metatile->getAttributes(); + for (int i = 0; i < projectConfig.metatileAttributesSize; i++) + data.append(static_cast(attributes >> (8 * i))); + } + attrs_file.write(data); +} + +void Tileset::loadTilesImage(QImage *importedImage) { + QImage image; + if (importedImage) { + image = *importedImage; + m_hasUnsavedTilesImage = true; + } else if (QFile::exists(this->tilesImagePath)) { + // No image provided, load from file path. + image = QImage(this->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); + } else { + // Use default image + image = QImage(8, 8, QImage::Format_Indexed8); + } + + // Validate image contains 16 colors. + int colorCount = image.colorCount(); + if (colorCount > 16) { + flattenTo4bppImage(&image); + } else if (colorCount < 16) { + QVector colorTable = image.colorTable(); + for (int i = colorTable.length(); i < 16; i++) { + colorTable.append(Qt::black); + } + image.setColorTable(colorTable); + } + + QList tiles; + int w = 8; + int h = 8; + for (int y = 0; y < image.height(); y += h) + for (int x = 0; x < image.width(); x += w) { + QImage tile = image.copy(x, y, w, h); + tiles.append(tile); + } + this->tilesImage = image; + this->tiles = tiles; +} + +void 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; + + if (!this->tilesImage.save(this->tilesImagePath, "PNG")) { + logError(QString("Failed to save tiles image '%1'").arg(this->tilesImagePath)); + return; + } + + m_hasUnsavedTilesImage = false; +} + +void Tileset::loadPalettes() { + this->palettes.clear(); + this->palettePreviews.clear(); + + for (int i = 0; i < Project::getNumPalettesTotal(); i++) { + QList palette; + QString path = this->palettePaths.value(i); + if (!path.isEmpty()) { + bool error = false; + palette = PaletteUtil::parse(path, &error); + if (error) palette.clear(); + } + if (palette.isEmpty()) { + // Either the palette failed to load, or no palette exists. + // We expect tilesets to have a certain number of palettes, + // so fill this palette with dummy colors. + for (int j = 0; j < 16; j++) { + palette.append(qRgb(j * 16, j * 16, j * 16)); + } + } + this->palettes.append(palette); + this->palettePreviews.append(palette); + } +} + +void Tileset::savePalettes() { + 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); + } +} + +void Tileset::load() { + loadMetatiles(); + loadMetatileAttributes(); + loadTilesImage(); + loadPalettes(); +} + +// Because metatile labels are global (and handled by the project) we don't save them here. +void Tileset::save() { + saveMetatiles(); + saveMetatileAttributes(); + saveTilesImage(); + savePalettes(); +} diff --git a/src/core/validator.cpp b/src/core/validator.cpp new file mode 100644 index 00000000..b3b36589 --- /dev/null +++ b/src/core/validator.cpp @@ -0,0 +1,30 @@ +#include "validator.h" + +// Identifiers must only contain word characters, and cannot start with a digit. +const QRegularExpression IdentifierValidator::re_identifier = QRegularExpression("[A-Za-z_]+[\\w]*"); + + +bool PrefixValidator::missingPrefix(const QString &input) const { + return !m_prefix.isEmpty() && !input.startsWith(m_prefix); +} + +QValidator::State PrefixValidator::validate(QString &input, int &pos) const { + auto state = QRegularExpressionValidator::validate(input, pos); + if (state == QValidator::Acceptable) { + // This input could be valid. If there's a prefix we should require it now. + if (missingPrefix(input)) + state = QValidator::Intermediate; + } + return state; +} + +void PrefixValidator::fixup(QString &input) const { + QRegularExpressionValidator::fixup(input); + if (missingPrefix(input)) + input.prepend(m_prefix); +} + +bool PrefixValidator::isValid(QString &input) const { + int pos = 0; + return validate(input, pos) == QValidator::Acceptable; +} diff --git a/src/editor.cpp b/src/editor.cpp index 86cedd5b..c22068d2 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -11,6 +11,7 @@ #include "config.h" #include "scripting.h" #include "customattributestable.h" +#include "validator.h" #include #include #include @@ -152,7 +153,7 @@ void Editor::setEditorView() { map_item->setEditsEnabled(false); case EditMode::Events: if (this->map) { - this->editGroup.setActiveStack(&this->map->editHistory); + this->editGroup.setActiveStack(this->map->editHistory()); } break; case EditMode::Header: @@ -240,23 +241,23 @@ void Editor::displayWildMonTables() { clearWildMonTables(); // Don't try to read encounter data if it doesn't exist on disk for this map. - if (!project->wildMonData.contains(map->constantName)) { + if (!project->wildMonData.contains(map->constantName())) { return; } QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel; - for (auto groupPair : project->wildMonData[map->constantName]) + for (auto groupPair : project->wildMonData[map->constantName()]) labelCombo->addItem(groupPair.first); labelCombo->setCurrentText(labelCombo->itemText(0)); QStackedWidget *stack = ui->stackedWidget_WildMons; int labelIndex = 0; - for (auto labelPair : project->wildMonData[map->constantName]) { + for (auto labelPair : project->wildMonData[map->constantName()]) { QString label = labelPair.first; - WildPokemonHeader header = project->wildMonData[map->constantName][label]; + WildPokemonHeader header = project->wildMonData[map->constantName()][label]; MonTabWidget *tabWidget = new MonTabWidget(this); stack->insertWidget(labelIndex++, tabWidget); @@ -267,7 +268,7 @@ void Editor::displayWildMonTables() { tabWidget->clearTableAt(tabIndex); - if (project->wildMonData.contains(map->constantName) && header.wildMons[fieldName].active) { + if (project->wildMonData.contains(map->constantName()) && header.wildMons[fieldName].active) { tabWidget->populateTab(tabIndex, header.wildMons[fieldName]); } else { tabWidget->setTabActive(tabIndex, false); @@ -299,9 +300,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { QLineEdit *lineEdit = new QLineEdit(); lineEdit->setClearButtonEnabled(true); form.addRow(new QLabel("Group Base Label:"), lineEdit); - static const QRegularExpression re_validChars("[_A-Za-z0-9]*"); - QRegularExpressionValidator *validator = new QRegularExpressionValidator(re_validChars); - lineEdit->setValidator(validator); + lineEdit->setValidator(new IdentifierValidator(lineEdit)); connect(lineEdit, &QLineEdit::textChanged, [this, &lineEdit, &buttonBox](QString text){ if (this->project->encounterGroupLabels.contains(text)) { lineEdit->setStyleSheet("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); @@ -312,7 +311,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { } }); // Give a default value to the label. - lineEdit->setText(QString("g%1%2").arg(map->name).arg(stack->count())); + lineEdit->setText(QString("g%1%2").arg(map->name()).arg(stack->count())); // Fields [x] copy from existing QLabel *fieldsLabel = new QLabel("Fields:"); @@ -415,9 +414,9 @@ void Editor::deleteWildMonGroup() { msgBox.exec(); if (msgBox.clickedButton() == deleteButton) { - auto it = project->wildMonData.find(map->constantName); + auto it = project->wildMonData.find(map->constantName()); if (it == project->wildMonData.end()) { - logError(QString("Failed to find data for map %1. Unable to delete").arg(map->constantName)); + logError(QString("Failed to find data for map %1. Unable to delete").arg(map->constantName())); return; } @@ -698,7 +697,7 @@ void Editor::saveEncounterTabData() { if (!stack->count()) return; - tsl::ordered_map &encounterMap = project->wildMonData[map->constantName]; + tsl::ordered_map &encounterMap = project->wildMonData[map->constantName()]; for (int groupIndex = 0; groupIndex < stack->count(); groupIndex++) { MonTabWidget *tabWidget = static_cast(stack->widget(groupIndex)); @@ -863,13 +862,13 @@ void Editor::addConnection(MapConnection *connection) { // It's possible this is a Dive/Emerge connection, but that's ok (no selection will occur). connection_to_select = connection; - this->map->editHistory.push(new MapConnectionAdd(this->map, connection)); + this->map->commit(new MapConnectionAdd(this->map, connection)); } void Editor::removeConnection(MapConnection *connection) { if (!connection) return; - this->map->editHistory.push(new MapConnectionRemove(this->map, connection)); + this->map->commit(new MapConnectionRemove(this->map, connection)); } void Editor::removeConnectionPixmap(MapConnection *connection) { @@ -986,7 +985,7 @@ void Editor::setDivingMapName(QString mapName, QString direction) { if (mapName.isEmpty()) { removeConnection(connection); } else { - map->editHistory.push(new MapConnectionChangeMap(connection, mapName)); + map->commit(new MapConnectionChangeMap(connection, mapName)); } } else if (!mapName.isEmpty()) { // Create new connection @@ -1167,7 +1166,7 @@ void Editor::setCursorRectVisible(bool visible) { void Editor::onHoveredMapMetatileChanged(const QPoint &pos) { int x = pos.x(); int y = pos.y(); - if (!layout->isWithinBounds(x, y)) + if (!layout || !layout->isWithinBounds(x, y)) return; this->updateCursorRectPos(x, y); @@ -1199,7 +1198,7 @@ void Editor::onHoveredMapMetatileCleared() { } void Editor::onHoveredMapMovementPermissionChanged(int x, int y) { - if (!layout->isWithinBounds(x, y)) + if (!layout || !layout->isWithinBounds(x, y)) return; this->updateCursorRectPos(x, y); @@ -1243,10 +1242,10 @@ QString Editor::getMovementPermissionText(uint16_t collision, uint16_t elevation void Editor::unsetMap() { // disconnect previous map's signals so they are not firing // multiple times if set again in the future - if (map) { - map->pruneEditHistory(); - map->disconnect(this); - for (auto connection : map->getConnections()) + if (this->map) { + this->map->pruneEditHistory(); + this->map->disconnect(this); + for (const auto &connection : this->map->getConnections()) disconnectMapConnection(connection); } clearMapConnections(); @@ -1259,19 +1258,18 @@ bool Editor::setMap(QString map_name) { return false; } - unsetMap(); - Map *loadedMap = project->loadMap(map_name); if (!loadedMap) { return false; } + unsetMap(); this->map = loadedMap; - setLayout(map->layout->id); + setLayout(map->layout()->id); - editGroup.addStack(&map->editHistory); - editGroup.setActiveStack(&map->editHistory); + editGroup.addStack(map->editHistory()); + editGroup.setActiveStack(map->editHistory()); selected_events->clear(); if (!displayMap()) { @@ -1292,13 +1290,17 @@ bool Editor::setLayout(QString layoutId) { return false; } - this->layout = this->project->loadLayout(layoutId); + Layout *loadedLayout = this->project->loadLayout(layoutId); + if (!loadedLayout) { + return false; + } + this->layout = loadedLayout; if (!displayLayout()) { return false; } - editGroup.addStack(&layout->editHistory); + editGroup.addStack(&this->layout->editHistory); map_ruler->setMapDimensions(QSize(this->layout->getWidth(), this->layout->getHeight())); connect(this->layout, &Layout::layoutDimensionsChanged, map_ruler, &MapRuler::setMapDimensions); @@ -1469,7 +1471,7 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, LayoutPixmapItem *i } selection_origin = QPoint(pos.x(), pos.y()); - map->editHistory.push(new EventShift(selectedEvents, xDelta, yDelta, actionId)); + map->commit(new EventShift(selectedEvents, xDelta, yDelta, actionId)); } } } @@ -1784,8 +1786,7 @@ void Editor::displayMapEvents() { events_group = new QGraphicsItemGroup; scene->addItem(events_group); - QList events = map->getAllEvents(); - for (Event *event : events) { + for (const auto &event : map->getEvents()) { project->setEventPixmap(event); addMapEvent(event); } @@ -2032,14 +2033,14 @@ void Editor::updateBorderVisibility() { // When connecting a map to itself we don't bother to re-render the map connections in real-time, // i.e. if the user paints a new metatile on the map this isn't immediately reflected in the connection. // We're rendering them now, so we take the opportunity to do a full re-render for self-connections. - bool fullRender = (this->map && item->connection && this->map->name == item->connection->targetMapName()); + bool fullRender = (this->map && item->connection && this->map->name() == item->connection->targetMapName()); item->render(fullRender); } } void Editor::updateCustomMapHeaderValues(QTableWidget *table) { - map->customHeaders = CustomAttributesTable::getAttributes(table); + map->setCustomAttributes(CustomAttributesTable::getAttributes(table)); map->modify(); } @@ -2080,13 +2081,13 @@ void Editor::redrawObject(DraggablePixmapItem *item) { void Editor::updateWarpEventWarning(Event *event) { if (porymapConfig.warpBehaviorWarningDisabled) return; - if (!project || !map || !map->layout || !event || event->getEventType() != Event::Type::Warp) + if (!project || !map || !map->layout() || !event || event->getEventType() != Event::Type::Warp) return; Block block; Metatile * metatile = nullptr; WarpEvent * warpEvent = static_cast(event); - if (map->layout->getBlock(warpEvent->getX(), warpEvent->getY(), &block)) { - metatile = Tileset::getMetatile(block.metatileId(), map->layout->tileset_primary, map->layout->tileset_secondary); + if (map->layout()->getBlock(warpEvent->getX(), warpEvent->getY(), &block)) { + metatile = Tileset::getMetatile(block.metatileId(), map->layout()->tileset_primary, map->layout()->tileset_secondary); } // metatile may be null if the warp is in the map border. Display the warning in this case bool validWarpBehavior = metatile && projectConfig.warpBehaviors.contains(metatile->behavior()); @@ -2144,10 +2145,7 @@ void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) { void Editor::selectedEventIndexChanged(int index, Event::Group eventGroup) { int event_offs = Event::getIndexOffset(eventGroup); index = index - event_offs; - Event *event = nullptr; - if (index < this->map->events.value(eventGroup).length()) { - event = this->map->events.value(eventGroup).at(index); - } + Event *event = this->map->getEvent(eventGroup, index); DraggablePixmapItem *selectedEvent = nullptr; for (QGraphicsItem *child : this->events_group->childItems()) { DraggablePixmapItem *item = static_cast(child); @@ -2189,7 +2187,7 @@ void Editor::duplicateSelectedEvents() { duplicate->setY(duplicate->getY() + 1); selectedEvents.append(duplicate); } - map->editHistory.push(new EventDuplicate(this, map, selectedEvents)); + map->commit(new EventDuplicate(this, map, selectedEvents)); } DraggablePixmapItem *Editor::addNewEvent(Event::Type type) { @@ -2209,7 +2207,7 @@ DraggablePixmapItem *Editor::addNewEvent(Event::Type type) { ((HealLocationEvent *)event)->setIndex(project->healLocations.length()); } - map->editHistory.push(new EventCreate(this, map, event)); + map->commit(new EventCreate(this, map, event)); return event->getPixmapItem(); } @@ -2217,7 +2215,7 @@ DraggablePixmapItem *Editor::addNewEvent(Event::Type type) { bool Editor::eventLimitReached(Event::Type event_type) { if (project && map) { if (Event::typeToGroup(event_type) == Event::Group::Object) - return map->events.value(Event::Group::Object).length() >= project->getMaxObjectEvents(); + return map->getNumEvents(Event::Group::Object) >= project->getMaxObjectEvents(); } return false; } @@ -2246,14 +2244,12 @@ void Editor::deleteSelectedEvents() { // If deleting multiple events, just let editor work out next selected. if (numDeleted == 1) { Event::Group event_group = selectedEvents[0]->getEventGroup(); - int index = this->map->events.value(event_group).indexOf(selectedEvents[0]); - if (index != this->map->events.value(event_group).size() - 1) + int index = this->map->getIndexOfEvent(selectedEvents[0]); + if (index != this->map->getNumEvents(event_group) - 1) index++; else index--; - Event *event = nullptr; - if (index >= 0) - event = this->map->events.value(event_group).at(index); + Event *event = this->map->getEvent(event_group, index); for (QGraphicsItem *child : this->events_group->childItems()) { DraggablePixmapItem *event_item = static_cast(child); if (event_item->event == event) { @@ -2262,7 +2258,7 @@ void Editor::deleteSelectedEvents() { } } } - this->map->editHistory.push(new EventDelete(this, this->map, selectedEvents, nextSelectedEvent ? nextSelectedEvent->event : nullptr)); + this->map->commit(new EventDelete(this, this->map, selectedEvents, nextSelectedEvent ? nextSelectedEvent->event : nullptr)); } } diff --git a/src/lib/collapsiblesection.cpp b/src/lib/collapsiblesection.cpp new file mode 100644 index 00000000..8ad8560a --- /dev/null +++ b/src/lib/collapsiblesection.cpp @@ -0,0 +1,143 @@ +/* + Elypson/qt-collapsible-section + (c) 2016 Michael A. Voelkel - michael.alexander.voelkel@gmail.com + + This file is part of Elypson/qt-collapsible section. + + Elypson/qt-collapsible-section is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Elypson/qt-collapsible-section is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Elypson/qt-collapsible-section. If not, see . + + + PORYMAP NOTE: Modified to support having the section expanded by default, to stop the contents + squashing during the collapse animation, and to add some guard rails against crashes. +*/ + +#include "collapsiblesection.h" +CollapsibleSection::CollapsibleSection(const QString& title, const bool expanded, const int animationDuration, QWidget* parent) + : QWidget(parent), animationDuration(animationDuration), expanded(expanded) +{ + toggleButton = new QToolButton(this); + headerLine = new QFrame(this); + toggleAnimation = new QParallelAnimationGroup(this); + contentArea = new QScrollArea(this); + mainLayout = new QGridLayout(this); + + toggleButton->setStyleSheet("QToolButton {border: none;}"); + toggleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toggleButton->setText(title); + toggleButton->setCheckable(true); + updateToggleButton(); + + headerLine->setFrameShape(QFrame::HLine); + headerLine->setFrameShadow(QFrame::Sunken); + headerLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + + contentArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + contentArea->setMaximumHeight(0); + contentArea->setMinimumHeight(0); + + sectionAnimations.insert(new QPropertyAnimation(this, "minimumHeight")); + sectionAnimations.insert(new QPropertyAnimation(this, "maximumHeight")); + for (const auto &anim : sectionAnimations) + toggleAnimation->addAnimation(anim); + contentAnimation = new QPropertyAnimation(contentArea, "maximumHeight"); + toggleAnimation->addAnimation(contentAnimation); + + mainLayout->setVerticalSpacing(0); + mainLayout->setContentsMargins(0, 0, 0, 0); + + int row = 0; + mainLayout->addWidget(toggleButton, row, 0, 1, 1, Qt::AlignLeft); + mainLayout->addWidget(headerLine, row++, 2, 1, 1); + mainLayout->addWidget(contentArea, row, 0, 1, 3); + setLayout(mainLayout); + + connect(toggleButton, &QToolButton::toggled, this, &CollapsibleSection::toggle); +} + +void CollapsibleSection::updateToggleButton() +{ + toggleButton->setChecked(this->expanded); + toggleButton->setArrowType(this->expanded ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); +} + +void CollapsibleSection::toggle(bool expand) +{ + if (toggleAnimation->state() != QAbstractAnimation::Stopped) + return; + if (this->expanded == expand) + return; + this->expanded = expand; + + updateToggleButton(); + + if (expand) { + // Opening animation. Set the contents to their maximum size immediately, + // and they will be revealed slowly by the section animation. + int contentHeight = getContentHeight(); + contentArea->setMinimumHeight(contentHeight); + contentArea->setMaximumHeight(contentHeight); + toggleAnimation->setDirection(QAbstractAnimation::Forward); + } else { + // Closing animation. Keep the contents at their current size, allowing + // them to be hidden slowly by the section animation, then change their size + // once the animation is complete so they aren't visible just below the title. + auto ctx = new QObject(); + connect(toggleAnimation, &QAbstractAnimation::finished, ctx, [this, ctx]() { + // This is a single-shot connection. Qt6 has built-in support for this kind of thing. + contentArea->setMinimumHeight(0); + contentArea->setMaximumHeight(0); + ctx->deleteLater(); + }); + toggleAnimation->setDirection(QAbstractAnimation::Backward); + } + toggleAnimation->start(); +} + +void CollapsibleSection::setContentLayout(QLayout* contentLayout) +{ + if (contentArea->layout() == contentLayout) + return; + delete contentArea->layout(); + contentArea->setLayout(contentLayout); + collapsedHeight = sizeHint().height() - contentArea->maximumHeight(); + + int contentHeight = this->expanded ? getContentHeight() : 0; + contentArea->setMinimumHeight(contentHeight); + contentArea->setMaximumHeight(contentHeight); + + updateAnimationTargets(); +} + +void CollapsibleSection::setTitle(const QString &title) +{ + toggleButton->setText(title); +} + +int CollapsibleSection::getContentHeight() const +{ + return contentArea->layout() ? contentArea->layout()->sizeHint().height() : 0; +} + +void CollapsibleSection::updateAnimationTargets() +{ + const int contentHeight = getContentHeight(); + for (auto anim : sectionAnimations) { + anim->setDuration(animationDuration); + anim->setStartValue(collapsedHeight); + anim->setEndValue(collapsedHeight + contentHeight); + } + contentAnimation->setDuration(animationDuration); + contentAnimation->setStartValue(0); + contentAnimation->setEndValue(contentHeight); +} diff --git a/src/lib/fex/parser.cpp b/src/lib/fex/parser.cpp index 2e2a6f3e..ce4a9bbb 100644 --- a/src/lib/fex/parser.cpp +++ b/src/lib/fex/parser.cpp @@ -337,9 +337,9 @@ namespace fex return DefineStatement(identifer, value); } - std::map Parser::ReadDefines(const QString &filename, std::vector matching) + tsl::ordered_map Parser::ReadDefines(const QString &filename, std::vector matching) { - std::map out; + tsl::ordered_map out; Lexer lexer; auto tokens = lexer.LexFile(filename); @@ -488,12 +488,12 @@ namespace fex return items; } - std::map Parser::ParseTopLevelObjects(std::vector tokens) + tsl::ordered_map Parser::ParseTopLevelObjects(std::vector tokens) { index_ = 0; tokens_ = std::move(tokens); - std::map items; + tsl::ordered_map items; while (index_ < tokens_.size()) { diff --git a/src/lib/fex/parser_util.cpp b/src/lib/fex/parser_util.cpp new file mode 100644 index 00000000..3a5d47f9 --- /dev/null +++ b/src/lib/fex/parser_util.cpp @@ -0,0 +1,50 @@ +#include "lib/fex/parser_util.h" + +#include + +#include "lib/fex/parser.h" + +ParserUtil::ParserUtil(QString root): root_(root) {} + +QStringList ParserUtil::ReadDefines(QString filename, QString prefix) +{ + if (filename.isEmpty()) { + return QStringList(); + } + + QString filepath = root_ + "/" + filename; + + fex::Parser parser; + + std::vector match_list = { prefix.toStdString() + ".*" }; + tsl::ordered_map defines = parser.ReadDefines(filepath.toStdString(), match_list); + + QStringList out; + for(auto const& define : defines) { + out.append(QString::fromStdString(define.first)); + } + + return out; +} + +QStringList ParserUtil::ReadDefinesValueSort(QString filename, QString prefix) +{ + + if (filename.isEmpty()) { + return QStringList(); + } + + QString filepath = root_ + "/" + filename; + + fex::Parser parser; + + std::vector match_list = { prefix.toStdString() + ".*" }; + tsl::ordered_map defines = parser.ReadDefines(filepath.toStdString(), match_list); + + QMultiMap defines_keyed_by_value; + for (const auto& pair : defines) { + defines_keyed_by_value.insert(pair.second, QString::fromStdString(pair.first)); + } + + return defines_keyed_by_value.values(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1608c786..16d55c2d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -14,7 +14,7 @@ #include "editcommands.h" #include "flowlayout.h" #include "shortcut.h" -#include "mapparser.h" +#include "advancemapparser.h" #include "prefab.h" #include "montabwidget.h" #include "imageexport.h" @@ -23,6 +23,11 @@ #include "newmapconnectiondialog.h" #include "config.h" #include "filedialog.h" +#include "newmapdialog.h" +#include "newtilesetdialog.h" +#include "newmapgroupdialog.h" +#include "newlocationdialog.h" +#include "message.h" #include #include @@ -33,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -65,7 +69,7 @@ MainWindow::MainWindow(QWidget *parent) : QCoreApplication::setOrganizationName("pret"); QCoreApplication::setApplicationName("porymap"); QCoreApplication::setApplicationVersion(PORYMAP_VERSION); - QApplication::setApplicationDisplayName("porymap"); + QApplication::setApplicationDisplayName(QApplication::applicationName()); QApplication::setWindowIcon(QIcon(":/icons/porymap-icon-2.ico")); ui->setupUi(this); @@ -86,11 +90,27 @@ MainWindow::MainWindow(QWidget *parent) : MainWindow::~MainWindow() { + // Some config settings are updated as subwindows are destroyed (e.g. their geometry), + // so we need to ensure that the configs are saved after this happens. + saveGlobalConfigs(); + delete label_MapRulerStatus; delete editor; delete ui; } +void MainWindow::saveGlobalConfigs() { + porymapConfig.setMainGeometry( + this->saveGeometry(), + this->saveState(), + this->ui->splitter_map->saveState(), + this->ui->splitter_main->saveState(), + this->ui->splitter_Metatiles->saveState() + ); + porymapConfig.save(); + shortcutsConfig.save(); +} + void MainWindow::setWindowDisabled(bool disabled) { for (auto action : findChildren()) action->setDisabled(disabled); @@ -132,6 +152,7 @@ void MainWindow::initWindow() { #endif setWindowDisabled(true); + show(); } void MainWindow::initShortcuts() { @@ -235,6 +256,10 @@ void MainWindow::initCustomUI() { ui->mainTabBar->addTab(mainTabNames.value(i)); ui->mainTabBar->setTabIcon(i, mainTabIcons.value(i)); } + + // Create map header data widget + this->mapHeaderForm = new MapHeaderForm(); + ui->layout_HeaderData->addWidget(this->mapHeaderForm); } void MainWindow::initExtraSignals() { @@ -270,6 +295,9 @@ void MainWindow::initExtraSignals() { label_MapRulerStatus->setAlignment(Qt::AlignCenter); label_MapRulerStatus->setTextFormat(Qt::PlainText); label_MapRulerStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); + + connect(ui->action_NewMap, &QAction::triggered, this, &MainWindow::openNewMapDialog); + connect(ui->action_NewLayout, &QAction::triggered, this, &MainWindow::openNewLayoutDialog); } void MainWindow::on_actionCheck_for_Updates_triggered() { @@ -383,7 +411,8 @@ void MainWindow::initMapList() { // Create add map/layout button QPushButton *buttonAdd = new QPushButton(QIcon(":/icons/add.ico"), ""); - connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::on_action_NewMap_triggered); + buttonAdd->setToolTip("Create New Map"); + connect(buttonAdd, &QPushButton::clicked, this, &MainWindow::openNewMapDialog); layout->addWidget(buttonAdd); /* TODO: Remove button disabled, no current support for deleting maps/layouts @@ -397,32 +426,32 @@ void MainWindow::initMapList() { // Connect tool bars to lists ui->mapListToolBar_Groups->setList(ui->mapList); - ui->mapListToolBar_Areas->setList(ui->areaList); + ui->mapListToolBar_Locations->setList(ui->locationList); ui->mapListToolBar_Layouts->setList(ui->layoutList); // Left-clicking on items in the map list opens the corresponding map/layout. - connect(ui->mapList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); - connect(ui->areaList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); - connect(ui->layoutList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); + connect(ui->mapList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); + connect(ui->locationList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); + connect(ui->layoutList, &QAbstractItemView::activated, this, &MainWindow::openMapListItem); // Right-clicking on items in the map list brings up a context menu. - connect(ui->mapList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); - connect(ui->areaList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); - connect(ui->layoutList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); + connect(ui->mapList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); + connect(ui->locationList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); + connect(ui->layoutList, &QTreeView::customContextMenuRequested, this, &MainWindow::onOpenMapListContextMenu); // Only the groups list allows reorganizing folder contents, editing folder names, etc. - ui->mapListToolBar_Areas->setEditsAllowedButtonVisible(false); + ui->mapListToolBar_Locations->setEditsAllowedButtonVisible(false); ui->mapListToolBar_Layouts->setEditsAllowedButtonVisible(false); // When map list search filter is cleared we want the current map/layout in the editor to be visible in the list. - connect(ui->mapListToolBar_Groups, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentMap); - connect(ui->mapListToolBar_Areas, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentMap); - connect(ui->mapListToolBar_Layouts, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentLayout); + connect(ui->mapListToolBar_Groups, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentMap); + connect(ui->mapListToolBar_Locations, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentMap); + connect(ui->mapListToolBar_Layouts, &MapListToolBar::filterCleared, this, &MainWindow::scrollMapListToCurrentLayout); // Connect the "add folder" button in each of the map lists - connect(ui->mapListToolBar_Groups, &MapListToolBar::addFolderClicked, this, &MainWindow::mapListAddGroup); - connect(ui->mapListToolBar_Areas, &MapListToolBar::addFolderClicked, this, &MainWindow::mapListAddArea); - connect(ui->mapListToolBar_Layouts, &MapListToolBar::addFolderClicked, this, &MainWindow::mapListAddLayout); + connect(ui->mapListToolBar_Groups, &MapListToolBar::addFolderClicked, this, &MainWindow::openNewMapGroupDialog); + connect(ui->mapListToolBar_Locations, &MapListToolBar::addFolderClicked, this, &MainWindow::openNewLocationDialog); + connect(ui->mapListToolBar_Layouts, &MapListToolBar::addFolderClicked, this, &MainWindow::openNewLayoutDialog); connect(ui->mapListContainer, &QTabWidget::currentChanged, this, &MainWindow::saveMapListTab); } @@ -442,7 +471,7 @@ void MainWindow::updateWindowTitle() { if (editor->map) { setWindowTitle(QString("%1%2 - %3") .arg(editor->map->hasUnsavedChanges() ? "* " : "") - .arg(editor->map->name) + .arg(editor->map->name()) .arg(projectName) ); } else { @@ -471,7 +500,7 @@ void MainWindow::markMapEdited() { void MainWindow::markSpecificMapEdited(Map* map) { if (!map) return; - map->hasUnsavedDataChanges = true; + map->setHasUnsavedDataChanges(true); if (editor && editor->map == map) updateWindowTitle(); @@ -588,8 +617,6 @@ bool MainWindow::openProject(QString dir, bool initial) { projectConfig.projectDir = dir; projectConfig.load(); - this->newMapDefaultsSet = false; - Scripting::init(this); // Create the project @@ -597,7 +624,13 @@ bool MainWindow::openProject(QString dir, bool initial) { project->set_root(dir); connect(project, &Project::fileChanged, this, &MainWindow::showFileWatcherWarning); connect(project, &Project::mapLoaded, this, &MainWindow::onMapLoaded); - connect(project, &Project::mapSectionIdNamesChanged, this, &MainWindow::refreshLocationsComboBox); + connect(project, &Project::mapCreated, this, &MainWindow::onNewMapCreated); + connect(project, &Project::layoutCreated, this, &MainWindow::onNewLayoutCreated); + connect(project, &Project::tilesetCreated, this, &MainWindow::onNewTilesetCreated); + connect(project, &Project::mapGroupAdded, this, &MainWindow::onNewMapGroupCreated); + connect(project, &Project::mapSectionAdded, this, &MainWindow::onNewMapSectionCreated); + connect(project, &Project::mapSectionDisplayNameChanged, this, &MainWindow::onMapSectionDisplayNameChanged); + connect(project, &Project::mapsExcluded, this, &MainWindow::showMapsExcludedAlert); this->editor->setProject(project); // Make sure project looks reasonable before attempting to load it @@ -647,14 +680,10 @@ bool MainWindow::checkProjectSanity() { logWarn(QString("The directory '%1' failed the project sanity check.").arg(editor->project->root)); - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText(QString("The selected directory appears to be invalid.")); + ErrorMessage msgBox(QStringLiteral("The selected directory appears to be invalid."), this); msgBox.setInformativeText(QString("The directory '%1' is missing key files.\n\n" "Make sure you selected the correct project directory " "(the one used to make your .gba file, e.g. 'pokeemerald').").arg(editor->project->root)); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); auto tryAnyway = msgBox.addButton("Try Anyway", QMessageBox::ActionRole); msgBox.exec(); if (msgBox.clickedButton() == tryAnyway) { @@ -666,10 +695,19 @@ bool MainWindow::checkProjectSanity() { } void MainWindow::showProjectOpenFailure() { - QString errorMsg = QString("There was an error opening the project. Please see %1 for full error details.").arg(getLogPath()); - QMessageBox error(QMessageBox::Critical, "porymap", errorMsg, QMessageBox::Ok, this); - error.setDetailedText(getMostRecentError()); - error.exec(); + RecentErrorMessage::show(QStringLiteral("There was an error opening the project."), this); +} + +// Alert the user that one or more maps have been excluded while loading the project. +void MainWindow::showMapsExcludedAlert(const QStringList &excludedMapNames) { + RecentErrorMessage msgBox("", this); + if (excludedMapNames.length() == 1) { + msgBox.setText(QString("Failed to load map '%1'. Saving will exclude this map from your project.").arg(excludedMapNames.first())); + } else { + msgBox.setText(QStringLiteral("Failed to load the maps listed below. Saving will exclude these maps from your project.")); + msgBox.setDetailedText(excludedMapNames.join("\n")); // Overwrites error details text, user will need to check the log. + } + msgBox.exec(); } bool MainWindow::isProjectOpen() { @@ -682,7 +720,7 @@ bool MainWindow::setInitialMap() { // User recently had a map open that still exists. if (setMap(recent)) return true; - } else if (editor->project->mapLayoutsTable.contains(recent)) { + } else if (editor->project->layoutIds.contains(recent)) { // User recently had a layout open that still exists. if (setLayout(recent)) return true; @@ -693,7 +731,7 @@ bool MainWindow::setInitialMap() { if (name != recent && setMap(name)) return true; } - for (const auto &id : editor->project->mapLayoutsTable) { + for (const auto &id : editor->project->layoutIds) { if (id != recent && setLayout(id)) return true; } @@ -765,22 +803,15 @@ void MainWindow::showFileWatcherWarning(QString filepath) { static bool showing = false; if (showing) return; - QMessageBox notice(this); - notice.setText("File Changed"); - notice.setInformativeText(QString("The file %1 has changed on disk. Would you like to reload the project?") - .arg(filepath.remove(project->root + "/"))); - notice.setStandardButtons(QMessageBox::No | QMessageBox::Yes); - notice.setDefaultButton(QMessageBox::No); - notice.setIcon(QMessageBox::Question); - + QuestionMessage msgBox(QString("The file %1 has changed on disk. Would you like to reload the project?").arg(filepath.remove(project->root + "/")), this); QCheckBox showAgainCheck("Do not ask again."); - notice.setCheckBox(&showAgainCheck); + msgBox.setCheckBox(&showAgainCheck); showing = true; - int choice = notice.exec(); - if (choice == QMessageBox::Yes) { + auto reply = msgBox.exec(); + if (reply == QMessageBox::Yes) { on_action_Reload_Project_triggered(); - } else if (choice == QMessageBox::No) { + } else if (reply == QMessageBox::No) { if (showAgainCheck.isChecked()) { porymapConfig.monitorFiles = false; if (this->preferenceEditor) @@ -803,14 +834,10 @@ void MainWindow::on_action_Open_Project_triggered() void MainWindow::on_action_Reload_Project_triggered() { // TODO: when undo history is complete show only if has unsaved changes - QMessageBox warning(this); - warning.setText("WARNING"); - warning.setInformativeText("Reloading this project will discard any unsaved changes."); - warning.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - warning.setDefaultButton(QMessageBox::Cancel); - warning.setIcon(QMessageBox::Warning); - - if (warning.exec() == QMessageBox::Ok) + WarningMessage msgBox(QStringLiteral("Reloading this project will discard any unsaved changes."), this); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + if (msgBox.exec() == QMessageBox::Ok) openProject(editor->project->root); } @@ -827,37 +854,31 @@ void MainWindow::unsetMap() { // setMap, but with a visible error message in case of failure. // Use when the user is specifically requesting a map to open. bool MainWindow::userSetMap(QString map_name) { - if (editor->map && editor->map->name == map_name) + if (editor->map && editor->map->name() == map_name) return true; // Already set - if (map_name == DYNAMIC_MAP_NAME) { - QMessageBox msgBox(this); - QString errorMsg = QString("The map '%1' can't be opened, it's a placeholder to indicate the specified map will be set programmatically.").arg(map_name); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); + if (map_name == editor->project->getDynamicMapName()) { + WarningMessage msgBox(QString("Cannot open map '%1'.").arg(map_name), this); + msgBox.setInformativeText(QStringLiteral("This map name is a placeholder to indicate that the warp's map will be set programmatically.")); + msgBox.exec(); return false; } if (!setMap(map_name)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening map %1. Please see %2 for full error details.\n\n%3") - .arg(map_name) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Map", errorMsg); + RecentErrorMessage::show(QString("There was an error opening map '%1'.").arg(map_name), this); return false; } return true; } bool MainWindow::setMap(QString map_name) { - if (map_name.isEmpty() || map_name == DYNAMIC_MAP_NAME) { - logInfo(QString("Cannot set map to '%1'").arg(map_name)); + if (!editor || !editor->project || map_name.isEmpty() || map_name == editor->project->getDynamicMapName()) { + logWarn(QString("Ignored setting map to '%1'").arg(map_name)); return false; } logInfo(QString("Setting map to '%1'").arg(map_name)); - - if (!editor || !editor->setMap(map_name)) { + if (!editor->setMap(map_name)) { logWarn(QString("Failed to set map to '%1'").arg(map_name)); return false; } @@ -900,33 +921,29 @@ void MainWindow::setLayoutOnlyMode(bool layoutOnly) { // Use when the user is specifically requesting a layout to open. bool MainWindow::userSetLayout(QString layoutId) { if (!setLayout(layoutId)) { - QMessageBox msgBox(this); - QString errorMsg = QString("There was an error opening layout %1. Please see %2 for full error details.\n\n%3") - .arg(layoutId) - .arg(getLogPath()) - .arg(getMostRecentError()); - msgBox.critical(nullptr, "Error Opening Layout", errorMsg); + RecentErrorMessage::show(QString("There was an error opening layout '%1'.").arg(layoutId), this); return false; } + + // Only the Layouts tab of the map list shows Layouts, so if we're not already on that tab we'll open it now. + ui->mapListContainer->setCurrentIndex(MapListTab::Layouts); + return true; } bool MainWindow::setLayout(QString layoutId) { - if (this->editor->map) - logInfo("Switching to a layout-only editing mode. Disabling map-related edits."); - - unsetMap(); - // Prefer logging the name of the layout as displayed in the map list. - const QString layoutName = this->editor->project ? this->editor->project->layoutIdsToNames.value(layoutId, layoutId) : layoutId; - logInfo(QString("Setting layout to '%1'").arg(layoutName)); + const Layout* layout = this->editor->project ? this->editor->project->mapLayouts.value(layoutId) : nullptr; + logInfo(QString("Setting layout to '%1'").arg(layout ? layout->name : layoutId)); if (!this->editor->setLayout(layoutId)) { return false; } - layoutTreeModel->setLayout(layoutId); + if (this->editor->map) + logInfo("Switching to layout-only editing mode. Disabling map-related edits."); + unsetMap(); refreshMapScene(); updateWindowTitle(); updateMapList(); @@ -981,246 +998,103 @@ void MainWindow::refreshCollisionSelector() { } void MainWindow::openWarpMap(QString map_name, int event_id, Event::Group event_group) { - // Ensure valid destination map name. - if (!editor->project->mapNames.contains(map_name)) { - logError(QString("Invalid map name '%1'").arg(map_name)); - return; - } - // Open the destination map. if (!userSetMap(map_name)) return; // Select the target event. int index = event_id - Event::getIndexOffset(event_group); - QList events = editor->map->events[event_group]; - if (index < events.length() && index >= 0) { - Event *event = events.at(index); + Event* event = editor->map->getEvent(event_group, index); + if (event) { for (DraggablePixmapItem *item : editor->getObjects()) { if (item->event == event) { editor->selected_events->clear(); editor->selected_events->append(item); editor->updateSelectedEvents(); + return; } } - } else { - // Can still warp to this map, but can't select the specified event - logWarn(QString("%1 %2 doesn't exist on map '%3'").arg(Event::eventGroupToString(event_group)).arg(event_id).arg(map_name)); } + // Can still warp to this map, but can't select the specified event + logWarn(QString("%1 %2 doesn't exist on map '%3'").arg(Event::eventGroupToString(event_group)).arg(event_id).arg(map_name)); } void MainWindow::displayMapProperties() { - // Block signals to the comboboxes while they are being modified - const QSignalBlocker blocker1(ui->comboBox_Song); - const QSignalBlocker blocker2(ui->comboBox_Location); - const QSignalBlocker blocker3(ui->comboBox_PrimaryTileset); - const QSignalBlocker blocker4(ui->comboBox_SecondaryTileset); - const QSignalBlocker blocker5(ui->comboBox_Weather); - const QSignalBlocker blocker6(ui->comboBox_BattleScene); - const QSignalBlocker blocker7(ui->comboBox_Type); - const QSignalBlocker blocker8(ui->checkBox_Visibility); - const QSignalBlocker blocker9(ui->checkBox_ShowLocation); - const QSignalBlocker blockerA(ui->checkBox_AllowRunning); - const QSignalBlocker blockerB(ui->checkBox_AllowBiking); - const QSignalBlocker blockerC(ui->spinBox_FloorNumber); - const QSignalBlocker blockerD(ui->checkBox_AllowEscaping); - - ui->checkBox_Visibility->setChecked(false); - ui->checkBox_ShowLocation->setChecked(false); - ui->checkBox_AllowRunning->setChecked(false); - ui->checkBox_AllowBiking->setChecked(false); - ui->checkBox_AllowEscaping->setChecked(false); + this->mapHeaderForm->clear(); if (!editor || !editor->map || !editor->project) { - ui->frame_3->setEnabled(false); + ui->frame_HeaderData->setEnabled(false); return; } + ui->frame_HeaderData->setEnabled(true); + this->mapHeaderForm->setHeader(editor->map->header()); - ui->frame_3->setEnabled(true); - Map *map = editor->map; + const QSignalBlocker b_PrimaryTileset(ui->comboBox_PrimaryTileset); + const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); + ui->comboBox_PrimaryTileset->setCurrentText(editor->map->layout()->tileset_primary_label); + ui->comboBox_SecondaryTileset->setCurrentText(editor->map->layout()->tileset_secondary_label); - ui->comboBox_PrimaryTileset->setCurrentText(map->layout->tileset_primary_label); - ui->comboBox_SecondaryTileset->setCurrentText(map->layout->tileset_secondary_label); - - ui->comboBox_Song->setCurrentText(map->song); - ui->comboBox_Location->setCurrentText(map->location); - ui->checkBox_Visibility->setChecked(map->requiresFlash); - ui->comboBox_Weather->setCurrentText(map->weather); - ui->comboBox_Type->setCurrentText(map->type); - ui->comboBox_BattleScene->setCurrentText(map->battle_scene); - ui->checkBox_ShowLocation->setChecked(map->show_location); - ui->checkBox_AllowRunning->setChecked(map->allowRunning); - ui->checkBox_AllowBiking->setChecked(map->allowBiking); - ui->checkBox_AllowEscaping->setChecked(map->allowEscaping); - ui->spinBox_FloorNumber->setValue(map->floorNumber); // Custom fields table. +/* // TODO: Re-enable ui->tableWidget_CustomHeaderFields->blockSignals(true); ui->tableWidget_CustomHeaderFields->setRowCount(0); for (auto it = map->customHeaders.begin(); it != map->customHeaders.end(); it++) CustomAttributesTable::addAttribute(ui->tableWidget_CustomHeaderFields, it.key(), it.value()); ui->tableWidget_CustomHeaderFields->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); ui->tableWidget_CustomHeaderFields->blockSignals(false); +*/ } void MainWindow::on_comboBox_LayoutSelector_currentTextChanged(const QString &text) { if (editor && editor->project && editor->map) { if (editor->project->mapLayouts.contains(text)) { editor->map->setLayout(editor->project->loadLayout(text)); - setMap(editor->map->name); + setMap(editor->map->name()); markMapEdited(); } } } -void MainWindow::on_comboBox_Song_currentTextChanged(const QString &song) -{ - if (editor && editor->map) { - editor->map->song = song; - markMapEdited(); - } -} - -void MainWindow::on_comboBox_Location_currentTextChanged(const QString &location) -{ - if (editor && editor->map) { - editor->map->location = location; - markMapEdited(); - } -} - -void MainWindow::on_comboBox_Weather_currentTextChanged(const QString &weather) -{ - if (editor && editor->map) { - editor->map->weather = weather; - markMapEdited(); - } -} - -void MainWindow::on_comboBox_Type_currentTextChanged(const QString &type) -{ - if (editor && editor->map) { - editor->map->type = type; - markMapEdited(); - } -} - -void MainWindow::on_comboBox_BattleScene_currentTextChanged(const QString &battle_scene) -{ - if (editor && editor->map) { - editor->map->battle_scene = battle_scene; - markMapEdited(); - } -} - -void MainWindow::on_checkBox_Visibility_stateChanged(int selected) -{ - if (editor && editor->map) { - editor->map->requiresFlash = (selected == Qt::Checked); - markMapEdited(); - } -} - -void MainWindow::on_checkBox_ShowLocation_stateChanged(int selected) -{ - if (editor && editor->map) { - editor->map->show_location = (selected == Qt::Checked); - markMapEdited(); - } -} - -void MainWindow::on_checkBox_AllowRunning_stateChanged(int selected) -{ - if (editor && editor->map) { - editor->map->allowRunning = (selected == Qt::Checked); - markMapEdited(); - } -} - -void MainWindow::on_checkBox_AllowBiking_stateChanged(int selected) -{ - if (editor && editor->map) { - editor->map->allowBiking = (selected == Qt::Checked); - markMapEdited(); - } -} - -void MainWindow::on_checkBox_AllowEscaping_stateChanged(int selected) -{ - if (editor && editor->map) { - editor->map->allowEscaping = (selected == Qt::Checked); - markMapEdited(); - } -} - -void MainWindow::on_spinBox_FloorNumber_valueChanged(int offset) -{ - if (editor && editor->map) { - editor->map->floorNumber = offset; - markMapEdited(); - } -} - // Update the UI using information we've read from the user's project files. bool MainWindow::setProjectUI() { Project *project = editor->project; - // Block signals to the comboboxes while they are being modified - const QSignalBlocker blocker1(ui->comboBox_Song); - const QSignalBlocker blocker3(ui->comboBox_PrimaryTileset); - const QSignalBlocker blocker4(ui->comboBox_SecondaryTileset); - const QSignalBlocker blocker5(ui->comboBox_Weather); - const QSignalBlocker blocker6(ui->comboBox_BattleScene); - const QSignalBlocker blocker7(ui->comboBox_Type); - const QSignalBlocker blocker8(ui->comboBox_DiveMap); - const QSignalBlocker blocker9(ui->comboBox_EmergeMap); - const QSignalBlocker blocker10(ui->comboBox_LayoutSelector); + this->mapHeaderForm->setProject(project); // Set up project comboboxes - ui->comboBox_Song->clear(); - ui->comboBox_Song->addItems(project->songNames); + const QSignalBlocker b_PrimaryTileset(ui->comboBox_PrimaryTileset); ui->comboBox_PrimaryTileset->clear(); ui->comboBox_PrimaryTileset->addItems(project->primaryTilesetLabels); + + const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); ui->comboBox_SecondaryTileset->clear(); ui->comboBox_SecondaryTileset->addItems(project->secondaryTilesetLabels); - ui->comboBox_Weather->clear(); - ui->comboBox_Weather->addItems(project->weatherNames); - ui->comboBox_BattleScene->clear(); - ui->comboBox_BattleScene->addItems(project->mapBattleScenes); - ui->comboBox_Type->clear(); - ui->comboBox_Type->addItems(project->mapTypes); + + const QSignalBlocker b_LayoutSelector(ui->comboBox_LayoutSelector); ui->comboBox_LayoutSelector->clear(); - ui->comboBox_LayoutSelector->addItems(project->mapLayoutsTable); + ui->comboBox_LayoutSelector->addItems(project->layoutIds); + + const QSignalBlocker b_DiveMap(ui->comboBox_DiveMap); ui->comboBox_DiveMap->clear(); ui->comboBox_DiveMap->addItems(project->mapNames); ui->comboBox_DiveMap->setClearButtonEnabled(true); ui->comboBox_DiveMap->setFocusedScrollingEnabled(false); + + const QSignalBlocker b_EmergeMap(ui->comboBox_EmergeMap); ui->comboBox_EmergeMap->clear(); ui->comboBox_EmergeMap->addItems(project->mapNames); ui->comboBox_EmergeMap->setClearButtonEnabled(true); ui->comboBox_EmergeMap->setFocusedScrollingEnabled(false); - refreshLocationsComboBox(); // Show/hide parts of the UI that are dependent on the user's project settings // Wild Encounters tab ui->mainTabBar->setTabEnabled(MainTab::WildPokemon, editor->project->wildEncountersLoaded); - bool hasFlags = projectConfig.mapAllowFlagsEnabled; - ui->checkBox_AllowRunning->setVisible(hasFlags); - ui->checkBox_AllowBiking->setVisible(hasFlags); - ui->checkBox_AllowEscaping->setVisible(hasFlags); - ui->label_AllowRunning->setVisible(hasFlags); - ui->label_AllowBiking->setVisible(hasFlags); - ui->label_AllowEscaping->setVisible(hasFlags); - ui->newEventToolButton->newWeatherTriggerAction->setVisible(projectConfig.eventWeatherTriggerEnabled); ui->newEventToolButton->newSecretBaseAction->setVisible(projectConfig.eventSecretBaseEnabled); ui->newEventToolButton->newCloneObjectAction->setVisible(projectConfig.eventCloneObjectEnabled); - bool floorNumEnabled = projectConfig.floorNumberEnabled; - ui->spinBox_FloorNumber->setVisible(floorNumEnabled); - ui->label_FloorNumber->setVisible(floorNumEnabled); - Event::setIcons(); editor->setCollisionGraphics(); ui->spinBox_SelectedElevation->setMaximum(Block::getMaxElevation()); @@ -1235,10 +1109,10 @@ bool MainWindow::setProjectUI() { this->ui->mapList->setItemDelegateForColumn(0, new GroupNameDelegate(this->editor->project, this)); connect(this->mapGroupModel, &MapGroupModel::dragMoveCompleted, this->ui->mapList, &MapTree::removeSelected); - this->mapAreaModel = new MapAreaModel(editor->project); - this->areaListProxyModel = new FilterChildrenProxyModel(); - areaListProxyModel->setSourceModel(this->mapAreaModel); - ui->areaList->setModel(areaListProxyModel); + this->mapLocationModel = new MapLocationModel(editor->project); + this->locationListProxyModel = new FilterChildrenProxyModel(); + locationListProxyModel->setSourceModel(this->mapLocationModel); + ui->locationList->setModel(locationListProxyModel); this->layoutTreeModel = new LayoutTreeModel(editor->project); this->layoutListProxyModel = new FilterChildrenProxyModel(); @@ -1248,46 +1122,30 @@ bool MainWindow::setProjectUI() { return true; } -void MainWindow::refreshLocationsComboBox() { - QStringList locations = this->editor->project->mapSectionIdNames; - locations.sort(); - - const QSignalBlocker b(ui->comboBox_Location); - ui->comboBox_Location->clear(); - ui->comboBox_Location->addItems(locations); - if (this->editor->map) - ui->comboBox_Location->setCurrentText(this->editor->map->location); -} - void MainWindow::clearProjectUI() { - // Block signals to the comboboxes while they are being modified - const QSignalBlocker blocker1(ui->comboBox_Song); - const QSignalBlocker blocker2(ui->comboBox_Location); - const QSignalBlocker blocker3(ui->comboBox_PrimaryTileset); - const QSignalBlocker blocker4(ui->comboBox_SecondaryTileset); - const QSignalBlocker blocker5(ui->comboBox_Weather); - const QSignalBlocker blocker6(ui->comboBox_BattleScene); - const QSignalBlocker blocker7(ui->comboBox_Type); - const QSignalBlocker blocker8(ui->comboBox_DiveMap); - const QSignalBlocker blocker9(ui->comboBox_EmergeMap); - const QSignalBlocker blockerA(ui->comboBox_LayoutSelector); - - ui->comboBox_Song->clear(); - ui->comboBox_Location->clear(); + // Clear project comboboxes + const QSignalBlocker b_PrimaryTileset(ui->comboBox_PrimaryTileset); ui->comboBox_PrimaryTileset->clear(); + + const QSignalBlocker b_SecondaryTileset(ui->comboBox_SecondaryTileset); ui->comboBox_SecondaryTileset->clear(); - ui->comboBox_Weather->clear(); - ui->comboBox_BattleScene->clear(); - ui->comboBox_Type->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); ui->comboBox_LayoutSelector->clear(); + this->mapHeaderForm->clear(); + // Clear map models delete this->mapGroupModel; delete this->groupListProxyModel; - delete this->mapAreaModel; - delete this->areaListProxyModel; + delete this->mapLocationModel; + delete this->locationListProxyModel; delete this->layoutTreeModel; delete this->layoutListProxyModel; resetMapListFilters(); @@ -1295,7 +1153,7 @@ void MainWindow::clearProjectUI() { Event::clearIcons(); } -void MainWindow::scrollMapList(MapTree *list, QString itemName) { +void MainWindow::scrollMapList(MapTree *list, const QString &itemName) { if (!list || itemName.isEmpty()) return; auto model = static_cast(list->model()); @@ -1314,7 +1172,7 @@ void MainWindow::scrollMapList(MapTree *list, QString itemName) { void MainWindow::scrollMapListToCurrentMap(MapTree *list) { if (this->editor->map) { - scrollMapList(list, this->editor->map->name); + scrollMapList(list, this->editor->map->name()); } } @@ -1334,15 +1192,25 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { auto sourceModel = static_cast(model->sourceModel()); QStandardItem *selectedItem = sourceModel->itemFromIndex(index); const QString itemType = selectedItem->data(MapListUserRoles::TypeRole).toString(); - const QString itemName = selectedItem->data(Qt::UserRole).toString(); + const QString itemName = selectedItem->data(MapListUserRoles::NameRole).toString(); QMenu menu(this); QAction* addToFolderAction = nullptr; QAction* deleteFolderAction = nullptr; QAction* openItemAction = nullptr; + QAction* copyListNameAction = nullptr; + QAction* copyToolTipAction = nullptr; + if (itemType == "map_name") { // Right-clicking on a map. openItemAction = menu.addAction("Open Map"); + menu.addSeparator(); + copyListNameAction = menu.addAction("Copy Map Name"); + copyToolTipAction = menu.addAction("Copy Map ID"); + menu.addSeparator(); + connect(menu.addAction("Duplicate Map"), &QAction::triggered, [this, itemName] { + openDuplicateMapDialog(itemName); + }); //menu.addSeparator(); //connect(menu.addAction("Delete Map"), &QAction::triggered, [this, index] { deleteMapListItem(index); }); // TODO: No support for deleting maps } else if (itemType == "map_group") { @@ -1352,23 +1220,34 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { deleteFolderAction = menu.addAction("Delete Map Group"); } else if (itemType == "map_section") { // Right-clicking on a MAPSEC folder - addToFolderAction = menu.addAction("Add New Map to Area"); + addToFolderAction = menu.addAction("Add New Map to Location"); menu.addSeparator(); - deleteFolderAction = menu.addAction("Delete Area"); + copyListNameAction = menu.addAction("Copy Location ID Name"); + copyToolTipAction = menu.addAction("Copy Location In-Game Name"); + menu.addSeparator(); + deleteFolderAction = menu.addAction("Delete Location"); if (itemName == this->editor->project->getEmptyMapsecName()) deleteFolderAction->setEnabled(false); // Disallow deleting the default name } else if (itemType == "map_layout") { // Right-clicking on a map layout openItemAction = menu.addAction("Open Layout"); + menu.addSeparator(); + copyListNameAction = menu.addAction("Copy Layout Name"); + copyToolTipAction = menu.addAction("Copy Layout ID"); + menu.addSeparator(); + connect(menu.addAction("Duplicate Layout"), &QAction::triggered, [this, itemName] { + openDuplicateLayoutDialog(itemName); + }); addToFolderAction = menu.addAction("Add New Map with Layout"); //menu.addSeparator(); //deleteFolderAction = menu.addAction("Delete Layout"); // TODO: No support for deleting layouts } if (addToFolderAction) { + // All folders only contain maps, so adding an item to any folder is adding a new map. connect(addToFolderAction, &QAction::triggered, [this, itemName] { - openNewMapPopupWindow(); - this->newMapPrompt->init(ui->mapListContainer->currentIndex(), itemName); + auto dialog = new NewMapDialog(this->editor->project, ui->mapListContainer->currentIndex(), itemName, this); + dialog->open(); }); } if (deleteFolderAction) { @@ -1380,404 +1259,159 @@ void MainWindow::onOpenMapListContextMenu(const QPoint &point) { deleteFolderAction->setEnabled(false); } } + if (openItemAction) { - connect(openItemAction, &QAction::triggered, [this, index] { openMapListItem(index); }); + connect(openItemAction, &QAction::triggered, [this, index] { + openMapListItem(index); + }); + } + if (copyListNameAction) { + connect(copyListNameAction, &QAction::triggered, [this, selectedItem] { + setClipboardData(selectedItem->text()); + }); + } + if (copyToolTipAction) { + connect(copyToolTipAction, &QAction::triggered, [this, selectedItem] { + setClipboardData(selectedItem->toolTip()); + }); } if (menu.actions().length() != 0) menu.exec(QCursor::pos()); } -void MainWindow::mapListAddGroup() { - QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::ApplicationModal); - QDialogButtonBox newItemButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); - connect(&newItemButtonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); +void MainWindow::onNewMapCreated(Map *newMap, const QString &groupName) { + logInfo(QString("Created a new map named %1.").arg(newMap->name())); - QLineEdit *newNameEdit = new QLineEdit(&dialog); - newNameEdit->setClearButtonEnabled(true); - - static const QRegularExpression re_validChars("[A-Za-z_]+[\\w]*"); - newNameEdit->setValidator(new QRegularExpressionValidator(re_validChars, newNameEdit)); - - QLabel *errorMessageLabel = new QLabel(&dialog); - errorMessageLabel->setVisible(false); - errorMessageLabel->setStyleSheet("QLabel { background-color: rgba(255, 0, 0, 25%) }"); - - connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ - const QString mapGroupName = newNameEdit->text(); - if (this->editor->project->groupNames.contains(mapGroupName)) { - errorMessageLabel->setText(QString("A map group with the name '%1' already exists").arg(mapGroupName)); - errorMessageLabel->setVisible(true); - } else { - dialog.accept(); - } - }); - - QFormLayout form(&dialog); - - form.addRow("New Group Name", newNameEdit); - form.addRow("", errorMessageLabel); - form.addRow(&newItemButtonBox); - - if (dialog.exec() == QDialog::Accepted) { - QString newFieldName = newNameEdit->text(); - if (newFieldName.isEmpty()) return; - this->mapGroupModel->insertGroupItem(newFieldName); - } -} - -// TODO: Pull this all out into a custom window. Connect that to an action in the main menu as well. -// (or, re-use the new map dialog with some tweaks) -// TODO: This needs to take the same default settings you would get for a new map (tilesets, dimensions, etc.) -// and initialize it with the same fill settings (default metatile/collision/elevation, default border) -void MainWindow::mapListAddLayout() { - if (!editor || !editor->project) return; - - QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::ApplicationModal); - QDialogButtonBox newItemButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); - connect(&newItemButtonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - QLineEdit *newNameEdit = new QLineEdit(&dialog); - newNameEdit->setClearButtonEnabled(true); - - static const QRegularExpression re_validChars("[A-Za-z_]+[\\w]*"); - newNameEdit->setValidator(new QRegularExpressionValidator(re_validChars, newNameEdit)); - - // TODO: Support arbitrary LAYOUT_ ID names (Note from GriffinR: This is already handled in an unopened PR) - QLabel *newId = new QLabel("LAYOUT_", &dialog); - connect(newNameEdit, &QLineEdit::textChanged, [&](QString text){ - newId->setText(Layout::layoutConstantFromName(text.remove("_Layout"))); - }); - - NoScrollComboBox *useExistingCombo = new NoScrollComboBox(&dialog); - useExistingCombo->addItems(this->editor->project->mapLayoutsTable); - useExistingCombo->setEnabled(false); - - QCheckBox *useExistingCheck = new QCheckBox(&dialog); - - QLabel *errorMessageLabel = new QLabel(&dialog); - errorMessageLabel->setVisible(false); - errorMessageLabel->setStyleSheet("QLabel { background-color: rgba(255, 0, 0, 25%) }"); - - QComboBox *primaryCombo = new QComboBox(&dialog); - primaryCombo->addItems(this->editor->project->primaryTilesetLabels); - QComboBox *secondaryCombo = new QComboBox(&dialog); - secondaryCombo->addItems(this->editor->project->secondaryTilesetLabels); - - QSpinBox *widthSpin = new QSpinBox(&dialog); - QSpinBox *heightSpin = new QSpinBox(&dialog); - - widthSpin->setMinimum(1); - heightSpin->setMinimum(1); - widthSpin->setMaximum(this->editor->project->getMaxMapWidth()); - heightSpin->setMaximum(this->editor->project->getMaxMapHeight()); - - connect(useExistingCheck, &QCheckBox::stateChanged, [&](int state){ - bool useExisting = (state == Qt::Checked); - useExistingCombo->setEnabled(useExisting); - primaryCombo->setEnabled(!useExisting); - secondaryCombo->setEnabled(!useExisting); - widthSpin->setEnabled(!useExisting); - heightSpin->setEnabled(!useExisting); - }); - - QFormLayout form(&dialog); - form.addRow("New Layout Name", newNameEdit); - form.addRow("New Layout ID", newId); - form.addRow("Copy Existing Layout", useExistingCheck); - form.addRow("", useExistingCombo); - form.addRow("Primary Tileset", primaryCombo); - form.addRow("Secondary Tileset", secondaryCombo); - form.addRow("Layout Width", widthSpin); - form.addRow("Layout Height", heightSpin); - form.addRow("", errorMessageLabel); - - connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ - // verify some things - QString errorMessage; - QString tryLayoutName = newNameEdit->text(); - // name not empty - if (tryLayoutName.isEmpty()) { - errorMessage = "Name cannot be empty"; - } - // unique layout name & id - else if (this->editor->project->mapLayoutsTable.contains(newId->text()) - || this->editor->project->layoutIdsToNames.find(tryLayoutName) != this->editor->project->layoutIdsToNames.end()) { - errorMessage = "Layout Name / ID is not unique"; - } - // from id is existing value - else if (useExistingCheck->isChecked()) { - if (!this->editor->project->mapLayoutsTable.contains(useExistingCombo->currentText())) { - errorMessage = "Existing layout ID is not valid"; - } - } - - if (!errorMessage.isEmpty()) { - // show error - errorMessageLabel->setText(errorMessage); - errorMessageLabel->setVisible(true); - } - else { - dialog.accept(); - } - }); - - form.addRow(&newItemButtonBox); - - if (dialog.exec() == QDialog::Accepted) { - Layout::SimpleSettings layoutSettings; - QString layoutName = newNameEdit->text(); - layoutSettings.name = layoutName; - layoutSettings.id = Layout::layoutConstantFromName(layoutName.remove("_Layout")); - if (useExistingCheck->isChecked()) { - layoutSettings.from_id = useExistingCombo->currentText(); - } else { - layoutSettings.width = widthSpin->value(); - layoutSettings.height = heightSpin->value(); - layoutSettings.tileset_primary_label = primaryCombo->currentText(); - layoutSettings.tileset_secondary_label = secondaryCombo->currentText(); - } - Layout *newLayout = this->editor->project->createNewLayout(layoutSettings); - this->layoutTreeModel->insertLayoutItem(newLayout->id); - setLayout(newLayout->id); - } -} - -void MainWindow::mapListAddArea() { - QDialog dialog(this, Qt::WindowTitleHint | Qt::WindowCloseButtonHint); - dialog.setWindowModality(Qt::ApplicationModal); - QDialogButtonBox newItemButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog); - connect(&newItemButtonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix); - QLineEdit *newNameEdit = new QLineEdit(&dialog); - QLineEdit *newNameDisplay = new QLineEdit(&dialog); - newNameDisplay->setText(prefix); - newNameDisplay->setEnabled(false); - connect(newNameEdit, &QLineEdit::textEdited, [newNameDisplay, prefix] (const QString &text) { - // As the user types a name, update the label to show the name with the prefix. - newNameDisplay->setText(prefix + text); - }); - - QLabel *errorMessageLabel = new QLabel(&dialog); - errorMessageLabel->setVisible(false); - errorMessageLabel->setStyleSheet("QLabel { background-color: rgba(255, 0, 0, 25%) }"); - - static const QRegularExpression re_validChars("[A-Za-z_]+[\\w]*"); - newNameEdit->setValidator(new QRegularExpressionValidator(re_validChars, newNameEdit)); - - connect(&newItemButtonBox, &QDialogButtonBox::accepted, [&](){ - const QString newAreaName = newNameDisplay->text(); - if (this->editor->project->mapSectionIdNames.contains(newAreaName)){ - errorMessageLabel->setText(QString("An area with the name '%1' already exists").arg(newAreaName)); - errorMessageLabel->setVisible(true); - } else { - dialog.accept(); - } - }); - - QLabel *newNameEditLabel = new QLabel("New Area Name", &dialog); - QLabel *newNameDisplayLabel = new QLabel("Constant Name", &dialog); - newNameDisplayLabel->setEnabled(false); - - QFormLayout form(&dialog); - - form.addRow(newNameEditLabel, newNameEdit); - form.addRow(newNameDisplayLabel, newNameDisplay); - form.addRow("", errorMessageLabel); - form.addRow(&newItemButtonBox); - - if (dialog.exec() == QDialog::Accepted) { - if (newNameEdit->text().isEmpty()) return; - this->mapAreaModel->insertAreaItem(newNameDisplay->text()); - } -} - -void MainWindow::onNewMapCreated() { - QString newMapName = this->newMapPrompt->map->name; - int newMapGroup = this->newMapPrompt->group; - Map *newMap = this->newMapPrompt->map; - bool existingLayout = this->newMapPrompt->existingLayout; - bool importedMap = this->newMapPrompt->importedMap; - - newMap = editor->project->addNewMapToGroup(newMapName, newMapGroup, newMap, existingLayout, importedMap); - - logInfo(QString("Created a new map named %1.").arg(newMapName)); - - // TODO: Creating a new map shouldn't be automatically saved + // TODO: Creating a new map shouldn't be automatically saved. + // For one, it takes away the option to discard the new map. + // For two, if the new map uses an existing layout, any unsaved changes to that layout will also be saved. editor->project->saveMap(newMap); editor->project->saveAllDataStructures(); - // Add new Map / Layout to the mapList models - this->mapGroupModel->insertMapItem(newMapName, editor->project->groupNames[newMapGroup]); - this->mapAreaModel->insertMapItem(newMapName, newMap->location, newMapGroup); - this->layoutTreeModel->insertMapItem(newMapName, newMap->layout->id); + // Add new map to the map lists + this->mapGroupModel->insertMapItem(newMap->name(), groupName); + this->mapLocationModel->insertMapItem(newMap->name(), newMap->header()->location()); + this->layoutTreeModel->insertMapItem(newMap->name(), newMap->layout()->id); // Refresh any combo box that displays map names and persists between maps // (other combo boxes like for warp destinations are repopulated when the map changes). - int mapIndex = this->editor->project->mapNames.indexOf(newMapName); + 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, newMapName); - ui->comboBox_EmergeMap->insertItem(mapIndex, newMapName); + ui->comboBox_DiveMap->insertItem(mapIndex, newMap->name()); + ui->comboBox_EmergeMap->insertItem(mapIndex, newMap->name()); } - // Refresh layout combo box (if a new one was created) - if (!existingLayout) { - int layoutIndex = this->editor->project->mapLayoutsTable.indexOf(newMap->layout->id); - if (layoutIndex >= 0) { - const QSignalBlocker b_Layouts(ui->comboBox_LayoutSelector); - ui->comboBox_LayoutSelector->insertItem(layoutIndex, newMap->layout->id); - } - } - - setMap(newMapName); - - if (newMap->needsHealLocation) { + if (newMap->needsHealLocation()) { addNewEvent(Event::Type::HealLocation); editor->project->saveHealLocations(newMap); editor->save(); } - disconnect(this->newMapPrompt, &NewMapPopup::applied, this, &MainWindow::onNewMapCreated); - delete newMap; + userSetMap(newMap->name()); } -void MainWindow::openNewMapPopupWindow() { - if (!this->newMapDefaultsSet) { - NewMapPopup::setDefaultSettings(this->editor->project); - this->newMapDefaultsSet = true; - } - if (!this->newMapPrompt) { - this->newMapPrompt = new NewMapPopup(this, this->editor->project); - connect(this->newMapPrompt, &NewMapPopup::applied, this, &MainWindow::onNewMapCreated); +// Called any time a new layout is created (including as a byproduct of creating a new map) +void MainWindow::onNewLayoutCreated(Layout *layout) { + logInfo(QString("Created a new layout named %1.").arg(layout->name)); + + // Refresh layout combo box + int layoutIndex = this->editor->project->layoutIds.indexOf(layout->id); + if (layoutIndex >= 0) { + const QSignalBlocker b(ui->comboBox_LayoutSelector); + ui->comboBox_LayoutSelector->insertItem(layoutIndex, layout->id); } - openSubWindow(this->newMapPrompt); + // Add new layout to the Layouts map list view + this->layoutTreeModel->insertMapFolderItem(layout->id); } -void MainWindow::on_action_NewMap_triggered() { - openNewMapPopupWindow(); - this->newMapPrompt->initUi(); - this->newMapPrompt->init(); +void MainWindow::onNewMapGroupCreated(const QString &groupName) { + // Add new map group to the Groups map list view + this->mapGroupModel->insertMapFolderItem(groupName); } -// Insert label for newly-created tileset into sorted list of existing labels -int MainWindow::insertTilesetLabel(QStringList * list, QString label) { - int i = 0; - for (; i < list->length(); i++) - if (list->at(i) > label) break; - list->insert(i, label); - return i; +void MainWindow::onNewMapSectionCreated(const QString &idName) { + // Add new map section to the Locations map list view + this->mapLocationModel->insertMapFolderItem(idName); +} + +void MainWindow::onMapSectionDisplayNameChanged(const QString &idName, const QString &displayName) { + // Update the tool tip in the map list that shows the MAPSEC's in-game name. + QStandardItem *item = this->mapLocationModel->itemAt(idName); + if (item) item->setToolTip(displayName); +} + +void MainWindow::onNewTilesetCreated(Tileset *tileset) { + logInfo(QString("Created a new tileset named %1.").arg(tileset->name)); + + // Refresh tileset combo boxes + if (!tileset->is_secondary) { + int index = this->editor->project->primaryTilesetLabels.indexOf(tileset->name); + ui->comboBox_PrimaryTileset->insertItem(index, tileset->name); + } else { + int index = this->editor->project->secondaryTilesetLabels.indexOf(tileset->name); + ui->comboBox_SecondaryTileset->insertItem(index, tileset->name); + } +} + +void MainWindow::openNewMapGroupDialog() { + auto dialog = new NewMapGroupDialog(this->editor->project, this); + dialog->open(); +} + +void MainWindow::openNewLocationDialog() { + auto dialog = new NewLocationDialog(this->editor->project, this); + dialog->open(); +} + +void MainWindow::openNewMapDialog() { + auto dialog = new NewMapDialog(this->editor->project, this); + dialog->open(); +} + +void MainWindow::openDuplicateMapDialog(const QString &mapName) { + const Map *map = this->editor->project->getMap(mapName); + if (map) { + auto dialog = new NewMapDialog(this->editor->project, map, this); + dialog->open(); + } else { + RecentErrorMessage::show(QString("Unable to duplicate '%1'.").arg(mapName), this); + } +} + +NewLayoutDialog* MainWindow::createNewLayoutDialog(const Layout *layoutToCopy) { + auto dialog = new NewLayoutDialog(this->editor->project, layoutToCopy, this); + connect(dialog, &NewLayoutDialog::applied, this, &MainWindow::userSetLayout); + return dialog; +} + +void MainWindow::openNewLayoutDialog() { + auto dialog = createNewLayoutDialog(); + dialog->open(); +} + +void MainWindow::openDuplicateLayoutDialog(const QString &layoutId) { + auto layout = this->editor->project->loadLayout(layoutId); + if (layout) { + auto dialog = createNewLayoutDialog(layout); + dialog->open(); + } else { + RecentErrorMessage::show(QString("Unable to duplicate '%1'.").arg(layoutId), this); + } } void MainWindow::on_actionNew_Tileset_triggered() { - NewTilesetDialog *createTilesetDialog = new NewTilesetDialog(editor->project, this); - if(createTilesetDialog->exec() == QDialog::Accepted){ - if(createTilesetDialog->friendlyName.isEmpty()) { - logError(QString("Tried to create a directory with an empty name.")); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The given name was empty."); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - QString fullDirectoryPath = editor->project->root + "/" + createTilesetDialog->path; - QDir directory; - if(directory.exists(fullDirectoryPath)) { - logError(QString("Could not create tileset \"%1\", the folder \"%2\" already exists.").arg(createTilesetDialog->friendlyName, fullDirectoryPath)); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The folder for tileset \"%1\" already exists. View porymap.log for specific errors.").arg(createTilesetDialog->friendlyName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - if (editor->project->tilesetLabelsOrdered.contains(createTilesetDialog->fullSymbolName)) { - logError(QString("Could not create tileset \"%1\", the symbol \"%2\" already exists.").arg(createTilesetDialog->friendlyName, createTilesetDialog->fullSymbolName)); - QMessageBox msgBox(this); - msgBox.setText("Failed to add new tileset."); - QString message = QString("The symbol for tileset \"%1\" (\"%2\") already exists.").arg(createTilesetDialog->friendlyName, createTilesetDialog->fullSymbolName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); - return; - } - directory.mkdir(fullDirectoryPath); - directory.mkdir(fullDirectoryPath + "/palettes"); - Tileset newSet; - newSet.name = createTilesetDialog->fullSymbolName; - newSet.tilesImagePath = fullDirectoryPath + "/tiles.png"; - newSet.metatiles_path = fullDirectoryPath + "/metatiles.bin"; - newSet.metatile_attrs_path = fullDirectoryPath + "/metatile_attributes.bin"; - newSet.is_secondary = createTilesetDialog->isSecondary; - int numMetatiles = createTilesetDialog->isSecondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); - QImage tilesImage(":/images/blank_tileset.png"); - editor->project->loadTilesetTiles(&newSet, tilesImage); - int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - for(int i = 0; i < numMetatiles; ++i) { - Metatile *mt = new Metatile(); - for(int j = 0; j < tilesPerMetatile; ++j){ - Tile tile = Tile(); - if (createTilesetDialog->checkerboardFill) { - // Create a checkerboard-style dummy tileset - if (((i / 8) % 2) == 0) - tile.tileId = ((i % 2) == 0) ? 1 : 2; - else - tile.tileId = ((i % 2) == 1) ? 1 : 2; - } - mt->tiles.append(tile); - } - newSet.addMetatile(mt); - } - for(int i = 0; i < 16; ++i) { - QList currentPal; - for(int i = 0; i < 16;++i) { - currentPal.append(qRgb(0,0,0)); - } - newSet.palettes.append(currentPal); - newSet.palettePreviews.append(currentPal); - QString fileName = QString("%1.pal").arg(i, 2, 10, QLatin1Char('0')); - newSet.palettePaths.append(fullDirectoryPath+"/palettes/" + fileName); - } - newSet.palettes[0][1] = qRgb(255,0,255); - newSet.palettePreviews[0][1] = qRgb(255,0,255); - exportIndexed4BPPPng(newSet.tilesImage, newSet.tilesImagePath); - editor->project->saveTilesetMetatiles(&newSet); - editor->project->saveTilesetMetatileAttributes(&newSet); - editor->project->saveTilesetPalettes(&newSet); - - //append to tileset specific files - newSet.appendToHeaders(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - newSet.appendToGraphics(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - newSet.appendToMetatiles(editor->project->root, createTilesetDialog->friendlyName, editor->project->usingAsmTilesets); - - if (!createTilesetDialog->isSecondary) { - int index = insertTilesetLabel(&editor->project->primaryTilesetLabels, createTilesetDialog->fullSymbolName); - this->ui->comboBox_PrimaryTileset->insertItem(index, createTilesetDialog->fullSymbolName); - } else { - int index = insertTilesetLabel(&editor->project->secondaryTilesetLabels, createTilesetDialog->fullSymbolName); - this->ui->comboBox_SecondaryTileset->insertItem(index, createTilesetDialog->fullSymbolName); - } - insertTilesetLabel(&editor->project->tilesetLabelsOrdered, createTilesetDialog->fullSymbolName); - - QMessageBox msgBox(this); - msgBox.setText("Successfully created tileset."); - QString message = QString("Tileset \"%1\" was created successfully.").arg(createTilesetDialog->friendlyName); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Information); - msgBox.exec(); - } + auto dialog = new NewTilesetDialog(editor->project, this); + connect(dialog, &NewTilesetDialog::applied, [this](Tileset *tileset) { + // Unlike creating a new map or layout (which immediately opens the new item) + // creating a new tileset has no visual feedback that it succeeded, so we show a message. + // It's important that we do this after the dialog has closed (sheet modal dialogs on macOS don't seem to play nice together). + InfoMessage::show(QString("New tileset created at '%1'!").arg(tileset->getExpectedDir()), this); + }); + dialog->open(); } void MainWindow::updateTilesetEditor() { @@ -1846,9 +1480,10 @@ void MainWindow::openMapListItem(const QModelIndex &index) { if (!index.isValid()) return; - QVariant data = index.data(Qt::UserRole); + QVariant data = index.data(MapListUserRoles::NameRole); if (data.isNull()) return; + const QString name = data.toString(); // Normally when a new map/layout is opened the search filters are cleared and the lists will scroll to display that map/layout in the list. // We don't want to do this when the user interacts with a list directly, so we temporarily prevent changes to the search filter. @@ -1857,49 +1492,51 @@ void MainWindow::openMapListItem(const QModelIndex &index) { QString type = index.data(MapListUserRoles::TypeRole).toString(); if (type == "map_name") { - userSetMap(data.toString()); + userSetMap(name); } else if (type == "map_layout") { - userSetLayout(data.toString()); + userSetLayout(name); } if (toolbar) toolbar->setFilterLocked(false); } void MainWindow::updateMapList() { + // Get the name of the open map/layout (or clear the relevant selection if there is none). + QString activeItemName; if (this->editor->map) { - this->mapGroupModel->setMap(this->editor->map->name); - this->groupListProxyModel->layoutChanged(); - this->mapAreaModel->setMap(this->editor->map->name); - this->areaListProxyModel->layoutChanged(); + activeItemName = this->editor->map->name(); } else { - this->mapGroupModel->setMap(QString()); - this->groupListProxyModel->layoutChanged(); - this->ui->mapList->clearSelection(); - this->mapAreaModel->setMap(QString()); - this->areaListProxyModel->layoutChanged(); - this->ui->areaList->clearSelection(); + ui->mapList->clearSelection(); + ui->locationList->clearSelection(); + + if (this->editor->layout) { + activeItemName = this->editor->layout->id; + } else { + ui->layoutList->clearSelection(); + } } - if (this->editor->layout) { - this->layoutTreeModel->setLayout(this->editor->layout->id); - this->layoutListProxyModel->layoutChanged(); - } else { - this->layoutTreeModel->setLayout(QString()); - this->layoutListProxyModel->layoutChanged(); - this->ui->layoutList->clearSelection(); - } + this->mapGroupModel->setActiveItem(activeItemName); + this->mapLocationModel->setActiveItem(activeItemName); + this->layoutTreeModel->setActiveItem(activeItemName); + + this->groupListProxyModel->layoutChanged(); + this->locationListProxyModel->layoutChanged(); + this->layoutListProxyModel->layoutChanged(); } void MainWindow::on_action_Save_Project_triggered() { editor->saveProject(); updateWindowTitle(); updateMapList(); + saveGlobalConfigs(); } void MainWindow::on_action_Save_triggered() { editor->save(); updateWindowTitle(); updateMapList(); + saveGlobalConfigs(); } void MainWindow::duplicate() { @@ -2013,12 +1650,17 @@ void MainWindow::setClipboardData(OrderedJson::object object) { QClipboard *clipboard = QGuiApplication::clipboard(); QString newText; int indent = 0; - object["application"] = "porymap"; + object["application"] = QApplication::applicationName(); OrderedJson data(object); data.dump(newText, &indent); clipboard->setText(newText); } +void MainWindow::setClipboardData(const QString &text) { + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(text); +} + void MainWindow::setClipboardData(QImage image) { QClipboard *clipboard = QGuiApplication::clipboard(); clipboard->setImage(image); @@ -2047,7 +1689,7 @@ void MainWindow::paste() { QJsonObject pasteObject = pasteJsonDoc.object(); //OrderedJson::object pasteObject = pasteJson.object_items(); - if (pasteObject["application"].toString() != "porymap") { + if (pasteObject["application"].toString() != QApplication::applicationName()) { return; } @@ -2112,7 +1754,7 @@ void MainWindow::paste() { } if (!newEvents.empty()) { - editor->map->editHistory.push(new EventPaste(this->editor, editor->map, newEvents)); + editor->map->commit(new EventPaste(this->editor, editor->map, newEvents)); updateObjects(); } @@ -2173,6 +1815,7 @@ void MainWindow::on_mainTabBar_tabBarClicked(int index) clickToolButtonFromEditAction(editor->objectEditAction); } else if (index == MainTab::Connections) { editor->setEditingConnections(); + ui->graphicsView_Connections->setFocus(); // Avoid opening tab with focus on something editable } else if (index == MainTab::WildPokemon) { editor->setEditingEncounters(); } @@ -2318,8 +1961,7 @@ void MainWindow::addNewEvent(Event::Type type) { updateObjects(); editor->selectMapEvent(object); } else { - QMessageBox msgBox(this); - msgBox.setText("Failed to add new event"); + WarningMessage msgBox(QStringLiteral("Failed to add new event."), this); if (Event::typeToGroup(type) == Event::Group::Object) { msgBox.setInformativeText(QString("The limit for object events (%1) has been reached.\n\n" "This limit can be adjusted with %2 in '%3'.") @@ -2327,8 +1969,6 @@ void MainWindow::addNewEvent(Event::Type type) { .arg(projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count)) .arg(projectConfig.getFilePath(ProjectFilePath::constants_global))); } - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Warning); msgBox.exec(); } } @@ -2336,7 +1976,7 @@ void MainWindow::addNewEvent(Event::Type type) { void MainWindow::tryAddEventTab(QWidget * tab) { auto group = getEventGroupFromTabWidget(tab); - if (editor->map->events.value(group).length()) + if (editor->map->getNumEvents(group)) ui->tabWidget_EventType->addTab(tab, QString("%1s").arg(Event::eventGroupToString(group))); } @@ -2370,7 +2010,7 @@ void MainWindow::updateSelectedObjects() { else { QList all_events; if (editor->map) { - all_events = editor->map->getAllEvents(); + all_events = editor->map->getEvents(); } if (all_events.length()) { DraggablePixmapItem *selectedEvent = all_events.first()->getPixmapItem(); @@ -2404,7 +2044,7 @@ void MainWindow::updateSelectedObjects() { QSignalBlocker b(this->ui->spinner_ObjectID); this->ui->spinner_ObjectID->setMinimum(event_offs); - this->ui->spinner_ObjectID->setMaximum(current->getMap()->events.value(eventGroup).length() + event_offs - 1); + this->ui->spinner_ObjectID->setMaximum(current->getMap()->getNumEvents(eventGroup) + event_offs - 1); this->ui->spinner_ObjectID->setValue(current->getEventIndex() + event_offs); break; } @@ -2415,7 +2055,7 @@ void MainWindow::updateSelectedObjects() { QSignalBlocker b(this->ui->spinner_WarpID); this->ui->spinner_WarpID->setMinimum(event_offs); - this->ui->spinner_WarpID->setMaximum(current->getMap()->events.value(eventGroup).length() + event_offs - 1); + this->ui->spinner_WarpID->setMaximum(current->getMap()->getNumEvents(eventGroup) + event_offs - 1); this->ui->spinner_WarpID->setValue(current->getEventIndex() + event_offs); break; } @@ -2426,7 +2066,7 @@ void MainWindow::updateSelectedObjects() { QSignalBlocker b(this->ui->spinner_TriggerID); this->ui->spinner_TriggerID->setMinimum(event_offs); - this->ui->spinner_TriggerID->setMaximum(current->getMap()->events.value(eventGroup).length() + event_offs - 1); + this->ui->spinner_TriggerID->setMaximum(current->getMap()->getNumEvents(eventGroup) + event_offs - 1); this->ui->spinner_TriggerID->setValue(current->getEventIndex() + event_offs); break; } @@ -2437,7 +2077,7 @@ void MainWindow::updateSelectedObjects() { QSignalBlocker b(this->ui->spinner_BgID); this->ui->spinner_BgID->setMinimum(event_offs); - this->ui->spinner_BgID->setMaximum(current->getMap()->events.value(eventGroup).length() + event_offs - 1); + this->ui->spinner_BgID->setMaximum(current->getMap()->getNumEvents(eventGroup) + event_offs - 1); this->ui->spinner_BgID->setValue(current->getEventIndex() + event_offs); break; } @@ -2448,7 +2088,7 @@ void MainWindow::updateSelectedObjects() { QSignalBlocker b(this->ui->spinner_HealID); this->ui->spinner_HealID->setMinimum(event_offs); - this->ui->spinner_HealID->setMaximum(current->getMap()->events.value(eventGroup).length() + event_offs - 1); + this->ui->spinner_HealID->setMaximum(current->getMap()->getNumEvents(eventGroup) + event_offs - 1); this->ui->spinner_HealID->setValue(current->getEventIndex() + event_offs); break; } @@ -2541,8 +2181,8 @@ void MainWindow::eventTabChanged(int index) { } if (!isProgrammaticEventTabChange) { - if (!selectedEvent && editor->map->events.value(group).count()) { - Event *event = editor->map->events.value(group).at(0); + if (!selectedEvent && editor->map->getNumEvents(group)) { + Event *event = editor->map->getEvent(group, 0); for (QGraphicsItem *child : editor->events_group->childItems()) { DraggablePixmapItem *item = static_cast(child); if (item->event == event) { @@ -2828,14 +2468,7 @@ void MainWindow::on_action_Export_Map_Image_triggered() { void MainWindow::on_actionExport_Stitched_Map_Image_triggered() { if (!this->editor->map) { - QMessageBox warning(this); - warning.setText("Notice"); - warning.setInformativeText("Map stitch images are not possible without a map selected."); - warning.setStandardButtons(QMessageBox::Ok); - warning.setDefaultButton(QMessageBox::Cancel); - warning.setIcon(QMessageBox::Warning); - - warning.exec(); + WarningMessage::show(QStringLiteral("Map stitch images are not possible without a map selected."), this); return; } showExportMapImageWindow(ImageExporterMode::Stitch); @@ -2845,33 +2478,23 @@ void MainWindow::on_actionExport_Map_Timelapse_Image_triggered() { showExportMapImageWindow(ImageExporterMode::Timelapse); } -void MainWindow::on_actionImport_Map_from_Advance_Map_1_92_triggered(){ - importMapFromAdvanceMap1_92(); -} - -void MainWindow::importMapFromAdvanceMap1_92() -{ +void MainWindow::on_actionImport_Map_from_Advance_Map_1_92_triggered() { QString filepath = FileDialog::getOpenFileName(this, "Import Map from Advance Map 1.92", "", "Advance Map 1.92 Map Files (*.map)"); if (filepath.isEmpty()) { return; } - MapParser parser; bool error = false; - Layout *mapLayout = parser.parse(filepath, &error, editor->project); + Layout *mapLayout = AdvanceMapParser::parseLayout(filepath, &error, editor->project); if (error) { - QMessageBox msgBox(this); - msgBox.setText("Failed to import map from Advance Map 1.92 .map file."); - QString message = QString("The .map file could not be processed. View porymap.log for specific errors."); - msgBox.setInformativeText(message); - msgBox.setDefaultButton(QMessageBox::Ok); - msgBox.setIcon(QMessageBox::Icon::Critical); - msgBox.exec(); + RecentErrorMessage::show(QStringLiteral("Failed to import map from Advance Map 1.92 .map file."), this); + delete mapLayout; return; } - openNewMapPopupWindow(); - this->newMapPrompt->init(mapLayout); + auto dialog = createNewLayoutDialog(mapLayout); + connect(dialog, &NewLayoutDialog::finished, [mapLayout] { mapLayout->deleteLater(); }); + dialog->open(); } void MainWindow::showExportMapImageWindow(ImageExporterMode mode) { @@ -2893,7 +2516,7 @@ void MainWindow::on_pushButton_AddConnection_clicked() { auto dialog = new NewMapConnectionDialog(this, this->editor->map, this->editor->project->mapNames); connect(dialog, &NewMapConnectionDialog::accepted, this->editor, &Editor::addConnection); - dialog->exec(); + dialog->open(); } void MainWindow::on_pushButton_NewWildMonGroup_clicked() { @@ -2919,15 +2542,11 @@ void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { } void MainWindow::on_button_OpenDiveMap_clicked() { - const QString mapName = ui->comboBox_DiveMap->currentText(); - if (editor->project->mapNames.contains(mapName)) - userSetMap(mapName); + userSetMap(ui->comboBox_DiveMap->currentText()); } void MainWindow::on_button_OpenEmergeMap_clicked() { - const QString mapName = ui->comboBox_EmergeMap->currentText(); - if (editor->project->mapNames.contains(mapName)) - userSetMap(mapName); + userSetMap(ui->comboBox_EmergeMap->currentText()); } void MainWindow::on_comboBox_DiveMap_currentTextChanged(const QString &mapName) { @@ -3090,9 +2709,9 @@ void MainWindow::initTilesetEditor() { MapListToolBar* MainWindow::getCurrentMapListToolBar() { switch (ui->mapListContainer->currentIndex()) { - case MapListTab::Groups: return ui->mapListToolBar_Groups; - case MapListTab::Areas: return ui->mapListToolBar_Areas; - case MapListTab::Layouts: return ui->mapListToolBar_Layouts; + case MapListTab::Groups: return ui->mapListToolBar_Groups; + case MapListTab::Locations: return ui->mapListToolBar_Locations; + case MapListTab::Layouts: return ui->mapListToolBar_Layouts; default: return nullptr; } } @@ -3108,7 +2727,7 @@ MapTree* MainWindow::getCurrentMapList() { // When the search filter is cleared the map lists will (if possible) display the currently-selected map/layout. void MainWindow::resetMapListFilters() { ui->mapListToolBar_Groups->clearFilter(); - ui->mapListToolBar_Areas->clearFilter(); + ui->mapListToolBar_Locations->clearFilter(); ui->mapListToolBar_Layouts->clearFilter(); } @@ -3183,8 +2802,7 @@ void MainWindow::on_actionProject_Settings_triggered() { } void MainWindow::onWarpBehaviorWarningClicked() { - static const QString text = QString("Warp Events only function as exits on certain metatiles"); - static const QString informative = QString( + static const QString informative = QStringLiteral( "

" "For instance, most floor metatiles in a cave have the metatile behavior MB_CAVE, but the floor space in front of an exit " "will have MB_SOUTH_ARROW_WARP, which is treated specially in your project's code to allow a Warp Event to warp the player. " @@ -3198,13 +2816,13 @@ void MainWindow::onWarpBehaviorWarningClicked() { "You can disable this warning or edit the list of behaviors that silence this warning under Options -> Project Settings..." "

" ); - QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); - QPushButton *settings = msgBox.addButton("Open Settings...", QMessageBox::ActionRole); - msgBox.setDefaultButton(QMessageBox::Close); + + InfoMessage msgBox(QStringLiteral("Warp Events only function as exits on certain metatiles"), this); + auto settingsButton = msgBox.addButton("Open Settings...", QMessageBox::ActionRole); msgBox.setTextFormat(Qt::RichText); msgBox.setInformativeText(informative); msgBox.exec(); - if (msgBox.clickedButton() == settings) + if (msgBox.clickedButton() == settingsButton) this->openProjectSettingsEditor(ProjectSettingsEditor::eventsTab); } @@ -3227,7 +2845,7 @@ void MainWindow::reloadScriptEngine() { // Lying to the scripts here, simulating a project reload Scripting::cb_ProjectOpened(projectConfig.projectDir); if (editor && editor->map) - Scripting::cb_MapOpened(editor->map->name); // TODO: API should have equivalent for layout + Scripting::cb_MapOpened(editor->map->name()); // TODO: API should have equivalent for layout } void MainWindow::on_pushButton_AddCustomHeaderField_clicked() @@ -3338,12 +2956,7 @@ bool MainWindow::initRegionMapEditor(bool silent) { } bool MainWindow::askToFixRegionMapEditor() { - QMessageBox msgBox; - msgBox.setIcon(QMessageBox::Critical); - msgBox.setText(QString("There was an error opening the region map data. Please see %1 for full error details.").arg(getLogPath())); - msgBox.setDetailedText(getMostRecentError()); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); + RecentErrorMessage msgBox(QStringLiteral("There was an error opening the region map data."), this); auto reconfigButton = msgBox.addButton("Reconfigure", QMessageBox::ActionRole); msgBox.exec(); if (msgBox.clickedButton() == reconfigButton) { @@ -3383,10 +2996,6 @@ bool MainWindow::closeSupplementaryWindows() { return false; this->mapImageExporter = nullptr; - if (this->newMapPrompt && !this->newMapPrompt->close()) - return false; - this->newMapPrompt = nullptr; - if (this->shortcutsEditor && !this->shortcutsEditor->close()) return false; this->shortcutsEditor = nullptr; @@ -3417,15 +3026,16 @@ bool MainWindow::closeProject() { return true; if (this->editor->project->hasUnsavedChanges()) { - QMessageBox::StandardButton result = QMessageBox::question( - this, "porymap", "The project has been modified, save changes?", - QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); + QuestionMessage msgBox(QStringLiteral("The project has been modified, save changes?"), this); + msgBox.addButton(QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Yes); - if (result == QMessageBox::Yes) { + auto reply = msgBox.exec(); + if (reply == QMessageBox::Yes) { editor->saveProject(); - } else if (result == QMessageBox::No) { + } else if (reply == QMessageBox::No) { logWarn("Closing project with unsaved changes."); - } else if (result == QMessageBox::Cancel) { + } else if (reply == QMessageBox::Cancel) { return false; } } @@ -3437,24 +3047,9 @@ bool MainWindow::closeProject() { return true; } -void MainWindow::saveGlobalConfigs() { - porymapConfig.setMainGeometry( - this->saveGeometry(), - this->saveState(), - this->ui->splitter_map->saveState(), - this->ui->splitter_main->saveState(), - this->ui->splitter_Metatiles->saveState() - ); - porymapConfig.save(); - shortcutsConfig.save(); -} - void MainWindow::on_action_Exit_triggered() { if (!closeProject()) return; - - saveGlobalConfigs(); - QApplication::quit(); } @@ -3463,8 +3058,5 @@ void MainWindow::closeEvent(QCloseEvent *event) { event->ignore(); return; } - - saveGlobalConfigs(); - QMainWindow::closeEvent(event); } diff --git a/src/project.cpp b/src/project.cpp index 1791bcdf..3abfcfbf 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -8,7 +8,7 @@ #include "tileset.h" #include "map.h" #include "filedialog.h" - +#include "validator.h" #include "orderedjson.h" #include @@ -32,7 +32,7 @@ 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_size = 20; +int Project::default_map_dimension = 20; int Project::max_object_events = 64; Project::Project(QObject *parent) : @@ -106,7 +106,15 @@ bool Project::load() { && readEventGraphics() && readSongNames() && readMapGroups(); - applyParsedLimits(); + + if (success) { + // No need to do this if something failed to load. + // (and in fact we shouldn't, because they contain + // assumptions that some things have loaded correctly). + initNewLayoutSettings(); + initNewMapSettings(); + applyParsedLimits(); + } return success; } @@ -119,35 +127,29 @@ QString Project::getProjectTitle() { } void Project::clearMapCache() { - for (auto *map : mapCache.values()) { - if (map) - delete map; - } - mapCache.clear(); + qDeleteAll(this->mapCache); + this->mapCache.clear(); } void Project::clearTilesetCache() { - for (auto *tileset : tilesetCache.values()) { - if (tileset) - delete tileset; - } - tilesetCache.clear(); + qDeleteAll(this->tilesetCache); + this->tilesetCache.clear(); } -Map* Project::loadMap(QString map_name) { - if (map_name == DYNAMIC_MAP_NAME) +Map* Project::loadMap(QString mapName) { + if (mapName == getDynamicMapName()) return nullptr; Map *map; - if (mapCache.contains(map_name)) { - map = mapCache.value(map_name); + if (mapCache.contains(mapName)) { + map = mapCache.value(mapName); // TODO: uncomment when undo/redo history is fully implemented for all actions. if (true/*map->hasUnsavedChanges()*/) { return map; } } else { map = new Map; - map->setName(map_name); + map->setName(mapName); } if (!(loadMapData(map) && loadMapLayout(map))){ @@ -155,81 +157,93 @@ Map* Project::loadMap(QString map_name) { return nullptr; } - mapCache.insert(map_name, map); + // If the map's MAPSEC value in the header changes, update our global array to keep it in sync. + connect(map->header(), &MapHeader::locationChanged, [this, map] { this->mapNameToMapSectionName.insert(map->name(), map->header()->location()); }); + + mapCache.insert(mapName, map); emit mapLoaded(map); return map; } -const QSet defaultTopLevelMapFields = { - "id", - "name", - "layout", - "music", - "region_map_section", - "requires_flash", - "weather", - "map_type", - "show_map_name", - "battle_scene", - "connections", - "object_events", - "warp_events", - "coord_events", - "bg_events", - "shared_events_map", - "shared_scripts_map", -}; - -QSet Project::getTopLevelMapFields() { - QSet topLevelMapFields = defaultTopLevelMapFields; +void Project::initTopLevelMapFields() { + static const QSet defaultTopLevelMapFields = { + "id", + "name", + "layout", + "music", + "region_map_section", + "requires_flash", + "weather", + "map_type", + "show_map_name", + "battle_scene", + "connections", + "object_events", + "warp_events", + "coord_events", + "bg_events", + "heal_locations", + "shared_events_map", + "shared_scripts_map", + }; + this->topLevelMapFields = defaultTopLevelMapFields; if (projectConfig.mapAllowFlagsEnabled) { - topLevelMapFields.insert("allow_cycling"); - topLevelMapFields.insert("allow_escaping"); - topLevelMapFields.insert("allow_running"); + this->topLevelMapFields.insert("allow_cycling"); + this->topLevelMapFields.insert("allow_escaping"); + this->topLevelMapFields.insert("allow_running"); } - if (projectConfig.floorNumberEnabled) { - topLevelMapFields.insert("floor_number"); + this->topLevelMapFields.insert("floor_number"); } - return topLevelMapFields; } -bool Project::loadMapData(Map* map) { - if (!map->isPersistedToFile) { - return true; - } - - QString mapFilepath = QString("%1/%3%2/map.json").arg(root).arg(map->name).arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)); - QJsonDocument mapDoc; - if (!parser.tryParseJsonFile(&mapDoc, mapFilepath)) { +bool Project::readMapJson(const QString &mapName, QJsonDocument * out) { + const QString mapFilepath = QString("%1%2/map.json").arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)).arg(mapName); + if (!parser.tryParseJsonFile(out, QString("%1/%2").arg(this->root).arg(mapFilepath))) { logError(QString("Failed to read map data from %1").arg(mapFilepath)); return false; } + return true; +} + +bool Project::loadMapData(Map* map) { + if (!map->isPersistedToFile()) { + return true; + } + + QJsonDocument mapDoc; + if (!readMapJson(map->name(), &mapDoc)) + return false; QJsonObject mapObj = mapDoc.object(); - map->song = ParseUtil::jsonToQString(mapObj["music"]); - map->layoutId = ParseUtil::jsonToQString(mapObj["layout"]); - map->location = ParseUtil::jsonToQString(mapObj["region_map_section"]); - map->requiresFlash = ParseUtil::jsonToBool(mapObj["requires_flash"]); - map->weather = ParseUtil::jsonToQString(mapObj["weather"]); - map->type = ParseUtil::jsonToQString(mapObj["map_type"]); - map->show_location = ParseUtil::jsonToBool(mapObj["show_map_name"]); - map->battle_scene = ParseUtil::jsonToQString(mapObj["battle_scene"]); + // We should already know the map constant ID from the initial project launch, but we'll ensure it's correct here anyway. + map->setConstantName(ParseUtil::jsonToQString(mapObj["id"])); + this->mapNamesToMapConstants.insert(map->name(), map->constantName()); + this->mapConstantsToMapNames.insert(map->constantName(), map->name()); + + map->header()->setSong(ParseUtil::jsonToQString(mapObj["music"])); + map->setLayoutId(ParseUtil::jsonToQString(mapObj["layout"])); + map->header()->setLocation(ParseUtil::jsonToQString(mapObj["region_map_section"])); + map->header()->setRequiresFlash(ParseUtil::jsonToBool(mapObj["requires_flash"])); + map->header()->setWeather(ParseUtil::jsonToQString(mapObj["weather"])); + map->header()->setType(ParseUtil::jsonToQString(mapObj["map_type"])); + map->header()->setShowsLocationName(ParseUtil::jsonToBool(mapObj["show_map_name"])); + map->header()->setBattleScene(ParseUtil::jsonToQString(mapObj["battle_scene"])); if (projectConfig.mapAllowFlagsEnabled) { - map->allowBiking = ParseUtil::jsonToBool(mapObj["allow_cycling"]); - map->allowEscaping = ParseUtil::jsonToBool(mapObj["allow_escaping"]); - map->allowRunning = ParseUtil::jsonToBool(mapObj["allow_running"]); + map->header()->setAllowsBiking(ParseUtil::jsonToBool(mapObj["allow_cycling"])); + map->header()->setAllowsEscaping(ParseUtil::jsonToBool(mapObj["allow_escaping"])); + map->header()->setAllowsRunning(ParseUtil::jsonToBool(mapObj["allow_running"])); } if (projectConfig.floorNumberEnabled) { - map->floorNumber = ParseUtil::jsonToInt(mapObj["floor_number"]); + map->header()->setFloorNumber(ParseUtil::jsonToInt(mapObj["floor_number"])); } - map->sharedEventsMap = ParseUtil::jsonToQString(mapObj["shared_events_map"]); - map->sharedScriptsMap = ParseUtil::jsonToQString(mapObj["shared_scripts_map"]); + map->setSharedEventsMap(ParseUtil::jsonToQString(mapObj["shared_events_map"])); + map->setSharedScriptsMap(ParseUtil::jsonToQString(mapObj["shared_scripts_map"])); // Events - map->events[Event::Group::Object].clear(); + map->resetEvents(); QJsonArray objectEventsArr = mapObj["object_events"].toArray(); for (int i = 0; i < objectEventsArr.size(); i++) { QJsonObject event = objectEventsArr[i].toObject(); @@ -248,11 +262,10 @@ bool Project::loadMapData(Map* map) { delete clone; } } else { - logError(QString("Map %1 object_event %2 has invalid type '%3'. Must be 'object' or 'clone'.").arg(map->name).arg(i).arg(type)); + logError(QString("Map %1 object_event %2 has invalid type '%3'. Must be 'object' or 'clone'.").arg(map->name()).arg(i).arg(type)); } } - map->events[Event::Group::Warp].clear(); QJsonArray warpEventsArr = mapObj["warp_events"].toArray(); for (int i = 0; i < warpEventsArr.size(); i++) { QJsonObject event = warpEventsArr[i].toObject(); @@ -265,7 +278,6 @@ bool Project::loadMapData(Map* map) { } } - map->events[Event::Group::Coord].clear(); QJsonArray coordEventsArr = mapObj["coord_events"].toArray(); for (int i = 0; i < coordEventsArr.size(); i++) { QJsonObject event = coordEventsArr[i].toObject(); @@ -279,11 +291,10 @@ bool Project::loadMapData(Map* map) { coord->loadFromJson(event, this); map->addEvent(coord); } else { - logError(QString("Map %1 coord_event %2 has invalid type '%3'. Must be 'trigger' or 'weather'.").arg(map->name).arg(i).arg(type)); + logError(QString("Map %1 coord_event %2 has invalid type '%3'. Must be 'trigger' or 'weather'.").arg(map->name()).arg(i).arg(type)); } } - map->events[Event::Group::Bg].clear(); QJsonArray bgEventsArr = mapObj["bg_events"].toArray(); for (int i = 0; i < bgEventsArr.size(); i++) { QJsonObject event = bgEventsArr[i].toObject(); @@ -301,12 +312,12 @@ bool Project::loadMapData(Map* map) { bg->loadFromJson(event, this); map->addEvent(bg); } else { - logError(QString("Map %1 bg_event %2 has invalid type '%3'. Must be 'sign', 'hidden_item', or 'secret_base'.").arg(map->name).arg(i).arg(type)); + logError(QString("Map %1 bg_event %2 has invalid type '%3'. Must be 'sign', 'hidden_item', or 'secret_base'.").arg(map->name()).arg(i).arg(type)); } } - map->events[Event::Group::Heal].clear(); - + +/* TODO: Re-enable const QString mapPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); for (auto it = healLocations.begin(); it != healLocations.end(); it++) { HealLocation loc = *it; @@ -328,6 +339,7 @@ bool Project::loadMapData(Map* map) { map->ownedEvents.append(heal); } } +*/ map->deleteConnections(); QJsonArray connectionsArr = mapObj["connections"].toArray(); @@ -335,110 +347,130 @@ bool Project::loadMapData(Map* map) { for (int i = 0; i < connectionsArr.size(); i++) { QJsonObject connectionObj = connectionsArr[i].toObject(); const QString direction = ParseUtil::jsonToQString(connectionObj["direction"]); - int offset = ParseUtil::jsonToInt(connectionObj["offset"]); + const int offset = ParseUtil::jsonToInt(connectionObj["offset"]); const QString mapConstant = ParseUtil::jsonToQString(connectionObj["map"]); - if (mapConstantsToMapNames.contains(mapConstant)) { - // Successully read map connection - map->loadConnection(new MapConnection(mapConstantsToMapNames.value(mapConstant), direction, offset)); - } else { - logError(QString("Failed to find connected map for map constant '%1'").arg(mapConstant)); - } + map->loadConnection(new MapConnection(this->mapConstantsToMapNames.value(mapConstant, mapConstant), direction, offset)); } } // Check for custom fields - QSet baseFields = this->getTopLevelMapFields(); +/* // TODO: Re-enable for (QString key : mapObj.keys()) { - if (!baseFields.contains(key)) { + if (!this->topLevelMapFields.contains(key)) { map->customHeaders.insert(key, mapObj[key]); } } +*/ return true; } -QString Project::readMapLayoutId(QString map_name) { - if (mapCache.contains(map_name)) { - return mapCache.value(map_name)->layoutId; +Map *Project::createNewMap(const Project::NewMapSettings &settings, const Map* toDuplicate) { + Map *map = toDuplicate ? new Map(*toDuplicate) : new Map; + map->setName(settings.name); + map->setHeader(settings.header); + map->setNeedsHealLocation(settings.canFlyTo); + + // Generate a unique MAP constant. + map->setConstantName(toUniqueIdentifier(Map::mapConstantFromName(map->name()))); + + Layout *layout = this->mapLayouts.value(settings.layout.id); + if (!layout) { + // Layout doesn't already exist, create it. + layout = createNewLayout(settings.layout, toDuplicate ? toDuplicate->layout() : nullptr); + if (!layout) { + // Layout creation failed. + delete map; + return nullptr; + } + } else { + // This layout already exists. Make sure it's loaded. + loadLayout(layout); + } + map->setLayout(layout); + + // Make sure we keep the order of the map names the same as in the map group order. + int mapNamePos; + if (this->groupNames.contains(settings.group)) { + mapNamePos = 0; + for (const auto &name : this->groupNames) { + mapNamePos += this->groupNameToMapNames[name].length(); + if (name == settings.group) + break; + } + } else { + // Adding map to a map group that doesn't exist yet. + // Create the group, and we already know the map will be last in the list. + if (isValidNewIdentifier(settings.group)) { + addNewMapGroup(settings.group); + } + mapNamePos = this->mapNames.length(); } - QString mapFilepath = QString("%1/%3%2/map.json").arg(root).arg(map_name).arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)); - QJsonDocument mapDoc; - if (!parser.tryParseJsonFile(&mapDoc, mapFilepath)) { - logError(QString("Failed to read map layout id from %1").arg(mapFilepath)); - return QString(); + const QString location = map->header()->location(); + if (!this->mapSectionIdNames.contains(location) && isValidNewIdentifier(location)) { + // Unrecognized MAPSEC name, we can automatically add a new MAPSEC for it. + addNewMapsec(location); } - QJsonObject mapObj = mapDoc.object(); - return ParseUtil::jsonToQString(mapObj["layout"]); + this->mapNames.insert(mapNamePos, map->name()); + this->groupNameToMapNames[settings.group].append(map->name()); + this->mapConstantsToMapNames.insert(map->constantName(), map->name()); + this->mapNamesToMapConstants.insert(map->name(), map->constantName()); + this->mapNameToLayoutId.insert(map->name(), map->layoutId()); + this->mapNameToMapSectionName.insert(map->name(), map->header()->location()); + + map->setIsPersistedToFile(false); + + emit mapCreated(map, settings.group); + + return map; } -QString Project::readMapLocation(QString map_name) { - if (mapCache.contains(map_name)) { - return mapCache.value(map_name)->location; - } +Layout *Project::createNewLayout(const Layout::Settings &settings, const Layout *toDuplicate) { + if (this->layoutIds.contains(settings.id)) + return nullptr; - QString mapFilepath = QString("%1/%3%2/map.json").arg(root).arg(map_name).arg(projectConfig.getFilePath(ProjectFilePath::data_map_folders)); - QJsonDocument mapDoc; - if (!parser.tryParseJsonFile(&mapDoc, mapFilepath)) { - logError(QString("Failed to read map's region map section from %1").arg(mapFilepath)); - return QString(); - } + Layout *layout = toDuplicate ? new Layout(*toDuplicate) : new Layout(); + layout->id = settings.id; + layout->name = settings.name; + layout->width = settings.width; + layout->height = settings.height; + layout->border_width = settings.borderWidth; + layout->border_height = settings.borderHeight; + layout->tileset_primary_label = settings.primaryTilesetLabel; + layout->tileset_secondary_label = settings.secondaryTilesetLabel; - QJsonObject mapObj = mapDoc.object(); - return ParseUtil::jsonToQString(mapObj["region_map_section"]); -} + // If a special folder name was specified (as in the case when we're creating a layout for a new map) then use that name. + // Otherwise the new layout's folder name will just be the layout's name. + const QString folderName = !settings.folderName.isEmpty() ? settings.folderName : layout->name; + const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_layouts_folders) + folderName; + layout->border_path = folderPath + "/border.bin"; + layout->blockdata_path = folderPath + "/map.bin"; -Layout *Project::createNewLayout(Layout::SimpleSettings &layoutSettings) { - QString basePath = projectConfig.getFilePath(ProjectFilePath::data_layouts_folders); - Layout *layout; - - // Handle the case where we are copying from an existing layout first. - if (!layoutSettings.from_id.isEmpty()) { - // load from layout - loadLayout(mapLayouts[layoutSettings.from_id]); - - layout = mapLayouts[layoutSettings.from_id]->copy(); - layout->name = layoutSettings.name; - layout->id = layoutSettings.id; - layout->border_path = QString("%1%2/border.bin").arg(basePath, layoutSettings.name); - layout->blockdata_path = QString("%1%2/map.bin").arg(basePath, layoutSettings.name); - } - else { - layout = new Layout; - - layout->name = layoutSettings.name; - layout->id = layoutSettings.id; - layout->width = layoutSettings.width; - layout->height = layoutSettings.height; - layout->border_width = DEFAULT_BORDER_WIDTH; - layout->border_height = DEFAULT_BORDER_HEIGHT; - layout->tileset_primary_label = layoutSettings.tileset_primary_label; - layout->tileset_secondary_label = layoutSettings.tileset_secondary_label; - layout->border_path = QString("%1%2/border.bin").arg(basePath, layoutSettings.name); - layout->blockdata_path = QString("%1%2/map.bin").arg(basePath, layoutSettings.name); - - setNewLayoutBlockdata(layout); - setNewLayoutBorder(layout); - } - - // Create a new directory for the layout - QString newLayoutDir = QString(root + "/%1%2").arg(projectConfig.getFilePath(ProjectFilePath::data_layouts_folders), layout->name); - if (!QDir::root().mkdir(newLayoutDir)) { - logError(QString("Error: failed to create directory for new layout: '%1'").arg(newLayoutDir)); + // Create a new directory for the layout, if it doesn't already exist. + const QString fullPath = QString("%1/%2").arg(this->root).arg(folderPath); + if (!QDir::root().mkpath(fullPath)) { + logError(QString("Failed to create directory for new layout: '%1'").arg(fullPath)); delete layout; return nullptr; } - mapLayouts.insert(layout->id, layout); - mapLayoutsMaster.insert(layout->id, layout->copy()); - mapLayoutsTable.append(layout->id); - mapLayoutsTableMaster.append(layout->id); - layoutIdsToNames.insert(layout->id, layout->name); + this->mapLayouts.insert(layout->id, layout); + this->layoutIds.append(layout->id); - saveLayout(layout); + if (layout->blockdata.isEmpty()) { + // Fill layout using default fill settings + setNewLayoutBlockdata(layout); + } + if (layout->border.isEmpty()) { + // Fill border using default fill settings + setNewLayoutBorder(layout); + } - this->loadLayout(layout); + saveLayout(layout); // TODO: Ideally we shouldn't automatically save new layouts + + emit layoutCreated(layout); return layout; } @@ -461,50 +493,47 @@ bool Project::loadLayout(Layout *layout) { } Layout *Project::loadLayout(QString layoutId) { - if (mapLayouts.contains(layoutId)) { - Layout *layout = mapLayouts[layoutId]; - if (loadLayout(layout)) { - return layout; - } + Layout *layout = this->mapLayouts.value(layoutId); + if (!layout || !loadLayout(layout)) { + logError(QString("Failed to load layout '%1'").arg(layoutId)); + return nullptr; } - - logError(QString("Error: Failed to load layout '%1'").arg(layoutId)); - return nullptr; + return layout; } bool Project::loadMapLayout(Map* map) { - if (!map->isPersistedToFile) { + if (!map->isPersistedToFile()) { return true; } - if (mapLayouts.contains(map->layoutId)) { - map->layout = mapLayouts[map->layoutId]; - } else { - logError(QString("Error: Map '%1' has an unknown layout '%2'").arg(map->name).arg(map->layoutId)); + Layout *layout = this->mapLayouts.value(map->layoutId()); + if (!layout) { + logError(QString("Map '%1' has an unknown layout '%2'").arg(map->name()).arg(map->layoutId())); return false; } + map->setLayout(layout); if (map->hasUnsavedChanges()) { return true; } else { - return loadLayout(map->layout); + return loadLayout(map->layout()); } } void Project::clearMapLayouts() { - qDeleteAll(mapLayouts); - mapLayouts.clear(); - qDeleteAll(mapLayoutsMaster); - mapLayoutsMaster.clear(); - mapLayoutsTable.clear(); - layoutIdsToNames.clear(); + qDeleteAll(this->mapLayouts); + this->mapLayouts.clear(); + qDeleteAll(this->mapLayoutsMaster); + this->mapLayoutsMaster.clear(); + this->layoutIds.clear(); + this->layoutIdsMaster.clear(); } bool Project::readMapLayouts() { clearMapLayouts(); - QString layoutsFilepath = projectConfig.getFilePath(ProjectFilePath::json_layouts); - QString fullFilepath = QString("%1/%2").arg(root).arg(layoutsFilepath); + const QString layoutsFilepath = projectConfig.getFilePath(ProjectFilePath::json_layouts); + const QString fullFilepath = QString("%1/%2").arg(this->root).arg(layoutsFilepath); fileWatcher.addPath(fullFilepath); QJsonDocument layoutsDoc; if (!parser.tryParseJsonFile(&layoutsDoc, fullFilepath)) { @@ -519,32 +548,18 @@ bool Project::readMapLayouts() { return false; } - layoutsLabel = ParseUtil::jsonToQString(layoutsObj["layouts_table_label"]); - if (layoutsLabel.isNull()) { - layoutsLabel = "gMapLayouts"; + this->layoutsLabel = ParseUtil::jsonToQString(layoutsObj["layouts_table_label"]); + if (this->layoutsLabel.isEmpty()) { + this->layoutsLabel = "gMapLayouts"; logWarn(QString("'layouts_table_label' value is missing from %1. Defaulting to %2") .arg(layoutsFilepath) .arg(layoutsLabel)); } - static const QList requiredFields = QList{ - "id", - "name", - "width", - "height", - "primary_tileset", - "secondary_tileset", - "border_filepath", - "blockdata_filepath", - }; for (int i = 0; i < layouts.size(); i++) { QJsonObject layoutObj = layouts[i].toObject(); if (layoutObj.isEmpty()) continue; - if (!parser.ensureFieldsExist(layoutObj, requiredFields)) { - logError(QString("Layout %1 is missing field(s) in %2.").arg(i).arg(layoutsFilepath)); - return false; - } Layout *layout = new Layout(); layout->id = ParseUtil::jsonToQString(layoutObj["id"]); if (layout->id.isEmpty()) { @@ -580,15 +595,11 @@ bool Project::readMapLayouts() { if (projectConfig.useCustomBorderSize) { int bwidth = ParseUtil::jsonToInt(layoutObj["border_width"]); if (bwidth <= 0) { // 0 is an expected border width/height that should be handled, GF used it for the RS layouts in FRLG - logWarn(QString("Invalid 'border_width' value '%1' for %2 in %3. Must be greater than 0. Using default (%4) instead.") - .arg(bwidth).arg(layout->id).arg(layoutsFilepath).arg(DEFAULT_BORDER_WIDTH)); bwidth = DEFAULT_BORDER_WIDTH; } layout->border_width = bwidth; int bheight = ParseUtil::jsonToInt(layoutObj["border_height"]); if (bheight <= 0) { - logWarn(QString("Invalid 'border_height' value '%1' for %2 in %3. Must be greater than 0. Using default (%4) instead.") - .arg(bheight).arg(layout->id).arg(layoutsFilepath).arg(DEFAULT_BORDER_HEIGHT)); bheight = DEFAULT_BORDER_HEIGHT; } layout->border_height = bheight; @@ -620,11 +631,11 @@ bool Project::readMapLayouts() { delete layout; return false; } - mapLayouts.insert(layout->id, layout); - mapLayoutsMaster.insert(layout->id, layout->copy()); - mapLayoutsTable.append(layout->id); - mapLayoutsTableMaster.append(layout->id); - layoutIdsToNames.insert(layout->id, layout->name); + + this->mapLayouts.insert(layout->id, layout); + this->mapLayoutsMaster.insert(layout->id, layout->copy()); + this->layoutIds.append(layout->id); + this->layoutIdsMaster.append(layout->id); } return true; @@ -639,11 +650,11 @@ void Project::saveMapLayouts() { } OrderedJson::object layoutsObj; - layoutsObj["layouts_table_label"] = layoutsLabel; + layoutsObj["layouts_table_label"] = this->layoutsLabel; OrderedJson::array layoutsArr; - for (QString layoutId : mapLayoutsTableMaster) { - Layout *layout = mapLayoutsMaster.value(layoutId); + for (const QString &layoutId : this->layoutIdsMaster) { + Layout *layout = this->mapLayoutsMaster.value(layoutId); OrderedJson::object layoutObj; layoutObj["id"] = layout->id; layoutObj["name"] = layout->name; @@ -690,14 +701,12 @@ void Project::saveMapGroups() { } mapGroupsObj["group_order"] = groupNamesArr; - int groupNum = 0; - for (QStringList mapNames : groupedMapNames) { + for (const auto &groupName : this->groupNames) { OrderedJson::array groupArr; - for (QString mapName : mapNames) { + for (const auto &mapName : this->groupNameToMapNames.value(groupName)) { groupArr.push_back(mapName); } - mapGroupsObj[this->groupNames.at(groupNum)] = groupArr; - groupNum++; + mapGroupsObj[groupName] = groupArr; } ignoreWatchedFileTemporarily(mapGroupsFilepath); @@ -718,18 +727,16 @@ void Project::saveRegionMapSections() { const QString emptyMapsecName = getEmptyMapsecName(); OrderedJson::array mapSectionArray; - for (const auto &idName : this->mapSectionIdNames) { - // The 'empty' map section (MAPSEC_NONE) isn't normally present in the region map sections data file. - // We append this name to mapSectionIdNames ourselves if it isn't present, in which case we don't want to output data for it here. - if (!this->saveEmptyMapsec && idName == emptyMapsecName) - continue; - + for (const auto &idName : this->mapSectionIdNamesSaveOrder) { OrderedJson::object mapSectionObj; mapSectionObj["id"] = idName; + if (this->mapSectionDisplayNames.contains(idName)) { + mapSectionObj["name"] = this->mapSectionDisplayNames.value(idName); + } + if (this->regionMapEntries.contains(idName)) { MapSectionEntry entry = this->regionMapEntries.value(idName); - mapSectionObj["name"] = entry.name; mapSectionObj["x"] = entry.x; mapSectionObj["y"] = entry.y; mapSectionObj["width"] = entry.width; @@ -840,50 +847,14 @@ void Project::saveWildMonData() { wildEncountersFile.close(); } -void Project::saveMapConstantsHeader() { - QString text = QString("#ifndef GUARD_CONSTANTS_MAP_GROUPS_H\n"); - text += QString("#define GUARD_CONSTANTS_MAP_GROUPS_H\n"); - text += QString("\n//\n// DO NOT MODIFY THIS FILE! It is auto-generated from %1\n//\n\n") - .arg(projectConfig.getFilePath(ProjectFilePath::json_map_groups)); - - int groupNum = 0; - for (QStringList mapNames : groupedMapNames) { - text += "// " + groupNames.at(groupNum) + "\n"; - int maxLength = 0; - for (QString mapName : mapNames) { - QString mapConstantName = mapNamesToMapConstants.value(mapName); - if (mapConstantName.length() > maxLength) - maxLength = mapConstantName.length(); - } - int groupIndex = 0; - for (QString mapName : mapNames) { - QString mapConstantName = mapNamesToMapConstants.value(mapName); - text += QString("#define %1%2(%3 | (%4 << 8))\n") - .arg(mapConstantName) - .arg(QString(" ").repeated(maxLength - mapConstantName.length() + 1)) - .arg(groupIndex) - .arg(groupNum); - groupIndex++; - } - text += QString("\n"); - groupNum++; - } - - text += QString("#define MAP_GROUPS_COUNT %1\n\n").arg(groupNum); - text += QString("#endif // GUARD_CONSTANTS_MAP_GROUPS_H\n"); - - QString mapGroupFilepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::constants_map_groups); - ignoreWatchedFileTemporarily(mapGroupFilepath); - saveTextFile(mapGroupFilepath, text); -} - void Project::saveHealLocations(Map *map) { this->saveHealLocationsData(map); this->saveHealLocationsConstants(); } // Saves heal location maps/coords/respawn data in root + /src/data/heal_locations.h -void Project::saveHealLocationsData(Map *map) { +void Project::saveHealLocationsData(Map *) { +/* TODO: Will be re-implemented as part of changes to reading heal locations from map.json // Update heal locations from map if (map->events[Event::Group::Heal].length() > 0) { for (Event *healEvent : map->events[Event::Group::Heal]) { @@ -892,6 +863,7 @@ void Project::saveHealLocationsData(Map *map) { } } + // Find any duplicate constant names QMap healLocationsDupes; QSet healLocationsUnique; @@ -958,6 +930,7 @@ void Project::saveHealLocationsData(Map *map) { QString filepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::data_heal_locations); ignoreWatchedFileTemporarily(filepath); saveTextFile(filepath, text); + */ } // Saves heal location defines in root + /include/constants/heal_locations.h @@ -1006,25 +979,21 @@ void Project::saveHealLocationsConstants() { void Project::saveTilesets(Tileset *primaryTileset, Tileset *secondaryTileset) { saveTilesetMetatileLabels(primaryTileset, secondaryTileset); - saveTilesetMetatileAttributes(primaryTileset); - saveTilesetMetatileAttributes(secondaryTileset); - saveTilesetMetatiles(primaryTileset); - saveTilesetMetatiles(secondaryTileset); - saveTilesetTilesImage(primaryTileset); - saveTilesetTilesImage(secondaryTileset); - saveTilesetPalettes(primaryTileset); - saveTilesetPalettes(secondaryTileset); + if (primaryTileset) + primaryTileset->save(); + if (secondaryTileset) + secondaryTileset->save(); } void Project::updateTilesetMetatileLabels(Tileset *tileset) { // Erase old labels, then repopulate with new labels const QString prefix = tileset->getMetatileLabelPrefix(); - metatileLabelsMap[tileset->name].clear(); + this->metatileLabelsMap[tileset->name].clear(); for (int metatileId : tileset->metatileLabels.keys()) { if (tileset->metatileLabels[metatileId].isEmpty()) continue; QString label = prefix + tileset->metatileLabels[metatileId]; - metatileLabelsMap[tileset->name][label] = metatileId; + this->metatileLabelsMap[tileset->name][label] = metatileId; } } @@ -1083,60 +1052,6 @@ void Project::saveTilesetMetatileLabels(Tileset *primaryTileset, Tileset *second saveTextFile(root + "/" + filename, outputText); } -void Project::saveTilesetMetatileAttributes(Tileset *tileset) { - QFile attrs_file(tileset->metatile_attrs_path); - if (attrs_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - QByteArray data; - for (const auto &metatile : tileset->metatiles()) { - uint32_t attributes = metatile->getAttributes(); - for (int i = 0; i < projectConfig.metatileAttributesSize; i++) - data.append(static_cast(attributes >> (8 * i))); - } - attrs_file.write(data); - } else { - logError(QString("Could not save tileset metatile attributes file '%1'").arg(tileset->metatile_attrs_path)); - } -} - -void Project::saveTilesetMetatiles(Tileset *tileset) { - QFile metatiles_file(tileset->metatiles_path); - if (metatiles_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - QByteArray data; - int numTiles = projectConfig.getNumTilesInMetatile(); - for (const auto &metatile : tileset->metatiles()) { - for (int i = 0; i < numTiles; i++) { - uint16_t tile = metatile->tiles.at(i).rawValue(); - data.append(static_cast(tile)); - data.append(static_cast(tile >> 8)); - } - } - metatiles_file.write(data); - } else { - tileset->clearMetatiles(); - logError(QString("Could not open tileset metatiles file '%1'").arg(tileset->metatiles_path)); - } -} - -void Project::saveTilesetTilesImage(Tileset *tileset) { - // Only write the tiles image if it was changed. - // Porymap will only ever change an existing tiles image by importing a new one. - if (tileset->hasUnsavedTilesImage) { - if (!tileset->tilesImage.save(tileset->tilesImagePath, "PNG")) { - logError(QString("Failed to save tiles image '%1'").arg(tileset->tilesImagePath)); - return; - } - tileset->hasUnsavedTilesImage = false; - } -} - -void Project::saveTilesetPalettes(Tileset *tileset) { - int numPalettes = qMin(tileset->palettePaths.length(), tileset->palettes.length()); - for (int i = 0; i < numPalettes; i++) { - QString filepath = tileset->palettePaths.at(i); - PaletteUtil::writeJASC(filepath, tileset->palettes.at(i).toVector(), 0, 16); - } -} - bool Project::loadLayoutTilesets(Layout *layout) { layout->tileset_primary = getTileset(layout->tileset_primary_label); if (!layout->tileset_primary) { @@ -1164,6 +1079,9 @@ bool Project::loadLayoutTilesets(Layout *layout) { return true; } +// TODO: We are parsing the tileset headers file whenever we load a tileset for the first time. +// At a minimum this means we're parsing the file three times per session (twice here for the first map's tilesets, once on launch in Project::readTilesetLabels). +// We can cache the header data instead and only parse it once on launch. Tileset* Project::loadTileset(QString label, Tileset *tileset) { auto memberMap = Tileset::getHeaderMemberMap(this->usingAsmTilesets); if (this->usingAsmTilesets) { @@ -1183,14 +1101,14 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { tileset->metatile_attrs_label = values.value(memberMap.key("metatileAttributes")); } else { // Read C tileset header - const auto structs = parser.readCStructs(projectConfig.getFilePath(ProjectFilePath::tilesets_headers), label, memberMap); + auto structs = parser.readCStructs(projectConfig.getFilePath(ProjectFilePath::tilesets_headers), label, memberMap); if (!structs.contains(label)) { return nullptr; } if (tileset == nullptr) { tileset = new Tileset; } - const auto tilesetAttributes = structs[label]; + auto tilesetAttributes = structs[label]; tileset->name = label; tileset->is_secondary = ParseUtil::gameStringToBool(tilesetAttributes.value("isSecondary")); tileset->tiles_label = tilesetAttributes.value("tiles"); @@ -1206,9 +1124,16 @@ Tileset* Project::loadTileset(QString label, Tileset *tileset) { } bool Project::loadBlockdata(Layout *layout) { + bool ok = true; QString path = QString("%1/%2").arg(root).arg(layout->blockdata_path); - layout->blockdata = readBlockdata(path); - layout->lastCommitBlocks.blocks = layout->blockdata; + auto blockdata = readBlockdata(path, &ok); + if (!ok) { + logError(QString("Failed to load layout blockdata from '%1'").arg(path)); + return false; + } + + layout->blockdata = blockdata; + layout->lastCommitBlocks.blocks = blockdata; layout->lastCommitBlocks.layoutDimensions = QSize(layout->getWidth(), layout->getHeight()); if (layout->blockdata.count() != layout->getWidth() * layout->getHeight()) { @@ -1235,9 +1160,16 @@ void Project::setNewLayoutBlockdata(Layout *layout) { } bool Project::loadLayoutBorder(Layout *layout) { + bool ok = true; QString path = QString("%1/%2").arg(root).arg(layout->border_path); - layout->border = readBlockdata(path); - layout->lastCommitBlocks.border = layout->border; + auto blockdata = readBlockdata(path, &ok); + if (!ok) { + logError(QString("Failed to load layout border from '%1'").arg(path)); + return false; + } + + layout->border = blockdata; + layout->lastCommitBlocks.border = blockdata; layout->lastCommitBlocks.borderDimensions = QSize(layout->getBorderWidth(), layout->getBorderHeight()); int borderLength = layout->getBorderWidth() * layout->getBorderHeight(); @@ -1299,39 +1231,32 @@ void Project::saveAllMaps() { void Project::saveMap(Map *map) { // Create/Modify a few collateral files for brand new maps. - QString basePath = projectConfig.getFilePath(ProjectFilePath::data_map_folders); - QString mapDataDir = root + "/" + basePath + map->name; - if (!map->isPersistedToFile) { - if (!QDir::root().mkdir(mapDataDir)) { - logError(QString("Error: failed to create directory for new map: '%1'").arg(mapDataDir)); + const QString folderPath = projectConfig.getFilePath(ProjectFilePath::data_map_folders) + map->name(); + const QString fullPath = QString("%1/%2").arg(this->root).arg(folderPath); + if (!map->isPersistedToFile()) { + if (!QDir::root().mkpath(fullPath)) { + logError(QString("Failed to create directory for new map: '%1'").arg(fullPath)); } // Create file data/maps//scripts.inc - QString text = this->getScriptDefaultString(projectConfig.usePoryScript, map->name); - saveTextFile(mapDataDir + "/scripts" + this->getScriptFileExtension(projectConfig.usePoryScript), text); + QString text = this->getScriptDefaultString(projectConfig.usePoryScript, map->name()); + saveTextFile(fullPath + "/scripts" + this->getScriptFileExtension(projectConfig.usePoryScript), text); if (projectConfig.createMapTextFileEnabled) { // Create file data/maps//text.inc - saveTextFile(mapDataDir + "/text" + this->getScriptFileExtension(projectConfig.usePoryScript), "\n"); + saveTextFile(fullPath + "/text" + this->getScriptFileExtension(projectConfig.usePoryScript), "\n"); } // Simply append to data/event_scripts.s. - text = QString("\n\t.include \"%1%2/scripts.inc\"\n").arg(basePath, map->name); + text = QString("\n\t.include \"%1/scripts.inc\"\n").arg(folderPath); if (projectConfig.createMapTextFileEnabled) { - text += QString("\t.include \"%1%2/text.inc\"\n").arg(basePath, map->name); + text += QString("\t.include \"%1/text.inc\"\n").arg(folderPath); } appendTextFile(root + "/" + projectConfig.getFilePath(ProjectFilePath::data_event_scripts), text); - - if (map->needsLayoutDir) { - QString newLayoutDir = QString(root + "/%1%2").arg(projectConfig.getFilePath(ProjectFilePath::data_layouts_folders), map->name); - if (!QDir::root().mkdir(newLayoutDir)) { - logError(QString("Error: failed to create directory for new layout: '%1'").arg(newLayoutDir)); - } - } } // Create map.json for map data. - QString mapFilepath = QString("%1/map.json").arg(mapDataDir); + QString mapFilepath = fullPath + "/map.json"; QFile mapFile(mapFilepath); if (!mapFile.open(QIODevice::WriteOnly)) { logError(QString("Error: Could not open %1 for writing").arg(mapFilepath)); @@ -1340,110 +1265,97 @@ void Project::saveMap(Map *map) { OrderedJson::object mapObj; // Header values. - mapObj["id"] = map->constantName; - mapObj["name"] = map->name; - mapObj["layout"] = map->layout->id; - mapObj["music"] = map->song; - mapObj["region_map_section"] = map->location; - mapObj["requires_flash"] = map->requiresFlash; - mapObj["weather"] = map->weather; - mapObj["map_type"] = map->type; + mapObj["id"] = map->constantName(); + mapObj["name"] = map->name(); + mapObj["layout"] = map->layout()->id; + mapObj["music"] = map->header()->song(); + mapObj["region_map_section"] = map->header()->location(); + mapObj["requires_flash"] = map->header()->requiresFlash(); + mapObj["weather"] = map->header()->weather(); + mapObj["map_type"] = map->header()->type(); if (projectConfig.mapAllowFlagsEnabled) { - mapObj["allow_cycling"] = map->allowBiking; - mapObj["allow_escaping"] = map->allowEscaping; - mapObj["allow_running"] = map->allowRunning; + mapObj["allow_cycling"] = map->header()->allowsBiking(); + mapObj["allow_escaping"] = map->header()->allowsEscaping(); + mapObj["allow_running"] = map->header()->allowsRunning(); } - mapObj["show_map_name"] = map->show_location; + mapObj["show_map_name"] = map->header()->showsLocationName(); if (projectConfig.floorNumberEnabled) { - mapObj["floor_number"] = map->floorNumber; + mapObj["floor_number"] = map->header()->floorNumber(); } - mapObj["battle_scene"] = map->battle_scene; + mapObj["battle_scene"] = map->header()->battleScene(); // Connections - auto connections = map->getConnections(); + const auto connections = map->getConnections(); if (connections.length() > 0) { OrderedJson::array connectionsArr; - for (auto connection : connections) { - if (mapNamesToMapConstants.contains(connection->targetMapName())) { - OrderedJson::object connectionObj; - connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName()); - connectionObj["offset"] = connection->offset(); - connectionObj["direction"] = connection->direction(); - connectionsArr.append(connectionObj); - } else { - logError(QString("Failed to write map connection. '%1' is not a valid map name").arg(connection->targetMapName())); - } + for (const auto &connection : connections) { + OrderedJson::object connectionObj; + connectionObj["map"] = this->mapNamesToMapConstants.value(connection->targetMapName(), connection->targetMapName()); + connectionObj["offset"] = connection->offset(); + connectionObj["direction"] = connection->direction(); + connectionsArr.append(connectionObj); } mapObj["connections"] = connectionsArr; } else { mapObj["connections"] = QJsonValue::Null; } - if (map->sharedEventsMap.isEmpty()) { + if (map->sharedEventsMap().isEmpty()) { // Object events OrderedJson::array objectEventsArr; - for (int i = 0; i < map->events[Event::Group::Object].length(); i++) { - Event *event = map->events[Event::Group::Object].value(i); - OrderedJson::object jsonObj = event->buildEventJson(this); - objectEventsArr.push_back(jsonObj); + for (const auto &event : map->getEvents(Event::Group::Object)){ + objectEventsArr.push_back(event->buildEventJson(this)); } mapObj["object_events"] = objectEventsArr; // Warp events OrderedJson::array warpEventsArr; - for (int i = 0; i < map->events[Event::Group::Warp].length(); i++) { - Event *event = map->events[Event::Group::Warp].value(i); - OrderedJson::object warpObj = event->buildEventJson(this); - warpEventsArr.append(warpObj); + for (const auto &event : map->getEvents(Event::Group::Warp)) { + warpEventsArr.push_back(event->buildEventJson(this)); } mapObj["warp_events"] = warpEventsArr; // Coord events OrderedJson::array coordEventsArr; - for (int i = 0; i < map->events[Event::Group::Coord].length(); i++) { - Event *event = map->events[Event::Group::Coord].value(i); - OrderedJson::object triggerObj = event->buildEventJson(this); - coordEventsArr.append(triggerObj); + for (const auto &event : map->getEvents(Event::Group::Coord)) { + coordEventsArr.push_back(event->buildEventJson(this)); } mapObj["coord_events"] = coordEventsArr; // Bg Events OrderedJson::array bgEventsArr; - for (int i = 0; i < map->events[Event::Group::Bg].length(); i++) { - Event *event = map->events[Event::Group::Bg].value(i); - OrderedJson::object bgObj = event->buildEventJson(this); - bgEventsArr.append(bgObj); + for (const auto &event : map->getEvents(Event::Group::Bg)) { + bgEventsArr.push_back(event->buildEventJson(this)); } mapObj["bg_events"] = bgEventsArr; } else { - mapObj["shared_events_map"] = map->sharedEventsMap; + mapObj["shared_events_map"] = map->sharedEventsMap(); } - if (!map->sharedScriptsMap.isEmpty()) { - mapObj["shared_scripts_map"] = map->sharedScriptsMap; + if (!map->sharedScriptsMap().isEmpty()) { + mapObj["shared_scripts_map"] = map->sharedScriptsMap(); } // Custom header fields. +/* // TODO: Re-enable for (QString key : map->customHeaders.keys()) { mapObj[key] = OrderedJson::fromQJsonValue(map->customHeaders[key]); } +*/ OrderedJson mapJson(mapObj); OrderedJsonDoc jsonDoc(&mapJson); jsonDoc.dump(&mapFile); mapFile.close(); - saveLayout(map->layout); + saveLayout(map->layout()); saveHealLocations(map); - map->isPersistedToFile = true; - map->hasUnsavedDataChanges = false; - map->editHistory.setClean(); + map->setClean(); } void Project::saveLayout(Layout *layout) { - // saveLayoutBorder(layout); saveLayoutBlockdata(layout); @@ -1454,15 +1366,15 @@ void Project::saveLayout(Layout *layout) { } void Project::updateLayout(Layout *layout) { - if (!mapLayoutsTableMaster.contains(layout->id)) { - mapLayoutsTableMaster.append(layout->id); + if (!this->layoutIdsMaster.contains(layout->id)) { + this->layoutIdsMaster.append(layout->id); } - if (mapLayoutsMaster.contains(layout->id)) { - mapLayoutsMaster[layout->id]->copyFrom(layout); + if (this->mapLayoutsMaster.contains(layout->id)) { + this->mapLayoutsMaster[layout->id]->copyFrom(layout); } else { - mapLayoutsMaster.insert(layout->id, layout->copy()); + this->mapLayoutsMaster.insert(layout->id, layout->copy()); } } @@ -1470,7 +1382,6 @@ void Project::saveAllDataStructures() { saveMapLayouts(); saveMapGroups(); saveRegionMapSections(); - saveMapConstantsHeader(); saveWildMonData(); saveConfig(); this->hasUnsavedDataChanges = false; @@ -1485,18 +1396,9 @@ void Project::loadTilesetAssets(Tileset* tileset) { if (tileset->name.isNull()) { return; } - this->readTilesetPaths(tileset); - QImage image; - if (QFile::exists(tileset->tilesImagePath)) { - image = QImage(tileset->tilesImagePath).convertToFormat(QImage::Format_Indexed8, Qt::ThresholdDither); - flattenTo4bppImage(&image); - } else { - image = QImage(8, 8, QImage::Format_Indexed8); - } - this->loadTilesetTiles(tileset, image); - this->loadTilesetMetatiles(tileset); - this->loadTilesetMetatileLabels(tileset); - this->loadTilesetPalettes(tileset); + readTilesetPaths(tileset); + loadTilesetMetatileLabels(tileset); + tileset->load(); } void Project::readTilesetPaths(Tileset* tileset) { @@ -1556,88 +1458,102 @@ void Project::readTilesetPaths(Tileset* tileset) { } } -void Project::loadTilesetPalettes(Tileset* tileset) { - QList> palettes; - QList> palettePreviews; - for (int i = 0; i < tileset->palettePaths.length(); i++) { - QString path = tileset->palettePaths.value(i); - bool error = false; - QList palette = PaletteUtil::parse(path, &error); - if (error) { - for (int j = 0; j < 16; j++) { - palette.append(qRgb(j * 16, j * 16, j * 16)); +Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboardFill) { + const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix); + if (!name.startsWith(prefix)) { + logError(QString("Tileset name '%1' doesn't begin with the prefix '%2'.").arg(name).arg(prefix)); + return nullptr; + } + + auto tileset = new Tileset(); + tileset->name = name; + tileset->is_secondary = secondary; + + // Create tileset directories + const QString fullDirectoryPath = QString("%1/%2").arg(this->root).arg(tileset->getExpectedDir()); + QDir directory; + if (!directory.mkpath(fullDirectoryPath)) { + logError(QString("Failed to create directory '%1' for new tileset '%2'").arg(fullDirectoryPath).arg(tileset->name)); + delete tileset; + return nullptr; + } + const QString palettesPath = fullDirectoryPath + "/palettes"; + if (!directory.mkpath(palettesPath)) { + logError(QString("Failed to create palettes directory '%1' for new tileset '%2'").arg(palettesPath).arg(tileset->name)); + delete tileset; + return nullptr; + } + + tileset->tilesImagePath = fullDirectoryPath + "/tiles.png"; + tileset->metatiles_path = fullDirectoryPath + "/metatiles.bin"; + tileset->metatile_attrs_path = fullDirectoryPath + "/metatile_attributes.bin"; + + // Set default tiles image + QImage tilesImage(":/images/blank_tileset.png"); + tileset->loadTilesImage(&tilesImage); + + // Create default metatiles + const int numMetatiles = tileset->is_secondary ? (Project::getNumMetatilesTotal() - Project::getNumMetatilesPrimary()) : Project::getNumMetatilesPrimary(); + const int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); + for (int i = 0; i < numMetatiles; ++i) { + auto metatile = new Metatile(); + for(int j = 0; j < tilesPerMetatile; ++j){ + Tile tile = Tile(); + if (checkerboardFill) { + // Create a checkerboard-style dummy tileset + if (((i / 8) % 2) == 0) + tile.tileId = ((i % 2) == 0) ? 1 : 2; + else + tile.tileId = ((i % 2) == 1) ? 1 : 2; + + if (tileset->is_secondary) + tile.tileId += Project::getNumTilesPrimary(); } + metatile->tiles.append(tile); } - - palettes.append(palette); - palettePreviews.append(palette); - } - tileset->palettes = palettes; - tileset->palettePreviews = palettePreviews; -} - -void Project::loadTilesetTiles(Tileset *tileset, QImage image) { - QList tiles; - int w = 8; - int h = 8; - for (int y = 0; y < image.height(); y += h) - for (int x = 0; x < image.width(); x += w) { - QImage tile = image.copy(x, y, w, h); - tiles.append(tile); - } - tileset->tilesImage = image; - tileset->tiles = tiles; -} - -void Project::loadTilesetMetatiles(Tileset* tileset) { - QFile metatiles_file(tileset->metatiles_path); - if (metatiles_file.open(QIODevice::ReadOnly)) { - QByteArray data = metatiles_file.readAll(); - int tilesPerMetatile = projectConfig.getNumTilesInMetatile(); - int bytesPerMetatile = 2 * tilesPerMetatile; - int num_metatiles = data.length() / bytesPerMetatile; - QList metatiles; - for (int i = 0; i < num_metatiles; i++) { - Metatile *metatile = new Metatile; - int index = i * bytesPerMetatile; - for (int j = 0; j < tilesPerMetatile; j++) { - uint16_t tileRaw = static_cast(data[index++]); - tileRaw |= static_cast(data[index++]) << 8; - metatile->tiles.append(Tile(tileRaw)); - } - metatiles.append(metatile); - } - tileset->setMetatiles(metatiles); - } else { - tileset->clearMetatiles(); - logError(QString("Could not open tileset metatiles file '%1'").arg(tileset->metatiles_path)); + tileset->addMetatile(metatile); } - QFile attrs_file(tileset->metatile_attrs_path); - if (attrs_file.open(QIODevice::ReadOnly)) { - QByteArray data = attrs_file.readAll(); - int num_metatiles = tileset->numMetatiles(); - int attrSize = projectConfig.metatileAttributesSize; - int num_metatileAttrs = data.length() / attrSize; - if (num_metatiles != num_metatileAttrs) { - logWarn(QString("Metatile count %1 does not match metatile attribute count %2 in %3").arg(num_metatiles).arg(num_metatileAttrs).arg(tileset->name)); - if (num_metatileAttrs > num_metatiles) - num_metatileAttrs = num_metatiles; + // Create default palettes + for(int i = 0; i < 16; ++i) { + QList currentPal; + for(int i = 0; i < 16;++i) { + currentPal.append(qRgb(0,0,0)); } - - for (int i = 0; i < num_metatileAttrs; i++) { - uint32_t attributes = 0; - for (int j = 0; j < attrSize; j++) - attributes |= static_cast(data.at(i * attrSize + j)) << (8 * j); - tileset->metatileAt(i)->setAttributes(attributes); - } - } else { - logError(QString("Could not open tileset metatile attributes file '%1'").arg(tileset->metatile_attrs_path)); + tileset->palettes.append(currentPal); + tileset->palettePreviews.append(currentPal); + tileset->palettePaths.append(QString("%1/%2.pal").arg(palettesPath).arg(i, 2, 10, QLatin1Char('0'))); } + tileset->palettes[0][1] = qRgb(255,0,255); + tileset->palettePreviews[0][1] = qRgb(255,0,255); + + // Update tileset label arrays + QStringList *labelList = tileset->is_secondary ? &this->secondaryTilesetLabels : &this->primaryTilesetLabels; + for (int i = 0; i < labelList->length(); i++) { + if (labelList->at(i) > tileset->name) { + labelList->insert(i, tileset->name); + break; + } + } + this->tilesetLabelsOrdered.append(tileset->name); + + // TODO: Ideally we wouldn't save new Tilesets immediately + // Append to tileset specific files. Strip prefix from name to get base name for use in other symbols. + name.remove(0, prefix.length()); + tileset->appendToHeaders(this->root, name, this->usingAsmTilesets); + tileset->appendToGraphics(this->root, name, this->usingAsmTilesets); + tileset->appendToMetatiles(this->root, name, this->usingAsmTilesets); + + tileset->save(); + + this->tilesetCache.insert(tileset->name, tileset); + + emit tilesetCreated(tileset); + return tileset; } QString Project::findMetatileLabelsTileset(QString label) { - for (QString tilesetName : this->tilesetLabelsOrdered) { + for (const QString &tilesetName : this->tilesetLabelsOrdered) { QString metatileLabelPrefix = Tileset::getMetatileLabelPrefix(tilesetName); if (label.startsWith(metatileLabelPrefix)) return tilesetName; @@ -1678,13 +1594,13 @@ void Project::loadTilesetMetatileLabels(Tileset* tileset) { QString metatileLabelPrefix = tileset->getMetatileLabelPrefix(); // Reverse map for faster lookup by metatile id - for (QString labelName : metatileLabelsMap[tileset->name].keys()) { - auto metatileId = metatileLabelsMap[tileset->name][labelName]; - tileset->metatileLabels[metatileId] = labelName.replace(metatileLabelPrefix, ""); + for (auto it = this->metatileLabelsMap[tileset->name].constBegin(); it != this->metatileLabelsMap[tileset->name].constEnd(); it++) { + QString labelName = it.key(); + tileset->metatileLabels[it.value()] = labelName.replace(metatileLabelPrefix, ""); } } -Blockdata Project::readBlockdata(QString path) { +Blockdata Project::readBlockdata(QString path, bool *ok) { Blockdata blockdata; QFile file(path); if (file.open(QIODevice::ReadOnly)) { @@ -1693,8 +1609,10 @@ Blockdata Project::readBlockdata(QString path) { uint16_t word = static_cast((data[i] & 0xff) + ((data[i + 1] & 0xff) << 8)); blockdata.append(word); } + if (ok) *ok = true; } else { - logError(QString("Failed to open blockdata path '%1'").arg(path)); + // Failed + if (ok) *ok = false; } return blockdata; @@ -1886,10 +1804,11 @@ bool Project::readWildMonData() { bool Project::readMapGroups() { this->mapConstantsToMapNames.clear(); this->mapNamesToMapConstants.clear(); - this->mapGroups.clear(); - this->groupNames.clear(); - this->groupedMapNames.clear(); this->mapNames.clear(); + this->groupNames.clear(); + this->groupNameToMapNames.clear(); + + this->initTopLevelMapFields(); const QString filepath = root + "/" + projectConfig.getFilePath(ProjectFilePath::json_map_groups); fileWatcher.addPath(filepath); @@ -1901,25 +1820,89 @@ bool Project::readMapGroups() { QJsonObject mapGroupsObj = mapGroupsDoc.object(); QJsonArray mapGroupOrder = mapGroupsObj["group_order"].toArray(); + + const QString dynamicMapName = getDynamicMapName(); + const QString dynamicMapConstant = getDynamicMapDefineName(); + + // Process the map group lists + QStringList failedMapNames; for (int groupIndex = 0; groupIndex < mapGroupOrder.size(); groupIndex++) { - QString groupName = ParseUtil::jsonToQString(mapGroupOrder.at(groupIndex)); - QJsonArray mapNamesJson = mapGroupsObj.value(groupName).toArray(); - this->groupedMapNames.append(QStringList()); + const QString groupName = ParseUtil::jsonToQString(mapGroupOrder.at(groupIndex)); + const QJsonArray mapNamesJson = mapGroupsObj.value(groupName).toArray(); this->groupNames.append(groupName); + + // Process the names in this map group for (int j = 0; j < mapNamesJson.size(); j++) { - QString mapName = ParseUtil::jsonToQString(mapNamesJson.at(j)); - if (mapName == DYNAMIC_MAP_NAME) { + const QString mapName = ParseUtil::jsonToQString(mapNamesJson.at(j)); + if (mapName == dynamicMapName) { logWarn(QString("Ignoring map with reserved name '%1'.").arg(mapName)); + failedMapNames.append(mapName); + continue; + } + if (this->mapNames.contains(mapName)) { + logWarn(QString("Ignoring repeated map name '%1'.").arg(mapName)); + failedMapNames.append(mapName); continue; } - this->mapGroups.insert(mapName, groupIndex); - this->groupedMapNames[groupIndex].append(mapName); - this->mapNames.append(mapName); - // Build the mapping and reverse mapping between map constants and map names. - QString mapConstant = Map::mapConstantFromName(mapName); + // Load the map's json file so we can get its ID constant (and two other constants we use for the map list). + QJsonDocument mapDoc; + if (!readMapJson(mapName, &mapDoc)) { + failedMapNames.append(mapName); + continue; // Error message has already been logged + } + + // Read and validate the map's ID from its JSON data. + const QJsonObject mapObj = mapDoc.object(); + const QString mapConstant = ParseUtil::jsonToQString(mapObj["id"]); + if (mapConstant.isEmpty()) { + logWarn(QString("Map '%1' is missing an \"id\" value and will be ignored.").arg(mapName)); + failedMapNames.append(mapName); + continue; + } + if (mapConstant == dynamicMapConstant) { + logWarn(QString("Ignoring map with reserved \"id\" value '%1'.").arg(mapName)); + failedMapNames.append(mapName); + continue; + } + const QString expectedPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); + if (!mapConstant.startsWith(expectedPrefix)) { + logWarn(QString("Map '%1' has invalid \"id\" value '%2' and will be ignored. Value must begin with '%3'.").arg(mapName).arg(mapConstant).arg(expectedPrefix)); + failedMapNames.append(mapName); + continue; + } + auto it = this->mapConstantsToMapNames.constFind(mapConstant); + if (it != this->mapConstantsToMapNames.constEnd()) { + logWarn(QString("Map '%1' has the same \"id\" value '%2' as map '%3' and will be ignored.").arg(mapName).arg(it.key()).arg(it.value())); + failedMapNames.append(mapName); + continue; + } + + // Read layout ID for map list + const QString layoutId = ParseUtil::jsonToQString(mapObj["layout"]); + if (!this->layoutIds.contains(layoutId)) { + // If a map has an unknown layout ID it won't be able to load it at all anyway, so skip it. + // Skipping these will let us assume all the map layout IDs are valid, which simplies some handling elsewhere. + logWarn(QString("Map '%1' has unknown \"layout\" value '%2' and will be ignored.").arg(mapName).arg(layoutId)); + failedMapNames.append(mapName); + continue; + } + + // Read MAPSEC name for map list + const QString mapSectionName = ParseUtil::jsonToQString(mapObj["region_map_section"]); + if (!this->mapSectionIdNames.contains(mapSectionName)) { + // An unknown location is OK. Aside from that name not appearing in the dropdowns this shouldn't cause problems. + // We'll log a warning, but allow this map to be displayed. + logWarn(QString("Map '%1' has unknown \"region_map_section\" value '%2'.").arg(mapName).arg(mapSectionName)); + } + + // Success, save the constants to the project + this->mapNames.append(mapName); + this->groupNameToMapNames[groupName].append(mapName); this->mapConstantsToMapNames.insert(mapConstant, mapName); this->mapNamesToMapConstants.insert(mapName, mapConstant); + this->mapNameToLayoutId.insert(mapName, layoutId); + this->mapNameToMapSectionName.insert(mapName, mapSectionName); } } @@ -1932,55 +1915,123 @@ bool Project::readMapGroups() { return false; } - const QString defineName = this->getDynamicMapDefineName(); - this->mapConstantsToMapNames.insert(defineName, DYNAMIC_MAP_NAME); - this->mapNamesToMapConstants.insert(DYNAMIC_MAP_NAME, defineName); - this->mapNames.append(DYNAMIC_MAP_NAME); + if (!failedMapNames.isEmpty()) { + // At least 1 map was excluded due to an error. + // User should be alerted of this, rather than just silently logging the details. + emit mapsExcluded(failedMapNames); + } + + // Save special "Dynamic" constant + this->mapConstantsToMapNames.insert(dynamicMapConstant, dynamicMapName); + this->mapNamesToMapConstants.insert(dynamicMapName, dynamicMapConstant); + this->mapNames.append(dynamicMapName); return true; } -Map* Project::addNewMapToGroup(QString mapName, int groupNum, Map *newMap, bool existingLayout, bool importedMap) { - int mapNamePos = 0; - for (int i = 0; i <= groupNum; i++) - mapNamePos += this->groupedMapNames.value(i).length(); +void Project::addNewMapGroup(const QString &groupName) { + if (this->groupNames.contains(groupName)) + return; - this->mapNames.insert(mapNamePos, mapName); - this->mapGroups.insert(mapName, groupNum); - this->groupedMapNames[groupNum].append(mapName); + this->groupNames.append(groupName); + this->groupNameToMapNames.insert(groupName, QStringList()); + this->hasUnsavedDataChanges = true; - newMap->isPersistedToFile = false; - newMap->setName(mapName); - - this->mapConstantsToMapNames.insert(newMap->constantName, newMap->name); - this->mapNamesToMapConstants.insert(newMap->name, newMap->constantName); - if (!existingLayout) { - this->mapLayouts.insert(newMap->layoutId, newMap->layout); - this->mapLayoutsTable.append(newMap->layoutId); - this->layoutIdsToNames.insert(newMap->layout->id, newMap->layout->name); - if (!importedMap) { - setNewLayoutBlockdata(newMap->layout); - } - if (newMap->layout->border.isEmpty()) { - setNewLayoutBorder(newMap->layout); - } - } - - loadLayoutTilesets(newMap->layout); - setNewMapEvents(newMap); - - return newMap; + emit mapGroupAdded(groupName); } -QString Project::getNewMapName() { - // Ensure default name doesn't already exist. - int i = 0; - QString newMapName; - do { - newMapName = QString("NewMap%1").arg(++i); - } while (mapNames.contains(newMapName)); +QString Project::mapNameToMapGroup(const QString &mapName) { + for (auto it = this->groupNameToMapNames.constBegin(); it != this->groupNameToMapNames.constEnd(); it++) { + const QStringList mapNames = it.value(); + if (mapNames.contains(mapName)) { + return it.key(); + } + } + return QString(); +} - return newMapName; +// When we ask the user to provide a new identifier for something (like a map name or MAPSEC id) +// we use this to make sure that it doesn't collide with any known identifiers first. +// Porymap knows of many more identifiers than this, but for simplicity we only check the lists that users can add to via Porymap. +// In general this only matters to Porymap if the identifier will be added to the group it collides with, +// but name collisions are likely undesirable in the project. +bool Project::isIdentifierUnique(const QString &identifier) const { + if (this->mapNames.contains(identifier)) + return false; + if (this->mapConstantsToMapNames.contains(identifier)) + return false; + if (this->groupNames.contains(identifier)) + return false; + if (this->mapSectionIdNames.contains(identifier)) + return false; + if (this->tilesetLabelsOrdered.contains(identifier)) + return false; + if (this->layoutIds.contains(identifier)) + return false; + for (const auto &layout : this->mapLayouts) { + if (layout->name == identifier) { + return false; + } + } + if (identifier == getEmptyMapDefineName()) + return false; + return true; +} + +// For some arbitrary string, return true if it's both a valid identifier name and not one that's already in-use. +bool Project::isValidNewIdentifier(QString identifier) const { + IdentifierValidator validator; + return validator.isValid(identifier) && isIdentifierUnique(identifier); +} + +// Assumes 'identifier' is a valid name. If 'identifier' is unique, returns 'identifier'. +// Otherwise returns the identifier with a numbered suffix added to make it unique. +QString Project::toUniqueIdentifier(const QString &identifier) const { + int suffix = 2; + QString uniqueIdentifier = identifier; + while (!isIdentifierUnique(uniqueIdentifier)) { + uniqueIdentifier = QString("%1_%2").arg(identifier).arg(suffix++); + } + return uniqueIdentifier; +} + +void Project::initNewMapSettings() { + this->newMapSettings.name = QString(); + this->newMapSettings.group = this->groupNames.at(0); + this->newMapSettings.canFlyTo = false; + + 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.borderWidth = DEFAULT_BORDER_WIDTH; + this->newMapSettings.layout.borderHeight = DEFAULT_BORDER_HEIGHT; + this->newMapSettings.layout.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); + this->newMapSettings.layout.secondaryTilesetLabel = getDefaultSecondaryTilesetLabel(); + + this->newMapSettings.header.setSong(this->defaultSong); + this->newMapSettings.header.setLocation(this->mapSectionIdNames.value(0, "0")); + this->newMapSettings.header.setRequiresFlash(false); + this->newMapSettings.header.setWeather(this->weatherNames.value(0, "0")); + this->newMapSettings.header.setType(this->mapTypes.value(0, "0")); + this->newMapSettings.header.setBattleScene(this->mapBattleScenes.value(0, "0")); + this->newMapSettings.header.setShowsLocationName(true); + this->newMapSettings.header.setAllowsRunning(false); + this->newMapSettings.header.setAllowsBiking(false); + this->newMapSettings.header.setAllowsEscaping(false); + this->newMapSettings.header.setFloorNumber(0); +} + +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.borderWidth = DEFAULT_BORDER_WIDTH; + this->newLayoutSettings.borderHeight = DEFAULT_BORDER_HEIGHT; + this->newLayoutSettings.primaryTilesetLabel = getDefaultPrimaryTilesetLabel(); + this->newLayoutSettings.secondaryTilesetLabel = getDefaultSecondaryTilesetLabel(); } Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) { @@ -1995,7 +2046,7 @@ Project::DataQualifiers Project::getDataQualifiers(QString text, QString label) return qualifiers; } -QString Project::getDefaultPrimaryTilesetLabel() { +QString Project::getDefaultPrimaryTilesetLabel() const { QString defaultLabel = projectConfig.defaultPrimaryTileset; if (!this->primaryTilesetLabels.contains(defaultLabel)) { QString firstLabel = this->primaryTilesetLabels.first(); @@ -2005,7 +2056,7 @@ QString Project::getDefaultPrimaryTilesetLabel() { return defaultLabel; } -QString Project::getDefaultSecondaryTilesetLabel() { +QString Project::getDefaultSecondaryTilesetLabel() const { QString defaultLabel = projectConfig.defaultSecondaryTileset; if (!this->secondaryTilesetLabels.contains(defaultLabel)) { QString firstLabel = this->secondaryTilesetLabels.first(); @@ -2015,7 +2066,7 @@ QString Project::getDefaultSecondaryTilesetLabel() { return defaultLabel; } -void Project::appendTilesetLabel(QString label, QString isSecondaryStr) { +void Project::appendTilesetLabel(const QString &label, const QString &isSecondaryStr) { bool ok; bool isSecondary = ParseUtil::gameStringToBool(isSecondaryStr, &ok); if (!ok) { @@ -2049,20 +2100,18 @@ bool Project::readTilesetLabels() { QRegularExpressionMatch match = iter.next(); appendTilesetLabel(match.captured("label"), match.captured("isSecondary")); } - this->primaryTilesetLabels.sort(); - this->secondaryTilesetLabels.sort(); - this->tilesetLabelsOrdered.sort(); filename = asm_filename; // For error reporting further down } else { this->usingAsmTilesets = false; const auto structs = parser.readCStructs(filename, "", Tileset::getHeaderMemberMap(this->usingAsmTilesets)); - const QStringList labels = structs.keys(); - // TODO: This is alphabetical, AdvanceMap import wants the vanilla order in tilesetLabelsOrdered - for (const auto &tilesetLabel : labels){ - appendTilesetLabel(tilesetLabel, structs[tilesetLabel].value("isSecondary")); + for (auto i = structs.cbegin(); i != structs.cend(); i++){ + appendTilesetLabel(i.key(), i.value().value("isSecondary")); } } + numericalModeSort(this->primaryTilesetLabels); + numericalModeSort(this->secondaryTilesetLabels); + bool success = true; if (this->secondaryTilesetLabels.isEmpty()) { logError(QString("Failed to find any secondary tilesets in %1").arg(filename)); @@ -2263,15 +2312,17 @@ bool Project::readFieldmapMasks() { bool Project::readRegionMapSections() { this->mapSectionIdNames.clear(); + this->mapSectionIdNamesSaveOrder.clear(); + this->mapSectionDisplayNames.clear(); this->regionMapEntries.clear(); - this->saveEmptyMapsec = false; const QString defaultName = getEmptyMapsecName(); const QString requiredPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix); QJsonDocument doc; - const QString filepath = QString("%1/%2").arg(this->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_map_entries)); + const QString baseFilepath = projectConfig.getFilePath(ProjectFilePath::json_region_map_entries); + const QString filepath = QString("%1/%2").arg(this->root).arg(baseFilepath); if (!parser.tryParseJsonFile(&doc, filepath)) { - logError(QString("Failed to read region map sections from '%1'").arg(filepath)); + logError(QString("Failed to read region map sections from '%1'").arg(baseFilepath)); return false; } fileWatcher.addPath(filepath); @@ -2280,29 +2331,36 @@ bool Project::readRegionMapSections() { for (int i = 0; i < mapSections.size(); i++) { QJsonObject mapSectionObj = mapSections.at(i).toObject(); - // For each map section, "id" is the only required field. This is the field we use to display the location names in various drop-downs. - const QString idField = "id"; + // For each map section, "id" is the only required field. This is the field we use to display the location names in the map list, and in various drop-downs. + QString idField = "id"; if (!mapSectionObj.contains(idField)) { - logWarn(QString("Ignoring data for map section %1. Missing required field \"%2\"").arg(i).arg(idField)); - continue; + const QString oldIdField = "map_section"; + if (mapSectionObj.contains(oldIdField)) { + // User has the old name for this field. Parse using this name, then save with the new name. + // This will presumably stop the user's project from compiling, but that's preferable to + // ignoring everything here and then wiping the file's data when we save later. + idField = oldIdField; + } else { + logWarn(QString("Ignoring data for map section %1 in '%2'. Missing required field \"%3\"").arg(i).arg(baseFilepath).arg(idField)); + continue; + } } const QString idName = ParseUtil::jsonToQString(mapSectionObj[idField]); if (!idName.startsWith(requiredPrefix)) { - logWarn(QString("Ignoring data for map section '%1'. IDs must start with the prefix '%2'").arg(idName).arg(requiredPrefix)); + logWarn(QString("Ignoring data for map section '%1' in '%2'. IDs must start with the prefix '%3'").arg(idName).arg(baseFilepath).arg(requiredPrefix)); continue; } this->mapSectionIdNames.append(idName); - if (idName == defaultName) { - // If the user has data for the 'empty' MAPSEC we need to know to output it later, - // because we will otherwise add a dummy entry for this value. - this->saveEmptyMapsec = true; - } + this->mapSectionIdNamesSaveOrder.append(idName); + + if (mapSectionObj.contains("name")) + this->mapSectionDisplayNames.insert(idName, ParseUtil::jsonToQString(mapSectionObj["name"])); // Map sections may have additional data indicating their position on the region map. // If they have this data, we can add them to the region map entry list. bool hasRegionMapData = true; - static const QSet regionMapFieldNames = { "name", "x", "y", "width", "height" }; + static const QSet regionMapFieldNames = { "x", "y", "width", "height" }; for (auto fieldName : regionMapFieldNames) { if (!mapSectionObj.contains(fieldName)) { hasRegionMapData = false; @@ -2313,7 +2371,6 @@ bool Project::readRegionMapSections() { continue; MapSectionEntry entry; - entry.name = ParseUtil::jsonToQString(mapSectionObj["name"]); entry.x = ParseUtil::jsonToInt(mapSectionObj["x"]); entry.y = ParseUtil::jsonToInt(mapSectionObj["y"]); entry.width = ParseUtil::jsonToInt(mapSectionObj["width"]); @@ -2326,6 +2383,7 @@ bool Project::readRegionMapSections() { if (!this->mapSectionIdNames.contains(defaultName)) { this->mapSectionIdNames.append(defaultName); } + numericalModeSort(this->mapSectionIdNames); return true; } @@ -2334,25 +2392,45 @@ QString Project::getEmptyMapsecName() { return projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_map_section_empty); } -// This function assumes a valid and unique name -void Project::addNewMapsec(const QString &name) { - if (!this->mapSectionIdNames.isEmpty() && this->mapSectionIdNames.last() == getEmptyMapsecName()) { - // If the default map section name (MAPSEC_NONE) is last in the list we'll keep it last in the list. - this->mapSectionIdNames.insert(this->mapSectionIdNames.length() - 1, name); - } else { - this->mapSectionIdNames.append(name); - } - this->hasUnsavedDataChanges = true; - emit mapSectionIdNamesChanged(); +QString Project::getMapGroupPrefix() { + // We could expose this to users, but it's never enforced so it probably won't affect anyone. + return QStringLiteral("gMapGroup_"); } -void Project::removeMapsec(const QString &name) { - if (!this->mapSectionIdNames.contains(name) || name == getEmptyMapsecName()) +// This function assumes a valid and unique name +void Project::addNewMapsec(const QString &idName) { + if (this->mapSectionIdNamesSaveOrder.last() == getEmptyMapsecName()) { + // If the default map section name (MAPSEC_NONE) is last in the list we'll keep it last in the list. + this->mapSectionIdNamesSaveOrder.insert(this->mapSectionIdNames.length() - 1, idName); + } else { + this->mapSectionIdNamesSaveOrder.append(idName); + } + + this->mapSectionIdNames.append(idName); + numericalModeSort(this->mapSectionIdNames); + + this->hasUnsavedDataChanges = true; + + emit mapSectionAdded(idName); + emit mapSectionIdNamesChanged(this->mapSectionIdNames); +} + +void Project::removeMapsec(const QString &idName) { + if (!this->mapSectionIdNames.contains(idName) || idName == getEmptyMapsecName()) return; - this->mapSectionIdNames.removeOne(name); + this->mapSectionIdNames.removeOne(idName); + this->mapSectionIdNamesSaveOrder.removeOne(idName); this->hasUnsavedDataChanges = true; - emit mapSectionIdNamesChanged(); + emit mapSectionIdNamesChanged(this->mapSectionIdNames); +} + +void Project::setMapsecDisplayName(const QString &idName, const QString &displayName) { + if (this->mapSectionDisplayNames.value(idName) == displayName) + return; + this->mapSectionDisplayNames[idName] = displayName; + this->hasUnsavedDataChanges = true; + emit mapSectionDisplayNameChanged(idName, displayName); } // Read the constants to preserve any "unused" heal locations when writing the file later @@ -2623,7 +2701,7 @@ bool Project::readSongNames() { // Song names don't have a very useful order (esp. if we include SE_* values), so sort them alphabetically. // The default song should be the first in the list, not the first alphabetically, so save that before sorting. this->defaultSong = this->songNames.value(0, "0"); - this->songNames.sort(); + numericalModeSort(this->songNames); return true; } @@ -2763,7 +2841,7 @@ bool Project::readEventGraphics() { }; QString filepath = projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx_info); - const auto gfxInfos = parser.readCStructs(filepath, "", gfxInfoMemberMap); + auto gfxInfos = parser.readCStructs(filepath, "", gfxInfoMemberMap); QMap picTables = parser.readCArrayMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_pic_tables)); QMap graphicIncbins = parser.readCIncbinMulti(projectConfig.getFilePath(ProjectFilePath::data_obj_event_gfx)); @@ -2773,7 +2851,7 @@ bool Project::readEventGraphics() { if (!gfxInfos.contains(info_label)) continue; - const auto gfxInfoAttributes = gfxInfos[info_label]; + auto gfxInfoAttributes = gfxInfos[info_label]; auto eventGraphics = new EventGraphics; eventGraphics->inanimate = ParseUtil::gameStringToBool(gfxInfoAttributes.value("inanimate")); @@ -2903,14 +2981,6 @@ bool Project::readSpeciesIconPaths() { return true; } -void Project::setNewMapEvents(Map *map) { - map->events[Event::Group::Object].clear(); - map->events[Event::Group::Warp].clear(); - map->events[Event::Group::Heal].clear(); - map->events[Event::Group::Coord].clear(); - map->events[Event::Group::Bg].clear(); -} - int Project::getNumTilesPrimary() { return Project::num_tiles_primary; @@ -2952,9 +3022,9 @@ int Project::getMapDataSize(int width, int height) return (width + 15) * (height + 14); } -int Project::getDefaultMapSize() +int Project::getDefaultMapDimension() { - return Project::default_map_size; + return Project::default_map_dimension; } int Project::getMaxMapWidth() @@ -2976,11 +3046,11 @@ bool Project::calculateDefaultMapSize(){ int max = getMaxMapDataSize(); if (max >= getMapDataSize(20, 20)) { - default_map_size = 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_size = qFloor((qSqrt(4 * getMaxMapDataSize() + 1) - 29) / 2); + 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)) @@ -2996,11 +3066,20 @@ int Project::getMaxObjectEvents() return Project::max_object_events; } +QString Project::getEmptyMapDefineName() { + const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); + return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_empty); +} + QString Project::getDynamicMapDefineName() { const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_dynamic); } +QString Project::getDynamicMapName() { + return projectConfig.getIdentifier(ProjectIdentifier::symbol_dynamic_map_name); +} + // 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. @@ -3057,3 +3136,14 @@ bool Project::hasUnsavedChanges() { } return false; } + +// TODO: This belongs in a more general utility file, once we have one. +// Sometimes we want to sort names alphabetically to make them easier to find in large combo box lists. +// QStringList::sort (as of writing) can only sort numbers in lexical order, which has an undesirable +// effect (e.g. MAPSEC_ROUTE_10 comes after MAPSEC_ROUTE_1, rather than MAPSEC_ROUTE_9). +// We can use QCollator to sort these lists with better handling for numbers. +void Project::numericalModeSort(QStringList &list) { + QCollator collator; + collator.setNumericMode(true); + std::sort(list.begin(), list.end(), collator); +} diff --git a/src/scriptapi/apimap.cpp b/src/scriptapi/apimap.cpp index 98478f94..2532f132 100644 --- a/src/scriptapi/apimap.cpp +++ b/src/scriptapi/apimap.cpp @@ -337,7 +337,7 @@ void MainWindow::refreshAfterPaletteChange(Tileset *tileset) { this->editor->map_item->draw(true); this->editor->updateMapBorder(); this->editor->updateMapConnections(); - this->editor->project->saveTilesetPalettes(tileset); + tileset->savePalettes(); } void MainWindow::setTilesetPalette(Tileset *tileset, int paletteIndex, QList> colors) { @@ -576,14 +576,14 @@ void MainWindow::setSecondaryTileset(QString tileset) { void MainWindow::saveMetatilesByMetatileId(int metatileId) { Tileset * tileset = Tileset::getMetatileTileset(metatileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (this->editor->project && tileset) - this->editor->project->saveTilesetMetatiles(tileset); + if (tileset) + tileset->saveMetatiles(); } void MainWindow::saveMetatileAttributesByMetatileId(int metatileId) { Tileset * tileset = Tileset::getMetatileTileset(metatileId, this->editor->layout->tileset_primary, this->editor->layout->tileset_secondary); - if (this->editor->project && tileset) - this->editor->project->saveTilesetMetatileAttributes(tileset); + if (tileset) + tileset->saveMetatileAttributes(); // If the tileset editor is open it needs to be refreshed with the new changes. // Rather than do a full refresh (which is costly) we tell the editor it will need @@ -814,156 +814,132 @@ QJSValue MainWindow::getTilePixels(int tileId) { QString MainWindow::getSong() { if (!this->editor || !this->editor->map) return QString(); - return this->editor->map->song; + return this->editor->map->header()->song(); } void MainWindow::setSong(QString song) { - if (!this->ui || !this->editor || !this->editor->project) + if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->songNames.contains(song)) { - logError(QString("Unknown song '%1'").arg(song)); - return; - } - this->ui->comboBox_Song->setCurrentText(song); + this->editor->map->header()->setSong(song); } QString MainWindow::getLocation() { if (!this->editor || !this->editor->map) return QString(); - return this->editor->map->location; + return this->editor->map->header()->location(); } void MainWindow::setLocation(QString location) { - if (!this->ui || !this->editor || !this->editor->project) + if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->mapSectionIdNames.contains(location)) { - logError(QString("Unknown location '%1'").arg(location)); - return; - } - this->ui->comboBox_Location->setCurrentText(location); + this->editor->map->header()->setLocation(location); } bool MainWindow::getRequiresFlash() { if (!this->editor || !this->editor->map) return false; - return this->editor->map->requiresFlash; + return this->editor->map->header()->requiresFlash(); } void MainWindow::setRequiresFlash(bool require) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - this->ui->checkBox_Visibility->setChecked(require); + this->editor->map->header()->setRequiresFlash(require); } QString MainWindow::getWeather() { if (!this->editor || !this->editor->map) return QString(); - return this->editor->map->weather; + return this->editor->map->header()->weather(); } void MainWindow::setWeather(QString weather) { - if (!this->ui || !this->editor || !this->editor->project) + if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->weatherNames.contains(weather)) { - logError(QString("Unknown weather '%1'").arg(weather)); - return; - } - this->ui->comboBox_Weather->setCurrentText(weather); + this->editor->map->header()->setWeather(weather); } QString MainWindow::getType() { if (!this->editor || !this->editor->map) return QString(); - return this->editor->map->type; + return this->editor->map->header()->type(); } void MainWindow::setType(QString type) { - if (!this->ui || !this->editor || !this->editor->project) + if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->mapTypes.contains(type)) { - logError(QString("Unknown map type '%1'").arg(type)); - return; - } - this->ui->comboBox_Type->setCurrentText(type); + this->editor->map->header()->setType(type); } QString MainWindow::getBattleScene() { if (!this->editor || !this->editor->map) return QString(); - return this->editor->map->battle_scene; + return this->editor->map->header()->battleScene(); } void MainWindow::setBattleScene(QString battleScene) { - if (!this->ui || !this->editor || !this->editor->project) + if (!this->editor || !this->editor->map || !this->editor->project) return; - if (!this->editor->project->mapBattleScenes.contains(battleScene)) { - logError(QString("Unknown battle scene '%1'").arg(battleScene)); - return; - } - this->ui->comboBox_BattleScene->setCurrentText(battleScene); + this->editor->map->header()->setBattleScene(battleScene); } bool MainWindow::getShowLocationName() { if (!this->editor || !this->editor->map) return false; - return this->editor->map->show_location; + return this->editor->map->header()->showsLocationName(); } void MainWindow::setShowLocationName(bool show) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - this->ui->checkBox_ShowLocation->setChecked(show); + this->editor->map->header()->setShowsLocationName(show); } bool MainWindow::getAllowRunning() { if (!this->editor || !this->editor->map) return false; - return this->editor->map->allowRunning; + return this->editor->map->header()->allowsRunning(); } void MainWindow::setAllowRunning(bool allow) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - this->ui->checkBox_AllowRunning->setChecked(allow); + this->editor->map->header()->setAllowsRunning(allow); } bool MainWindow::getAllowBiking() { if (!this->editor || !this->editor->map) return false; - return this->editor->map->allowBiking; + return this->editor->map->header()->allowsBiking(); } void MainWindow::setAllowBiking(bool allow) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - this->ui->checkBox_AllowBiking->setChecked(allow); + this->editor->map->header()->setAllowsBiking(allow); } bool MainWindow::getAllowEscaping() { if (!this->editor || !this->editor->map) return false; - return this->editor->map->allowEscaping; + return this->editor->map->header()->allowsEscaping(); } void MainWindow::setAllowEscaping(bool allow) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - this->ui->checkBox_AllowEscaping->setChecked(allow); + this->editor->map->header()->setAllowsEscaping(allow); } int MainWindow::getFloorNumber() { if (!this->editor || !this->editor->map) return 0; - return this->editor->map->floorNumber; + return this->editor->map->header()->floorNumber(); } void MainWindow::setFloorNumber(int floorNumber) { - if (!this->ui) + if (!this->editor || !this->editor->map) return; - if (floorNumber < -128 || floorNumber > 127) { - logError(QString("Invalid floor number '%1'").arg(floorNumber)); - return; - } - this->ui->spinBox_FloorNumber->setValue(floorNumber); + this->editor->map->header()->setFloorNumber(floorNumber); } diff --git a/src/ui/colorinputwidget.cpp b/src/ui/colorinputwidget.cpp index 8b40be27..6ed587aa 100644 --- a/src/ui/colorinputwidget.cpp +++ b/src/ui/colorinputwidget.cpp @@ -1,16 +1,10 @@ #include "colorinputwidget.h" #include "ui_colorinputwidget.h" #include "colorpicker.h" +#include "validator.h" #include -class HexCodeValidator : public QValidator { - virtual QValidator::State validate(QString &input, int &) const override { - input = input.toUpper(); - return QValidator::Acceptable; - } -}; - static inline int rgb5(int rgb) { return round(static_cast(rgb * 31) / 255.0); } static inline int rgb8(int rgb) { return round(rgb * 255. / 31.); } static inline int gbaRed(int rgb) { return rgb & 0x1f; } @@ -43,8 +37,8 @@ void ColorInputWidget::init() { connect(ui->spinBox_Green, QOverload::of(&QSpinBox::valueChanged), this, &ColorInputWidget::setRgbFromSpinners); connect(ui->spinBox_Blue, QOverload::of(&QSpinBox::valueChanged), this, &ColorInputWidget::setRgbFromSpinners); - static const HexCodeValidator hexValidator; - ui->lineEdit_Hex->setValidator(&hexValidator); + static const UppercaseValidator uppercaseValidator; + ui->lineEdit_Hex->setValidator(&uppercaseValidator); connect(ui->lineEdit_Hex, &QLineEdit::textEdited, this, &ColorInputWidget::setRgbFromHexString); // We have separate signals for when color input editing finishes. diff --git a/src/ui/connectionpixmapitem.cpp b/src/ui/connectionpixmapitem.cpp index 35e07a15..d3ff13ae 100644 --- a/src/ui/connectionpixmapitem.cpp +++ b/src/ui/connectionpixmapitem.cpp @@ -66,7 +66,7 @@ QVariant ConnectionPixmapItem::itemChange(GraphicsItemChange change, const QVari // This is convoluted because of how our edit history works; this would otherwise just be 'this->connection->setOffset(newOffset);' if (this->connection->parentMap() && newOffset != this->connection->offset()) - this->connection->parentMap()->editHistory.push(new MapConnectionMove(this->connection, newOffset, this->actionId)); + this->connection->parentMap()->commit(new MapConnectionMove(this->connection, newOffset, this->actionId)); return QPointF(x, y); } diff --git a/src/ui/connectionslistitem.cpp b/src/ui/connectionslistitem.cpp index ccdf7e6c..dcff253c 100644 --- a/src/ui/connectionslistitem.cpp +++ b/src/ui/connectionslistitem.cpp @@ -79,24 +79,24 @@ void ConnectionsListItem::mousePressEvent(QMouseEvent *) { void ConnectionsListItem::on_comboBox_Direction_currentTextChanged(QString direction) { this->setSelected(true); if (this->map) - this->map->editHistory.push(new MapConnectionChangeDirection(this->connection, direction)); + this->map->commit(new MapConnectionChangeDirection(this->connection, direction)); } void ConnectionsListItem::on_comboBox_Map_currentTextChanged(QString mapName) { this->setSelected(true); if (this->map && ui->comboBox_Map->findText(mapName) >= 0) - this->map->editHistory.push(new MapConnectionChangeMap(this->connection, mapName)); + this->map->commit(new MapConnectionChangeMap(this->connection, mapName)); } void ConnectionsListItem::on_spinBox_Offset_valueChanged(int offset) { this->setSelected(true); if (this->map) - this->map->editHistory.push(new MapConnectionMove(this->connection, offset, this->actionId)); + this->map->commit(new MapConnectionMove(this->connection, offset, this->actionId)); } void ConnectionsListItem::on_button_Delete_clicked() { if (this->map) - this->map->editHistory.push(new MapConnectionRemove(this->map, this->connection)); + this->map->commit(new MapConnectionRemove(this->map, this->connection)); } void ConnectionsListItem::on_button_OpenMap_clicked() { diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp index 284ea333..c3c412c2 100644 --- a/src/ui/customscriptseditor.cpp +++ b/src/ui/customscriptseditor.cpp @@ -185,7 +185,7 @@ void CustomScriptsEditor::displayNewScript(QString filepath) { // Verify new script path is not already in list for (int i = 0; i < ui->list->count(); i++) { if (filepath == this->getScriptFilepath(ui->list->item(i), false)) { - QMessageBox::information(this, "", QString("The script '%1' is already loaded").arg(filepath)); + QMessageBox::information(this, QApplication::applicationName(), QString("The script '%1' is already loaded").arg(filepath)); return; } } @@ -219,7 +219,7 @@ void CustomScriptsEditor::openScript(QListWidgetItem * item) { const QString path = this->getScriptFilepath(item); QFileInfo fileInfo(path); if (!fileInfo.exists() || !fileInfo.isFile()){ - QMessageBox::warning(this, "", QString("Failed to open script '%1'").arg(path)); + QMessageBox::warning(this, QApplication::applicationName(), QString("Failed to open script '%1'").arg(path)); return; } Editor::openInTextEditor(path); diff --git a/src/ui/draggablepixmapitem.cpp b/src/ui/draggablepixmapitem.cpp index 59b069dd..78a00bb6 100644 --- a/src/ui/draggablepixmapitem.cpp +++ b/src/ui/draggablepixmapitem.cpp @@ -91,7 +91,7 @@ void DraggablePixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *mouse) { } else { selectedEvents.append(this->event); } - editor->map->editHistory.push(new EventMove(selectedEvents, moveDistance.x(), moveDistance.y(), currentActionId)); + editor->map->commit(new EventMove(selectedEvents, moveDistance.x(), moveDistance.y(), currentActionId)); this->releaseSelectionQueued = false; } diff --git a/src/ui/eventframes.cpp b/src/ui/eventframes.cpp index e90658a2..cb06fea1 100644 --- a/src/ui/eventframes.cpp +++ b/src/ui/eventframes.cpp @@ -108,7 +108,7 @@ void EventFrame::connectSignals(MainWindow *) { connect(this->spinner_x, QOverload::of(&QSpinBox::valueChanged), [this](int value) { int delta = value - event->getX(); if (delta) { - this->event->getMap()->editHistory.push(new EventMove(QList() << this->event, delta, 0, this->spinner_x->getActionId())); + this->event->getMap()->commit(new EventMove(QList() << this->event, delta, 0, this->spinner_x->getActionId())); } }); @@ -118,7 +118,7 @@ void EventFrame::connectSignals(MainWindow *) { connect(this->spinner_y, QOverload::of(&QSpinBox::valueChanged), [this](int value) { int delta = value - event->getY(); if (delta) { - this->event->getMap()->editHistory.push(new EventMove(QList() << this->event, 0, delta, this->spinner_y->getActionId())); + this->event->getMap()->commit(new EventMove(QList() << this->event, 0, delta, this->spinner_y->getActionId())); } }); connect(this->event->getPixmapItem(), &DraggablePixmapItem::yChanged, this->spinner_y, &NoScrollSpinBox::setValue); diff --git a/src/ui/mapheaderform.cpp b/src/ui/mapheaderform.cpp new file mode 100644 index 00000000..09cb22c7 --- /dev/null +++ b/src/ui/mapheaderform.cpp @@ -0,0 +1,226 @@ +#include "mapheaderform.h" +#include "ui_mapheaderform.h" +#include "project.h" + +MapHeaderForm::MapHeaderForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::MapHeaderForm) +{ + ui->setupUi(this); + + // This value is an s8 by default, but we don't need to unnecessarily limit users. + ui->spinBox_FloorNumber->setMinimum(INT_MIN); + ui->spinBox_FloorNumber->setMaximum(INT_MAX); + + // The layout for this UI keeps fields at their size hint, which is a little short for the line edit. + ui->lineEdit_LocationName->setMinimumWidth(ui->comboBox_Location->sizeHint().width()); + + connect(ui->comboBox_Song, &QComboBox::currentTextChanged, this, &MapHeaderForm::onSongUpdated); + connect(ui->comboBox_Location, &QComboBox::currentTextChanged, this, &MapHeaderForm::onLocationChanged); + connect(ui->comboBox_Weather, &QComboBox::currentTextChanged, this, &MapHeaderForm::onWeatherChanged); + connect(ui->comboBox_Type, &QComboBox::currentTextChanged, this, &MapHeaderForm::onTypeChanged); + connect(ui->comboBox_BattleScene, &QComboBox::currentTextChanged, this, &MapHeaderForm::onBattleSceneChanged); + + connect(ui->checkBox_RequiresFlash, &QCheckBox::stateChanged, this, &MapHeaderForm::onRequiresFlashChanged); + connect(ui->checkBox_ShowLocationName, &QCheckBox::stateChanged, this, &MapHeaderForm::onShowLocationNameChanged); + connect(ui->checkBox_AllowRunning, &QCheckBox::stateChanged, this, &MapHeaderForm::onAllowRunningChanged); + connect(ui->checkBox_AllowBiking, &QCheckBox::stateChanged, this, &MapHeaderForm::onAllowBikingChanged); + connect(ui->checkBox_AllowEscaping, &QCheckBox::stateChanged, this, &MapHeaderForm::onAllowEscapingChanged); + + connect(ui->spinBox_FloorNumber, QOverload::of(&QSpinBox::valueChanged), this, &MapHeaderForm::onFloorNumberChanged); + + connect(ui->lineEdit_LocationName, &QLineEdit::textChanged, this, &MapHeaderForm::onLocationNameChanged); +} + +MapHeaderForm::~MapHeaderForm() +{ + delete ui; +} + +void MapHeaderForm::setProject(Project * project, bool allowChanges) { + clear(); + + if (m_project) { + m_project->disconnect(this); + } + m_project = project; + m_allowProjectChanges = allowChanges; + + if (!m_project) + return; + + // Populate combo boxes + + const QSignalBlocker b_Song(ui->comboBox_Song); + ui->comboBox_Song->clear(); + ui->comboBox_Song->addItems(m_project->songNames); + + const QSignalBlocker b_Weather(ui->comboBox_Weather); + ui->comboBox_Weather->clear(); + ui->comboBox_Weather->addItems(m_project->weatherNames); + + const QSignalBlocker b_Type(ui->comboBox_Type); + ui->comboBox_Type->clear(); + ui->comboBox_Type->addItems(m_project->mapTypes); + + const QSignalBlocker b_BattleScene(ui->comboBox_BattleScene); + ui->comboBox_BattleScene->clear(); + ui->comboBox_BattleScene->addItems(m_project->mapBattleScenes); + + const QSignalBlocker b_Locations(ui->comboBox_Location); + ui->comboBox_Location->clear(); + ui->comboBox_Location->addItems(m_project->mapSectionIdNames); + + // Hide config-specific settings + + bool hasFlags = projectConfig.mapAllowFlagsEnabled; + ui->checkBox_AllowRunning->setVisible(hasFlags); + ui->checkBox_AllowBiking->setVisible(hasFlags); + ui->checkBox_AllowEscaping->setVisible(hasFlags); + ui->label_AllowRunning->setVisible(hasFlags); + ui->label_AllowBiking->setVisible(hasFlags); + ui->label_AllowEscaping->setVisible(hasFlags); + + bool floorNumEnabled = projectConfig.floorNumberEnabled; + ui->spinBox_FloorNumber->setVisible(floorNumEnabled); + ui->label_FloorNumber->setVisible(floorNumEnabled); + + // If the project changes any of the displayed data, update it accordingly. + connect(m_project, &Project::mapSectionIdNamesChanged, this, &MapHeaderForm::setLocations); + connect(m_project, &Project::mapSectionDisplayNameChanged, this, &MapHeaderForm::updateLocationName); +} + +void MapHeaderForm::setLocations(const QStringList &locations) { + const QSignalBlocker b(ui->comboBox_Location); + const QString before = ui->comboBox_Location->currentText(); + ui->comboBox_Location->clear(); + ui->comboBox_Location->addItems(locations); + ui->comboBox_Location->setCurrentText(before); +} + +// Assign a MapHeader that the form will keep in sync with the UI. +void MapHeaderForm::setHeader(MapHeader *header) { + if (m_header == header) + return; + + if (m_header) { + m_header->disconnect(this); + } + m_header = header; + + if (!m_header) { + clear(); + return; + } + + // If the MapHeader is changed externally (for example, with the scripting API) update the UI accordingly + connect(m_header, &MapHeader::songChanged, this, &MapHeaderForm::setSong); + connect(m_header, &MapHeader::locationChanged, this, &MapHeaderForm::setLocation); + connect(m_header, &MapHeader::requiresFlashChanged, this, &MapHeaderForm::setRequiresFlash); + connect(m_header, &MapHeader::weatherChanged, this, &MapHeaderForm::setWeather); + connect(m_header, &MapHeader::typeChanged, this, &MapHeaderForm::setType); + connect(m_header, &MapHeader::battleSceneChanged, this, &MapHeaderForm::setBattleScene); + connect(m_header, &MapHeader::showsLocationNameChanged, this, &MapHeaderForm::setShowsLocationName); + connect(m_header, &MapHeader::allowsRunningChanged, this, &MapHeaderForm::setAllowsRunning); + connect(m_header, &MapHeader::allowsBikingChanged, this, &MapHeaderForm::setAllowsBiking); + connect(m_header, &MapHeader::allowsEscapingChanged, this, &MapHeaderForm::setAllowsEscaping); + connect(m_header, &MapHeader::floorNumberChanged, this, &MapHeaderForm::setFloorNumber); + + // Immediately update the UI to reflect the assigned MapHeader + setHeaderData(*m_header); +} + +void MapHeaderForm::clear() { + m_header = nullptr; + setHeaderData(MapHeader()); +} + +void MapHeaderForm::setHeaderData(const MapHeader &header) { + setSong(header.song()); + setLocation(header.location()); + setRequiresFlash(header.requiresFlash()); + setWeather(header.weather()); + setType(header.type()); + setBattleScene(header.battleScene()); + setShowsLocationName(header.showsLocationName()); + setAllowsRunning(header.allowsRunning()); + setAllowsBiking(header.allowsBiking()); + setAllowsEscaping(header.allowsEscaping()); + setFloorNumber(header.floorNumber()); + updateLocationName(); +} + +MapHeader MapHeaderForm::headerData() const { + if (m_header) + return *m_header; + + // Build header from UI + MapHeader header; + header.setSong(song()); + header.setLocation(location()); + header.setRequiresFlash(requiresFlash()); + header.setWeather(weather()); + header.setType(type()); + header.setBattleScene(battleScene()); + header.setShowsLocationName(showsLocationName()); + header.setAllowsRunning(allowsRunning()); + header.setAllowsBiking(allowsBiking()); + header.setAllowsEscaping(allowsEscaping()); + header.setFloorNumber(floorNumber()); + return header; +} + +void MapHeaderForm::updateLocationName() { + setLocationName(m_project ? m_project->getMapsecDisplayName(location()) : QString()); +} + +// 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::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::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); } + +// Read data from UI +QString MapHeaderForm::song() const { return ui->comboBox_Song->currentText(); } +QString MapHeaderForm::location() const { return ui->comboBox_Location->currentText(); } +QString MapHeaderForm::locationName() const { return ui->lineEdit_LocationName->text(); } +bool MapHeaderForm::requiresFlash() const { return ui->checkBox_RequiresFlash->isChecked(); } +QString MapHeaderForm::weather() const { return ui->comboBox_Weather->currentText(); } +QString MapHeaderForm::type() const { return ui->comboBox_Type->currentText(); } +QString MapHeaderForm::battleScene() const { return ui->comboBox_BattleScene->currentText(); } +bool MapHeaderForm::showsLocationName() const { return ui->checkBox_ShowLocationName->isChecked(); } +bool MapHeaderForm::allowsRunning() const { return ui->checkBox_AllowRunning->isChecked(); } +bool MapHeaderForm::allowsBiking() const { return ui->checkBox_AllowBiking->isChecked(); } +bool MapHeaderForm::allowsEscaping() const { return ui->checkBox_AllowEscaping->isChecked(); } +int MapHeaderForm::floorNumber() const { return ui->spinBox_FloorNumber->value(); } + +// Send changes in UI to tracked MapHeader (if there is one) +void MapHeaderForm::onSongUpdated(const QString &song) { if (m_header) m_header->setSong(song); } +void MapHeaderForm::onWeatherChanged(const QString &weather) { if (m_header) m_header->setWeather(weather); } +void MapHeaderForm::onTypeChanged(const QString &type) { if (m_header) m_header->setType(type); } +void MapHeaderForm::onBattleSceneChanged(const QString &battleScene) { if (m_header) m_header->setBattleScene(battleScene); } +void MapHeaderForm::onRequiresFlashChanged(int selected) { if (m_header) m_header->setRequiresFlash(selected == Qt::Checked); } +void MapHeaderForm::onShowLocationNameChanged(int selected) { if (m_header) m_header->setShowsLocationName(selected == Qt::Checked); } +void MapHeaderForm::onAllowRunningChanged(int selected) { if (m_header) m_header->setAllowsRunning(selected == Qt::Checked); } +void MapHeaderForm::onAllowBikingChanged(int selected) { if (m_header) m_header->setAllowsBiking(selected == Qt::Checked); } +void MapHeaderForm::onAllowEscapingChanged(int selected) { if (m_header) m_header->setAllowsEscaping(selected == Qt::Checked); } +void MapHeaderForm::onFloorNumberChanged(int offset) { if (m_header) m_header->setFloorNumber(offset); } +void MapHeaderForm::onLocationChanged(const QString &location) { + if (m_header) m_header->setLocation(location); + updateLocationName(); +} +void MapHeaderForm::onLocationNameChanged(const QString &locationName) { + if (m_project && m_allowProjectChanges) { + // The location name is actually part of the project, not the map header. + // If the field is changed in the UI we can push these changes to the project. + m_project->setMapsecDisplayName(location(), locationName); + } +} diff --git a/src/ui/mapimageexporter.cpp b/src/ui/mapimageexporter.cpp index a7a26629..4596ba36 100644 --- a/src/ui/mapimageexporter.cpp +++ b/src/ui/mapimageexporter.cpp @@ -36,25 +36,25 @@ QString getDescription(ImageExporterMode mode) { return ""; } -MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExporterMode mode) : - QDialog(parent_), +MapImageExporter::MapImageExporter(QWidget *parent, Editor *editor, ImageExporterMode mode) : + QDialog(parent), ui(new Ui::MapImageExporter) { - this->setAttribute(Qt::WA_DeleteOnClose); + setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - this->map = editor_->map; - this->layout = editor_->layout; - this->editor = editor_; - this->mode = mode; - this->setWindowTitle(getTitle(this->mode)); - this->ui->label_Description->setText(getDescription(this->mode)); - this->ui->groupBox_Connections->setVisible(this->mode != ImageExporterMode::Stitch); - this->ui->groupBox_Timelapse->setVisible(this->mode == ImageExporterMode::Timelapse); + m_map = editor->map; + m_layout = editor->layout; + m_editor = editor; + m_mode = mode; + setWindowTitle(getTitle(m_mode)); + ui->label_Description->setText(getDescription(m_mode)); + ui->groupBox_Connections->setVisible(m_mode != ImageExporterMode::Stitch); + ui->groupBox_Timelapse->setVisible(m_mode == ImageExporterMode::Timelapse); - if (this->map) { - this->ui->comboBox_MapSelection->addItems(editor->project->mapNames); - this->ui->comboBox_MapSelection->setCurrentText(map->name); - this->ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down + if (m_map) { + ui->comboBox_MapSelection->addItems(editor->project->mapNames); + ui->comboBox_MapSelection->setCurrentText(m_map->name()); + ui->comboBox_MapSelection->setEnabled(false);// TODO: allow selecting map from drop-down } connect(ui->pushButton_Save, &QPushButton::pressed, this, &MapImageExporter::saveImage); @@ -62,7 +62,7 @@ MapImageExporter::MapImageExporter(QWidget *parent_, Editor *editor_, ImageExpor } MapImageExporter::~MapImageExporter() { - delete scene; + delete m_scene; delete ui; } @@ -80,48 +80,49 @@ void MapImageExporter::resizeEvent(QResizeEvent *event) { void MapImageExporter::saveImage() { // Make sure preview is up-to-date before we save. - if (this->preview.isNull()) + if (m_preview.isNull()) updatePreview(); - if (this->preview.isNull()) + if (m_preview.isNull()) return; - QString title = getTitle(this->mode); + const QString title = getTitle(m_mode); + const QString itemName = m_map ? m_map->name() : m_layout->name; QString defaultFilename; - switch (this->mode) + switch (m_mode) { case ImageExporterMode::Normal: - defaultFilename = this->map? this->map->name : this->layout->name; + defaultFilename = itemName; break; case ImageExporterMode::Stitch: - defaultFilename = QString("Stitch_From_%1").arg(this->map? this->map->name : this->layout->name); + defaultFilename = QString("Stitch_From_%1").arg(itemName); break; case ImageExporterMode::Timelapse: - defaultFilename = QString("Timelapse_%1").arg(this->map? this->map->name : this->layout->name); + defaultFilename = QString("Timelapse_%1").arg(itemName); break; } QString defaultFilepath = QString("%1/%2.%3") .arg(FileDialog::getDirectory()) .arg(defaultFilename) - .arg(this->mode == ImageExporterMode::Timelapse ? "gif" : "png"); - QString filter = this->mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)"; + .arg(m_mode == ImageExporterMode::Timelapse ? "gif" : "png"); + QString filter = m_mode == ImageExporterMode::Timelapse ? "Image Files (*.gif)" : "Image Files (*.png *.jpg *.bmp)"; QString filepath = FileDialog::getSaveFileName(this, title, defaultFilepath, filter); if (!filepath.isEmpty()) { - switch (this->mode) { + switch (m_mode) { case ImageExporterMode::Normal: case ImageExporterMode::Stitch: // Normal and Stitch modes already have the image ready to go in the preview. - this->preview.save(filepath); + m_preview.save(filepath); break; case ImageExporterMode::Timelapse: // Timelapse will play in order of layout changes then map changes (events) // TODO: potentially update in the future? QGifImage timelapseImg; - timelapseImg.setDefaultDelay(this->settings.timelapseDelayMs); + timelapseImg.setDefaultDelay(m_settings.timelapseDelayMs); timelapseImg.setDefaultTransparentColor(QColor(0, 0, 0)); // lambda to avoid redundancy - auto generateTimelapseFromHistory = [this, &timelapseImg](QString progressText, QUndoStack &historyStack){ + auto generateTimelapseFromHistory = [this, &timelapseImg](QString progressText, QUndoStack *historyStack){ QProgressDialog progress(progressText, "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); @@ -129,20 +130,20 @@ void MapImageExporter::saveImage() { progress.setMaximum(1); progress.setValue(0); - int maxWidth = this->layout->getWidth() * 16; - int maxHeight = this->layout->getHeight() * 16; - if (this->settings.showBorder) { + int maxWidth = m_layout->getWidth() * 16; + int maxHeight = m_layout->getHeight() * 16; + if (m_settings.showBorder) { maxWidth += 2 * STITCH_MODE_BORDER_DISTANCE * 16; maxHeight += 2 * STITCH_MODE_BORDER_DISTANCE * 16; } // Rewind to the specified start of the map edit history. int i = 0; - while (historyStack.canUndo()) { + while (historyStack->canUndo()) { progress.setValue(i); - historyStack.undo(); - int width = this->layout->getWidth() * 16; - int height = this->layout->getHeight() * 16; - if (this->settings.showBorder) { + historyStack->undo(); + int width = m_layout->getWidth() * 16; + int height = m_layout->getHeight() * 16; + if (m_settings.showBorder) { width += 2 * STITCH_MODE_BORDER_DISTANCE * 16; height += 2 * STITCH_MODE_BORDER_DISTANCE * 16; } @@ -161,19 +162,19 @@ void MapImageExporter::saveImage() { while (i > 0) { if (progress.wasCanceled()) { progress.close(); - while (i > 0 && historyStack.canRedo()) { + while (i > 0 && historyStack->canRedo()) { i--; - historyStack.redo(); + historyStack->redo(); } return; } - while (historyStack.canRedo() && - !historyItemAppliesToFrame(historyStack.command(historyStack.index()))) { + while (historyStack->canRedo() && + !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { i--; - historyStack.redo(); + historyStack->redo(); } progress.setValue(progress.maximum() - i); - QPixmap pixmap = this->getFormattedMapPixmap(this->map); + QPixmap pixmap = getFormattedMapPixmap(); if (pixmap.width() < maxWidth || pixmap.height() < maxHeight) { QPixmap pixmap2 = QPixmap(maxWidth, maxHeight); QPainter painter(&pixmap2); @@ -183,34 +184,34 @@ void MapImageExporter::saveImage() { pixmap = pixmap2; } timelapseImg.addFrame(pixmap.toImage()); - for (int j = 0; j < this->settings.timelapseSkipAmount; j++) { + for (int j = 0; j < m_settings.timelapseSkipAmount; j++) { if (i > 0) { i--; - historyStack.redo(); - while (historyStack.canRedo() && - !historyItemAppliesToFrame(historyStack.command(historyStack.index()))) { + historyStack->redo(); + while (historyStack->canRedo() && + !historyItemAppliesToFrame(historyStack->command(historyStack->index()))) { i--; - historyStack.redo(); + historyStack->redo(); } } } } // The latest map state is the last animated frame. - QPixmap pixmap = this->getFormattedMapPixmap(this->map); + QPixmap pixmap = getFormattedMapPixmap(); timelapseImg.addFrame(pixmap.toImage()); progress.close(); }; - if (this->layout) - generateTimelapseFromHistory("Building layout timelapse...", this->layout->editHistory); + if (m_layout) + generateTimelapseFromHistory("Building layout timelapse...", &m_layout->editHistory); - if (this->map) - generateTimelapseFromHistory("Building map timelapse...", this->map->editHistory); + if (m_map) + generateTimelapseFromHistory("Building map timelapse...", m_map->editHistory()); timelapseImg.save(filepath); break; } - this->close(); + close(); } } @@ -229,26 +230,26 @@ bool MapImageExporter::historyItemAppliesToFrame(const QUndoCommand *command) { case CommandId::ID_PaintCollision: case CommandId::ID_BucketFillCollision: case CommandId::ID_MagicFillCollision: - return this->settings.showCollision; + return m_settings.showCollision; case CommandId::ID_PaintBorder: - return this->settings.showBorder; + return m_settings.showBorder; case CommandId::ID_MapConnectionMove: case CommandId::ID_MapConnectionChangeDirection: case CommandId::ID_MapConnectionChangeMap: case CommandId::ID_MapConnectionAdd: case CommandId::ID_MapConnectionRemove: - return this->settings.showUpConnections || this->settings.showDownConnections || this->settings.showLeftConnections || this->settings.showRightConnections; + return m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections; case CommandId::ID_EventMove: case CommandId::ID_EventShift: case CommandId::ID_EventCreate: case CommandId::ID_EventDelete: case CommandId::ID_EventDuplicate: { bool eventTypeIsApplicable = - (this->settings.showObjects && (command->id() & IDMask_EventType_Object) != 0) - || (this->settings.showWarps && (command->id() & IDMask_EventType_Warp) != 0) - || (this->settings.showBGs && (command->id() & IDMask_EventType_BG) != 0) - || (this->settings.showTriggers && (command->id() & IDMask_EventType_Trigger) != 0) - || (this->settings.showHealLocations && (command->id() & IDMask_EventType_Heal) != 0); + (m_settings.showObjects && (command->id() & IDMask_EventType_Object) != 0) + || (m_settings.showWarps && (command->id() & IDMask_EventType_Warp) != 0) + || (m_settings.showBGs && (command->id() & IDMask_EventType_BG) != 0) + || (m_settings.showTriggers && (command->id() & IDMask_EventType_Trigger) != 0) + || (m_settings.showHealLocations && (command->id() & IDMask_EventType_Heal) != 0); return eventTypeIsApplicable; } default: @@ -268,7 +269,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu QSet visited; QList stitchedMaps; QList unvisited; - unvisited.append(StitchedMap{0, 0, this->editor->map}); + unvisited.append(StitchedMap{0, 0, m_editor->map}); progress->setLabelText("Gathering stitched maps..."); while (!unvisited.isEmpty()) { @@ -279,9 +280,9 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu progress->setValue(visited.size()); StitchedMap cur = unvisited.takeFirst(); - if (visited.contains(cur.map->name)) + if (visited.contains(cur.map->name())) continue; - visited.insert(cur.map->name); + visited.insert(cur.map->name()); stitchedMaps.append(cur); for (MapConnection *connection : cur.map->getConnections()) { @@ -361,7 +362,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu pixelX -= STITCH_MODE_BORDER_DISTANCE * 16; pixelY -= STITCH_MODE_BORDER_DISTANCE * 16; } - QPixmap pixmap = this->getFormattedMapPixmap(map.map); + QPixmap pixmap = getFormattedMapPixmap(map.map); painter.drawPixmap(pixelX, pixelY, pixmap); } @@ -382,7 +383,7 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu int pixelX = (map.x - minX) * 16; int pixelY = (map.y - minY) * 16; - QPixmap pixmapWithoutBorders = this->getFormattedMapPixmap(map.map, true); + QPixmap pixmapWithoutBorders = getFormattedMapPixmap(map.map, true); painter.drawPixmap(pixelX, pixelY, pixmapWithoutBorders); } } @@ -391,56 +392,50 @@ QPixmap MapImageExporter::getStitchedImage(QProgressDialog *progress, bool inclu } void MapImageExporter::updatePreview() { - if (this->scene) { - delete this->scene; - this->scene = nullptr; + if (m_scene) { + delete m_scene; + m_scene = nullptr; } - this->scene = new QGraphicsScene; + m_scene = new QGraphicsScene; - if (this->mode == ImageExporterMode::Stitch) { + if (m_mode == ImageExporterMode::Stitch) { QProgressDialog progress("Building map stitch...", "Cancel", 0, 1, this); progress.setAutoClose(true); progress.setWindowModality(Qt::WindowModal); progress.setModal(true); progress.setMinimumDuration(1000); - this->preview = getStitchedImage(&progress, this->settings.showBorder); + m_preview = getStitchedImage(&progress, m_settings.showBorder); progress.close(); } else { // Timelapse mode doesn't currently have a real preview. It just displays the current map as in Normal mode. - this->preview = getFormattedMapPixmap(this->map); + m_preview = getFormattedMapPixmap(); } - this->scene->addPixmap(this->preview); - ui->graphicsView_Preview->setScene(scene); + m_scene->addPixmap(m_preview); + ui->graphicsView_Preview->setScene(m_scene); scalePreview(); } void MapImageExporter::scalePreview() { - if (this->scene && !this->settings.previewActualSize){ - ui->graphicsView_Preview->fitInView(this->scene->sceneRect(), Qt::KeepAspectRatioByExpanding); + if (m_scene && !m_settings.previewActualSize){ + ui->graphicsView_Preview->fitInView(m_scene->sceneRect(), Qt::KeepAspectRatioByExpanding); } } -// THIS -QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { - QPixmap pixmap; +QPixmap MapImageExporter::getFormattedMapPixmap() { + return m_map ? getFormattedMapPixmap(m_map) : getFormattedLayoutPixmap(m_layout); +} - Layout *layout; +QPixmap MapImageExporter::getFormattedLayoutPixmap(Layout *layout, bool ignoreBorder, bool ignoreGrid) { + if (!layout) + return QPixmap(); - // draw background layer / base image - if (!this->map) { - layout = this->layout; - layout->render(true); - pixmap = layout->pixmap; - } else { - layout = map->layout; - map->layout->render(true); - pixmap = map->layout->pixmap; - } + layout->render(true); + QPixmap pixmap = layout->pixmap; - if (this->settings.showCollision) { + if (m_settings.showCollision) { QPainter collisionPainter(&pixmap); layout->renderCollision(true); - collisionPainter.setOpacity(editor->collisionOpacity); + collisionPainter.setOpacity(m_editor->collisionOpacity); collisionPainter.drawPixmap(0, 0, layout->collision_pixmap); collisionPainter.end(); } @@ -448,11 +443,11 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { // draw map border // note: this will break when allowing map to be selected from drop down maybe int borderHeight = 0, borderWidth = 0; - if (!ignoreBorder && this->settings.showBorder) { - int borderDistance = this->mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; + if (!ignoreBorder && m_settings.showBorder) { + int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; layout->renderBorder(); - int borderHorzDist = editor->getBorderDrawDistance(layout->getBorderWidth()); - int borderVertDist = editor->getBorderDrawDistance(layout->getBorderHeight()); + int borderHorzDist = m_editor->getBorderDrawDistance(layout->getBorderWidth()); + int borderVertDist = m_editor->getBorderDrawDistance(layout->getBorderHeight()); borderWidth = borderDistance * 16; borderHeight = borderDistance * 16; QPixmap newPixmap = QPixmap(layout->pixmap.width() + borderWidth * 2, layout->pixmap.height() + borderHeight * 2); @@ -467,20 +462,34 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { pixmap = newPixmap; } - if (!this->map) { - return pixmap; - } + // The grid should be painted last, so if this layout pixmap is being painted + // as part of a map (which has more to paint after this) then don't paint the grid yet. + if (!ignoreGrid) + paintGrid(&pixmap, ignoreBorder); - if (!ignoreBorder && (this->settings.showUpConnections || this->settings.showDownConnections || this->settings.showLeftConnections || this->settings.showRightConnections)) { + return pixmap; +} + +QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { + if (!map) + return QPixmap(); + + QPixmap pixmap = getFormattedLayoutPixmap(map->layout(), ignoreBorder, true); + + if (!ignoreBorder && (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections)) { // if showing connections, draw on outside of image QPainter connectionPainter(&pixmap); + + int borderDistance = m_mode ? STITCH_MODE_BORDER_DISTANCE : BORDER_DISTANCE; + int borderWidth = borderDistance * 16; + int borderHeight = borderDistance * 16; // TODO: Reading the connections from the editor and not 'map' is incorrect. - for (auto connectionItem : editor->connection_items) { + for (auto connectionItem : m_editor->connection_items) { const QString direction = connectionItem->connection->direction(); - if ((this->settings.showUpConnections && direction == "up") - || (this->settings.showDownConnections && direction == "down") - || (this->settings.showLeftConnections && direction == "left") - || (this->settings.showRightConnections && direction == "right")) + if ((m_settings.showUpConnections && direction == "up") + || (m_settings.showDownConnections && direction == "down") + || (m_settings.showLeftConnections && direction == "left") + || (m_settings.showRightConnections && direction == "right")) connectionPainter.drawImage(connectionItem->x() + borderWidth, connectionItem->y() + borderHeight, connectionItem->connection->getPixmap().toImage()); } @@ -488,37 +497,43 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { } // draw events - if (this->settings.showObjects || this->settings.showWarps || this->settings.showBGs || this->settings.showTriggers || this->settings.showHealLocations) { + if (m_settings.showObjects || m_settings.showWarps || m_settings.showBGs || m_settings.showTriggers || m_settings.showHealLocations) { QPainter eventPainter(&pixmap); int pixelOffset = 0; - if (!ignoreBorder && this->settings.showBorder) { - pixelOffset = this->mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; + if (!ignoreBorder && m_settings.showBorder) { + pixelOffset = m_mode == ImageExporterMode::Normal ? BORDER_DISTANCE * 16 : STITCH_MODE_BORDER_DISTANCE * 16; } - const QList events = map->getAllEvents(); + const QList events = map->getEvents(); for (const auto &event : events) { Event::Group group = event->getEventGroup(); - if ((this->settings.showObjects && group == Event::Group::Object) - || (this->settings.showWarps && group == Event::Group::Warp) - || (this->settings.showBGs && group == Event::Group::Bg) - || (this->settings.showTriggers && group == Event::Group::Coord) - || (this->settings.showHealLocations && group == Event::Group::Heal)) { - editor->project->setEventPixmap(event); + if ((m_settings.showObjects && group == Event::Group::Object) + || (m_settings.showWarps && group == Event::Group::Warp) + || (m_settings.showBGs && group == Event::Group::Bg) + || (m_settings.showTriggers && group == Event::Group::Coord) + || (m_settings.showHealLocations && group == Event::Group::Heal)) { + m_editor->project->setEventPixmap(event); eventPainter.drawImage(QPoint(event->getPixelX() + pixelOffset, event->getPixelY() + pixelOffset), event->getPixmap().toImage()); } } eventPainter.end(); } + paintGrid(&pixmap, ignoreBorder); + return pixmap; +} + +void MapImageExporter::paintGrid(QPixmap *pixmap, bool ignoreBorder) { // draw grid directly onto the pixmap // since the last grid lines are outside of the pixmap, add a pixel to the bottom and right - if (this->settings.showGrid) { + if (m_settings.showGrid) { + bool hasBorder = !ignoreBorder && m_settings.showBorder; int addX = 1, addY = 1; - if (borderHeight) addY = 0; - if (borderWidth) addX = 0; + if (hasBorder) addY = 0; + if (hasBorder) addX = 0; - QPixmap newPixmap= QPixmap(pixmap.width() + addX, pixmap.height() + addY); + QPixmap newPixmap= QPixmap(pixmap->width() + addX, pixmap->height() + addY); QPainter gridPainter(&newPixmap); - gridPainter.drawImage(QPoint(0, 0), pixmap.toImage()); + gridPainter.drawImage(QPoint(0, 0), pixmap->toImage()); for (int x = 0; x < newPixmap.width(); x += 16) { gridPainter.drawLine(x, 0, x, newPixmap.height()); } @@ -526,58 +541,56 @@ QPixmap MapImageExporter::getFormattedMapPixmap(Map *map, bool ignoreBorder) { gridPainter.drawLine(0, y, newPixmap.width(), y); } gridPainter.end(); - pixmap = newPixmap; + *pixmap = newPixmap; } - - return pixmap; } void MapImageExporter::updateShowBorderState() { // If any of the Connections settings are enabled then this setting is locked (it's implicitly enabled) - bool on = (this->settings.showUpConnections || this->settings.showDownConnections || this->settings.showLeftConnections || this->settings.showRightConnections); + bool on = (m_settings.showUpConnections || m_settings.showDownConnections || m_settings.showLeftConnections || m_settings.showRightConnections); const QSignalBlocker blocker(ui->checkBox_Border); ui->checkBox_Border->setChecked(on); ui->checkBox_Border->setDisabled(on); - this->settings.showBorder = on; + m_settings.showBorder = on; } void MapImageExporter::on_checkBox_Elevation_stateChanged(int state) { - this->settings.showCollision = (state == Qt::Checked); + m_settings.showCollision = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Grid_stateChanged(int state) { - this->settings.showGrid = (state == Qt::Checked); + m_settings.showGrid = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Border_stateChanged(int state) { - this->settings.showBorder = (state == Qt::Checked); + m_settings.showBorder = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Objects_stateChanged(int state) { - this->settings.showObjects = (state == Qt::Checked); + m_settings.showObjects = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Warps_stateChanged(int state) { - this->settings.showWarps = (state == Qt::Checked); + m_settings.showWarps = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_BGs_stateChanged(int state) { - this->settings.showBGs = (state == Qt::Checked); + m_settings.showBGs = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_Triggers_stateChanged(int state) { - this->settings.showTriggers = (state == Qt::Checked); + m_settings.showTriggers = (state == Qt::Checked); updatePreview(); } void MapImageExporter::on_checkBox_HealLocations_stateChanged(int state) { - this->settings.showHealLocations = (state == Qt::Checked); + m_settings.showHealLocations = (state == Qt::Checked); updatePreview(); } @@ -588,51 +601,51 @@ void MapImageExporter::on_checkBox_AllEvents_stateChanged(int state) { const QSignalBlocker b_Objects(ui->checkBox_Objects); ui->checkBox_Objects->setChecked(on); ui->checkBox_Objects->setDisabled(on); - this->settings.showObjects = on; + m_settings.showObjects = on; const QSignalBlocker b_Warps(ui->checkBox_Warps); ui->checkBox_Warps->setChecked(on); ui->checkBox_Warps->setDisabled(on); - this->settings.showWarps = on; + m_settings.showWarps = on; const QSignalBlocker b_BGs(ui->checkBox_BGs); ui->checkBox_BGs->setChecked(on); ui->checkBox_BGs->setDisabled(on); - this->settings.showBGs = on; + m_settings.showBGs = on; const QSignalBlocker b_Triggers(ui->checkBox_Triggers); ui->checkBox_Triggers->setChecked(on); ui->checkBox_Triggers->setDisabled(on); - this->settings.showTriggers = on; + m_settings.showTriggers = on; const QSignalBlocker b_HealLocations(ui->checkBox_HealLocations); ui->checkBox_HealLocations->setChecked(on); ui->checkBox_HealLocations->setDisabled(on); - this->settings.showHealLocations = on; + m_settings.showHealLocations = on; updatePreview(); } void MapImageExporter::on_checkBox_ConnectionUp_stateChanged(int state) { - this->settings.showUpConnections = (state == Qt::Checked); + m_settings.showUpConnections = (state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionDown_stateChanged(int state) { - this->settings.showDownConnections = (state == Qt::Checked); + m_settings.showDownConnections = (state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionLeft_stateChanged(int state) { - this->settings.showLeftConnections = (state == Qt::Checked); + m_settings.showLeftConnections = (state == Qt::Checked); updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ConnectionRight_stateChanged(int state) { - this->settings.showRightConnections = (state == Qt::Checked); + m_settings.showRightConnections = (state == Qt::Checked); updateShowBorderState(); updatePreview(); } @@ -644,30 +657,30 @@ void MapImageExporter::on_checkBox_AllConnections_stateChanged(int state) { const QSignalBlocker b_Up(ui->checkBox_ConnectionUp); ui->checkBox_ConnectionUp->setChecked(on); ui->checkBox_ConnectionUp->setDisabled(on); - this->settings.showUpConnections = on; + m_settings.showUpConnections = on; const QSignalBlocker b_Down(ui->checkBox_ConnectionDown); ui->checkBox_ConnectionDown->setChecked(on); ui->checkBox_ConnectionDown->setDisabled(on); - this->settings.showDownConnections = on; + m_settings.showDownConnections = on; const QSignalBlocker b_Left(ui->checkBox_ConnectionLeft); ui->checkBox_ConnectionLeft->setChecked(on); ui->checkBox_ConnectionLeft->setDisabled(on); - this->settings.showLeftConnections = on; + m_settings.showLeftConnections = on; const QSignalBlocker b_Right(ui->checkBox_ConnectionRight); ui->checkBox_ConnectionRight->setChecked(on); ui->checkBox_ConnectionRight->setDisabled(on); - this->settings.showRightConnections = on; + m_settings.showRightConnections = on; updateShowBorderState(); updatePreview(); } void MapImageExporter::on_checkBox_ActualSize_stateChanged(int state) { - this->settings.previewActualSize = (state == Qt::Checked); - if (this->settings.previewActualSize) { + m_settings.previewActualSize = (state == Qt::Checked); + if (m_settings.previewActualSize) { ui->graphicsView_Preview->resetTransform(); } else { scalePreview(); @@ -675,20 +688,20 @@ void MapImageExporter::on_checkBox_ActualSize_stateChanged(int state) { } void MapImageExporter::on_pushButton_Reset_pressed() { - this->settings = {}; + m_settings = {}; for (auto widget : this->findChildren()) { const QSignalBlocker b(widget); // Prevent calls to updatePreview widget->setChecked(false); } - ui->spinBox_TimelapseDelay->setValue(this->settings.timelapseDelayMs); - ui->spinBox_FrameSkip->setValue(this->settings.timelapseSkipAmount); + ui->spinBox_TimelapseDelay->setValue(m_settings.timelapseDelayMs); + ui->spinBox_FrameSkip->setValue(m_settings.timelapseSkipAmount); updatePreview(); } void MapImageExporter::on_spinBox_TimelapseDelay_valueChanged(int delayMs) { - this->settings.timelapseDelayMs = delayMs; + m_settings.timelapseDelayMs = delayMs; } void MapImageExporter::on_spinBox_FrameSkip_valueChanged(int skip) { - this->settings.timelapseSkipAmount = skip; + m_settings.timelapseSkipAmount = skip; } diff --git a/src/ui/maplistmodels.cpp b/src/ui/maplistmodels.cpp index a270736a..d08bb2cf 100644 --- a/src/ui/maplistmodels.cpp +++ b/src/ui/maplistmodels.cpp @@ -1,11 +1,11 @@ #include "maplistmodels.h" +#include "validator.h" +#include "project.h" +#include "filterchildrenproxymodel.h" #include #include -#include "project.h" -#include "filterchildrenproxymodel.h" - void MapTree::removeSelected() { @@ -38,8 +38,52 @@ void MapTree::keyPressEvent(QKeyEvent *event) { } } + + +MapListModel::MapListModel(Project *project, QObject *parent) : QStandardItemModel(parent) { + this->project = project; + this->root = invisibleRootItem(); + + this->mapGrayIcon = QIcon(QStringLiteral(":/icons/map_grayed.ico")); + this->mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); + this->mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); + this->mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); + + this->mapFolderIcon.addFile(QStringLiteral(":/icons/folder_closed_map.ico"), QSize(), QIcon::Normal, QIcon::Off); + this->mapFolderIcon.addFile(QStringLiteral(":/icons/folder_map.ico"), QSize(), QIcon::Normal, QIcon::On); + + this->emptyMapFolderIcon.addFile(QStringLiteral(":/icons/folder_closed.ico"), QSize(), QIcon::Normal, QIcon::Off); + this->emptyMapFolderIcon.addFile(QStringLiteral(":/icons/folder.ico"), QSize(), QIcon::Normal, QIcon::On); +} + +QStandardItem *MapListModel::itemAt(const QModelIndex &index) const { + if (index.isValid()) { + QStandardItem *item = static_cast(index.internalPointer()); + if (item) + return item; + } + return this->root; +} + +QStandardItem *MapListModel::itemAt(const QString &itemName) const { + QModelIndex index = this->indexOf(itemName); + if (!index.isValid()) + return nullptr; + return this->itemAt(index)->child(index.row(), index.column()); +} + +QModelIndex MapListModel::indexOf(const QString &itemName) const { + if (this->mapItems.contains(itemName)) + return this->mapItems.value(itemName)->index(); + + if (this->mapFolderItems.contains(itemName)) + return this->mapFolderItems.value(itemName)->index(); + + return QModelIndex(); +} + void MapListModel::removeItemAt(const QModelIndex &index) { - QStandardItem *item = this->getItem(index)->child(index.row(), index.column()); + QStandardItem *item = this->itemAt(index)->child(index.row(), index.column()); if (!item) return; @@ -49,24 +93,106 @@ void MapListModel::removeItemAt(const QModelIndex &index) { } else { // TODO: Because there's no support for deleting maps we can only delete empty folders if (!item->hasChildren()) { - this->removeItem(item); + removeItem(item); } } } +QStandardItem *MapListModel::createMapItem(const QString &mapName, QStandardItem *map) { + if (!map) map = new QStandardItem; + map->setText(mapName); + map->setData(mapName, MapListUserRoles::NameRole); + map->setData("map_name", MapListUserRoles::TypeRole); + map->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemNeverHasChildren); + map->setToolTip(this->project->mapNamesToMapConstants.value(mapName)); + this->mapItems.insert(mapName, map); + return map; +} + +QStandardItem *MapListModel::createMapFolderItem(const QString &folderName, QStandardItem *folder) { + if (!folder) folder = new QStandardItem; + folder->setText(folderName); + folder->setData(folderName, MapListUserRoles::NameRole); + folder->setData(this->folderTypeName, MapListUserRoles::TypeRole); + folder->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled); + folder->setEditable(this->editable); // Will override flags if necessary + this->mapFolderItems.insert(folderName, folder); + return folder; +} + +QStandardItem *MapListModel::insertMapItem(const QString &mapName, const QString &folderName) { + if (mapName.isEmpty() || mapName == this->project->getDynamicMapName()) // Disallow adding MAP_DYNAMIC to the map list. + return nullptr; + + QStandardItem *map = createMapItem(mapName); + + QStandardItem *folder = this->mapFolderItems[folderName]; + if (!folder) { + // Folder doesn't exist yet, add it. + folder = insertMapFolderItem(folderName); + } + // If folder is still nullptr here it's because we failed to create it. + if (folder) { + folder->appendRow(map); + } + + if (this->sortingEnabled) + this->sort(0, Qt::AscendingOrder); + return map; +} + +QStandardItem *MapListModel::insertMapFolderItem(const QString &folderName) { + if (folderName.isEmpty()) + return nullptr; + + QStandardItem *item = createMapFolderItem(folderName); + this->root->appendRow(item); + if (this->sortingEnabled) + this->sort(0, Qt::AscendingOrder); + return item; +} + +QVariant MapListModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int col = index.column(); + + const QStandardItem *item = this->itemAt(index)->child(row, col); + const QString type = item->data(MapListUserRoles::TypeRole).toString(); + const QString name = item->data(MapListUserRoles::NameRole).toString(); + + if (role == Qt::DecorationRole) { + if (type == "map_name") { + // Decorating map in the map list + if (name == this->activeItemName) + return this->mapOpenedIcon; + + const Map* map = this->project->mapCache.value(name); + if (!map) + return this->mapGrayIcon; + return map->hasUnsavedChanges() ? this->mapEditedIcon : this->mapIcon; + } else if (type == this->folderTypeName) { + // Decorating map folder in the map list + return item->hasChildren() ? this->mapFolderIcon : this->emptyMapFolderIcon; + } + } + return QStandardItemModel::data(index, role); +} + QWidget *GroupNameDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const { QLineEdit *editor = new QLineEdit(parent); - static const QRegularExpression expression("[A-Za-z_]+[\\w]*"); - editor->setPlaceholderText("gMapGroup_"); - editor->setValidator(new QRegularExpressionValidator(expression, parent)); + editor->setPlaceholderText(Project::getMapGroupPrefix()); + editor->setValidator(new IdentifierValidator(parent)); editor->setFrame(false); return editor; } void GroupNameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { - QString groupName = index.data(Qt::UserRole).toString(); + QString groupName = index.data(MapListUserRoles::NameRole).toString(); QLineEdit *le = static_cast(editor); le->setText(groupName); } @@ -74,7 +200,7 @@ void GroupNameDelegate::setEditorData(QWidget *editor, const QModelIndex &index) void GroupNameDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const { QLineEdit *le = static_cast(editor); QString groupName = le->text(); - model->setData(index, groupName, Qt::UserRole); + model->setData(index, groupName, MapListUserRoles::NameRole); } void GroupNameDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &) const { @@ -83,11 +209,16 @@ void GroupNameDelegate::updateEditorGeometry(QWidget *editor, const QStyleOption -MapGroupModel::MapGroupModel(Project *project, QObject *parent) : MapListModel(parent) { - this->project = project; - this->root = this->invisibleRootItem(); +MapGroupModel::MapGroupModel(Project *project, QObject *parent) : MapListModel(project, parent) { + this->folderTypeName = "map_group"; + this->editable = true; - initialize(); + for (const auto &groupName : this->project->groupNames) { + insertMapFolderItem(groupName); + for (const auto &mapName : this->project->groupNameToMapNames.value(groupName)) { + insertMapItem(mapName, groupName); + } + } } Qt::DropActions MapGroupModel::supportedDropActions() const { @@ -112,7 +243,7 @@ QMimeData *MapGroupModel::mimeData(const QModelIndexList &indexes) const { // if dropping a selection containing a group(s) and map(s), clear all selection but first group. for (const QModelIndex &index : indexes) { if (index.isValid() && data(index, MapListUserRoles::TypeRole).toString() == "map_group") { - QString groupName = data(index, Qt::UserRole).toString(); + QString groupName = data(index, MapListUserRoles::NameRole).toString(); stream << groupName; mimeData->setData("application/porymap.mapgroupmodel.group", encodedData); mimeData->setData("application/porymap.mapgroupmodel.source.row", QByteArray::number(index.row())); @@ -122,7 +253,7 @@ QMimeData *MapGroupModel::mimeData(const QModelIndexList &indexes) const { for (const QModelIndex &index : indexes) { if (index.isValid()) { - QString mapName = data(index, Qt::UserRole).toString(); + QString mapName = data(index, MapListUserRoles::NameRole).toString(); stream << mapName; } } @@ -168,12 +299,12 @@ bool MapGroupModel::dropMimeData(const QMimeData *data, Qt::DropAction action, i QStringList mapsToMove; for (int i = 0; i < this->rowCount(originIndex); ++i ) { children << this->index( i, 0, originIndex); - mapsToMove << this->index( i, 0 , originIndex).data(Qt::UserRole).toString(); + mapsToMove << this->index( i, 0 , originIndex).data(MapListUserRoles::NameRole).toString(); } QModelIndex groupIndex = index(row, 0, parentIndex); QStandardItem *groupItem = this->itemFromIndex(groupIndex); - createGroupItem(groupName, row, groupItem); + createMapFolderItem(groupName, groupItem); for (QString mapName : mapsToMove) { QStandardItem *mapItem = createMapItem(mapName); @@ -224,177 +355,63 @@ bool MapGroupModel::dropMimeData(const QMimeData *data, Qt::DropAction action, i void MapGroupModel::updateProject() { if (!this->project) return; - QStringList groupNames; - QMap mapGroups; - QList groupedMapNames; + // Temporary objects in case of failure, so we won't modify the project unless it succeeds. QStringList mapNames; + QStringList groupNames; + QMap groupNameToMapNames; for (int g = 0; g < this->root->rowCount(); g++) { - QStandardItem *groupItem = this->item(g); - QString groupName = groupItem->data(Qt::UserRole).toString(); + const QStandardItem *groupItem = this->item(g); + QString groupName = groupItem->data(MapListUserRoles::NameRole).toString(); groupNames.append(groupName); - mapGroups[groupName] = g; - QStringList mapsInGroup; for (int m = 0; m < groupItem->rowCount(); m++) { - QStandardItem *mapItem = groupItem->child(m); + const QStandardItem *mapItem = groupItem->child(m); if (!mapItem) { logError("An error occured while trying to apply updates to map group structure."); return; } - QString mapName = mapItem->data(Qt::UserRole).toString(); - mapsInGroup.append(mapName); + QString mapName = mapItem->data(MapListUserRoles::NameRole).toString(); + groupNameToMapNames[groupName].append(mapName); mapNames.append(mapName); } - groupedMapNames.append(mapsInGroup); } - this->project->groupNames = groupNames; - this->project->mapGroups = mapGroups; - this->project->groupedMapNames = groupedMapNames; this->project->mapNames = mapNames; + this->project->groupNames = groupNames; + this->project->groupNameToMapNames = groupNameToMapNames; this->project->hasUnsavedDataChanges = true; } -QStandardItem *MapGroupModel::createGroupItem(QString groupName, int groupIndex, QStandardItem *group) { - if (!group) group = new QStandardItem; - group->setText(groupName); - group->setData(groupName, Qt::UserRole); - group->setData("map_group", MapListUserRoles::TypeRole); - group->setData(groupIndex, MapListUserRoles::GroupRole); - group->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable); - this->groupItems.insert(groupName, group); - return group; -} - -QStandardItem *MapGroupModel::createMapItem(QString mapName, QStandardItem *map) { - if (!map) map = new QStandardItem; - map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListUserRoles::TypeRole); - map->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); - this->mapItems[mapName] = map; - return map; -} - -QStandardItem *MapGroupModel::insertGroupItem(QString groupName) { - QStandardItem *group = createGroupItem(groupName, this->groupItems.size()); - this->root->appendRow(group); - this->updateProject(); - return group; -} - void MapGroupModel::removeItem(QStandardItem *item) { this->removeRow(item->row()); this->updateProject(); } -QStandardItem *MapGroupModel::insertMapItem(QString mapName, QString groupName) { - QStandardItem *group = this->groupItems[groupName]; - if (!group) { - group = insertGroupItem(groupName); - } - QStandardItem *map = createMapItem(mapName); - group->appendRow(map); - return map; -} - -void MapGroupModel::initialize() { - this->groupItems.clear(); - this->mapItems.clear(); - for (int i = 0; i < this->project->groupNames.length(); i++) { - QString group_name = this->project->groupNames.value(i); - QStandardItem *group = createGroupItem(group_name, i); - root->appendRow(group); - QStringList names = this->project->groupedMapNames.value(i); - for (int j = 0; j < names.length(); j++) { - QString map_name = names.value(j); - QStandardItem *map = createMapItem(map_name); - group->appendRow(map); - } - } -} - -QStandardItem *MapGroupModel::getItem(const QModelIndex &index) const { - if (index.isValid()) { - QStandardItem *item = static_cast(index.internalPointer()); - if (item) - return item; - } - return this->root; -} - -QModelIndex MapGroupModel::indexOf(QString mapName) const { - if (this->mapItems.contains(mapName)) { - return this->mapItems[mapName]->index(); - } - return QModelIndex(); -} - QVariant MapGroupModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) return QVariant(); + if (!index.isValid()) + return QVariant(); int row = index.row(); int col = index.column(); - if (role == Qt::DecorationRole) { - static QIcon mapGrayIcon = QIcon(QStringLiteral(":/icons/map_grayed.ico")); - static QIcon mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); - static QIcon mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); - static QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); - - static QIcon mapFolderIcon; - static QIcon folderIcon; - static bool loaded = false; - if (!loaded) { - mapFolderIcon.addFile(QStringLiteral(":/icons/folder_closed_map.ico"), QSize(), QIcon::Normal, QIcon::Off); - mapFolderIcon.addFile(QStringLiteral(":/icons/folder_map.ico"), QSize(), QIcon::Normal, QIcon::On); - folderIcon.addFile(QStringLiteral(":/icons/folder_closed.ico"), QSize(), QIcon::Normal, QIcon::Off); - folderIcon.addFile(QStringLiteral(":/icons/folder.ico"), QSize(), QIcon::Normal, QIcon::On); - loaded = true; - } - - QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListUserRoles::TypeRole).toString(); - - if (type == "map_group") { - if (!item->hasChildren()) { - return folderIcon; - } - return mapFolderIcon; - } else if (type == "map_name") { - QString mapName = item->data(Qt::UserRole).toString(); - if (mapName == this->openMap) { - return mapOpenedIcon; - } - else if (this->project->mapCache.contains(mapName)) { - if (this->project->mapCache.value(mapName)->hasUnsavedChanges()) { - return mapEditedIcon; - } - else { - return mapIcon; - } - } - return mapGrayIcon; - } - } - else if (role == Qt::DisplayRole) { - QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListUserRoles::TypeRole).toString(); + const QStandardItem *item = this->itemAt(index)->child(row, col); + const QString type = item->data(MapListUserRoles::TypeRole).toString(); + const QString name = item->data(MapListUserRoles::NameRole).toString(); + if (role == Qt::DisplayRole) { if (type == "map_name") { - return QString("[%1.%2] ").arg(this->getItem(index)->row()).arg(row, 2, 10, QLatin1Char('0')) + item->data(Qt::UserRole).toString(); + return QString("[%1.%2] ").arg(this->itemAt(index)->row()).arg(row, 2, 10, QLatin1Char('0')) + name; } - else if (type == "map_group") { - return item->data(Qt::UserRole).toString(); + else if (type == this->folderTypeName) { + return name; } } - - return QStandardItemModel::data(index, role); + return MapListModel::data(index, role); } bool MapGroupModel::setData(const QModelIndex &index, const QVariant &value, int role) { - if (role == Qt::UserRole && data(index, MapListUserRoles::TypeRole).toString() == "map_group") { - // verify uniqueness of new group name - if (this->project->groupNames.contains(value.toString())) { + if (role == MapListUserRoles::NameRole && data(index, MapListUserRoles::TypeRole).toString() == "map_group") { + if (!this->project->isIdentifierUnique(value.toString())) { return false; } } @@ -406,287 +423,87 @@ bool MapGroupModel::setData(const QModelIndex &index, const QVariant &value, int -MapAreaModel::MapAreaModel(Project *project, QObject *parent) : MapListModel(parent) { - this->project = project; - this->root = this->invisibleRootItem(); +MapLocationModel::MapLocationModel(Project *project, QObject *parent) : MapListModel(project, parent) { + this->folderTypeName = "map_section"; - initialize(); -} - -QStandardItem *MapAreaModel::createAreaItem(QString mapsecName) { - QStandardItem *area = new QStandardItem; - area->setText(mapsecName); - area->setEditable(false); - area->setData(mapsecName, Qt::UserRole); - area->setData("map_section", MapListUserRoles::TypeRole); - // group->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); - this->areaItems.insert(mapsecName, area); - return area; -} - -QStandardItem *MapAreaModel::createMapItem(QString mapName, int, int) { - QStandardItem *map = new QStandardItem; - map->setText(mapName); - map->setEditable(false); - map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListUserRoles::TypeRole); - // map->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); - this->mapItems.insert(mapName, map); - return map; -} - -QStandardItem *MapAreaModel::insertAreaItem(QString areaName) { - this->project->addNewMapsec(areaName); - QStandardItem *item = createAreaItem(areaName); - this->root->appendRow(item); - this->sort(0, Qt::AscendingOrder); - return item; -} - -QStandardItem *MapAreaModel::insertMapItem(QString mapName, QString areaName, int groupIndex) { - QStandardItem *area = this->areaItems[areaName]; - if (!area) { - return nullptr; + for (const auto &idName : this->project->mapSectionIdNames) { + insertMapFolderItem(idName); } - int mapIndex = area->rowCount(); - QStandardItem *map = createMapItem(mapName, groupIndex, mapIndex); - area->appendRow(map); - return map; + for (const auto &mapName : this->project->mapNames) { + insertMapItem(mapName, this->project->mapNameToMapSectionName.value(mapName)); + } + + this->sortingEnabled = true; + sort(0, Qt::AscendingOrder); } -void MapAreaModel::removeItem(QStandardItem *item) { - this->project->removeMapsec(item->data(Qt::UserRole).toString()); +void MapLocationModel::removeItem(QStandardItem *item) { + this->project->removeMapsec(item->data(MapListUserRoles::NameRole).toString()); this->removeRow(item->row()); } -void MapAreaModel::initialize() { - this->areaItems.clear(); - this->mapItems.clear(); - - for (const auto &idName : this->project->mapSectionIdNames) { - this->root->appendRow(createAreaItem(idName)); - } - - for (int i = 0; i < this->project->groupNames.length(); i++) { - QStringList names = this->project->groupedMapNames.value(i); - for (int j = 0; j < names.length(); j++) { - QString mapName = names.value(j); - QStandardItem *map = createMapItem(mapName, i, j); - QString mapsecName = this->project->readMapLocation(mapName); - if (this->areaItems.contains(mapsecName)) { - this->areaItems[mapsecName]->appendRow(map); - } - } - } - this->sort(0, Qt::AscendingOrder); -} - -QStandardItem *MapAreaModel::getItem(const QModelIndex &index) const { - if (index.isValid()) { - QStandardItem *item = static_cast(index.internalPointer()); - if (item) - return item; - } - return this->root; -} - -QModelIndex MapAreaModel::indexOf(QString mapName) const { - if (this->mapItems.contains(mapName)) { - return this->mapItems[mapName]->index(); - } - return QModelIndex(); -} - -QVariant MapAreaModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) return QVariant(); - - int row = index.row(); - int col = index.column(); - - if (role == Qt::DecorationRole) { - static QIcon mapGrayIcon = QIcon(QStringLiteral(":/icons/map_grayed.ico")); - static QIcon mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); - static QIcon mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); - static QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); - - static QIcon mapFolderIcon; - static QIcon folderIcon; - static bool loaded = false; - if (!loaded) { - mapFolderIcon.addFile(QStringLiteral(":/icons/folder_closed_map.ico"), QSize(), QIcon::Normal, QIcon::Off); - mapFolderIcon.addFile(QStringLiteral(":/icons/folder_map.ico"), QSize(), QIcon::Normal, QIcon::On); - folderIcon.addFile(QStringLiteral(":/icons/folder_closed.ico"), QSize(), QIcon::Normal, QIcon::Off); - folderIcon.addFile(QStringLiteral(":/icons/folder.ico"), QSize(), QIcon::Normal, QIcon::On); - loaded = true; - } - - QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListUserRoles::TypeRole).toString(); - - if (type == "map_section") { - if (item->hasChildren()) { - return mapFolderIcon; - } - return folderIcon; - } else if (type == "map_name") { - QString mapName = item->data(Qt::UserRole).toString(); - if (mapName == this->openMap) { - return mapOpenedIcon; - } - else if (this->project->mapCache.contains(mapName)) { - if (this->project->mapCache.value(mapName)->hasUnsavedChanges()) { - return mapEditedIcon; - } - else { - return mapIcon; - } - } - return mapGrayIcon; - } - } - else if (role == Qt::DisplayRole) { - QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListUserRoles::TypeRole).toString(); - - if (type == "map_section") { - return item->data(Qt::UserRole).toString(); - } - } - - return QStandardItemModel::data(index, role); +QStandardItem *MapLocationModel::createMapFolderItem(const QString &folderName, QStandardItem *folder) { + folder = MapListModel::createMapFolderItem(folderName, folder); + folder->setToolTip(this->project->getMapsecDisplayName(folderName)); + return folder; } -LayoutTreeModel::LayoutTreeModel(Project *project, QObject *parent) : MapListModel(parent) { - this->project = project; - this->root = this->invisibleRootItem(); +LayoutTreeModel::LayoutTreeModel(Project *project, QObject *parent) : MapListModel(project, parent) { + this->folderTypeName = "map_layout"; - initialize(); -} - -QStandardItem *LayoutTreeModel::createLayoutItem(QString layoutId) { - QStandardItem *layout = new QStandardItem; - layout->setText(this->project->layoutIdsToNames[layoutId]); - layout->setEditable(false); - layout->setData(layoutId, Qt::UserRole); - layout->setData("map_layout", MapListUserRoles::TypeRole); - // // group->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled); - this->layoutItems.insert(layoutId, layout); - return layout; -} - -QStandardItem *LayoutTreeModel::createMapItem(QString mapName) { - QStandardItem *map = new QStandardItem; - map->setText(mapName); - map->setEditable(false); - map->setData(mapName, Qt::UserRole); - map->setData("map_name", MapListUserRoles::TypeRole); - map->setFlags(Qt::NoItemFlags | Qt::ItemNeverHasChildren); - this->mapItems.insert(mapName, map); - return map; -} - -QStandardItem *LayoutTreeModel::insertLayoutItem(QString layoutId) { - QStandardItem *layoutItem = this->createLayoutItem(layoutId); - this->root->appendRow(layoutItem); - this->sort(0, Qt::AscendingOrder); - return layoutItem; -} - -QStandardItem *LayoutTreeModel::insertMapItem(QString mapName, QString layoutId) { - QStandardItem *layout = nullptr; - if (this->layoutItems.contains(layoutId)) { - layout = this->layoutItems[layoutId]; + for (const auto &layoutId : this->project->layoutIds) { + insertMapFolderItem(layoutId); } - else { - layout = createLayoutItem(layoutId); - this->root->appendRow(layout); + for (const auto &mapName : this->project->mapNames) { + insertMapItem(mapName, this->project->mapNameToLayoutId.value(mapName)); } - if (!layout) { - return nullptr; - } - QStandardItem *map = createMapItem(mapName); - layout->appendRow(map); - return map; + + this->sortingEnabled = true; + sort(0, Qt::AscendingOrder); } void LayoutTreeModel::removeItem(QStandardItem *) { // TODO: Deleting layouts not supported } +QStandardItem *LayoutTreeModel::createMapFolderItem(const QString &folderName, QStandardItem *folder) { + folder = MapListModel::createMapFolderItem(folderName, folder); -void LayoutTreeModel::initialize() { - this->layoutItems.clear(); - this->mapItems.clear(); - for (int i = 0; i < this->project->mapLayoutsTable.length(); i++) { - QString layoutId = project->mapLayoutsTable.value(i); - QStandardItem *layoutItem = createLayoutItem(layoutId); - this->root->appendRow(layoutItem); - } + // Despite using layout IDs internally, the Layouts map list shows layouts using their file path name. + // We could handle this with Qt::DisplayRole in LayoutTreeModel::data, but then it would be sorted using the ID instead of the name. + const Layout* layout = this->project->mapLayouts.value(folderName); + if (layout) folder->setText(layout->name); - for (auto mapList : this->project->groupedMapNames) { - for (auto mapName : mapList) { - QString layoutId = project->readMapLayoutId(mapName); - QStandardItem *map = createMapItem(mapName); - this->layoutItems[layoutId]->appendRow(map); - } - } - this->sort(0, Qt::AscendingOrder); -} + // The layout ID will instead be shown as a tool tip. + folder->setToolTip(folderName); -QStandardItem *LayoutTreeModel::getItem(const QModelIndex &index) const { - if (index.isValid()) { - QStandardItem *item = static_cast(index.internalPointer()); - if (item) - return item; - } - return this->root; -} - -QModelIndex LayoutTreeModel::indexOf(QString layoutName) const { - if (this->layoutItems.contains(layoutName)) { - return this->layoutItems[layoutName]->index(); - } - return QModelIndex(); + return folder; } QVariant LayoutTreeModel::data(const QModelIndex &index, int role) const { - if (!index.isValid()) return QVariant(); + if (!index.isValid()) + return QVariant(); int row = index.row(); int col = index.column(); - if (role == Qt::DecorationRole) { - static QIcon mapGrayIcon = QIcon(QStringLiteral(":/icons/map_grayed.ico")); - static QIcon mapIcon = QIcon(QStringLiteral(":/icons/map.ico")); - static QIcon mapEditedIcon = QIcon(QStringLiteral(":/icons/map_edited.ico")); - static QIcon mapOpenedIcon = QIcon(QStringLiteral(":/icons/map_opened.ico")); + const QStandardItem *item = this->itemAt(index)->child(row, col); + const QString type = item->data(MapListUserRoles::TypeRole).toString(); + const QString name = item->data(MapListUserRoles::NameRole).toString(); - QStandardItem *item = this->getItem(index)->child(row, col); - QString type = item->data(MapListUserRoles::TypeRole).toString(); + if (type == this->folderTypeName) { + if (role == Qt::DecorationRole) { + // Map layouts are used as folders, but we display them with the same icons as maps. + if (name == this->activeItemName) + return this->mapOpenedIcon; - if (type == "map_layout") { - QString layoutId = item->data(Qt::UserRole).toString(); - if (layoutId == this->openLayout) { - return mapOpenedIcon; - } - else if (this->project->mapLayouts.contains(layoutId)) { - if (this->project->mapLayouts.value(layoutId)->hasUnsavedChanges()) { - return mapEditedIcon; - } - else if (!this->project->mapLayouts[layoutId]->loaded) { - return mapGrayIcon; - } - } - return mapIcon; + const Layout* layout = this->project->mapLayouts.value(name); + if (!layout || !layout->loaded) + return this->mapGrayIcon; + return layout->hasUnsavedChanges() ? this->mapEditedIcon : this->mapIcon; } - else if (type == "map_name") { - return QVariant(); - } - - return QVariant(); } - - return QStandardItemModel::data(index, role); + return MapListModel::data(index, role); } diff --git a/src/ui/message.cpp b/src/ui/message.cpp new file mode 100644 index 00000000..d2d91f75 --- /dev/null +++ b/src/ui/message.cpp @@ -0,0 +1,78 @@ +#include "message.h" +#include "log.h" + +#include + +Message::Message(QMessageBox::Icon icon, const QString &text, QMessageBox::StandardButtons buttons, QWidget *parent) : + QMessageBox(icon, QApplication::applicationName(), text, buttons, parent) +{ + setWindowModality(Qt::WindowModal); +} + +ErrorMessage::ErrorMessage(const QString &message, QWidget *parent) : + Message(QMessageBox::Critical, + message, + QMessageBox::Ok, + parent) +{ + setDefaultButton(QMessageBox::Ok); +} + +WarningMessage::WarningMessage(const QString &message, QWidget *parent) : + Message(QMessageBox::Warning, + message, + QMessageBox::Ok, + parent) +{ + setDefaultButton(QMessageBox::Ok); +} + +InfoMessage::InfoMessage(const QString &message, QWidget *parent) : + Message(QMessageBox::Information, + message, + QMessageBox::Close, + parent) +{ + setDefaultButton(QMessageBox::Close); +} + +QuestionMessage::QuestionMessage(const QString &message, QWidget *parent) : + Message(QMessageBox::Question, + message, + QMessageBox::No | QMessageBox::Yes, + parent) +{ + setDefaultButton(QMessageBox::No); +} + +RecentErrorMessage::RecentErrorMessage(const QString &message, QWidget *parent) : + ErrorMessage(message, parent) +{ + setInformativeText(QString("Please see %1 for full error details.").arg(getLogPath())); + setDetailedText(getMostRecentError()); +} + +int RecentErrorMessage::show(const QString &message, QWidget *parent) { + RecentErrorMessage msgBox(message, parent); + return msgBox.exec(); +}; + +int ErrorMessage::show(const QString &message, QWidget *parent) { + ErrorMessage msgBox(message, parent); + return msgBox.exec(); +}; + +int WarningMessage::show(const QString &message, QWidget *parent) { + WarningMessage msgBox(message, parent); + return msgBox.exec(); +}; + +int QuestionMessage::show(const QString &message, QWidget *parent) { + QuestionMessage msgBox(message, parent); + return msgBox.exec(); +}; + +int InfoMessage::show(const QString &message, QWidget *parent) { + InfoMessage msgBox(message, parent); + return msgBox.exec(); +}; diff --git a/src/ui/newlayoutdialog.cpp b/src/ui/newlayoutdialog.cpp new file mode 100644 index 00000000..779a4694 --- /dev/null +++ b/src/ui/newlayoutdialog.cpp @@ -0,0 +1,164 @@ +#include "newlayoutdialog.h" +#include "maplayout.h" +#include "ui_newlayoutdialog.h" +#include "config.h" +#include "validator.h" + +#include +#include +#include + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; + +NewLayoutDialog::NewLayoutDialog(Project *project, QWidget *parent) : + NewLayoutDialog(project, nullptr, parent) +{} + +NewLayoutDialog::NewLayoutDialog(Project *project, const Layout *layoutToCopy, QWidget *parent) : + QDialog(parent), + ui(new Ui::NewLayoutDialog), + layoutToCopy(layoutToCopy) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + this->project = project; + Layout::Settings *settings = &project->newLayoutSettings; + + // Note: 'layoutToCopy' will have an empty name if it's an import from AdvanceMap + if (this->layoutToCopy && !this->layoutToCopy->name.isEmpty()) { + settings->name = project->toUniqueIdentifier(this->layoutToCopy->name); + } else { + settings->name = QString(); + } + // Generate a unique Layout constant + settings->id = project->toUniqueIdentifier(Layout::layoutConstantFromName(settings->name)); + + ui->newLayoutForm->initUi(project); + + auto validator = new IdentifierValidator(this); + ui->lineEdit_Name->setValidator(validator); + ui->lineEdit_LayoutID->setValidator(validator); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewLayoutDialog::dialogButtonClicked); + + refresh(); + + if (porymapConfig.newLayoutDialogGeometry.isEmpty()){ + // On first display resize to fit contents a little better + adjustSize(); + } else { + restoreGeometry(porymapConfig.newLayoutDialogGeometry); + } +} + +NewLayoutDialog::~NewLayoutDialog() +{ + porymapConfig.newLayoutDialogGeometry = saveGeometry(); + saveSettings(); + delete ui; +} + +void NewLayoutDialog::refresh() { + const Layout::Settings *settings = &this->project->newLayoutSettings; + + if (this->layoutToCopy) { + // If we're importing a layout then some settings will be enforced. + ui->newLayoutForm->setSettings(this->layoutToCopy->settings()); + ui->newLayoutForm->setDisabled(true); + } else { + ui->newLayoutForm->setSettings(*settings); + ui->newLayoutForm->setDisabled(false); + } + + ui->lineEdit_Name->setText(settings->name); + ui->lineEdit_LayoutID->setText(settings->id); +} + +void NewLayoutDialog::saveSettings() { + Layout::Settings *settings = &this->project->newLayoutSettings; + + *settings = ui->newLayoutForm->settings(); + settings->id = ui->lineEdit_LayoutID->text(); + settings->name = ui->lineEdit_Name->text(); +} + +bool NewLayoutDialog::validateLayoutID(bool allowEmpty) { + QString id = ui->lineEdit_LayoutID->text(); + + QString errorText; + if (id.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_LayoutID->text()); + } else if (!this->project->isIdentifierUnique(id)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_LayoutID->text()).arg(id); + } + + bool isValid = errorText.isEmpty(); + ui->label_LayoutIDError->setText(errorText); + ui->label_LayoutIDError->setVisible(!isValid); + ui->lineEdit_LayoutID->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewLayoutDialog::on_lineEdit_LayoutID_textChanged(const QString &) { + validateLayoutID(true); +} + +bool NewLayoutDialog::validateName(bool allowEmpty) { + QString name = ui->lineEdit_Name->text(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_Name->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewLayoutDialog::on_lineEdit_Name_textChanged(const QString &text) { + validateName(true); + + // Changing the layout name updates the layout ID field to match. + ui->lineEdit_LayoutID->setText(Layout::layoutConstantFromName(text)); +} + +void NewLayoutDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::ResetRole) { + this->project->initNewLayoutSettings(); + refresh(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewLayoutDialog::accept() { + // Make sure to call each validation function so that all errors are shown at once. + bool success = true; + if (!ui->newLayoutForm->validate()) success = false; + if (!validateLayoutID()) success = false; + if (!validateName()) success = false; + if (!success) + return; + + // Update settings from UI + saveSettings(); + + Layout *layout = this->project->createNewLayout(this->project->newLayoutSettings, this->layoutToCopy); + if (!layout) { + ui->label_GenericError->setText(QString("Failed to create layout. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); + return; + } + ui->label_GenericError->setVisible(false); + + emit applied(layout->id); + QDialog::accept(); +} diff --git a/src/ui/newlayoutform.cpp b/src/ui/newlayoutform.cpp new file mode 100644 index 00000000..aa8178ce --- /dev/null +++ b/src/ui/newlayoutform.cpp @@ -0,0 +1,144 @@ +#include "newlayoutform.h" +#include "ui_newlayoutform.h" +#include "project.h" + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; + +NewLayoutForm::NewLayoutForm(QWidget *parent) + : QWidget(parent) + , ui(new Ui::NewLayoutForm) +{ + ui->setupUi(this); + + ui->groupBox_BorderDimensions->setVisible(projectConfig.useCustomBorderSize); + + ui->spinBox_BorderWidth->setMaximum(MAX_BORDER_WIDTH); + ui->spinBox_BorderHeight->setMaximum(MAX_BORDER_HEIGHT); + + 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); }); +} + +NewLayoutForm::~NewLayoutForm() +{ + delete ui; +} + +void NewLayoutForm::initUi(Project *project) { + m_project = project; + + ui->comboBox_PrimaryTileset->clear(); + ui->comboBox_SecondaryTileset->clear(); + + if (m_project) { + ui->comboBox_PrimaryTileset->addItems(m_project->primaryTilesetLabels); + ui->comboBox_SecondaryTileset->addItems(m_project->secondaryTilesetLabels); + + ui->spinBox_MapWidth->setMaximum(m_project->getMaxMapWidth()); + ui->spinBox_MapHeight->setMaximum(m_project->getMaxMapHeight()); + } +} + +void NewLayoutForm::setDisabled(bool disabled) { + ui->groupBox_MapDimensions->setDisabled(disabled); + ui->groupBox_BorderDimensions->setDisabled(disabled); + ui->groupBox_Tilesets->setDisabled(disabled); +} + +void NewLayoutForm::setSettings(const Layout::Settings &settings) { + ui->spinBox_MapWidth->setValue(settings.width); + ui->spinBox_MapHeight->setValue(settings.height); + ui->spinBox_BorderWidth->setValue(settings.borderWidth); + ui->spinBox_BorderHeight->setValue(settings.borderHeight); + ui->comboBox_PrimaryTileset->setTextItem(settings.primaryTilesetLabel); + ui->comboBox_SecondaryTileset->setTextItem(settings.secondaryTilesetLabel); +} + +Layout::Settings NewLayoutForm::settings() const { + Layout::Settings settings; + settings.width = ui->spinBox_MapWidth->value(); + settings.height = ui->spinBox_MapHeight->value(); + if (ui->groupBox_BorderDimensions->isVisible()) { + settings.borderWidth = ui->spinBox_BorderWidth->value(); + settings.borderHeight = ui->spinBox_BorderHeight->value(); + } else { + settings.borderWidth = DEFAULT_BORDER_WIDTH; + settings.borderHeight = DEFAULT_BORDER_HEIGHT; + } + settings.primaryTilesetLabel = ui->comboBox_PrimaryTileset->currentText(); + settings.secondaryTilesetLabel = ui->comboBox_SecondaryTileset->currentText(); + return settings; +} + +bool NewLayoutForm::validate() { + // Make sure to call each validation function so that all errors are shown at once. + bool valid = true; + if (!validateMapDimensions()) valid = false; + if (!validatePrimaryTileset()) valid = false; + if (!validateSecondaryTileset()) valid = false; + return valid; +} + +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) { + 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(maxSize) + .arg(ui->spinBox_MapWidth->value()) + .arg(ui->spinBox_MapHeight->value()) + .arg(size); + } + + bool isValid = errorText.isEmpty(); + ui->label_MapDimensionsError->setText(errorText); + ui->label_MapDimensionsError->setVisible(!isValid); + return isValid; +} + +bool NewLayoutForm::validatePrimaryTileset(bool allowEmpty) { + const QString name = ui->comboBox_PrimaryTileset->currentText(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("The Primary Tileset cannot be empty."); + } else if (ui->comboBox_PrimaryTileset->findText(name) < 0) { + errorText = QString("The Primary Tileset '%1' does not exist.").arg(ui->label_PrimaryTileset->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_PrimaryTilesetError->setText(errorText); + ui->label_PrimaryTilesetError->setVisible(!isValid); + ui->comboBox_PrimaryTileset->lineEdit()->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +bool NewLayoutForm::validateSecondaryTileset(bool allowEmpty) { + const QString name = ui->comboBox_SecondaryTileset->currentText(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("The Secondary Tileset cannot be empty."); + } else if (ui->comboBox_SecondaryTileset->findText(name) < 0) { + errorText = QString("The Secondary Tileset '%1' does not exist.").arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_SecondaryTilesetError->setText(errorText); + ui->label_SecondaryTilesetError->setVisible(!isValid); + ui->comboBox_SecondaryTileset->lineEdit()->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} diff --git a/src/ui/newlocationdialog.cpp b/src/ui/newlocationdialog.cpp new file mode 100644 index 00000000..d0586d99 --- /dev/null +++ b/src/ui/newlocationdialog.cpp @@ -0,0 +1,79 @@ +#include "newlocationdialog.h" +#include "ui_newlocationdialog.h" +#include "project.h" +#include "validator.h" + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; + +NewLocationDialog::NewLocationDialog(Project* project, QWidget *parent) : + QDialog(parent), + ui(new Ui::NewLocationDialog), + namePrefix(projectConfig.getIdentifier(ProjectIdentifier::define_map_section_prefix)) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + this->project = project; + + ui->lineEdit_IdName->setValidator(new IdentifierValidator(namePrefix, this)); + ui->lineEdit_IdName->setText(namePrefix); + + connect(ui->lineEdit_IdName, &QLineEdit::textChanged, this, &NewLocationDialog::onIdNameChanged); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewLocationDialog::dialogButtonClicked); + + adjustSize(); +} + +NewLocationDialog::~NewLocationDialog() +{ + delete ui; +} + +void NewLocationDialog::onIdNameChanged(const QString &idName) { + validateIdName(true); + + // Extract a presumed display name from the ID name + QString displayName = idName; + if (displayName.startsWith(namePrefix)) + displayName.remove(0, namePrefix.length()); + displayName.replace("_", " "); + ui->lineEdit_DisplayName->setText(displayName); +} + +bool NewLocationDialog::validateIdName(bool allowEmpty) { + const QString name = ui->lineEdit_IdName->text(); + + QString errorText; + if (name.isEmpty() || name == namePrefix) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_IdName->text()); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_IdName->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_IdNameError->setText(errorText); + ui->label_IdNameError->setVisible(!isValid); + ui->lineEdit_IdName->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewLocationDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewLocationDialog::accept() { + if (!validateIdName()) + return; + + const QString idName = ui->lineEdit_IdName->text(); + const QString displayName = ui->lineEdit_DisplayName->text(); + + this->project->addNewMapsec(idName); + this->project->setMapsecDisplayName(idName, displayName); + + QDialog::accept(); +} diff --git a/src/ui/newmapconnectiondialog.cpp b/src/ui/newmapconnectiondialog.cpp index af06789f..def341fd 100644 --- a/src/ui/newmapconnectiondialog.cpp +++ b/src/ui/newmapconnectiondialog.cpp @@ -33,7 +33,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() == map->name() && mapNames.length() > 1) { // Prefer not to connect the map to itself defaultMapName = mapNames.at(1); } else { diff --git a/src/ui/newmapdialog.cpp b/src/ui/newmapdialog.cpp new file mode 100644 index 00000000..caa1e839 --- /dev/null +++ b/src/ui/newmapdialog.cpp @@ -0,0 +1,268 @@ +#include "newmapdialog.h" +#include "maplayout.h" +#include "mainwindow.h" +#include "ui_newmapdialog.h" +#include "config.h" +#include "validator.h" + +#include +#include +#include + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; + +NewMapDialog::NewMapDialog(Project *project, QWidget *parent) : + NewMapDialog(project, nullptr, parent) +{} + +NewMapDialog::NewMapDialog(Project *project, const Map *mapToCopy, QWidget *parent) : + QDialog(parent), + ui(new Ui::NewMapDialog), + mapToCopy(mapToCopy) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + this->project = project; + Project::NewMapSettings *settings = &project->newMapSettings; + + if (this->mapToCopy) { + // Copy settings from the map we're duplicating + if (this->mapToCopy->layout()){ + settings->layout = this->mapToCopy->layout()->settings(); + } + settings->header = *this->mapToCopy->header(); + settings->group = project->mapNameToMapGroup(this->mapToCopy->name()); + settings->name = project->toUniqueIdentifier(this->mapToCopy->name()); + + } else { + // Clear the previously-used map name. The rest of the settings are preserved between sessions. + settings->name = QString(); + } + // Generate a unique Layout constant + settings->layout.id = project->toUniqueIdentifier(Layout::layoutConstantFromName(settings->name)); + + ui->newLayoutForm->initUi(project); + ui->comboBox_Group->addItems(project->groupNames); + ui->comboBox_LayoutID->addItems(project->layoutIds); + + auto validator = new IdentifierValidator(this); + ui->lineEdit_Name->setValidator(validator); + ui->comboBox_Group->setValidator(validator); + ui->comboBox_LayoutID->setValidator(validator); + + // Create a collapsible section that has all the map header data. + this->headerForm = new MapHeaderForm(); + this->headerForm->setProject(project, false); + auto sectionLayout = new QVBoxLayout(); + sectionLayout->addWidget(this->headerForm); + + this->headerSection = new CollapsibleSection("Header Data", porymapConfig.newMapHeaderSectionExpanded, 150, this); + this->headerSection->setContentLayout(sectionLayout); + ui->layout_HeaderData->addWidget(this->headerSection); + + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewMapDialog::dialogButtonClicked); + + refresh(); + restoreGeometry(porymapConfig.newMapDialogGeometry); +} + +// Adding new map to an existing map list folder. Initialize settings accordingly. +// Even if we initialize settings like this we'll allow users to change them afterwards, +// because nothing is expecting them to stay at these values (with exception to layouts). +NewMapDialog::NewMapDialog(Project *project, int mapListTab, const QString &mapListItem, QWidget *parent) : + NewMapDialog(project, parent) +{ + switch (mapListTab) + { + case MapListTab::Groups: + ui->comboBox_Group->setTextItem(mapListItem); + break; + case MapListTab::Locations: + this->headerForm->setLocation(mapListItem); + break; + case MapListTab::Layouts: + // We specifically lock the layout ID because otherwise the setting would be overwritten when + // the user changes the map name (which will normally automatically update the layout ID to match). + // For the Group/Location settings above we don't care if the user changes them afterwards. + ui->comboBox_LayoutID->setTextItem(mapListItem); + ui->comboBox_LayoutID->setDisabled(true); + break; + } +} + +NewMapDialog::~NewMapDialog() +{ + porymapConfig.newMapDialogGeometry = saveGeometry(); + saveSettings(); + delete ui; +} + +// Reload the UI from the last-saved settings. +void NewMapDialog::refresh() { + const Project::NewMapSettings *settings = &this->project->newMapSettings; + + ui->lineEdit_Name->setText(settings->name); + ui->comboBox_Group->setTextItem(settings->group); + + // If the layout combo box is disabled, it's because we're enforcing the setting. Leave it unchanged. + if (ui->comboBox_LayoutID->isEnabled()) + ui->comboBox_LayoutID->setTextItem(settings->layout.id); + + ui->newLayoutForm->setSettings(settings->layout); + ui->checkBox_CanFlyTo->setChecked(settings->canFlyTo); + this->headerForm->setHeaderData(settings->header); +} + +void NewMapDialog::saveSettings() { + Project::NewMapSettings *settings = &this->project->newMapSettings; + + settings->name = ui->lineEdit_Name->text(); + settings->group = ui->comboBox_Group->currentText(); + settings->layout = ui->newLayoutForm->settings(); + settings->layout.id = ui->comboBox_LayoutID->currentText(); + settings->canFlyTo = ui->checkBox_CanFlyTo->isChecked(); + settings->header = this->headerForm->headerData(); + + // This dialog doesn't give users the option to give new layouts a name, we generate one using the map name. + // We instead add a "_Layout" suffix to differentiate the layout name from the map name. + // (an older iteration of this dialog gave users an option to name new layouts, but it's extra clutter for + // something the majority of users creating a map won't need. If they want to give a specific name to a layout + // they can create the layout first, then create a new map that uses that layout.) + const Layout *layout = this->project->mapLayouts.value(settings->layout.id); + if (!layout) { + const QString newLayoutName = settings->name + QStringLiteral("_Layout"); + settings->layout.name = this->project->toUniqueIdentifier(newLayoutName); + } else { + // Pre-existing layout. The layout name won't be read, but we'll make sure it's correct anyway. + settings->layout.name = layout->name; + } + + // Folders for new layouts created for new maps use the map name, rather than the layout name + // (i.e., if you create "MyMap", you'll get a 'data/layouts/MyMap/', rather than 'data/layouts/MyMap_Layout/'). + // There's no real reason for this, aside from maintaining consistency with the default layout folder names that do this. + settings->layout.folderName = settings->name; + + porymapConfig.newMapHeaderSectionExpanded = this->headerSection->isExpanded(); +} + +bool NewMapDialog::validateName(bool allowEmpty) { + QString name = ui->lineEdit_Name->text(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_Name->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewMapDialog::on_lineEdit_Name_textChanged(const QString &text) { + validateName(true); + + // Changing the map name updates the layout ID field to match. + if (ui->comboBox_LayoutID->isEnabled()) { + ui->comboBox_LayoutID->setCurrentText(Layout::layoutConstantFromName(text)); + } +} + +bool NewMapDialog::validateGroup(bool allowEmpty) { + QString groupName = ui->comboBox_Group->currentText(); + + QString errorText; + if (groupName.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Group->text()); + } else if (!this->project->groupNames.contains(groupName) && !this->project->isIdentifierUnique(groupName)) { + errorText = QString("%1 must either be the name of an existing map group, or a unique identifier for a new map group.").arg(ui->label_Group->text()); + } + + bool isValid = errorText.isEmpty(); + ui->label_GroupError->setText(errorText); + ui->label_GroupError->setVisible(!isValid); + ui->comboBox_Group->lineEdit()->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewMapDialog::on_comboBox_Group_currentTextChanged(const QString &) { + validateGroup(true); +} + +bool NewMapDialog::validateLayoutID(bool allowEmpty) { + QString layoutId = ui->comboBox_LayoutID->currentText(); + + QString errorText; + if (layoutId.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_LayoutID->text()); + } else if (!this->project->layoutIds.contains(layoutId) && !this->project->isIdentifierUnique(layoutId)) { + errorText = QString("%1 must either be the ID for an existing layout, or a unique identifier for a new layout.").arg(ui->label_LayoutID->text()); + } + + bool isValid = errorText.isEmpty(); + ui->label_LayoutIDError->setText(errorText); + ui->label_LayoutIDError->setVisible(!isValid); + ui->comboBox_LayoutID->lineEdit()->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewMapDialog::on_comboBox_LayoutID_currentTextChanged(const QString &text) { + validateLayoutID(true); + + // Changing the layout ID to an existing layout updates the layout settings to match. + const Layout *layout = this->project->mapLayouts.value(text); + if (!layout && this->mapToCopy) { + // When duplicating a map, if a new layout ID is specified the settings will be updated + // to match the layout of the map we're duplicating. + layout = this->mapToCopy->layout(); + } + + if (layout) { + ui->newLayoutForm->setSettings(layout->settings()); + ui->newLayoutForm->setDisabled(true); + } else { + ui->newLayoutForm->setDisabled(false); + } +} + +void NewMapDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::ResetRole) { + this->project->initNewMapSettings(); + refresh(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewMapDialog::accept() { + // Make sure to call each validation function so that all errors are shown at once. + bool success = true; + if (!ui->newLayoutForm->validate()) success = false; + if (!validateName()) success = false; + if (!validateGroup()) success = false; + if (!validateLayoutID()) success = false; + if (!success) + return; + + // Update settings from UI + saveSettings(); + + Map *map = this->project->createNewMap(this->project->newMapSettings, this->mapToCopy); + if (!map) { + ui->label_GenericError->setText(QString("Failed to create map. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); + return; + } + ui->label_GenericError->setVisible(false); + + // If the location name field was changed, update the project for that too. + this->project->setMapsecDisplayName(this->headerForm->location(), this->headerForm->locationName()); + + QDialog::accept(); +} diff --git a/src/ui/newmapgroupdialog.cpp b/src/ui/newmapgroupdialog.cpp new file mode 100644 index 00000000..13461b68 --- /dev/null +++ b/src/ui/newmapgroupdialog.cpp @@ -0,0 +1,67 @@ +#include "newmapgroupdialog.h" +#include "ui_newmapgroupdialog.h" +#include "project.h" +#include "validator.h" + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; + +NewMapGroupDialog::NewMapGroupDialog(Project* project, QWidget *parent) : + QDialog(parent), + ui(new Ui::NewMapGroupDialog) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + this->project = project; + + ui->lineEdit_Name->setValidator(new IdentifierValidator(this)); + ui->lineEdit_Name->setText(Project::getMapGroupPrefix()); + + connect(ui->lineEdit_Name, &QLineEdit::textChanged, this, &NewMapGroupDialog::onNameChanged); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewMapGroupDialog::dialogButtonClicked); + + adjustSize(); +} + +NewMapGroupDialog::~NewMapGroupDialog() +{ + delete ui; +} + +void NewMapGroupDialog::onNameChanged(const QString &) { + validateName(true); +} + +bool NewMapGroupDialog::validateName(bool allowEmpty) { + const QString name = ui->lineEdit_Name->text(); + + QString errorText; + if (name.isEmpty()) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_Name->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; +} + +void NewMapGroupDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewMapGroupDialog::accept() { + if (!validateName()) + return; + + this->project->addNewMapGroup(ui->lineEdit_Name->text()); + + QDialog::accept(); +} diff --git a/src/ui/newmappopup.cpp b/src/ui/newmappopup.cpp deleted file mode 100644 index def485c2..00000000 --- a/src/ui/newmappopup.cpp +++ /dev/null @@ -1,361 +0,0 @@ -#include "newmappopup.h" -#include "maplayout.h" -#include "mainwindow.h" -#include "ui_newmappopup.h" -#include "config.h" - -#include -#include -#include - -// TODO: Convert to modal dialog (among other things, this means we wouldn't need to worry about changes to the map list while this is open) - -struct NewMapPopup::Settings NewMapPopup::settings = {}; - -NewMapPopup::NewMapPopup(QWidget *parent, Project *project) : - QMainWindow(parent), - ui(new Ui::NewMapPopup) -{ - this->setAttribute(Qt::WA_DeleteOnClose); - ui->setupUi(this); - this->project = project; - this->existingLayout = false; - this->importedMap = false; -} - -NewMapPopup::~NewMapPopup() -{ - saveSettings(); - delete ui; -} - -void NewMapPopup::initUi() { - // Populate combo boxes - ui->comboBox_NewMap_Primary_Tileset->addItems(project->primaryTilesetLabels); - ui->comboBox_NewMap_Secondary_Tileset->addItems(project->secondaryTilesetLabels); - ui->comboBox_NewMap_Group->addItems(project->groupNames); - ui->comboBox_NewMap_Song->addItems(project->songNames); - ui->comboBox_NewMap_Type->addItems(project->mapTypes); - ui->comboBox_NewMap_Location->addItems(project->mapSectionIdNames); - - const QSignalBlocker b(ui->comboBox_Layout); - ui->comboBox_Layout->addItems(project->mapLayoutsTable); - this->layoutId = project->mapLayoutsTable.first(); - - // Set spin box limits - ui->spinBox_NewMap_Width->setMinimum(1); - ui->spinBox_NewMap_Height->setMinimum(1); - ui->spinBox_NewMap_Width->setMaximum(project->getMaxMapWidth()); - ui->spinBox_NewMap_Height->setMaximum(project->getMaxMapHeight()); - ui->spinBox_NewMap_BorderWidth->setMinimum(1); - ui->spinBox_NewMap_BorderHeight->setMinimum(1); - ui->spinBox_NewMap_BorderWidth->setMaximum(MAX_BORDER_WIDTH); - ui->spinBox_NewMap_BorderHeight->setMaximum(MAX_BORDER_HEIGHT); - ui->spinBox_NewMap_Floor_Number->setMinimum(-128); - ui->spinBox_NewMap_Floor_Number->setMaximum(127); - - // Hide config specific ui elements - bool hasFlags = projectConfig.mapAllowFlagsEnabled; - ui->checkBox_NewMap_Allow_Running->setVisible(hasFlags); - ui->checkBox_NewMap_Allow_Biking->setVisible(hasFlags); - ui->checkBox_NewMap_Allow_Escape_Rope->setVisible(hasFlags); - ui->label_NewMap_Allow_Running->setVisible(hasFlags); - ui->label_NewMap_Allow_Biking->setVisible(hasFlags); - ui->label_NewMap_Allow_Escape_Rope->setVisible(hasFlags); - - bool hasCustomBorders = projectConfig.useCustomBorderSize; - ui->spinBox_NewMap_BorderWidth->setVisible(hasCustomBorders); - ui->spinBox_NewMap_BorderHeight->setVisible(hasCustomBorders); - ui->label_NewMap_BorderWidth->setVisible(hasCustomBorders); - ui->label_NewMap_BorderHeight->setVisible(hasCustomBorders); - - bool hasFloorNumber = projectConfig.floorNumberEnabled; - ui->spinBox_NewMap_Floor_Number->setVisible(hasFloorNumber); - ui->label_NewMap_Floor_Number->setVisible(hasFloorNumber); - - this->updateGeometry(); -} - -void NewMapPopup::init() { - // Restore previous settings - ui->lineEdit_NewMap_Name->setText(project->getNewMapName()); - ui->comboBox_NewMap_Group->setTextItem(settings.group); - ui->spinBox_NewMap_Width->setValue(settings.width); - ui->spinBox_NewMap_Height->setValue(settings.height); - ui->spinBox_NewMap_BorderWidth->setValue(settings.borderWidth); - ui->spinBox_NewMap_BorderHeight->setValue(settings.borderHeight); - ui->comboBox_NewMap_Primary_Tileset->setTextItem(settings.primaryTilesetLabel); - ui->comboBox_NewMap_Secondary_Tileset->setTextItem(settings.secondaryTilesetLabel); - ui->comboBox_NewMap_Type->setTextItem(settings.type); - ui->comboBox_NewMap_Location->setTextItem(settings.location); - ui->comboBox_NewMap_Song->setTextItem(settings.song); - ui->checkBox_NewMap_Flyable->setChecked(settings.canFlyTo); - ui->checkBox_NewMap_Show_Location->setChecked(settings.showLocationName); - ui->checkBox_NewMap_Allow_Running->setChecked(settings.allowRunning); - ui->checkBox_NewMap_Allow_Biking->setChecked(settings.allowBiking); - ui->checkBox_NewMap_Allow_Escape_Rope->setChecked(settings.allowEscaping); - ui->spinBox_NewMap_Floor_Number->setValue(settings.floorNumber); - - // Connect signals - connect(ui->spinBox_NewMap_Width, QOverload::of(&QSpinBox::valueChanged), [=](int){checkNewMapDimensions();}); - connect(ui->spinBox_NewMap_Height, QOverload::of(&QSpinBox::valueChanged), [=](int){checkNewMapDimensions();}); - - ui->frame_NewMap_Options->setEnabled(true); -} - -// Creating new map by right-clicking in the map list -void NewMapPopup::init(int tabIndex, QString fieldName) { - initUi(); - switch (tabIndex) - { - case MapListTab::Groups: - settings.group = fieldName; - break; - case MapListTab::Areas: - settings.location = fieldName; - break; - case MapListTab::Layouts: - this->ui->checkBox_UseExistingLayout->setCheckState(Qt::Checked); - useLayout(fieldName); - break; - } - init(); -} - -// Creating new map from AdvanceMap import -void NewMapPopup::init(Layout *mapLayout) { - initUi(); - this->importedMap = true; - useLayoutSettings(mapLayout); - - this->map = new Map(); - this->map->layout = new Layout(); - this->map->layout->blockdata = mapLayout->blockdata; - - if (!mapLayout->border.isEmpty()) { - this->map->layout->border = mapLayout->border; - } - init(); -} - -bool NewMapPopup::checkNewMapDimensions() { - int numMetatiles = project->getMapDataSize(ui->spinBox_NewMap_Width->value(), ui->spinBox_NewMap_Height->value()); - int maxMetatiles = project->getMaxMapDataSize(); - - if (numMetatiles > maxMetatiles) { - ui->frame_NewMap_Warning->setVisible(true); - QString errorText = QString("Error: The specified width and height are too large.\n" - "The maximum map width and height is the following: (width + 15) * (height + 14) <= %1\n" - "The specified map width and height was: (%2 + 15) * (%3 + 14) = %4") - .arg(maxMetatiles) - .arg(ui->spinBox_NewMap_Width->value()) - .arg(ui->spinBox_NewMap_Height->value()) - .arg(numMetatiles); - ui->label_NewMap_WarningMessage->setText(errorText); - ui->label_NewMap_WarningMessage->setWordWrap(true); - return false; - } - else { - ui->frame_NewMap_Warning->setVisible(false); - ui->label_NewMap_WarningMessage->clear(); - return true; - } -} - -bool NewMapPopup::checkNewMapGroup() { - group = project->groupNames.indexOf(this->ui->comboBox_NewMap_Group->currentText()); - - if (group < 0) { - ui->frame_NewMap_Warning->setVisible(true); - QString errorText = QString("Error: The specified map group '%1' does not exist.") - .arg(ui->comboBox_NewMap_Group->currentText()); - ui->label_NewMap_WarningMessage->setText(errorText); - ui->label_NewMap_WarningMessage->setWordWrap(true); - return false; - } else { - ui->frame_NewMap_Warning->setVisible(false); - ui->label_NewMap_WarningMessage->clear(); - return true; - } -} - -void NewMapPopup::setDefaultSettings(Project *project) { - settings.group = project->groupNames.at(0); - settings.width = project->getDefaultMapSize(); - settings.height = project->getDefaultMapSize(); - settings.borderWidth = DEFAULT_BORDER_WIDTH; - settings.borderHeight = DEFAULT_BORDER_HEIGHT; - settings.primaryTilesetLabel = project->getDefaultPrimaryTilesetLabel(); - settings.secondaryTilesetLabel = project->getDefaultSecondaryTilesetLabel(); - settings.type = project->mapTypes.value(0, "0"); - settings.location = project->mapSectionIdNames.value(0, "0"); - settings.song = project->defaultSong; - settings.canFlyTo = false; - settings.showLocationName = true; - settings.allowRunning = false; - settings.allowBiking = false; - settings.allowEscaping = false; - settings.floorNumber = 0; -} - -void NewMapPopup::saveSettings() { - settings.group = ui->comboBox_NewMap_Group->currentText(); - settings.width = ui->spinBox_NewMap_Width->value(); - settings.height = ui->spinBox_NewMap_Height->value(); - settings.borderWidth = ui->spinBox_NewMap_BorderWidth->value(); - settings.borderHeight = ui->spinBox_NewMap_BorderHeight->value(); - settings.primaryTilesetLabel = ui->comboBox_NewMap_Primary_Tileset->currentText(); - settings.secondaryTilesetLabel = ui->comboBox_NewMap_Secondary_Tileset->currentText(); - settings.type = ui->comboBox_NewMap_Type->currentText(); - settings.location = ui->comboBox_NewMap_Location->currentText(); - settings.song = ui->comboBox_NewMap_Song->currentText(); - settings.canFlyTo = ui->checkBox_NewMap_Flyable->isChecked(); - settings.showLocationName = ui->checkBox_NewMap_Show_Location->isChecked(); - settings.allowRunning = ui->checkBox_NewMap_Allow_Running->isChecked(); - settings.allowBiking = ui->checkBox_NewMap_Allow_Biking->isChecked(); - settings.allowEscaping = ui->checkBox_NewMap_Allow_Escape_Rope->isChecked(); - settings.floorNumber = ui->spinBox_NewMap_Floor_Number->value(); -} - -void NewMapPopup::useLayoutSettings(Layout *layout) { - if (!layout) return; - - settings.width = layout->width; - ui->spinBox_NewMap_Width->setValue(layout->width); - - settings.height = layout->height; - ui->spinBox_NewMap_Height->setValue(layout->height); - - settings.borderWidth = layout->border_width; - ui->spinBox_NewMap_BorderWidth->setValue(layout->border_width); - - settings.borderHeight = layout->border_height; - ui->spinBox_NewMap_BorderWidth->setValue(layout->border_height); - - settings.primaryTilesetLabel = layout->tileset_primary_label; - ui->comboBox_NewMap_Primary_Tileset->setCurrentIndex(ui->comboBox_NewMap_Primary_Tileset->findText(layout->tileset_primary_label)); - - settings.secondaryTilesetLabel = layout->tileset_secondary_label; - ui->comboBox_NewMap_Secondary_Tileset->setCurrentIndex(ui->comboBox_NewMap_Secondary_Tileset->findText(layout->tileset_secondary_label)); -} - -void NewMapPopup::useLayout(QString layoutId) { - this->existingLayout = true; - this->layoutId = layoutId; - - this->ui->comboBox_Layout->setCurrentIndex(this->ui->comboBox_Layout->findText(layoutId)); - - useLayoutSettings(project->mapLayouts.value(this->layoutId)); -} - -void NewMapPopup::on_checkBox_UseExistingLayout_stateChanged(int state) { - bool layoutEditsEnabled = (state == Qt::Unchecked); - - this->ui->comboBox_Layout->setEnabled(!layoutEditsEnabled); - - this->ui->spinBox_NewMap_Width->setEnabled(layoutEditsEnabled); - this->ui->spinBox_NewMap_Height->setEnabled(layoutEditsEnabled); - this->ui->spinBox_NewMap_BorderWidth->setEnabled(layoutEditsEnabled); - this->ui->spinBox_NewMap_BorderWidth->setEnabled(layoutEditsEnabled); - this->ui->comboBox_NewMap_Primary_Tileset->setEnabled(layoutEditsEnabled); - this->ui->comboBox_NewMap_Secondary_Tileset->setEnabled(layoutEditsEnabled); - - if (!layoutEditsEnabled) { - useLayout(this->layoutId);//this->ui->comboBox_Layout->currentText()); - } else { - this->existingLayout = false; - } -} - -void NewMapPopup::on_comboBox_Layout_currentTextChanged(const QString &text) { - if (this->project->mapLayoutsTable.contains(text)) { - useLayout(text); - } -} - -void NewMapPopup::on_lineEdit_NewMap_Name_textChanged(const QString &text) { - if (project->mapNames.contains(text)) { - this->ui->lineEdit_NewMap_Name->setStyleSheet("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); - } else { - this->ui->lineEdit_NewMap_Name->setStyleSheet(""); - } -} - -void NewMapPopup::on_pushButton_NewMap_Accept_clicked() { - if (!checkNewMapDimensions() || !checkNewMapGroup()) { - // ignore when map dimensions or map group are invalid - return; - } - Map *newMap = new Map; - Layout *layout; - - // If map name is not unique, use default value. Also use only valid characters. - // After stripping invalid characters, strip any leading digits. - static const QRegularExpression re_invalidChars("[^a-zA-Z0-9_]+"); - QString newMapName = this->ui->lineEdit_NewMap_Name->text().remove(re_invalidChars); - static const QRegularExpression re_NaN("^[0-9]*"); - newMapName.remove(re_NaN); - if (project->mapNames.contains(newMapName) || newMapName.isEmpty()) { - newMapName = project->getNewMapName(); - } - - newMap->name = newMapName; - newMap->type = this->ui->comboBox_NewMap_Type->currentText(); - newMap->location = this->ui->comboBox_NewMap_Location->currentText(); - newMap->song = this->ui->comboBox_NewMap_Song->currentText(); - newMap->requiresFlash = false; - newMap->weather = this->project->weatherNames.value(0, "0"); - newMap->show_location = this->ui->checkBox_NewMap_Show_Location->isChecked(); - newMap->battle_scene = this->project->mapBattleScenes.value(0, "0"); - - if (this->existingLayout) { - layout = this->project->mapLayouts.value(this->layoutId); - newMap->needsLayoutDir = false; - } else { - layout = new Layout; - layout->id = Layout::layoutConstantFromName(newMapName); - layout->name = QString("%1_Layout").arg(newMap->name); - layout->width = this->ui->spinBox_NewMap_Width->value(); - layout->height = this->ui->spinBox_NewMap_Height->value(); - if (projectConfig.useCustomBorderSize) { - layout->border_width = this->ui->spinBox_NewMap_BorderWidth->value(); - layout->border_height = this->ui->spinBox_NewMap_BorderHeight->value(); - } else { - layout->border_width = DEFAULT_BORDER_WIDTH; - layout->border_height = DEFAULT_BORDER_HEIGHT; - } - layout->tileset_primary_label = this->ui->comboBox_NewMap_Primary_Tileset->currentText(); - layout->tileset_secondary_label = this->ui->comboBox_NewMap_Secondary_Tileset->currentText(); - QString basePath = projectConfig.getFilePath(ProjectFilePath::data_layouts_folders); - layout->border_path = QString("%1%2/border.bin").arg(basePath, newMapName); - layout->blockdata_path = QString("%1%2/map.bin").arg(basePath, newMapName); - } - - if (this->importedMap) { - layout->blockdata = map->layout->blockdata; - if (!map->layout->border.isEmpty()) - layout->border = map->layout->border; - } - - if (this->ui->checkBox_NewMap_Flyable->isChecked()) { - newMap->needsHealLocation = true; - } - - if (projectConfig.mapAllowFlagsEnabled) { - newMap->allowRunning = this->ui->checkBox_NewMap_Allow_Running->isChecked(); - newMap->allowBiking = this->ui->checkBox_NewMap_Allow_Biking->isChecked(); - newMap->allowEscaping = this->ui->checkBox_NewMap_Allow_Escape_Rope->isChecked(); - } - if (projectConfig.floorNumberEnabled) { - newMap->floorNumber = this->ui->spinBox_NewMap_Floor_Number->value(); - } - - newMap->layout = layout; - newMap->layoutId = layout->id; - if (this->existingLayout) { - project->loadMapLayout(newMap); - } - map = newMap; - emit applied(); - this->close(); -} diff --git a/src/ui/newtilesetdialog.cpp b/src/ui/newtilesetdialog.cpp index e9eee946..968b3b88 100644 --- a/src/ui/newtilesetdialog.cpp +++ b/src/ui/newtilesetdialog.cpp @@ -1,49 +1,80 @@ #include "newtilesetdialog.h" #include "ui_newtilesetdialog.h" #include "project.h" +#include "imageexport.h" +#include "validator.h" + +const QString lineEdit_ErrorStylesheet = "QLineEdit { background-color: rgba(255, 0, 0, 25%) }"; NewTilesetDialog::NewTilesetDialog(Project* project, QWidget *parent) : QDialog(parent), - ui(new Ui::NewTilesetDialog) + ui(new Ui::NewTilesetDialog), + symbolPrefix(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix)) { + setAttribute(Qt::WA_DeleteOnClose); ui->setupUi(this); - this->setFixedSize(this->width(), this->height()); this->project = project; - //only allow characters valid for a symbol - static const QRegularExpression expression("[_A-Za-z0-9]+$"); - QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression); - this->ui->nameLineEdit->setValidator(validator); - bool checkerboard = porymapConfig.tilesetCheckerboardFill; - this->ui->fillCheckBox->setChecked(checkerboard); - this->checkerboardFill = checkerboard; + ui->checkBox_CheckerboardFill->setChecked(porymapConfig.tilesetCheckerboardFill); + ui->comboBox_Type->setMinimumContentsLength(12 + this->symbolPrefix.length()); + ui->lineEdit_Name->setValidator(new IdentifierValidator(this->symbolPrefix, this)); + ui->lineEdit_Name->setText(this->symbolPrefix); - connect(this->ui->nameLineEdit, &QLineEdit::textChanged, this, &NewTilesetDialog::NameOrSecondaryChanged); - connect(this->ui->typeComboBox, &QComboBox::currentTextChanged, this, &NewTilesetDialog::SecondaryChanged); - connect(this->ui->fillCheckBox, &QCheckBox::stateChanged, this, &NewTilesetDialog::FillChanged); - //connect(this->ui->toolButton, &QToolButton::clicked, this, &NewTilesetDialog::ChangeFilePath); - this->SecondaryChanged(); + connect(ui->lineEdit_Name, &QLineEdit::textChanged, this, &NewTilesetDialog::onNameChanged); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &NewTilesetDialog::dialogButtonClicked); + + adjustSize(); } NewTilesetDialog::~NewTilesetDialog() { + porymapConfig.tilesetCheckerboardFill = ui->checkBox_CheckerboardFill->isChecked(); delete ui; } -void NewTilesetDialog::SecondaryChanged(){ - this->isSecondary = (this->ui->typeComboBox->currentIndex() == 1); - NameOrSecondaryChanged(); +void NewTilesetDialog::onNameChanged(const QString &) { + validateName(true); } -void NewTilesetDialog::NameOrSecondaryChanged() { - this->friendlyName = this->ui->nameLineEdit->text(); - this->fullSymbolName = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix) + this->friendlyName; - this->ui->symbolNameLineEdit->setText(this->fullSymbolName); - this->path = Tileset::getExpectedDir(this->fullSymbolName, this->isSecondary); - this->ui->pathLineEdit->setText(this->path); +bool NewTilesetDialog::validateName(bool allowEmpty) { + const QString name = ui->lineEdit_Name->text(); + + QString errorText; + if (name.isEmpty() || name == symbolPrefix) { + if (!allowEmpty) errorText = QString("%1 cannot be empty.").arg(ui->label_Name->text()); + } else if (!this->project->isIdentifierUnique(name)) { + errorText = QString("%1 '%2' is not unique.").arg(ui->label_Name->text()).arg(name); + } + + bool isValid = errorText.isEmpty(); + ui->label_NameError->setText(errorText); + ui->label_NameError->setVisible(!isValid); + ui->lineEdit_Name->setStyleSheet(!isValid ? lineEdit_ErrorStylesheet : ""); + return isValid; } -void NewTilesetDialog::FillChanged() { - this->checkerboardFill = this->ui->fillCheckBox->isChecked(); - porymapConfig.tilesetCheckerboardFill = this->checkerboardFill; +void NewTilesetDialog::dialogButtonClicked(QAbstractButton *button) { + auto role = ui->buttonBox->buttonRole(button); + if (role == QDialogButtonBox::RejectRole){ + reject(); + } else if (role == QDialogButtonBox::AcceptRole) { + accept(); + } +} + +void NewTilesetDialog::accept() { + if (!validateName()) + return; + + bool secondary = ui->comboBox_Type->currentIndex() == 1; + Tileset *tileset = this->project->createNewTileset(ui->lineEdit_Name->text(), secondary, ui->checkBox_CheckerboardFill->isChecked()); + if (!tileset) { + ui->label_GenericError->setText(QString("Failed to create tileset. See %1 for details.").arg(getLogPath())); + ui->label_GenericError->setVisible(true); + return; + } + ui->label_GenericError->setVisible(false); + + QDialog::accept(); + emit applied(tileset); } diff --git a/src/ui/prefab.cpp b/src/ui/prefab.cpp index 10642178..e1fc83e9 100644 --- a/src/ui/prefab.cpp +++ b/src/ui/prefab.cpp @@ -303,7 +303,7 @@ bool Prefab::tryImportDefaultPrefabs(QWidget * parent, BaseGameVersion version, // into their project. QMessageBox::StandardButton prompt = QMessageBox::question(parent, - "Import Default Prefabs", + QApplication::applicationName(), QString("Would you like to import the default prefabs for %1? %2.") .arg(projectConfig.getBaseGameVersionString(version)) .arg(fileWarning), diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp index 1c68137c..016480a7 100644 --- a/src/ui/projectsettingseditor.cpp +++ b/src/ui/projectsettingseditor.cpp @@ -116,11 +116,6 @@ void ProjectSettingsEditor::initUi() { ui->lineEdit_BorderMetatiles->setValidator(validator_HexList); this->setBorderMetatilesUi(projectConfig.useCustomBorderSize); - // Validate that the text added to the warp behavior list could be a valid define - // (we don't care whether it actually is a metatile behavior define) - static const QRegularExpression expression_Word("^[A-Za-z0-9_]*$"); - QRegularExpressionValidator *validator_Word = new QRegularExpressionValidator(expression_Word); - ui->comboBox_WarpBehaviors->setValidator(validator_Word); ui->textEdit_WarpBehaviors->setTextColor(Qt::gray); // Set spin box limits @@ -298,7 +293,7 @@ QStringList ProjectSettingsEditor::getWarpBehaviorsList() { void ProjectSettingsEditor::setWarpBehaviorsList(QStringList list) { list.removeDuplicates(); - list.sort(); + Project::numericalModeSort(list); ui->textEdit_WarpBehaviors->setText(list.join("\n")); } @@ -400,7 +395,7 @@ QString ProjectSettingsEditor::chooseProjectFile(const QString &defaultFilepath) if (!path.startsWith(this->baseDir)){ // Most of Porymap's file-parsing code for project files will assume that filepaths // are relative to the root project folder, so we enforce that here. - QMessageBox::warning(this, "Failed to set custom filepath", + QMessageBox::warning(this, QApplication::applicationName(), QString("Custom filepaths must be inside the root project folder '%1'").arg(this->baseDir)); return QString(); } diff --git a/src/ui/regionmapeditor.cpp b/src/ui/regionmapeditor.cpp index c6485d51..af0e220b 100644 --- a/src/ui/regionmapeditor.cpp +++ b/src/ui/regionmapeditor.cpp @@ -28,6 +28,7 @@ RegionMapEditor::RegionMapEditor(QWidget *parent, Project *project) : this->setAttribute(Qt::WA_DeleteOnClose); this->ui->setupUi(this); this->project = project; + connect(this->project, &Project::mapSectionIdNamesChanged, this, &RegionMapEditor::setLocations); this->configFilepath = QString("%1/%2").arg(this->project->root).arg(projectConfig.getFilePath(ProjectFilePath::json_region_porymap_cfg)); this->initShortcuts(); this->restoreWindowState(); @@ -651,10 +652,9 @@ void RegionMapEditor::displayRegionMapLayout() { void RegionMapEditor::displayRegionMapLayoutOptions() { if (!this->region_map->layoutEnabled()) return; - this->ui->comboBox_RM_ConnectedMap->blockSignals(true); + const QSignalBlocker b(ui->comboBox_RM_ConnectedMap); this->ui->comboBox_RM_ConnectedMap->clear(); this->ui->comboBox_RM_ConnectedMap->addItems(this->project->mapSectionIdNames); - this->ui->comboBox_RM_ConnectedMap->blockSignals(false); this->ui->frame_RM_Options->setEnabled(true); @@ -662,22 +662,19 @@ void RegionMapEditor::displayRegionMapLayoutOptions() { } void RegionMapEditor::updateRegionMapLayoutOptions(int index) { - this->ui->comboBox_RM_ConnectedMap->blockSignals(true); + const QSignalBlocker b_ConnectedMap(ui->comboBox_RM_ConnectedMap); this->ui->comboBox_RM_ConnectedMap->setCurrentText(this->region_map->squareMapSection(index)); - this->ui->comboBox_RM_ConnectedMap->blockSignals(false); this->ui->pushButton_RM_Options_delete->setEnabled(this->region_map->squareHasMap(index)); - this->ui->spinBox_RM_LayoutWidth->blockSignals(true); - this->ui->spinBox_RM_LayoutHeight->blockSignals(true); + const QSignalBlocker b_LayoutWidth(ui->spinBox_RM_LayoutWidth); + const QSignalBlocker b_LayoutHeight(ui->spinBox_RM_LayoutHeight); this->ui->spinBox_RM_LayoutWidth->setMinimum(1); this->ui->spinBox_RM_LayoutWidth->setMaximum(this->region_map->tilemapWidth() - this->region_map->padLeft()); this->ui->spinBox_RM_LayoutHeight->setMinimum(1); this->ui->spinBox_RM_LayoutHeight->setMaximum(this->region_map->tilemapHeight() - this->region_map->padTop()); this->ui->spinBox_RM_LayoutWidth->setValue(this->region_map->layoutWidth()); this->ui->spinBox_RM_LayoutHeight->setValue(this->region_map->layoutHeight()); - this->ui->spinBox_RM_LayoutWidth->blockSignals(false); - this->ui->spinBox_RM_LayoutHeight->blockSignals(false); } void RegionMapEditor::displayRegionMapEntriesImage() { @@ -732,8 +729,12 @@ void RegionMapEditor::displayRegionMapEntryOptions() { void RegionMapEditor::updateRegionMapEntryOptions(QString section) { if (!this->region_map->layoutEnabled()) return; + const QSignalBlocker b_X(this->ui->spinBox_RM_Entry_x); + const QSignalBlocker b_Y(this->ui->spinBox_RM_Entry_y); + const QSignalBlocker b_W(this->ui->spinBox_RM_Entry_width); + const QSignalBlocker b_H(this->ui->spinBox_RM_Entry_height); + bool enabled = (section != this->region_map->default_map_section) && this->region_map_entries.contains(section); - this->ui->lineEdit_RM_MapName->setEnabled(enabled); this->ui->spinBox_RM_Entry_x->setEnabled(enabled); this->ui->spinBox_RM_Entry_y->setEnabled(enabled); this->ui->spinBox_RM_Entry_width->setEnabled(enabled); @@ -741,56 +742,31 @@ void RegionMapEditor::updateRegionMapEntryOptions(QString section) { this->ui->pushButton_entryActivate->setEnabled(section != this->region_map->default_map_section); this->ui->pushButton_entryActivate->setText(enabled ? "Remove" : "Add"); - this->ui->lineEdit_RM_MapName->blockSignals(true); - this->ui->spinBox_RM_Entry_x->blockSignals(true); - this->ui->spinBox_RM_Entry_y->blockSignals(true); - this->ui->spinBox_RM_Entry_width->blockSignals(true); - this->ui->spinBox_RM_Entry_height->blockSignals(true); - this->ui->comboBox_RM_Entry_MapSection->setCurrentText(section); this->activeEntry = section; this->region_map_entries_item->currentSection = section; MapSectionEntry entry = enabled ? this->region_map_entries[section] : MapSectionEntry(); - this->ui->lineEdit_RM_MapName->setText(entry.name); this->ui->spinBox_RM_Entry_x->setValue(entry.x); this->ui->spinBox_RM_Entry_y->setValue(entry.y); this->ui->spinBox_RM_Entry_width->setValue(entry.width); this->ui->spinBox_RM_Entry_height->setValue(entry.height); - - this->ui->lineEdit_RM_MapName->blockSignals(false); - this->ui->spinBox_RM_Entry_x->blockSignals(false); - this->ui->spinBox_RM_Entry_y->blockSignals(false); - this->ui->spinBox_RM_Entry_width->blockSignals(false); - this->ui->spinBox_RM_Entry_height->blockSignals(false); } void RegionMapEditor::on_pushButton_entryActivate_clicked() { QString section = this->ui->comboBox_RM_Entry_MapSection->currentText(); if (section == this->region_map->default_map_section) return; + MapSectionEntry oldEntry = this->region_map->getEntry(section); if (this->region_map_entries.contains(section)) { // disable - MapSectionEntry oldEntry = this->region_map->getEntry(section); - this->region_map->removeEntry(section); - MapSectionEntry newEntry = this->region_map->getEntry(section); - RemoveEntry *commit = new RemoveEntry(this->region_map, section, oldEntry, newEntry); - this->region_map->editHistory.push(commit); - updateRegionMapEntryOptions(section); - - this->ui->pushButton_entryActivate->setText("Add"); + this->region_map->editHistory.push(new RemoveEntry(this->region_map, section, oldEntry, MapSectionEntry())); } else { // enable - MapSectionEntry oldEntry = this->region_map->getEntry(section); - MapSectionEntry entry = MapSectionEntry(); - entry.valid = true; - this->region_map->setEntry(section, entry); - MapSectionEntry newEntry = this->region_map->getEntry(section); - AddEntry *commit = new AddEntry(this->region_map, section, oldEntry, newEntry); - this->region_map->editHistory.push(commit); - updateRegionMapEntryOptions(section); - - this->ui->pushButton_entryActivate->setText("Remove"); + MapSectionEntry newEntry = MapSectionEntry(); + newEntry.valid = true; + this->region_map->editHistory.push(new AddEntry(this->region_map, section, oldEntry, newEntry)); } + updateRegionMapEntryOptions(section); } void RegionMapEditor::displayRegionMapTileSelector() { @@ -936,7 +912,7 @@ void RegionMapEditor::on_tabWidget_Region_Map_currentChanged(int index) { break; case 2: this->ui->verticalSlider_Zoom_Image_Tiles->setVisible(false); - on_comboBox_RM_Entry_MapSection_textActivated(ui->comboBox_RM_Entry_MapSection->currentText()); + on_comboBox_RM_Entry_MapSection_currentTextChanged(ui->comboBox_RM_Entry_MapSection->currentText()); break; } } @@ -947,7 +923,7 @@ void RegionMapEditor::on_comboBox_RM_ConnectedMap_textActivated(const QString &m onRegionMapLayoutSelectedTileChanged(this->currIndex);// re-draw layout image } -void RegionMapEditor::on_comboBox_RM_Entry_MapSection_textActivated(const QString &text) { +void RegionMapEditor::on_comboBox_RM_Entry_MapSection_currentTextChanged(const QString &text) { this->activeEntry = text; this->region_map_entries_item->currentSection = text; updateRegionMapEntryOptions(text); @@ -1050,15 +1026,6 @@ void RegionMapEditor::on_spinBox_RM_LayoutHeight_valueChanged(int value) { } } -void RegionMapEditor::on_lineEdit_RM_MapName_textEdited(const QString &text) { - if (!this->region_map_entries.contains(activeEntry)) return; - MapSectionEntry oldEntry = this->region_map_entries[activeEntry]; - this->region_map_entries[activeEntry].name = text; - MapSectionEntry newEntry = this->region_map_entries[activeEntry]; - EditEntry *commit = new EditEntry(this->region_map, activeEntry, oldEntry, newEntry); - this->region_map->editHistory.push(commit); -} - void RegionMapEditor::on_pushButton_RM_Options_delete_clicked() { int index = this->region_map->tilemapToLayoutIndex(this->currIndex); QList oldLayout = this->region_map->getLayout(this->region_map->getLayer()); @@ -1151,7 +1118,7 @@ void RegionMapEditor::on_action_Swap_triggered() { connect(&buttonBox, &QDialogButtonBox::accepted, [&popup, &oldSecBox, &newSecBox, &beforeSection, &afterSection](){ beforeSection = oldSecBox->currentText(); afterSection = newSecBox->currentText(); - if (!beforeSection.isEmpty() && !afterSection.isEmpty()) { + if (!beforeSection.isEmpty() && !afterSection.isEmpty() && beforeSection != afterSection) { popup.accept(); } }); @@ -1190,7 +1157,7 @@ void RegionMapEditor::on_action_Replace_triggered() { connect(&buttonBox, &QDialogButtonBox::accepted, [&popup, &oldSecBox, &newSecBox, &beforeSection, &afterSection](){ beforeSection = oldSecBox->currentText(); afterSection = newSecBox->currentText(); - if (!beforeSection.isEmpty() && !afterSection.isEmpty()) { + if (!beforeSection.isEmpty() && !afterSection.isEmpty() && beforeSection != afterSection) { popup.accept(); } }); @@ -1269,7 +1236,7 @@ void RegionMapEditor::closeEvent(QCloseEvent *event) if (this->modified()) { QMessageBox::StandardButton result = QMessageBox::question( this, - "porymap", + QApplication::applicationName(), "The region map has been modified, save changes?", QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); @@ -1323,3 +1290,18 @@ void RegionMapEditor::on_verticalSlider_Zoom_Image_Tiles_valueChanged(int val) { ui->graphicsView_RegionMap_Tiles->setTransform(transform); ui->graphicsView_RegionMap_Tiles->setFixedSize(width + 2, height + 2); } + +// Repopulate the combo boxes that display MAPSEC names. +void RegionMapEditor::setLocations(const QStringList &locations) { + const QSignalBlocker b_ConnectedMap(ui->comboBox_RM_ConnectedMap); + auto before = ui->comboBox_RM_ConnectedMap->currentText(); + ui->comboBox_RM_ConnectedMap->clear(); + ui->comboBox_RM_ConnectedMap->addItems(locations); + ui->comboBox_RM_ConnectedMap->setCurrentText(before); + + const QSignalBlocker b_MapSection(ui->comboBox_RM_Entry_MapSection); + before = ui->comboBox_RM_Entry_MapSection->currentText(); + ui->comboBox_RM_Entry_MapSection->clear(); + ui->comboBox_RM_Entry_MapSection->addItems(locations); + ui->comboBox_RM_Entry_MapSection->setCurrentText(before); +} diff --git a/src/ui/regionmapentriespixmapitem.cpp b/src/ui/regionmapentriespixmapitem.cpp index ace3c38d..8ac888a0 100644 --- a/src/ui/regionmapentriespixmapitem.cpp +++ b/src/ui/regionmapentriespixmapitem.cpp @@ -8,7 +8,7 @@ void RegionMapEntriesPixmapItem::draw() { int entry_x, entry_y, entry_w, entry_h; - if (!entry.valid || entry.name == region_map->default_map_section) { + if (!entry.valid || currentSection == region_map->default_map_section) { entry_x = entry_y = 0; entry_w = entry_h = 1; } else { diff --git a/src/ui/shortcutseditor.cpp b/src/ui/shortcutseditor.cpp index 30f9d62a..36bc30d0 100644 --- a/src/ui/shortcutseditor.cpp +++ b/src/ui/shortcutseditor.cpp @@ -158,7 +158,7 @@ void ShortcutsEditor::promptUserOnDuplicateFound(MultiKeyEdit *sender, MultiKeyE .arg(duplicateKeySequence.toString()).arg(siblingLabel); const auto result = QMessageBox::question( - this, "porymap", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + this, QApplication::applicationName(), message, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (result == QMessageBox::Yes) removeKeySequence(duplicateKeySequence, sibling); diff --git a/src/ui/tileseteditor.cpp b/src/ui/tileseteditor.cpp index b8da1912..e05750dc 100644 --- a/src/ui/tileseteditor.cpp +++ b/src/ui/tileseteditor.cpp @@ -2,12 +2,13 @@ #include "ui_tileseteditor.h" #include "log.h" #include "imageproviders.h" -#include "metatileparser.h" +#include "advancemapparser.h" #include "paletteutil.h" #include "imageexport.h" #include "config.h" #include "shortcut.h" #include "filedialog.h" +#include "validator.h" #include #include #include @@ -20,9 +21,33 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent) layout(layout), hasUnsavedChanges(false) { - this->setAttribute(Qt::WA_DeleteOnClose); - this->setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); - this->initUi(); + setAttribute(Qt::WA_DeleteOnClose); + setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label); + ui->setupUi(this); + + this->tileXFlip = ui->checkBox_xFlip->isChecked(); + this->tileYFlip = ui->checkBox_yFlip->isChecked(); + this->paletteId = ui->spinBox_paletteSelector->value(); + + // TODO: The dividing line at the moment is only accurate if the number of primary metatiles is divisible by 8. + // If it's not, the secondary metatiles will wrap above the line. This has other problems (like skewing + // metatile groups the user may have designed) so this should be fixed by filling the primary metatiles + // image with invalid magenta metatiles until it's divisible by 8. Then the line can be re-enabled as-is. + ui->actionShow_Tileset_Divider->setChecked(/*porymapConfig.showTilesetEditorDivider*/false); + ui->actionShow_Tileset_Divider->setVisible(false); + + ui->spinBox_paletteSelector->setMinimum(0); + ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); + ui->lineEdit_metatileLabel->setValidator(new IdentifierValidator(this)); + + setAttributesUi(); + initMetatileSelector(); + initMetatileLayersItem(); + initTileSelector(); + initSelectedTileItem(); + initShortcuts(); + this->metatileSelector->select(0); + restoreWindowState(); } TilesetEditor::~TilesetEditor() @@ -57,7 +82,7 @@ void TilesetEditor::updateTilesets(QString primaryTilesetLabel, QString secondar if (this->hasUnsavedChanges) { QMessageBox::StandardButton result = QMessageBox::question( this, - "porymap", + QApplication::applicationName(), "Tileset has been modified, save changes?", QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes); @@ -92,33 +117,6 @@ void TilesetEditor::setTilesets(QString primaryTilesetLabel, QString secondaryTi this->initMetatileHistory(); } -void TilesetEditor::initUi() { - ui->setupUi(this); - this->tileXFlip = ui->checkBox_xFlip->isChecked(); - this->tileYFlip = ui->checkBox_yFlip->isChecked(); - this->paletteId = ui->spinBox_paletteSelector->value(); - this->ui->spinBox_paletteSelector->setMinimum(0); - this->ui->spinBox_paletteSelector->setMaximum(Project::getNumPalettesTotal() - 1); - - // TODO: The dividing line at the moment is only accurate if the number of primary metatiles is divisible by 8. - // If it's not, the secondary metatiles will wrap above the line. This has other problems (like skewing - // metatile groups the user may have designed) so this should be fixed by filling the primary metatiles - // image with invalid magenta metatiles until it's divisible by 8. Then the line can be re-enabled as-is. - this->ui->actionShow_Tileset_Divider->setChecked(/*porymapConfig.showTilesetEditorDivider*/false); - this->ui->actionShow_Tileset_Divider->setVisible(false); - - this->setAttributesUi(); - this->setMetatileLabelValidator(); - - this->initMetatileSelector(); - this->initMetatileLayersItem(); - this->initTileSelector(); - this->initSelectedTileItem(); - this->initShortcuts(); - this->metatileSelector->select(0); - this->restoreWindowState(); -} - void TilesetEditor::setAttributesUi() { // Behavior if (projectConfig.metatileBehaviorMask) { @@ -178,13 +176,6 @@ void TilesetEditor::setAttributesUi() { this->ui->frame_Properties->adjustSize(); } -void TilesetEditor::setMetatileLabelValidator() { - //only allow characters valid for a symbol - static const QRegularExpression expression("[_A-Za-z0-9]*$"); - QRegularExpressionValidator *validator = new QRegularExpressionValidator(expression); - this->ui->lineEdit_metatileLabel->setValidator(validator); -} - void TilesetEditor::initMetatileSelector() { this->metatileSelector = new TilesetEditorMetatileSelector(this->primaryTileset, this->secondaryTileset, this->layout); @@ -726,22 +717,9 @@ void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) { image = image.convertToFormat(QImage::Format::Format_Indexed8, colorTable); } - // Validate image is properly indexed to 16 colors. - int colorCount = image.colorCount(); - if (colorCount > 16) { - flattenTo4bppImage(&image); - } else if (colorCount < 16) { - QVector colorTable = image.colorTable(); - for (int i = colorTable.length(); i < 16; i++) { - colorTable.append(Qt::black); - } - image.setColorTable(colorTable); - } - - this->project->loadTilesetTiles(tileset, image); + tileset->loadTilesImage(&image); this->refresh(); this->hasUnsavedChanges = true; - tileset->hasUnsavedTilesImage = true; } void TilesetEditor::closeEvent(QCloseEvent *event) @@ -749,7 +727,7 @@ void TilesetEditor::closeEvent(QCloseEvent *event) if (this->hasUnsavedChanges) { QMessageBox::StandardButton result = QMessageBox::question( this, - "porymap", + QApplication::applicationName(), "Tileset has been modified, save changes?", QMessageBox::No | QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Yes); @@ -988,7 +966,7 @@ void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) } bool error = false; - QList metatiles = MetatileParser::parse(filepath, &error, primary); + QList metatiles = AdvanceMapParser::parseMetatiles(filepath, &error, primary); if (error) { QMessageBox msgBox(this); msgBox.setText("Failed to import metatiles from Advance Map 1.92 .bvd file."); @@ -997,6 +975,7 @@ void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary) msgBox.setDefaultButton(QMessageBox::Ok); msgBox.setIcon(QMessageBox::Icon::Critical); msgBox.exec(); + qDeleteAll(metatiles); return; } diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp index 32ff9bc9..80c41f81 100644 --- a/src/ui/wildmonchart.cpp +++ b/src/ui/wildmonchart.cpp @@ -465,7 +465,7 @@ void WildMonChart::showHelpDialog() { informativeText = levelTabInfo; } - QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this); + QMessageBox msgBox(QMessageBox::Information, QApplication::applicationName(), text, QMessageBox::Close, this); msgBox.setTextFormat(Qt::RichText); msgBox.setInformativeText(informativeText); msgBox.exec();