// A slider whose track carries a meaning, and whose value is committed once the // gesture is over rather than on every pixel. // // Two rows on the Displays page need something SliderRow cannot give them: // // * The night light temperature, whose track should BE the warmth it sets -- // a neutral track with a prism fill says nothing about what 2700 K looks // like, and this is the one slider whose numbers most people cannot picture. // * The SDR trims, which write through the display transaction. A transaction // per pixel of drag would arm a fifteen-second countdown dozens of times; // the value is applied once, when the button comes up. // // On release, not on an idle timer. Holding a slider still for a third of a // second is not the end of a gesture, and the commit an idle timer made // mid-hold armed a display transaction that turned this row's `enabled` false // with the button still down -- the control went dead under the hand using it. // The wheel has no release, so it keeps the timer, and that is all the timer is // for now. // // Not schema-bound and not a SettingRow: the caller says what the value is and // what to do with a new one, and the row stacks its control below the label in // a narrow window exactly as SliderRow does. import QtQuick import qs.config Item { id: root property string label: "" property string detail: "" property bool divider: true property bool enabled: true property real minimum: 0 property real maximum: 100 property real step: 1 // What is in effect. While a drag is in flight the row shows `pending` // instead, and hands the display back once the commit has been made. property real value: 0 property var pending: null readonly property real shown: root.pending !== null ? root.pending : root.value // The caller formats the number, because "1.15×", "3500 K" and "72%" have // nothing in common but the digits. property string readout: "" // Two or three colors, left to right. Painted across the whole track when // `fullTrack` is set -- for a value whose range is the point, like color // temperature -- and as the fill alone otherwise. property var trackColors: [Theme.accent, Theme.accentSecondary] property bool fullTrack: false property int commitDelay: 320 signal committed(real value) readonly property bool inline: root.width >= 520 readonly property int controlSpan: 300 function stopColor(index: int): color { const colors = root.trackColors; if (!colors || colors.length === 0) return Theme.accent; return colors[Math.min(index, colors.length - 1)]; } // Hands the pending value to the caller. Called on release, and by the // wheel's idle timer, which is the one input that never has one. function commit(): void { commitTimer.stop(); if (root.pending === null) return; root.committed(root.pending); releaseTimer.restart(); } function quantise(ratio: real): real { const raw = root.minimum + ratio * (root.maximum - root.minimum); const snapped = Math.round(raw / root.step) * root.step; const clamped = Math.max(root.minimum, Math.min(root.maximum, snapped)); return root.step < 1 ? Math.round(clamped * 100) / 100 : clamped; } width: parent ? parent.width : 620 implicitHeight: root.inline ? Math.max(56, copy.implicitHeight + 20) : copy.implicitHeight + 32 + 30 opacity: root.enabled ? 1 : 0.45 Column { id: copy x: 0 y: root.inline ? (root.height - height) / 2 : 10 width: root.inline ? root.width - root.controlSpan - 20 : root.width spacing: 3 Text { width: parent.width text: root.label color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.Medium elide: Text.ElideRight } Text { width: parent.width visible: root.detail !== "" text: root.detail color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } } Item { id: control width: root.inline ? root.controlSpan : root.width height: 32 x: root.inline ? root.width - width : 0 y: root.inline ? (root.height - height) / 2 : copy.y + copy.height + 10 Rectangle { id: track anchors.left: parent.left anchors.right: valueLabel.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter height: 10 radius: 5 border.width: 0 color: Theme.alpha(Theme.fg, 0.12) readonly property real ratio: root.maximum > root.minimum ? Math.max(0, Math.min(1, (root.shown - root.minimum) / (root.maximum - root.minimum))) : 0 Rectangle { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom width: root.fullTrack ? parent.width : parent.width * track.ratio radius: parent.radius border.width: 0 gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0; color: root.stopColor(0) } GradientStop { position: 0.5; color: root.stopColor(1) } GradientStop { position: 1.0; color: root.stopColor(2) } } } Rectangle { width: 16 height: 16 radius: 8 border.width: 0 color: Theme.fg anchors.verticalCenter: parent.verticalCenter x: Math.max(0, Math.min(parent.width - width, parent.width * track.ratio - width / 2)) } MouseArea { id: drag anchors.fill: parent anchors.margins: -8 enabled: root.enabled hoverEnabled: true cursorShape: Qt.PointingHandCursor // Local x is 8px ahead of the track's own origin because of the // negative margin above; subtract it before turning the // position into a fraction of the track. function move(positionX: real): void { root.pending = root.quantise( Math.max(0, Math.min(1, (positionX - 8) / track.width))); } onPressed: event => drag.move(event.x) onPositionChanged: event => { if (drag.pressed) drag.move(event.x); } onReleased: root.commit() // A grab taken away mid-drag is not a decision. The row goes // back to showing whatever is really in effect rather than // keeping a number nobody asked it to apply. onCanceled: { commitTimer.stop(); root.pending = null; } onWheel: event => { const direction = event.angleDelta.y > 0 ? 1 : -1; root.pending = Math.max(root.minimum, Math.min(root.maximum, root.shown + direction * root.step)); commitTimer.restart(); } } } Text { id: valueLabel anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 58 horizontalAlignment: Text.AlignRight text: root.readout color: Theme.fgDim font.family: Theme.fontFamily font.features: Theme.tabularFigures font.pixelSize: Theme.fontSize elide: Text.ElideRight } } Rectangle { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom height: 1 visible: root.divider color: Theme.alpha(Theme.fg, 0.065) } // The wheel's release. A notch is a whole gesture on its own, and a hand // spinning through a range means one commit at the end of the spin rather // than one per notch -- there is no button coming up to say when that is. Timer { id: commitTimer interval: root.commitDelay onTriggered: root.commit() } // Hands the readout back to whatever is really in effect. If the change was // refused -- an unclean value, a busy transaction -- the row snaps back to // the value it had rather than showing one nothing accepted. Timer { id: releaseTimer interval: 400 onTriggered: root.pending = null } }