Add new metatile image export window

This commit is contained in:
GriffinR 2025-07-02 14:23:57 -04:00
parent b2798e77d4
commit 38e2772213
35 changed files with 1276 additions and 326 deletions

View File

@ -0,0 +1,526 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MetatileImageExporter</class>
<widget class="QDialog" name="MetatileImageExporter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>649</width>
<height>601</height>
</rect>
</property>
<property name="windowTitle">
<string>Export Metatiles Image</string>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="1">
<widget class="QFrame" name="frame_Options">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::ClickFocus</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_Options">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="scrollArea_Options">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_Options">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>304</width>
<height>532</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QGroupBox" name="groupBox_Tilesets">
<property name="title">
<string>Tilesets</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="topMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="1" column="0">
<widget class="QCheckBox" name="checkBox_SecondaryTileset">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, automatically update the metatile range to include the full secondary tileset.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Secondary Tileset</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="NoScrollComboBox" name="comboBox_PrimaryTileset">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="NoScrollComboBox" name="comboBox_SecondaryTileset">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="checkBox_PrimaryTileset">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, automatically update the metatile range to include the full primary tileset.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Primary Tileset</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_MetatileRange">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Metatile Range</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="topMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_MetatileStart">
<property name="text">
<string>Start</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="UIntHexSpinBox" name="spinBox_MetatileStart">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The metatile ID to start the rendered image at.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_MetatileEnd">
<property name="text">
<string>End</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="UIntHexSpinBox" name="spinBox_MetatileEnd">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The metatile ID to end the rendered image at.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Layers">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Each metatile consists of 3 layers of tiles. These layers can be toggled here by clicking the checkbox, or rearranged by clicking and dragging them up or down in the list.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="title">
<string>Layers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="ReorderableListWidget" name="listWidget_Layers">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustToContentsOnFirstShow</enum>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDropMode::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::DropAction::MoveAction</enum>
</property>
<property name="resizeMode">
<enum>QListView::ResizeMode::Adjust</enum>
</property>
<property name="itemAlignment">
<set>Qt::AlignmentFlag::AlignVCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Transparency">
<property name="title">
<string>Transparency</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="radioButton_TransparencyNormal">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, transparent pixels in the image will be rendered with alpha of 0.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Normal</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButton_TransparencyBlack">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, transparent pixels in the image will be rendered as black. This is the default in-game behavior.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Black</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButton_TransparencyFirst">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, transparent pixels in the image will be rendered using the first color in tileset palette 0. This is the default behavior of the GBA.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>First palette color</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Misc">
<property name="title">
<string>Miscellaneous</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="topMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_Placeholders">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;If checked, display the placeholder tiles that are rendered for the unused layer in-game. For a given metatile only 2 of the 3 tile layers are used, and the 3rd layer is filled with these placeholder tiles. The unused layer and placeholder tile change depending on the metatile's layer type.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Render placeholder metatiles</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_WidthMetatiles">
<property name="text">
<string>Width (metatiles)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="UIntSpinBox" name="spinBox_WidthMetatiles">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Width of the output image in metatiles.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_WidthPixels">
<property name="text">
<string>Width (pixels)</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="UIntSpinBox" name="spinBox_WidthPixels">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Width of the output image in pixels. Automatically rounded up to a multiple of a metatile's pixel width.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="minimum" stdset="0">
<number>16</number>
</property>
<property name="maximum" stdset="0">
<number>128</number>
</property>
<property name="singleStep" stdset="0">
<number>16</number>
</property>
<property name="value" stdset="0">
<number>128</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_Buttons">
<item>
<widget class="QPushButton" name="pushButton_Reset">
<property name="text">
<string>Reset</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_Close">
<property name="text">
<string>Close</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_Save">
<property name="text">
<string>Save</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="0" column="0">
<widget class="QGroupBox" name="groupBox_Preview">
<property name="title">
<string>Preview</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<property name="leftMargin">
<number>6</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>6</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<item row="0" column="0">
<widget class="QScrollArea" name="scrollArea_Preview">
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_Preview">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>285</width>
<height>551</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item row="2" column="3">
<widget class="QGraphicsView" name="graphicsView_Preview">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="mouseTracking">
<bool>false</bool>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustIgnored</enum>
</property>
<property name="dragMode">
<enum>QGraphicsView::DragMode::NoDrag</enum>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>NoScrollComboBox</class>
<extends>QComboBox</extends>
<header>noscrollcombobox.h</header>
</customwidget>
<customwidget>
<class>UIntSpinBox</class>
<extends>QAbstractSpinBox</extends>
<header>uintspinbox.h</header>
</customwidget>
<customwidget>
<class>UIntHexSpinBox</class>
<extends>UIntSpinBox</extends>
<header location="global">uintspinbox.h</header>
</customwidget>
<customwidget>
<class>ReorderableListWidget</class>
<extends>QListWidget</extends>
<header>metatileimageexporter.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@ -11,7 +11,7 @@
</rect>
</property>
<property name="focusPolicy">
<enum>Qt::FocusPolicy::ClickFocus</enum>
<enum>Qt::ClickFocus</enum>
</property>
<property name="windowTitle">
<string>Tileset Editor</string>
@ -21,7 +21,7 @@
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
@ -34,10 +34,10 @@
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
<enum>QFrame::Plain</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
@ -58,14 +58,14 @@
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop</set>
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_Metatiles">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>239</width>
<width>253</width>
<height>659</height>
</rect>
</property>
@ -88,17 +88,17 @@
<item row="0" column="0">
<widget class="NoScrollGraphicsView" name="graphicsView_Metatiles">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -121,7 +121,7 @@
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
@ -129,10 +129,10 @@
</widget>
<widget class="QFrame" name="frame_Editing">
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
@ -162,14 +162,14 @@
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Raised</enum>
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="sizeConstraint">
<enum>QLayout::SizeConstraint::SetMinimumSize</enum>
<enum>QLayout::SetMinimumSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
@ -255,7 +255,7 @@
</sizepolicy>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
<enum>QComboBox::NoInsert</enum>
</property>
</widget>
</item>
@ -274,10 +274,10 @@
</size>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
@ -290,17 +290,17 @@
</sizepolicy>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
<enum>QComboBox::NoInsert</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Maximum</enum>
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -319,7 +319,7 @@
</sizepolicy>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
<enum>QComboBox::NoInsert</enum>
</property>
</widget>
</item>
@ -361,7 +361,7 @@
<item row="16" column="0">
<spacer name="verticalSpacer_7">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -412,7 +412,7 @@
<item row="1" column="1">
<widget class="QCheckBox" name="checkBox_xFlip">
<property name="layoutDirection">
<enum>Qt::LayoutDirection::LeftToRight</enum>
<enum>Qt::LeftToRight</enum>
</property>
<property name="text">
<string/>
@ -436,10 +436,10 @@
<item row="3" column="0" colspan="2">
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Policy::Fixed</enum>
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -464,10 +464,10 @@
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
<enum>QFrame::Plain</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
@ -510,13 +510,13 @@
</size>
</property>
<property name="frameShape">
<enum>QFrame::Shape::StyledPanel</enum>
<enum>QFrame::StyledPanel</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
@ -526,7 +526,7 @@
<item row="6" column="0" colspan="2">
<spacer name="verticalSpacer_6">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -548,14 +548,14 @@
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignHCenter|Qt::AlignmentFlag::AlignTop</set>
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_Tiles">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>499</width>
<width>446</width>
<height>241</height>
</rect>
</property>
@ -575,17 +575,17 @@
<item row="0" column="0">
<widget class="NoScrollGraphicsView" name="graphicsView_Tiles">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -602,7 +602,7 @@
<item>
<widget class="QSlider" name="horizontalSlider_TilesZoom">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
@ -625,18 +625,34 @@
<property name="title">
<string>File</string>
</property>
<widget class="QMenu" name="menuExport_Tiles_Image">
<property name="title">
<string>Export Tiles Image</string>
</property>
<addaction name="actionExport_Primary_Tiles_Image"/>
<addaction name="actionExport_Secondary_Tiles_Image"/>
</widget>
<widget class="QMenu" name="menuImport_AdvanceMap_Metatiles">
<property name="title">
<string>Import Metatiles from Advance Map 1.92</string>
</property>
<addaction name="actionImport_Primary_AdvanceMap_Metatiles"/>
<addaction name="actionImport_Secondary_AdvanceMap_Metatiles"/>
</widget>
<widget class="QMenu" name="menuImport_Tiles_Image">
<property name="title">
<string>Import Tiles Image</string>
</property>
<addaction name="actionImport_Primary_Tiles_Image"/>
<addaction name="actionImport_Secondary_Tiles_Image"/>
</widget>
<addaction name="actionSave_Tileset"/>
<addaction name="separator"/>
<addaction name="actionImport_Primary_Tiles"/>
<addaction name="actionImport_Secondary_Tiles"/>
<addaction name="menuImport_Tiles_Image"/>
<addaction name="menuImport_AdvanceMap_Metatiles"/>
<addaction name="separator"/>
<addaction name="actionImport_Primary_Metatiles"/>
<addaction name="actionImport_Secondary_Metatiles"/>
<addaction name="separator"/>
<addaction name="actionExport_Primary_Tiles_Image"/>
<addaction name="actionExport_Secondary_Tiles_Image"/>
<addaction name="actionExport_Primary_Metatiles_Image"/>
<addaction name="actionExport_Secondary_Metatiles_Image"/>
<addaction name="menuExport_Tiles_Image"/>
<addaction name="actionExport_Metatiles_Image"/>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
@ -685,16 +701,6 @@
<string>Ctrl+S</string>
</property>
</action>
<action name="actionImport_Primary_Tiles">
<property name="text">
<string>Import Primary Tiles Image...</string>
</property>
</action>
<action name="actionImport_Secondary_Tiles">
<property name="text">
<string>Import Secondary Tiles Image...</string>
</property>
</action>
<action name="actionChange_Metatiles_Count">
<property name="text">
<string>Change Number of Metatiles...</string>
@ -745,36 +751,6 @@
<string>Ctrl+Y</string>
</property>
</action>
<action name="actionExport_Primary_Tiles_Image">
<property name="text">
<string>Export Primary Tiles Image...</string>
</property>
</action>
<action name="actionExport_Secondary_Tiles_Image">
<property name="text">
<string>Export Secondary Tiles Image...</string>
</property>
</action>
<action name="actionImport_Primary_Metatiles">
<property name="text">
<string>Import Primary Metatiles from Advance Map 1.92...</string>
</property>
</action>
<action name="actionImport_Secondary_Metatiles">
<property name="text">
<string>Import Secondary Metatiles from Advance Map 1.92...</string>
</property>
</action>
<action name="actionExport_Primary_Metatiles_Image">
<property name="text">
<string>Export Primary Metatiles Image...</string>
</property>
</action>
<action name="actionExport_Secondary_Metatiles_Image">
<property name="text">
<string>Export Secondary Metatiles Image...</string>
</property>
</action>
<action name="actionCut">
<property name="text">
<string>Cut</string>
@ -834,6 +810,41 @@
<string>Show Raw Metatile Attributes</string>
</property>
</action>
<action name="actionExport_Primary_Tiles_Image">
<property name="text">
<string>Primary...</string>
</property>
</action>
<action name="actionExport_Secondary_Tiles_Image">
<property name="text">
<string>Secondary...</string>
</property>
</action>
<action name="actionImport_Primary_AdvanceMap_Metatiles">
<property name="text">
<string>Primary...</string>
</property>
</action>
<action name="actionImport_Secondary_AdvanceMap_Metatiles">
<property name="text">
<string>Secondary...</string>
</property>
</action>
<action name="actionImport_Primary_Tiles_Image">
<property name="text">
<string>Primary...</string>
</property>
</action>
<action name="actionImport_Secondary_Tiles_Image">
<property name="text">
<string>Secondary...</string>
</property>
</action>
<action name="actionExport_Metatiles_Image">
<property name="text">
<string>Export Metatiles Image...</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>

View File

@ -54,7 +54,8 @@ protected:
static bool getConfigBool(const QString &key, const QString &value);
static int getConfigInteger(const QString &key, const QString &value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0);
static uint32_t getConfigUint32(const QString &key, const QString &value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0);
static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = Qt::black);
static QColor getConfigColor(const QString &key, const QString &value, const QColor &defaultValue = QColor(Qt::black));
static QString toConfigColor(const QColor &color);
QString m_root;
QString m_filename;
@ -82,7 +83,7 @@ public:
this->collisionOpacity = 50;
this->collisionZoom = 30;
this->metatilesZoom = 30;
this->tilesetEditorMetatilesZoom = 45;
this->tilesetEditorMetatilesZoom = 30;
this->tilesetEditorTilesZoom = 30;
this->showPlayerView = false;
this->showCursorTile = true;
@ -351,7 +352,7 @@ public:
this->prefabImportPrompted = false;
this->tilesetsHaveCallback = true;
this->tilesetsHaveIsCompressed = true;
this->setTransparentPixelsBlack = true;
this->transparencyColor = QColor(Qt::black);
this->preserveMatchingOnlyData = false;
this->filePaths.clear();
this->eventIconPaths.clear();
@ -426,7 +427,7 @@ public:
bool prefabImportPrompted;
bool tilesetsHaveCallback;
bool tilesetsHaveIsCompressed;
bool setTransparentPixelsBlack;
QColor transparencyColor;
bool preserveMatchingOnlyData;
int metatileAttributesSize;
uint32_t metatileBehaviorMask;

View File

@ -65,14 +65,24 @@ public:
} lastCommitBlocks; // to track map changes
void setMetatileLayerOrder(const QList<int> &layerOrder) { m_metatileLayerOrder = layerOrder; }
QList<int> metatileLayerOrder() const;
static void setDefaultMetatileLayerOrder(const QList<int> &layerOrder) { s_defaultMetatileLayerOrder = layerOrder; }
static QList<int> defaultMetatileLayerOrder();
const QList<int> &metatileLayerOrder() const {
return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::globalMetatileLayerOrder();
}
static void setGlobalMetatileLayerOrder(const QList<int> &layerOrder) { s_globalMetatileLayerOrder = layerOrder; }
static const QList<int> &globalMetatileLayerOrder() {
static const QList<int> defaultLayerOrder = {0, 1, 2};
return !s_globalMetatileLayerOrder.isEmpty() ? s_globalMetatileLayerOrder : defaultLayerOrder;
}
void setMetatileLayerOpacity(const QList<float> &layerOpacity) { m_metatileLayerOpacity = layerOpacity; }
QList<float> metatileLayerOpacity() const;
static void setDefaultMetatileLayerOpacity(const QList<float> &layerOpacity) { s_defaultMetatileLayerOpacity = layerOpacity; }
static QList<float> defaultMetatileLayerOpacity();
const QList<float> &metatileLayerOpacity() const {
return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::globalMetatileLayerOpacity();
}
static void setGlobalMetatileLayerOpacity(const QList<float> &layerOpacity) { s_globalMetatileLayerOpacity = layerOpacity; }
static const QList<float> &globalMetatileLayerOpacity() {
static const QList<float> defaultLayerOpacity = {1.0, 1.0, 1.0};
return !s_globalMetatileLayerOpacity.isEmpty() ? s_globalMetatileLayerOpacity : defaultLayerOpacity;
}
LayoutPixmapItem *layoutItem = nullptr;
CollisionPixmapItem *collisionItem = nullptr;
@ -166,8 +176,8 @@ private:
QList<int> m_metatileLayerOrder;
QList<float> m_metatileLayerOpacity;
static QList<int> s_defaultMetatileLayerOrder;
static QList<float> s_defaultMetatileLayerOpacity;
static QList<int> s_globalMetatileLayerOrder;
static QList<float> s_globalMetatileLayerOpacity;
signals:
void dimensionsChanged(const QSize &size);

View File

@ -62,6 +62,14 @@ public:
static void setLayout(Project*);
static QString getMetatileIdString(uint16_t metatileId);
static QString getMetatileIdStrings(const QList<uint16_t> metatileIds);
static QString getLayerName(int layerNum);
static int tileWidth() { return 2; }
static int tileHeight() { return 2; }
static int tilesPerLayer() { return Metatile::tileWidth() * Metatile::tileHeight(); }
static int pixelWidth() { return Metatile::tileWidth() * Tile::pixelWidth(); }
static int pixelHeight() { return Metatile::tileHeight() * Tile::pixelHeight(); }
static QSize pixelSize() { return QSize(pixelWidth(), pixelHeight()); }
inline bool operator==(const Metatile &other) {
return this->tiles == other.tiles && this->attributes == other.attributes;

View File

@ -3,6 +3,7 @@
#define TILE_H
#include <QObject>
#include <QSize>
class Tile
{
@ -24,6 +25,10 @@ public:
static int getIndexInTileset(int);
static const uint16_t maxValue;
static int pixelWidth() { return 8; }
static int pixelHeight() { return 8; }
static QSize pixelSize() { return QSize(Tile::pixelWidth(), Tile::pixelHeight()); }
};
inline bool operator==(const Tile &a, const Tile &b) {

View File

@ -38,6 +38,7 @@ public:
QList<QList<QRgb>> palettes;
QList<QList<QRgb>> palettePreviews;
static QString stripPrefix(const QString &fullName);
static Tileset* getMetatileTileset(int, Tileset*, Tileset*);
static Tileset* getTileTileset(int, Tileset*, Tileset*);
static Metatile* getMetatile(int, Tileset*, Tileset*);

View File

@ -7,7 +7,7 @@
namespace Util {
void numericalModeSort(QStringList &list);
int roundUp(int numToRound, int multiple);
int roundUpToMultiple(int numToRound, int multiple);
QString toDefineCase(QString input);
QString toHexString(uint32_t value, int minLength = 0);
QString toHtmlParagraph(const QString &text);

View File

@ -12,8 +12,10 @@ QImage getCollisionMetatileImage(Block);
QImage getCollisionMetatileImage(int, int);
QImage getMetatileImage(uint16_t, Layout*, bool useTruePalettes = false);
QImage getMetatileImage(Metatile*, Layout*, bool useTruePalettes = false);
QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList<int>&, const QList<float>&, bool useTruePalettes = false);
QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList<int>&, const QList<float>&, bool useTruePalettes = false);
QImage getMetatileImage(uint16_t, Tileset*, Tileset*, const QList<int>&, const QList<float>& = {}, bool useTruePalettes = false);
QImage getMetatileImage(Metatile*, Tileset*, Tileset*, const QList<int>&, const QList<float>& = {}, bool useTruePalettes = false);
QImage getMetatileSheetImage(Layout *, int, bool useTruePalettes = false);
QImage getMetatileSheetImage(Tileset *, Tileset *, uint16_t, int, int, const QList<int> &, const QList<float> & = {}, const QSize &size = Metatile::pixelSize(), bool useTruePalettes = false);
QImage getTileImage(uint16_t, Tileset*, Tileset*);
QImage getPalettedTileImage(uint16_t, Tileset*, Tileset*, int, bool useTruePalettes = false);
QImage getGreyscaleTileImage(uint16_t tile, Tileset *primaryTileset, Tileset *secondaryTileset);

View File

@ -0,0 +1,101 @@
#ifndef METATILEIMAGEEXPORTER_H
#define METATILEIMAGEEXPORTER_H
#include <QDialog>
#include <QGraphicsScene>
#include <QShowEvent>
#include <QCloseEvent>
#include <QListWidget>
#include <QDropEvent>
#include <QRadioButton>
#include "config.h"
class Tileset;
namespace Ui {
class MetatileImageExporter;
}
class ReorderableListWidget : public QListWidget
{
Q_OBJECT
public:
explicit ReorderableListWidget(QWidget *parent = nullptr) : QListWidget(parent) {
setDragEnabled(true);
setDragDropMode(QAbstractItemView::InternalMove);
setDefaultDropAction(Qt::MoveAction);
};
signals:
void reordered();
protected:
virtual void dropEvent(QDropEvent *event) override {
QListWidget::dropEvent(event);
if (event->isAccepted()) {
emit reordered();
}
}
};
class MetatileImageExporter : public QDialog
{
Q_OBJECT
public:
struct Settings {
OrderedMap<int,bool> layerOrder = {
{2, true},
{1, true},
{0, true},
};
uint16_t metatileStart = 0;
uint16_t metatileEnd = 0xFFFF;
uint16_t numMetatilesWide = 8;
bool usePrimaryTileset = true;
bool useSecondaryTileset = false;
bool renderPlaceholders = false;
int transparencyMode = 0;
};
explicit MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings = nullptr);
~MetatileImageExporter();
protected:
virtual void showEvent(QShowEvent *) override;
virtual void closeEvent(QCloseEvent *) override;
private:
Ui::MetatileImageExporter *ui;
Tileset *m_primaryTileset;
Tileset *m_secondaryTileset;
Settings *m_savedSettings;
QGraphicsScene *m_scene = nullptr;
QGraphicsPixmapItem *m_preview = nullptr;
bool m_previewUpdateQueued = false;
QList<int> m_layerOrder;
ProjectConfig m_savedConfig;
QList<QRadioButton*> m_transparencyButtons;
void applySettings(const Settings &settings);
void updatePreview();
void tryUpdatePreview();
void queuePreviewUpdate();
void updateTilesetUI();
void syncPixelWidth();
void syncMetatileWidth();
void validateMetatileStart();
void validateMetatileEnd();
uint16_t getExpectedMetatileStart();
uint16_t getExpectedMetatileEnd();
void updateMetatileRange();
void copyRenderSettings();
void restoreRenderSettings();
void saveImage();
void reset();
};
#endif // METATILEIMAGEEXPORTER_H

View File

@ -8,6 +8,7 @@
#include "tileseteditormetatileselector.h"
#include "tileseteditortileselector.h"
#include "metatilelayersitem.h"
#include "metatileimageexporter.h"
class NoScrollComboBox;
class Layout;
@ -71,10 +72,6 @@ private slots:
void on_spinBox_paletteSelector_valueChanged(int arg1);
void on_actionImport_Primary_Tiles_triggered();
void on_actionImport_Secondary_Tiles_triggered();
void on_actionChange_Metatiles_Count_triggered();
void on_actionChange_Palettes_triggered();
@ -91,14 +88,6 @@ private slots:
void on_lineEdit_metatileLabel_editingFinished();
void on_actionExport_Primary_Tiles_Image_triggered();
void on_actionExport_Secondary_Tiles_Image_triggered();
void on_actionExport_Primary_Metatiles_Image_triggered();
void on_actionExport_Secondary_Metatiles_Image_triggered();
void on_actionImport_Primary_Metatiles_triggered();
void on_actionImport_Secondary_Metatiles_triggered();
void on_copyButton_metatileLabel_clicked();
void on_actionCut_triggered();
@ -122,8 +111,10 @@ private:
void drawSelectedTiles();
void redrawTileSelector();
void redrawMetatileSelector();
void importTilesetTiles(Tileset*, bool);
void importTilesetMetatiles(Tileset*, bool);
void importTilesetTiles(Tileset*);
void importAdvanceMapMetatiles(Tileset*);
void exportTilesImage(Tileset*);
void exportMetatilesImage();
void refresh();
void commitMetatileLabel();
void closeEvent(QCloseEvent*);
@ -170,6 +161,7 @@ private:
QGraphicsScene *metatileLayersScene = nullptr;
bool lockSelection = false;
QSet<uint16_t> metatileReloadQueue;
MetatileImageExporter::Settings *metatileImageExportSettings = nullptr;
bool save();

View File

@ -21,8 +21,6 @@ public:
uint16_t getSelectedMetatileId();
void updateSelectedMetatile();
QPoint getMetatileIdCoordsOnWidget(uint16_t metatileId);
QImage buildPrimaryMetatilesImage();
QImage buildSecondaryMetatilesImage();
QVector<uint16_t> usedMetatiles;
bool selectorShowUnused = false;
@ -56,8 +54,6 @@ private:
void drawFilters();
void drawUnused();
void drawCounts();
QImage buildAllMetatilesImage();
QImage buildImage(int metatileIdStart, int numMetatiles);
int numPrimaryMetatilesRounded() const;
signals:

View File

@ -21,6 +21,7 @@ public:
uint32_t value() const { return m_value; }
uint32_t minimum() const { return m_minimum; }
uint32_t maximum() const { return m_maximum; }
uint32_t singleStep() const { return m_singleStep; }
QString prefix() const { return m_prefix; }
int displayIntegerBase() const { return m_displayIntegerBase; }
bool hasPadding() const { return m_hasPadding; }
@ -28,6 +29,7 @@ public:
void setMinimum(uint32_t min);
void setMaximum(uint32_t max);
void setRange(uint32_t min, uint32_t max);
void setSingleStep(uint32_t val);
void setPrefix(const QString &prefix);
void setDisplayIntegerBase(int base);
void setHasPadding(bool enabled);
@ -36,6 +38,7 @@ private:
uint32_t m_minimum;
uint32_t m_maximum;
uint32_t m_value;
uint32_t m_singleStep;
QString m_prefix;
int m_displayIntegerBase;
bool m_hasPadding;

View File

@ -124,6 +124,7 @@ SOURCES += src/core/advancemapparser.cpp \
src/ui/regionmapeditor.cpp \
src/ui/newmapdialog.cpp \
src/ui/mapimageexporter.cpp \
src/ui/metatileimageexporter.cpp \
src/ui/newtilesetdialog.cpp \
src/ui/flowlayout.cpp \
src/ui/mapruler.cpp \
@ -240,6 +241,7 @@ HEADERS += include/core/advancemapparser.h \
include/ui/regionmapeditor.h \
include/ui/newmapdialog.h \
include/ui/mapimageexporter.h \
include/ui/metatileimageexporter.h \
include/ui/newtilesetdialog.h \
include/ui/overlay.h \
include/ui/flowlayout.h \
@ -289,6 +291,7 @@ FORMS += forms/mainwindow.ui \
forms/aboutporymap.ui \
forms/newtilesetdialog.ui \
forms/mapimageexporter.ui \
forms/metatileimageexporter.ui \
forms/shortcutseditor.ui \
forms/preferenceeditor.ui \
forms/regionmappropertiesdialog.ui \

View File

@ -283,7 +283,7 @@ int KeyValueConfigBase::getConfigInteger(const QString &key, const QString &valu
logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue));
result = defaultValue;
}
return qMin(max, qMax(min, result));
return qBound(min, result, max);
}
uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &value, uint32_t min, uint32_t max, uint32_t defaultValue) {
@ -293,7 +293,7 @@ uint32_t KeyValueConfigBase::getConfigUint32(const QString &key, const QString &
logWarn(QString("Invalid config value for %1: '%2'. Must be an integer. Using default value '%3'.").arg(key).arg(value).arg(defaultValue));
result = defaultValue;
}
return qMin(max, qMax(min, result));
return qBound(min, result, max);
}
QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &value, const QColor &defaultValue) {
@ -305,6 +305,10 @@ QColor KeyValueConfigBase::getConfigColor(const QString &key, const QString &val
return color;
}
QString KeyValueConfigBase::toConfigColor(const QColor &color) {
return color.name().remove("#"); // Our text config treats '#' as the start of a comment.
}
PorymapConfig porymapConfig;
PorymapConfig::PorymapConfig() : KeyValueConfigBase(QStringLiteral("porymap.cfg")) {
@ -571,7 +575,7 @@ QMap<QString, QString> PorymapConfig::getKeyValueMap() {
map.insert("grid_x", QString::number(this->gridSettings.offsetX));
map.insert("grid_y", QString::number(this->gridSettings.offsetY));
map.insert("grid_style", GridSettings::getStyleName(this->gridSettings.style));
map.insert("grid_color", this->gridSettings.color.name().remove("#")); // Our text config treats '#' as the start of a comment.
map.insert("grid_color", toConfigColor(this->gridSettings.color));
QStringList logTypesStrings;
for (const auto &type : this->statusBarLogTypes) {
@ -898,8 +902,8 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
this->tilesetsHaveCallback = getConfigBool(key, value);
} else if (key == "tilesets_have_is_compressed") {
this->tilesetsHaveIsCompressed = getConfigBool(key, value);
} else if (key == "set_transparent_pixels_black") {
this->setTransparentPixelsBlack = getConfigBool(key, value);
} else if (key == "transparency_color") {
this->transparencyColor = getConfigColor(key, value);
} else if (key == "preserve_matching_only_data") {
this->preserveMatchingOnlyData = getConfigBool(key, value);
} else if (key == "event_icon_path_object") {
@ -1005,7 +1009,7 @@ QMap<QString, QString> ProjectConfig::getKeyValueMap() {
}
map.insert("tilesets_have_callback", QString::number(this->tilesetsHaveCallback));
map.insert("tilesets_have_is_compressed", QString::number(this->tilesetsHaveIsCompressed));
map.insert("set_transparent_pixels_black", QString::number(this->setTransparentPixelsBlack));
map.insert("transparency_color", toConfigColor(this->transparencyColor));
map.insert("preserve_matching_only_data", QString::number(this->preserveMatchingOnlyData));
map.insert("metatile_attributes_size", QString::number(this->metatileAttributesSize));
map.insert("metatile_behavior_mask", Util::toHexString(this->metatileBehaviorMask));

View File

@ -6,6 +6,9 @@
#include "imageproviders.h"
#include "utility.h"
QList<int> Layout::s_globalMetatileLayerOrder;
QList<float> Layout::s_globalMetatileLayerOpacity;
Layout::Layout(const Layout &other) : Layout() {
copyFrom(&other);
}
@ -612,23 +615,3 @@ Blockdata Layout::readBlockdata(const QString &path, QString *error) {
return blockdata;
}
QList<int> Layout::metatileLayerOrder() const {
return !m_metatileLayerOrder.isEmpty() ? m_metatileLayerOrder : Layout::defaultMetatileLayerOrder();
}
QList<int> Layout::s_defaultMetatileLayerOrder;
QList<int> Layout::defaultMetatileLayerOrder() {
static const QList<int> initialDefault = {0, 1, 2};
return !s_defaultMetatileLayerOrder.isEmpty() ? s_defaultMetatileLayerOrder : initialDefault;
}
QList<float> Layout::metatileLayerOpacity() const {
return !m_metatileLayerOpacity.isEmpty() ? m_metatileLayerOpacity : Layout::defaultMetatileLayerOpacity();
}
QList<float> Layout::s_defaultMetatileLayerOpacity;
QList<float> Layout::defaultMetatileLayerOpacity() {
static const QList<float> initialDefault = {1.0, 1.0, 1.0};
return !s_defaultMetatileLayerOpacity.isEmpty() ? s_defaultMetatileLayerOpacity : initialDefault;
}

View File

@ -36,8 +36,8 @@ int Metatile::getIndexInTileset(int metatileId) {
}
QPoint Metatile::coordFromPixmapCoord(const QPointF &pixelCoord) {
int x = static_cast<int>(pixelCoord.x()) / 16;
int y = static_cast<int>(pixelCoord.y()) / 16;
int x = static_cast<int>(pixelCoord.x()) / pixelWidth();
int y = static_cast<int>(pixelCoord.y()) / pixelHeight();
if (pixelCoord.x() < 0) x--;
if (pixelCoord.y() < 0) y--;
return QPoint(x, y);
@ -55,6 +55,11 @@ QString Metatile::getMetatileIdStrings(const QList<uint16_t> metatileIds) {
return metatiles.join(",");
};
QString Metatile::getLayerName(int layerNum) {
static const QStringList layerTitles = { "Bottom", "Middle", "Top"};
return layerTitles.value(layerNum);
}
// Read and pack together this metatile's attributes.
uint32_t Metatile::getAttributes() const {
uint32_t data = 0;

View File

@ -200,9 +200,8 @@ QString Tileset::getMetatileLabelPrefix()
QString Tileset::getMetatileLabelPrefix(const QString &name)
{
// Default is "gTileset_Name" --> "METATILE_Name_"
const QString tilesetPrefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix);
const QString labelPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_metatile_label_prefix);
return QString("%1%2_").arg(labelPrefix).arg(QString(name).replace(tilesetPrefix, ""));
return QString("%1%2_").arg(labelPrefix).arg(Tileset::stripPrefix(name));
}
bool Tileset::metatileIsValid(uint16_t metatileId, Tileset *primaryTileset, Tileset *secondaryTileset) {
@ -382,8 +381,7 @@ QString Tileset::getExpectedDir(QString tilesetName, bool isSecondary)
: projectConfig.getFilePath(ProjectFilePath::data_primary_tilesets_folders);
static const QRegularExpression re("([a-z])([A-Z0-9])");
const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix);
return basePath + tilesetName.replace(prefix, "").replace(re, "\\1_\\2").toLower();
return basePath + Tileset::stripPrefix(tilesetName).replace(re, "\\1_\\2").toLower();
}
// Get the expected positions of the members in struct Tileset.
@ -600,3 +598,7 @@ bool Tileset::save() {
if (!savePalettes()) success = false;
return success;
}
QString Tileset::stripPrefix(const QString &fullName) {
return QString(fullName).replace(projectConfig.getIdentifier(ProjectIdentifier::symbol_tilesets_prefix), "");
}

View File

@ -14,7 +14,7 @@ void Util::numericalModeSort(QStringList &list) {
std::sort(list.begin(), list.end(), collator);
}
int Util::roundUp(int numToRound, int multiple) {
int Util::roundUpToMultiple(int numToRound, int multiple) {
if (multiple <= 0)
return numToRound;

View File

@ -1176,8 +1176,8 @@ void Editor::setPlayerViewRect(const QRectF &rect) {
}
void Editor::setCursorRectPos(const QPoint &pos) {
int x = qMax(0, qMin(pos.x(), this->layout ? this->layout->getWidth() - 1 : 0));
int y = qMax(0, qMin(pos.y(), this->layout ? this->layout->getHeight() - 1 : 0));
int x = qBound(0, pos.x(), this->layout ? this->layout->getWidth() - 1 : 0);
int y = qBound(0, pos.y(), this->layout ? this->layout->getHeight() - 1 : 0);
if (this->playerViewRect)
this->playerViewRect->updateLocation(x, y);

View File

@ -6,6 +6,8 @@
int main(int argc, char *argv[])
{
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Round);
QCoreApplication::setAttribute(Qt::AA_UseStyleSheetPropagationInWidgetStyles, true);
QApplication a(argc, argv);
a.setStyle("fusion");

View File

@ -1618,7 +1618,7 @@ Tileset *Project::createNewTileset(QString name, bool secondary, bool checkerboa
metatilesFilepath.append(projectConfig.getFilePath(ProjectFilePath::tilesets_metatiles));
}
ignoreWatchedFilesTemporarily({headersFilepath, graphicsFilepath, metatilesFilepath});
name.remove(0, prefix.length()); // Strip prefix from name to get base name for use in other symbols.
name = Tileset::stripPrefix(name);
tileset->appendToHeaders(headersFilepath, name, this->usingAsmTilesets);
tileset->appendToGraphics(graphicsFilepath, name, this->usingAsmTilesets);
tileset->appendToMetatiles(metatilesFilepath, name, this->usingAsmTilesets);
@ -3462,12 +3462,12 @@ void Project::applyParsedLimits() {
Block::setLayout();
Metatile::setLayout(this);
Project::num_metatiles_primary = qMin(qMax(Project::num_metatiles_primary, 1), Block::getMaxMetatileId() + 1);
Project::num_metatiles_primary = qBound(1, Project::num_metatiles_primary, Block::getMaxMetatileId() + 1);
projectConfig.defaultMetatileId = qMin(projectConfig.defaultMetatileId, Block::getMaxMetatileId());
projectConfig.defaultElevation = qMin(projectConfig.defaultElevation, Block::getMaxElevation());
projectConfig.defaultCollision = qMin(projectConfig.defaultCollision, Block::getMaxCollision());
projectConfig.collisionSheetSize.setHeight(qMin(qMax(projectConfig.collisionSheetSize.height(), 1), Block::getMaxElevation() + 1));
projectConfig.collisionSheetSize.setWidth(qMin(qMax(projectConfig.collisionSheetSize.width(), 1), Block::getMaxCollision() + 1));
projectConfig.collisionSheetSize.setHeight(qBound(1, projectConfig.collisionSheetSize.height(), Block::getMaxElevation() + 1));
projectConfig.collisionSheetSize.setWidth(qBound(1, projectConfig.collisionSheetSize.width(), Block::getMaxCollision() + 1));
}
bool Project::hasUnsavedChanges() {

View File

@ -201,7 +201,7 @@ QList<QString> ScriptUtility::getCustomScripts() {
}
QList<int> ScriptUtility::getMetatileLayerOrder() {
return Layout::defaultMetatileLayerOrder();
return Layout::globalMetatileLayerOrder();
}
bool ScriptUtility::validateMetatileLayerOrder(const QList<int> &order) {
@ -220,16 +220,16 @@ bool ScriptUtility::validateMetatileLayerOrder(const QList<int> &order) {
void ScriptUtility::setMetatileLayerOrder(const QList<int> &order) {
if (!validateMetatileLayerOrder(order))
return;
Layout::setDefaultMetatileLayerOrder(order);
Layout::setGlobalMetatileLayerOrder(order);
if (window) window->refreshAfterPalettePreviewChange();
}
QList<float> ScriptUtility::getMetatileLayerOpacity() {
return Layout::defaultMetatileLayerOpacity();
return Layout::globalMetatileLayerOpacity();
}
void ScriptUtility::setMetatileLayerOpacity(const QList<float> &opacities) {
Layout::setDefaultMetatileLayerOpacity(opacities);
Layout::setGlobalMetatileLayerOpacity(opacities);
if (window) window->refreshAfterPalettePreviewChange();
}

View File

@ -56,11 +56,9 @@ QImage getMetatileImage(
const QList<float> &layerOpacity,
bool useTruePalettes)
{
const int numTilesWide = 2;
const int numTilesTall = 2;
QImage metatile_image(8 * numTilesWide, 8 * numTilesTall, QImage::Format_RGBA8888);
QImage metatile_image(Metatile::pixelWidth(), Metatile::pixelHeight(), QImage::Format_RGBA8888);
if (!metatile) {
metatile_image.fill(Qt::magenta);
metatile_image.fill(projectConfig.transparencyColor == QColor(Qt::transparent) ? projectConfig.transparencyColor : QColor(Qt::magenta));
return metatile_image;
}
@ -70,21 +68,20 @@ QImage getMetatileImage(
// tile pixels line up across layers we will still have something to render.
// The GBA renders transparent pixels using palette 0 color 0. We have this color,
// but all 3 games actually overwrite it with black when loading the tileset palettes,
// so we have a setting to choose between these two behaviors.
metatile_image.fill(projectConfig.setTransparentPixelsBlack ? QColor("black") : QColor(palettes.value(0).value(0)));
// so we have a setting to specify an override transparency color.
metatile_image.fill(projectConfig.transparencyColor.isValid() ? projectConfig.transparencyColor : QColor(palettes.value(0).value(0)));
QPainter metatile_painter(&metatile_image);
uint32_t layerType = metatile->layerType();
const int numTilesPerLayer = numTilesWide * numTilesTall;
for (const auto &layer : layerOrder)
for (int y = 0; y < numTilesTall; y++)
for (int x = 0; x < numTilesWide; x++) {
for (int y = 0; y < Metatile::tileHeight(); y++)
for (int x = 0; x < Metatile::tileWidth(); x++) {
// Get the tile to render next
Tile tile;
int tileOffset = (y * numTilesWide) + x;
int tileOffset = (y * Metatile::tileWidth()) + x;
if (projectConfig.tripleLayerMetatilesEnabled) {
tile = metatile->tiles.value(tileOffset + (layer * numTilesPerLayer));
tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer()));
} else {
// "Vanilla" metatiles only have 8 tiles, but render 12.
// The remaining 4 tiles are rendered using user-specified tiles depending on layer type.
@ -95,19 +92,19 @@ QImage getMetatileImage(
if (layer == 0)
tile = Tile(projectConfig.unusedTileNormal);
else // Tiles are on layers 1 and 2
tile = metatile->tiles.value(tileOffset + ((layer - 1) * numTilesPerLayer));
tile = metatile->tiles.value(tileOffset + ((layer - 1) * Metatile::tilesPerLayer()));
break;
case Metatile::LayerType::Covered:
if (layer == 2)
tile = Tile(projectConfig.unusedTileCovered);
else // Tiles are on layers 0 and 1
tile = metatile->tiles.value(tileOffset + (layer * numTilesPerLayer));
tile = metatile->tiles.value(tileOffset + (layer * Metatile::tilesPerLayer()));
break;
case Metatile::LayerType::Split:
if (layer == 1)
tile = Tile(projectConfig.unusedTileSplit);
else // Tiles are on layers 0 and 2
tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * numTilesPerLayer));
tile = metatile->tiles.value(tileOffset + ((layer == 0 ? 0 : 1) * Metatile::tilesPerLayer()));
break;
}
}
@ -130,7 +127,7 @@ QImage getMetatileImage(
logWarn(QString("Tile '%1' is referring to invalid palette number: '%2'").arg(tile.tileId).arg(tile.palette));
}
QPoint origin = QPoint(x*8, y*8);
QPoint origin = QPoint(x * Tile::pixelWidth(), y * Tile::pixelHeight());
float opacity = layerOpacity.value(layer, 1.0);
if (opacity < 1.0) {
int alpha = 255 * opacity;
@ -165,9 +162,9 @@ QImage getTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondary
QImage getColoredTileImage(uint16_t tileId, Tileset *primaryTileset, Tileset *secondaryTileset, const QList<QRgb> &palette) {
QImage tileImage = getTileImage(tileId, primaryTileset, secondaryTileset);
if (tileImage.isNull()) {
tileImage = QImage(8, 8, QImage::Format_RGBA8888);
tileImage = QImage(Tile::pixelWidth(), Tile::pixelHeight(), QImage::Format_RGBA8888);
QPainter painter(&tileImage);
painter.fillRect(0, 0, 8, 8, palette.at(0));
painter.fillRect(0, 0, tileImage.width(), tileImage.height(), palette.at(0));
} else {
for (int i = 0; i < 16; i++) {
tileImage.setColor(i, palette.at(i));
@ -194,3 +191,57 @@ void flattenTo4bppImage(QImage * image) {
for (int i = 0; i < image->sizeInBytes(); i++, pixel++)
*pixel %= 16;
}
QImage getMetatileSheetImage(Layout *layout, int numMetatilesWide, bool useTruePalettes) {
return getMetatileSheetImage(layout->tileset_primary,
layout->tileset_secondary,
0,
-1,
numMetatilesWide,
layout->metatileLayerOrder(),
layout->metatileLayerOpacity(),
Metatile::pixelSize(),
useTruePalettes);
}
QImage getMetatileSheetImage(Tileset *primaryTileset,
Tileset *secondaryTileset,
uint16_t metatileIdStart,
int numMetatilesToDraw,
int numMetatilesWide,
const QList<int> &layerOrder,
const QList<float> &layerOpacity,
const QSize &metatileSize,
bool useTruePalettes)
{
// We round up the number of primary metatiles to keep the tilesets on separate rows.
int numPrimary = Util::roundUpToMultiple(primaryTileset ? primaryTileset->numMetatiles() : 0, numMetatilesWide);
int maxPrimary = Project::getNumMetatilesPrimary();
bool includesPrimary = metatileIdStart < maxPrimary;
// Negative values are used to indicate 'draw all metatiles'
if (numMetatilesToDraw < 0) {
numMetatilesToDraw = numPrimary + (secondaryTileset ? secondaryTileset->numMetatiles() : 0) - metatileIdStart;
}
// Round up height for incomplete last row
int numMetatilesTall = ceil((double)numMetatilesToDraw / numMetatilesWide);
QImage image(numMetatilesWide * metatileSize.width(), numMetatilesTall * metatileSize.height(), QImage::Format_RGBA8888);
image.fill(projectConfig.transparencyColor == QColor(Qt::transparent) ? projectConfig.transparencyColor : QColor(Qt::magenta));
QPainter painter(&image);
for (int i = 0; i < numMetatilesToDraw; i++) {
uint16_t metatileId = i + metatileIdStart;
if (includesPrimary && metatileId >= numPrimary)
metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset
QImage metatile_image = getMetatileImage(metatileId, primaryTileset, secondaryTileset, layerOrder, layerOpacity, useTruePalettes)
.scaled(metatileSize.width(), metatileSize.height());
int map_y = i / numMetatilesWide;
int map_x = i % numMetatilesWide;
QPoint metatile_origin = QPoint(map_x * metatileSize.width(), map_y * metatileSize.height());
painter.drawImage(metatile_origin, metatile_image);
}
painter.end();
return image;
}

View File

@ -0,0 +1,321 @@
#include "metatileimageexporter.h"
#include "ui_metatileimageexporter.h"
#include "filedialog.h"
#include "imageproviders.h"
#include "utility.h"
#include "project.h"
#include "metatile.h"
#include <QTimer>
MetatileImageExporter::MetatileImageExporter(QWidget *parent, Tileset *primaryTileset, Tileset *secondaryTileset, Settings *savedSettings) :
QDialog(parent),
ui(new Ui::MetatileImageExporter),
m_primaryTileset(primaryTileset),
m_secondaryTileset(secondaryTileset),
m_savedSettings(savedSettings)
{
setAttribute(Qt::WA_DeleteOnClose);
ui->setupUi(this);
m_transparencyButtons = {
ui->radioButton_TransparencyNormal,
ui->radioButton_TransparencyBlack,
ui->radioButton_TransparencyFirst,
};
m_scene = new QGraphicsScene(this);
m_preview = m_scene->addPixmap(QPixmap());
ui->graphicsView_Preview->setScene(m_scene);
if (projectConfig.tripleLayerMetatilesEnabled) {
// When triple-layer metatiles are enabled there is no unused layer,
// so this setting becomes pointless. Disable it.
ui->checkBox_Placeholders->setVisible(false);
}
uint16_t maxMetatileId = Block::getMaxMetatileId();
ui->spinBox_MetatileStart->setMaximum(maxMetatileId);
ui->spinBox_MetatileEnd->setMaximum(maxMetatileId);
ui->spinBox_WidthMetatiles->setRange(1, maxMetatileId);
ui->spinBox_WidthPixels->setRange(1 * Metatile::pixelWidth(), maxMetatileId * Metatile::pixelWidth());
if (m_primaryTileset) {
ui->comboBox_PrimaryTileset->setTextItem(m_primaryTileset->name);
}
if (m_secondaryTileset) {
ui->comboBox_SecondaryTileset->setTextItem(m_secondaryTileset->name);
}
if (m_savedSettings) {
applySettings(*m_savedSettings);
} else {
applySettings({});
}
connect(ui->listWidget_Layers, &ReorderableListWidget::itemChanged, this, &MetatileImageExporter::updatePreview);
connect(ui->listWidget_Layers, &ReorderableListWidget::reordered, this, &MetatileImageExporter::updatePreview);
connect(ui->pushButton_Save, &QPushButton::pressed, this, &MetatileImageExporter::saveImage);
connect(ui->pushButton_Close, &QPushButton::pressed, this, &MetatileImageExporter::close);
connect(ui->pushButton_Reset, &QPushButton::pressed, this, &MetatileImageExporter::reset);
connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncPixelWidth);
connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate);
connect(ui->spinBox_WidthMetatiles, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview);
connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::syncMetatileWidth);
connect(ui->spinBox_WidthPixels, &UIntSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate);
connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::syncPixelWidth); // Round pixel width to multiple of 16
connect(ui->spinBox_WidthPixels, &UIntSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview);
connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileEnd);
connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate);
connect(ui->spinBox_MetatileStart, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview);
connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::validateMetatileStart);
connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::valueChanged, this, &MetatileImageExporter::queuePreviewUpdate);
connect(ui->spinBox_MetatileEnd, &UIntHexSpinBox::editingFinished, this, &MetatileImageExporter::tryUpdatePreview);
// If we used toggled instead of clicked we'd get two preview updates instead of one when the setting changes.
connect(ui->radioButton_TransparencyNormal, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview);
connect(ui->radioButton_TransparencyBlack, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview);
connect(ui->radioButton_TransparencyFirst, &QRadioButton::clicked, this, &MetatileImageExporter::updatePreview);
connect(ui->checkBox_Placeholders, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview);
connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updateTilesetUI);
connect(ui->checkBox_PrimaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview);
connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updateTilesetUI);
connect(ui->checkBox_SecondaryTileset, &QCheckBox::toggled, this, &MetatileImageExporter::updatePreview);
ui->graphicsView_Preview->setFocus();
}
MetatileImageExporter::~MetatileImageExporter() {
delete ui;
}
// Allow the window to open before displaying the preview.
// Metatile sheet image creation is generally quick, so this only
// really matters so that the graphics view can adjust the scale properly.
void MetatileImageExporter::showEvent(QShowEvent *event) {
QDialog::showEvent(event);
if (!event->spontaneous()) {
QTimer::singleShot(0, this, &MetatileImageExporter::updatePreview);
}
}
void MetatileImageExporter::closeEvent(QCloseEvent *event) {
if (m_savedSettings) {
m_savedSettings->metatileStart = ui->spinBox_MetatileStart->value();
m_savedSettings->metatileEnd = ui->spinBox_MetatileEnd->value();
m_savedSettings->numMetatilesWide = ui->spinBox_WidthMetatiles->value();
m_savedSettings->usePrimaryTileset = ui->checkBox_PrimaryTileset->isChecked();
m_savedSettings->useSecondaryTileset = ui->checkBox_SecondaryTileset->isChecked();
m_savedSettings->renderPlaceholders = ui->checkBox_Placeholders->isChecked();
for (int i = 0; i < m_transparencyButtons.length(); i++) {
if (m_transparencyButtons.at(i)->isChecked()) {
m_savedSettings->transparencyMode = i;
break;
}
}
m_savedSettings->layerOrder.clear();
for (int i = 0; i < ui->listWidget_Layers->count(); i++) {
auto item = ui->listWidget_Layers->item(i);
int layerNum = item->data(Qt::UserRole).toInt();
m_savedSettings->layerOrder[layerNum] = (item->checkState() == Qt::Checked);
}
}
QDialog::closeEvent(event);
}
void MetatileImageExporter::applySettings(const Settings &settings) {
ui->spinBox_MetatileStart->setValue(settings.metatileStart);
ui->spinBox_MetatileEnd->setValue(settings.metatileEnd);
ui->spinBox_WidthMetatiles->setValue(settings.numMetatilesWide);
ui->spinBox_WidthPixels->setValue(settings.numMetatilesWide * Metatile::pixelWidth());
ui->checkBox_PrimaryTileset->setChecked(settings.usePrimaryTileset);
ui->checkBox_SecondaryTileset->setChecked(settings.useSecondaryTileset);
ui->checkBox_Placeholders->setChecked(settings.renderPlaceholders);
if (m_transparencyButtons.value(settings.transparencyMode)) {
m_transparencyButtons[settings.transparencyMode]->setChecked(true);
}
// Build layer list from settings
ui->listWidget_Layers->clear();
for (auto it = settings.layerOrder.cbegin(); it != settings.layerOrder.cend(); it++) {
int layerNum = it.key();
bool enabled = it.value();
auto item = new QListWidgetItem(Metatile::getLayerName(layerNum));
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren);
item->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
item->setData(Qt::UserRole, layerNum); // Save the original index to identify the layer
ui->listWidget_Layers->addItem(item);
}
// Don't give extra unnecessary space to the list
ui->listWidget_Layers->setFixedHeight(ui->listWidget_Layers->sizeHintForRow(0) * ui->listWidget_Layers->count() + 4);
updateTilesetUI();
}
void MetatileImageExporter::reset() {
applySettings({});
updatePreview();
}
void MetatileImageExporter::saveImage() {
// Ensure the image in the preview is up-to-date before exporting.
updatePreview();
QString defaultFilename;
if (m_layerOrder.length() == 1) {
// Exporting a metatile layer image is an expected use case for Porytiles, which expects certain file names.
// We can make the process a little easier by setting the default file name to those expected file names.
static const QStringList layerFilenames = { "bottom", "middle", "top" };
defaultFilename = (layerFilenames.at(m_layerOrder.constFirst()));
} else {
if (ui->checkBox_PrimaryTileset->isChecked() && m_primaryTileset) {
defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_primaryTileset->name)));
}
if (ui->checkBox_SecondaryTileset->isChecked() && m_secondaryTileset) {
defaultFilename.append(QString("%1_").arg(Tileset::stripPrefix(m_secondaryTileset->name)));
}
if (!m_layerOrder.isEmpty() && m_layerOrder != QList<int>({0,1,2})) {
for (int i = m_layerOrder.length() - 1; i >= 0; i--) {
defaultFilename.append(Metatile::getLayerName(m_layerOrder.at(i)));
}
defaultFilename.append("_");
}
defaultFilename.append("Metatiles");
}
QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultFilename);
QString filepath = FileDialog::getSaveFileName(this, windowTitle(), defaultFilepath, QStringLiteral("Image Files (*.png *.jpg *.bmp)"));
if (!filepath.isEmpty()) {
m_preview->pixmap().save(filepath);
}
}
void MetatileImageExporter::queuePreviewUpdate() {
m_previewUpdateQueued = true;
}
// For updating only when a change has been recorded.
// Useful for something that might happen often, like an input widget losing focus.
void MetatileImageExporter::tryUpdatePreview() {
if (m_preview->pixmap().isNull() || m_previewUpdateQueued) {
updatePreview();
}
}
void MetatileImageExporter::updatePreview() {
copyRenderSettings();
int numMetatilesWide = ui->spinBox_WidthMetatiles->value();
int metatileStart = ui->spinBox_MetatileStart->value();
int numMetatiles = Util::roundUpToMultiple(ui->spinBox_MetatileEnd->value() - metatileStart + 1, numMetatilesWide);
m_layerOrder.clear();
for (int i = 0; i < ui->listWidget_Layers->count(); i++) {
auto item = ui->listWidget_Layers->item(i);
if (item->checkState() == Qt::Checked) {
int layerNum = item->data(Qt::UserRole).toInt();
m_layerOrder.prepend(qBound(0, layerNum, 2));
}
}
QImage previewImage = getMetatileSheetImage(m_primaryTileset,
m_secondaryTileset,
metatileStart,
numMetatiles,
numMetatilesWide,
m_layerOrder);
m_preview->setPixmap(QPixmap::fromImage(previewImage));
m_scene->setSceneRect(m_scene->itemsBoundingRect());
m_previewUpdateQueued = false;
restoreRenderSettings();
}
void MetatileImageExporter::validateMetatileStart() {
const QSignalBlocker b(ui->spinBox_MetatileStart);
ui->spinBox_MetatileStart->setValue(qMin(ui->spinBox_MetatileStart->value(),
ui->spinBox_MetatileEnd->value()));
}
void MetatileImageExporter::validateMetatileEnd() {
const QSignalBlocker b(ui->spinBox_MetatileEnd);
ui->spinBox_MetatileEnd->setValue(qMax(ui->spinBox_MetatileStart->value(),
ui->spinBox_MetatileEnd->value()));
}
uint16_t MetatileImageExporter::getExpectedMetatileStart() {
if (ui->checkBox_PrimaryTileset->isChecked()) return 0;
if (ui->checkBox_SecondaryTileset->isChecked()) return Project::getNumMetatilesPrimary();
return ui->spinBox_MetatileStart->value();
}
// TODO: Combining tilesets is not rendering the correct range of metatiles
uint16_t MetatileImageExporter::getExpectedMetatileEnd() {
if (ui->checkBox_SecondaryTileset->isChecked()) return Project::getNumMetatilesPrimary() + (m_secondaryTileset ? (m_secondaryTileset->numMetatiles() - 1) : 0);
if (ui->checkBox_PrimaryTileset->isChecked()) return m_primaryTileset ? (m_primaryTileset->numMetatiles() - 1) : 0;
return ui->spinBox_MetatileEnd->value();
}
void MetatileImageExporter::updateMetatileRange() {
const QSignalBlocker b_MetatileStart(ui->spinBox_MetatileStart);
const QSignalBlocker b_MetatileEnd(ui->spinBox_MetatileEnd);
ui->spinBox_MetatileStart->setValue(getExpectedMetatileStart());
ui->spinBox_MetatileEnd->setValue(getExpectedMetatileEnd());
}
void MetatileImageExporter::updateTilesetUI() {
// Users can either specify which tileset(s) to render, or specify a range of metatiles, but not both.
if (ui->checkBox_PrimaryTileset->isChecked() || ui->checkBox_SecondaryTileset->isChecked()) {
updateMetatileRange();
ui->groupBox_MetatileRange->setDisabled(true);
} else {
ui->groupBox_MetatileRange->setDisabled(false);
}
}
void MetatileImageExporter::syncPixelWidth() {
const QSignalBlocker b(ui->spinBox_WidthPixels);
ui->spinBox_WidthPixels->setValue(ui->spinBox_WidthMetatiles->value() * Metatile::pixelWidth());
}
void MetatileImageExporter::syncMetatileWidth() {
const QSignalBlocker b(ui->spinBox_WidthMetatiles);
ui->spinBox_WidthMetatiles->setValue(Util::roundUpToMultiple(ui->spinBox_WidthPixels->value(), Metatile::pixelWidth()) / Metatile::pixelWidth());
}
// These settings control some rendering behavior that make metatiles render accurately to their in-game appearance,
// which may be undesirable when exporting metatile images for editing.
// The settings are buried in getMetatileImage at the moment, to change them we'll temporarily overwrite them.
void MetatileImageExporter::copyRenderSettings() {
m_savedConfig.transparencyColor = projectConfig.transparencyColor;
m_savedConfig.unusedTileNormal = projectConfig.unusedTileNormal;
m_savedConfig.unusedTileCovered = projectConfig.unusedTileCovered;
m_savedConfig.unusedTileSplit = projectConfig.unusedTileSplit;
if (ui->radioButton_TransparencyNormal->isChecked()) {
projectConfig.transparencyColor = QColor(Qt::transparent);
} else if (ui->radioButton_TransparencyBlack->isChecked()) {
projectConfig.transparencyColor = QColor(Qt::black);
} else {
projectConfig.transparencyColor = QColor();
}
if (!ui->checkBox_Placeholders->isChecked()) {
projectConfig.unusedTileNormal = 0;
projectConfig.unusedTileCovered = 0;
projectConfig.unusedTileSplit = 0;
}
}
void MetatileImageExporter::restoreRenderSettings() {
projectConfig.transparencyColor = m_savedConfig.transparencyColor;
projectConfig.unusedTileNormal = m_savedConfig.unusedTileNormal;
projectConfig.unusedTileCovered = m_savedConfig.unusedTileCovered;
projectConfig.unusedTileSplit = m_savedConfig.unusedTileSplit;
}

View File

@ -148,6 +148,6 @@ void MetatileLayersItem::clearLastHoveredCoords() {
QPoint MetatileLayersItem::getBoundedPos(const QPointF &pos) {
int x = static_cast<int>(pos.x()) / this->cellWidth;
int y = static_cast<int>(pos.y()) / this->cellHeight;
return QPoint( qMax(0, qMin(x, this->maxSelectionWidth - 1)),
qMax(0, qMin(y, this->maxSelectionHeight - 1)) );
return QPoint(qBound(0, x, this->maxSelectionWidth - 1),
qBound(0, y, this->maxSelectionHeight - 1));
}

View File

@ -15,28 +15,7 @@ int MetatileSelector::numPrimaryMetatilesRounded() const {
}
void MetatileSelector::updateBasePixmap() {
int primaryLength = this->numPrimaryMetatilesRounded();
int length_ = primaryLength + this->secondaryTileset()->numMetatiles();
int height_ = length_ / this->numMetatilesWide;
if (length_ % this->numMetatilesWide != 0) {
height_++;
}
QImage image(this->numMetatilesWide * this->cellWidth, height_ * this->cellHeight, QImage::Format_RGBA8888);
image.fill(Qt::magenta);
QPainter painter(&image);
for (int i = 0; i < length_; i++) {
int metatileId = i;
if (i >= primaryLength) {
metatileId += Project::getNumMetatilesPrimary() - primaryLength;
}
QImage metatile_image = getMetatileImage(metatileId, this->layout);
int map_y = i / this->numMetatilesWide;
int map_x = i % this->numMetatilesWide;
QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight);
painter.drawImage(metatile_origin, metatile_image);
}
painter.end();
this->basePixmap = QPixmap::fromImage(image);
this->basePixmap = QPixmap::fromImage(getMetatileSheetImage(this->layout, this->numMetatilesWide));
}
void MetatileSelector::draw() {

View File

@ -114,8 +114,8 @@ void ResizableRect::mousePressEvent(QGraphicsSceneMouseEvent *event) {
}
void ResizableRect::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
int dx = Util::roundUp(event->scenePos().x() - this->clickedPos.x(), 16);
int dy = Util::roundUp(event->scenePos().y() - this->clickedPos.y(), 16);
int dx = Util::roundUpToMultiple(event->scenePos().x() - this->clickedPos.x(), 16);
int dy = Util::roundUpToMultiple(event->scenePos().y() - this->clickedPos.y(), 16);
QRect resizedRect = this->clickedRect;

View File

@ -475,10 +475,11 @@ void ProjectSettingsEditor::refresh() {
ui->checkBox_PreserveMatchingOnlyData->setChecked(projectConfig.preserveMatchingOnlyData);
// Radio buttons
if (projectConfig.setTransparentPixelsBlack)
// TODO: Replace
/*if (projectConfig.setTransparentPixelsBlack)
ui->radioButton_RenderBlack->setChecked(true);
else
ui->radioButton_RenderFirstPalColor->setChecked(true);
ui->radioButton_RenderFirstPalColor->setChecked(true);*/
// Set spin box values
ui->spinBox_Elevation->setValue(projectConfig.defaultElevation);
@ -574,7 +575,7 @@ void ProjectSettingsEditor::save() {
projectConfig.tilesetsHaveCallback = ui->checkBox_OutputCallback->isChecked();
projectConfig.tilesetsHaveIsCompressed = ui->checkBox_OutputIsCompressed->isChecked();
porymapConfig.warpBehaviorWarningDisabled = ui->checkBox_DisableWarning->isChecked();
projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked();
//projectConfig.setTransparentPixelsBlack = ui->radioButton_RenderBlack->isChecked(); // TODO
projectConfig.preserveMatchingOnlyData = ui->checkBox_PreserveMatchingOnlyData->isChecked();
// Save spin box settings

View File

@ -61,7 +61,7 @@ void BoundedPixmapItem::paint(QPainter *painter, const QStyleOptionGraphicsItem
QVariant BoundedPixmapItem::itemChange(GraphicsItemChange change, const QVariant &value) {
if (change == ItemPositionChange && scene()) {
QPointF newPos = value.toPointF();
return QPointF(Util::roundUp(newPos.x(), 16), Util::roundUp(newPos.y(), 16));
return QPointF(Util::roundUpToMultiple(newPos.x(), 16), Util::roundUpToMultiple(newPos.y(), 16));
}
else
return QGraphicsItem::itemChange(change, value);

View File

@ -19,8 +19,8 @@ void SelectablePixmapItem::select(int x, int y, int width, int height)
{
this->selectionInitialX = x;
this->selectionInitialY = y;
this->selectionOffsetX = qMax(0, qMin(width, this->maxSelectionWidth));
this->selectionOffsetY = qMax(0, qMin(height, this->maxSelectionHeight));
this->selectionOffsetX = qBound(0, width, this->maxSelectionWidth);
this->selectionOffsetY = qBound(0, height, this->maxSelectionHeight);
this->draw();
emit this->selectionChanged(x, y, width, height);
}

View File

@ -24,8 +24,10 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent)
hasUnsavedChanges(false)
{
setAttribute(Qt::WA_DeleteOnClose);
setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label);
ui->setupUi(this);
setTilesets(this->layout->tileset_primary_label, this->layout->tileset_secondary_label);
connect(ui->checkBox_xFlip, &QCheckBox::toggled, this, &TilesetEditor::setXFlip);
connect(ui->checkBox_yFlip, &QCheckBox::toggled, this, &TilesetEditor::setYFlip);
@ -35,6 +37,17 @@ TilesetEditor::TilesetEditor(Project *project, Layout *layout, QWidget *parent)
connect(ui->actionSave_Tileset, &QAction::triggered, this, &TilesetEditor::save);
connect(ui->actionImport_Primary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->primaryTileset); });
connect(ui->actionImport_Secondary_Tiles_Image, &QAction::triggered, [this] { importTilesetTiles(this->secondaryTileset); });
connect(ui->actionImport_Primary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->primaryTileset); });
connect(ui->actionImport_Secondary_AdvanceMap_Metatiles, &QAction::triggered, [this] { importAdvanceMapMetatiles(this->secondaryTileset); });
connect(ui->actionExport_Primary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->primaryTileset); });
connect(ui->actionExport_Secondary_Tiles_Image, &QAction::triggered, [this] { exportTilesImage(this->secondaryTileset); });
connect(ui->actionExport_Metatiles_Image, &QAction::triggered, [this] { exportMetatilesImage(); });
ui->actionShow_Tileset_Divider->setChecked(porymapConfig.showTilesetEditorDivider);
ui->actionShow_Raw_Metatile_Attributes->setChecked(porymapConfig.showTilesetEditorRawAttributes);
@ -74,6 +87,7 @@ TilesetEditor::~TilesetEditor()
delete selectedTileScene;
delete metatileLayersScene;
delete copiedMetatile;
delete metatileImageExportSettings;
this->metatileHistory.clear();
}
@ -694,17 +708,8 @@ bool TilesetEditor::save() {
return success;
}
void TilesetEditor::on_actionImport_Primary_Tiles_triggered()
{
this->importTilesetTiles(this->primaryTileset, true);
}
void TilesetEditor::on_actionImport_Secondary_Tiles_triggered()
{
this->importTilesetTiles(this->secondaryTileset, false);
}
void TilesetEditor::importTilesetTiles(Tileset *tileset, bool primary) {
void TilesetEditor::importTilesetTiles(Tileset *tileset) {
bool primary = !tileset->is_secondary;
QString descriptor = primary ? "primary" : "secondary";
QString descriptorCaps = primary ? "Primary" : "Secondary";
@ -968,62 +973,27 @@ void TilesetEditor::pasteMetatile(const Metatile * toPaste, QString newLabel)
this->commitMetatileAndLabelChange(prevMetatile, prevLabel);
}
void TilesetEditor::on_actionExport_Primary_Tiles_Image_triggered()
{
QString defaultName = QString("%1_Tiles_Pal%2").arg(this->primaryTileset->name).arg(this->paletteId);
QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName);
QString filepath = FileDialog::getSaveFileName(this, "Export Primary Tiles Image", defaultFilepath, "Image Files (*.png)");
void TilesetEditor::exportTilesImage(Tileset *tileset) {
bool primary = !tileset->is_secondary;
QString defaultFilepath = QString("%1/%2_Tiles_Pal%3.png").arg(FileDialog::getDirectory()).arg(tileset->name).arg(this->paletteId);
QString filepath = FileDialog::getSaveFileName(this, QString("Export %1 Tiles Image").arg(primary ? "Primary" : "Secondary"), defaultFilepath, "Image Files (*.png)");
if (!filepath.isEmpty()) {
QImage image = this->tileSelector->buildPrimaryTilesIndexedImage();
QImage image = primary ? this->tileSelector->buildPrimaryTilesIndexedImage() : this->tileSelector->buildSecondaryTilesIndexedImage();
exportIndexed4BPPPng(image, filepath);
}
}
void TilesetEditor::on_actionExport_Secondary_Tiles_Image_triggered()
{
QString defaultName = QString("%1_Tiles_Pal%2").arg(this->secondaryTileset->name).arg(this->paletteId);
QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName);
QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Tiles Image", defaultFilepath, "Image Files (*.png)");
if (!filepath.isEmpty()) {
QImage image = this->tileSelector->buildSecondaryTilesIndexedImage();
exportIndexed4BPPPng(image, filepath);
// There are many more options for exporting metatile images than tile images, so we open a separate dialog to ask the user for settings.
void TilesetEditor::exportMetatilesImage() {
if (!this->metatileImageExportSettings) {
this->metatileImageExportSettings = new MetatileImageExporter::Settings;
}
auto dialog = new MetatileImageExporter(this, this->primaryTileset, this->secondaryTileset, this->metatileImageExportSettings);
dialog->open();
}
void TilesetEditor::on_actionExport_Primary_Metatiles_Image_triggered()
{
QString defaultName = QString("%1_Metatiles").arg(this->primaryTileset->name);
QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName);
QString filepath = FileDialog::getSaveFileName(this, "Export Primary Metatiles Image", defaultFilepath, "Image Files (*.png)");
if (!filepath.isEmpty()) {
QImage image = this->metatileSelector->buildPrimaryMetatilesImage();
image.save(filepath, "PNG");
}
}
void TilesetEditor::on_actionExport_Secondary_Metatiles_Image_triggered()
{
QString defaultName = QString("%1_Metatiles").arg(this->secondaryTileset->name);
QString defaultFilepath = QString("%1/%2.png").arg(FileDialog::getDirectory()).arg(defaultName);
QString filepath = FileDialog::getSaveFileName(this, "Export Secondary Metatiles Image", defaultFilepath, "Image Files (*.png)");
if (!filepath.isEmpty()) {
QImage image = this->metatileSelector->buildSecondaryMetatilesImage();
image.save(filepath, "PNG");
}
}
void TilesetEditor::on_actionImport_Primary_Metatiles_triggered()
{
this->importTilesetMetatiles(this->primaryTileset, true);
}
void TilesetEditor::on_actionImport_Secondary_Metatiles_triggered()
{
this->importTilesetMetatiles(this->secondaryTileset, false);
}
void TilesetEditor::importTilesetMetatiles(Tileset *tileset, bool primary)
{
void TilesetEditor::importAdvanceMapMetatiles(Tileset *tileset) {
bool primary = !tileset->is_secondary;
QString descriptorCaps = primary ? "Primary" : "Secondary";
QString filepath = FileDialog::getOpenFileName(this, QString("Import %1 Tileset Metatiles from Advance Map 1.92").arg(descriptorCaps), "", "Advance Map 1.92 Metatile Files (*.bvd)");

View File

@ -4,7 +4,7 @@
#include <QPainter>
TilesetEditorMetatileSelector::TilesetEditorMetatileSelector(Tileset *primaryTileset, Tileset *secondaryTileset, Layout *layout)
: SelectablePixmapItem(16, 16, 1, 1) {
: SelectablePixmapItem(32, 32, 1, 1) {
this->primaryTileset = primaryTileset;
this->secondaryTileset = secondaryTileset;
this->numMetatilesWide = 8;
@ -31,48 +31,6 @@ int TilesetEditorMetatileSelector::numPrimaryMetatilesRounded() const {
return ceil((double)this->primaryTileset->numMetatiles() / this->numMetatilesWide) * this->numMetatilesWide;
}
QImage TilesetEditorMetatileSelector::buildAllMetatilesImage() {
return this->buildImage(0, this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles());
}
QImage TilesetEditorMetatileSelector::buildPrimaryMetatilesImage() {
return this->buildImage(0, this->primaryTileset->numMetatiles());
}
QImage TilesetEditorMetatileSelector::buildSecondaryMetatilesImage() {
return this->buildImage(Project::getNumMetatilesPrimary(), this->secondaryTileset->numMetatiles());
}
QImage TilesetEditorMetatileSelector::buildImage(int metatileIdStart, int numMetatiles) {
int numMetatilesHigh = this->numRows(numMetatiles);
int numPrimary = this->numPrimaryMetatilesRounded();
int maxPrimary = Project::getNumMetatilesPrimary();
bool includesPrimary = metatileIdStart < maxPrimary;
QImage image(this->numMetatilesWide * this->cellWidth, numMetatilesHigh * this->cellHeight, QImage::Format_RGBA8888);
image.fill(Qt::magenta);
QPainter painter(&image);
for (int i = 0; i < numMetatiles; i++) {
int metatileId = i + metatileIdStart;
if (includesPrimary && metatileId >= numPrimary)
metatileId += maxPrimary - numPrimary; // Skip over unused region of primary tileset
QImage metatile_image = getMetatileImage(
metatileId,
this->primaryTileset,
this->secondaryTileset,
this->layout->metatileLayerOrder(),
this->layout->metatileLayerOpacity(),
true)
.scaled(this->cellWidth, this->cellHeight);
int map_y = i / this->numMetatilesWide;
int map_x = i % this->numMetatilesWide;
QPoint metatile_origin = QPoint(map_x * this->cellWidth, map_y * this->cellHeight);
painter.drawImage(metatile_origin, metatile_image);
}
painter.end();
return image;
}
void TilesetEditorMetatileSelector::drawMetatile(uint16_t metatileId) {
QPoint pos = getMetatileIdCoords(metatileId);
@ -97,7 +55,15 @@ void TilesetEditorMetatileSelector::drawSelectedMetatile() {
}
void TilesetEditorMetatileSelector::updateBasePixmap() {
this->baseImage = buildAllMetatilesImage();
this->baseImage = getMetatileSheetImage(this->primaryTileset,
this->secondaryTileset,
0,
this->numPrimaryMetatilesRounded() + this->secondaryTileset->numMetatiles(),
this->numMetatilesWide,
this->layout->metatileLayerOrder(),
this->layout->metatileLayerOpacity(),
QSize(this->cellWidth, this->cellHeight),
true);
this->basePixmap = QPixmap::fromImage(this->baseImage);
}

View File

@ -2,24 +2,24 @@
#include <QWheelEvent>
UIntSpinBox::UIntSpinBox(QWidget *parent)
: QAbstractSpinBox(parent)
: QAbstractSpinBox(parent),
m_minimum(0),
m_maximum(99),
m_value(m_minimum),
m_singleStep(1),
m_displayIntegerBase(10),
m_hasPadding(false),
m_numChars(2)
{
// Don't let scrolling hijack focus.
setFocusPolicy(Qt::StrongFocus);
m_minimum = 0;
m_maximum = 99;
m_value = m_minimum;
m_displayIntegerBase = 10;
m_numChars = 2;
m_hasPadding = false;
this->updateEdit();
connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(onEditFinished()));
};
void UIntSpinBox::setValue(uint32_t val) {
val = qMax(m_minimum, qMin(m_maximum, val));
val = qBound(m_minimum, val, m_maximum);
if (m_value != val) {
m_value = val;
emit valueChanged(m_value);
@ -69,6 +69,12 @@ void UIntSpinBox::setRange(uint32_t min, uint32_t max) {
this->updateEdit();
}
void UIntSpinBox::setSingleStep(uint32_t val) {
if (m_singleStep != val) {
m_singleStep = val;
}
}
void UIntSpinBox::setPrefix(const QString &prefix) {
if (m_prefix != prefix) {
m_prefix = prefix;
@ -127,6 +133,7 @@ void UIntSpinBox::onEditFinished() {
}
void UIntSpinBox::stepBy(int steps) {
steps *= m_singleStep;
auto newValue = m_value;
if (steps < 0 && newValue + steps > newValue) {
newValue = 0;

View File

@ -367,7 +367,7 @@ QChart* WildMonChart::createLevelDistributionChart() {
series->attachAxis(axisY);
// We round the y-axis max up to a multiple of 5.
axisY->setMax(Util::roundUp(qCeil(axisY->max()), 5));
axisY->setMax(Util::roundUpToMultiple(qCeil(axisY->max()), 5));
return chart;
}