diff --git a/.gitignore b/.gitignore index 6ca9d39a16..2151356b4a 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ share/qt/Info.plist src/qt/*.moc src/qt/moc_*.cpp +src/qml/components/moc_*.cpp src/qt/forms/ui_*.h src/qt/test/moc*.cpp diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 749ab2c614..f08aa2d6df 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -35,7 +35,9 @@ QT_FORMS_UI = \ qt/forms/transactiondescdialog.ui QT_MOC_CPP = \ + qml/components/moc_blockclockdial.cpp \ qml/moc_appmode.cpp \ + qml/moc_chainmodel.cpp \ qml/moc_nodemodel.cpp \ qml/moc_options_model.cpp \ qt/moc_addressbookpage.cpp \ @@ -111,6 +113,8 @@ QT_QRC_LOCALE = qt/bitcoin_locale.qrc BITCOIN_QT_H = \ qml/appmode.h \ qml/bitcoin.h \ + qml/chainmodel.h \ + qml/components/blockclockdial.h \ qml/imageprovider.h \ qml/nodemodel.h \ qml/options_model.h \ @@ -291,6 +295,8 @@ BITCOIN_QT_WALLET_CPP = \ BITCOIN_QML_BASE_CPP = \ qml/bitcoin.cpp \ + qml/chainmodel.cpp \ + qml/components/blockclockdial.cpp \ qml/imageprovider.cpp \ qml/nodemodel.cpp \ qml/options_model.cpp \ @@ -322,6 +328,7 @@ QML_QRC_CPP = qml/qrc_bitcoin.cpp QML_QRC = qml/bitcoin_qml.qrc QML_RES_QML = \ qml/components/AboutOptions.qml \ + qml/components/BlockClock.qml \ qml/components/BlockCounter.qml \ qml/components/ConnectionOptions.qml \ qml/components/ConnectionSettings.qml \ diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index a3fa753a98..a98a6cd786 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -128,6 +128,9 @@ class Chain //! Get block hash. Height must be valid or this function will abort. virtual uint256 getBlockHash(int height) = 0; + //! Get block time, Height must be valid or this function will abort. + virtual int64_t getBlockTime(int height) = 0; + //! Check that the block is available on disk (i.e. has not been //! pruned), and contains transactions. virtual bool haveBlockOnDisk(int height) = 0; diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index 4f3dc99bbf..ed265cef9d 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -514,6 +514,11 @@ class ChainImpl : public Chain LOCK(::cs_main); return Assert(chainman().ActiveChain()[height])->GetBlockHash(); } + int64_t getBlockTime(int height) override + { + LOCK(::cs_main); + return Assert(chainman().ActiveChain()[height])->GetBlockTime(); + } bool haveBlockOnDisk(int height) override { LOCK(::cs_main); diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp index 5eccb1a43a..4dbd0501b1 100644 --- a/src/qml/bitcoin.cpp +++ b/src/qml/bitcoin.cpp @@ -6,12 +6,15 @@ #include #include +#include #include #include #include #include #include #include +#include +#include #include #include #include @@ -155,6 +158,7 @@ int QmlGuiMain(int argc, char* argv[]) GUIUtil::LogQtInfo(); std::unique_ptr node = init->makeNode(); + std::unique_ptr chain = init->makeChain(); if (!node->baseInitialize()) { // A dialog with detailed error will have been shown by InitError(). return EXIT_FAILURE; @@ -170,6 +174,11 @@ int QmlGuiMain(int argc, char* argv[]) QObject::connect(&init_executor, &InitExecutor::shutdownResult, qGuiApp, &QGuiApplication::quit, Qt::QueuedConnection); // QObject::connect(&init_executor, &InitExecutor::runawayException, &node_model, &NodeModel::handleRunawayException); + ChainModel chain_model{*chain}; + + QObject::connect(&node_model, &NodeModel::setTimeRatioList, &chain_model, &ChainModel::setTimeRatioList); + QObject::connect(&node_model, &NodeModel::setTimeRatioListInitial, &chain_model, &ChainModel::setTimeRatioListInitial); + qGuiApp->setQuitOnLastWindowClosed(false); QObject::connect(qGuiApp, &QGuiApplication::lastWindowClosed, [&] { node->startShutdown(); @@ -185,6 +194,7 @@ int QmlGuiMain(int argc, char* argv[]) engine.addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()}); engine.rootContext()->setContextProperty("nodeModel", &node_model); + engine.rootContext()->setContextProperty("chainModel", &chain_model); OptionsQmlModel options_model{*node}; engine.rootContext()->setContextProperty("optionsModel", &options_model); @@ -196,6 +206,7 @@ int QmlGuiMain(int argc, char* argv[]) #endif // __ANDROID__ qmlRegisterSingletonInstance("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode); + qmlRegisterType("org.bitcoincore.qt", 1, 0, "BlockClockDial"); engine.load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml"))); if (engine.rootObjects().isEmpty()) { diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index d1d1a84b1f..3b0b990284 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -1,6 +1,7 @@ components/AboutOptions.qml + components/BlockClock.qml components/BlockCounter.qml components/ConnectionOptions.qml components/ConnectionSettings.qml diff --git a/src/qml/chainmodel.cpp b/src/qml/chainmodel.cpp new file mode 100644 index 0000000000..80a237b794 --- /dev/null +++ b/src/qml/chainmodel.cpp @@ -0,0 +1,91 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include + +ChainModel::ChainModel(interfaces::Chain& chain) + : m_chain{chain} +{ + QTimer* timer = new QTimer(); + connect(timer, &QTimer::timeout, this, &ChainModel::setCurrentTimeRatio); + timer->start(1000); + + QThread* timer_thread = new QThread; + timer->moveToThread(timer_thread); + timer_thread->start(); +} + +void ChainModel::setTimeRatioList(int new_time) +{ + if (m_time_ratio_list.isEmpty()) { + setTimeRatioListInitial(); + } + int time_at_meridian = timestampAtMeridian(); + + if (new_time < time_at_meridian) { + return; + } + m_time_ratio_list.push_back(double(new_time - time_at_meridian) / SECS_IN_12_HOURS); + + Q_EMIT timeRatioListChanged(); +} + +int ChainModel::timestampAtMeridian() +{ + int secs_since_meridian = (QTime::currentTime().msecsSinceStartOfDay() / 1000) % SECS_IN_12_HOURS; + int current_timestamp = QDateTime::currentSecsSinceEpoch(); + + return current_timestamp - secs_since_meridian; +} + +void ChainModel::setTimeRatioListInitial() +{ + int time_at_meridian = timestampAtMeridian(); + m_time_ratio_list.clear(); + /* m_time_ratio_list[0] = current_time_ratio + * m_time_ratio_list[1] = 0 + * These two positions remain fixed for these + * values in m_time_ratio_list */ + m_time_ratio_list.push_back(double(QDateTime::currentSecsSinceEpoch() - time_at_meridian) / SECS_IN_12_HOURS); + m_time_ratio_list.push_back(0); + + int first_block_height; + int active_chain_height = m_chain.getHeight().value(); + bool success = m_chain.findFirstBlockWithTimeAndHeight(/*min_time=*/time_at_meridian, /*min_height=*/0, interfaces::FoundBlock().height(first_block_height)); + + if (!success) { + Q_EMIT timeRatioListChanged(); + return; + } + + for (int height = first_block_height; height < active_chain_height + 1; height++) { + m_time_ratio_list.push_back(double(m_chain.getBlockTime(height) - time_at_meridian) / SECS_IN_12_HOURS); + } + + Q_EMIT timeRatioListChanged(); +} + +void ChainModel::setCurrentTimeRatio() +{ + int secs_since_meridian = (QTime::currentTime().msecsSinceStartOfDay() / 1000) % SECS_IN_12_HOURS; + double current_time_ratio = double(secs_since_meridian) / SECS_IN_12_HOURS; + + if (current_time_ratio < m_time_ratio_list[0].toDouble()) { // That means time has crossed a meridian + m_time_ratio_list.clear(); + } + + if (m_time_ratio_list.isEmpty()) { + m_time_ratio_list.push_back(current_time_ratio); + m_time_ratio_list.push_back(0); + } else { + m_time_ratio_list[0] = current_time_ratio; + } + + Q_EMIT timeRatioListChanged(); +} diff --git a/src/qml/chainmodel.h b/src/qml/chainmodel.h new file mode 100644 index 0000000000..e8919acea1 --- /dev/null +++ b/src/qml/chainmodel.h @@ -0,0 +1,54 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_CHAINMODEL_H +#define BITCOIN_QML_CHAINMODEL_H + +#include + +#include +#include +#include + +namespace interfaces { +class FoundBlock; +class Chain; +} // namespace interfaces + +static const int SECS_IN_12_HOURS = 43200; + +class ChainModel : public QObject +{ + Q_OBJECT + Q_PROPERTY(QVariantList timeRatioList READ timeRatioList NOTIFY timeRatioListChanged) + +public: + explicit ChainModel(interfaces::Chain& chain); + + QVariantList timeRatioList() const { return m_time_ratio_list; }; + + int timestampAtMeridian(); + + void setCurrentTimeRatio(); + +public Q_SLOTS: + void setTimeRatioList(int new_time); + void setTimeRatioListInitial(); + +Q_SIGNALS: + void timeRatioListChanged(); + +private: + /* time_ratio: Ratio between the time at which an event + * happened and 12 hours. So, for example, if a block is + * found at 4 am or pm, the time_ratio would be 0.3. + * The m_time_ratio_list stores the time ratio value for + * the current_time and the time at which the blocks in + * the last 12 hours were mined. */ + QVariantList m_time_ratio_list{0.0}; + + interfaces::Chain& m_chain; +}; + +#endif // BITCOIN_QML_CHAINMODEL_H diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml new file mode 100644 index 0000000000..cb7bae72e4 --- /dev/null +++ b/src/qml/components/BlockClock.qml @@ -0,0 +1,149 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.bitcoincore.qt 1.0 + +import "../controls" + +Item { + id: root + + Layout.alignment: Qt.AlignCenter + implicitWidth: 200 + implicitHeight: 200 + + property alias header: mainText.text + property alias headerSize: mainText.font.pixelSize + property alias subText: subText.text + property int headerSize: 32 + property bool synced: nodeModel.verificationProgress > 0.999 + property bool paused: false + property bool conns: true + + BlockClockDial { + id: dial + anchors.fill: parent + timeRatioList: chainModel.timeRatioList + verificationProgress: nodeModel.verificationProgress + paused: root.paused + synced: nodeModel.verificationProgress > 0.999 + backgroundColor: Theme.color.neutral2 + timeTickColor: Theme.color.neutral5 + } + + Button { + id: bitcoinIcon + background: null + icon.source: "image://images/bitcoin-circle" + icon.color: Theme.color.neutral9 + icon.width: 40 + icon.height: 40 + anchors.bottom: mainText.top + anchors.horizontalCenter: root.horizontalCenter + } + + Label { + id: mainText + anchors.centerIn: parent + font.family: "Inter" + font.styleName: "Semi Bold" + font.pixelSize: 32 + color: Theme.color.neutral9 + } + + Label { + id: subText + anchors.top: mainText.bottom + anchors.horizontalCenter: root.horizontalCenter + font.family: "Inter" + font.styleName: "Semi Bold" + font.pixelSize: 18 + color: Theme.color.neutral4 + } + + RowLayout { + id: peersIndicator + anchors.top: subText.bottom + anchors.topMargin: 20 + anchors.horizontalCenter: root.horizontalCenter + spacing: 5 + Repeater { + model: 5 + Rectangle { + width: 3 + height: width + radius: width/2 + color: Theme.color.neutral9 + } + } + } + + MouseArea { + anchors.fill: dial + onClicked: { + root.paused = !root.paused + nodeModel.pause = root.paused + } + } + + states: [ + State { + name: "intialBlockDownload"; when: !synced && !paused && conns + PropertyChanges { + target: root + header: Math.round(nodeModel.verificationProgress * 100) + "%" + subText: Math.round(nodeModel.remainingSyncTime/60000) > 0 ? Math.round(nodeModel.remainingSyncTime/60000) + "mins" : Math.round(nodeModel.remainingSyncTime/1000) + "secs" + } + }, + + State { + name: "blockClock"; when: synced && !paused && conns + PropertyChanges { + target: root + header: Number(nodeModel.blockTipHeight).toLocaleString(Qt.locale(), 'f', 0) + subText: "Blocktime" + } + }, + + State { + name: "Manual Pause"; when: paused + PropertyChanges { + target: root + header: "Paused" + headerSize: 24 + subText: "Tap to resume" + } + PropertyChanges { + target: bitcoinIcon + anchors.bottomMargin: 5 + } + PropertyChanges { + target: subText + anchors.topMargin: 4 + } + }, + + State { + name: "Connecting"; when: !paused && !conns + PropertyChanges { + target: root + header: "Connecting" + headerSize: 24 + subText: "Please Wait" + } + PropertyChanges { + target: bitcoinIcon + anchors.bottomMargin: 5 + } + PropertyChanges { + target: subText + anchors.topMargin: 4 + } + } + ] +} diff --git a/src/qml/components/blockclockdial.cpp b/src/qml/components/blockclockdial.cpp new file mode 100644 index 0000000000..1da60edb2c --- /dev/null +++ b/src/qml/components/blockclockdial.cpp @@ -0,0 +1,196 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include + +BlockClockDial::BlockClockDial(QQuickItem *parent) +: QQuickPaintedItem(parent) +, m_time_ratio_list{0.0} +, m_background_color{QColor("#2D2D2D")} +, m_time_tick_color{QColor("#000000")} +{ +} + +void BlockClockDial::setTimeRatioList(QVariantList new_list) +{ + m_time_ratio_list = new_list; + update(); +} + +void BlockClockDial::setVerificationProgress(double progress) +{ + m_verification_progress = progress; + update(); +} + +void BlockClockDial::setSynced(bool synced) +{ + m_is_synced = synced; + update(); +} + +void BlockClockDial::setPaused(bool paused) +{ + m_is_paused = paused; + update(); +} + +void BlockClockDial::setBackgroundColor(QColor color) +{ + m_background_color = color; + update(); +} + +void BlockClockDial::setTimeTickColor(QColor color) +{ + m_time_tick_color = color; + update(); +} + +QRectF BlockClockDial::getBoundsForPen(const QPen & pen) +{ + const QRectF bounds = boundingRect(); + const qreal smallest = qMin(bounds.width(), bounds.height()); + QRectF rect = QRectF(pen.widthF() / 2.0 + 1, pen.widthF() / 2.0 + 1, smallest - pen.widthF() - 2, smallest - pen.widthF() - 2); + rect.moveCenter(bounds.center()); + + // Make sure the arc is aligned to whole pixels. + if (rect.x() - int(rect.x()) > 0) + rect.setX(qCeil(rect.x())); + if (rect.y() - int(rect.y()) > 0) + rect.setY(qCeil(rect.y())); + if (rect.width() - int(rect.width()) > 0) + rect.setWidth(qFloor(rect.width())); + if (rect.height() - int(rect.height()) > 0) + rect.setHeight(qFloor(rect.height())); + + return rect; +} + +void BlockClockDial::paintBlocks(QPainter * painter) +{ + int numberOfBlocks = m_time_ratio_list.length(); + if (numberOfBlocks < 2) { + return; + } + + QPen pen(QColor("#F1D54A")); + pen.setWidth(4); + pen.setCapStyle(Qt::FlatCap); + const QRectF bounds = getBoundsForPen(pen); + painter->setPen(pen); + + QColor confirmationColors[] = { + QColor("#FF1C1C"), // red + QColor("#ED6E46"), + QColor("#EE8847"), + QColor("#EFA148"), + QColor("#F0BB49"), + QColor("#F1D54A"), // yellow + }; + + // The gap is calculated here and is used to create a + // one pixel spacing between each block + double gap = degreesPerPixel(); + + // Paint blocks + for (int i = 1; i < numberOfBlocks; i++) { + if (numberOfBlocks - i <= 6) { + QPen pen(confirmationColors[numberOfBlocks - i - 1]); + pen.setWidth(4); + pen.setCapStyle(Qt::FlatCap); + painter->setPen(pen); + } + + const qreal startAngle = 90 + (-360 * m_time_ratio_list[i].toDouble()); + qreal nextAngle; + if (i == numberOfBlocks - 1) { + nextAngle = 90 + (-360 * m_time_ratio_list[0].toDouble()); + } else { + nextAngle = 90 + (-360 * m_time_ratio_list[i+1].toDouble()); + } + const qreal spanAngle = -1 * (startAngle - nextAngle) + gap; + QPainterPath path; + path.arcMoveTo(bounds, startAngle); + path.arcTo(bounds, startAngle, spanAngle); + painter->drawPath(path); + } +} + +void BlockClockDial::paintProgress(QPainter * painter) +{ + QPen pen(QColor("#F1D54A")); + pen.setWidthF(4); + pen.setCapStyle(Qt::RoundCap); + const QRectF bounds = getBoundsForPen(pen); + painter->setPen(pen); + + // QPainter::drawArc uses positive values for counter clockwise - the opposite of our API - + // so we must reverse the angles with * -1. Also, our angle origin is at 12 o'clock, whereas + // QPainter's is 3 o'clock, hence - 90. + const qreal startAngle = 90; + const qreal spanAngle = verificationProgress() * -360; + + // QPainter::drawArc parameters are 1/16 of a degree + painter->drawArc(bounds, startAngle * 16, spanAngle * 16); +} + +void BlockClockDial::paintBackground(QPainter * painter) +{ + QPen pen(m_background_color); + pen.setWidthF(4); + const QRectF bounds = getBoundsForPen(pen); + painter->setPen(pen); + + painter->drawEllipse(bounds); +} + +double BlockClockDial::degreesPerPixel() +{ + double circumference = width() * 3.1415926; + return 360 / circumference; +} + +void BlockClockDial::paintTimeTicks(QPainter * painter) +{ + QPen pen(m_time_tick_color); + pen.setWidthF(4); + // Calculate bound based on width of default pen + const QRectF bounds = getBoundsForPen(pen); + + QPen time_tick_pen = QPen(m_time_tick_color); + time_tick_pen.setWidth(2); + time_tick_pen.setCapStyle(Qt::RoundCap); + painter->setPen(time_tick_pen); + for (double angle = 0; angle < 360; angle += 30) { + QPainterPath path; + path.arcMoveTo(bounds, angle); + path.arcTo(bounds, angle, degreesPerPixel()); + painter->drawPath(path); + } +} + +void BlockClockDial::paint(QPainter * painter) +{ + if (width() <= 0 || height() <= 0) { + return; + } + painter->setRenderHint(QPainter::Antialiasing); + + paintBackground(painter); + paintTimeTicks(painter); + + if (paused()) return; + + if (synced()) { + paintBlocks(painter); + } else { + paintProgress(painter); + } +} diff --git a/src/qml/components/blockclockdial.h b/src/qml/components/blockclockdial.h new file mode 100644 index 0000000000..70ab7082ef --- /dev/null +++ b/src/qml/components/blockclockdial.h @@ -0,0 +1,56 @@ +// Copyright (c) 2022 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_COMPONENTS_BLOCKCLOCKDIAL_H +#define BITCOIN_QML_COMPONENTS_BLOCKCLOCKDIAL_H + +#include +#include + +class BlockClockDial : public QQuickPaintedItem +{ + Q_OBJECT + Q_PROPERTY(QVariantList timeRatioList READ timeRatioList WRITE setTimeRatioList) + Q_PROPERTY(double verificationProgress READ verificationProgress WRITE setVerificationProgress) + Q_PROPERTY(bool synced READ synced WRITE setSynced) + Q_PROPERTY(bool paused READ paused WRITE setPaused) + Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) + Q_PROPERTY(QColor timeTickColor READ timeTickColor WRITE setTimeTickColor) + +public: + explicit BlockClockDial(QQuickItem * parent = nullptr); + void paint(QPainter * painter) override; + + QVariantList timeRatioList() const { return m_time_ratio_list; }; + double verificationProgress() const { return m_verification_progress; }; + bool synced() const { return m_is_synced; }; + bool paused() const { return m_is_paused; }; + QColor backgroundColor() const { return m_background_color; }; + QColor timeTickColor() const { return m_time_tick_color; }; + +public Q_SLOTS: + void setTimeRatioList(QVariantList new_time); + void setVerificationProgress(double progress); + void setSynced(bool synced); + void setPaused(bool paused); + void setBackgroundColor(QColor color); + void setTimeTickColor(QColor color); + +private: + void paintProgress(QPainter * painter); + void paintBlocks(QPainter * painter); + void paintBackground(QPainter * painter); + void paintTimeTicks(QPainter * painter); + QRectF getBoundsForPen(const QPen & pen); + double degreesPerPixel(); + + QVariantList m_time_ratio_list; + double m_verification_progress; + bool m_is_synced; + bool m_is_paused; + QColor m_background_color; + QColor m_time_tick_color; +}; + +#endif // BITCOIN_QML_COMPONENTS_BLOCKCLOCKDIAL_H diff --git a/src/qml/controls/Header.qml b/src/qml/controls/Header.qml index 84ef0e771f..7c6d3ea1c2 100644 --- a/src/qml/controls/Header.qml +++ b/src/qml/controls/Header.qml @@ -8,14 +8,16 @@ import QtQuick.Layouts 1.15 ColumnLayout { id: root - property bool bold: false property bool center: true required property string header property int headerMargin property int headerSize: 28 + property bool headerBold: false property string description: "" property int descriptionMargin: 10 property int descriptionSize: 18 + property string descriptionColor: Theme.color.neutral8 + property bool descriptionBold: false property string subtext: "" property int subtextMargin property int subtextSize: 15 @@ -26,7 +28,7 @@ ColumnLayout { Layout.fillWidth: true topPadding: root.headerMargin font.family: "Inter" - font.styleName: root.bold ? "Semi Bold" : "Regular" + font.styleName: root.headerBold ? "Semi Bold" : "Regular" font.pixelSize: root.headerSize color: Theme.color.neutral9 text: root.header @@ -40,9 +42,9 @@ ColumnLayout { sourceComponent: Label { topPadding: root.descriptionMargin font.family: "Inter" - font.styleName: "Regular" + font.styleName: root.descriptionBold ? "Semi Bold" : "Regular" font.pixelSize: root.descriptionSize - color: Theme.color.neutral8 + color: root.descriptionColor text: root.description horizontalAlignment: root.center ? Text.AlignHCenter : Text.AlignLeft wrapMode: wrap ? Text.WordWrap : Text.NoWrap diff --git a/src/qml/controls/InformationPage.qml b/src/qml/controls/InformationPage.qml index 6956998af1..64974e5263 100644 --- a/src/qml/controls/InformationPage.qml +++ b/src/qml/controls/InformationPage.qml @@ -65,7 +65,7 @@ Page { Layout.fillWidth: true Layout.leftMargin: 20 Layout.rightMargin: 20 - bold: root.bold + headerBold: root.bold center: root.center header: root.headerText headerMargin: root.headerMargin diff --git a/src/qml/nodemodel.cpp b/src/qml/nodemodel.cpp index 862597779d..42b6656b52 100644 --- a/src/qml/nodemodel.cpp +++ b/src/qml/nodemodel.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -27,14 +28,60 @@ void NodeModel::setBlockTipHeight(int new_height) } } +void NodeModel::setRemainingSyncTime(double new_progress) +{ + int currentTime = QDateTime::currentDateTime().toMSecsSinceEpoch(); + + // keep a vector of samples of verification progress at height + m_block_process_time.push_front(qMakePair(currentTime, new_progress)); + + // show progress speed if we have more than one sample + if (m_block_process_time.size() >= 2) { + double progressDelta = 0; + int timeDelta = 0; + int remainingMSecs = 0; + double remainingProgress = 1.0 - new_progress; + for (int i = 1; i < m_block_process_time.size(); i++) { + QPair sample = m_block_process_time[i]; + + // take first sample after 500 seconds or last available one + if (sample.first < (currentTime - 500 * 1000) || i == m_block_process_time.size() - 1) { + progressDelta = m_block_process_time[0].second - sample.second; + timeDelta = m_block_process_time[0].first - sample.first; + remainingMSecs = (progressDelta > 0) ? remainingProgress / progressDelta * timeDelta : -1; + break; + } + } + if (remainingMSecs > 0 && m_block_process_time.count() % 1000 == 0) { + m_remaining_sync_time = remainingMSecs; + + Q_EMIT remainingSyncTimeChanged(); + } + static const int MAX_SAMPLES = 5000; + if (m_block_process_time.count() > MAX_SAMPLES) { + m_block_process_time.remove(1, m_block_process_time.count() - 1); + } + } +} void NodeModel::setVerificationProgress(double new_progress) { if (new_progress != m_verification_progress) { + setRemainingSyncTime(new_progress); + m_verification_progress = new_progress; Q_EMIT verificationProgressChanged(); } } +void NodeModel::setPause(bool new_pause) +{ + if(m_pause != new_pause) { + m_pause = new_pause; + m_node.setNetworkActive(!new_pause); + Q_EMIT pauseChanged(new_pause); + } +} + void NodeModel::startNodeInitializionThread() { Q_EMIT requestedInitialize(); @@ -45,6 +92,8 @@ void NodeModel::initializeResult([[maybe_unused]] bool success, interfaces::Bloc // TODO: Handle the `success` parameter, setBlockTipHeight(tip_info.block_height); setVerificationProgress(tip_info.verification_progress); + + Q_EMIT setTimeRatioListInitial(); } void NodeModel::startShutdownPolling() @@ -75,6 +124,8 @@ void NodeModel::ConnectToBlockTipSignal() QMetaObject::invokeMethod(this, [=] { setBlockTipHeight(tip.block_height); setVerificationProgress(verification_progress); + + Q_EMIT setTimeRatioList(tip.block_time); }); }); } diff --git a/src/qml/nodemodel.h b/src/qml/nodemodel.h index c6a04a1f2c..2da453f152 100644 --- a/src/qml/nodemodel.h +++ b/src/qml/nodemodel.h @@ -25,15 +25,21 @@ class NodeModel : public QObject { Q_OBJECT Q_PROPERTY(int blockTipHeight READ blockTipHeight NOTIFY blockTipHeightChanged) + Q_PROPERTY(int remainingSyncTime READ remainingSyncTime NOTIFY remainingSyncTimeChanged) Q_PROPERTY(double verificationProgress READ verificationProgress NOTIFY verificationProgressChanged) + Q_PROPERTY(bool pause READ pause WRITE setPause NOTIFY pauseChanged) public: explicit NodeModel(interfaces::Node& node); int blockTipHeight() const { return m_block_tip_height; } void setBlockTipHeight(int new_height); + int remainingSyncTime() const { return m_remaining_sync_time; } + void setRemainingSyncTime(double new_progress); double verificationProgress() const { return m_verification_progress; } void setVerificationProgress(double new_progress); + bool pause() const { return m_pause; } + void setPause(bool new_pause); Q_INVOKABLE void startNodeInitializionThread(); @@ -45,9 +51,14 @@ public Q_SLOTS: Q_SIGNALS: void blockTipHeightChanged(); + void remainingSyncTimeChanged(); void requestedInitialize(); void requestedShutdown(); void verificationProgressChanged(); + void pauseChanged(bool new_pause); + + void setTimeRatioList(int new_time); + void setTimeRatioListInitial(); protected: void timerEvent(QTimerEvent* event) override; @@ -55,10 +66,14 @@ public Q_SLOTS: private: // Properties that are exposed to QML. int m_block_tip_height{0}; + int m_remaining_sync_time{0}; double m_verification_progress{0.0}; + bool m_pause{false}; int m_shutdown_polling_timer_id{0}; + QVector> m_block_process_time; + interfaces::Node& m_node; std::unique_ptr m_handler_notify_block_tip; diff --git a/src/qml/pages/main.qml b/src/qml/pages/main.qml index 4f753c4828..b624d0f831 100644 --- a/src/qml/pages/main.qml +++ b/src/qml/pages/main.qml @@ -5,6 +5,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 import "../components" import "../controls" import "./onboarding" @@ -57,7 +58,7 @@ ApplicationWindow { } NodeSettings { navMiddleDetail: Header { - bold: true + headerBold: true headerSize: 18 header: "Settings" } diff --git a/src/qml/pages/node/NodeRunner.qml b/src/qml/pages/node/NodeRunner.qml index 873ee9334b..c4ed4e3a10 100644 --- a/src/qml/pages/node/NodeRunner.qml +++ b/src/qml/pages/node/NodeRunner.qml @@ -15,25 +15,10 @@ Page { header: NavigationBar { id: navbar } - ColumnLayout { - width: 600 - spacing: 0 + + Component.onCompleted: nodeModel.startNodeInitializionThread(); + + BlockClock { anchors.centerIn: parent - Component.onCompleted: nodeModel.startNodeInitializionThread(); - Image { - Layout.alignment: Qt.AlignCenter - source: "image://images/app" - sourceSize.width: 64 - sourceSize.height: 64 - } - BlockCounter { - Layout.alignment: Qt.AlignCenter - blockHeight: nodeModel.blockTipHeight - } - ProgressIndicator { - width: 200 - Layout.alignment: Qt.AlignCenter - progress: nodeModel.verificationProgress - } } }