Files
tactileipc3d/qml/content/ColorPickerDialog.qml
2026-01-20 19:55:56 +08:00

617 lines
22 KiB
QML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ColorPickerWindow.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import TactileIPC 1.0
Window {
id: root
width: 360
height: 650
visible: false
modality: Qt.ApplicationModal
flags: Qt.Dialog
title: qsTr("颜色选择")
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
Material.accent: Material.Green
Material.primary: Material.Green
readonly property bool isDark: !Backend.lightMode
readonly property color windowBg: isDark ? "#1F1F1F" : "#F7F7F7"
readonly property color panelBorder: isDark ? "#2E2E2E" : "#D9D9D9"
readonly property color surfaceBorder: isDark ? "#343434" : "#CFCFCF"
readonly property color textPrimary: isDark ? "#EDEDED" : "#1F1F1F"
readonly property color textSecondary: isDark ? "#D8D8D8" : "#616161"
readonly property color textMuted: isDark ? "#BEBEBE" : "#6E6E6E"
readonly property color controlBg: isDark ? "#2A2A2A" : "#FFFFFF"
readonly property color controlBorder: isDark ? "#3A3A3A" : "#CFCFCF"
readonly property color highlightBg: isDark ? "#55FFFFFF" : "#33000000"
readonly property color highlightText: isDark ? "#111111" : "#FFFFFF"
readonly property color indicatorBorder: isDark ? "#6A6A6A" : "#9E9E9E"
readonly property color accentColor: Material.color(Material.Green)
// ===== API =====
property color color: "#FF7032D2" // AARRGGBB
signal accepted(color c)
signal rejected()
function openWith(c) {
syncFromColor(c ?? root.color)
visible = true
requestActivate()
}
Keys.onEscapePressed: {
visible = false
rejected()
}
// ===== internal HSV(A) =====
property real h: 0.75 // 0..1
property real s: 0.45
property real v: 0.82
property real a: 1.0
property bool _lock: false
function clamp01(x) { return Math.max(0, Math.min(1, x)) }
function clampInt(x, lo, hi) { return Math.max(lo, Math.min(hi, Math.round(x))) }
function hsvToRgb(hh, ss, vv) {
const h6 = (hh % 1) * 6
const c = vv * ss
const x = c * (1 - Math.abs((h6 % 2) - 1))
const m = vv - c
let r1=0, g1=0, b1=0
if (0 <= h6 && h6 < 1) { r1=c; g1=x; b1=0 }
else if (1 <= h6 && h6 < 2) { r1=x; g1=c; b1=0 }
else if (2 <= h6 && h6 < 3) { r1=0; g1=c; b1=x }
else if (3 <= h6 && h6 < 4) { r1=0; g1=x; b1=c }
else if (4 <= h6 && h6 < 5) { r1=x; g1=0; b1=c }
else { r1=c; g1=0; b1=x }
return { r: r1 + m, g: g1 + m, b: b1 + m }
}
function rgbToHsv(r, g, b) {
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const d = max - min
let hh = 0
if (d !== 0) {
if (max === r) hh = ((g - b) / d) % 6
else if (max === g) hh = (b - r) / d + 2
else hh = (r - g) / d + 4
hh /= 6
if (hh < 0) hh += 1
}
const ss = (max === 0) ? 0 : d / max
const vv = max
return { h: hh, s: ss, v: vv }
}
function toHex2(v01) {
const n = clampInt(clamp01(v01) * 255, 0, 255)
const s = n.toString(16).toUpperCase()
return (s.length === 1) ? ("0" + s) : s
}
function colorToHexAARRGGBB(c) {
return "#" + toHex2(c.a) + toHex2(c.r) + toHex2(c.g) + toHex2(c.b)
}
function parseHex(str) {
let t = ("" + str).trim()
if (t.startsWith("0x") || t.startsWith("0X")) t = t.slice(2)
if (t.startsWith("#")) t = t.slice(1)
if (t.length === 6) {
const rr = parseInt(t.slice(0,2), 16)
const gg = parseInt(t.slice(2,4), 16)
const bb = parseInt(t.slice(4,6), 16)
if ([rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, 1)
}
if (t.length === 8) {
const aa = parseInt(t.slice(0,2), 16)
const rr = parseInt(t.slice(2,4), 16)
const gg = parseInt(t.slice(4,6), 16)
const bb = parseInt(t.slice(6,8), 16)
if ([aa,rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, aa/255)
}
return null
}
function syncFromHSV() {
if (_lock) return
_lock = true
const rgb = hsvToRgb(h, s, v)
color = Qt.rgba(rgb.r, rgb.g, rgb.b, a)
// 更新 UI
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(color.r * 255, 0, 255)
gField.value = clampInt(color.g * 255, 0, 255)
bField.value = clampInt(color.b * 255, 0, 255)
// HSV 显示用度/百分比
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function syncFromColor(c) {
if (_lock) return
_lock = true
const hsv = rgbToHsv(c.r, c.g, c.b)
h = hsv.h; s = hsv.s; v = hsv.v; a = c.a
color = Qt.rgba(c.r, c.g, c.b, a)
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(c.r * 255, 0, 255)
gField.value = clampInt(c.g * 255, 0, 255)
bField.value = clampInt(c.b * 255, 0, 255)
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function applyRGB(r255, g255, b255) {
if (_lock) return
const rr = clampInt(r255, 0, 255) / 255
const gg = clampInt(g255, 0, 255) / 255
const bb = clampInt(b255, 0, 255) / 255
const hsv = rgbToHsv(rr, gg, bb)
h = hsv.h; s = hsv.s; v = hsv.v
syncFromHSV()
}
function applyHSV(hDeg, sPct, vPct) {
if (_lock) return
h = clamp01(hDeg / 360)
s = clamp01(sPct / 100)
v = clamp01(vPct / 100)
syncFromHSV()
}
// 初始同步
Component.onCompleted: syncFromColor(root.color)
// ====== UI: 深色窗口面板(不是卡片)======
Rectangle {
anchors.fill: parent
radius: 0
color: root.windowBg
border.width: 1
border.color: root.panelBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 顶部:标题 + 勾选(参考图右上角)
// RowLayout {
// Layout.fillWidth: true
// spacing: 8
// Label {
// text: root.title
// color: root.textPrimary
// font.pixelSize: 14
// font.weight: Font.DemiBold
// Layout.fillWidth: true
// }
// CheckBox {
// id: alphaToggle
// checked: true
// contentItem: Label {
// text: qsTr("实时预览")
// color: root.textSecondary
// verticalAlignment: Text.AlignVCenter
// elide: Text.ElideRight
// }
// indicator: Rectangle {
// implicitWidth: 16
// implicitHeight: 16
// radius: 3
// border.width: 1
// border.color: root.indicatorBorder
// color: alphaToggle.checked ? root.accentColor : "transparent"
// // 选中勾
// Canvas {
// anchors.fill: parent
// onPaint: {
// const ctx = getContext("2d")
// ctx.clearRect(0,0,width,height)
// if (!alphaToggle.checked) return
// ctx.strokeStyle = "white"
// ctx.lineWidth = 2
// ctx.lineCap = "round"
// ctx.beginPath()
// ctx.moveTo(width*0.25, height*0.55)
// ctx.lineTo(width*0.45, height*0.72)
// ctx.lineTo(width*0.78, height*0.30)
// ctx.stroke()
// }
// }
// }
// }
// ToolButton {
// text: "✕"
// onClicked: { root.visible = false; root.rejected() }
// }
// }
// HSV 面板
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 185
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: svCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgbHue = root.hsvToRgb(root.h, 1, 1)
// X: white -> hue
const gx = ctx.createLinearGradient(0,0,w,0)
gx.addColorStop(0, "rgb(255,255,255)")
gx.addColorStop(1, "rgb(" + Math.round(rgbHue.r*255) + "," + Math.round(rgbHue.g*255) + "," + Math.round(rgbHue.b*255) + ")")
ctx.fillStyle = gx
ctx.fillRect(0,0,w,hh)
// Y: transparent -> black
const gy = ctx.createLinearGradient(0,0,0,hh)
gy.addColorStop(0, "rgba(0,0,0,0)")
gy.addColorStop(1, "rgba(0,0,0,1)")
ctx.fillStyle = gy
ctx.fillRect(0,0,w,hh)
// handle
const x = root.s * w
const y = (1 - root.v) * hh
ctx.beginPath()
ctx.arc(x, y, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, y, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx, my) {
root.s = root.clamp01(mx / width)
root.v = root.clamp01(1 - (my / height))
root.syncFromHSV()
}
onPressed: (e) => apply(e.x, e.y)
onPositionChanged: (e) => { if (pressed) apply(e.x, e.y) }
}
}
// Hue 彩虹条(参考图那种)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: hueCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const grad = ctx.createLinearGradient(0,0,w,0)
for (let i=0; i<=6; i++) {
const t = i/6
const rgb = root.hsvToRgb(t, 1, 1)
grad.addColorStop(t, "rgb(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ")")
}
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.h * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.h = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// Alpha 条(棋盘 + 渐变 + 右侧圆点)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
// checker
Canvas {
id: checkerCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 8
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.16)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Canvas {
id: alphaCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgb = root.hsvToRgb(root.h, root.s, root.v)
const grad = ctx.createLinearGradient(0,0,w,0)
grad.addColorStop(0, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",0)")
grad.addColorStop(1, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",1)")
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.a * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.a = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// 下方:色块 + Hex + 透明度百分比
RowLayout {
Layout.fillWidth: true
spacing: 10
// 预览色块(带棋盘底)
Rectangle {
width: 44
height: 44
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 10
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Rectangle {
anchors.fill: parent
color: root.color
}
}
TextField {
id: hexField
Layout.fillWidth: true
text: "#FF7032D2"
placeholderText: "#AARRGGBB"
inputMethodHints: Qt.ImhPreferUppercase | Qt.ImhNoPredictiveText
// 用 palette而不是 color/selectionColor/selectedTextColor
palette.text: root.textPrimary
palette.placeholderText: root.textMuted
palette.highlight: root.highlightBg
palette.highlightedText: root.highlightText
palette.base: root.controlBg // 输入框底色(有的 style 会用它)
palette.buttonText: root.textPrimary
background: Rectangle {
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
}
onEditingFinished: {
const c = root.parseHex(text)
if (c) root.syncFromColor(c)
else text = root.colorToHexAARRGGBB(root.color)
}
}
Rectangle {
width: 54
height: 44
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
Label {
id: alphaPercent
anchors.centerIn: parent
text: "100%"
color: root.textPrimary
font.pixelSize: 12
}
}
}
// RGB / HSV 数值区(像参考图那样两行块)
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 8
rowSpacing: 8
// ---- Row1: RGB ----
Label { text: "RGB"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: rField
from: 0; to: 255; editable: true
value: 112
Layout.fillWidth: true
onValueModified: root.applyRGB(value, gField.value, bField.value)
}
SpinBox {
id: gField
from: 0; to: 255; editable: true
value: 50
Layout.fillWidth: true
onValueModified: root.applyRGB(rField.value, value, bField.value)
}
Item { width: 1; height: 1 } // 占位,让下一行对齐
SpinBox {
id: bField
from: 0; to: 255; editable: true
value: 210
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyRGB(rField.value, gField.value, value)
}
// ---- Row2: HSV ----
Label { text: "HSV"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: hField
from: 0; to: 360; editable: true
value: 263
Layout.fillWidth: true
onValueModified: root.applyHSV(value, sField.value, vField.value)
}
SpinBox {
id: sField
from: 0; to: 100; editable: true
value: 64
Layout.fillWidth: true
onValueModified: root.applyHSV(hField.value, value, vField.value)
}
Item { width: 1; height: 1 }
SpinBox {
id: vField
from: 0; to: 100; editable: true
value: 51
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyHSV(hField.value, sField.value, value)
}
}
// 底部按钮(像窗口)
RowLayout {
Layout.fillWidth: true
spacing: 10
Item { Layout.fillWidth: true }
Button {
text: qsTr("Cancel")
onClicked: { root.visible = false; root.rejected() }
}
Button {
text: qsTr("OK")
highlighted: true
onClicked: { root.visible = false; root.accepted(root.color) }
}
}
}
}
// 每次 HSV 改变UI 同步
onHChanged: if (!_lock) syncFromHSV()
onSChanged: if (!_lock) syncFromHSV()
onVChanged: if (!_lock) syncFromHSV()
onAChanged: if (!_lock) syncFromHSV()
}