Added delay button

This commit is contained in:
2026-06-18 04:07:53 +03:00
parent 70513c1136
commit 21fb4704a6
11 changed files with 268 additions and 9 deletions
+152 -1
View File
@@ -8,6 +8,10 @@ Item {
property bool showLabel: true
property bool darkMode: settingsContainer.darkMode
// 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
// Resolved feedback colour/width for the current status (ok / error).
readonly property color okColor: settingsContainer.feedbackOkColor
readonly property color errorColor: settingsContainer.feedbackErrorColor
@@ -69,6 +73,14 @@ Item {
Behavior on border.width { NumberAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
// Hold-to-activate state (0..1). Only used in "hold" trigger mode.
property real holdProgress: 0.0
property bool holding: false
// Squash slightly while held for tactile feedback.
scale: holding ? 0.93 : 1.0
Behavior on scale { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Image {
anchors.centerIn: parent
width: parent.width * 0.8
@@ -79,9 +91,148 @@ Item {
asynchronous: true
}
// Circular progress arc that fills up while the button is held.
Canvas {
id: holdRing
anchors.fill: parent
anchors.margins: -Math.max(3, width * 0.04)
visible: circle.holdProgress > 0.001
antialiasing: true
renderStrategy: Canvas.Threaded
// Ring colour derived from the button colour for a cohesive look.
property color baseColor: Qt.lighter(btnColor || "#7b007b", 1.9)
property color headColor: "#ffffff"
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
// Recessed track underneath for a sense of depth.
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()
// Progress arc drawn as overlapping segments with a manual
// colour fade (dim tail -> bright head). Segment boundaries are
// fixed in ANGLE space (not derived from progress) so they never
// shift between frames -> no jitter. Opaque colours + overlap
// hide the joints. Avoids the seam a conical gradient produces.
var tail = Qt.darker(baseColor, 1.5) // opaque dim tail
var N = 120
var segAng = 2 * Math.PI / N
var eps = Math.min(segAng * 0.5, 1.5 / r) // ~1px opaque overlap
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 // 0..1 along progress
ctx.beginPath()
ctx.strokeStyle = mix(tail, headColor, t)
ctx.arc(cx, cy, r, start + a0 - eps, start + a1 + eps, false)
ctx.stroke()
}
// Rounded tail cap.
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()
// Bright rounded head for a glossy, raised feel.
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()
}
// Soft bloom around the ring for volume.
layer.enabled: true
layer.effect: Glow {
radius: 14
samples: 25
spread: 0.2
color: Qt.lighter(btnColor || "#7b007b", 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)
resetAnim.restart()
}
}
}
NumberAnimation {
id: resetAnim
target: circle
property: "holdProgress"
to: 0.0
duration: 220
easing.type: Easing.OutCubic
}
MouseArea {
anchors.fill: parent
onClicked: btnController.invokeButton(btnId)
onPressed: {
if (delegateRoot.triggerModeResolved !== "click") {
resetAnim.stop()
circle.holding = true
holdAnim.restart()
}
}
onReleased: {
if (delegateRoot.triggerModeResolved !== "click" && circle.holding) {
// Released too early — cancel and rewind.
circle.holding = false
holdAnim.stop()
resetAnim.restart()
}
}
onCanceled: {
if (delegateRoot.triggerModeResolved !== "click") {
circle.holding = false
holdAnim.stop()
resetAnim.restart()
}
}
onClicked: {
if (delegateRoot.triggerModeResolved === "click")
btnController.invokeButton(btnId)
}
}
}