Files
Button/qml/ButtonDelegate.qml
T

287 lines
11 KiB
QML

import QtQuick 2.12
import QtQuick.Controls 2.12
import QtGraphicalEffects 1.12
Item {
id: delegateRoot
property bool showLabel: true
// Per-button trigger config (from the model). Default: hold-to-activate.
readonly property string triggerModeResolved: (typeof triggerMode !== "undefined" && triggerMode) ? triggerMode : "hold"
readonly property int holdMsResolved: (typeof holdMs !== "undefined" && holdMs > 0) ? holdMs : 800
readonly property bool latchOn: typeof latchResetEnabled !== "undefined" && latchResetEnabled
readonly property bool isVoice: (typeof kind !== "undefined" && kind === "voice")
// Voice: allow interaction while lit (cancel) and while recording (stop in click mode).
// Block only while uploading (status 3 and not recording) or error.
readonly property bool voiceRecording: isVoice && status === 3 && btnController.isVoiceRecording(btnId)
readonly property bool inputBlocked: {
if (isVoice) {
if (status === 2) return true
if (status === 3 && !voiceRecording) return true // uploading
return false
}
return status === 3 || status === 2 || (status !== 0 && !latchOn)
}
// Resolved feedback colour/width for the current status (ok / error).
readonly property color okColor: settingsContainer.feedbackOkColor
readonly property color errorColor: settingsContainer.feedbackErrorColor
readonly property color holdColor: settingsContainer.feedbackHoldColor
readonly property color pendingColor: settingsContainer.feedbackPendingColor
readonly property color feedbackColor: status === 1 ? okColor
: status === 2 ? errorColor
: status === 3 ? pendingColor
: "#ffffff"
readonly property int feedbackWidth: status === 1 ? settingsContainer.feedbackOkWidth
: status === 2 ? settingsContainer.feedbackErrorWidth
: status === 3 ? settingsContainer.feedbackPendingWidth
: 2
// Glow: white=in-progress, ok/error use configured colours
Rectangle {
id: glowRect
anchors.centerIn: parent
width: circle.width * glowScale
height: width
radius: width / 2
color: delegateRoot.feedbackColor
opacity: status === 0 ? 0 : 0.5
visible: status !== 0 && settingsContainer.feedbackGlowRadius > 0
property real glowScale: 1.0
Behavior on opacity { NumberAnimation { duration: 300 } }
Behavior on color { ColorAnimation { duration: 200 } }
SequentialAnimation on glowScale {
running: status === 3
loops: Animation.Infinite
NumberAnimation { to: 1.4; duration: 700; easing.type: Easing.InOutSine }
NumberAnimation { to: 1.0; duration: 700; easing.type: Easing.InOutSine }
}
NumberAnimation on glowScale {
running: status !== 3
to: 1.0; duration: 300
}
layer.enabled: true
layer.effect: Glow {
radius: settingsContainer.feedbackGlowRadius
samples: 24
color: delegateRoot.feedbackColor
transparentBorder: true
}
}
// Circle button — fixed color from config; only the ring reacts to status.
Rectangle {
id: circle
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.82
height: width
radius: width / 2
color: btnColor || "#7b007b"
border.color: status === 1 ? delegateRoot.okColor
: status === 2 ? delegateRoot.errorColor
: status === 3 ? delegateRoot.pendingColor
: Qt.lighter(btnColor || "#7b007b", 1.5)
border.width: status === 1 || status === 2 || status === 3 ? delegateRoot.feedbackWidth : 0
Behavior on border.width { NumberAnimation { duration: 200 } }
Behavior on border.color {
enabled: status === 1 || status === 2 || status === 3
ColorAnimation { duration: 200 }
}
property real holdProgress: 0.0
property bool holding: false
scale: holding || delegateRoot.voiceRecording ? 0.93 : 1.0
Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Image {
anchors.centerIn: parent
width: parent.width * 0.8
height: width
fillMode: Image.PreserveAspectFit
source: iconPath
visible: iconPath !== ""
asynchronous: true
}
// Circular progress arc that fills up while the button is held (press buttons only).
Canvas {
id: holdRing
anchors.fill: parent
anchors.margins: -Math.max(3, width * 0.04)
visible: !delegateRoot.isVoice && circle.holdProgress > 0.001
antialiasing: true
renderStrategy: Canvas.Threaded
property color baseColor: Qt.lighter(delegateRoot.holdColor, 1.2)
property color headColor: delegateRoot.holdColor
function mix(c1, c2, t) {
return Qt.rgba(c1.r + (c2.r - c1.r) * t,
c1.g + (c2.g - c1.g) * t,
c1.b + (c2.b - c1.b) * t,
c1.a + (c2.a - c1.a) * t)
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var p = circle.holdProgress
if (p <= 0) return
var cx = width / 2
var cy = height / 2
var lw = Math.max(5, width * 0.085)
var r = width / 2 - lw / 2 - 1
var start = -Math.PI / 2
var sweep = p * 2 * Math.PI
var end = start + sweep
ctx.beginPath()
ctx.lineWidth = lw
ctx.lineCap = "round"
ctx.strokeStyle = Qt.rgba(0, 0, 0, 0.30)
ctx.arc(cx, cy, r, 0, 2 * Math.PI, false)
ctx.stroke()
var tail = Qt.darker(baseColor, 1.5)
var N = 120
var segAng = 2 * Math.PI / N
var eps = Math.min(segAng * 0.5, 1.5 / r)
ctx.lineCap = "butt"
ctx.lineWidth = lw
for (var i = 0; i < N; i++) {
var a0 = i * segAng
if (a0 >= sweep) break
var a1 = Math.min((i + 1) * segAng, sweep)
var t = sweep > 0 ? (a0 / sweep) : 0
ctx.beginPath()
ctx.strokeStyle = mix(tail, headColor, t)
ctx.arc(cx, cy, r, start + a0 - eps, start + a1 + eps, false)
ctx.stroke()
}
ctx.beginPath()
ctx.fillStyle = tail
ctx.arc(cx + r * Math.cos(start), cy + r * Math.sin(start),
lw / 2, 0, 2 * Math.PI, false)
ctx.fill()
ctx.beginPath()
ctx.fillStyle = headColor
ctx.arc(cx + r * Math.cos(end), cy + r * Math.sin(end),
lw * 0.55, 0, 2 * Math.PI, false)
ctx.fill()
}
layer.enabled: true
layer.effect: Glow {
radius: 14
samples: 25
spread: 0.2
color: Qt.lighter(delegateRoot.holdColor, 1.7)
transparentBorder: true
}
Connections {
target: circle
function onHoldProgressChanged() { holdRing.requestPaint() }
}
}
NumberAnimation {
id: holdAnim
target: circle
property: "holdProgress"
from: 0.0
to: 1.0
duration: delegateRoot.holdMsResolved
easing.type: Easing.Linear
onFinished: {
if (circle.holding) {
circle.holding = false
btnController.invokeButton(btnId)
holdLingerTimer.restart()
}
}
}
Timer {
id: holdLingerTimer
interval: 700
onTriggered: resetAnim.restart()
}
NumberAnimation {
id: resetAnim
target: circle
property: "holdProgress"
to: 0.0
duration: 400
easing.type: Easing.OutCubic
}
MouseArea {
anchors.fill: parent
enabled: !delegateRoot.inputBlocked
onPressed: {
if (delegateRoot.inputBlocked) return
if (delegateRoot.isVoice) {
circle.holding = true
btnController.voicePointerPressed(btnId)
return
}
if (delegateRoot.triggerModeResolved !== "click") {
resetAnim.stop()
circle.holding = true
holdAnim.restart()
}
}
onReleased: {
if (delegateRoot.isVoice) {
circle.holding = false
btnController.voicePointerReleased(btnId)
return
}
if (delegateRoot.triggerModeResolved !== "click" && circle.holding) {
circle.holding = false
holdAnim.stop()
resetAnim.restart()
}
}
onCanceled: {
if (delegateRoot.isVoice) {
circle.holding = false
btnController.voicePointerReleased(btnId)
return
}
if (delegateRoot.triggerModeResolved !== "click") {
circle.holding = false
holdAnim.stop()
resetAnim.restart()
}
}
onClicked: {
if (delegateRoot.inputBlocked) return
if (delegateRoot.isVoice) {
if (status === 1 || delegateRoot.triggerModeResolved === "click")
btnController.voiceClicked(btnId)
return
}
if (delegateRoot.triggerModeResolved === "click")
btnController.invokeButton(btnId)
}
}
}
Text {
anchors.top: circle.bottom
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
visible: showLabel
text: statusText !== "" ? statusText : label
color: settingsContainer.labelColor
font.pixelSize: Math.max(10, parent.width * 0.1)
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}