diff --git a/forms/projectsettingseditor.ui b/forms/projectsettingseditor.ui
index be99e633..34504fb5 100644
--- a/forms/projectsettingseditor.ui
+++ b/forms/projectsettingseditor.ui
@@ -321,7 +321,7 @@
-
-
+
The mask used to read/write Terrain Type from the metatile's attributes data. If 0, this attribute is disabled.
@@ -378,7 +378,7 @@
-
-
+
The mask used to read/write Metatile Behavior from the metatile's attributes data. If 0, this attribute is disabled.
@@ -411,7 +411,7 @@
-
-
+
The mask used to read/write Encounter Type from the metatile's attributes data. If 0, this attribute is disabled.
@@ -438,7 +438,7 @@
-
-
+
The mask used to read/write Layer Type from the metatile's attributes data. If 0, this attribute is disabled.
@@ -671,6 +671,11 @@
QSpinBox
+
+ UIntSpinBox
+ QAbstractSpinBox
+
+
diff --git a/include/ui/uintspinbox.h b/include/ui/uintspinbox.h
new file mode 100644
index 00000000..b561e219
--- /dev/null
+++ b/include/ui/uintspinbox.h
@@ -0,0 +1,67 @@
+#ifndef UINTSPINBOX_H
+#define UINTSPINBOX_H
+
+#include
+#include
+
+/*
+ QSpinBox stores minimum/maximum/value as ints. This class is a version of QAbstractSpinBox for values above 0x7FFFFFFF.
+ It minimally implements some QSpinBox-specific features like prefixes to simplify support for hex value input.
+ It also implements the scroll focus requirements of NoScrollSpinBox.
+*/
+
+class UIntSpinBox : public QAbstractSpinBox
+{
+ Q_OBJECT
+
+public:
+ explicit UIntSpinBox(QWidget *parent = nullptr);
+ ~UIntSpinBox() {};
+
+ uint32_t value() const { return m_value; }
+ uint32_t minimum() const { return m_minimum; }
+ uint32_t maximum() const { return m_maximum; }
+ QString prefix() const { return m_prefix; }
+ int displayIntegerBase() const { return m_displayIntegerBase; }
+ bool hasPadding() const { return m_hasPadding; }
+
+ void setMinimum(uint32_t min);
+ void setMaximum(uint32_t max);
+ void setRange(uint32_t min, uint32_t max);
+ void setPrefix(const QString &prefix);
+ void setDisplayIntegerBase(int base);
+ void setHasPadding(bool enabled);
+
+private:
+ uint32_t m_minimum;
+ uint32_t m_maximum;
+ uint32_t m_value;
+ QString m_prefix;
+ int m_displayIntegerBase;
+ bool m_hasPadding;
+ int m_numChars;
+
+ void updateEdit();
+ QString stripped(QString input) const;
+
+ virtual void stepBy(int steps) override;
+ virtual void wheelEvent(QWheelEvent *event) override;
+ virtual void focusOutEvent(QFocusEvent *event) override;
+
+protected:
+ virtual uint32_t valueFromText(const QString &text) const;
+ virtual QString textFromValue(uint32_t val) const;
+ virtual QValidator::State validate(QString &input, int &pos) const override;
+ virtual QAbstractSpinBox::StepEnabled stepEnabled() const override;
+
+
+public slots:
+ void setValue(uint32_t val);
+ void onEditFinished();
+
+signals:
+ void valueChanged(uint32_t val);
+ void textChanged(const QString &text);
+};
+
+#endif // UINTSPINBOX_H
\ No newline at end of file
diff --git a/porymap.pro b/porymap.pro
index 4a2a2008..63b97c1a 100644
--- a/porymap.pro
+++ b/porymap.pro
@@ -100,7 +100,8 @@ SOURCES += src/core/block.cpp \
src/mainwindow.cpp \
src/project.cpp \
src/settings.cpp \
- src/log.cpp
+ src/log.cpp \
+ src/ui/uintspinbox.cpp
HEADERS += include/core/block.h \
include/core/blockdata.h \
@@ -192,7 +193,8 @@ HEADERS += include/core/block.h \
include/scripting.h \
include/scriptutility.h \
include/settings.h \
- include/log.h
+ include/log.h \
+ include/ui/uintspinbox.h
FORMS += forms/mainwindow.ui \
forms/prefabcreationdialog.ui \
diff --git a/src/ui/projectsettingseditor.cpp b/src/ui/projectsettingseditor.cpp
index cfd330eb..ae64053e 100644
--- a/src/ui/projectsettingseditor.cpp
+++ b/src/ui/projectsettingseditor.cpp
@@ -42,10 +42,12 @@ void ProjectSettingsEditor::connectSignals() {
connect(combo, &QComboBox::currentTextChanged, this, &ProjectSettingsEditor::markEdited);
for (auto checkBox : ui->centralwidget->findChildren())
connect(checkBox, &QCheckBox::stateChanged, this, &ProjectSettingsEditor::markEdited);
- for (auto spinBox : ui->centralwidget->findChildren())
- connect(spinBox, QOverload::of(&QSpinBox::valueChanged), [this](int) { this->markEdited(); });
for (auto lineEdit : ui->centralwidget->findChildren())
connect(lineEdit, &QLineEdit::textEdited, this, &ProjectSettingsEditor::markEdited);
+ for (auto spinBox : ui->centralwidget->findChildren())
+ connect(spinBox, &QSpinBox::textChanged, this, &ProjectSettingsEditor::markEdited);
+ for (auto spinBox : ui->centralwidget->findChildren())
+ connect(spinBox, &UIntSpinBox::textChanged, this, &ProjectSettingsEditor::markEdited);
}
void ProjectSettingsEditor::markEdited() {
@@ -71,10 +73,10 @@ void ProjectSettingsEditor::initUi() {
ui->spinBox_FillMetatile->setMaximum(Project::getNumMetatilesTotal() - 1);
// TODO: These need to be subclassed (or changed to line edits) to handle larger values
- ui->spinBox_BehaviorMask->setMaximum(INT_MAX);
- ui->spinBox_EncounterTypeMask->setMaximum(INT_MAX);
- ui->spinBox_LayerTypeMask->setMaximum(INT_MAX);
- ui->spinBox_TerrainTypeMask->setMaximum(INT_MAX);
+ ui->spinBox_BehaviorMask->setMaximum(UINT_MAX);
+ ui->spinBox_EncounterTypeMask->setMaximum(UINT_MAX);
+ ui->spinBox_LayerTypeMask->setMaximum(UINT_MAX);
+ ui->spinBox_TerrainTypeMask->setMaximum(UINT_MAX);
}
void ProjectSettingsEditor::createProjectPathsTable() {
diff --git a/src/ui/uintspinbox.cpp b/src/ui/uintspinbox.cpp
new file mode 100644
index 00000000..31f66ba9
--- /dev/null
+++ b/src/ui/uintspinbox.cpp
@@ -0,0 +1,178 @@
+#include "uintspinbox.h"
+
+UIntSpinBox::UIntSpinBox(QWidget *parent)
+ : QAbstractSpinBox(parent)
+{
+ // 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) {
+ if (m_value != val) {
+ m_value = val;
+ emit valueChanged(m_value);
+ }
+ this->updateEdit();
+}
+
+uint32_t UIntSpinBox::valueFromText(const QString &text) const {
+ return this->stripped(text).toUInt(nullptr, m_displayIntegerBase);
+}
+
+QString UIntSpinBox::textFromValue(uint32_t val) const {
+ if (m_hasPadding)
+ return m_prefix + QString("%1").arg(val, m_numChars, m_displayIntegerBase, QChar('0')).toUpper();
+
+ return m_prefix + QString::number(val, m_displayIntegerBase).toUpper();
+}
+
+void UIntSpinBox::setMinimum(uint32_t min) {
+ this->setRange(min, qMax(min, m_maximum));
+}
+
+void UIntSpinBox::setMaximum(uint32_t max) {
+ this->setRange(qMin(m_minimum, max), max);
+}
+
+void UIntSpinBox::setRange(uint32_t min, uint32_t max) {
+ max = qMax(min, max);
+
+ if (m_maximum == max && m_minimum == min)
+ return;
+
+ if (m_maximum != max) {
+ // Update number of characters for padding
+ m_numChars = 0;
+ for (uint32_t i = max; i != 0; i /= m_displayIntegerBase)
+ m_numChars++;
+ }
+
+ m_minimum = min;
+ m_maximum = max;
+
+ if (m_value < min)
+ m_value %= min;
+ else if (m_value > max)
+ m_value %= max;
+ this->updateEdit();
+}
+
+void UIntSpinBox::setPrefix(const QString &prefix) {
+ if (m_prefix != prefix) {
+ m_prefix = prefix;
+ this->updateEdit();
+ }
+}
+
+void UIntSpinBox::setDisplayIntegerBase(int base) {
+ if (base < 2 || base > 36)
+ base = 10;
+ if (m_displayIntegerBase != base) {
+ m_displayIntegerBase = base;
+ this->updateEdit();
+ }
+}
+
+void UIntSpinBox::setHasPadding(bool enabled) {
+ if (m_hasPadding != enabled) {
+ m_hasPadding = enabled;
+ this->updateEdit();
+ }
+}
+
+void UIntSpinBox::updateEdit() {
+ const QString text = this->textFromValue(m_value);
+ if (text != this->lineEdit()->text()) {
+ this->lineEdit()->setText(text);
+ emit textChanged(this->lineEdit()->text());
+ }
+}
+
+void UIntSpinBox::onEditFinished() {
+ int pos = this->lineEdit()->cursorPosition();
+ QString input = this->lineEdit()->text();
+
+ auto state = this->validate(input, pos);
+ if (state == QValidator::Acceptable) {
+ // Valid input
+ m_value = this->valueFromText(input);
+ } else if (state == QValidator::Intermediate) {
+ // User has deleted all the number text.
+ // If they did this by selecting all text and then hitting delete
+ // make sure to put the cursor back in front of the prefix.
+ m_value = m_minimum;
+ this->lineEdit()->setCursorPosition(m_prefix.length());
+ }
+}
+
+void UIntSpinBox::stepBy(int steps)
+{
+ auto new_value = m_value;
+ if (steps < 0 && new_value + steps > new_value) {
+ new_value = 0;
+ } else if (steps > 0 && new_value + steps < new_value) {
+ new_value = UINT_MAX;
+ } else {
+ new_value += steps;
+ }
+ this->setValue(new_value);
+}
+
+QString UIntSpinBox::stripped(QString input) const {
+ if (m_prefix.length() && input.startsWith(m_prefix))
+ input.remove(0, m_prefix.length());
+ return input.trimmed();
+}
+
+QValidator::State UIntSpinBox::validate(QString &input, int &pos) const {
+ QString copy(input);
+ input = m_prefix;
+
+ // Adjust for prefix
+ copy = this->stripped(copy);
+ if (copy.isEmpty())
+ return QValidator::Intermediate;
+
+ // Editing the prefix (if not deleting all text) is not allowed
+ if (pos < m_prefix.length())
+ return QValidator::Invalid;
+
+ bool ok;
+ uint32_t num = copy.toUInt(&ok, m_displayIntegerBase);
+ if (!ok || num < m_minimum || num > m_maximum)
+ return QValidator::Invalid;
+
+ input += copy.toUpper();
+ return QValidator::Acceptable;
+}
+
+QAbstractSpinBox::StepEnabled UIntSpinBox::stepEnabled() const {
+ QAbstractSpinBox::StepEnabled flags = QAbstractSpinBox::StepNone;
+ if (m_value < m_maximum)
+ flags |= QAbstractSpinBox::StepUpEnabled;
+ if (m_value > m_minimum)
+ flags |= QAbstractSpinBox::StepDownEnabled;
+
+ return flags;
+}
+
+void UIntSpinBox::wheelEvent(QWheelEvent *event) {
+ // Only allow scrolling to modify contents when it explicitly has focus.
+ if (hasFocus())
+ QAbstractSpinBox::wheelEvent(event);
+}
+
+void UIntSpinBox::focusOutEvent(QFocusEvent *event) {
+ this->updateEdit();
+ QAbstractSpinBox::focusOutEvent(event);
+}