Files
noctalia-shell/Widgets/NSpinBox.qml
T

315 lines
9.2 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
RowLayout {
id: root
// Public properties
property int value: 0
property int from: 0
property int to: 100
property int stepSize: 1
property string suffix: ""
property string prefix: ""
property string label: ""
property string description: ""
property bool hovering: false
property int baseSize: Style.baseWidgetSize
property var defaultValue: undefined
property string settingsPath: ""
// Convenience properties for common naming
property alias minimum: root.from
property alias maximum: root.to
// Properties for repeating
property int initialRepeatDelay: 400 // The "pause" after the first click (in ms)
property int repeatInterval: 80 // How often to step up after fist pause (ms)
property int rampFactor: 4 // How many ticks to wait before increasing the step multiplier
property int maxStepMultiplier: 10 // The max step (e.g., 10 * stepSize)
property int _holdTicks: 0 // Internal counter for hold duration
property int _repeatDirection: 0 // -1 for decrease, 1 for increase
signal entered
signal exited
Layout.fillWidth: true
readonly property bool isValueChanged: (defaultValue !== undefined) && (value !== defaultValue)
readonly property string indicatorTooltip: defaultValue !== undefined ? I18n.tr("panels.indicator.default-value", {
"value": String(defaultValue)
}) : ""
Timer {
id: repeatTimer
repeat: true
interval: root.initialRepeatDelay
onTriggered: {
if (repeatTimer.interval === root.initialRepeatDelay) {
repeatTimer.interval = root.repeatInterval;
root._holdTicks = 0;
}
root._holdTicks++;
var stepMultiplier = Math.min(root.maxStepMultiplier, 1 + Math.floor(root._holdTicks / root.rampFactor));
changeValue(root._repeatDirection, root.stepSize * stepMultiplier);
}
}
function changeValue(direction, step) {
var currentStep = step || root.stepSize;
if (direction === 1 && root.value < root.to) {
root.value = Math.min(root.to, root.value + currentStep);
} else if (direction === -1 && root.value > root.from) {
root.value = Math.max(root.from, root.value - currentStep);
} else {
return;
}
if (root.value === root.to || root.value === root.from) {
stopRepeat();
}
}
function stopRepeat() {
root._repeatDirection = 0;
repeatTimer.stop();
repeatTimer.interval = root.initialRepeatDelay;
}
NLabel {
label: root.label
description: root.description
showIndicator: root.isValueChanged
indicatorTooltip: root.indicatorTooltip
}
// Main spinbox container
Rectangle {
id: spinBoxContainer
Layout.margins: Style.borderS
implicitWidth: 120
implicitHeight: Math.round((root.baseSize - 4) / 2) * 2
radius: Style.iRadiusS
color: Color.mSurfaceVariant
border.color: (root.hovering || decreaseArea.containsMouse || increaseArea.containsMouse) ? Color.mHover : Color.mOutline
border.width: Style.borderS
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
// Mouse area for hover and scroll
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
onEntered: {
if (!root.enabled)
return;
root.hovering = true;
root.entered();
}
onExited: {
root.hovering = false;
root.exited();
}
onWheel: wheel => {
if (wheel.angleDelta.y > 0 && root.value < root.to) {
let newValue = Math.min(root.to, root.value + root.stepSize);
root.value = newValue;
} else if (wheel.angleDelta.y < 0 && root.value > root.from) {
let newValue = Math.max(root.from, root.value - root.stepSize);
root.value = newValue;
}
}
}
// Decrease button (left)
Item {
id: decreaseButton
height: parent.height
width: height
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
opacity: (root.enabled && root.value > root.from) || decreaseArea.containsMouse ? 1.0 : 0.3
Rectangle {
anchors.centerIn: parent
width: parent.height
height: width
radius: spinBoxContainer.radius
color: Color.mHover
opacity: decreaseArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
NIcon {
anchors.centerIn: parent
icon: "chevron-left"
pointSize: Style.fontSizeS
color: decreaseArea.containsMouse ? Color.mOnHover : Color.mPrimary
}
MouseArea {
id: decreaseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled && root.value > root.from
onPressed: {
root._repeatDirection = -1;
changeValue(root._repeatDirection, root.stepSize);
repeatTimer.start();
}
onReleased: stopRepeat()
onExited: stopRepeat()
}
}
// Increase button (right)
Item {
id: increaseButton
height: parent.height
width: height
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
opacity: (root.enabled && root.value < root.to) || increaseArea.containsMouse ? 1.0 : 0.3
Rectangle {
anchors.centerIn: parent
width: parent.height
height: width
radius: spinBoxContainer.radius
color: Color.mHover
opacity: increaseArea.containsMouse ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
NIcon {
anchors.centerIn: parent
icon: "chevron-right"
pointSize: Style.fontSizeS
color: increaseArea.containsMouse ? Color.mOnHover : Color.mPrimary
}
MouseArea {
id: increaseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
enabled: root.enabled && root.value < root.to
onPressed: {
root._repeatDirection = 1;
changeValue(root._repeatDirection, root.stepSize);
repeatTimer.start();
}
onReleased: stopRepeat()
onExited: stopRepeat()
}
}
// Center value display with separate prefix, value, and suffix
Item {
id: valueContainer
anchors.left: decreaseButton.right
anchors.right: increaseButton.left
anchors.verticalCenter: parent.verticalCenter
anchors.margins: 4
height: parent.height
RowLayout {
anchors.centerIn: parent
spacing: 0
// Prefix text (non-editable)
NText {
text: root.prefix
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6)
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
visible: root.prefix !== ""
}
// Editable number input
TextInput {
id: valueInput
text: valueInput.focus ? valueInput.text : root.value.toString()
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6)
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
selectByMouse: true
enabled: root.enabled
// Only allow numeric input within range
validator: IntValidator {
bottom: root.from
top: root.to
}
onAccepted: {
applyValue();
focus = false;
}
Keys.onEscapePressed: {
text = root.value.toString();
focus = false;
}
onFocusChanged: {
if (focus) {
selectAll();
} else {
applyValue();
}
}
function applyValue() {
let newValue = parseInt(text);
if (!isNaN(newValue)) {
// Don't manually set text here - let the binding handle it
newValue = Math.max(root.from, Math.min(root.to, newValue));
root.value = newValue;
}
}
}
// Suffix text (non-editable)
NText {
text: root.suffix
family: Settings.data.ui.fontFixed
pointSize: Style.fontSizeM
font.weight: Style.fontWeightMedium
color: Qt.alpha(Color.mOnSurface, root.enabled ? 1.0 : 0.6)
verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter
visible: root.suffix !== ""
}
}
}
}
}