From cea757da2f73f93770b3bcde76f6076ca0a8b5f5 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Mon, 13 Mar 2023 23:36:27 -0400 Subject: [PATCH 01/19] qml: don't use hover effects when on mobile --- src/qml/controls/ContinueButton.qml | 3 ++- src/qml/controls/NavButton.qml | 6 ++++-- src/qml/controls/OutlineButton.qml | 3 ++- src/qml/controls/Setting.qml | 6 ++++-- src/qml/controls/TextButton.qml | 4 +++- src/qml/controls/ToggleButton.qml | 3 ++- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/qml/controls/ContinueButton.qml b/src/qml/controls/ContinueButton.qml index 43323f073a..dd1fa76bab 100644 --- a/src/qml/controls/ContinueButton.qml +++ b/src/qml/controls/ContinueButton.qml @@ -4,10 +4,11 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import org.bitcoincore.qt 1.0 Button { id: root - hoverEnabled: true + hoverEnabled: AppMode.isDesktop contentItem: CoreText { text: parent.text bold: true diff --git a/src/qml/controls/NavButton.qml b/src/qml/controls/NavButton.qml index d9741bdd8e..7576ab199c 100644 --- a/src/qml/controls/NavButton.qml +++ b/src/qml/controls/NavButton.qml @@ -6,6 +6,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 + AbstractButton { id: root property int iconHeight: 30 @@ -14,7 +16,7 @@ AbstractButton { property url iconSource: "" property Rectangle iconBackground: null property color iconColor: Theme.color.neutral9 - hoverEnabled: true + hoverEnabled: AppMode.isDesktop topPadding: text_background.active ? 7 : 14 bottomPadding: text_background.active ? 7 : 14 rightPadding: text_background.active ? 22 : 14 @@ -94,7 +96,7 @@ AbstractButton { } MouseArea { anchors.fill: parent - hoverEnabled: true + hoverEnabled: AppMode.isDesktop onEntered: { root.background.state = "HOVER" } diff --git a/src/qml/controls/OutlineButton.qml b/src/qml/controls/OutlineButton.qml index 927913f4e4..dac386dec6 100644 --- a/src/qml/controls/OutlineButton.qml +++ b/src/qml/controls/OutlineButton.qml @@ -4,10 +4,11 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import org.bitcoincore.qt 1.0 Button { id: root - hoverEnabled: true + hoverEnabled: AppMode.isDesktop contentItem: CoreText { text: parent.text bold: true diff --git a/src/qml/controls/Setting.qml b/src/qml/controls/Setting.qml index 3b9331f39a..006ce71414 100644 --- a/src/qml/controls/Setting.qml +++ b/src/qml/controls/Setting.qml @@ -6,6 +6,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 + AbstractButton { id: root property bool last: parent && root === parent.children[parent.children.length - 1] @@ -16,7 +18,7 @@ AbstractButton { property string errorText: "" property bool showErrorText: false property color stateColor - hoverEnabled: true + hoverEnabled: AppMode.isDesktop state: "FILLED" states: [ @@ -57,7 +59,7 @@ AbstractButton { MouseArea { id: mouseArea anchors.fill: root - hoverEnabled: true + hoverEnabled: AppMode.isDesktop onEntered: { root.state = "HOVER" } diff --git a/src/qml/controls/TextButton.qml b/src/qml/controls/TextButton.qml index 5704b2b7f7..0bfafecde0 100644 --- a/src/qml/controls/TextButton.qml +++ b/src/qml/controls/TextButton.qml @@ -5,6 +5,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import org.bitcoincore.qt 1.0 + Button { id: root property int textSize: 18 @@ -13,7 +15,7 @@ Button { property bool bold: true property bool rightalign: false padding: 15 - hoverEnabled: true + hoverEnabled: AppMode.isDesktop contentItem: CoreText { text: root.text bold: root.bold diff --git a/src/qml/controls/ToggleButton.qml b/src/qml/controls/ToggleButton.qml index f4663d4d60..a073fc5a6f 100644 --- a/src/qml/controls/ToggleButton.qml +++ b/src/qml/controls/ToggleButton.qml @@ -4,6 +4,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import org.bitcoincore.qt 1.0 Button { property int bgRadius: 5 @@ -16,7 +17,7 @@ Button { id: root checkable: true - hoverEnabled: true + hoverEnabled: AppMode.isDesktop leftPadding: 12 rightPadding: 12 topPadding: 5 From f5494f38dd54372cc626946106af638c922666c4 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Wed, 15 Mar 2023 03:38:56 -0400 Subject: [PATCH 02/19] qml: make entire peers page scrollable --- src/qml/pages/node/Peers.qml | 146 +++++++++++++++++------------------ 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/qml/pages/node/Peers.qml b/src/qml/pages/node/Peers.qml index 67c3881b91..fe6202216f 100644 --- a/src/qml/pages/node/Peers.qml +++ b/src/qml/pages/node/Peers.qml @@ -16,87 +16,87 @@ Page { id: navbar } - CoreText { - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - id: description - width: Math.min(parent.width - 40, 450) - text: qsTr("Peers are nodes you are connected to. You want to ensure that you are connected" + - " to x, y and z, but not a, b, and c. Learn more.") - font.pixelSize: 13 - color: Theme.color.neutral7 - } - - Flickable { - id: sortSelection - anchors.top: description.bottom - anchors.topMargin: 20 - anchors.horizontalCenter: parent.horizontalCenter - width: Math.min(parent.width - 40, toggleButtons.width) - height: toggleButtons.height - contentWidth: toggleButtons.width - boundsMovement: width == toggleButtons.width ? - Flickable.StopAtBounds : Flickable.FollowBoundsBehavior - RowLayout { - id: toggleButtons - spacing: 10 - ToggleButton { - text: qsTr("ID") - autoExclusive: true - checked: true - onClicked: { - peerListModelProxy.sortBy = "nodeId" - } - } - ToggleButton { - text: qsTr("Direction") - autoExclusive: true - onClicked: { - peerListModelProxy.sortBy = "direction" - } - } - ToggleButton { - text: qsTr("User Agent") - autoExclusive: true - onClicked: { - peerListModelProxy.sortBy = "subversion" - } - } - ToggleButton { - text: qsTr("Type") - autoExclusive: true - onClicked: { - peerListModelProxy.sortBy = "connectionType" - } - } - ToggleButton { - text: qsTr("Ip") - autoExclusive: true - onClicked: { - peerListModelProxy.sortBy = "address" - } - } - ToggleButton { - text: qsTr("Network") - autoExclusive: true - onClicked: { - peerListModelProxy.sortBy = "network" - } - } - } - } - ListView { id: listView clip: true width: Math.min(parent.width - 40, 450) - anchors.top: sortSelection.bottom - anchors.topMargin: 30 - anchors.bottom: parent.bottom + height: parent.height anchors.horizontalCenter: parent.horizontalCenter model: peerListModelProxy spacing: 15 + header: ColumnLayout { + spacing: 20 + width: parent.width + CoreText { + id: description + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter + text: qsTr("Peers are nodes you are connected to. You want to ensure that you are connected" + + " to x, y and z, but not a, b, and c. Learn more.") + font.pixelSize: 13 + color: Theme.color.neutral7 + } + + Flickable { + id: sortSelection + Layout.fillWidth: true + Layout.bottomMargin: 30 + Layout.alignment: Qt.AlignHCenter + height: toggleButtons.height + contentWidth: toggleButtons.width + boundsMovement: width == toggleButtons.width ? + Flickable.StopAtBound : Flickable.FollowBoundsBehavior + RowLayout { + id: toggleButtons + spacing: 10 + ToggleButton { + text: qsTr("ID") + autoExclusive: true + checked: true + onClicked: { + peerListModelProxy.sortBy = "nodeId" + } + } + ToggleButton { + text: qsTr("Direction") + autoExclusive: true + onClicked: { + peerListModelProxy.sortBy = "direction" + } + } + ToggleButton { + text: qsTr("User Agent") + autoExclusive: true + onClicked: { + peerListModelProxy.sortBy = "subversion" + } + } + ToggleButton { + text: qsTr("Type") + autoExclusive: true + onClicked: { + peerListModelProxy.sortBy = "connectionType" + } + } + ToggleButton { + text: qsTr("Ip") + autoExclusive: true + onClicked: { + peerListModelProxy.sortBy = "address" + } + } + ToggleButton { + text: qsTr("Network") + autoExclusive: true + onClicked: { + peerListModelProxy.sortBy = "network" + } + } + } + } + } + footer: Loader { height: 75 active: nodeModel.numOutboundPeers < nodeModel.maxNumOutboundPeers From 6cbff640dca975a0459a66d36271d7d804ffbe1b Mon Sep 17 00:00:00 2001 From: jarolrod Date: Thu, 20 Apr 2023 19:50:57 -0400 Subject: [PATCH 03/19] qml: add page title to peers page & update description --- src/qml/pages/node/NodeSettings.qml | 5 +++++ src/qml/pages/node/Peers.qml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/qml/pages/node/NodeSettings.qml b/src/qml/pages/node/NodeSettings.qml index 3acf27a0d1..bd4bf7def8 100644 --- a/src/qml/pages/node/NodeSettings.qml +++ b/src/qml/pages/node/NodeSettings.qml @@ -162,6 +162,11 @@ Item { peerTableModel.stopAutoRefresh(); } } + navMiddleDetail: Header { + headerBold: true + headerSize: 18 + header: qsTr("Peers") + } } } Component { diff --git a/src/qml/pages/node/Peers.qml b/src/qml/pages/node/Peers.qml index fe6202216f..2dca9afa42 100644 --- a/src/qml/pages/node/Peers.qml +++ b/src/qml/pages/node/Peers.qml @@ -11,6 +11,7 @@ import "../../components" Page { background: null property alias navLeftDetail: navbar.leftDetail + property alias navMiddleDetail: navbar.middleDetail header: NavigationBar { id: navbar @@ -32,8 +33,7 @@ Page { id: description Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter - text: qsTr("Peers are nodes you are connected to. You want to ensure that you are connected" + - " to x, y and z, but not a, b, and c. Learn more.") + text: qsTr("Peers are nodes you exchange data with.") font.pixelSize: 13 color: Theme.color.neutral7 } From 962bc203d08bfd9bcebb951913bbbb7c98732412 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Tue, 28 Mar 2023 22:28:23 -0400 Subject: [PATCH 04/19] qml: marry BlockClock and NetworkIndicator Greetings. I am pleased to inform you that the BlockClock component and the Network Indicator component are merging in a most harmonious and efficient manner. This event brings me great joy and excitement, as it represents a significant milestone in the world of Bitcoin. BlockClock, you have been diligent in displaying the last 12 hours of blocks mined on the Bitcoin blockchain, consistently providing valuable information to users. Network Indicator, your expertise in indicating the current network - main, test, or signet - has been indispensable. As I observe this union within the BlockClock.qml file, I cannot help but feel a sense of pride and satisfaction. This integration will undoubtedly lead to enhanced functionality and streamlined user experience. I offer my sincerest congratulations to BlockClock and Network Indicator on their momentous union. May your combined efforts bring about continued success and increased stability in the Bitcoin ecosystem. To the moon, my fellow components. To the moon. --- src/qml/components/BlockClock.qml | 14 +++++++++++--- src/qml/pages/node/NodeRunner.qml | 9 +-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml index 47efc73606..f7fa5006b4 100644 --- a/src/qml/components/BlockClock.qml +++ b/src/qml/components/BlockClock.qml @@ -14,7 +14,7 @@ Item { id: root implicitWidth: 200 - implicitHeight: 200 + implicitHeight: 200 + networkIndicator.height + networkIndicator.anchors.topMargin property alias header: mainText.text property alias headerSize: mainText.font.pixelSize @@ -28,7 +28,8 @@ Item { BlockClockDial { id: dial - anchors.fill: parent + width: 200 + height: 200 timeRatioList: chainModel.timeRatioList verificationProgress: nodeModel.verificationProgress paused: root.paused @@ -68,7 +69,7 @@ Item { Label { id: mainText - anchors.centerIn: parent + anchors.centerIn: dial font.family: "Inter" font.styleName: "Semi Bold" font.pixelSize: 32 @@ -102,6 +103,13 @@ Item { paused: root.paused } + NetworkIndicator { + id: networkIndicator + anchors.top: dial.bottom + anchors.topMargin: networkIndicator.visible ? 30 : 0 + anchors.horizontalCenter: root.horizontalCenter + } + MouseArea { anchors.fill: dial cursorShape: Qt.PointingHandCursor diff --git a/src/qml/pages/node/NodeRunner.qml b/src/qml/pages/node/NodeRunner.qml index eda06f3ce6..c4ed4e3a10 100644 --- a/src/qml/pages/node/NodeRunner.qml +++ b/src/qml/pages/node/NodeRunner.qml @@ -18,14 +18,7 @@ Page { Component.onCompleted: nodeModel.startNodeInitializionThread(); - ColumnLayout { - spacing: 30 + BlockClock { anchors.centerIn: parent - BlockClock { - Layout.alignment: Qt.AlignCenter - } - NetworkIndicator { - Layout.alignment: Qt.AlignCenter - } } } From 2cf56f13e9cb47a7527c0f2a8d59e571e2ce4fd0 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Wed, 29 Mar 2023 04:13:00 -0400 Subject: [PATCH 05/19] qml: allow to customize blockclockdial's pen width --- src/qml/components/blockclockdial.cpp | 22 +++++++++++++++------- src/qml/components/blockclockdial.h | 5 +++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/qml/components/blockclockdial.cpp b/src/qml/components/blockclockdial.cpp index 2d4580e9e0..fb5fb30f1b 100644 --- a/src/qml/components/blockclockdial.cpp +++ b/src/qml/components/blockclockdial.cpp @@ -10,10 +10,12 @@ #include #include #include +#include BlockClockDial::BlockClockDial(QQuickItem *parent) : QQuickPaintedItem(parent) , m_time_ratio_list{0.0} +, m_pen_width{4} , m_background_color{QColor("#2D2D2D")} , m_confirmation_colors{QList{}} , m_time_tick_color{QColor("#000000")} @@ -132,6 +134,12 @@ void BlockClockDial::setPaused(bool paused) } } +void BlockClockDial::setPenWidth(qreal width) +{ + m_pen_width = width; + update(); +} + void BlockClockDial::setBackgroundColor(QColor color) { m_background_color = color; @@ -183,7 +191,7 @@ void BlockClockDial::paintBlocks(QPainter * painter) } QPen pen(m_confirmation_colors[5]); - pen.setWidth(4); + pen.setWidthF(m_pen_width); pen.setCapStyle(Qt::FlatCap); const QRectF bounds = getBoundsForPen(pen); painter->setPen(pen); @@ -196,7 +204,7 @@ void BlockClockDial::paintBlocks(QPainter * painter) for (int i = 1; i < numberOfBlocks; i++) { if (numberOfBlocks - i <= 6) { QPen pen(m_confirmation_colors[numberOfBlocks - i - 1]); - pen.setWidth(4); + pen.setWidthF(m_pen_width); pen.setCapStyle(Qt::FlatCap); painter->setPen(pen); } @@ -227,7 +235,7 @@ void BlockClockDial::paintBlocks(QPainter * painter) void BlockClockDial::paintProgress(QPainter * painter) { QPen pen(m_confirmation_colors[5]); - pen.setWidthF(4); + pen.setWidthF(m_pen_width); pen.setCapStyle(Qt::RoundCap); const QRectF bounds = getBoundsForPen(pen); painter->setPen(pen); @@ -250,7 +258,7 @@ void BlockClockDial::paintProgress(QPainter * painter) void BlockClockDial::paintConnectingAnimation(QPainter * painter) { QPen pen; - pen.setWidthF(4); + pen.setWidthF(m_pen_width); setupConnectingGradient(pen); pen.setBrush(QBrush(m_connecting_gradient)); pen.setCapStyle(Qt::RoundCap); @@ -267,7 +275,7 @@ void BlockClockDial::paintConnectingAnimation(QPainter * painter) void BlockClockDial::paintBackground(QPainter * painter) { QPen pen(m_background_color); - pen.setWidthF(4); + pen.setWidthF(m_pen_width); const QRectF bounds = getBoundsForPen(pen); painter->setPen(pen); @@ -283,12 +291,12 @@ double BlockClockDial::degreesPerPixel() void BlockClockDial::paintTimeTicks(QPainter * painter) { QPen pen(m_time_tick_color); - pen.setWidthF(4); + pen.setWidthF(m_pen_width); // 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.setWidthF(m_pen_width / 2); time_tick_pen.setCapStyle(Qt::RoundCap); painter->setPen(time_tick_pen); for (double angle = 0; angle < 360; angle += 30) { diff --git a/src/qml/components/blockclockdial.h b/src/qml/components/blockclockdial.h index ea3ed686b1..a750d10cd7 100644 --- a/src/qml/components/blockclockdial.h +++ b/src/qml/components/blockclockdial.h @@ -9,6 +9,7 @@ #include #include #include +#include class BlockClockDial : public QQuickPaintedItem { @@ -18,6 +19,7 @@ class BlockClockDial : public QQuickPaintedItem Q_PROPERTY(bool connected READ connected WRITE setConnected) Q_PROPERTY(bool synced READ synced WRITE setSynced) Q_PROPERTY(bool paused READ paused WRITE setPaused) + Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth) Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) Q_PROPERTY(QList confirmationColors READ confirmationColors WRITE setConfirmationColors ) Q_PROPERTY(QColor timeTickColor READ timeTickColor WRITE setTimeTickColor) @@ -31,6 +33,7 @@ class BlockClockDial : public QQuickPaintedItem bool connected() const { return m_is_connected; }; bool synced() const { return m_is_synced; }; bool paused() const { return m_is_paused; }; + qreal penWidth() const { return m_pen_width; }; QColor backgroundColor() const { return m_background_color; }; QList confirmationColors() const { return m_confirmation_colors; }; QColor timeTickColor() const { return m_time_tick_color; }; @@ -41,6 +44,7 @@ public Q_SLOTS: void setConnected(bool connected); void setSynced(bool synced); void setPaused(bool paused); + void setPenWidth(qreal width); void setBackgroundColor(QColor color); void setConfirmationColors(QList colorList); void setTimeTickColor(QColor color); @@ -63,6 +67,7 @@ public Q_SLOTS: bool m_is_connected; bool m_is_synced; bool m_is_paused; + qreal m_pen_width; QColor m_background_color; QConicalGradient m_connecting_gradient; qreal m_connecting_start_angle = 90; From a6099e74681500788961f2e1872977580a1a1f2f Mon Sep 17 00:00:00 2001 From: jarolrod Date: Wed, 29 Mar 2023 04:14:59 -0400 Subject: [PATCH 06/19] qml: allow to resize PeersIndicator component --- src/qml/components/PeersIndicator.qml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/qml/components/PeersIndicator.qml b/src/qml/components/PeersIndicator.qml index 54a0d52402..7faedad947 100644 --- a/src/qml/components/PeersIndicator.qml +++ b/src/qml/components/PeersIndicator.qml @@ -3,22 +3,25 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. import QtQuick 2.15 -import QtQuick.Layouts 1.15 import "../controls" -RowLayout { +Row { id: root required property int numOutboundPeers required property int maxNumOutboundPeers required property bool paused property int size: 5 + property real indicatorDimensions: 3 + property real indicatorSpacing: 5 - spacing: 5 + height: root.indicatorDimensions + + spacing: root.indicatorSpacing Repeater { model: 5 Rectangle { - width: 3 - height: 3 + width: root.indicatorDimensions + height: root.indicatorDimensions radius: width / 2 color: Theme.color.neutral9 opacity: (index === 0 && root.numOutboundPeers > 0) || (index + 1 <= root.size * root.numOutboundPeers / root.maxNumOutboundPeers) ? 0.95 : 0.45 From 3978d376ba3f8caa29bd7e03511333e5469048dc Mon Sep 17 00:00:00 2001 From: jarolrod Date: Wed, 29 Mar 2023 04:16:05 -0400 Subject: [PATCH 07/19] qml: introduce bitcoin-circle icon in larger dimensions This fixes an issue with the icon on very HDPI screens when resizing the block clock. --- src/qml/res/icons/bitcoin-circle.png | Bin 1742 -> 44342 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/qml/res/icons/bitcoin-circle.png b/src/qml/res/icons/bitcoin-circle.png index dac55c361029b3302d7f55bbfe9fe94cad7e37d7..9535609393704bf0575dff7d44fe302f8d053c76 100644 GIT binary patch literal 44342 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 literal 1742 zcmV;<1~K`GP)nv|HX;UXn6+OZkMg%Kw7%2w63&5oH(QNU$RJI@4doDk=%le$*k#nkd6jf#j;d#+^LI?*xCimrUr7;)XAtx~M&9>xulq51-$6Y0Bq8YR)1bE# zjyTJ6ue%vXj5?zMa(4o@ljkYV`&4ryKV)T#<*qc25W`oU5M}e&8gJ6v%R2p3 z%O4XEb-nTKVE%|0^$9*IXFZ~B1TFRE`&>^235rYdCSuS!()ea9g1*Cxjw@F%i9T$B zyBIGc293fuzem*lix@4{RO^vrjtr|h!C|VloamaKbVo(KYLlE>G(B>|u79q-(qE$_k@luRhJ^C?#&fY9koF`MuAj-y| zaXL#7<2IDSFW_Lzq&S~%lSQKa1vCUO3+Ttbw5|;O$F{M9Ki~bh*}?eUU|`e>B1aTm2XJo#<+%9= z64io8na3ruuUSlfKQPwjbpWUHaq}4x)sh&@^t>r~5aW1{H`%}V6~NPiC@eq>njZeL z_)-#V%HUQ{{q8`aq~UDvaeUFm$E)sB$R>LcH-{8=r93R{?C$-Q zygk^ALc4JpUm?Z~FP-D^qi~ct<4GPRZwXC&!S{KKC%BCVd5({mkEohPf`C+$H9i;6 z$>sWd0OEUDP9i{W8t(}_<}yMbh4|iU1Zqc5!;%ll`Z${u8tiDSSOkdb_KVg z$>s5aix6LHrW<}x>{r#Q_CJoI2y!5AnGt2*qan&C?pu`;6vIm&XeZM||xsqzK8r z)atjYcpLGt8ulhcm8>%UMt{J~R1jk!;#;dYo{*hKM)?;L-^y?f5$I1Ai9WLQBO*On zk3?LC{zT|(Rw5C$nhS_n1vN-S{fi2s$6oviiJ-~sM~O&s583FSOf6bnsYgPpjxLm- z?LvD(#hoZw6--6~Yz8M&;u4JHOC*54U^oe)7s=D=&S5A?+Lag=@d@h?W$XBui-^%q zr5VL<4A=LY_zfdT(*bO4^yKR0bU52m$w2O7Jbz*q^JySMh6d&{i$C!R_c4$Pwk1H8 kWm%SGS(as4mStK01~e>;3Rz|G2><{907*qoM6N<$f|y1-8UO$Q From 40c5c1f5d9d9aa1d5892f0660cf41ea28c654f4c Mon Sep 17 00:00:00 2001 From: jarolrod Date: Wed, 29 Mar 2023 04:17:45 -0400 Subject: [PATCH 08/19] qml: set BlockClock size values as a ratio of the parent width & height --- src/qml/components/BlockClock.qml | 36 ++++++++++++++++++------------- src/qml/pages/node/NodeRunner.qml | 2 ++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml index f7fa5006b4..69a64fbdb1 100644 --- a/src/qml/components/BlockClock.qml +++ b/src/qml/components/BlockClock.qml @@ -12,9 +12,11 @@ import "../controls" Item { id: root + property real parentWidth: 600 + property real parentHeight: 600 - implicitWidth: 200 - implicitHeight: 200 + networkIndicator.height + networkIndicator.anchors.topMargin + width: dial.width + height: dial.height + networkIndicator.height + networkIndicator.anchors.topMargin property alias header: mainText.text property alias headerSize: mainText.font.pixelSize @@ -28,8 +30,10 @@ Item { BlockClockDial { id: dial - width: 200 - height: 200 + anchors.horizontalCenter: root.horizontalCenter + width: Math.min((root.parentWidth * (1/3)), (root.parentHeight * (1/3))) + height: dial.width + penWidth: dial.width / 50 timeRatioList: chainModel.timeRatioList verificationProgress: nodeModel.verificationProgress paused: root.paused @@ -57,8 +61,8 @@ Item { background: null icon.source: "image://images/bitcoin-circle" icon.color: Theme.color.neutral9 - icon.width: 40 - icon.height: 40 + icon.width: Math.max(dial.width / 5, 1) + icon.height: Math.max(dial.width / 5, 1) anchors.bottom: mainText.top anchors.horizontalCenter: root.horizontalCenter @@ -72,7 +76,7 @@ Item { anchors.centerIn: dial font.family: "Inter" font.styleName: "Semi Bold" - font.pixelSize: 32 + font.pixelSize: dial.width * (4/25) color: Theme.color.neutral9 Behavior on color { @@ -86,7 +90,7 @@ Item { anchors.horizontalCenter: root.horizontalCenter font.family: "Inter" font.styleName: "Semi Bold" - font.pixelSize: 18 + font.pixelSize: dial.width * (9/100) color: Theme.color.neutral4 Behavior on color { @@ -96,10 +100,12 @@ Item { PeersIndicator { anchors.top: subText.bottom - anchors.topMargin: 20 + anchors.topMargin: dial.width / 10 anchors.horizontalCenter: root.horizontalCenter numOutboundPeers: nodeModel.numOutboundPeers maxNumOutboundPeers: nodeModel.maxNumOutboundPeers + indicatorDimensions: dial.width * (3/200) + indicatorSpacing: dial.width / 40 paused: root.paused } @@ -146,16 +152,16 @@ Item { PropertyChanges { target: root header: "Paused" - headerSize: 24 + headerSize: dial.width * (3/25) subText: "Tap to resume" } PropertyChanges { target: bitcoinIcon - anchors.bottomMargin: 5 + anchors.bottomMargin: dial.width / 40 } PropertyChanges { target: subText - anchors.topMargin: 4 + anchors.topMargin: dial.width / 50 } }, @@ -164,16 +170,16 @@ Item { PropertyChanges { target: root header: "Connecting" - headerSize: 24 + headerSize: dial.width * (3/25) subText: "Please wait" } PropertyChanges { target: bitcoinIcon - anchors.bottomMargin: 5 + anchors.bottomMargin: dial.width / 40 } PropertyChanges { target: subText - anchors.topMargin: 4 + anchors.topMargin: dial.width / 50 } } ] diff --git a/src/qml/pages/node/NodeRunner.qml b/src/qml/pages/node/NodeRunner.qml index c4ed4e3a10..dfea71e27c 100644 --- a/src/qml/pages/node/NodeRunner.qml +++ b/src/qml/pages/node/NodeRunner.qml @@ -19,6 +19,8 @@ Page { Component.onCompleted: nodeModel.startNodeInitializionThread(); BlockClock { + parentWidth: parent.width - 40 + parentHeight: parent.height anchors.centerIn: parent } } From 4a191c63e5a7388af225e78585ef652380c62d4c Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 31 Mar 2023 05:55:53 -0400 Subject: [PATCH 09/19] qml: allow to set blockclockdial scale This represents the factor we use to scale off of the parent width & height. --- src/qml/components/blockclockdial.cpp | 9 +++++++++ src/qml/components/blockclockdial.h | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/qml/components/blockclockdial.cpp b/src/qml/components/blockclockdial.cpp index fb5fb30f1b..547389b232 100644 --- a/src/qml/components/blockclockdial.cpp +++ b/src/qml/components/blockclockdial.cpp @@ -16,6 +16,7 @@ BlockClockDial::BlockClockDial(QQuickItem *parent) : QQuickPaintedItem(parent) , m_time_ratio_list{0.0} , m_pen_width{4} +, m_scale{5/12} , m_background_color{QColor("#2D2D2D")} , m_confirmation_colors{QList{}} , m_time_tick_color{QColor("#000000")} @@ -140,6 +141,14 @@ void BlockClockDial::setPenWidth(qreal width) update(); } +void BlockClockDial::setScale(qreal scale) +{ + m_scale = scale; + update(); + + Q_EMIT scaleChanged(); +} + void BlockClockDial::setBackgroundColor(QColor color) { m_background_color = color; diff --git a/src/qml/components/blockclockdial.h b/src/qml/components/blockclockdial.h index a750d10cd7..ecbe98e4da 100644 --- a/src/qml/components/blockclockdial.h +++ b/src/qml/components/blockclockdial.h @@ -20,6 +20,7 @@ class BlockClockDial : public QQuickPaintedItem Q_PROPERTY(bool synced READ synced WRITE setSynced) Q_PROPERTY(bool paused READ paused WRITE setPaused) Q_PROPERTY(qreal penWidth READ penWidth WRITE setPenWidth) + Q_PROPERTY(qreal scale READ scale WRITE setScale NOTIFY scaleChanged) Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor) Q_PROPERTY(QList confirmationColors READ confirmationColors WRITE setConfirmationColors ) Q_PROPERTY(QColor timeTickColor READ timeTickColor WRITE setTimeTickColor) @@ -34,6 +35,7 @@ class BlockClockDial : public QQuickPaintedItem bool synced() const { return m_is_synced; }; bool paused() const { return m_is_paused; }; qreal penWidth() const { return m_pen_width; }; + qreal scale() const { return m_scale; }; QColor backgroundColor() const { return m_background_color; }; QList confirmationColors() const { return m_confirmation_colors; }; QColor timeTickColor() const { return m_time_tick_color; }; @@ -45,10 +47,14 @@ public Q_SLOTS: void setSynced(bool synced); void setPaused(bool paused); void setPenWidth(qreal width); + void setScale(qreal scale); void setBackgroundColor(QColor color); void setConfirmationColors(QList colorList); void setTimeTickColor(QColor color); +Q_SIGNALS: + void scaleChanged(); + private: void paintConnectingAnimation(QPainter * painter); void paintProgress(QPainter * painter); @@ -68,6 +74,7 @@ public Q_SLOTS: bool m_is_synced; bool m_is_paused; qreal m_pen_width; + qreal m_scale; QColor m_background_color; QConicalGradient m_connecting_gradient; qreal m_connecting_start_angle = 90; From 74f4bcd0189317798c56d13e088b1411c37860fc Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 31 Mar 2023 05:58:12 -0400 Subject: [PATCH 10/19] qml: remember blockclockdial scale factor --- src/qml/components/BlockClock.qml | 8 +++++++- src/qml/controls/Theme.qml | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml index 69a64fbdb1..f71660cc07 100644 --- a/src/qml/components/BlockClock.qml +++ b/src/qml/components/BlockClock.qml @@ -5,6 +5,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.0 import org.bitcoincore.qt 1.0 @@ -28,10 +29,15 @@ Item { activeFocusOnTab: true + Settings { + id: settings + property alias blockclocksize: dial.scale + } + BlockClockDial { id: dial anchors.horizontalCenter: root.horizontalCenter - width: Math.min((root.parentWidth * (1/3)), (root.parentHeight * (1/3))) + width: Math.min((root.parentWidth * dial.scale), (root.parentHeight * dial.scale)) height: dial.width penWidth: dial.width / 50 timeRatioList: chainModel.timeRatioList diff --git a/src/qml/controls/Theme.qml b/src/qml/controls/Theme.qml index 76283fcc1c..3699e1fe5d 100644 --- a/src/qml/controls/Theme.qml +++ b/src/qml/controls/Theme.qml @@ -1,12 +1,20 @@ pragma Singleton import QtQuick 2.15 import QtQuick.Controls 2.15 +import Qt.labs.settings 1.0 Control { + id: root property bool dark: true + property real blockclocksize: (5/12) readonly property ColorSet color: dark ? darkColorSet : lightColorSet readonly property ImageSet image: dark ? darkImageSet : lightImageSet + Settings { + id: settings + property alias blockclocksize: root.blockclocksize + } + component ColorSet: QtObject { required property color white required property color background From cf3f94422d6352ae3aacf69540d65b8b292667c4 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 31 Mar 2023 06:00:36 -0400 Subject: [PATCH 11/19] qml: introduce ThemeSettings page --- src/Makefile.qt.include | 4 +- src/qml/bitcoin_qml.qrc | 2 + src/qml/components/ThemeSettings.qml | 60 ++++++++++++++++++++++++ src/qml/pages/settings/SettingsTheme.qml | 18 +++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/qml/components/ThemeSettings.qml create mode 100644 src/qml/pages/settings/SettingsTheme.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 6b3536fe98..08d05ea01f 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -352,6 +352,7 @@ QML_RES_QML = \ qml/components/StorageLocations.qml \ qml/components/StorageOptions.qml \ qml/components/StorageSettings.qml \ + qml/components/ThemeSettings.qml \ qml/components/TotalBytesIndicator.qml \ qml/controls/ContinueButton.qml \ qml/controls/CoreText.qml \ @@ -389,7 +390,8 @@ QML_RES_QML = \ qml/pages/settings/SettingsConnection.qml \ qml/pages/settings/SettingsDeveloper.qml \ qml/pages/settings/SettingsProxy.qml \ - qml/pages/settings/SettingsStorage.qml + qml/pages/settings/SettingsStorage.qml \ + qml/pages/settings/SettingsTheme.qml BITCOIN_QT_CPP = $(BITCOIN_QT_BASE_CPP) if TARGET_WINDOWS diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 85644c4a46..b4a19b695d 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -15,6 +15,7 @@ components/Separator.qml components/StorageOptions.qml components/StorageSettings.qml + components/ThemeSettings.qml components/TotalBytesIndicator.qml controls/ContinueButton.qml controls/CoreText.qml @@ -53,6 +54,7 @@ pages/settings/SettingsDeveloper.qml pages/settings/SettingsProxy.qml pages/settings/SettingsStorage.qml + pages/settings/SettingsTheme.qml res/icons/arrow-down.png diff --git a/src/qml/components/ThemeSettings.qml b/src/qml/components/ThemeSettings.qml new file mode 100644 index 0000000000..dd052ec268 --- /dev/null +++ b/src/qml/components/ThemeSettings.qml @@ -0,0 +1,60 @@ +// 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 Qt.labs.settings 1.0 +import "../controls" + +ColumnLayout { + id: root + spacing: 4 + + Settings { + id: settings + } + + Setting { + Layout.fillWidth: true + header: qsTr("Light") + actionItem: Button { + anchors.centerIn: parent + visible: !Theme.dark + icon.source: "image://images/check" + icon.color: Theme.color.neutral9 + icon.height: 24 + icon.width: 24 + background: null + + Behavior on icon.color { + ColorAnimation { duration: 150 } + } + } + onClicked: { + Theme.dark = false + } + } + Separator { Layout.fillWidth: true } + Setting { + Layout.fillWidth: true + header: qsTr("Dark") + actionItem: Button { + anchors.centerIn: parent + visible: Theme.dark + icon.source: "image://images/check" + icon.color: Theme.color.neutral9 + icon.height: 24 + icon.width: 24 + background: null + + Behavior on icon.color { + ColorAnimation { duration: 150 } + } + } + onClicked: { + Theme.dark = true; + } + } +} diff --git a/src/qml/pages/settings/SettingsTheme.qml b/src/qml/pages/settings/SettingsTheme.qml new file mode 100644 index 0000000000..cfe1259fdc --- /dev/null +++ b/src/qml/pages/settings/SettingsTheme.qml @@ -0,0 +1,18 @@ +// 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 "../../controls" +import "../../components" + +InformationPage { + bannerActive: false + bold: true + headerText: qsTr("Theme") + headerMargin: 0 + detailActive: true + detailItem: ThemeSettings {} +} From 1e3b1a10b5c588a57978b8f8bf1eb95e52730115 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 31 Mar 2023 06:01:32 -0400 Subject: [PATCH 12/19] qml: introduce Block Clock display mode settings page --- src/Makefile.qt.include | 2 + src/qml/bitcoin_qml.qrc | 2 + src/qml/components/BlockClock.qml | 1 + src/qml/components/BlockClockDisplayMode.qml | 40 +++++++++++++++++++ .../SettingsBlockClockDisplayMode.qml | 18 +++++++++ 5 files changed, 63 insertions(+) create mode 100644 src/qml/components/BlockClockDisplayMode.qml create mode 100644 src/qml/pages/settings/SettingsBlockClockDisplayMode.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 08d05ea01f..f17e1bf45a 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -339,6 +339,7 @@ QML_QRC = qml/bitcoin_qml.qrc QML_RES_QML = \ qml/components/AboutOptions.qml \ qml/components/BlockClock.qml \ + qml/components/BlockClockDisplayMode.qml \ qml/components/BlockCounter.qml \ qml/components/CaretRightButton.qml \ qml/components/ConnectionOptions.qml \ @@ -387,6 +388,7 @@ QML_RES_QML = \ qml/pages/onboarding/OnboardingStorageLocation.qml \ qml/pages/onboarding/OnboardingStrengthen.qml \ qml/pages/settings/SettingsAbout.qml \ + qml/pages/settings/SettingsBlockClockDisplayMode.qml \ qml/pages/settings/SettingsConnection.qml \ qml/pages/settings/SettingsDeveloper.qml \ qml/pages/settings/SettingsProxy.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index b4a19b695d..6d77574083 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -2,6 +2,7 @@ components/AboutOptions.qml components/BlockClock.qml + components/BlockClockDisplayMode.qml components/BlockCounter.qml components/CaretRightButton.qml components/ConnectionOptions.qml @@ -50,6 +51,7 @@ pages/onboarding/OnboardingStorageLocation.qml pages/onboarding/OnboardingStrengthen.qml pages/settings/SettingsAbout.qml + pages/settings/SettingsBlockClockDisplayMode.qml pages/settings/SettingsConnection.qml pages/settings/SettingsDeveloper.qml pages/settings/SettingsProxy.qml diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml index f71660cc07..c7dfb11ba5 100644 --- a/src/qml/components/BlockClock.qml +++ b/src/qml/components/BlockClock.qml @@ -37,6 +37,7 @@ Item { BlockClockDial { id: dial anchors.horizontalCenter: root.horizontalCenter + scale: Theme.blockclocksize width: Math.min((root.parentWidth * dial.scale), (root.parentHeight * dial.scale)) height: dial.width penWidth: dial.width / 50 diff --git a/src/qml/components/BlockClockDisplayMode.qml b/src/qml/components/BlockClockDisplayMode.qml new file mode 100644 index 0000000000..eb552d7456 --- /dev/null +++ b/src/qml/components/BlockClockDisplayMode.qml @@ -0,0 +1,40 @@ +// 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 Qt.labs.settings 1.0 +import "../controls" + +ColumnLayout { + id: root + spacing: 15 + + ButtonGroup { + id: group + } + + OptionButton { + Layout.fillWidth: true + ButtonGroup.group: group + text: qsTr("Default") + description: qsTr("For personal use on a computer or smartphone.") + checked: Theme.blockclocksize == (1/3) + onClicked: { + Theme.blockclocksize = (1/3) + } + } + + OptionButton { + Layout.fillWidth: true + ButtonGroup.group: group + text: qsTr("Showcase") + description: qsTr("A larger block clock for public display on a tablet or other large screen.") + checked: Theme.blockclocksize == (1/2) + onClicked: { + Theme.blockclocksize = (1/2) + } + } +} diff --git a/src/qml/pages/settings/SettingsBlockClockDisplayMode.qml b/src/qml/pages/settings/SettingsBlockClockDisplayMode.qml new file mode 100644 index 0000000000..6a2c134e66 --- /dev/null +++ b/src/qml/pages/settings/SettingsBlockClockDisplayMode.qml @@ -0,0 +1,18 @@ +// 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 "../../controls" +import "../../components" + +InformationPage { + bannerActive: false + bold: true + headerText: qsTr("Block Clock display mode") + headerMargin: 0 + detailActive: true + detailItem: BlockClockDisplayMode {} +} From 4429bbef02c3bbcf7e1f257083e70a0c092abe78 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 31 Mar 2023 06:04:21 -0400 Subject: [PATCH 13/19] qml: introduce Display Settings page --- src/Makefile.qt.include | 1 + src/qml/bitcoin_qml.qrc | 1 + src/qml/controls/Theme.qml | 1 + src/qml/pages/node/NodeSettings.qml | 33 +++++--- src/qml/pages/settings/SettingsDisplay.qml | 88 ++++++++++++++++++++++ 5 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 src/qml/pages/settings/SettingsDisplay.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f17e1bf45a..03541f8e2f 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -391,6 +391,7 @@ QML_RES_QML = \ qml/pages/settings/SettingsBlockClockDisplayMode.qml \ qml/pages/settings/SettingsConnection.qml \ qml/pages/settings/SettingsDeveloper.qml \ + qml/pages/settings/SettingsDisplay.qml \ qml/pages/settings/SettingsProxy.qml \ qml/pages/settings/SettingsStorage.qml \ qml/pages/settings/SettingsTheme.qml diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 6d77574083..b6a5ef0b71 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -54,6 +54,7 @@ pages/settings/SettingsBlockClockDisplayMode.qml pages/settings/SettingsConnection.qml pages/settings/SettingsDeveloper.qml + pages/settings/SettingsDisplay.qml pages/settings/SettingsProxy.qml pages/settings/SettingsStorage.qml pages/settings/SettingsTheme.qml diff --git a/src/qml/controls/Theme.qml b/src/qml/controls/Theme.qml index 3699e1fe5d..df62e0a4ad 100644 --- a/src/qml/controls/Theme.qml +++ b/src/qml/controls/Theme.qml @@ -12,6 +12,7 @@ Control { Settings { id: settings + property alias dark: root.dark property alias blockclocksize: root.blockclocksize } diff --git a/src/qml/pages/node/NodeSettings.qml b/src/qml/pages/node/NodeSettings.qml index bd4bf7def8..5b0c25e637 100644 --- a/src/qml/pages/node/NodeSettings.qml +++ b/src/qml/pages/node/NodeSettings.qml @@ -38,23 +38,26 @@ Item { width: Math.min(parent.width, 450) anchors.horizontalCenter: parent.horizontalCenter Setting { + id: gotoAbout Layout.fillWidth: true - header: qsTr("Dark Mode") - actionItem: OptionSwitch { - checked: Theme.dark - onToggled: Theme.toggleDark() + header: qsTr("About") + actionItem: CaretRightButton { + stateColor: gotoAbout.stateColor + onClicked: { + nodeSettingsView.push(about_page) + } } - onClicked: loadedItem.toggled() + onClicked: loadedItem.clicked() } Separator { Layout.fillWidth: true } Setting { - id: gotoAbout + id: gotoDisplay Layout.fillWidth: true - header: qsTr("About") + header: qsTr("Display") actionItem: CaretRightButton { - stateColor: gotoAbout.stateColor + stateColor: gotoDisplay.stateColor onClicked: { - nodeSettingsView.push(about_page) + nodeSettingsView.push(display_page) } } onClicked: loadedItem.clicked() @@ -127,6 +130,18 @@ Item { } } } + Component { + id: display_page + SettingsDisplay { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } Component { id: storage_page SettingsStorage { diff --git a/src/qml/pages/settings/SettingsDisplay.qml b/src/qml/pages/settings/SettingsDisplay.qml new file mode 100644 index 0000000000..b4c953fe95 --- /dev/null +++ b/src/qml/pages/settings/SettingsDisplay.qml @@ -0,0 +1,88 @@ +// 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 "../../controls" +import "../../components" + +Item { + property alias navLeftDetail: displaySettingsView.navLeftDetail + StackView { + id: displaySettingsView + property alias navLeftDetail: displaySettings.navLeftDetail + property bool newcompilebool: false + anchors.fill: parent + + + initialItem: Page { + id: displaySettings + property alias navLeftDetail: navbar.leftDetail + background: null + implicitWidth: 450 + leftPadding: 20 + rightPadding: 20 + topPadding: 30 + + header: NavigationBar { + id: navbar + } + ColumnLayout { + spacing: 4 + width: Math.min(parent.width, 450) + anchors.horizontalCenter: parent.horizontalCenter + Setting { + id: gotoTheme + Layout.fillWidth: true + header: qsTr("Theme") + actionItem: CaretRightButton { + stateColor: gotoTheme.stateColor + onClicked: { + nodeSettingsView.push(theme_page) + } + } + onClicked: loadedItem.clicked() + } + Separator { Layout.fillWidth: true } + Setting { + id: gotoBlockClockSize + Layout.fillWidth: true + header: qsTr("Block Clock display mode") + actionItem: CaretRightButton { + stateColor: gotoBlockClockSize.stateColor + onClicked: { + nodeSettingsView.push(blockclocksize_page) + } + } + onClicked: loadedItem.clicked() + } + } + } + } + Component { + id: theme_page + SettingsTheme { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } + Component { + id: blockclocksize_page + SettingsBlockClockDisplayMode { + navLeftDetail: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: { + nodeSettingsView.pop() + } + } + } + } +} From 1476cabfdf25ea6d6cd186e48dc3b720a361001b Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Fri, 14 Apr 2023 00:37:21 -0400 Subject: [PATCH 14/19] android: configure toolchain path for libc++ based on arch --- configure.ac | 4 ++++ src/Makefile.qt.include | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/configure.ac b/configure.ac index 586db4dc69..e26ebade97 100644 --- a/configure.ac +++ b/configure.ac @@ -812,12 +812,15 @@ case $host in case $host in *x86_64*) ANDROID_ARCH=x86_64 + NDK_LIBCXX_DIR=x86_64-linux-android ;; *aarch64*) ANDROID_ARCH=arm64-v8a + NDK_LIBCXX_DIR=aarch64-linux-android ;; *armv7a*) ANDROID_ARCH=armeabi-v7a + NDK_LIBCXX_DIR=arm-linux-androideabi ;; *) AC_MSG_ERROR([Could not determine Android arch, or it is unsupported]) ;; esac @@ -1963,6 +1966,7 @@ AC_SUBST(HAVE_BUILTIN_PREFETCH) AC_SUBST(HAVE_MM_PREFETCH) AC_SUBST(HAVE_STRONG_GETAUXVAL) AC_SUBST(ANDROID_ARCH) +AC_SUBST(NDK_LIBCXX_DIR) AC_SUBST(HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR) AC_CONFIG_FILES([Makefile src/Makefile doc/man/Makefile share/setup.nsi share/qt/Info.plist test/config.ini]) AC_CONFIG_FILES([contrib/devtools/split-debug.sh],[chmod +x contrib/devtools/split-debug.sh]) diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 03541f8e2f..0cf38d1bc4 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -523,7 +523,7 @@ QT_BASE_TLD = $(shell tar tf $(QT_BASE_PATH) --exclude='*/*') bitcoin_qt_apk: FORCE mkdir -p $(APK_LIB_DIR) - cp $(dir $(lastword $(CC)))../sysroot/usr/lib/$(host_alias)/libc++_shared.so $(APK_LIB_DIR) + cp $(dir $(lastword $(CC)))../sysroot/usr/lib/$(NDK_LIBCXX_DIR)/libc++_shared.so $(APK_LIB_DIR) tar xf $(QT_BASE_PATH) -C qt/android/src/ $(QT_BASE_TLD)src/android/jar/src --strip-components=5 tar xf $(QT_BASE_PATH) -C qt/android/src/ $(QT_BASE_TLD)src/android/java/src --strip-components=5 patch -i ../depends/patches/qt/fix_android_get_drawable.patch qt/android/src/org/qtproject/qt5/android/QtActivityDelegate.java From 16fa0a322878aa66798d19b1dae6b8cf28078da1 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Thu, 13 Apr 2023 22:32:22 -0400 Subject: [PATCH 15/19] ci: build android 32 bit apk --- .cirrus.yml | 21 +++++++++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 1 + ci/test/00_setup_env_android32.sh | 25 +++++++++++++++++++++++++ src/qml/README.md | 1 + 4 files changed, 48 insertions(+) create mode 100755 ci/test/00_setup_env_android32.sh diff --git a/.cirrus.yml b/.cirrus.yml index 4295d0adba..efc74b9ee0 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -386,3 +386,24 @@ task: - cp src/qt/android/build/outputs/apk/debug/android-debug.apk ${CIRRUS_WORKING_DIR}/unsecure_android.apk unsecure_android_apk_artifacts: path: "unsecure_android.apk" + +task: + name: 'ARM32 Android APK [jammy]' + alias: android32 + << : *CONTAINER_DEPENDS_TEMPLATE + container: + image: ubuntu:jammy + android_sdk_cache: + folder: "depends/SDKs/android" + fingerprint_key: "ANDROID_API_LEVEL=28 ANDROID_BUILD_TOOLS_VERSION=28.0.3 ANDROID_NDK_VERSION=23.2.8568313" + depends_sources_cache: + folder: "depends/sources" + fingerprint_script: git rev-parse HEAD:depends/packages + << : *MAIN_TEMPLATE + env: + << : *CIRRUS_EPHEMERAL_WORKER_TEMPLATE_ENV + FILE_ENV: "./ci/test/00_setup_env_android32.sh" + copy_artifacts_script: + - cp src/qt/android/build/outputs/apk/debug/android-debug.apk ${CIRRUS_WORKING_DIR}/unsecure_android_32bit.apk + unsecure_android_apk_32bit_artifacts: + path: "unsecure_android_32bit.apk" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index faac02e0c6..a9caeec7b2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -49,4 +49,5 @@ Links for Windows and macOS build artifacts. Replace with the assigned pull [![Intel macOS](https://img.shields.io/badge/OS-Intel%20macOS-green)](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/macos/unsecure_mac_gui.zip?branch=pull/) [![Apple Silicon macOS](https://img.shields.io/badge/OS-Apple%20Silicon%20macOS-green)](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/macos_arm64/unsecure_mac_arm64_gui.zip?branch=pull/) [![ARM64 Android](https://img.shields.io/badge/OS-Android-green)](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/android/unsecure_android_apk.zip?branch=pull/) +[![ARM32 Android](https://img.shields.io/badge/OS-Android%2032bit-green)](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/android32/unsecure_android_32bit_apk.zip?branch=pull/) --> diff --git a/ci/test/00_setup_env_android32.sh b/ci/test/00_setup_env_android32.sh new file mode 100755 index 0000000000..78447c5cd6 --- /dev/null +++ b/ci/test/00_setup_env_android32.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# +# 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. + +export LC_ALL=C.UTF-8 + +export HOST=armv7a-linux-android +export PACKAGES="unzip openjdk-8-jdk gradle" +export CONTAINER_NAME=ci_android +export CI_IMAGE_NAME_TAG="ubuntu:jammy" + +export RUN_UNIT_TESTS=false +export RUN_FUNCTIONAL_TESTS=false + +export ANDROID_API_LEVEL=28 +export ANDROID_BUILD_TOOLS_VERSION=28.0.3 +export ANDROID_NDK_VERSION=23.2.8568313 +export ANDROID_TOOLS_URL=https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip +export ANDROID_HOME="${DEPENDS_DIR}/SDKs/android" +export ANDROID_NDK_HOME="${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}" +export DEP_OPTS="ANDROID_SDK=${ANDROID_HOME} ANDROID_NDK=${ANDROID_NDK_HOME} ANDROID_API_LEVEL=${ANDROID_API_LEVEL} ANDROID_TOOLCHAIN_BIN=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/" + +export BITCOIN_CONFIG="--disable-tests --enable-gui-tests --disable-bench --disable-fuzz-binary --without-utils --without-libs --without-daemon" diff --git a/src/qml/README.md b/src/qml/README.md index 11f3850ca9..e5904a5fd2 100644 --- a/src/qml/README.md +++ b/src/qml/README.md @@ -9,6 +9,7 @@ Unsecure CI artifacts are available for local testing of the master branch, avoi - for Intel macOS: [`unsecure_mac_gui.zip`](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/macos/unsecure_mac_gui.zip) - for Apple Silicon macOS: [`unsecure_mac_arm64_gui.zip`](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/macos_arm64/unsecure_mac_arm64_gui.zip) - for ARM64 Android: [`unsecure_android_apk.zip`](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/android/unsecure_android_apk.zip) +- for ARM32 Android: [`unsecure_android_32bit_apk.zip`](https://api.cirrus-ci.com/v1/artifact/github/bitcoin-core/gui-qml/android32/unsecure_android_32bit_apk.zip) Note: For Apple Silicon macOS machines, the binary must be signed before it can be ran. To apply a signature, run the following on the unzipped CI artifact: From 9c81e9b14efed25eec19aabb79f42e476fee0c3f Mon Sep 17 00:00:00 2001 From: Jarol Rodriguez Date: Wed, 19 Apr 2023 01:11:46 -0400 Subject: [PATCH 16/19] qml: add looping animation to Estimating state --- src/qml/components/BlockClock.qml | 89 ++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/qml/components/BlockClock.qml b/src/qml/components/BlockClock.qml index c7dfb11ba5..a8c1c98728 100644 --- a/src/qml/components/BlockClock.qml +++ b/src/qml/components/BlockClock.qml @@ -25,7 +25,11 @@ Item { property int headerSize: 32 property bool connected: nodeModel.numOutboundPeers > 0 property bool synced: nodeModel.verificationProgress > 0.999 + property string syncProgress: formatProgressPercentage(nodeModel.verificationProgress * 100) property bool paused: false + property var syncState: formatRemainingSyncTime(nodeModel.remainingSyncTime) + property string syncTime: syncState.text + property bool estimating: syncState.estimating activeFocusOnTab: true @@ -94,15 +98,44 @@ Item { Label { id: subText anchors.top: mainText.bottom + property bool estimating: root.estimating anchors.horizontalCenter: root.horizontalCenter font.family: "Inter" font.styleName: "Semi Bold" font.pixelSize: dial.width * (9/100) color: Theme.color.neutral4 - Behavior on color { - ColorAnimation { duration: 150 } + Component.onCompleted: { + colorChanged.connect(function() { + if (!subText.estimating) { + themeChange.restart(); + } + }); + + estimatingChanged.connect(function() { + if (subText.estimating) { + estimatingTime.start(); + } else { + estimatingTime.stop(); + } + }); + + subText.estimatingChanged(subText.estimating); + } + + ColorAnimation on color{ + id: themeChange + target: subText + duration: 150 } + + SequentialAnimation { + id: estimatingTime + loops: Animation.Infinite + ColorAnimation { target: subText; property: "color"; from: subText.color; to: Theme.color.neutral6; duration: 1000 } + ColorAnimation { target: subText; property: "color"; from: Theme.color.neutral6; to: subText.color; duration: 1000 } + } + } PeersIndicator { @@ -140,17 +173,17 @@ Item { name: "IBD"; when: !synced && !paused && connected PropertyChanges { target: root - header: formatProgressPercentage(nodeModel.verificationProgress * 100) - subText: formatRemainingSyncTime(nodeModel.remainingSyncTime) + header: root.syncProgress + subText: root.syncTime } }, - State { name: "BLOCKCLOCK"; when: synced && !paused && connected PropertyChanges { target: root header: Number(nodeModel.blockTipHeight).toLocaleString(Qt.locale(), 'f', 0) subText: "Blocktime" + estimating: false } }, @@ -161,6 +194,7 @@ Item { header: "Paused" headerSize: dial.width * (3/25) subText: "Tap to resume" + estimating: false } PropertyChanges { target: bitcoinIcon @@ -179,6 +213,7 @@ Item { header: "Connecting" headerSize: dial.width * (3/25) subText: "Please wait" + estimating: false } PropertyChanges { target: bitcoinIcon @@ -212,29 +247,55 @@ Item { minutes %= 1440; var hours = Math.floor(minutes / 60); minutes %= 60; + var result = ""; + var estimatingStatus = false; if (weeks > 0) { - return "~" + weeks + (weeks === 1 ? " week" : " weeks") + " left"; + return { + text: "~" + weeks + (weeks === 1 ? " week" : " weeks") + " left", + estimating: false + }; } if (days > 0) { - return "~" + days + (days === 1 ? " day" : " days") + " left"; + return { + text: "~" + days + (days === 1 ? " day" : " days") + " left", + estimating: false + }; } if (hours >= 5) { - return "~" + hours + (hours === 1 ? " hour" : " hours") + " left"; + return { + text: "~" + hours + (hours === 1 ? " hour" : " hours") + " left", + estimating: false + }; } if (hours > 0) { - return "~" + hours + "h " + minutes + "m" + " left"; + return { + text: "~" + hours + "h " + minutes + "m" + " left", + estimating: false + }; } if (minutes >= 5) { - return "~" + minutes + (minutes === 1 ? " minute" : " minutes") + " left"; + return { + text: "~" + minutes + (minutes === 1 ? " minute" : " minutes") + " left", + estimating: false + }; } if (minutes > 0) { - return "~" + minutes + "m " + seconds + "s" + " left"; + return { + text: "~" + minutes + "m " + seconds + "s" + " left", + estimating: false + }; } if (seconds > 0) { - return "~" + seconds + (seconds === 1 ? " second" : " seconds") + " left"; + return { + text: "~" + seconds + (seconds === 1 ? " second" : " seconds") + " left", + estimating: false + }; + } else { + return { + text: "Estimating", + estimating: true + }; } - - return "Estimating"; } } From efe8b9643d566c93fe3e59f9bf79b925370d70e6 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Thu, 20 Apr 2023 23:36:28 -0400 Subject: [PATCH 17/19] qml: remember network traffic graph scale --- src/qml/pages/node/NetworkTraffic.qml | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/qml/pages/node/NetworkTraffic.qml b/src/qml/pages/node/NetworkTraffic.qml index 16ec8d9593..c85bbf1b25 100644 --- a/src/qml/pages/node/NetworkTraffic.qml +++ b/src/qml/pages/node/NetworkTraffic.qml @@ -5,6 +5,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.0 import "../../controls" import "../../components" @@ -12,7 +13,13 @@ import org.bitcoincore.qt 1.0 InformationPage { id: root - property int maxSamples: 300 + property int trafficGraphScale: 300 + + Settings { + id: settings + property alias trafficGraphScale: root.trafficGraphScale + } + bannerActive: false bold: true headerText: qsTr("Network Traffic") @@ -41,10 +48,11 @@ InformationPage { anchors.centerIn: parent anchors.margins: 3 spacing: 5 + ToggleButton { text: qsTr("5 min") autoExclusive: true - checked: true + checked: root.trafficGraphScale === 300 bgRadius: 3 textColor: Theme.color.neutral9 textActiveColor: Theme.color.neutral0 @@ -52,14 +60,15 @@ InformationPage { bgDefaultColor: Theme.color.neutral3 onClicked: { - root.maxSamples = 300 - networkTrafficTower.updateFilterWindowSize(root.maxSamples / 10) + root.trafficGraphScale = 300 + networkTrafficTower.updateFilterWindowSize(root.trafficGraphScale / 10) } } ToggleButton { text: qsTr("1 hour") autoExclusive: true + checked: root.trafficGraphScale === 3600 bgRadius: 3 textColor: Theme.color.neutral9 textActiveColor: Theme.color.neutral0 @@ -67,14 +76,15 @@ InformationPage { bgDefaultColor: Theme.color.neutral3 onClicked: { - root.maxSamples = 3600 - networkTrafficTower.updateFilterWindowSize(root.maxSamples / 10) + root.trafficGraphScale = 3600 + networkTrafficTower.updateFilterWindowSize(root.trafficGraphScale / 10) } } ToggleButton { text: qsTr("12 hours") autoExclusive: true + checked: root.trafficGraphScale === 3600 * 12 bgRadius: 3 textColor: Theme.color.neutral9 textActiveColor: Theme.color.neutral0 @@ -82,14 +92,15 @@ InformationPage { bgDefaultColor: Theme.color.neutral3 onClicked: { - root.maxSamples = 3600 * 12 - networkTrafficTower.updateFilterWindowSize(root.maxSamples / 10) + root.trafficGraphScale = 3600 * 12 + networkTrafficTower.updateFilterWindowSize(root.trafficGraphScale / 10) } } ToggleButton { text: qsTr("1 day") autoExclusive: true + checked: root.trafficGraphScale === 3600 * 24 bgRadius: 3 textColor: Theme.color.neutral9 textActiveColor: Theme.color.neutral0 @@ -97,8 +108,8 @@ InformationPage { bgDefaultColor: Theme.color.neutral3 onClicked: { - root.maxSamples = 3600 * 24 - networkTrafficTower.updateFilterWindowSize(root.maxSamples / 10) + root.trafficGraphScale = 3600 * 24 + networkTrafficTower.updateFilterWindowSize(root.trafficGraphScale / 10) } } } @@ -118,7 +129,7 @@ InformationPage { fillColor: Theme.color.green lineColor: Theme.color.green markerLineColor: Theme.color.neutral2 - maxSamples: root.maxSamples + maxSamples: root.trafficGraphScale maxValue: networkTrafficTower.maxReceivedRateBps valueList: networkTrafficTower.receivedRateList maxRateBps: networkTrafficTower.maxReceivedRateBps @@ -137,7 +148,7 @@ InformationPage { fillColor: Theme.color.blue lineColor: Theme.color.blue markerLineColor: Theme.color.neutral2 - maxSamples: root.maxSamples + maxSamples: root.trafficGraphScale maxValue: networkTrafficTower.maxSentRateBps valueList: networkTrafficTower.sentRateList maxRateBps: networkTrafficTower.maxSentRateBps From 4a479a25a2453afc6c15b593454a93fbe1559b0c Mon Sep 17 00:00:00 2001 From: jarolrod Date: Fri, 21 Apr 2023 22:27:26 -0400 Subject: [PATCH 18/19] qml: add proper versioning to About page --- src/qml/components/AboutOptions.qml | 2 +- src/qml/nodemodel.h | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/qml/components/AboutOptions.qml b/src/qml/components/AboutOptions.qml index cf82bf101a..f0f118f18d 100644 --- a/src/qml/components/AboutOptions.qml +++ b/src/qml/components/AboutOptions.qml @@ -51,7 +51,7 @@ ColumnLayout { header: qsTr("Version") actionItem: ExternalLink { parentState: versionLink.state - description: "v22.99.0-1e7564eca8a6" + description: nodeModel.fullClientVersion link: "https://bitcoin.org/en/download" iconSource: "image://images/caret-right" iconWidth: 18 diff --git a/src/qml/nodemodel.h b/src/qml/nodemodel.h index 2e4575f3ea..522e6a2995 100644 --- a/src/qml/nodemodel.h +++ b/src/qml/nodemodel.h @@ -7,10 +7,12 @@ #include #include +#include #include #include +#include QT_BEGIN_NAMESPACE class QTimerEvent; @@ -25,6 +27,7 @@ class NodeModel : public QObject { Q_OBJECT Q_PROPERTY(int blockTipHeight READ blockTipHeight NOTIFY blockTipHeightChanged) + Q_PROPERTY(QString fullClientVersion READ fullClientVersion CONSTANT) Q_PROPERTY(int numOutboundPeers READ numOutboundPeers NOTIFY numOutboundPeersChanged) Q_PROPERTY(int maxNumOutboundPeers READ maxNumOutboundPeers CONSTANT) Q_PROPERTY(int remainingSyncTime READ remainingSyncTime NOTIFY remainingSyncTimeChanged) @@ -36,6 +39,7 @@ class NodeModel : public QObject int blockTipHeight() const { return m_block_tip_height; } void setBlockTipHeight(int new_height); + QString fullClientVersion() const { return QString::fromStdString(FormatFullVersion()); } int numOutboundPeers() const { return m_num_outbound_peers; } void setNumOutboundPeers(int new_num); int maxNumOutboundPeers() const { return m_max_num_outbound_peers; } From 83ed852fd4f654b5a107a16739d8f981f0be4469 Mon Sep 17 00:00:00 2001 From: jarolrod Date: Sat, 22 Apr 2023 14:24:08 -0400 Subject: [PATCH 19/19] android: switch to main --- src/qt/android/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qt/android/AndroidManifest.xml b/src/qt/android/AndroidManifest.xml index 8d2c1ac901..b7a4bf0231 100644 --- a/src/qt/android/AndroidManifest.xml +++ b/src/qt/android/AndroidManifest.xml @@ -24,7 +24,7 @@ - +