// Advanced accent editing remains a labelled, keyboard-operable extension of // the named fast path. Hue is always accompanied by saturation, value, a // numeric readout, and the two-colour preview supplied by Appearance. import QtQuick import Quickshell import Quickshell.Io 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 property string pickerTarget: "primary" property string lastError: "" readonly property var primaryHsv: ThemeProfileModel.hexToHsv( ThemeProfiles.activeProfile.accent) || ({ h: 0, s: 0, v: 0 }) readonly property var secondaryHsv: ThemeProfileModel.hexToHsv( ThemeProfiles.activeProfile.secondary) || ({ 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); const primary = target === "primary" ? changed : ThemeProfiles.activeProfile.accent; const secondary = target === "secondary" ? changed : ThemeProfiles.activeProfile.secondary; ThemeProfiles.setAccentPair(primary, secondary); } function pick(target: string): void { if (screenPicker.running) return; editor.pickerTarget = target; editor.lastError = ""; screenPicker.exec(["hyprpicker", "--format=hex", "--lowercase-hex", "--quiet", "--no-fancy"]); } function acceptPicked(value: string): void { const picked = String(value).trim().toLowerCase(); if (ThemeProfileModel.hexToHsv(picked) === null) { editor.lastError = "The sampled colour was not valid."; return; } const primary = editor.pickerTarget === "primary" ? picked : ThemeProfiles.activeProfile.accent; const secondary = editor.pickerTarget === "secondary" ? picked : ThemeProfiles.activeProfile.secondary; ThemeProfiles.setAccentPair(primary, secondary); } component HsvRow: SettingRow { id: root 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: root.label Accessible.description: Math.round(root.channelValue) + root.suffix + ", range 0 to " + root.channelMaximum Accessible.focusable: true Accessible.focused: activeFocus Accessible.onIncreaseAction: keyboardSlider.step(1) Accessible.onDecreaseAction: keyboardSlider.step(-1) function step(direction: int): void { const increment = root.channel === "h" ? 1 : 1; const value = Math.max(0, Math.min(root.channelMaximum, root.channelValue + direction * increment)); editor.changeChannel(root.target, root.channel, value / root.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(root.target, root.channel, 0); event.accepted = true; } else if (event.key === Qt.Key_End) { editor.changeChannel(root.target, root.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: root.channelMaximum > 0 ? root.channelValue / root.channelMaximum : 0 onMoved: ratio => editor.changeChannel(root.target, root.channel, ratio) } } Text { id: readout anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter width: 56 horizontalAlignment: Text.AlignRight text: Math.round(root.channelValue) + root.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: "Colour family, measured from 0 to 360 degrees" } HsvRow { target: "primary"; channel: "s"; channelValue: editor.primaryHsv.s; channelMaximum: 100 label: "Primary saturation"; detail: "Colour 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 colour" } HsvRow { target: "secondary"; channel: "h"; channelValue: editor.secondaryHsv.h; channelMaximum: 360 suffix: "°"; label: "Secondary hue"; detail: "Colour family at the far end of the Prism gradient" } HsvRow { target: "secondary"; channel: "s"; channelValue: editor.secondaryHsv.s; channelMaximum: 100 label: "Secondary saturation"; detail: "Colour 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 colour" } SettingRow { label: "Pick colour from screen" detail: editor.lastError !== "" ? editor.lastError : "Sample either end of the accent gradient with hyprpicker" divider: false controlWidth: 330 Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 7 SettingsButton { text: "Pick primary from screen" enabled: !screenPicker.running activeFocusOnTab: enabled border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: editor.pick("primary") Keys.onReturnPressed: if (enabled) editor.pick("primary") Keys.onSpacePressed: if (enabled) editor.pick("primary") } SettingsButton { text: "Pick secondary from screen" enabled: !screenPicker.running activeFocusOnTab: enabled border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: editor.pick("secondary") Keys.onReturnPressed: if (enabled) editor.pick("secondary") Keys.onSpacePressed: if (enabled) editor.pick("secondary") } } } Process { id: screenPicker stdout: StdioCollector { onStreamFinished: editor.acceptPicked(this.text) } onExited: (exitCode, exitStatus) => { if (exitCode !== 0) editor.lastError = "Screen colour picking was cancelled or unavailable."; } } }