// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick import QtQuick.Templates as T import StudioTheme as StudioTheme import StudioQuickUtils T.SpinBox { id: control property StudioTheme.ControlStyle style: StudioTheme.Values.controlStyle property alias labelColor: spinBoxInput.color property alias actionIndicator: actionIndicator property int decimals: 0 property int factor: Math.pow(10, control.decimals) property real minStepSize: 1 property real maxStepSize: 10 property bool edit: spinBoxInput.activeFocus // This property is used to indicate the global hover state property bool hover: (spinBoxInput.hover || actionIndicator.hover || spinBoxIndicatorUp.hover || spinBoxIndicatorDown.hover || sliderIndicator.hover) && control.enabled property bool drag: false property bool sliderDrag: sliderPopup.drag property bool dirty: false // user modification flag property alias actionIndicatorVisible: actionIndicator.visible property real __actionIndicatorWidth: control.style.actionIndicatorSize.width property real __actionIndicatorHeight: control.style.actionIndicatorSize.height property bool spinBoxIndicatorVisible: true property real __spinBoxIndicatorWidth: control.style.spinBoxIndicatorSize.width property real __spinBoxIndicatorHeight: control.style.spinBoxIndicatorSize.height property alias sliderIndicatorVisible: sliderIndicator.visible property real __sliderIndicatorWidth: control.style.squareControlSize.width property real __sliderIndicatorHeight: control.style.squareControlSize.height property alias __devicePixelRatio: spinBoxInput.devicePixelRatio property alias pixelsPerUnit: spinBoxInput.pixelsPerUnit property alias inputHAlignment: spinBoxInput.horizontalAlignment property alias compressedValueTimer: myTimer property string preFocusText: "" signal compressedValueModified signal dragStarted signal dragEnded signal dragging locale: Utils.locale // Use custom wheel handling due to bugs property bool __wheelEnabled: false wheelEnabled: false hoverEnabled: true width: control.style.controlSize.width height: control.style.controlSize.height leftPadding: spinBoxIndicatorDown.x + spinBoxIndicatorDown.width rightPadding: sliderIndicator.width + control.style.borderWidth font.pixelSize: control.style.baseFontSize editable: true validator: control.decimals ? doubleValidator : intValidator function checkAndClearFocus() { if (!spinBoxIndicatorUp.activeFocus && !spinBoxIndicatorDown.activeFocus && !spinBoxInput.activeFocus) control.focus = false } DoubleValidator { id: doubleValidator locale: control.locale notation: DoubleValidator.StandardNotation decimals: control.decimals bottom: Math.min(control.from, control.to) / control.factor top: Math.max(control.from, control.to) / control.factor } IntValidator { id: intValidator locale: control.locale bottom: Math.min(control.from, control.to) top: Math.max(control.from, control.to) } ActionIndicator { id: actionIndicator style: control.style __parentControl: control x: 0 y: 0 width: actionIndicator.visible ? control.__actionIndicatorWidth : 0 height: actionIndicator.visible ? control.__actionIndicatorHeight : 0 } up.indicator: SpinBoxIndicator { id: spinBoxIndicatorUp style: control.style __parentControl: control iconFlip: -1 visible: control.spinBoxIndicatorVisible pressed: control.up.pressed x: actionIndicator.width + control.style.borderWidth y: control.style.borderWidth width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0 height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0 enabled: (control.from < control.to) ? control.value < control.to : control.value > control.to } down.indicator: SpinBoxIndicator { id: spinBoxIndicatorDown style: control.style __parentControl: control visible: control.spinBoxIndicatorVisible pressed: control.down.pressed x: actionIndicator.width + control.style.borderWidth y: spinBoxIndicatorUp.y + spinBoxIndicatorUp.height width: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorWidth : 0 height: control.spinBoxIndicatorVisible ? control.__spinBoxIndicatorHeight : 0 enabled: (control.from < control.to) ? control.value > control.from : control.value < control.from } contentItem: SpinBoxInput { id: spinBoxInput style: control.style __parentControl: control function handleEditingFinished() { control.checkAndClearFocus() // Keep the dirty state before calling setValueFromInput(), // it will be set to false (cleared) internally var valueModified = control.dirty control.setValueFromInput() myTimer.stop() // Only trigger the signal, if the value was modified if (valueModified) control.compressedValueModified() } onEditingFinished: { spinBoxInput.focus = false spinBoxInput.handleEditingFinished() } } background: Rectangle { id: spinBoxBackground color: control.style.background.idle border.color: control.style.border.idle border.width: control.style.borderWidth x: actionIndicator.width width: control.width - actionIndicator.width height: control.height } CheckIndicator { id: sliderIndicator style: control.style __parentControl: control __parentPopup: sliderPopup x: spinBoxInput.x + spinBoxInput.width y: control.style.borderWidth width: sliderIndicator.visible ? control.__sliderIndicatorWidth - control.style.borderWidth : 0 height: sliderIndicator.visible ? control.__sliderIndicatorHeight - (control.style.borderWidth * 2) : 0 visible: false // reasonable default } SliderPopup { id: sliderPopup style: control.style __parentControl: control x: actionIndicator.width + control.style.borderWidth y: control.style.controlSize.height width: control.width - actionIndicator.width - (control.style.borderWidth * 2) height: control.style.smallControlSize.height enter: Transition {} exit: Transition {} } textFromValue: function (value, locale) { locale.numberOptions = Locale.OmitGroupSeparator return Number(value / control.factor).toLocaleString(locale, 'f', control.decimals) } valueFromText: function (text, locale) { return Number.fromLocaleString(locale, text) * control.factor } states: [ State { name: "default" when: control.enabled && !control.hover && !control.hovered && !control.edit && !control.drag && !control.sliderDrag PropertyChanges { target: control __wheelEnabled: false } PropertyChanges { target: spinBoxInput selectByMouse: false } PropertyChanges { target: spinBoxBackground border.color: control.style.border.idle } }, State { name: "hover" when: control.enabled && control.hover && control.hovered && !control.edit && !control.drag && !control.sliderDrag PropertyChanges { target: spinBoxBackground border.color: control.style.border.hover } }, State { name: "edit" when: control.edit PropertyChanges { target: control __wheelEnabled: true } PropertyChanges { target: spinBoxInput selectByMouse: true } PropertyChanges { target: spinBoxBackground border.color: control.style.border.idle } }, State { name: "drag" when: control.drag || control.sliderDrag PropertyChanges { target: spinBoxBackground border.color: control.style.border.interaction } }, State { name: "disable" when: !control.enabled PropertyChanges { target: spinBoxBackground border.color: control.style.border.disabled } } ] Timer { id: myTimer repeat: false running: false interval: 400 onTriggered: control.compressedValueModified() } onValueModified: myTimer.restart() onFocusChanged: control.setValueFromInput() onDisplayTextChanged: spinBoxInput.text = control.displayText onActiveFocusChanged: { if (control.activeFocus) { // QTBUG-75862 && mySpinBox.focusReason === Qt.TabFocusReason) control.preFocusText = spinBoxInput.text spinBoxInput.selectAll() } if (sliderPopup.opened && !control.activeFocus) sliderPopup.close() } onDecimalsChanged: spinBoxInput.text = control.textFromValue(control.value, control.locale) Keys.onPressed: function(event) { if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) { event.accepted = true // Store current step size var currStepSize = control.stepSize if (event.modifiers & Qt.ControlModifier) control.stepSize = control.minStepSize if (event.modifiers & Qt.ShiftModifier) control.stepSize = control.maxStepSize // Check if value is in sync with text input, if not sync it! var val = control.valueFromText(spinBoxInput.text, control.locale) if (control.value !== val) control.value = val var currValue = control.value if (event.key === Qt.Key_Up) control.increase() else control.decrease() if (currValue !== control.value) control.valueModified() // Reset step size control.stepSize = currStepSize } if (event.key === Qt.Key_Escape) { spinBoxInput.text = control.preFocusText control.dirty = true spinBoxInput.handleEditingFinished() } // FIX: This is a temporary fix for QTBUG-74239 if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) control.setValueFromInput() } function clamp(v, lo, hi) { return (v < lo || v > hi) ? Math.min(Math.max(lo, v), hi) : v } function setValueFromInput() { if (!control.dirty) return // FIX: This is a temporary fix for QTBUG-74239 var currValue = control.value if (!spinBoxInput.acceptableInput) control.value = clamp(valueFromText(spinBoxInput.text, control.locale), control.validator.bottom * control.factor, control.validator.top * control.factor) else control.value = valueFromText(spinBoxInput.text, control.locale) if (spinBoxInput.text !== control.displayText) spinBoxInput.text = control.displayText if (control.value !== currValue) control.valueModified() control.dirty = false } }