Skip to content

Commit 66c7fac

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 <shaavan.github@gmail.com>
1 parent 0da49e0 commit 66c7fac

File tree

7 files changed

+413
-19
lines changed

7 files changed

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

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)