spinbox.cpp 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. // Licensed under GPLv2 or any later version
  2. // Refer to the license.txt file included.
  3. // Copyright 2014 Tony Wasserka
  4. // All rights reserved.
  5. //
  6. // Redistribution and use in source and binary forms, with or without
  7. // modification, are permitted provided that the following conditions are met:
  8. //
  9. // * Redistributions of source code must retain the above copyright
  10. // notice, this list of conditions and the following disclaimer.
  11. // * Redistributions in binary form must reproduce the above copyright
  12. // notice, this list of conditions and the following disclaimer in the
  13. // documentation and/or other materials provided with the distribution.
  14. // * Neither the name of the owner nor the names of its contributors may
  15. // be used to endorse or promote products derived from this software
  16. // without specific prior written permission.
  17. //
  18. // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. #include <cstdlib>
  30. #include <QLineEdit>
  31. #include <QRegExpValidator>
  32. #include "citra_qt/util/spinbox.h"
  33. #include "common/assert.h"
  34. CSpinBox::CSpinBox(QWidget* parent)
  35. : QAbstractSpinBox(parent), min_value(-100), max_value(100), value(0), base(10), num_digits(0) {
  36. // TODO: Might be nice to not immediately call the slot.
  37. // Think of an address that is being replaced by a different one, in which case a lot
  38. // invalid intermediate addresses would be read from during editing.
  39. connect(lineEdit(), SIGNAL(textEdited(QString)), this, SLOT(OnEditingFinished()));
  40. UpdateText();
  41. }
  42. void CSpinBox::SetValue(qint64 val) {
  43. auto old_value = value;
  44. value = std::max(std::min(val, max_value), min_value);
  45. if (old_value != value) {
  46. UpdateText();
  47. emit ValueChanged(value);
  48. }
  49. }
  50. void CSpinBox::SetRange(qint64 min, qint64 max) {
  51. min_value = min;
  52. max_value = max;
  53. SetValue(value);
  54. UpdateText();
  55. }
  56. void CSpinBox::stepBy(int steps) {
  57. auto new_value = value;
  58. // Scale number of steps by the currently selected digit
  59. // TODO: Move this code elsewhere and enable it.
  60. // TODO: Support for num_digits==0, too
  61. // TODO: Support base!=16, too
  62. // TODO: Make the cursor not jump back to the end of the line...
  63. /*if (base == 16 && num_digits > 0) {
  64. int digit = num_digits - (lineEdit()->cursorPosition() - prefix.length()) - 1;
  65. digit = std::max(0, std::min(digit, num_digits - 1));
  66. steps <<= digit * 4;
  67. }*/
  68. // Increment "new_value" by "steps", and perform annoying overflow checks, too.
  69. if (steps < 0 && new_value + steps > new_value) {
  70. new_value = std::numeric_limits<qint64>::min();
  71. } else if (steps > 0 && new_value + steps < new_value) {
  72. new_value = std::numeric_limits<qint64>::max();
  73. } else {
  74. new_value += steps;
  75. }
  76. SetValue(new_value);
  77. UpdateText();
  78. }
  79. QAbstractSpinBox::StepEnabled CSpinBox::stepEnabled() const {
  80. StepEnabled ret = StepNone;
  81. if (value > min_value)
  82. ret |= StepDownEnabled;
  83. if (value < max_value)
  84. ret |= StepUpEnabled;
  85. return ret;
  86. }
  87. void CSpinBox::SetBase(int base) {
  88. this->base = base;
  89. UpdateText();
  90. }
  91. void CSpinBox::SetNumDigits(int num_digits) {
  92. this->num_digits = num_digits;
  93. UpdateText();
  94. }
  95. void CSpinBox::SetPrefix(const QString& prefix) {
  96. this->prefix = prefix;
  97. UpdateText();
  98. }
  99. void CSpinBox::SetSuffix(const QString& suffix) {
  100. this->suffix = suffix;
  101. UpdateText();
  102. }
  103. static QString StringToInputMask(const QString& input) {
  104. QString mask = input;
  105. // ... replace any special characters by their escaped counterparts ...
  106. mask.replace("\\", "\\\\");
  107. mask.replace("A", "\\A");
  108. mask.replace("a", "\\a");
  109. mask.replace("N", "\\N");
  110. mask.replace("n", "\\n");
  111. mask.replace("X", "\\X");
  112. mask.replace("x", "\\x");
  113. mask.replace("9", "\\9");
  114. mask.replace("0", "\\0");
  115. mask.replace("D", "\\D");
  116. mask.replace("d", "\\d");
  117. mask.replace("#", "\\#");
  118. mask.replace("H", "\\H");
  119. mask.replace("h", "\\h");
  120. mask.replace("B", "\\B");
  121. mask.replace("b", "\\b");
  122. mask.replace(">", "\\>");
  123. mask.replace("<", "\\<");
  124. mask.replace("!", "\\!");
  125. return mask;
  126. }
  127. void CSpinBox::UpdateText() {
  128. // If a fixed number of digits is used, we put the line edit in insertion mode by setting an
  129. // input mask.
  130. QString mask;
  131. if (num_digits != 0) {
  132. mask += StringToInputMask(prefix);
  133. // For base 10 and negative range, demand a single sign character
  134. if (HasSign())
  135. mask += "X"; // identified as "-" or "+" in the validator
  136. // Uppercase digits greater than 9.
  137. mask += ">";
  138. // Match num_digits digits
  139. // Digits irrelevant to the chosen number base are filtered in the validator
  140. mask += QString("H").repeated(std::max(num_digits, 1));
  141. // Switch off case conversion
  142. mask += "!";
  143. mask += StringToInputMask(suffix);
  144. }
  145. lineEdit()->setInputMask(mask);
  146. // Set new text without changing the cursor position. This will cause the cursor to briefly
  147. // appear at the end of the line and then to jump back to its original position. That's
  148. // a bit ugly, but better than having setText() move the cursor permanently all the time.
  149. int cursor_position = lineEdit()->cursorPosition();
  150. lineEdit()->setText(TextFromValue());
  151. lineEdit()->setCursorPosition(cursor_position);
  152. }
  153. QString CSpinBox::TextFromValue() {
  154. return prefix + QString(HasSign() ? ((value < 0) ? "-" : "+") : "") +
  155. QString("%1").arg(std::abs(value), num_digits, base, QLatin1Char('0')).toUpper() +
  156. suffix;
  157. }
  158. qint64 CSpinBox::ValueFromText() {
  159. unsigned strpos = prefix.length();
  160. QString num_string = text().mid(strpos, text().length() - strpos - suffix.length());
  161. return num_string.toLongLong(nullptr, base);
  162. }
  163. bool CSpinBox::HasSign() const {
  164. return base == 10 && min_value < 0;
  165. }
  166. void CSpinBox::OnEditingFinished() {
  167. // Only update for valid input
  168. QString input = lineEdit()->text();
  169. int pos = 0;
  170. if (QValidator::Acceptable == validate(input, pos))
  171. SetValue(ValueFromText());
  172. }
  173. QValidator::State CSpinBox::validate(QString& input, int& pos) const {
  174. if (!prefix.isEmpty() && input.left(prefix.length()) != prefix)
  175. return QValidator::Invalid;
  176. int strpos = prefix.length();
  177. // Empty "numbers" allowed as intermediate values
  178. if (strpos >= input.length() - HasSign() - suffix.length())
  179. return QValidator::Intermediate;
  180. DEBUG_ASSERT(base <= 10 || base == 16);
  181. QString regexp;
  182. // Demand sign character for negative ranges
  183. if (HasSign())
  184. regexp += "[+\\-]";
  185. // Match digits corresponding to the chosen number base.
  186. regexp += QString("[0-%1").arg(std::min(base, 9));
  187. if (base == 16) {
  188. regexp += "a-fA-F";
  189. }
  190. regexp += "]";
  191. // Specify number of digits
  192. if (num_digits > 0) {
  193. regexp += QString("{%1}").arg(num_digits);
  194. } else {
  195. regexp += "+";
  196. }
  197. // Match string
  198. QRegExp num_regexp(regexp);
  199. int num_pos = strpos;
  200. QString sub_input = input.mid(strpos, input.length() - strpos - suffix.length());
  201. if (!num_regexp.exactMatch(sub_input) && num_regexp.matchedLength() == 0)
  202. return QValidator::Invalid;
  203. sub_input = sub_input.left(num_regexp.matchedLength());
  204. bool ok;
  205. qint64 val = sub_input.toLongLong(&ok, base);
  206. if (!ok)
  207. return QValidator::Invalid;
  208. // Outside boundaries => don't accept
  209. if (val < min_value || val > max_value)
  210. return QValidator::Invalid;
  211. // Make sure we are actually at the end of this string...
  212. strpos += num_regexp.matchedLength();
  213. if (!suffix.isEmpty() && input.mid(strpos) != suffix) {
  214. return QValidator::Invalid;
  215. } else {
  216. strpos += suffix.length();
  217. }
  218. if (strpos != input.length())
  219. return QValidator::Invalid;
  220. // At this point we can say for sure that the input is fine. Let's fix it up a bit though
  221. input.replace(num_pos, sub_input.length(), sub_input.toUpper());
  222. return QValidator::Acceptable;
  223. }