citra-qt: Add a utility spinbox class called CSpinBox.
This class has a few advantages over the regular QSpinBox: - QSpinBox stores its as signed 32 bit integers, which for instance is unsuitable for representing memory addresses. CSpinBox uses 64 bit integers instead. - QSpinBox does not provide an easy way to handle number input from bases different than 10. - QSpinBox is quite inflexible in general and almost any sort of customization requires reimplementing it anyway.
This commit is contained in:
		| @@ -11,6 +11,7 @@ set(SRCS | ||||
|             debugger/graphics_cmdlists.cpp | ||||
|             debugger/ramview.cpp | ||||
|             debugger/registers.cpp | ||||
|             util/spinbox.cpp | ||||
|             bootmanager.cpp | ||||
|             hotkeys.cpp | ||||
|             main.cpp | ||||
| @@ -26,6 +27,7 @@ set(HEADERS | ||||
|             debugger/graphics_cmdlists.hxx | ||||
|             debugger/ramview.hxx | ||||
|             debugger/registers.hxx | ||||
|             util/spinbox.hxx | ||||
|             bootmanager.hxx | ||||
|             hotkeys.hxx | ||||
|             main.hxx | ||||
|   | ||||
							
								
								
									
										303
									
								
								src/citra_qt/util/spinbox.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								src/citra_qt/util/spinbox.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | ||||
| // Licensed under GPLv2+ | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
|  | ||||
| // Copyright 2014 Tony Wasserka | ||||
| // All rights reserved. | ||||
| // | ||||
| // Redistribution and use in source and binary forms, with or without | ||||
| // modification, are permitted provided that the following conditions are met: | ||||
| // | ||||
| //     * Redistributions of source code must retain the above copyright | ||||
| //       notice, this list of conditions and the following disclaimer. | ||||
| //     * Redistributions in binary form must reproduce the above copyright | ||||
| //       notice, this list of conditions and the following disclaimer in the | ||||
| //       documentation and/or other materials provided with the distribution. | ||||
| //     * Neither the name of the owner nor the names of its contributors may | ||||
| //       be used to endorse or promote products derived from this software | ||||
| //       without specific prior written permission. | ||||
| // | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
| #include <QLineEdit> | ||||
| #include <QRegExpValidator> | ||||
|  | ||||
| #include "common/log.h" | ||||
|  | ||||
| #include "spinbox.hxx" | ||||
|  | ||||
| CSpinBox::CSpinBox(QWidget* parent) : QAbstractSpinBox(parent), base(10), min_value(-100), max_value(100), value(0), num_digits(0) | ||||
| { | ||||
|     // TODO: Might be nice to not immediately call the slot. | ||||
|     //       Think of an address that is being replaced by a different one, in which case a lot | ||||
|     //       invalid intermediate addresses would be read from during editing. | ||||
|     connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(OnEditingFinished())); | ||||
|  | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetValue(qint64 val) | ||||
| { | ||||
|     auto old_value = value; | ||||
|     value = std::max(std::min(val, max_value), min_value); | ||||
|  | ||||
|     if (old_value != value) { | ||||
|         UpdateText(); | ||||
|         emit ValueChanged(value); | ||||
|     } | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetRange(qint64 min, qint64 max) | ||||
| { | ||||
|     min_value = min; | ||||
|     max_value = max; | ||||
|  | ||||
|     SetValue(value); | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| void CSpinBox::stepBy(int steps) | ||||
| { | ||||
|     auto new_value = value; | ||||
|     // Scale number of steps by the currently selected digit | ||||
|     // TODO: Move this code elsewhere and enable it. | ||||
|     // TODO: Support for num_digits==0, too | ||||
|     // TODO: Support base!=16, too | ||||
|     // TODO: Make the cursor not jump back to the end of the line... | ||||
|     /*if (base == 16 && num_digits > 0) { | ||||
|         int digit = num_digits - (lineEdit()->cursorPosition() - prefix.length()) - 1; | ||||
|         digit = std::max(0, std::min(digit, num_digits - 1)); | ||||
|         steps <<= digit * 4; | ||||
|     }*/ | ||||
|  | ||||
|     // Increment "new_value" by "steps", and perform annoying overflow checks, too. | ||||
|     if (steps < 0 && new_value + steps > new_value) { | ||||
|         new_value = std::numeric_limits<qint64>::min(); | ||||
|     } else if (steps > 0 && new_value + steps < new_value) { | ||||
|         new_value = std::numeric_limits<qint64>::max(); | ||||
|     } else { | ||||
|         new_value += steps; | ||||
|     } | ||||
|  | ||||
|     SetValue(new_value); | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| QAbstractSpinBox::StepEnabled CSpinBox::stepEnabled() const | ||||
| { | ||||
|     StepEnabled ret = StepNone; | ||||
|  | ||||
|     if (value > min_value) | ||||
|         ret |= StepDownEnabled; | ||||
|  | ||||
|     if (value < max_value) | ||||
|         ret |= StepUpEnabled; | ||||
|  | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetBase(int base) | ||||
| { | ||||
|     this->base = base; | ||||
|  | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetNumDigits(int num_digits) | ||||
| { | ||||
|     this->num_digits = num_digits; | ||||
|  | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetPrefix(const QString& prefix) | ||||
| { | ||||
|     this->prefix = prefix; | ||||
|  | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| void CSpinBox::SetSuffix(const QString& suffix) | ||||
| { | ||||
|     this->suffix = suffix; | ||||
|  | ||||
|     UpdateText(); | ||||
| } | ||||
|  | ||||
| static QString StringToInputMask(const QString& input) { | ||||
|     QString mask = input; | ||||
|  | ||||
|     // ... replace any special characters by their escaped counterparts ... | ||||
|     mask.replace("\\", "\\\\"); | ||||
|     mask.replace("A", "\\A"); | ||||
|     mask.replace("a", "\\a"); | ||||
|     mask.replace("N", "\\N"); | ||||
|     mask.replace("n", "\\n"); | ||||
|     mask.replace("X", "\\X"); | ||||
|     mask.replace("x", "\\x"); | ||||
|     mask.replace("9", "\\9"); | ||||
|     mask.replace("0", "\\0"); | ||||
|     mask.replace("D", "\\D"); | ||||
|     mask.replace("d", "\\d"); | ||||
|     mask.replace("#", "\\#"); | ||||
|     mask.replace("H", "\\H"); | ||||
|     mask.replace("h", "\\h"); | ||||
|     mask.replace("B", "\\B"); | ||||
|     mask.replace("b", "\\b"); | ||||
|     mask.replace(">", "\\>"); | ||||
|     mask.replace("<", "\\<"); | ||||
|     mask.replace("!", "\\!"); | ||||
|  | ||||
|     return mask; | ||||
| } | ||||
|  | ||||
| void CSpinBox::UpdateText() | ||||
| { | ||||
|     // If a fixed number of digits is used, we put the line edit in insertion mode by setting an | ||||
|     // input mask. | ||||
|     QString mask; | ||||
|     if (num_digits != 0) { | ||||
|         mask += StringToInputMask(prefix); | ||||
|  | ||||
|         // For base 10 and negative range, demand a single sign character | ||||
|         if (HasSign()) | ||||
|             mask += "X"; // identified as "-" or "+" in the validator | ||||
|  | ||||
|         // Uppercase digits greater than 9. | ||||
|         mask += ">"; | ||||
|  | ||||
|         // The greatest signed 64-bit number has 19 decimal digits. | ||||
|         // TODO: Could probably make this more generic with some logarithms. | ||||
|         // For reference, unsigned 64-bit can have up to 20 decimal digits. | ||||
|         int digits = (num_digits != 0) ? num_digits | ||||
|                      : (base == 16) ? 16 | ||||
|                      : (base == 10) ? 19 | ||||
|                      : 0xFF; // fallback case... | ||||
|  | ||||
|         // Match num_digits digits | ||||
|         // Digits irrelevant to the chosen number base are filtered in the validator | ||||
|         mask += QString("H").repeated(std::max(num_digits, 1)); | ||||
|  | ||||
|         // Switch off case conversion | ||||
|         mask += "!"; | ||||
|  | ||||
|         mask += StringToInputMask(suffix); | ||||
|     } | ||||
|     lineEdit()->setInputMask(mask); | ||||
|  | ||||
|     // Set new text without changing the cursor position. This will cause the cursor to briefly | ||||
|     // appear at the end of the line and then to jump back to its original position. That's | ||||
|     // a bit ugly, but better than having setText() move the cursor permanently all the time. | ||||
|     int cursor_position = lineEdit()->cursorPosition(); | ||||
|     lineEdit()->setText(TextFromValue()); | ||||
|     lineEdit()->setCursorPosition(cursor_position); | ||||
| } | ||||
|  | ||||
| QString CSpinBox::TextFromValue() | ||||
| { | ||||
|     return prefix | ||||
|            + QString(HasSign() ? ((value < 0) ? "-" : "+") : "") | ||||
|            + QString("%1").arg(abs(value), num_digits, base, QLatin1Char('0')).toUpper() | ||||
|            + suffix; | ||||
| } | ||||
|  | ||||
| qint64 CSpinBox::ValueFromText() | ||||
| { | ||||
|     unsigned strpos = prefix.length(); | ||||
|  | ||||
|     QString num_string = text().mid(strpos, text().length() - strpos - suffix.length()); | ||||
|     return num_string.toLongLong(nullptr, base); | ||||
| } | ||||
|  | ||||
| bool CSpinBox::HasSign() const | ||||
| { | ||||
|     return base == 10 && min_value < 0; | ||||
| } | ||||
|  | ||||
| void CSpinBox::OnEditingFinished() | ||||
| { | ||||
|     // Only update for valid input | ||||
|     QString input = lineEdit()->text(); | ||||
|     int pos = 0; | ||||
|     if (QValidator::Acceptable == validate(input, pos)) | ||||
|         SetValue(ValueFromText()); | ||||
| } | ||||
|  | ||||
| QValidator::State CSpinBox::validate(QString& input, int& pos) const | ||||
| { | ||||
|     if (!prefix.isEmpty() && input.left(prefix.length()) != prefix) | ||||
|         return QValidator::Invalid; | ||||
|  | ||||
|     unsigned strpos = prefix.length(); | ||||
|  | ||||
|     // Empty "numbers" allowed as intermediate values | ||||
|     if (strpos >= input.length() - HasSign() - suffix.length()) | ||||
|         return QValidator::Intermediate; | ||||
|  | ||||
|     _dbg_assert_(GUI, base <= 10 || base == 16); | ||||
|     QString regexp; | ||||
|  | ||||
|     // Demand sign character for negative ranges | ||||
|     if (HasSign()) | ||||
|         regexp += "[+\\-]"; | ||||
|  | ||||
|     // Match digits corresponding to the chosen number base. | ||||
|     regexp += QString("[0-%1").arg(std::min(base, 9)); | ||||
|     if (base == 16) { | ||||
|         regexp += "a-fA-F"; | ||||
|     } | ||||
|     regexp += "]"; | ||||
|  | ||||
|     // Specify number of digits | ||||
|     if (num_digits > 0) { | ||||
|         regexp += QString("{%1}").arg(num_digits); | ||||
|     } else { | ||||
|         regexp += "+"; | ||||
|     } | ||||
|  | ||||
|     // Match string | ||||
|     QRegExp num_regexp(regexp); | ||||
|     int num_pos = strpos; | ||||
|     QString sub_input = input.mid(strpos, input.length() - strpos - suffix.length()); | ||||
|  | ||||
|     if (!num_regexp.exactMatch(sub_input) && num_regexp.matchedLength() == 0) | ||||
|         return QValidator::Invalid; | ||||
|  | ||||
|     sub_input = sub_input.left(num_regexp.matchedLength()); | ||||
|     bool ok; | ||||
|     qint64 val = sub_input.toLongLong(&ok, base); | ||||
|  | ||||
|     if (!ok) | ||||
|         return QValidator::Invalid; | ||||
|  | ||||
|     // Outside boundaries => don't accept | ||||
|     if (val < min_value || val > max_value) | ||||
|         return QValidator::Invalid; | ||||
|  | ||||
|     // Make sure we are actually at the end of this string... | ||||
|     strpos += num_regexp.matchedLength(); | ||||
|  | ||||
|     if (!suffix.isEmpty() && input.mid(strpos) != suffix) { | ||||
|         return QValidator::Invalid; | ||||
|     } else { | ||||
|         strpos += suffix.length(); | ||||
|     } | ||||
|  | ||||
|     if (strpos != input.length()) | ||||
|         return QValidator::Invalid; | ||||
|  | ||||
|     // At this point we can say for sure that the input is fine. Let's fix it up a bit though | ||||
|     input.replace(num_pos, sub_input.length(), sub_input.toUpper()); | ||||
|  | ||||
|     return QValidator::Acceptable; | ||||
| } | ||||
							
								
								
									
										88
									
								
								src/citra_qt/util/spinbox.hxx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/citra_qt/util/spinbox.hxx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| // Licensed under GPLv2+ | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
|  | ||||
| // Copyright 2014 Tony Wasserka | ||||
| // All rights reserved. | ||||
| // | ||||
| // Redistribution and use in source and binary forms, with or without | ||||
| // modification, are permitted provided that the following conditions are met: | ||||
| // | ||||
| //     * Redistributions of source code must retain the above copyright | ||||
| //       notice, this list of conditions and the following disclaimer. | ||||
| //     * Redistributions in binary form must reproduce the above copyright | ||||
| //       notice, this list of conditions and the following disclaimer in the | ||||
| //       documentation and/or other materials provided with the distribution. | ||||
| //     * Neither the name of the owner nor the names of its contributors may | ||||
| //       be used to endorse or promote products derived from this software | ||||
| //       without specific prior written permission. | ||||
| // | ||||
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||||
| // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||||
| // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||||
| // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||||
| // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||||
| // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||||
| // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||||
| // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||||
| // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||
| // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <QAbstractSpinBox> | ||||
| #include <QtGlobal> | ||||
|  | ||||
| class QVariant; | ||||
|  | ||||
| /** | ||||
|  * A custom spin box widget with enhanced functionality over Qt's QSpinBox | ||||
|  */ | ||||
| class CSpinBox : public QAbstractSpinBox { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     CSpinBox(QWidget* parent = nullptr); | ||||
|  | ||||
|     void stepBy(int steps) override; | ||||
|     StepEnabled stepEnabled() const override; | ||||
|  | ||||
|     void SetValue(qint64 val); | ||||
|  | ||||
|     void SetRange(qint64 min, qint64 max); | ||||
|  | ||||
|     void SetBase(int base); | ||||
|  | ||||
|     void SetPrefix(const QString& prefix); | ||||
|     void SetSuffix(const QString& suffix); | ||||
|  | ||||
|     void SetNumDigits(int num_digits); | ||||
|  | ||||
|     QValidator::State validate(QString& input, int& pos) const override; | ||||
|  | ||||
| signals: | ||||
|     void ValueChanged(qint64 val); | ||||
|  | ||||
| private slots: | ||||
|     void OnEditingFinished(); | ||||
|  | ||||
| private: | ||||
|     void UpdateText(); | ||||
|  | ||||
|     bool HasSign() const; | ||||
|  | ||||
|     QString TextFromValue(); | ||||
|     qint64 ValueFromText(); | ||||
|  | ||||
|     qint64 min_value, max_value; | ||||
|  | ||||
|     qint64 value; | ||||
|  | ||||
|     QString prefix, suffix; | ||||
|  | ||||
|     int base; | ||||
|  | ||||
|     int num_digits; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user