187 lines
5.6 KiB
QML
187 lines
5.6 KiB
QML
/*
|
|
* Split-flap display mode — airport departure board style.
|
|
* Only digits are shown as flaps; the euro sign is rendered as plain
|
|
* text (default font, white) next to the board, and the trend shows
|
|
* on a small LED. Changed characters flip with a cascading animation
|
|
* spread over the configured number of seconds.
|
|
* Pure QtQuick so it can be tested outside the plasmoid shell.
|
|
*/
|
|
|
|
import QtQuick 2.15
|
|
|
|
Row {
|
|
id: root
|
|
|
|
// wired by the parent loader
|
|
property string display: "--"
|
|
property color trendColor: "#9e9e9e"
|
|
// seconds over which the cascade to a new price is spread
|
|
property int flapSeconds: 3
|
|
|
|
spacing: height * 0.06
|
|
|
|
function digitsOf(s) {
|
|
var out = "";
|
|
for (var i = 0; i < s.length; i++) {
|
|
var ch = s.charAt(i);
|
|
if ((ch >= "0" && ch <= "9") || ch === "-") {
|
|
out += ch;
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// UI-only derived values (euro visibility); logic must not rely on these —
|
|
// QML gives no ordering guarantee between a change handler and the
|
|
// re-evaluation of dependent bindings.
|
|
readonly property string digitsOnly: digitsOf(display)
|
|
readonly property string boardText: digitsOnly.length > 0 ? digitsOnly : "--"
|
|
|
|
property var chars: []
|
|
property bool ready: false
|
|
readonly property real cellFont: height * 0.72
|
|
readonly property real cellWidth: cellFont * 0.80
|
|
// one flap open+close takes 260 ms; keep delays above that
|
|
readonly property int flapDuration: 260
|
|
|
|
Component.onCompleted: {
|
|
chars = boardText.split("");
|
|
ready = true;
|
|
}
|
|
|
|
onDisplayChanged: {
|
|
if (!ready) {
|
|
return;
|
|
}
|
|
// compute from display directly: derived bindings may still be
|
|
// stale at this point (QML makes no ordering guarantee)
|
|
var digits = digitsOf(display);
|
|
var nc = (digits.length > 0 ? digits : "--").split("");
|
|
var n = Math.max(chars.length, nc.length);
|
|
// spread the changed cells evenly across flapSeconds
|
|
var changed = [];
|
|
for (var i = 0; i < n; i++) {
|
|
var from = i < chars.length ? chars[i] : "";
|
|
var to = i < nc.length ? nc[i] : "";
|
|
if (from !== to) {
|
|
changed.push([i, to]);
|
|
}
|
|
}
|
|
var span = Math.max(1, flapSeconds) * 1000 - flapDuration;
|
|
var step = changed.length > 1 ? Math.max(0, Math.floor(span / (changed.length - 1))) : 0;
|
|
for (var j = 0; j < changed.length; j++) {
|
|
var cell = rep.itemAt(changed[j][0]);
|
|
if (cell) {
|
|
cell.flipTo(changed[j][1], j * step);
|
|
}
|
|
}
|
|
chars = nc;
|
|
}
|
|
|
|
Repeater {
|
|
id: rep
|
|
model: root.chars.length
|
|
|
|
delegate: Rectangle {
|
|
id: cell
|
|
|
|
width: root.cellWidth
|
|
height: root.height
|
|
radius: 2
|
|
color: "#0b0b0d"
|
|
border.color: "#26262b"
|
|
border.width: 1
|
|
|
|
// plain property (not a binding) so the flip animation
|
|
// controls when the character actually changes
|
|
property string ch: ""
|
|
property string pendingCh: ""
|
|
|
|
Component.onCompleted: {
|
|
ch = root.chars[index] !== undefined ? root.chars[index] : "";
|
|
}
|
|
|
|
function flipTo(newCh, delay) {
|
|
if (newCh === ch) {
|
|
return;
|
|
}
|
|
pendingCh = newCh;
|
|
delayTimer.interval = Math.max(0, delay);
|
|
delayTimer.restart();
|
|
}
|
|
|
|
Timer {
|
|
id: delayTimer
|
|
interval: 0
|
|
onTriggered: flipAnim.start()
|
|
}
|
|
|
|
transform: Scale {
|
|
id: scaleTransform
|
|
origin.y: cell.height / 2
|
|
yScale: 1
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: flipAnim
|
|
NumberAnimation {
|
|
target: scaleTransform
|
|
property: "yScale"
|
|
to: 0.06
|
|
duration: 130
|
|
easing.type: Easing.InQuad
|
|
}
|
|
ScriptAction {
|
|
script: cell.ch = cell.pendingCh
|
|
}
|
|
NumberAnimation {
|
|
target: scaleTransform
|
|
property: "yScale"
|
|
to: 1
|
|
duration: 130
|
|
easing.type: Easing.OutQuad
|
|
}
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: cell.ch
|
|
color: "#f2f2f2"
|
|
font.family: "DejaVu Sans Mono"
|
|
font.pixelSize: root.cellFont
|
|
}
|
|
|
|
// the flap split line across the middle
|
|
Rectangle {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
y: parent.height / 2 - 1
|
|
width: parent.width
|
|
height: 2
|
|
color: "#000000"
|
|
opacity: 0.55
|
|
}
|
|
}
|
|
}
|
|
|
|
// euro sign in the plain-text font, white
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "\u20AC"
|
|
visible: root.digitsOnly.length > 0
|
|
color: "#ffffff"
|
|
font.family: Qt.application.font.family
|
|
font.pixelSize: root.height * 0.66
|
|
}
|
|
|
|
// trend LED (green/red/grey)
|
|
Rectangle {
|
|
width: root.height * 0.14
|
|
height: width
|
|
radius: width / 2
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: root.trendColor
|
|
border.color: "#26262b"
|
|
border.width: 1
|
|
}
|
|
}
|