// 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 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