198 lines
7.1 KiB
QML
198 lines
7.1 KiB
QML
// Saturation, as a nudge rather than a stored number.
|
||
//
|
||
// There is no saturation preference: `setSaturation` re-saturates the palette
|
||
// that is in effect right now, which makes it compounding -- committing 1.5
|
||
// twice really is 2.25. So the row keeps the reading it has shown you and
|
||
// commits the RATIO between the old reading and the new one, which makes the
|
||
// number on screen an honest total rather than a lever that lies the second
|
||
// time it is pulled. Selecting a different theme resets the reading to neutral,
|
||
// because the new palette is a new ground.
|
||
//
|
||
// The commit happens on release only. A drag through 0.6 → 0.9 → 1.4 would
|
||
// otherwise rewrite all nineteen tokens at every pixel, and each rewrite is
|
||
// lossy: HSV saturation cannot be recovered once it has been rounded to a hex.
|
||
|
||
import QtQuick
|
||
import qs.config
|
||
import qs.services
|
||
|
||
SettingRow {
|
||
id: root
|
||
|
||
label: "Saturation"
|
||
detail: "Shifts every derived surface and text tone together"
|
||
controlWidth: 280
|
||
|
||
// 1.0 is the palette as it stands. Range 0 – 2, shown as 0 – 200%.
|
||
property real shown: 1
|
||
property real committed: 1
|
||
|
||
readonly property real maximum: 2
|
||
|
||
// A theme change replaces the ground this row was measuring against.
|
||
readonly property string activeId: ThemeProfiles.activeId
|
||
onActiveIdChanged: {
|
||
root.shown = 1;
|
||
root.committed = 1;
|
||
}
|
||
|
||
function apply(): void {
|
||
if (Math.abs(root.shown - root.committed) < 0.005)
|
||
return;
|
||
// Dividing by the last reading turns an absolute slider into the
|
||
// relative factor the service actually takes.
|
||
const factor = root.committed > 0.02 ? root.shown / root.committed : root.maximum;
|
||
if (ThemeProfiles.setSaturation(Math.max(0, Math.min(root.maximum, factor))))
|
||
root.committed = root.shown;
|
||
else
|
||
root.shown = root.committed;
|
||
}
|
||
|
||
function moveTo(ratio: real): void {
|
||
root.shown = Math.round(Math.max(0, Math.min(1, ratio)) * root.maximum * 100) / 100;
|
||
}
|
||
|
||
function step(direction: int): void {
|
||
root.moveTo((root.shown + direction * 0.05) / root.maximum);
|
||
root.apply();
|
||
}
|
||
|
||
Item {
|
||
id: control
|
||
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: 270
|
||
height: 32
|
||
activeFocusOnTab: true
|
||
|
||
Accessible.role: Accessible.Slider
|
||
Accessible.name: root.label
|
||
Accessible.description: Math.round(root.shown * 100) + " percent, range 0 to 200"
|
||
Accessible.focusable: true
|
||
Accessible.focused: control.activeFocus
|
||
Accessible.onIncreaseAction: root.step(1)
|
||
Accessible.onDecreaseAction: root.step(-1)
|
||
|
||
Keys.onPressed: event => {
|
||
if (event.key === Qt.Key_Left || event.key === Qt.Key_Down) {
|
||
root.step(-1);
|
||
event.accepted = true;
|
||
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Up) {
|
||
root.step(1);
|
||
event.accepted = true;
|
||
} else if (event.key === Qt.Key_Home) {
|
||
root.moveTo(0);
|
||
root.apply();
|
||
event.accepted = true;
|
||
} else if (event.key === Qt.Key_End) {
|
||
root.moveTo(1);
|
||
root.apply();
|
||
event.accepted = true;
|
||
}
|
||
}
|
||
|
||
Rectangle {
|
||
id: frame
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: readout.left
|
||
anchors.rightMargin: 10
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
height: 24
|
||
radius: 9
|
||
color: "transparent"
|
||
border.width: control.activeFocus ? 2 : 1
|
||
border.color: control.activeFocus
|
||
? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.08)
|
||
|
||
// Written out rather than reusing ValueSlider: this row needs the
|
||
// release, and ValueSlider reports only movement.
|
||
Rectangle {
|
||
id: track
|
||
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
anchors.margins: 4
|
||
height: 10
|
||
radius: 5
|
||
color: Theme.alpha(Theme.fg, 0.12)
|
||
border.width: 0
|
||
|
||
Rectangle {
|
||
id: fill
|
||
|
||
anchors.left: parent.left
|
||
anchors.top: parent.top
|
||
anchors.bottom: parent.bottom
|
||
width: parent.width * Math.max(0, Math.min(1, root.shown / root.maximum))
|
||
radius: parent.radius
|
||
border.width: 0
|
||
gradient: Gradient {
|
||
orientation: Gradient.Horizontal
|
||
GradientStop { position: 0.0; color: Theme.accent }
|
||
GradientStop { position: 1.0; color: Theme.accentSecondary }
|
||
}
|
||
}
|
||
|
||
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, fill.width - width / 2))
|
||
visible: drag.containsMouse || drag.pressed
|
||
}
|
||
|
||
MouseArea {
|
||
id: drag
|
||
|
||
anchors.fill: parent
|
||
anchors.margins: -8
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
|
||
function ratioAt(mouseX: real): real {
|
||
return (mouseX - 8) / track.width;
|
||
}
|
||
|
||
onPressed: event => root.moveTo(drag.ratioAt(event.x))
|
||
onPositionChanged: event => {
|
||
if (drag.pressed)
|
||
root.moveTo(drag.ratioAt(event.x));
|
||
}
|
||
onReleased: root.apply()
|
||
// A grab taken away mid-drag never releases, so nothing
|
||
// would ever apply the reading left on screen -- and the
|
||
// next commit would measure its ratio against a number the
|
||
// palette was never re-saturated to. Back to the last
|
||
// reading that was really applied.
|
||
onCanceled: root.shown = root.committed
|
||
onWheel: event => {
|
||
root.moveTo((root.shown + (event.angleDelta.y > 0 ? 0.05 : -0.05))
|
||
/ root.maximum);
|
||
root.apply();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Text {
|
||
id: readout
|
||
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: 56
|
||
horizontalAlignment: Text.AlignRight
|
||
text: Math.round(root.shown * 100) + "%"
|
||
color: Theme.fgDim
|
||
font.family: Theme.fontFamily
|
||
font.features: Theme.tabularFigures
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
}
|
||
}
|
||
}
|