maxLibQt
MLDoubleSpinBox.qml
Go to the documentation of this file.
1 /*
2  MLDoubleSpinBox
3  https://github.com/mpaperno/maxLibQt
4 
5  COPYRIGHT: (c)2018 Maxim Paperno; All Right Reserved.
6  Contact: http://www.WorldDesign.com/contact
7 
8  LICENSE:
9 
10  Commercial License Usage
11  Licensees holding valid commercial licenses may use this file in
12  accordance with the terms contained in a written agreement between
13  you and the copyright holder.
14 
15  GNU General Public License Usage
16  Alternatively, this file may be used under the terms of the GNU
17  General Public License as published by the Free Software Foundation,
18  either version 3 of the License, or (at your option) any later version.
19 
20  This program is distributed in the hope that it will be useful,
21  but WITHOUT ANY WARRANTY; without even the implied warranty of
22  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23  GNU General Public License for more details.
24 
25  A copy of the GNU General Public License is available at <http://www.gnu.org/licenses/>.
26 */
27 
28 import QtQuick 2.10
29 import QtQuick.Controls 2.3
30 
73 Control {
74  id: control
75  objectName: "MLDoubleSpinBox"
76 
77  // Standard SpinBox API properties (v2.4)
78  property double value: 0.0
79  property double from: 0.0
80  property double to: 100.0
81  property double stepSize: 1.0
82  property bool editable: true
83  property bool wrap: true
84  property QtObject validator: doubleValidator
85  property int inputMethodHints: Qt.ImhFormattedNumbersOnly
86  readonly property string displayText: textFromValue(value, effectiveLocale)
87  readonly property bool inputMethodComposing: textInputItem ? textInputItem.inputMethodComposing : false
88 
89  // Custom properties
90  property int decimals: 2
91  property int notation: DoubleValidator.StandardNotation
92  property string inputMask
93  property bool selectByMouse: true
94  property bool useLocaleFormat: true
95  property bool showGroupSeparator: true
96  property bool trimExtraZeros: true
97  property string prefix
98  property string suffix
99  property int pageSteps: 10
100  property int buttonRepeatDelay: 300
101  property int buttonRepeatInterval: 100
103  readonly property string cleanText: getCleanText(displayText)
104  readonly property bool acceptableInput: textInputItem && textInputItem.acceptableInput
105  readonly property real topValue: Math.max(from, to)
106  readonly property real botValue: Math.min(from, to)
110  readonly property SpinBox spinBoxItem: contentItem
112  property Item textInputItem: spinBoxItem ? spinBoxItem.contentItem : null
115  readonly property QtObject doubleValidator: DoubleValidator {
116  top: control.topValue
117  bottom: control.botValue
118  decimals: Math.max(control.decimals, 0)
119  notation: control.notation
120  locale: control.effectiveLocale.name
121  }
122 
128  readonly property QtObject regExpValidator: RegExpValidator { regExp: control.doubleValidationRegEx(); }
129 
130  // signals
132  signal valueModified()
133 
134  // QtQuick Control properties
135 
136  // By default wheel is enabled only if editor has active focus or item is not editable.
137  wheelEnabled: !editable || (textInputItem && textInputItem.activeFocus)
138 
139  // The spin box itself... it's really only here for its buttons and overall formatting, we ignore its actual value/etc.
140  contentItem: SpinBox {
141  width: control.availableWidth
142  height: control.availableHeight
143  editable: control.editable
144  inputMethodHints: control.inputMethodHints
145  validator: control.validator
146  from: -0x7FFFFFFF; to: 0x7FFFFFFF; // prevent interference with our real from/to values
147  // wrap peroperty is set below as a Binding in case SpinBox vesion is < 2.3 (Qt 5.10).
148  }
149 
150  // Public function API
151 
153  function increase() {
154  stepBy(1);
155  }
156 
158  function decrease() {
159  stepBy(-1);
160  }
161 
166  function stepBy(steps, noWrap) {
167  // always use current editor value in case user has changed it w/out losing focus
168  setValue(textValue() + (stepSize * steps), noWrap);
169  }
170 
177  function setValue(newValue, noWrap, notModified)
178  {
179  if (!wrap || noWrap)
180  newValue = Math.max(Math.min(newValue, control.topValue), control.botValue);
181  else if (newValue < control.botValue)
182  newValue = control.topValue;
183  else if (newValue > control.topValue)
184  newValue = control.botValue;
185 
186  newValue = Number(newValue.toFixed(Math.max(decimals, 0))); // round
187 
188  if (value !== newValue) {
189  isValidated = true;
190  value = newValue;
191  isValidated = false;
192  if (!notModified)
193  valueModified();
194  if (spinBoxItem)
195  spinBoxItem.value = 0; // reset this to prevent it from disabling the buttons or other weirdness
196  //console.log("setValue:", newValue.toFixed(control.decimals));
197  return true;
198  }
199  return false;
200  }
201 
203  function textFromValue(value, locale)
204  {
205  if (!locale)
206  locale = effectiveLocale;
207 
208  var text = value.toLocaleString(locale, (notation === DoubleValidator.StandardNotation ? 'f' : 'e'), Math.max(decimals, 0));
209 
210  if (!showGroupSeparator && locale.name !== "C")
211  text = text.replace(new RegExp("\\" + locale.groupSeparator, "g"), "");
212  if (trimExtraZeros) {
213  var pt = locale.decimalPoint;
214  var ex = new RegExp("\\" + pt + "0*$|(\\" + pt + "\\d*[1-9])(0+)$").exec(text);
215  if (ex)
216  text = text.replace(ex[0], ex[1] || "");
217  }
218 
219  if (prefix)
220  text = prefix + text;
221  if (suffix)
222  text = text + suffix;
223 
224  return text;
225  }
226 
228  function valueFromText(text, locale)
229  {
230  if (!locale)
231  locale = effectiveLocale;
232  // strip prefix/suffix, or custom pre-processor
233  text = getCleanText(text, locale);
234  // We need to clean the string before using Number::fromLocaleString because it throws errors when the input format isn't valid, eg. thousands separator in the wrong place. D'oh.
235  var re = "[^\\+\\-\\d\\" + locale.decimalPoint + locale.exponential + "]+";
236  text = text.replace(new RegExp(re, "gi"), "");
237  if (!text.length)
238  text = "0";
239  //console.log("valueFromText:", text, locale.name, Number.fromLocaleString(locale, text));
240  return Number.fromLocaleString(locale, text);
241  }
242 
245  function getCleanText(text, locale)
246  {
247  text = String(text);
248  if (prefix)
249  text = text.replace(prefixRegEx, "");
250  if (suffix)
251  text = text.replace(suffixRegEx, "");
252  return text.trim();
253  }
254 
256  function escapeRegExpChars(string) {
257  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
258  }
259 
261  function escapeInputMaskChars(string) {
262  return string.replace(/[{}\[\]\\><!#09anxdhb]/gi, '\\$&');
263  }
264 
266  function doubleValidationRegEx()
267  {
268  var locale = effectiveLocale,
269  pnt = locale.decimalPoint,
270  grp = locale.groupSeparator,
271  exp = locale.exponential,
272  pfx = escapeRegExpChars(prefix),
273  sfx = escapeRegExpChars(suffix),
274  expRe = "(?:" + exp + "[+-]?[\\d]+)?",
275  re = "^" + pfx + "[+-]?(?:[\\d]{1,3}\\" + grp + "?)+\\" + pnt + "?[\\d]*" + expRe + sfx + "$";
276  // ^[+-]?(?:[\d]{1,3},?)+\.?[\d]*(?:e[+-]?[\d]+)?$
277  return new RegExp(re, "i");
278  }
279 
280  // internals
281 
282  property bool isValidated: false
283  property bool completed: false
284  readonly property var defaultLocale: Qt.locale("C")
285  readonly property var effectiveLocale: useLocaleFormat ? locale : defaultLocale
286  readonly property var prefixRegEx: new RegExp("^" + escapeRegExpChars(prefix))
287  readonly property var suffixRegEx: new RegExp(escapeRegExpChars(suffix) + "$")
288 
289 
290 
291  function textValue() {
292  return textInputItem ? valueFromText(textInputItem.text, effectiveLocale) : 0;
293  }
294 
296  function updateValueFromText() {
297  if (!setValue(textValue(), true))
298  updateUi(); // make sure the text is formatted anyway
299  }
300 
302  function handleKeyEvent(event)
303  {
304  var steps = 0;
305  if (event.key === Qt.Key_Up)
306  steps = 1;
307  else if (event.key === Qt.Key_Down)
308  steps = -1;
309  else if (event.key === Qt.Key_PageUp)
310  steps = control.pageSteps;
311  else if (event.key === Qt.Key_PageDown)
312  steps = -control.pageSteps;
313  else if (event.key !== Qt.Key_Enter && event.key !== Qt.Key_Return)
314  return;
315 
316  event.accepted = true;
317 
318  if (steps)
319  stepBy(steps);
320  else
321  updateValueFromText();
322  }
323 
325  function toggleButtonPress(press, increment)
326  {
327  if (!press) {
328  btnRepeatTimer.stop();
329  return;
330  }
331 
332  if (increment)
333  increase();
334  else
335  decrease();
336  btnRepeatTimer.increment = increment;
337  btnRepeatTimer.start();
338  }
339 
341  function updateUi()
342  {
343  if (!completed)
344  return;
345 
346  if (textInputItem)
347  textInputItem.text = textFromValue(value, effectiveLocale);
348 
349  if (spinBoxItem) {
350  if (spinBoxItem.up && spinBoxItem.up.indicator)
351  spinBoxItem.up.indicator.enabled = (wrap || value < topValue);
352  if (spinBoxItem.down && spinBoxItem.down.indicator)
353  spinBoxItem.down.indicator.enabled = (wrap || value > botValue);
354  }
355  }
356 
357  onValueChanged: {
358  if (!completed)
359  return;
360  if (!isValidated)
361  setValue(value, true, true);
362  updateUi();
363  }
364 
365  // We need to override spin box arrow key events to distinguish from +/- button presses, otherwise we get double repeats.
366  onSpinBoxItemChanged: {
367  if (spinBoxItem)
368  spinBoxItem.Keys.forwardTo = [control];
369  }
370 
371  Component.onCompleted: {
372  completed = true;
373  // An initial value may have been set, but not validated. Do that now.
374  if (!setValue(value, true, true))
375  updateUi(); // in case it hasn't changed
376  }
377 
378  onWrapChanged: updateUi()
379  onNotationChanged: updateUi()
380  onTrimExtraZerosChanged: updateUi()
381  onShowGroupSeparatorChanged: updateUi()
382  onEffectiveLocaleChanged: updateUi()
383  Keys.onPressed: handleKeyEvent(event)
384 
385  Connections {
386  target: control.spinBoxItem ? control.spinBoxItem.up : null
387  onPressedChanged: control.toggleButtonPress(control.spinBoxItem.up.pressed, true)
388  }
389 
390  Connections {
391  target: control.spinBoxItem ? control.spinBoxItem.down : null
392  onPressedChanged: control.toggleButtonPress(control.spinBoxItem.down.pressed, false)
393  }
394 
395  Connections {
396  target: control.textInputItem
397  // Checking active focus works better than onEditingFinished because the latter doesn't fire if input is invalid (nor does it fix it up automatically).
398  onActiveFocusChanged: {
399  if (!control.textInputItem.activeFocus)
400  control.updateValueFromText();
401  }
402  }
403 
404  // We use a binding here just in case the resident SpinBox is older than v2.3
405  Binding {
406  target: control.spinBoxItem
407  when: control.spinBoxItem && typeof control.spinBoxItem.wrap !== "undefined"
408  property: "wrap"
409  value: control.wrap
410  }
411 
412  Binding {
413  target: control.textInputItem
414  property: "selectByMouse"
415  value: control.selectByMouse
416  }
417 
418  Binding {
419  target: control.textInputItem
420  property: "inputMask"
421  value: control.inputMask
422  }
423 
424  // Timer for firing the +/- button repeat events while they're held down.
425  Timer {
426  id: btnRepeatTimer
427  property bool delay: true
428  property bool increment: true
429  interval: delay ? control.buttonRepeatDelay : control.buttonRepeatInterval
430  repeat: true
431  onRunningChanged: delay = true
432  onTriggered: {
433  if (delay)
434  delay = false;
435  else if (increment)
436  control.increase();
437  else
438  control.decrease();
439  }
440  }
441 
442  // Wheel/scroll action detection area
443  MouseArea {
444  anchors.fill: control
445  z: control.contentItem.z + 1
446  acceptedButtons: Qt.NoButton
447  enabled: control.wheelEnabled
448  onWheel: {
449  var delta = (wheel.angleDelta.y === 0.0 ? -wheel.angleDelta.x : wheel.angleDelta.y) / 120;
450  if (wheel.inverted)
451  delta *= -1;
452  if (wheel.modifiers & Qt.ControlModifier)
453  delta *= control.pageSteps;
454  control.stepBy(delta);
455  }
456  }
457 
458 }
virtual bool event(QEvent *e)
QVariant property(const char *name) const const
QString objectName() const const