216 lines
6.8 KiB
QML
216 lines
6.8 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, followed by a trend
|
|
* arrow (green up / red down; unchanged keeps the last arrow).
|
|
* On a price change the board first resets every flap to "0" (swept
|
|
* over ~2 s), then takes the new values (swept over the remainder of
|
|
* the cascade setting).
|
|
* 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: "--"
|
|
// 1 = up, -1 = down, 0 = unchanged -> keep showing the last arrow
|
|
property int trendDir: 0
|
|
property int lastDir: 0
|
|
onTrendDirChanged: {
|
|
if (trendDir !== 0) {
|
|
lastDir = trendDir;
|
|
}
|
|
}
|
|
// total seconds over which the reset-to-zero + new value cycle spreads
|
|
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/arrow 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);
|
|
|
|
// Phase 1: every flap resets to "0", swept over ~2 s.
|
|
// Phase 2: every flap takes its new value, swept over the
|
|
// remainder of the total cascade time.
|
|
var total = Math.max(1, flapSeconds) * 1000;
|
|
var resetSpan = Math.min(2000, Math.floor(total * 2 / 3));
|
|
var valueStart = resetSpan;
|
|
var resetStep = n > 1 ? Math.floor(Math.max(0, resetSpan - flapDuration) / (n - 1)) : 0;
|
|
var valueStep = n > 1 ? Math.floor(Math.max(0, total - valueStart - flapDuration) / (n - 1)) : 0;
|
|
|
|
for (var i = 0; i < n; i++) {
|
|
var cell = rep.itemAt(i);
|
|
var to = i < nc.length ? nc[i] : "";
|
|
if (cell) {
|
|
cell.schedule(to, i * resetStep, valueStart + i * valueStep);
|
|
}
|
|
}
|
|
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 properties (not bindings) so the animations control
|
|
// when the character actually changes
|
|
property string ch: ""
|
|
property string targetCh: ""
|
|
property string animCh: ""
|
|
|
|
Component.onCompleted: {
|
|
ch = root.chars[index] !== undefined ? root.chars[index] : "";
|
|
}
|
|
|
|
// schedule the two-phase cycle: reset to "0", then target
|
|
function schedule(t, resetDelay, valueDelay) {
|
|
targetCh = t;
|
|
resetTimer.interval = Math.max(0, resetDelay);
|
|
resetTimer.restart();
|
|
valueTimer.interval = Math.max(0, valueDelay);
|
|
valueTimer.restart();
|
|
}
|
|
|
|
function startFlip(newCh) {
|
|
if (newCh === ch) {
|
|
return;
|
|
}
|
|
animCh = newCh;
|
|
flipAnim.restart();
|
|
}
|
|
|
|
Timer {
|
|
id: resetTimer
|
|
interval: 0
|
|
onTriggered: cell.startFlip("0")
|
|
}
|
|
|
|
Timer {
|
|
id: valueTimer
|
|
interval: 0
|
|
onTriggered: cell.startFlip(cell.targetCh)
|
|
}
|
|
|
|
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.animCh
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// breathing room between the board and the currency sign
|
|
Item {
|
|
width: root.height * 0.22
|
|
height: 1
|
|
}
|
|
|
|
// 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.85
|
|
}
|
|
|
|
// trend arrow: green up / red down; unchanged keeps the last arrow
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.lastDir > 0 ? "\u25B2" : "\u25BC"
|
|
visible: root.digitsOnly.length > 0 && root.lastDir !== 0
|
|
color: root.lastDir > 0 ? "#2ecc71" : "#e74c3c"
|
|
font.family: Qt.application.font.family
|
|
font.pixelSize: root.height * 0.5
|
|
}
|
|
}
|