187 lines
7.2 KiB
QML
187 lines
7.2 KiB
QML
// Hue, saturation and value for both ends of the Prism accent.
|
|
//
|
|
// This is the fine-tune behind a disclosure, not the fast path: the four wells
|
|
// above cover what most edits are, and these six rows are for the last few
|
|
// degrees. Each row is a labelled, keyboard-operable slider with a numeric
|
|
// readout, because a hue ring alone tells someone with a color vision
|
|
// deficiency nothing.
|
|
//
|
|
// Committed on a debounce rather than per move. Each move used to write BOTH
|
|
// accent preferences, so a single drag across the hue row spent the whole
|
|
// gesture in apply-and-verify round trips and left the desktop repainting
|
|
// behind the pointer. The sliders now track the drag locally and one commit
|
|
// lands once it stops.
|
|
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
import qs.widgets
|
|
import "../../services/ThemeProfileModel.js" as ThemeProfileModel
|
|
|
|
Column {
|
|
id: editor
|
|
|
|
width: parent ? parent.width : 620
|
|
spacing: 0
|
|
|
|
// The pair being dragged; null means "nothing pending, show what is stored".
|
|
property var pending: null
|
|
|
|
readonly property string shownAccent: editor.pending
|
|
? editor.pending.accent : String(ThemeProfiles.activeProfile.accent)
|
|
readonly property string shownSecondary: editor.pending
|
|
? editor.pending.secondary : String(ThemeProfiles.activeProfile.secondary)
|
|
|
|
readonly property var primaryHsv: ThemeProfileModel.hexToHsv(editor.shownAccent)
|
|
|| ({ h: 0, s: 0, v: 0 })
|
|
readonly property var secondaryHsv: ThemeProfileModel.hexToHsv(editor.shownSecondary)
|
|
|| ({ h: 0, s: 0, v: 0 })
|
|
|
|
function changeChannel(target: string, channel: string, ratio: real): void {
|
|
const source = target === "primary" ? editor.primaryHsv : editor.secondaryHsv;
|
|
const next = { h: source.h, s: source.s, v: source.v };
|
|
next[channel] = Math.round(Math.max(0, Math.min(1, ratio))
|
|
* (channel === "h" ? 360 : 100));
|
|
const changed = ThemeProfileModel.hsvToHex(next.h, next.s, next.v);
|
|
if (!changed)
|
|
return;
|
|
editor.pending = {
|
|
accent: target === "primary" ? changed : editor.shownAccent,
|
|
secondary: target === "secondary" ? changed : editor.shownSecondary
|
|
};
|
|
commitTimer.restart();
|
|
}
|
|
|
|
component HsvRow: SettingRow {
|
|
id: row
|
|
|
|
required property string target
|
|
required property string channel
|
|
required property real channelValue
|
|
required property real channelMaximum
|
|
property string suffix: "%"
|
|
|
|
controlWidth: 280
|
|
|
|
Item {
|
|
id: keyboardSlider
|
|
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 270
|
|
height: 32
|
|
activeFocusOnTab: true
|
|
|
|
Accessible.role: Accessible.Slider
|
|
Accessible.name: row.label
|
|
Accessible.description: Math.round(row.channelValue) + row.suffix
|
|
+ ", range 0 to " + row.channelMaximum
|
|
Accessible.focusable: true
|
|
Accessible.focused: activeFocus
|
|
Accessible.onIncreaseAction: keyboardSlider.step(1)
|
|
Accessible.onDecreaseAction: keyboardSlider.step(-1)
|
|
|
|
function step(direction: int): void {
|
|
const value = Math.max(0, Math.min(row.channelMaximum,
|
|
row.channelValue + direction));
|
|
editor.changeChannel(row.target, row.channel, value / row.channelMaximum);
|
|
}
|
|
|
|
Keys.onPressed: event => {
|
|
if (event.key === Qt.Key_Left || event.key === Qt.Key_Down) {
|
|
keyboardSlider.step(-1);
|
|
event.accepted = true;
|
|
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_Up) {
|
|
keyboardSlider.step(1);
|
|
event.accepted = true;
|
|
} else if (event.key === Qt.Key_Home) {
|
|
editor.changeChannel(row.target, row.channel, 0);
|
|
event.accepted = true;
|
|
} else if (event.key === Qt.Key_End) {
|
|
editor.changeChannel(row.target, row.channel, 1);
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: readout.left
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: 24
|
|
radius: 9
|
|
color: "transparent"
|
|
border.width: keyboardSlider.activeFocus ? 2 : 1
|
|
border.color: keyboardSlider.activeFocus
|
|
? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.08)
|
|
|
|
ValueSlider {
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
value: row.channelMaximum > 0 ? row.channelValue / row.channelMaximum : 0
|
|
onMoved: ratio => editor.changeChannel(row.target, row.channel, ratio)
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: readout
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 56
|
|
horizontalAlignment: Text.AlignRight
|
|
text: Math.round(row.channelValue) + row.suffix
|
|
color: Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.features: Theme.tabularFigures
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
}
|
|
|
|
HsvRow {
|
|
target: "primary"; channel: "h"; channelValue: editor.primaryHsv.h; channelMaximum: 360
|
|
suffix: "°"; label: "Primary hue"; detail: "Color family, measured from 0 to 360 degrees"
|
|
}
|
|
HsvRow {
|
|
target: "primary"; channel: "s"; channelValue: editor.primaryHsv.s; channelMaximum: 100
|
|
label: "Primary saturation"; detail: "Color intensity from grey to vivid"
|
|
}
|
|
HsvRow {
|
|
target: "primary"; channel: "v"; channelValue: editor.primaryHsv.v; channelMaximum: 100
|
|
label: "Primary value"; detail: "Brightness from black to full color"
|
|
}
|
|
HsvRow {
|
|
target: "secondary"; channel: "h"; channelValue: editor.secondaryHsv.h; channelMaximum: 360
|
|
suffix: "°"; label: "Secondary hue"; detail: "Color family at the far end of the Prism gradient"
|
|
}
|
|
HsvRow {
|
|
target: "secondary"; channel: "s"; channelValue: editor.secondaryHsv.s; channelMaximum: 100
|
|
label: "Secondary saturation"; detail: "Color intensity from grey to vivid"
|
|
}
|
|
HsvRow {
|
|
target: "secondary"; channel: "v"; channelValue: editor.secondaryHsv.v; channelMaximum: 100
|
|
label: "Secondary value"; detail: "Brightness from black to full color"
|
|
divider: false
|
|
}
|
|
|
|
Timer {
|
|
id: commitTimer
|
|
interval: 200
|
|
onTriggered: {
|
|
if (editor.pending === null)
|
|
return;
|
|
ThemeProfiles.setAccentPair(editor.pending.accent, editor.pending.secondary);
|
|
releaseTimer.restart();
|
|
}
|
|
}
|
|
|
|
// Hands the sliders back to the stored pair. If the write was refused they
|
|
// snap back to what is really in effect rather than showing a color
|
|
// nothing accepted.
|
|
Timer {
|
|
id: releaseTimer
|
|
interval: 160
|
|
onTriggered: editor.pending = null
|
|
}
|
|
}
|