From 603df4defa3232744cae8742378ddc2067266185 Mon Sep 17 00:00:00 2001 From: GriffinR Date: Mon, 11 Sep 2023 12:44:15 -0400 Subject: [PATCH] Add UIntSpinBox class --- forms/projectsettingseditor.ui | 13 ++- include/ui/uintspinbox.h | 67 ++++++++++++ porymap.pro | 6 +- src/ui/projectsettingseditor.cpp | 14 +-- src/ui/uintspinbox.cpp | 178 +++++++++++++++++++++++++++++++ 5 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 include/ui/uintspinbox.h create mode 100644 src/ui/uintspinbox.cpp 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
noscrollspinbox.h
+ + UIntSpinBox + QAbstractSpinBox +
uintspinbox.h
+
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); +}