Skip to content

Commit 48f3fb7

Browse files
johnny9shaavan
authored andcommitted
qml: introduce the BlockClock
Implements a few of the BlockClock dial states. Sync progress while downloading, rendering blocks after sync, and pausing/unpausing the node. An additional Model is added, ChainModel, to provide the dial block information. Changes to NodeModel are also made to support the pausing/unpausing Dial feature. Github-Pull: #220 Rebased-From: 6701ad2 Co-authored-by: shaavan <[email protected]>
1 parent 0da49e0 commit 48f3fb7

File tree

7 files changed

+414
-19
lines changed

7 files changed

+414
-19
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) 2023 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
import QtQuick
6+
import QtQuick.Controls
7+
import QtQuick.Layouts
8+
import BitcoinApp.Controls
9+
import org.bitcoincore.qt
10+
11+
Item {
12+
id: root
13+
14+
Layout.alignment: Qt.AlignCenter
15+
implicitWidth: 200
16+
implicitHeight: 200
17+
18+
property alias header: mainText.text
19+
// property alias headerSize: mainText.font.pixelSize
20+
property alias subText: subText.text
21+
property int headerSize: 32
22+
property bool synced: nodeModel.verificationProgress > 0.999
23+
property bool paused: false
24+
property bool conns: true
25+
26+
BlockClockDial {
27+
id: dial
28+
anchors.fill: parent
29+
timeRatioList: chainModel.timeRatioList
30+
verificationProgress: nodeModel.verificationProgress
31+
paused: root.paused
32+
synced: nodeModel.verificationProgress > 0.999
33+
backgroundColor: Theme.color.neutral2
34+
timeTickColor: Theme.color.neutral5
35+
}
36+
37+
Button {
38+
id: bitcoinIcon
39+
background: null
40+
icon.source: "image://images/bitcoin-circle"
41+
icon.color: Theme.color.neutral9
42+
icon.width: 40
43+
icon.height: 40
44+
anchors.bottom: mainText.top
45+
anchors.horizontalCenter: root.horizontalCenter
46+
}
47+
48+
Label {
49+
id: mainText
50+
anchors.centerIn: parent
51+
font.family: "Inter"
52+
font.styleName: "Semi Bold"
53+
font.pixelSize: 32
54+
color: Theme.color.neutral9
55+
}
56+
57+
Label {
58+
id: subText
59+
anchors.top: mainText.bottom
60+
anchors.horizontalCenter: root.horizontalCenter
61+
font.family: "Inter"
62+
font.styleName: "Semi Bold"
63+
font.pixelSize: 18
64+
color: Theme.color.neutral4
65+
}
66+
67+
RowLayout {
68+
id: peersIndicator
69+
anchors.top: subText.bottom
70+
anchors.topMargin: 20
71+
anchors.horizontalCenter: root.horizontalCenter
72+
spacing: 5
73+
Repeater {
74+
model: 5
75+
Rectangle {
76+
width: 3
77+
height: width
78+
radius: width/2
79+
color: Theme.color.neutral9
80+
}
81+
}
82+
}
83+
84+
MouseArea {
85+
anchors.fill: dial
86+
onClicked: {
87+
root.paused = !root.paused
88+
nodeModel.pause = root.paused
89+
}
90+
}
91+
92+
states: [
93+
State {
94+
name: "intialBlockDownload"; when: !synced && !paused && conns
95+
PropertyChanges {
96+
target: root
97+
header: Math.round(nodeModel.verificationProgress * 100) + "%"
98+
subText: Math.round(nodeModel.remainingSyncTime/60000) > 0 ? Math.round(nodeModel.remainingSyncTime/60000) + "mins" : Math.round(nodeModel.remainingSyncTime/1000) + "secs"
99+
}
100+
},
101+
102+
State {
103+
name: "blockClock"; when: synced && !paused && conns
104+
PropertyChanges {
105+
target: root
106+
header: Number(nodeModel.blockTipHeight).toLocaleString(Qt.locale(), 'f', 0)
107+
subText: "Blocktime"
108+
}
109+
},
110+
111+
State {
112+
name: "Manual Pause"; when: paused
113+
PropertyChanges {
114+
target: root
115+
header: "Paused"
116+
headerSize: 24
117+
subText: "Tap to resume"
118+
}
119+
PropertyChanges {
120+
target: bitcoinIcon
121+
anchors.bottomMargin: 5
122+
}
123+
PropertyChanges {
124+
target: subText
125+
anchors.topMargin: 4
126+
}
127+
},
128+
129+
State {
130+
name: "Connecting"; when: !paused && !conns
131+
PropertyChanges {
132+
target: root
133+
header: "Connecting"
134+
headerSize: 24
135+
subText: "Please Wait"
136+
}
137+
PropertyChanges {
138+
target: bitcoinIcon
139+
anchors.bottomMargin: 5
140+
}
141+
PropertyChanges {
142+
target: subText
143+
anchors.topMargin: 4
144+
}
145+
}
146+
]
147+
}

src/qml/BitcoinApp/Components/CMakeLists.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ qt6_add_qml_module(bitcoin_qml_components
66
URI BitcoinApp.Components
77
VERSION 1.0
88
STATIC
9+
SOURCES
10+
blockclockdial.cpp
911
RESOURCE_PREFIX /qt/qml/
1012
QML_FILES
1113
AboutOptions.qml
14+
BlockClock.qml
1215
BlockCounter.qml
1316
ConnectionOptions.qml
1417
ConnectionSettings.qml
@@ -17,3 +20,8 @@ qt6_add_qml_module(bitcoin_qml_components
1720
StorageOptions.qml
1821
StorageSettings.qml
1922
)
23+
24+
target_link_libraries(bitcoin_qml_components
25+
PRIVATE
26+
Qt6::Quick
27+
)
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// Copyright (c) 2023 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/BitcoinApp/Components/blockclockdial.h>
6+
7+
#include <QColor>
8+
#include <QPainterPath>
9+
#include <QPen>
10+
#include <QtMath>
11+
12+
BlockClockDial::BlockClockDial(QQuickItem *parent)
13+
: QQuickPaintedItem(parent)
14+
, m_time_ratio_list{0.0}
15+
, m_background_color{QColor("#2D2D2D")}
16+
, m_time_tick_color{QColor("#000000")}
17+
{
18+
}
19+
20+
void BlockClockDial::setTimeRatioList(QVariantList new_list)
21+
{
22+
m_time_ratio_list = new_list;
23+
update();
24+
}
25+
26+
void BlockClockDial::setVerificationProgress(double progress)
27+
{
28+
m_verification_progress = progress;
29+
update();
30+
}
31+
32+
void BlockClockDial::setSynced(bool synced)
33+
{
34+
m_is_synced = synced;
35+
update();
36+
}
37+
38+
void BlockClockDial::setPaused(bool paused)
39+
{
40+
m_is_paused = paused;
41+
update();
42+
}
43+
44+
void BlockClockDial::setBackgroundColor(QColor color)
45+
{
46+
m_background_color = color;
47+
update();
48+
}
49+
50+
void BlockClockDial::setTimeTickColor(QColor color)
51+
{
52+
m_time_tick_color = color;
53+
update();
54+
}
55+
56+
QRectF BlockClockDial::getBoundsForPen(const QPen & pen)
57+
{
58+
const QRectF bounds = boundingRect();
59+
const qreal smallest = qMin(bounds.width(), bounds.height());
60+
QRectF rect = QRectF(pen.widthF() / 2.0 + 1, pen.widthF() / 2.0 + 1, smallest - pen.widthF() - 2, smallest - pen.widthF() - 2);
61+
rect.moveCenter(bounds.center());
62+
63+
// Make sure the arc is aligned to whole pixels.
64+
if (rect.x() - int(rect.x()) > 0)
65+
rect.setX(qCeil(rect.x()));
66+
if (rect.y() - int(rect.y()) > 0)
67+
rect.setY(qCeil(rect.y()));
68+
if (rect.width() - int(rect.width()) > 0)
69+
rect.setWidth(qFloor(rect.width()));
70+
if (rect.height() - int(rect.height()) > 0)
71+
rect.setHeight(qFloor(rect.height()));
72+
73+
return rect;
74+
}
75+
76+
void BlockClockDial::paintBlocks(QPainter * painter)
77+
{
78+
int numberOfBlocks = m_time_ratio_list.length();
79+
if (numberOfBlocks < 2) {
80+
return;
81+
}
82+
83+
QPen pen(QColor("#F1D54A"));
84+
pen.setWidth(4);
85+
pen.setCapStyle(Qt::FlatCap);
86+
const QRectF bounds = getBoundsForPen(pen);
87+
painter->setPen(pen);
88+
89+
QColor confirmationColors[] = {
90+
QColor("#FF1C1C"), // red
91+
QColor("#ED6E46"),
92+
QColor("#EE8847"),
93+
QColor("#EFA148"),
94+
QColor("#F0BB49"),
95+
QColor("#F1D54A"), // yellow
96+
};
97+
98+
// The gap is calculated here and is used to create a
99+
// one pixel spacing between each block
100+
double gap = degreesPerPixel();
101+
102+
// Paint blocks
103+
for (int i = 1; i < numberOfBlocks; i++) {
104+
if (numberOfBlocks - i <= 6) {
105+
QPen pen(confirmationColors[numberOfBlocks - i - 1]);
106+
pen.setWidth(4);
107+
pen.setCapStyle(Qt::FlatCap);
108+
painter->setPen(pen);
109+
}
110+
111+
const qreal startAngle = 90 + (-360 * m_time_ratio_list[i].toDouble());
112+
qreal nextAngle;
113+
if (i == numberOfBlocks - 1) {
114+
nextAngle = 90 + (-360 * m_time_ratio_list[0].toDouble());
115+
} else {
116+
nextAngle = 90 + (-360 * m_time_ratio_list[i+1].toDouble());
117+
}
118+
const qreal spanAngle = -1 * (startAngle - nextAngle) + gap;
119+
QPainterPath path;
120+
path.arcMoveTo(bounds, startAngle);
121+
path.arcTo(bounds, startAngle, spanAngle);
122+
painter->drawPath(path);
123+
}
124+
}
125+
126+
void BlockClockDial::paintProgress(QPainter * painter)
127+
{
128+
QPen pen(QColor("#F1D54A"));
129+
pen.setWidthF(4);
130+
pen.setCapStyle(Qt::RoundCap);
131+
const QRectF bounds = getBoundsForPen(pen);
132+
painter->setPen(pen);
133+
134+
// QPainter::drawArc uses positive values for counter clockwise - the opposite of our API -
135+
// so we must reverse the angles with * -1. Also, our angle origin is at 12 o'clock, whereas
136+
// QPainter's is 3 o'clock, hence - 90.
137+
const qreal startAngle = 90;
138+
const qreal spanAngle = verificationProgress() * -360;
139+
140+
// QPainter::drawArc parameters are 1/16 of a degree
141+
painter->drawArc(bounds, startAngle * 16, spanAngle * 16);
142+
}
143+
144+
void BlockClockDial::paintBackground(QPainter * painter)
145+
{
146+
QPen pen(m_background_color);
147+
pen.setWidthF(4);
148+
const QRectF bounds = getBoundsForPen(pen);
149+
painter->setPen(pen);
150+
151+
painter->drawEllipse(bounds);
152+
}
153+
154+
double BlockClockDial::degreesPerPixel()
155+
{
156+
double circumference = width() * 3.1415926;
157+
return 360 / circumference;
158+
}
159+
160+
void BlockClockDial::paintTimeTicks(QPainter * painter)
161+
{
162+
QPen pen(m_time_tick_color);
163+
pen.setWidthF(4);
164+
// Calculate bound based on width of default pen
165+
const QRectF bounds = getBoundsForPen(pen);
166+
167+
QPen time_tick_pen = QPen(m_time_tick_color);
168+
time_tick_pen.setWidth(2);
169+
time_tick_pen.setCapStyle(Qt::RoundCap);
170+
painter->setPen(time_tick_pen);
171+
for (double angle = 0; angle < 360; angle += 30) {
172+
QPainterPath path;
173+
path.arcMoveTo(bounds, angle);
174+
path.arcTo(bounds, angle, degreesPerPixel());
175+
painter->drawPath(path);
176+
}
177+
}
178+
179+
void BlockClockDial::paint(QPainter * painter)
180+
{
181+
if (width() <= 0 || height() <= 0) {
182+
return;
183+
}
184+
painter->setRenderHint(QPainter::Antialiasing);
185+
186+
paintBackground(painter);
187+
paintTimeTicks(painter);
188+
189+
if (paused()) return;
190+
191+
if (synced()) {
192+
paintBlocks(painter);
193+
} else {
194+
paintProgress(painter);
195+
}
196+
}

0 commit comments

Comments
 (0)