diff --git a/qml/bitcoin_qml.qrc b/qml/bitcoin_qml.qrc
index e0ad8a4ea0..7f422368eb 100644
--- a/qml/bitcoin_qml.qrc
+++ b/qml/bitcoin_qml.qrc
@@ -10,7 +10,6 @@
components/BitcoinAmountDisplayField.qml
components/BitcoinAmountInputField.qml
components/BlockClock.qml
- components/BlockClockDisplayMode.qml
components/BlockCounter.qml
components/ConnectionOptions.qml
components/ConnectionSettings.qml
@@ -33,12 +32,14 @@
components/OptionPopup.qml
components/PeersIndicator.qml
components/ProxySettings.qml
+ components/SettingsPageContainer.qml
components/SettingsRestartNotice.qml
+ components/SettingsSidebar.qml
+ components/SettingsView.qml
components/StorageLocations.qml
components/Separator.qml
components/StorageOptions.qml
components/StorageSettings.qml
- components/ThemeSettings.qml
components/ToastBanner.qml
components/ToastPopup.qml
components/TotalBytesIndicator.qml
@@ -50,7 +51,6 @@
components/LabeledValueField.qml
components/MultipleRecipientsSummary.qml
components/SingleRecipientSummary.qml
- components/WalletSettings.qml
components/WalletMigrationPopup.qml
components/WalletPassphrasePopup.qml
controls/AddWalletButton.qml
@@ -68,11 +68,15 @@
controls/EditableKeyValueRow.qml
controls/ExternalLink.qml
controls/FocusBorder.qml
+ controls/FormRow.qml
+ controls/FormSection.qml
controls/Header.qml
controls/Icon.qml
controls/IconButton.qml
controls/InformationPage.qml
controls/KeyValueRow.qml
+ controls/LinkRow.qml
+ controls/ListRow.qml
controls/LabeledTextInput.qml
controls/LabeledCoinControlButton.qml
controls/NavButton.qml
@@ -83,7 +87,9 @@
controls/OptionSwitch.qml
controls/OutlineButton.qml
controls/PageIndicator.qml
+ controls/PageHeading.qml
controls/PageStack.qml
+ controls/PopupPicker.qml
controls/ProgressIndicator.qml
controls/ProxyLocationInput.qml
controls/QRImage.qml
@@ -93,13 +99,16 @@
controls/SegmentedPicker.qml
controls/Setting.qml
controls/SettingsHeader.qml
+ controls/SettingsPage.qml
controls/Skeleton.qml
controls/SpinningIndicator.qml
controls/TextButton.qml
+ controls/TextFieldRow.qml
controls/Theme.qml
controls/ToggleButton.qml
controls/utils.js
controls/ValueInput.qml
+ controls/ValueRow.qml
controls/WalletTypeListItem.qml
pages/initerrormessage.qml
pages/MainWindow.qml
@@ -107,9 +116,7 @@
pages/node/BannedPeers.qml
pages/node/CommandConsole.qml
pages/node/NetworkTraffic.qml
- pages/node/MempoolInformationSettings.qml
pages/node/NodeRunner.qml
- pages/node/NodeSettings.qml
pages/node/Peers.qml
pages/node/PeerDetails.qml
pages/node/Shutdown.qml
@@ -121,19 +128,24 @@
pages/onboarding/OnboardingStrengthen.qml
pages/onboarding/OnboardingWizard.qml
pages/settings/SettingsAbout.qml
- pages/settings/SettingsDisplayUnit.qml
pages/settings/SettingsLanguage.qml
- pages/settings/SettingsWindowBehavior.qml
- pages/settings/SettingsBlockClockDisplayMode.qml
pages/settings/SettingsConnection.qml
pages/settings/SettingsDebugLog.qml
pages/settings/SettingsDesignSystem.qml
pages/settings/SettingsDeveloper.qml
- pages/settings/SettingsDisplay.qml
pages/settings/SettingsProxy.qml
- pages/settings/SettingsWallet.qml
pages/settings/SettingsStorage.qml
- pages/settings/SettingsTheme.qml
+ pages/settings/AboutSettingsPage.qml
+ pages/settings/ConnectionSettingsPage.qml
+ pages/settings/DisplaySettingsPage.qml
+ pages/settings/ExternalSignerSettingsPage.qml
+ pages/settings/MempoolSettingsPage.qml
+ pages/settings/NetworkTrafficSettingsPage.qml
+ pages/settings/ProxySettingsPage.qml
+ pages/settings/RpcConsoleSettingsPage.qml
+ pages/settings/StorageSettingsPage.qml
+ pages/settings/WalletSectionPage.qml
+ pages/settings/WindowBehaviorSettingsPage.qml
pages/wallet/Activity.qml
pages/wallet/ActivityDetails.qml
pages/wallet/ActivityTransactionVisuals.qml
@@ -163,7 +175,6 @@
pages/wallet/SignVerifyMessage.qml
pages/wallet/WalletBadge.qml
pages/wallet/WalletPasswordSettings.qml
- pages/wallet/WalletSettings.qml
pages/wallet/WalletSelect.qml
diff --git a/qml/components/BlockClockDisplayMode.qml b/qml/components/BlockClockDisplayMode.qml
deleted file mode 100644
index cb3d4797a0..0000000000
--- a/qml/components/BlockClockDisplayMode.qml
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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"
-
-ColumnLayout {
- id: root
- spacing: 15
-
- ButtonGroup {
- id: group
- }
-
- OptionButton {
- Layout.fillWidth: true
- ButtonGroup.group: group
- text: qsTr("Compact")
- description: qsTr("For personal use on a computer or smartphone.")
- image: "image://images/blockclock-size-compact"
- 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.")
- image: "image://images/blockclock-size-showcase"
- checked: Theme.blockclocksize == (1/2)
- onClicked: {
- Theme.blockclocksize = (1/2)
- }
- }
-}
diff --git a/qml/components/SettingsPageContainer.qml b/qml/components/SettingsPageContainer.qml
new file mode 100644
index 0000000000..bc5d1cdf6f
--- /dev/null
+++ b/qml/components/SettingsPageContainer.qml
@@ -0,0 +1,118 @@
+// Copyright (c) 2026 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 "../controls"
+
+Page {
+ id: root
+
+ readonly property string currentSectionId: internal.currentSectionId
+ readonly property int depth: internal.currentStack ? internal.currentStack.depth : 0
+ readonly property bool canGoBack: internal.currentStack ? internal.currentStack.canGoBack : false
+ readonly property var currentItem: internal.currentStack ? internal.currentStack.currentItem : null
+ readonly property var stack: internal.currentStack
+
+ signal sectionChanged(string sectionId)
+
+ function showSection(sectionId, page, properties) {
+ if (!page) {
+ if (internal.currentStack) internal.currentStack.visible = false
+ internal.currentStack = null
+ internal.currentSectionId = ""
+ root.sectionChanged("")
+ return null
+ }
+
+ if (sectionId === root.currentSectionId && internal.currentStack) {
+ return internal.currentStack.currentItem
+ }
+
+ let nextStack = internal.sectionStacks[sectionId]
+ if (!nextStack) {
+ nextStack = sectionStackComponent.createObject(stackHost, {
+ "sectionId": sectionId
+ })
+ if (!nextStack) return null
+
+ const nextSectionStacks = {}
+ for (const cachedSectionId in internal.sectionStacks) {
+ nextSectionStacks[cachedSectionId] = internal.sectionStacks[cachedSectionId]
+ }
+ nextSectionStacks[sectionId] = nextStack
+ internal.sectionStacks = nextSectionStacks
+ nextStack.push(page, properties || {}, StackView.Immediate)
+ }
+
+ if (internal.currentStack) internal.currentStack.visible = false
+ internal.currentStack = nextStack
+ internal.currentSectionId = sectionId
+ nextStack.visible = true
+ root.sectionChanged(sectionId)
+ return nextStack.currentItem
+ }
+
+ function push(page, properties) {
+ if (!page || !internal.currentStack) return null
+ return internal.currentStack.push(page, properties || {})
+ }
+
+ function pop() {
+ if (!internal.currentStack || !internal.currentStack.canGoBack) return null
+ return internal.currentStack.pop()
+ }
+
+ function sectionDepth(sectionId) {
+ const sectionStack = internal.sectionStacks[sectionId]
+ return sectionStack ? sectionStack.depth : 0
+ }
+
+ function resetSection(sectionId) {
+ const sectionStack = internal.sectionStacks[sectionId]
+ if (!sectionStack || sectionStack.depth <= 1) return
+ sectionStack.pop(null, StackView.Immediate)
+ }
+
+ function clear() {
+ const sectionStacks = internal.sectionStacks
+ if (internal.currentStack) internal.currentStack.visible = false
+ internal.currentStack = null
+ internal.currentSectionId = ""
+ internal.sectionStacks = ({})
+
+ for (const sectionId in sectionStacks) {
+ sectionStacks[sectionId].clear(StackView.Immediate)
+ sectionStacks[sectionId].destroy()
+ }
+ root.sectionChanged("")
+ }
+
+ background: null
+ clip: true
+
+ QtObject {
+ id: internal
+ property string currentSectionId: ""
+ property var currentStack: null
+ property var sectionStacks: ({})
+ }
+
+ Item {
+ id: stackHost
+ anchors.fill: parent
+ }
+
+ Component {
+ id: sectionStackComponent
+
+ PageStack {
+ required property string sectionId
+ objectName: "settingsNavigationStack_" + sectionId
+ anchors.fill: parent
+ visible: false
+ }
+ }
+}
diff --git a/qml/components/SettingsSidebar.qml b/qml/components/SettingsSidebar.qml
new file mode 100644
index 0000000000..83b875001d
--- /dev/null
+++ b/qml/components/SettingsSidebar.qml
@@ -0,0 +1,166 @@
+// Copyright (c) 2026 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 org.bitcoincore.qt 1.0
+
+import "../controls"
+
+Control {
+ id: root
+
+ property var model: []
+ property var groupTitles: ({})
+ property string currentSectionId: ""
+ property int rowHeight: 36
+ property int groupSpacing: 16
+ property int groupTitleHeight: 25
+ property int cornerRadius: 8
+ property color selectedBackgroundColor: Qt.rgba(Theme.color.orange.r, Theme.color.orange.g, Theme.color.orange.b, 0.15)
+ property color hoverBackgroundColor: Theme.color.neutral2
+ readonly property var visibleSections: root.filteredSections()
+ readonly property alias listView: sectionList
+
+ signal sectionActivated(string sectionId)
+
+ function filteredSections() {
+ const result = []
+ if (!root.model) return result
+
+ if (root.model.count !== undefined && root.model.get !== undefined) {
+ for (let index = 0; index < root.model.count; ++index) {
+ const section = root.model.get(index)
+ if (section.visible !== false) result.push(section)
+ }
+ return result
+ }
+
+ for (let index = 0; index < root.model.length; ++index) {
+ const section = root.model[index]
+ if (section.visible !== false) result.push(section)
+ }
+ return result
+ }
+
+ function titleForGroup(groupId) {
+ if (!root.groupTitles) return ""
+ const title = root.groupTitles[groupId]
+ return title === undefined || title === null ? "" : String(title)
+ }
+
+ background: null
+ padding: 0
+ implicitWidth: 190
+ implicitHeight: sectionList.contentHeight
+
+ contentItem: ListView {
+ id: sectionList
+ objectName: "settingsSidebarList"
+ model: root.visibleSections
+ clip: true
+ boundsBehavior: Flickable.StopAtBounds
+ keyNavigationEnabled: true
+
+ delegate: Item {
+ id: delegate
+ required property var modelData
+ required property int index
+
+ readonly property bool startsGroup: delegate.index === 0
+ || root.visibleSections[delegate.index - 1].group !== delegate.modelData.group
+ readonly property string groupTitle: delegate.startsGroup
+ ? root.titleForGroup(delegate.modelData.group)
+ : ""
+ readonly property bool showsGroupTitle: delegate.groupTitle.length > 0
+ readonly property int groupOffset: (delegate.showsGroupTitle ? root.groupTitleHeight : 0)
+ + (delegate.startsGroup && delegate.index > 0 ? root.groupSpacing : 0)
+
+ width: sectionList.width
+ height: root.rowHeight + delegate.groupOffset
+
+ CoreText {
+ objectName: delegate.showsGroupTitle
+ ? "settingsSidebarGroup_" + delegate.modelData.group
+ : ""
+ visible: delegate.showsGroupTitle
+ anchors {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ topMargin: delegate.index > 0 ? root.groupSpacing : 0
+ leftMargin: 10
+ rightMargin: 10
+ }
+ height: Theme.text.caption.lineHeight
+ text: delegate.groupTitle
+ color: Theme.color.neutral6
+ font.family: Theme.text.caption.family
+ font.pixelSize: Theme.text.caption.pixelSize
+ fontStyleName: "Semi Bold"
+ lineHeight: Theme.text.caption.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ Accessible.ignored: true
+ }
+
+ AbstractButton {
+ id: button
+ objectName: delegate.modelData.objectName || "settingsSidebar_" + delegate.modelData.id
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ height: root.rowHeight
+ hoverEnabled: AppMode.isDesktop
+ focusPolicy: Qt.TabFocus
+ leftPadding: 10
+ rightPadding: 10
+ Accessible.name: delegate.modelData.label
+ Accessible.role: Accessible.ListItem
+
+ onClicked: root.sectionActivated(delegate.modelData.id)
+
+ background: Rectangle {
+ radius: root.cornerRadius
+ color: delegate.modelData.id === root.currentSectionId
+ ? root.selectedBackgroundColor
+ : button.hovered
+ ? root.hoverBackgroundColor
+ : "transparent"
+
+ FocusBorder {
+ visible: button.visualFocus
+ borderRadius: root.cornerRadius + 2
+ topMargin: -2
+ bottomMargin: -2
+ leftMargin: -2
+ rightMargin: -2
+ }
+ }
+
+ contentItem: CoreText {
+ text: delegate.modelData.label
+ color: delegate.modelData.id === root.currentSectionId
+ ? Theme.color.orange
+ : Theme.color.neutral9
+ font: Theme.text.description.font
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+ }
+
+ HoverHandler {
+ cursorShape: Qt.PointingHandCursor
+ }
+ }
+ }
+ }
+}
diff --git a/qml/components/SettingsView.qml b/qml/components/SettingsView.qml
new file mode 100644
index 0000000000..fd0119fdae
--- /dev/null
+++ b/qml/components/SettingsView.qml
@@ -0,0 +1,359 @@
+// Copyright (c) 2026 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"
+import "../pages/settings" as SettingsPages
+import "../pages/wallet" as WalletPages
+
+Page {
+ id: root
+ objectName: "settingsView"
+
+ signal doneClicked()
+ signal selectWalletRequested()
+ signal receiveRequested()
+
+ property bool showDoneButton: true
+ property string selectedSectionId: ""
+ property int sidebarWidth: 286
+ readonly property alias sidebar: sidebar
+ readonly property alias pageContainer: pageContainer
+ readonly property var groupTitles: ({
+ "wallet": qsTr("Wallet"),
+ "general": qsTr("General"),
+ "network": qsTr("Network"),
+ "advanced": qsTr("Advanced")
+ })
+ readonly property var sections: [
+ {
+ id: "wallet",
+ label: qsTr("Wallet settings"),
+ group: "wallet",
+ visible: AppMode.walletEnabled,
+ pageComponent: walletPage
+ },
+ {
+ id: "external-signer",
+ label: qsTr("External signer"),
+ group: "wallet",
+ visible: AppMode.walletEnabled,
+ pageComponent: externalSignerPage
+ },
+ {
+ id: "display",
+ label: qsTr("Display"),
+ group: "general",
+ pageComponent: displayPage
+ },
+ {
+ id: "window-behavior",
+ label: qsTr("Window behavior"),
+ group: "general",
+ visible: AppMode.isDesktop,
+ pageComponent: windowBehaviorPage
+ },
+ {
+ id: "storage",
+ label: qsTr("Storage"),
+ group: "general",
+ pageComponent: storagePage
+ },
+ {
+ id: "connection",
+ label: qsTr("Connection"),
+ group: "network",
+ pageComponent: connectionPage
+ },
+ {
+ id: "network-traffic",
+ label: qsTr("Network traffic"),
+ group: "network",
+ pageComponent: networkTrafficPage
+ },
+ {
+ id: "mempool",
+ label: qsTr("Mempool information"),
+ group: "advanced",
+ visible: nodeModel.mempoolInformationAvailable,
+ pageComponent: mempoolPage
+ },
+ {
+ id: "rpc-console",
+ label: qsTr("RPC console"),
+ group: "advanced",
+ visible: AppMode.isDesktop,
+ pageComponent: rpcConsolePage
+ },
+ {
+ id: "debug-log",
+ label: qsTr("Debug log"),
+ group: "advanced",
+ pageComponent: debugLogPage
+ },
+ {
+ id: "about",
+ label: qsTr("About"),
+ group: "about",
+ pageComponent: aboutPage
+ }
+ ]
+
+ function sectionForId(sectionId) {
+ for (let index = 0; index < root.sections.length; ++index) {
+ if (root.sections[index].id === sectionId) return root.sections[index]
+ }
+ return null
+ }
+
+ function componentForSection(sectionId) {
+ const section = root.sectionForId(sectionId)
+ return section ? section.pageComponent : null
+ }
+
+ function sectionIsVisible(sectionId) {
+ const section = root.sectionForId(sectionId)
+ return section !== null && section.visible !== false
+ }
+
+ function firstVisibleSectionId() {
+ for (let index = 0; index < root.sections.length; ++index) {
+ if (root.sections[index].visible !== false) return root.sections[index].id
+ }
+ return ""
+ }
+
+ function selectSection(sectionId, forceReload) {
+ const resolvedId = root.sectionIsVisible(sectionId) ? sectionId : root.firstVisibleSectionId()
+ if (resolvedId.length === 0) {
+ root.selectedSectionId = ""
+ pageContainer.clear()
+ return
+ }
+
+ if (forceReload === true) pageContainer.clear()
+ root.selectedSectionId = resolvedId
+ pageContainer.showSection(resolvedId, root.componentForSection(resolvedId))
+ }
+
+ function ensureVisibleSelection() {
+ if (!root.sectionIsVisible(root.selectedSectionId)) root.selectSection(root.firstVisibleSectionId())
+ }
+
+ function openWalletSettings() {
+ root.selectSection("wallet")
+ }
+
+ function openWalletAddressHistory() {
+ if (!walletController.isWalletLoaded || !walletController.selectedWallet) return
+ walletController.selectedWallet.addressListModel.refresh()
+ root.selectSection("wallet", true)
+ pageContainer.push(addressListPage)
+ }
+
+ background: null
+ padding: 0
+
+ onSectionsChanged: ensureVisibleSelection()
+ onVisibleChanged: {
+ if (visible) root.selectSection(root.selectedSectionId)
+ }
+
+ Component.onCompleted: root.selectSection(
+ root.sectionIsVisible(root.selectedSectionId) ? root.selectedSectionId : root.firstVisibleSectionId())
+
+ Connections {
+ target: typeof walletController !== "undefined" ? walletController : null
+
+ function onSelectedWalletChanged() {
+ pageContainer.resetSection("wallet")
+ }
+
+ function onIsWalletLoadedChanged() {
+ if (!walletController.isWalletLoaded) pageContainer.resetSection("wallet")
+ }
+ }
+
+ contentItem: RowLayout {
+ spacing: 0
+
+ Rectangle {
+ id: sidebarSurface
+ objectName: "settingsSidebarSurface"
+ Layout.preferredWidth: root.sidebarWidth
+ Layout.minimumWidth: root.sidebarWidth
+ Layout.maximumWidth: root.sidebarWidth
+ Layout.fillHeight: true
+ color: Theme.color.neutral1
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+
+ ColumnLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 20
+ anchors.rightMargin: 20
+ anchors.topMargin: 20
+ anchors.bottomMargin: 16
+ spacing: 0
+
+ CoreText {
+ objectName: "settingsSidebarHeading"
+ Layout.fillWidth: true
+ Layout.leftMargin: 10
+ Layout.rightMargin: 10
+ Layout.bottomMargin: 24
+ text: qsTr("Settings")
+ color: Theme.color.neutral9
+ font: Theme.text.display.font
+ lineHeight: Theme.text.display.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ SettingsSidebar {
+ id: sidebar
+ objectName: "settingsSidebar"
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ model: root.sections
+ groupTitles: root.groupTitles
+ currentSectionId: root.selectedSectionId
+ onSectionActivated: function(sectionId) { root.selectSection(sectionId) }
+ }
+
+ NavButton {
+ objectName: "settingsDoneButton"
+ visible: root.showDoneButton
+ text: qsTr("Done")
+ Layout.alignment: Qt.AlignHCenter
+ Layout.bottomMargin: 20
+ onClicked: root.doneClicked()
+ }
+ }
+ }
+
+ SettingsPageContainer {
+ id: pageContainer
+ objectName: "settingsPageContainer"
+ Layout.minimumWidth: 0
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+ }
+
+ Component {
+ id: walletPage
+
+ SettingsPages.WalletSectionPage {
+ onSelectWalletRequested: root.selectWalletRequested()
+ onPasswordRequested: pageContainer.push(walletPasswordPage, {
+ "updating": walletController.selectedWallet.isEncrypted
+ })
+ onSignVerifyMessageRequested: pageContainer.push(signVerifyPage)
+ onAddressesRequested: {
+ if (!walletController.isWalletLoaded || !walletController.selectedWallet) return
+ walletController.selectedWallet.addressListModel.refresh()
+ pageContainer.push(addressListPage)
+ }
+ }
+ }
+
+ Component {
+ id: walletPasswordPage
+
+ WalletPages.WalletPasswordSettings {
+ onBack: pageContainer.pop()
+ onSaved: pageContainer.pop()
+ }
+ }
+
+ Component {
+ id: signVerifyPage
+
+ WalletPages.SignVerifyMessage {
+ onBack: pageContainer.pop()
+ }
+ }
+
+ Component {
+ id: addressListPage
+
+ WalletPages.AddressList {
+ onBack: pageContainer.pop()
+ onReceiveRequested: {
+ pageContainer.pop()
+ root.receiveRequested()
+ }
+ }
+ }
+
+ Component {
+ id: externalSignerPage
+ SettingsPages.ExternalSignerSettingsPage {}
+ }
+
+ Component {
+ id: displayPage
+ SettingsPages.DisplaySettingsPage {}
+ }
+
+ Component {
+ id: windowBehaviorPage
+ SettingsPages.WindowBehaviorSettingsPage {}
+ }
+
+ Component {
+ id: storagePage
+ SettingsPages.StorageSettingsPage {}
+ }
+
+ Component {
+ id: connectionPage
+ SettingsPages.ConnectionSettingsPage {}
+ }
+
+ Component {
+ id: networkTrafficPage
+
+ SettingsPages.NetworkTrafficSettingsPage {}
+ }
+
+ Component {
+ id: mempoolPage
+ SettingsPages.MempoolSettingsPage {}
+ }
+
+ Component {
+ id: rpcConsolePage
+
+ SettingsPages.RpcConsoleSettingsPage {
+ walletName: typeof walletController !== "undefined"
+ && walletController.isWalletLoaded && walletController.selectedWallet
+ ? walletController.selectedWallet.name
+ : ""
+ }
+ }
+
+ Component {
+ id: debugLogPage
+
+ SettingsPages.SettingsDebugLog {
+ showBackButton: false
+ maximumContentWidth: width
+ contentHorizontalPadding: width >= 900 ? 56 : width >= 640 ? 40 : 24
+ }
+ }
+
+ Component {
+ id: aboutPage
+ SettingsPages.AboutSettingsPage {}
+ }
+}
diff --git a/qml/components/ThemeSettings.qml b/qml/components/ThemeSettings.qml
deleted file mode 100644
index 9be3a8adcc..0000000000
--- a/qml/components/ThemeSettings.qml
+++ /dev/null
@@ -1,75 +0,0 @@
-// 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"
-
-ColumnLayout {
- id: root
- spacing: 4
-
- signal designSystemRequested
-
- AppSettings {
- id: settings
- }
-
- Setting {
- Layout.fillWidth: true
- header: qsTr("Light")
- actionItem: Icon {
- anchors.centerIn: parent
- visible: !Theme.dark
- source: "image://images/check"
- color: Theme.color.neutral9
- size: 24
- }
- onClicked: {
- Theme.dark = false
- }
- }
- Separator { Layout.fillWidth: true }
- Setting {
- Layout.fillWidth: true
- header: qsTr("Dark")
- actionItem: Icon {
- anchors.centerIn: parent
- visible: Theme.dark
- source: "image://images/check"
- color: Theme.color.neutral9
- size: 24
- }
- onClicked: {
- Theme.dark = true;
- }
- }
- CoreText {
- Layout.topMargin: 36
- Layout.fillWidth: true
- Layout.leftMargin: 4
- visible: BuildInfo.isDebug
- horizontalAlignment: Text.AlignLeft
- bold: true
- font.pixelSize: 13
- color: Theme.color.neutral7
- text: qsTr("Developer")
- }
- Separator {
- Layout.fillWidth: true
- visible: BuildInfo.isDebug
- }
- Setting {
- id: gotoDesignSystem
- Layout.fillWidth: true
- visible: BuildInfo.isDebug
- header: qsTr("Design system")
- actionItem: CaretRightIcon {
- color: gotoDesignSystem.stateColor
- }
- onClicked: root.designSystemRequested()
- }
-}
diff --git a/qml/components/WalletSettings.qml b/qml/components/WalletSettings.qml
deleted file mode 100644
index 3da2e40076..0000000000
--- a/qml/components/WalletSettings.qml
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (c) 2026 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"
-
-ColumnLayout {
- id: root
-
- spacing: 0
- readonly property var signerStatus: (optionsModel.coreSettingStatuses || ({})).signer || ({})
- readonly property string signerPathError: optionsModel.externalSignerPathValidationError(signerPathInput.text)
-
- Component.onCompleted: walletController.refreshExternalSignerStatus()
-
- function commitSignerPath() {
- if (signerPathError.length > 0) {
- return false
- }
- const normalizedPath = signerPathInput.text.trim()
- if (normalizedPath !== optionsModel.externalSignerPath) {
- optionsModel.externalSignerPath = normalizedPath
- }
- return true
- }
-
- CoreText {
- Layout.topMargin: 16
- Layout.fillWidth: true
- text: qsTr("Signer path")
- font.pixelSize: 15
- color: Theme.color.neutral9
- }
-
- CoreTextField {
- id: signerPathInput
- objectName: "externalSignerPathInput"
- Layout.topMargin: 8
- Layout.fillWidth: true
- placeholderText: qsTr("Enter external signer path")
- text: optionsModel.externalSignerPath
- enabled: root.signerStatus.canEdit !== false
- onEditingFinished: {
- if (root.commitSignerPath()) {
- walletController.refreshExternalSignerStatus()
- }
- }
- }
-
- CoreText {
- visible: root.signerPathError.length > 0 || (root.signerStatus.infoText || "").length > 0
- Layout.topMargin: 10
- Layout.fillWidth: true
- wrapMode: Text.WordWrap
- color: root.signerPathError.length > 0 ? Theme.color.red : Theme.color.neutral7
- text: root.signerPathError.length > 0 ? root.signerPathError : (root.signerStatus.infoText || "")
- }
-
- CoreText {
- Layout.topMargin: root.signerPathError.length > 0 ? 6 : 10
- Layout.fillWidth: true
- wrapMode: Text.WordWrap
- text: qsTr("The add wallet flow can offer external wallets when exactly one supported signer is connected.")
- font.pixelSize: 15
- color: Theme.color.neutral7
- }
-
- Rectangle {
- Layout.topMargin: 16
- Layout.fillWidth: true
- radius: 5
- color: Qt.rgba(Theme.color.neutral2.r, Theme.color.neutral2.g, Theme.color.neutral2.b, 0.5)
- implicitHeight: statusRow.implicitHeight + 20
-
- RowLayout {
- id: statusRow
- anchors.fill: parent
- anchors.margins: 10
- spacing: 8
-
- Icon {
- source: root.signerPathError.length > 0
- ? "image://images/error"
- : walletController.canCreateExternalSignerWallet
- ? "image://images/green-check"
- : "image://images/info-filled"
- color: root.signerPathError.length > 0
- ? Theme.color.red
- : walletController.canCreateExternalSignerWallet
- ? Theme.color.green
- : Theme.color.neutral9
- size: 16
- Layout.alignment: Qt.AlignVCenter
- }
-
- CoreText {
- objectName: "externalSignerStatusText"
- Layout.fillWidth: true
- wrapMode: Text.WordWrap
- color: Theme.color.neutral9
- text: {
- if (root.signerPathError.length > 0) {
- return root.signerPathError
- }
- if (walletController.canCreateExternalSignerWallet) {
- return qsTr("Detected external signer: %1").arg(walletController.externalSignerName)
- }
- if (walletController.externalSignerError.length > 0) {
- return walletController.externalSignerError
- }
- if (optionsModel.walletSettingsDirty) {
- return qsTr("Path updated. Press Check device to rescan with the current signer command.")
- }
- if (optionsModel.externalSignerPath.length > 0) {
- return qsTr("No external signer is currently detected.")
- }
- return qsTr("Set the command path for HWI or another external signer tool.")
- }
- }
- }
- }
-
- ContinueButton {
- objectName: "externalSignerCheckDeviceButton"
- Layout.topMargin: 20
- Layout.preferredWidth: Math.min(300, parent.width)
- Layout.alignment: Qt.AlignHCenter
- text: qsTr("Check device")
- enabled: root.signerPathError.length === 0 && root.signerStatus.canEdit !== false
- onClicked: {
- if (root.commitSignerPath()) {
- walletController.refreshExternalSignerStatus()
- }
- }
- }
-}
diff --git a/qml/controls/CaretRightIcon.qml b/qml/controls/CaretRightIcon.qml
index fe39d43c11..2b8eac54a4 100644
--- a/qml/controls/CaretRightIcon.qml
+++ b/qml/controls/CaretRightIcon.qml
@@ -7,5 +7,5 @@ import QtQuick.Controls 2.15
Icon {
source: "image://images/caret-right"
- size: 18
+ size: 14
}
diff --git a/qml/controls/ContextMenu.qml b/qml/controls/ContextMenu.qml
index 692a931b7f..f77e222de5 100644
--- a/qml/controls/ContextMenu.qml
+++ b/qml/controls/ContextMenu.qml
@@ -13,6 +13,7 @@ Popup {
property int minMenuWidth: 240
property int itemSpacing: 0
property int menuPadding: 6
+ property color backgroundColor: Theme.color.neutral1
default property alias menuItems: _column.data
@@ -36,7 +37,7 @@ Popup {
height: implicitHeight
background: Rectangle {
- color: Theme.color.neutral1
+ color: root.backgroundColor
border.color: Theme.dark ? Theme.color.neutral2 : Theme.color.neutral3
border.width: 1
radius: 5
diff --git a/qml/controls/ContextMenuButton.qml b/qml/controls/ContextMenuButton.qml
index 4465cd6594..6e24ae1302 100644
--- a/qml/controls/ContextMenuButton.qml
+++ b/qml/controls/ContextMenuButton.qml
@@ -18,6 +18,7 @@ AbstractButton {
property url iconSource
property int role: ContextMenuButton.Normal
property bool autoClose: true
+ property color hoverBackgroundColor: Theme.color.neutral3
readonly property bool _destructive: role === ContextMenuButton.Destructive
readonly property bool _highlighted: enabled && (hovered || down || visualFocus)
@@ -95,7 +96,7 @@ AbstractButton {
}
background: Rectangle {
- color: root._highlighted ? Theme.color.neutral2 : "transparent"
+ color: root._highlighted ? root.hoverBackgroundColor : "transparent"
radius: 6
}
}
diff --git a/qml/controls/ContextMenuPicker.qml b/qml/controls/ContextMenuPicker.qml
index 5415993921..f767b9d490 100644
--- a/qml/controls/ContextMenuPicker.qml
+++ b/qml/controls/ContextMenuPicker.qml
@@ -15,10 +15,12 @@ Item {
property string textRole: "text"
property string valueRole: "value"
property string subtitleRole: ""
+ property string iconRole: ""
property string objectNameRole: ""
property string subtitleObjectNameRole: ""
property var currentValue
property url selectionIconSource: "image://images/check"
+ property int iconSize: 18
property int rowHeight: 36
property int subtitleRowHeight: 52
@@ -39,6 +41,11 @@ Item {
const v = item[root.subtitleRole]
return v === undefined || v === null ? "" : v
}
+ function _rowIconSource(item) {
+ if (root.iconRole === "" || typeof item !== 'object' || item === null) return ""
+ const v = item[root.iconRole]
+ return v === undefined || v === null ? "" : v
+ }
function _rowObjectName(item) {
if (root.objectNameRole === "" || typeof item !== 'object' || item === null) return ""
const v = item[root.objectNameRole]
@@ -88,12 +95,13 @@ Item {
property string rowText: root._rowText(rowData)
property var rowValue: root._rowValue(rowData)
property string subtitle: root._rowSubtitle(rowData)
+ property url rowIconSource: root._rowIconSource(rowData)
property string subtitleObjectName: root._rowSubtitleObjectName(rowData)
objectName: root._rowObjectName(rowData)
readonly property bool selected: root.currentValue === rowValue
- readonly property int _effectiveHeight: subtitle !== ""
- ? root.subtitleRowHeight
- : root.rowHeight
+ readonly property int _textHeight: subtitle !== "" ? root.subtitleRowHeight : root.rowHeight
+ readonly property int _iconHeight: rowIconSource.toString() !== "" ? root.iconSize + 12 : 0
+ readonly property int _effectiveHeight: Math.max(_textHeight, _iconHeight)
Accessible.name: rowText
Accessible.checkable: true
@@ -111,6 +119,20 @@ Item {
contentItem: RowLayout {
spacing: 7
+ Item {
+ visible: _row.rowIconSource.toString() !== ""
+ Layout.alignment: Qt.AlignVCenter
+ Layout.preferredWidth: visible ? root.iconSize : 0
+ Layout.preferredHeight: visible ? root.iconSize : 0
+
+ Icon {
+ anchors.centerIn: parent
+ source: _row.rowIconSource
+ color: _row._highlighted ? _row._hoverColor : _row._idleColor
+ size: root.iconSize
+ }
+ }
+
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
diff --git a/qml/controls/FormRow.qml b/qml/controls/FormRow.qml
new file mode 100644
index 0000000000..e00ce7799e
--- /dev/null
+++ b/qml/controls/FormRow.qml
@@ -0,0 +1,157 @@
+// Copyright (c) 2026 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
+
+Control {
+ id: root
+
+ property string title: ""
+ property string description: ""
+ property string supportingText: ""
+ property string errorText: ""
+ property alias leadingItem: leadingContainer.data
+ readonly property Item loadedLeadingItem: leadingContainer.children.length > 0 ? leadingContainer.children[0] : null
+ property alias trailingItem: trailingContainer.data
+ readonly property Item loadedTrailingItem: trailingContainer.children.length > 0 ? trailingContainer.children[0] : null
+ property alias bodyItem: bodyContainer.data
+ readonly property Item loadedBodyItem: bodyContainer.children.length > 0 ? bodyContainer.children[0] : null
+ property bool showDivider: true
+ property int minimumRowHeight: description.length > 0 || supportingText.length > 0 || errorText.length > 0 ? 62 : 48
+ property int dividerLeftInset: leftPadding
+ property int dividerRightInset: rightPadding
+ property int contentSpacing: 12
+ property int bodySpacing: 8
+ property bool showsDisclosureIndicator: false
+ property string disclosureIndicatorObjectName: root.objectName.length > 0
+ ? root.objectName + "DisclosureIndicator"
+ : ""
+ property color disclosureIndicatorColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4
+ property color titleColor: enabled ? Theme.color.neutral9 : Theme.color.neutral4
+ property color descriptionColor: enabled ? Theme.color.neutral7 : Theme.color.neutral4
+ property color supportingTextColor: enabled ? Theme.color.blue : Theme.color.neutral4
+ property color errorTextColor: enabled ? Theme.color.red : Theme.color.neutral4
+ property var titleTextStyle: Theme.text.description
+ property var descriptionTextStyle: Theme.text.caption
+ property var supportingTextStyle: Theme.text.caption
+
+ Accessible.name: title
+ Accessible.description: description
+ padding: 0
+ leftPadding: 16
+ rightPadding: 16
+ topPadding: 10
+ bottomPadding: 10
+ implicitWidth: Math.max(320, contentItem.implicitWidth + leftPadding + rightPadding)
+ implicitHeight: Math.max(minimumRowHeight, contentItem.implicitHeight + topPadding + bottomPadding)
+
+ background: Item {
+ Rectangle {
+ visible: root.showDivider
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: root.dividerLeftInset
+ anchors.rightMargin: root.dividerRightInset
+ height: 1
+ color: Theme.color.neutral2
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+ }
+ }
+
+ contentItem: ColumnLayout {
+ spacing: root.bodySpacing
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: root.contentSpacing
+
+ RowLayout {
+ id: leadingContainer
+ visible: children.length > 0
+ enabled: root.enabled
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 0
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 0
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 2
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Title" : ""
+ visible: root.title.length > 0
+ Layout.fillWidth: true
+ text: root.title
+ color: root.titleColor
+ font: root.titleTextStyle.font
+ lineHeight: root.titleTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: false
+ elide: Text.ElideRight
+ }
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Description" : ""
+ visible: root.description.length > 0
+ Layout.fillWidth: true
+ text: root.description
+ color: root.descriptionColor
+ font: root.descriptionTextStyle.font
+ lineHeight: root.descriptionTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "SupportingText" : ""
+ visible: root.errorText.length > 0 || root.supportingText.length > 0
+ Layout.fillWidth: true
+ text: root.errorText.length > 0 ? root.errorText : root.supportingText
+ color: root.errorText.length > 0 ? root.errorTextColor : root.supportingTextColor
+ font: root.supportingTextStyle.font
+ lineHeight: root.supportingTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+
+ RowLayout {
+ id: trailingContainer
+ visible: children.length > 0
+ enabled: root.enabled
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 0
+ }
+
+ CaretRightIcon {
+ id: disclosureIcon
+ objectName: root.disclosureIndicatorObjectName
+ visible: root.showsDisclosureIndicator
+ Layout.preferredWidth: visible ? disclosureIcon.size : 0
+ Layout.preferredHeight: visible ? disclosureIcon.size : 0
+ Layout.alignment: Qt.AlignVCenter
+ color: root.disclosureIndicatorColor
+ }
+ }
+
+ ColumnLayout {
+ id: bodyContainer
+ visible: children.length > 0
+ enabled: root.enabled
+ Layout.fillWidth: true
+ spacing: 0
+ }
+ }
+}
diff --git a/qml/controls/FormSection.qml b/qml/controls/FormSection.qml
new file mode 100644
index 0000000000..9774f0c07f
--- /dev/null
+++ b/qml/controls/FormSection.qml
@@ -0,0 +1,104 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+ColumnLayout {
+ id: root
+
+ default property alias content: contentColumn.data
+ property string title: ""
+ property string description: ""
+ property string footerText: ""
+ property bool showBackground: true
+ property int rowSpacing: 0
+ property int sectionSpacing: 8
+ property int cornerRadius: 16
+ property color backgroundColor: Theme.color.neutral1
+ property var titleTextStyle: Theme.text.subheading
+ property var descriptionTextStyle: Theme.text.caption
+ property var footerTextStyle: Theme.text.caption
+
+ spacing: sectionHeader.visible ? sectionSpacing : 0
+ implicitWidth: 450
+ implicitHeight: sectionColumn.implicitHeight
+
+ ColumnLayout {
+ id: sectionColumn
+ Layout.fillWidth: true
+ spacing: sectionHeader.visible || sectionFooter.visible ? root.sectionSpacing : 0
+
+ ColumnLayout {
+ id: sectionHeader
+ visible: root.title.length > 0 || root.description.length > 0
+ Layout.fillWidth: true
+ Layout.leftMargin: 4
+ Layout.rightMargin: 4
+ spacing: 2
+
+ CoreText {
+ visible: root.title.length > 0
+ Layout.fillWidth: true
+ text: root.title
+ color: Theme.color.neutral9
+ font: root.titleTextStyle.font
+ lineHeight: root.titleTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: false
+ elide: Text.ElideRight
+ }
+
+ CoreText {
+ visible: root.description.length > 0
+ Layout.fillWidth: true
+ text: root.description
+ color: Theme.color.neutral7
+ font: root.descriptionTextStyle.font
+ lineHeight: root.descriptionTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+
+ Rectangle {
+ id: card
+ objectName: root.objectName.length > 0 ? root.objectName + "Card" : ""
+ Layout.fillWidth: true
+ implicitHeight: contentColumn.implicitHeight
+ radius: root.cornerRadius
+ color: root.showBackground ? root.backgroundColor : "transparent"
+ clip: true
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+
+ ColumnLayout {
+ id: contentColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ spacing: root.rowSpacing
+ }
+ }
+
+ CoreText {
+ id: sectionFooter
+ objectName: root.objectName.length > 0 ? root.objectName + "Footer" : ""
+ visible: root.footerText.length > 0
+ Layout.fillWidth: true
+ Layout.leftMargin: 4
+ Layout.rightMargin: 4
+ text: root.footerText
+ color: Theme.color.neutral7
+ font: root.footerTextStyle.font
+ lineHeight: root.footerTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+}
diff --git a/qml/controls/LinkRow.qml b/qml/controls/LinkRow.qml
new file mode 100644
index 0000000000..5a20b9f55c
--- /dev/null
+++ b/qml/controls/LinkRow.qml
@@ -0,0 +1,52 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+ListRow {
+ id: root
+
+ property string value: ""
+ property url link: ""
+ property url linkIconSource: "image://images/export"
+ property int linkIconSize: 18
+ property int valueMaximumWidth: 300
+ property color valueColor: enabled ? Theme.color.neutral9 : Theme.color.neutral4
+ property color linkIconColor: valueColor
+ property var valueTextStyle: Theme.text.description
+
+ signal activated(url link)
+
+ Accessible.name: value.length > 0 ? title + ", " + value : title
+ accessibleRole: Accessible.Link
+
+ trailingItem: RowLayout {
+ spacing: 6
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Value" : ""
+ Layout.maximumWidth: root.valueMaximumWidth
+ text: root.value
+ color: root.valueColor
+ font: root.valueTextStyle.font
+ lineHeight: root.valueTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignRight
+ wrap: false
+ elide: Text.ElideMiddle
+ }
+
+ Icon {
+ visible: root.linkIconSource.toString().length > 0
+ Layout.preferredWidth: visible ? root.linkIconSize : 0
+ Layout.preferredHeight: visible ? root.linkIconSize : 0
+ source: root.linkIconSource
+ color: root.linkIconColor
+ size: root.linkIconSize
+ }
+ }
+
+ onClicked: root.activated(root.link)
+}
diff --git a/qml/controls/ListRow.qml b/qml/controls/ListRow.qml
new file mode 100644
index 0000000000..32d39ae5e2
--- /dev/null
+++ b/qml/controls/ListRow.qml
@@ -0,0 +1,69 @@
+// Copyright (c) 2026 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 org.bitcoincore.qt 1.0
+
+AbstractButton {
+ id: root
+
+ property alias title: row.title
+ property alias description: row.description
+ property alias supportingText: row.supportingText
+ property alias errorText: row.errorText
+ property alias leadingItem: row.leadingItem
+ property alias loadedLeadingItem: row.loadedLeadingItem
+ property alias trailingItem: row.trailingItem
+ property alias loadedTrailingItem: row.loadedTrailingItem
+ property alias showDivider: row.showDivider
+ property alias showsDisclosureIndicator: row.showsDisclosureIndicator
+ property alias disclosureIndicatorObjectName: row.disclosureIndicatorObjectName
+ property alias disclosureIndicatorColor: row.disclosureIndicatorColor
+ property bool selected: false
+ property int accessibleRole: Accessible.ListItem
+ property int cornerRadius: 16
+ property color selectedBackgroundColor: Qt.rgba(Theme.color.orange.r, Theme.color.orange.g, Theme.color.orange.b, 0.15)
+ property color hoverBackgroundColor: Theme.color.neutral2
+ property color selectedTextColor: Theme.color.orange
+
+ Accessible.name: title
+ Accessible.description: description
+ Accessible.role: accessibleRole
+ hoverEnabled: AppMode.isDesktop
+ focusPolicy: Qt.StrongFocus
+ padding: 0
+ implicitWidth: row.implicitWidth
+ implicitHeight: row.implicitHeight
+
+ HoverHandler {
+ enabled: root.enabled && AppMode.isDesktop
+ cursorShape: Qt.PointingHandCursor
+ }
+
+ background: Rectangle {
+ radius: root.cornerRadius
+ color: root.selected
+ ? root.selectedBackgroundColor
+ : root.down || root.hovered
+ ? root.hoverBackgroundColor
+ : "transparent"
+
+ FocusBorder {
+ visible: root.visualFocus
+ borderRadius: root.cornerRadius + 2
+ topMargin: -2
+ bottomMargin: -2
+ leftMargin: -2
+ rightMargin: -2
+ }
+ }
+
+ contentItem: FormRow {
+ id: row
+ enabled: root.enabled
+ width: root.availableWidth
+ titleColor: root.selected && root.enabled ? root.selectedTextColor : (root.enabled ? Theme.color.neutral9 : Theme.color.neutral4)
+ }
+}
diff --git a/qml/controls/OptionSwitch.qml b/qml/controls/OptionSwitch.qml
index 9a9ec32442..cb4f676ca9 100644
--- a/qml/controls/OptionSwitch.qml
+++ b/qml/controls/OptionSwitch.qml
@@ -9,6 +9,7 @@ Switch {
id: root
implicitWidth: 45
implicitHeight: 28
+ focusPolicy: Qt.StrongFocus
background: Rectangle {
radius: Math.floor(height / 2)
color: root.checked ? Theme.color.orange : Theme.color.neutral4
@@ -34,4 +35,11 @@ Switch {
ColorAnimation { duration: 150 }
}
}
+
+ FocusBorder {
+ objectName: root.objectName.length > 0 ? root.objectName + "FocusBorder" : ""
+ visible: root.visualFocus
+ borderRadius: Math.floor(height / 2)
+ z: 1
+ }
}
diff --git a/qml/controls/PageHeading.qml b/qml/controls/PageHeading.qml
new file mode 100644
index 0000000000..d6a9f84ae8
--- /dev/null
+++ b/qml/controls/PageHeading.qml
@@ -0,0 +1,71 @@
+// Copyright (c) 2026 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
+
+Control {
+ id: root
+
+ property string title: ""
+ property string description: ""
+ property alias trailingItem: trailingLoader.sourceComponent
+ property alias loadedTrailingItem: trailingLoader.item
+ property int contentSpacing: 16
+ property var titleTextStyle: Theme.text.headline
+ property var descriptionTextStyle: Theme.text.description
+
+ Accessible.name: title
+ Accessible.description: description
+ padding: 0
+ implicitWidth: Math.max(320, contentItem.implicitWidth)
+ implicitHeight: contentItem.implicitHeight
+ background: null
+
+ contentItem: RowLayout {
+ spacing: root.contentSpacing
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 0
+ spacing: 4
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Title" : ""
+ visible: root.title.length > 0
+ Layout.fillWidth: true
+ text: root.title
+ color: Theme.color.neutral9
+ font: root.titleTextStyle.font
+ lineHeight: root.titleTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: false
+ elide: Text.ElideRight
+ }
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Description" : ""
+ visible: root.description.length > 0
+ Layout.fillWidth: true
+ text: root.description
+ color: Theme.color.neutral7
+ font: root.descriptionTextStyle.font
+ lineHeight: root.descriptionTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignHCenter
+ wrap: true
+ }
+ }
+
+ Loader {
+ id: trailingLoader
+ active: sourceComponent !== null
+ visible: item !== null
+ enabled: root.enabled
+ Layout.alignment: Qt.AlignTop | Qt.AlignRight
+ }
+ }
+}
diff --git a/qml/controls/PopupPicker.qml b/qml/controls/PopupPicker.qml
new file mode 100644
index 0000000000..6e55d8a9cb
--- /dev/null
+++ b/qml/controls/PopupPicker.qml
@@ -0,0 +1,118 @@
+// Copyright (c) 2026 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
+
+Control {
+ id: root
+
+ property var model: []
+ property string textRole: "text"
+ property string valueRole: "value"
+ property string subtitleRole: ""
+ property string iconRole: ""
+ property string objectNameRole: ""
+ property string subtitleObjectNameRole: ""
+ property var currentValue
+ property string displayText: ""
+ property string placeholderText: ""
+ property string menuTitle: ""
+ property int minimumMenuWidth: 240
+ property int textAlignment: Text.AlignRight
+ property int caretSize: 20
+ property url selectionIconSource: "image://images/check"
+ property int iconSize: 18
+ property var labelTextStyle: Theme.text.description
+ property bool embedded: false
+ readonly property string currentText: displayText.length > 0 ? displayText : _currentText()
+ readonly property bool opened: popup.visible
+
+ signal activated(var value)
+
+ function _count() {
+ if (root.model === null || root.model === undefined) return 0
+ if (root.model.count !== undefined) return root.model.count
+ return root.model.length !== undefined ? root.model.length : 0
+ }
+
+ function _itemAt(index) {
+ if (root.model && typeof root.model.get === "function") return root.model.get(index)
+ return root.model[index]
+ }
+
+ function _itemText(item) {
+ if (typeof item === "object" && item !== null && item[root.textRole] !== undefined) return item[root.textRole]
+ return item === undefined || item === null ? "" : String(item)
+ }
+
+ function _itemValue(item) {
+ if (typeof item === "object" && item !== null && item[root.valueRole] !== undefined) return item[root.valueRole]
+ return item
+ }
+
+ function _currentText() {
+ const count = root._count()
+ for (let i = 0; i < count; ++i) {
+ const item = root._itemAt(i)
+ if (root._itemValue(item) === root.currentValue) return root._itemText(item)
+ }
+ return root.placeholderText
+ }
+
+ function open() { popup.open() }
+ function close() { popup.close() }
+ function itemAtIndex(index) { return picker.itemAtIndex(index) }
+
+ padding: 0
+ implicitWidth: button.implicitWidth
+ implicitHeight: button.implicitHeight
+ background: null
+
+ contentItem: DropdownButton {
+ id: button
+ objectName: root.objectName.length > 0 ? root.objectName + "Button" : ""
+ enabled: root.enabled && root._count() > 0
+ text: root.currentText
+ opened: root.opened
+ textAlignment: root.textAlignment
+ caretSize: root.caretSize
+ labelTextStyle: root.labelTextStyle
+ defaultBgColor: root.embedded ? Theme.color.neutral2 : Theme.color.background
+ hoverBgColor: root.embedded ? Theme.color.neutral3 : Theme.color.neutral2
+ onClicked: root.opened ? root.close() : root.open()
+ }
+
+ ContextMenu {
+ id: popup
+ objectName: root.objectName.length > 0 ? root.objectName + "Menu" : ""
+ parent: button
+ modal: true
+ dim: false
+ backgroundColor: root.embedded ? Theme.color.neutral2 : Theme.color.neutral1
+ minMenuWidth: Math.max(root.minimumMenuWidth, root.width)
+ x: button.width - width
+ y: button.height + 2
+
+ ContextMenuPicker {
+ id: picker
+ objectName: root.objectName.length > 0 ? root.objectName + "List" : ""
+ title: root.menuTitle
+ model: root.model
+ textRole: root.textRole
+ valueRole: root.valueRole
+ subtitleRole: root.subtitleRole
+ iconRole: root.iconRole
+ objectNameRole: root.objectNameRole
+ subtitleObjectNameRole: root.subtitleObjectNameRole
+ currentValue: root.currentValue
+ selectionIconSource: root.selectionIconSource
+ iconSize: root.iconSize
+ onActivated: function(value) {
+ root.close()
+ root.activated(value)
+ }
+ }
+ }
+}
diff --git a/qml/controls/SegmentedPicker.qml b/qml/controls/SegmentedPicker.qml
index a74b9fba88..7c39090d36 100644
--- a/qml/controls/SegmentedPicker.qml
+++ b/qml/controls/SegmentedPicker.qml
@@ -27,7 +27,7 @@ Control {
padding: 5
background: Rectangle {
- color: Theme.color.neutral3
+ color: Theme.color.neutral2
radius: 8
Behavior on color {
@@ -45,6 +45,7 @@ Control {
required property int index
required property var modelData
+ objectName: root.objectName.length > 0 ? root.objectName + "Option_" + index : ""
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 1
@@ -53,13 +54,13 @@ Control {
checked: index === root.currentIndex
text: root.optionText(modelData)
bgRadius: 5
- textColor: Theme.color.white
- textHoverColor: Theme.color.orangeLight1
+ textColor: Theme.color.neutral9
+ textHoverColor: checked ? Theme.color.white : Theme.color.neutral9
textActiveColor: Theme.color.white
textActiveBold: true
- bgHoverColor: checked ? Theme.color.neutral6 : Theme.color.neutral4
- bgActiveColor: Theme.color.neutral6
- bgDefaultColor: Theme.color.neutral3
+ bgHoverColor: checked ? Theme.color.orange : Theme.color.neutral4
+ bgActiveColor: Theme.color.orange
+ bgDefaultColor: Theme.color.neutral2
onClicked: {
root.selected(index, modelData)
diff --git a/qml/controls/SettingsHeader.qml b/qml/controls/SettingsHeader.qml
index c3e7388783..579e8e3c26 100644
--- a/qml/controls/SettingsHeader.qml
+++ b/qml/controls/SettingsHeader.qml
@@ -64,16 +64,16 @@ Pane {
rightMargin: -2
border.color: Theme.color.orange
}
-
- Behavior on color {
- ColorAnimation { duration: 150 }
- }
}
- contentItem: Icon {
- source: "image://images/caret-left"
- color: Theme.color.neutral9
- size: 24
+ contentItem: Item {
+ Icon {
+ objectName: "settingsHeaderBackIcon"
+ anchors.centerIn: parent
+ source: "image://images/caret-left"
+ color: Theme.color.neutral9
+ size: 24
+ }
}
HoverHandler {
diff --git a/qml/controls/SettingsPage.qml b/qml/controls/SettingsPage.qml
new file mode 100644
index 0000000000..54d8899002
--- /dev/null
+++ b/qml/controls/SettingsPage.qml
@@ -0,0 +1,71 @@
+// Copyright (c) 2026 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
+
+Page {
+ id: root
+
+ default property alias content: contentLayout.data
+
+ property bool showBackButton: true
+ property string backButtonObjectName: ""
+ property string backButtonText: ""
+ property alias rightItem: settingsHeader.rightItem
+ property real maximumContentWidth: 840
+ property real contentHorizontalPadding: width >= 900 ? 56 : width >= 640 ? 40 : 24
+ property real contentSpacing: 24
+ property real contentTopPadding: 20
+ property real contentBottomPadding: 40
+
+ readonly property alias pageHeader: settingsHeader
+ readonly property alias scrollView: scrollView
+ readonly property alias contentLayout: contentLayout
+
+ signal back
+
+ background: null
+ padding: 0
+ contentWidth: root.maximumContentWidth
+
+ header: SettingsHeader {
+ id: settingsHeader
+ objectName: "settingsPageHeader"
+ title: root.title
+ showBackButton: root.showBackButton
+ backButtonObjectName: root.backButtonObjectName
+ backButtonText: root.backButtonText
+ onBack: root.back()
+ }
+
+ ScrollView {
+ id: scrollView
+ objectName: "settingsPageScrollView"
+ anchors.fill: parent
+ contentWidth: availableWidth
+ contentHeight: contentFrame.height
+ clip: true
+ ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
+
+ Item {
+ id: contentFrame
+ width: scrollView.availableWidth
+ height: contentLayout.implicitHeight + root.contentTopPadding + root.contentBottomPadding
+
+ ColumnLayout {
+ id: contentLayout
+ objectName: "settingsPageContentLayout"
+ anchors.top: parent.top
+ anchors.topMargin: root.contentTopPadding
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.max(0, Math.min(
+ parent.width - root.contentHorizontalPadding * 2,
+ root.maximumContentWidth))
+ spacing: root.contentSpacing
+ }
+ }
+ }
+}
diff --git a/qml/controls/TextFieldRow.qml b/qml/controls/TextFieldRow.qml
new file mode 100644
index 0000000000..fd4fe798e3
--- /dev/null
+++ b/qml/controls/TextFieldRow.qml
@@ -0,0 +1,77 @@
+// Copyright (c) 2026 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
+
+FormRow {
+ id: root
+
+ property string fieldObjectName: ""
+ property string text: ""
+ property string placeholderText: ""
+ property bool readOnly: false
+ property var validator: null
+ property int maximumLength: 32767
+ property int inputMethodHints: Qt.ImhNone
+ property int echoMode: TextInput.Normal
+ property int fieldWidth: 180
+ property int textAlignment: Text.AlignRight
+ property color fieldColor: enabled ? Theme.color.neutral9 : Theme.color.neutral4
+ property color placeholderColor: enabled ? Theme.color.neutral5 : Theme.color.neutral4
+ property color focusBorderColor: Theme.color.orange
+ property var fieldTextStyle: Theme.text.description
+ readonly property var field: loadedTrailingItem
+
+ signal textEdited(string text)
+ signal editingFinished()
+ signal accepted()
+
+ trailingItem: TextField {
+ id: input
+ objectName: root.fieldObjectName.length > 0
+ ? root.fieldObjectName
+ : root.objectName.length > 0 ? root.objectName + "Field" : ""
+ implicitWidth: root.fieldWidth
+ implicitHeight: 32
+ enabled: root.enabled
+ readOnly: root.readOnly
+ text: root.text
+ placeholderText: root.placeholderText
+ placeholderTextColor: root.placeholderColor
+ validator: root.validator
+ maximumLength: root.maximumLength
+ inputMethodHints: root.inputMethodHints
+ echoMode: root.echoMode
+ selectByMouse: true
+ leftPadding: 4
+ rightPadding: 4
+ color: root.fieldColor
+ font: root.fieldTextStyle.font
+ horizontalAlignment: root.textAlignment
+ verticalAlignment: TextInput.AlignVCenter
+ Accessible.name: root.title
+ Accessible.description: root.description
+
+ background: FocusBorder {
+ visible: input.activeFocus
+ border.color: root.focusBorderColor
+ borderRadius: 6
+ topMargin: -2
+ bottomMargin: -2
+ leftMargin: -2
+ rightMargin: -2
+ }
+
+ onTextChanged: {
+ if (root.text !== text) root.text = text
+ }
+ onTextEdited: root.textEdited(text)
+ onEditingFinished: root.editingFinished()
+ onAccepted: {
+ root.accepted()
+ input.focus = false
+ }
+ }
+}
diff --git a/qml/controls/Theme.qml b/qml/controls/Theme.qml
index bb3772e773..d7d8344dff 100644
--- a/qml/controls/Theme.qml
+++ b/qml/controls/Theme.qml
@@ -76,9 +76,9 @@ Control {
amber: "#C9B500"
purple: "#C075DC"
neutral0: "#000000"
- neutral1: "#1A1A1A"
- neutral2: "#2D2D2D"
- neutral3: "#444444"
+ neutral1: "#121212"
+ neutral2: "#222222"
+ neutral3: "#383838"
neutral4: "#5C5C5C"
neutral5: "#787878"
neutral6: "#949494"
@@ -108,8 +108,8 @@ Control {
amber: "#C9B500"
purple: "#BB6BD9"
neutral0: "#FFFFFF"
- neutral1: "#F8F8F8"
- neutral2: "#F4F4F4"
+ neutral1: "#F6F6F6"
+ neutral2: "#EEEEEE"
neutral3: "#EDEDED"
neutral4: "#DEDEDE"
neutral5: "#BBBBBB"
diff --git a/qml/controls/ToggleButton.qml b/qml/controls/ToggleButton.qml
index 6e3aa89cb4..dbd867e5c5 100644
--- a/qml/controls/ToggleButton.qml
+++ b/qml/controls/ToggleButton.qml
@@ -20,6 +20,7 @@ Button {
id: root
checkable: true
hoverEnabled: AppMode.isDesktop
+ focusPolicy: Qt.StrongFocus
leftPadding: 12
rightPadding: 12
topPadding: 5
@@ -47,6 +48,13 @@ Button {
}
}
+ FocusBorder {
+ objectName: root.objectName.length > 0 ? root.objectName + "FocusBorder" : ""
+ visible: root.visualFocus
+ borderRadius: root.bgRadius + 4
+ z: 1
+ }
+
states: [
State {
name: "CHECKED"; when: root.checked
diff --git a/qml/controls/ValueRow.qml b/qml/controls/ValueRow.qml
new file mode 100644
index 0000000000..2c18ced1f6
--- /dev/null
+++ b/qml/controls/ValueRow.qml
@@ -0,0 +1,44 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+FormRow {
+ id: root
+
+ property string value: ""
+ property url valueIconSource: ""
+ property int valueIconSize: 18
+ property int valueMaximumWidth: 260
+ property color valueColor: enabled ? Theme.color.neutral9 : Theme.color.neutral4
+ property color valueIconColor: valueColor
+ property var valueTextStyle: Theme.text.description
+
+ trailingItem: RowLayout {
+ spacing: 6
+
+ CoreText {
+ objectName: root.objectName.length > 0 ? root.objectName + "Value" : ""
+ Layout.maximumWidth: root.valueMaximumWidth
+ text: root.value
+ color: root.valueColor
+ font: root.valueTextStyle.font
+ lineHeight: root.valueTextStyle.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignRight
+ wrap: false
+ elide: Text.ElideMiddle
+ }
+
+ Icon {
+ visible: root.valueIconSource.toString().length > 0
+ Layout.preferredWidth: visible ? root.valueIconSize : 0
+ Layout.preferredHeight: visible ? root.valueIconSize : 0
+ source: root.valueIconSource
+ color: root.valueIconColor
+ size: root.valueIconSize
+ }
+ }
+}
diff --git a/qml/pages/MainWindow.qml b/qml/pages/MainWindow.qml
index 22016968fd..2c4f6b16c1 100644
--- a/qml/pages/MainWindow.qml
+++ b/qml/pages/MainWindow.qml
@@ -315,23 +315,18 @@ ApplicationWindow {
id: node
NodeRunner {
onSettingsClicked: {
- nodeStack.push(nodeSettings)
+ nodeStack.push(settingsPage)
}
onPeersClicked: {
peerTableModel.startAutoRefresh()
nodeStack.push(peersPage)
}
- onConsoleClicked: {
- nodeStack.push(consolePage)
- }
}
}
Component {
- id: nodeSettings
- NodeSettings {
- onDoneClicked: {
- nodeStack.pop()
- }
+ id: settingsPage
+ SettingsView {
+ onDoneClicked: nodeStack.pop()
}
}
Component {
@@ -361,12 +356,6 @@ ApplicationWindow {
onBack: nodeStack.pop()
}
}
- Component {
- id: consolePage
- CommandConsole {
- onBack: nodeStack.pop()
- }
- }
}
}
}
diff --git a/qml/pages/node/MempoolInformationSettings.qml b/qml/pages/node/MempoolInformationSettings.qml
deleted file mode 100644
index 10196eb3ab..0000000000
--- a/qml/pages/node/MempoolInformationSettings.qml
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2026 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 {
- id: root
- objectName: "mempoolInformationSettingsPage"
- property bool showBackButton: true
-
- showNavBar: false
- header: SettingsHeader {
- title: qsTr("Mempool information")
- showBackButton: root.showBackButton
- backButtonObjectName: "mempoolInformationBackButton"
- onBack: root.back()
- }
-
- bannerActive: false
- bold: true
- showHeader: false
- headerText: ""
- headerMargin: 0
- description: ""
- descriptionMargin: 0
- detailActive: true
- detailTopMargin: 0
- detailMaximumWidth: 450
- detailItem: ColumnLayout {
- spacing: 4
-
- SettingsRestartNotice {
- objectName: "mempoolRestartNotice"
- visible: optionsModel.mempoolSettingsDirty
- Layout.fillWidth: true
- Layout.bottomMargin: visible ? 12 : 0
- }
-
- MempoolInformationRows {
- id: mempoolInformationRows
- Layout.fillWidth: true
- }
- }
-
- Component.onCompleted: nodeModel.mempoolInfoPollingActive = visible
- Component.onDestruction: nodeModel.mempoolInfoPollingActive = false
- onVisibleChanged: {
- nodeModel.mempoolInfoPollingActive = visible
- }
-}
diff --git a/qml/pages/node/NodeRunner.qml b/qml/pages/node/NodeRunner.qml
index d60c4b727e..0ea1ad915a 100644
--- a/qml/pages/node/NodeRunner.qml
+++ b/qml/pages/node/NodeRunner.qml
@@ -12,7 +12,6 @@ import "../../components"
Page {
signal settingsClicked
signal peersClicked
- signal consoleClicked
id: root
objectName: "nodeRunner"
background: null
@@ -43,16 +42,6 @@ Page {
Layout.alignment: Qt.AlignVCenter
onClicked: root.peersClicked()
}
- IconButton {
- objectName: "consoleTabButton"
- iconSource: "image://images/console"
- iconColor: Theme.color.neutral7
- hoverColor: Theme.color.neutral9
- size: 34
- iconSize: 24
- Layout.alignment: Qt.AlignVCenter
- onClicked: root.consoleClicked()
- }
IconButton {
objectName: "nodeSettingsButton"
iconSource: "image://images/gear"
diff --git a/qml/pages/node/NodeSettings.qml b/qml/pages/node/NodeSettings.qml
deleted file mode 100644
index 8a9a428d67..0000000000
--- a/qml/pages/node/NodeSettings.qml
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright (c) 2022-2026 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"
-import "../../components"
-import "../wallet"
-import "../settings"
-
-Page {
- signal doneClicked
- signal selectWalletRequested
- signal receiveRequested
-
- property alias showDoneButton: doneButton.visible
-
- id: root
- objectName: "nodeSettingsStack"
- background: null
-
- readonly property int settingsSidebarWidth: 185
- readonly property int settingsContentWidth: 450
- readonly property int settingsContentGap: 50
- readonly property int settingsHorizontalPadding: 20
- readonly property int settingsSidebarItemHeight: 31
- readonly property int settingsSidebarGroupSpacing: 20
- readonly property int settingsBodyWidth: Math.min(
- Math.max(0, width - settingsHorizontalPadding * 2),
- settingsSidebarWidth + settingsContentGap + settingsContentWidth)
- readonly property color settingsSidebarSelectedBackgroundColor: Qt.rgba(Theme.color.orange.r, Theme.color.orange.g, Theme.color.orange.b, 0.15)
- property int currentSection: 0
-
- function openWalletSettings() {
- for (var i = 0; i < sidebarModel.count; i++) {
- if (sidebarModel.get(i).section === "wallet") {
- root.currentSection = i
- return
- }
- }
- }
-
- function openAddressHistory() {
- if (!walletController.isWalletLoaded || !walletController.selectedWallet) {
- return
- }
- walletController.selectedWallet.addressListModel.refresh()
- openWalletSettings()
- walletStack.push(addressListComp)
- }
-
- function openWalletAddressHistory() {
- root.openAddressHistory()
- }
-
- Connections {
- target: typeof walletController !== "undefined" ? walletController : null
- function onOpenWalletSettingsRequested() {
- root.openWalletSettings()
- }
- function onSelectedWalletChanged() {
- if (walletStack.depth > 1) walletStack.pop(null)
- }
- function onIsWalletLoadedChanged() {
- if (!walletController.isWalletLoaded && walletStack.depth > 1) walletStack.pop(null)
- }
- }
-
- ListModel { id: sidebarModel }
-
- Component.onCompleted: {
- // Display order and grouping follow the desktop settings design
- // (BitcoinDesign/Bitcoin-Core-App#163). Row order is kept in lockstep
- // with the contentStack page order below, so a row's index is its page
- // index. Peers and Console live on the main nav bar, not in settings.
- sidebarModel.append({ label: qsTr("Wallet"), section: "wallet", group: "wallet", alwaysVisible: false })
- sidebarModel.append({ label: qsTr("External signer"), section: "externalsigner", group: "wallet", alwaysVisible: false })
- sidebarModel.append({ label: qsTr("Display"), section: "display", group: "display", alwaysVisible: true })
- sidebarModel.append({ label: qsTr("Window behavior"), section: "windowbehavior", group: "display", alwaysVisible: false })
- sidebarModel.append({ label: qsTr("Storage"), section: "storage", group: "display", alwaysVisible: true })
- sidebarModel.append({ label: qsTr("Connection"), section: "connection", group: "network", alwaysVisible: true })
- sidebarModel.append({ label: qsTr("Network traffic"), section: "networktraffic", group: "network", alwaysVisible: true })
- sidebarModel.append({ label: qsTr("Mempool information"), section: "mempool", group: "network", alwaysVisible: false })
- sidebarModel.append({ label: qsTr("Debug log"), section: "debuglog", group: "developer", alwaysVisible: true })
- sidebarModel.append({ label: qsTr("About"), section: "about", group: "about", alwaysVisible: true })
- root.selectFirstVisibleSection()
- }
-
- function isSectionVisible(index) {
- var item = sidebarModel.get(index)
- if (item.alwaysVisible) return true
- if (item.section === "wallet" || item.section === "externalsigner")
- return AppMode.walletEnabled
- if (item.section === "mempool")
- return nodeModel.mempoolInformationAvailable
- if (item.section === "windowbehavior")
- return AppMode.isDesktop
- return true
- }
-
- // Land on the first visible row so node-only mode (where Wallet/External
- // Signer are hidden) never opens on a hidden section.
- function selectFirstVisibleSection() {
- for (var i = 0; i < sidebarModel.count; i++) {
- if (isSectionVisible(i)) { root.currentSection = i; return }
- }
- }
-
- // True when this row begins a new group relative to the previous *visible*
- // row, so the delegate can add leading space between groups while skipping
- // hidden rows.
- function isFirstVisibleInGroup(index) {
- var group = sidebarModel.get(index).group
- for (var j = index - 1; j >= 0; j--) {
- if (!isSectionVisible(j)) continue
- return sidebarModel.get(j).group !== group
- }
- return false
- }
-
- contentItem: Item {
- RowLayout {
- anchors.top: parent.top
- anchors.horizontalCenter: parent.horizontalCenter
- width: root.settingsBodyWidth
- height: parent.height
- spacing: root.settingsContentGap
-
- ColumnLayout {
- Layout.preferredWidth: root.settingsSidebarWidth
- Layout.maximumWidth: root.settingsSidebarWidth
- Layout.minimumWidth: root.settingsSidebarWidth
- Layout.fillWidth: false
- Layout.fillHeight: true
- Layout.topMargin: 25
- spacing: 0
-
- Repeater {
- model: sidebarModel
- delegate: AbstractButton {
- id: sidebarButton
- objectName: "settings_" + model.section
- Layout.fillWidth: true
- Layout.preferredHeight: root.settingsSidebarItemHeight
- Layout.topMargin: root.isFirstVisibleInGroup(index) ? root.settingsSidebarGroupSpacing : 0
- visible: root.isSectionVisible(index)
- hoverEnabled: AppMode.isDesktop
- focusPolicy: Qt.TabFocus
- leftPadding: 10
- rightPadding: 10
- topPadding: 5
- bottomPadding: 5
- Accessible.name: model.label
- Accessible.role: Accessible.ListItem
-
- onClicked: root.currentSection = index
-
- background: Rectangle {
- radius: 5
- color: root.currentSection === index
- ? root.settingsSidebarSelectedBackgroundColor
- : sidebarButton.hovered
- ? Theme.color.neutral1
- : "transparent"
- Behavior on color { ColorAnimation { duration: 150 } }
-
- FocusBorder {
- visible: sidebarButton.visualFocus
- borderRadius: 7
- topMargin: -2
- bottomMargin: -2
- leftMargin: -2
- rightMargin: -2
- }
- }
-
- contentItem: CoreText {
- horizontalAlignment: Text.AlignLeft
- verticalAlignment: Text.AlignVCenter
- text: model.label
- font.pixelSize: 15
- color: root.currentSection === index
- ? Theme.color.orange
- : Theme.color.neutral9
- }
-
- HoverHandler {
- cursorShape: Qt.PointingHandCursor
- }
- }
- }
-
- Item { Layout.fillHeight: true }
-
- NavButton {
- id: doneButton
- objectName: "nodeSettingsDoneButton"
- text: qsTr("Done")
- Layout.alignment: Qt.AlignHCenter
- Layout.bottomMargin: 20
- onClicked: root.doneClicked()
- }
- }
-
- Rectangle {
- Layout.preferredWidth: Math.min(root.settingsContentWidth, Math.max(0, root.settingsBodyWidth - root.settingsSidebarWidth - root.settingsContentGap))
- Layout.maximumWidth: root.settingsContentWidth
- Layout.minimumWidth: 0
- Layout.fillWidth: true
- Layout.fillHeight: true
- color: "transparent"
- clip: true
-
- StackLayout {
- id: contentStack
- anchors.fill: parent
- // Content order is kept in lockstep with the sidebar row order
- // so the selected row maps directly to its page.
- currentIndex: root.currentSection
-
- PageStack {
- id: walletStack
- objectName: "walletSettingsStack"
- initialItem: WalletSettings {
- objectName: "walletSettingsPage"
- // Reached from the settings sidebar, like the other
- // sections, so it has no back button of its own; the
- // pushed sub-pages carry theirs. Binding this to
- // depth > 1 turned the back button on as soon as a
- // sub-page was pushed, flashing it on this page for the
- // duration of the push transition.
- showBackButton: false
- onBack: walletStack.pop()
- onSelectWalletRequested: root.selectWalletRequested()
- onPasswordRequested: walletStack.push(walletPasswordComp, { "updating": walletController.selectedWallet.isEncrypted })
- onSignVerifyMessageRequested: walletStack.push(signVerifyComp)
- onAddressesRequested: {
- if (walletController.isWalletLoaded && walletController.selectedWallet) {
- walletController.selectedWallet.addressListModel.refresh()
- walletStack.push(addressListComp)
- }
- }
- }
- Component {
- id: walletPasswordComp
- WalletPasswordSettings {
- onBack: walletStack.pop()
- onSaved: walletStack.pop()
- }
- }
- Component {
- id: signVerifyComp
- SignVerifyMessage {
- onBack: walletStack.pop()
- }
- }
- Component {
- id: addressListComp
- AddressList {
- onBack: walletStack.pop()
- onReceiveRequested: {
- walletStack.pop()
- root.receiveRequested()
- }
- }
- }
- }
- SettingsWallet { showBackButton: false }
- SettingsDisplay { showBackButton: false }
- SettingsWindowBehavior { showBackButton: false }
- SettingsStorage { showBackButton: false }
- SettingsConnection { showBackButton: false }
- Loader {
- id: networkTrafficLoader
- objectName: "networkTrafficLoader"
- active: root.visible && root.currentSection === 6
- sourceComponent: NetworkTraffic { showBackButton: false; showHeader: false }
- }
- MempoolInformationSettings { showBackButton: false }
- Loader {
- id: debugLogLoader
- objectName: "settingsDebugLogLoader"
- active: root.visible && root.currentSection === 8
- sourceComponent: SettingsDebugLog { showBackButton: false }
- }
- PageStack {
- id: aboutStack
- initialItem: SettingsAbout {
- showBackButton: false
- }
- }
- }
- }
- }
- }
-}
diff --git a/qml/pages/settings/AboutSettingsPage.qml b/qml/pages/settings/AboutSettingsPage.qml
new file mode 100644
index 0000000000..21de145d9e
--- /dev/null
+++ b/qml/pages/settings/AboutSettingsPage.qml
@@ -0,0 +1,93 @@
+// Copyright (c) 2026 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"
+import "../../components"
+
+SettingsPage {
+ id: root
+ objectName: "aboutSettingsPage"
+ title: qsTr("About")
+ showBackButton: false
+
+ PageHeading {
+ Layout.fillWidth: true
+ description: qsTr("Bitcoin Core is an open source project. If you find it useful, please contribute.\n\nThis is experimental software.")
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+
+ LinkRow {
+ Layout.fillWidth: true
+ title: qsTr("Website")
+ value: "bitcoincore.org"
+ link: "https://bitcoincore.org"
+ onActivated: function(link) { root.openExternalLink(link) }
+ }
+
+ LinkRow {
+ Layout.fillWidth: true
+ title: qsTr("Source code")
+ value: "github.com/bitcoin/bitcoin"
+ link: "https://github.com/bitcoin/bitcoin"
+ onActivated: function(link) { root.openExternalLink(link) }
+ }
+
+ LinkRow {
+ Layout.fillWidth: true
+ title: qsTr("License")
+ value: "MIT"
+ link: "https://opensource.org/licenses/MIT"
+ onActivated: function(link) { root.openExternalLink(link) }
+ }
+
+ LinkRow {
+ objectName: "aboutVersionRow"
+ Layout.fillWidth: true
+ title: qsTr("Version")
+ value: BuildInfo.fullClientVersion
+ link: "https://bitcoin.org/en/download"
+ linkIconSource: ""
+ showsDisclosureIndicator: true
+ onActivated: function(link) { root.openExternalLink(link) }
+ }
+
+ ListRow {
+ objectName: "aboutDeveloperRow"
+ Layout.fillWidth: true
+ title: qsTr("Developer options")
+ description: qsTr("Only use these if you have development experience.")
+ showDivider: false
+ showsDisclosureIndicator: true
+ onClicked: root.StackView.view.push(developerPage)
+ }
+ }
+
+ function openExternalLink(link) {
+ externalLinkPopup.link = link
+ externalLinkPopup.open()
+ }
+
+ ExternalPopup {
+ id: externalLinkPopup
+ objectName: "aboutExternalLinkPopup"
+ parent: Overlay.overlay
+ anchors.centerIn: parent
+ width: Math.min(450, Math.max(0, parent ? parent.width - 40 : 0))
+ }
+
+ Component {
+ id: developerPage
+
+ SettingsDeveloper {
+ onBack: root.StackView.view.pop()
+ }
+ }
+}
diff --git a/qml/pages/settings/ConnectionSettingsPage.qml b/qml/pages/settings/ConnectionSettingsPage.qml
new file mode 100644
index 0000000000..0c106bbdc4
--- /dev/null
+++ b/qml/pages/settings/ConnectionSettingsPage.qml
@@ -0,0 +1,95 @@
+// Copyright (c) 2026 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"
+
+SettingsPage {
+ id: root
+ objectName: "connectionSettingsPage"
+ title: qsTr("Connection")
+ showBackButton: false
+
+ property var settingsModel: optionsModel
+ property var coreSettingsModel: settingsModel.coreSettings
+ readonly property var listenSetting: coreSettingsModel.entry("listen")
+ readonly property var natpmpSetting: coreSettingsModel.entry("natpmp")
+ readonly property var serverSetting: coreSettingsModel.entry("server")
+
+ SettingsRestartNotice {
+ visible: root.settingsModel.connectionSettingsDirty
+ Layout.fillWidth: true
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Incoming connections")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Enable listening")
+ description: qsTr("Allow incoming peer connections.")
+ supportingText: root.listenSetting.infoText
+ enabled: root.listenSetting.canEdit
+ trailingItem: OptionSwitch {
+ objectName: "listenSwitch"
+ checked: root.listenSetting.value
+ onToggled: root.listenSetting.value = checked
+ }
+ }
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Map port using NAT-PMP")
+ supportingText: root.natpmpSetting.infoText
+ enabled: root.natpmpSetting.canEdit
+ trailingItem: OptionSwitch {
+ objectName: "natpmpSwitch"
+ checked: root.natpmpSetting.value
+ onToggled: root.natpmpSetting.value = checked
+ }
+ }
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Enable RPC server")
+ supportingText: root.serverSetting.infoText
+ enabled: root.serverSetting.canEdit
+ showDivider: false
+ trailingItem: OptionSwitch {
+ objectName: "serverSwitch"
+ checked: root.serverSetting.value
+ onToggled: root.serverSetting.value = checked
+ }
+ }
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Privacy")
+
+ ListRow {
+ objectName: "proxySettingsRow"
+ Layout.fillWidth: true
+ title: qsTr("Proxy settings")
+ description: qsTr("Route peer and Tor connections through SOCKS5 proxies.")
+ showDivider: false
+ showsDisclosureIndicator: true
+ onClicked: root.StackView.view.push(proxyPage)
+ }
+ }
+
+ Component {
+ id: proxyPage
+
+ ProxySettingsPage {
+ settingsModel: root.settingsModel
+ onCloseRequested: root.StackView.view.pop()
+ }
+ }
+}
diff --git a/qml/pages/settings/DisplaySettingsPage.qml b/qml/pages/settings/DisplaySettingsPage.qml
new file mode 100644
index 0000000000..3958564149
--- /dev/null
+++ b/qml/pages/settings/DisplaySettingsPage.qml
@@ -0,0 +1,232 @@
+// Copyright (c) 2026 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"
+
+SettingsPage {
+ id: root
+ objectName: "displaySettingsPage"
+ title: qsTranslate("SettingsDisplay", "Display")
+ showBackButton: false
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Appearance")
+
+ FormRow {
+ objectName: "displayThemeRow"
+ Layout.fillWidth: true
+ title: qsTranslate("SettingsDisplay", "Theme")
+ trailingItem: SegmentedPicker {
+ objectName: "displayThemePicker"
+ implicitWidth: 190
+ implicitHeight: 36
+ model: [qsTr("Light"), qsTr("Dark")]
+ currentIndex: Theme.dark ? 1 : 0
+ onSelected: function(index, option) {
+ Theme.dark = index === 1
+ }
+ }
+ }
+
+ FormRow {
+ objectName: "displayBlockStatusSizeRow"
+ Layout.fillWidth: true
+ title: qsTranslate("SettingsDisplay", "Block status size")
+ trailingItem: PopupPicker {
+ objectName: "displayBlockStatusSizePicker"
+ embedded: true
+ minimumMenuWidth: 520
+ subtitleRole: "description"
+ iconRole: "icon"
+ iconSize: 40
+ currentValue: Theme.blockclocksize >= 1 / 2 ? 1 / 2 : 1 / 3
+ model: [
+ {
+ text: qsTr("Compact"),
+ value: 1 / 3,
+ description: qsTr("For personal use on a computer or smartphone."),
+ icon: "image://images/blockclock-size-compact"
+ },
+ {
+ text: qsTr("Showcase"),
+ value: 1 / 2,
+ description: qsTr("A larger block clock for public display on a tablet or other large screen."),
+ icon: "image://images/blockclock-size-showcase"
+ }
+ ]
+ onActivated: function(value) {
+ Theme.blockclocksize = value
+ }
+ }
+ }
+
+ FormRow {
+ objectName: "displayMoneyFontRow"
+ Layout.fillWidth: true
+ title: qsTr("Money font")
+ showDivider: false
+ trailingItem: PopupPicker {
+ objectName: "displayMoneyFontPicker"
+ embedded: true
+ minimumMenuWidth: 400
+ subtitleRole: "description"
+ currentValue: optionsModel.moneyFontChoice
+ model: [
+ {
+ text: qsTr("Roboto Mono"),
+ value: "embedded",
+ description: qsTr("Included with Bitcoin Core")
+ },
+ {
+ text: qsTr("System Monospace"),
+ value: "best_system",
+ description: qsTr("Uses your operating system’s default monospaced font")
+ }
+ ]
+ onActivated: function(value) {
+ optionsModel.moneyFontChoice = value
+ }
+ }
+ }
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Language and format")
+
+ FormRow {
+ objectName: "displayUnitRow"
+ Layout.fillWidth: true
+ title: qsTranslate("SettingsDisplay", "Display unit")
+ trailingItem: PopupPicker {
+ objectName: "displayUnitPicker"
+ embedded: true
+ minimumMenuWidth: 400
+ subtitleRole: "description"
+ objectNameRole: "objectName"
+ currentValue: optionsModel.displayUnit
+ model: [
+ {
+ text: qsTr("BTC"),
+ value: 0,
+ objectName: "displayUnitBTC",
+ description: qsTr("8 decimal places (0.00000001 BTC = 1 sat)")
+ },
+ {
+ text: qsTr("mBTC"),
+ value: 1,
+ objectName: "displayUnitMBTC",
+ description: qsTr("5 decimal places (0.00001 mBTC = 1 sat)")
+ },
+ {
+ text: qsTr("bits"),
+ value: 2,
+ objectName: "displayUnitBits",
+ description: qsTr("2 decimal places (0.01 bits = 1 sat)")
+ },
+ {
+ text: qsTr("sat"),
+ value: 3,
+ objectName: "displayUnitSAT",
+ description: qsTr("Satoshi, the smallest unit (1 sat = 0.00000001 BTC)")
+ }
+ ]
+ onActivated: function(value) {
+ optionsModel.displayUnit = value
+ }
+ }
+ }
+
+ ListRow {
+ objectName: "displayLanguageRow"
+ Layout.fillWidth: true
+ title: qsTranslate("SettingsDisplay", "Language")
+ enabled: ((optionsModel.coreSettingStatuses || ({})).lang || ({})).canEdit !== false
+ showsDisclosureIndicator: true
+ disclosureIndicatorObjectName: "displayLanguageDisclosureIndicator"
+ trailingItem: CoreText {
+ text: optionsModel.languageLabel(optionsModel.language)
+ color: Theme.color.neutral7
+ font: Theme.text.description.font
+ }
+ onClicked: root.StackView.view.push(languagePage)
+ }
+
+ ListRow {
+ objectName: "displayTransactionUrlsRow"
+ Layout.fillWidth: true
+ title: qsTr("Third-party transaction URLs")
+ showDivider: false
+ showsDisclosureIndicator: true
+ onClicked: root.StackView.view.push(transactionUrlsPage)
+ }
+ }
+
+ FormSection {
+ objectName: "displayDeveloperSection"
+ Layout.fillWidth: true
+ visible: BuildInfo.isDebug
+ title: qsTr("Developer")
+
+ ListRow {
+ objectName: "displayDesignSystemRow"
+ Layout.fillWidth: true
+ title: qsTr("Design system")
+ description: qsTr("Preview reusable controls and design tokens.")
+ showDivider: false
+ showsDisclosureIndicator: true
+ onClicked: root.StackView.view.push(designSystemPage)
+ }
+ }
+
+ Component {
+ id: languagePage
+
+ SettingsLanguage {
+ onBack: root.StackView.view.pop()
+ }
+ }
+
+ Component {
+ id: designSystemPage
+
+ SettingsDesignSystem {
+ objectName: "displayDesignSystemPage"
+ onBack: root.StackView.view.pop()
+ }
+ }
+
+ Component {
+ id: transactionUrlsPage
+
+ SettingsPage {
+ id: transactionUrls
+ title: qsTr("Transaction URLs")
+ maximumContentWidth: 560
+ onBack: transactionUrls.StackView.view.pop()
+
+ PageHeading {
+ Layout.fillWidth: true
+ title: qsTr("Third-party transaction URLs")
+ description: qsTr("Use %s for the transaction hash. Separate multiple URLs with |.")
+ }
+
+ CoreTextField {
+ objectName: "thirdPartyTransactionUrlsInput"
+ Layout.fillWidth: true
+ text: optionsModel.thirdPartyTransactionUrls
+ placeholderText: "https://example.com/tx/%s"
+ onEditingFinished: optionsModel.thirdPartyTransactionUrls = text
+ }
+ }
+ }
+
+}
diff --git a/qml/pages/settings/ExternalSignerSettingsPage.qml b/qml/pages/settings/ExternalSignerSettingsPage.qml
new file mode 100644
index 0000000000..d2d6f8460c
--- /dev/null
+++ b/qml/pages/settings/ExternalSignerSettingsPage.qml
@@ -0,0 +1,168 @@
+// Copyright (c) 2026 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"
+
+SettingsPage {
+ id: root
+ objectName: "externalSignerSettingsPage"
+ title: qsTr("External signer")
+ showBackButton: false
+
+ readonly property var signerStatus: (optionsModel.coreSettingStatuses || ({})).signer || ({})
+ readonly property string signerPathError: optionsModel.externalSignerPathValidationError(signerPathInput.text)
+ readonly property bool signerConnected: root.signerPathError.length === 0
+ && walletController.canCreateExternalSignerWallet
+ readonly property string signerStatusText: {
+ if (root.signerPathError.length > 0) return root.signerPathError
+ if (walletController.canCreateExternalSignerWallet) {
+ return qsTr("Detected external signer: %1").arg(walletController.externalSignerName)
+ }
+ if (walletController.externalSignerError.length > 0) {
+ return walletController.externalSignerError
+ }
+ if ((root.signerStatus.infoText || "").length > 0) {
+ return root.signerStatus.infoText
+ }
+ if (optionsModel.walletSettingsDirty) {
+ return qsTr("Path updated. Press Check device to rescan with the current signer command.")
+ }
+ if (optionsModel.externalSignerPath.length > 0) {
+ return qsTr("No external signer is currently detected.")
+ }
+ return qsTr("Set the command path for HWI or another external signer tool.")
+ }
+
+ function commitSignerPath() {
+ if (root.signerPathError.length > 0) return false
+ const normalizedPath = signerPathInput.text.trim()
+ if (normalizedPath !== optionsModel.externalSignerPath) {
+ optionsModel.externalSignerPath = normalizedPath
+ }
+ return true
+ }
+
+ function checkDevice() {
+ if (root.commitSignerPath()) walletController.refreshExternalSignerStatus()
+ }
+
+ PageHeading {
+ objectName: "externalSignerIntroduction"
+ Layout.fillWidth: true
+ description: qsTr("Connect a hardware wallet or another external signing tool.")
+ }
+
+ FormSection {
+ objectName: "externalSignerPathSection"
+ Layout.fillWidth: true
+ title: qsTr("Signer path")
+ footerText: qsTr("The add wallet flow can offer external wallets when exactly one supported signer is connected.")
+
+ FormRow {
+ objectName: "externalSignerPathRow"
+ Layout.fillWidth: true
+ enabled: root.signerStatus.canEdit !== false
+ showDivider: false
+ bodySpacing: 0
+ topPadding: 16
+ bottomPadding: 16
+ bodyItem: RowLayout {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 0
+ spacing: 16
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.minimumWidth: 0
+ spacing: 12
+
+ TextField {
+ id: signerPathInput
+ objectName: "externalSignerPathInput"
+ Layout.fillWidth: true
+ implicitHeight: 37
+ text: optionsModel.externalSignerPath
+ placeholderText: qsTr("Enter external signer path")
+ placeholderTextColor: Theme.color.neutral7
+ color: Theme.color.neutral9
+ font: Theme.text.description.font
+ selectByMouse: true
+ leftPadding: 15
+ rightPadding: 10
+ topPadding: 0
+ bottomPadding: 0
+ verticalAlignment: TextInput.AlignVCenter
+ background: Rectangle {
+ color: Theme.color.neutral2
+ radius: 5
+
+ FocusBorder {
+ objectName: "externalSignerPathFocusBorder"
+ visible: signerPathInput.activeFocus
+ border.color: Theme.color.orange
+ borderRadius: 7
+ topMargin: -2
+ bottomMargin: -2
+ leftMargin: -2
+ rightMargin: -2
+ }
+ }
+ onEditingFinished: root.checkDevice()
+ onAccepted: focus = false
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Rectangle {
+ objectName: "externalSignerStatusIndicator"
+ Layout.preferredWidth: 10
+ Layout.preferredHeight: 10
+ Layout.alignment: Qt.AlignTop
+ Layout.topMargin: 4
+ radius: width / 2
+ color: root.signerConnected ? Theme.color.green : Theme.color.red
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+ }
+
+ CoreText {
+ objectName: "externalSignerStatusText"
+ Layout.fillWidth: true
+ text: root.signerStatusText
+ color: Theme.color.neutral7
+ font: Theme.text.description.font
+ lineHeight: Theme.text.description.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+ }
+
+ ContinueButton {
+ objectName: "externalSignerCheckDeviceButton"
+ Layout.preferredWidth: 140
+ Layout.preferredHeight: 40
+ Layout.alignment: Qt.AlignVCenter
+ text: qsTr("Check device")
+ textStyle: Theme.text.subheading
+ enabled: root.signerPathError.length === 0
+ && root.signerStatus.canEdit !== false
+ onClicked: root.checkDevice()
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: walletController.refreshExternalSignerStatus()
+}
diff --git a/qml/pages/settings/MempoolSettingsPage.qml b/qml/pages/settings/MempoolSettingsPage.qml
new file mode 100644
index 0000000000..b0da668bf3
--- /dev/null
+++ b/qml/pages/settings/MempoolSettingsPage.qml
@@ -0,0 +1,101 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+import "../../controls"
+import "../../components"
+
+SettingsPage {
+ id: root
+ objectName: "mempoolSettingsPage"
+ title: qsTr("Mempool information")
+ showBackButton: false
+
+ readonly property var maxMempoolStatus: (optionsModel.coreSettingStatuses || ({})).maxmempool || ({})
+ property string mempoolSizeText: String(optionsModel.maxMempoolSizeMB)
+ property string mempoolSizeError: ""
+
+ function formatMegabytes(valueMb) {
+ const rounded = Math.round(valueMb)
+ const decimals = Math.abs(valueMb - rounded) < 0.005 ? 0 : 2
+ return Number(valueMb).toLocaleString(Qt.locale(), "f", decimals) + " MB"
+ }
+
+ function validateMempoolSize(valueMb) {
+ if (isNaN(valueMb)
+ || valueMb < optionsModel.minMaxMempoolSizeMB
+ || valueMb > optionsModel.maxMaxMempoolSizeMB) {
+ return qsTr("Choose a value between %1 MB and %2 MB.")
+ .arg(optionsModel.minMaxMempoolSizeMB)
+ .arg(optionsModel.maxMaxMempoolSizeMB)
+ }
+ return ""
+ }
+
+ SettingsRestartNotice {
+ objectName: "mempoolRestartNotice"
+ visible: optionsModel.mempoolSettingsDirty
+ Layout.fillWidth: true
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Mempool")
+
+ ValueRow {
+ objectName: "mempoolTransactionsRow"
+ Layout.fillWidth: true
+ title: qsTr("Transactions")
+ value: Number(nodeModel.mempoolTransactionCount).toLocaleString(Qt.locale(), "f", 0)
+ }
+
+ ValueRow {
+ objectName: "mempoolMemoryUsedRow"
+ Layout.fillWidth: true
+ title: qsTr("Memory used")
+ value: qsTr("%1 / %2")
+ .arg(root.formatMegabytes(nodeModel.mempoolUsageMB))
+ .arg(root.formatMegabytes(nodeModel.mempoolMaxUsageMB))
+ }
+
+ TextFieldRow {
+ id: mempoolSizeRow
+ objectName: "mempoolSizeLimitRow"
+ Layout.fillWidth: true
+ title: qsTr("Mempool size limit (MB)")
+ enabled: root.maxMempoolStatus.canEdit !== false
+ fieldObjectName: "mempoolSizeLimitInput"
+ fieldWidth: 80
+ text: root.mempoolSizeText
+ validator: IntValidator {
+ bottom: optionsModel.minMaxMempoolSizeMB
+ top: optionsModel.maxMaxMempoolSizeMB
+ }
+ inputMethodHints: Qt.ImhDigitsOnly
+ errorText: root.mempoolSizeError
+ supportingText: root.mempoolSizeError.length === 0
+ ? root.maxMempoolStatus.infoText || ""
+ : ""
+ showDivider: false
+ onTextEdited: function(text) {
+ root.mempoolSizeText = text
+ root.mempoolSizeError = ""
+ }
+ onEditingFinished: {
+ const parsed = parseInt(mempoolSizeRow.text, 10)
+ root.mempoolSizeError = root.validateMempoolSize(parsed)
+ if (root.mempoolSizeError.length === 0) {
+ optionsModel.maxMempoolSizeMB = parsed
+ root.mempoolSizeText = String(parsed)
+ }
+ }
+ }
+ }
+
+ Component.onCompleted: nodeModel.mempoolInfoPollingActive = visible
+ Component.onDestruction: nodeModel.mempoolInfoPollingActive = false
+ onVisibleChanged: nodeModel.mempoolInfoPollingActive = visible
+}
diff --git a/qml/pages/settings/NetworkTrafficSettingsPage.qml b/qml/pages/settings/NetworkTrafficSettingsPage.qml
new file mode 100644
index 0000000000..845abab578
--- /dev/null
+++ b/qml/pages/settings/NetworkTrafficSettingsPage.qml
@@ -0,0 +1,160 @@
+// Copyright (c) 2026 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"
+import "../../components"
+
+SettingsPage {
+ id: root
+ objectName: "networkTrafficSettingsPage"
+ title: qsTr("Network traffic")
+ showBackButton: false
+ maximumContentWidth: width
+
+ property int trafficGraphScale: 300
+ property bool ownsNetworkTrafficActivity: false
+ readonly property var scaleOptions: [
+ { text: qsTr("5 min"), seconds: 300 },
+ { text: qsTr("1 hour"), seconds: 3600 },
+ { text: qsTr("12 hours"), seconds: 3600 * 12 },
+ { text: qsTr("1 day"), seconds: 3600 * 24 }
+ ]
+
+ function scaleIndex(scale) {
+ for (let index = 0; index < root.scaleOptions.length; ++index) {
+ if (root.scaleOptions[index].seconds === scale) return index
+ }
+ return 0
+ }
+
+ function selectScale(scale) {
+ root.trafficGraphScale = scale
+ networkTrafficTower.updateFilterWindowSize(scale / 10)
+ }
+
+ function formatBytes(bytes) {
+ const suffixes = ["Bytes", "KB", "MB", "GB", "TB", "PB"]
+ let index = 0
+ while (bytes >= 1000 && index < suffixes.length - 1) {
+ bytes /= 1000
+ index++
+ }
+ return bytes.toFixed(0) + " " + suffixes[index]
+ }
+
+ function updateNetworkTrafficActivity() {
+ if (root.visible) {
+ networkTrafficTower.active = true
+ root.ownsNetworkTrafficActivity = true
+ } else {
+ if (root.ownsNetworkTrafficActivity) networkTrafficTower.active = false
+ root.ownsNetworkTrafficActivity = false
+ }
+ }
+
+ AppSettings {
+ id: settings
+ property alias trafficGraphScale: root.trafficGraphScale
+ }
+
+ PageHeading {
+ objectName: "networkTrafficHeading"
+ Layout.fillWidth: true
+ description: qsTr("How much data you have sent to and received from your peers.")
+ }
+
+ FormSection {
+ objectName: "networkTrafficSection"
+ Layout.fillWidth: true
+
+ SegmentedPicker {
+ objectName: "networkTrafficRangePicker"
+ Layout.fillWidth: true
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ Layout.topMargin: 16
+ Layout.bottomMargin: 6
+ implicitHeight: 36
+ model: root.scaleOptions
+ currentIndex: root.scaleIndex(root.trafficGraphScale)
+ onSelected: function(index, option) {
+ root.selectScale(option.seconds)
+ }
+ }
+
+ ValueRow {
+ objectName: "networkTrafficReceivedRow"
+ Layout.fillWidth: true
+ title: qsTr("Received")
+ value: root.formatBytes(networkTrafficTower.totalBytesReceived)
+ showDivider: false
+ leadingItem: Rectangle {
+ implicitWidth: 10
+ implicitHeight: 10
+ radius: width / 2
+ color: Theme.color.green
+ }
+ bodyItem: NetworkTrafficGraph {
+ objectName: "networkTrafficReceivedGraph"
+ Layout.fillWidth: true
+ Layout.preferredHeight: 250
+ backgroundColor: Theme.color.neutral1
+ borderColor: Theme.color.neutral3
+ fillColor: Theme.color.green
+ lineColor: Theme.color.green
+ markerLineColor: Theme.color.neutral3
+ unitLabelColor: Theme.color.neutral7
+ maxSamples: root.trafficGraphScale
+ maxValue: networkTrafficTower.maxReceivedRateBps
+ valueList: networkTrafficTower.receivedRateList
+ maxRateBps: networkTrafficTower.maxReceivedRateBps
+ }
+ }
+
+ ValueRow {
+ objectName: "networkTrafficSentRow"
+ Layout.fillWidth: true
+ title: qsTr("Sent")
+ value: root.formatBytes(networkTrafficTower.totalBytesSent)
+ showDivider: false
+ bottomPadding: 16
+ leadingItem: Rectangle {
+ implicitWidth: 10
+ implicitHeight: 10
+ radius: width / 2
+ color: Theme.color.blue
+ }
+ bodyItem: NetworkTrafficGraph {
+ objectName: "networkTrafficSentGraph"
+ Layout.fillWidth: true
+ Layout.preferredHeight: 250
+ backgroundColor: Theme.color.neutral1
+ borderColor: Theme.color.neutral3
+ fillColor: Theme.color.blue
+ lineColor: Theme.color.blue
+ markerLineColor: Theme.color.neutral3
+ unitLabelColor: Theme.color.neutral7
+ maxSamples: root.trafficGraphScale
+ maxValue: networkTrafficTower.maxSentRateBps
+ valueList: networkTrafficTower.sentRateList
+ maxRateBps: networkTrafficTower.maxSentRateBps
+ }
+ }
+ }
+
+ Component.onCompleted: {
+ networkTrafficTower.updateFilterWindowSize(root.trafficGraphScale / 10)
+ root.updateNetworkTrafficActivity()
+ }
+ onVisibleChanged: root.updateNetworkTrafficActivity()
+ Component.onDestruction: {
+ if (root.ownsNetworkTrafficActivity) networkTrafficTower.active = false
+ }
+}
diff --git a/qml/pages/settings/ProxySettingsPage.qml b/qml/pages/settings/ProxySettingsPage.qml
new file mode 100644
index 0000000000..87b378ea5a
--- /dev/null
+++ b/qml/pages/settings/ProxySettingsPage.qml
@@ -0,0 +1,227 @@
+// Copyright (c) 2026 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"
+
+SettingsPage {
+ id: root
+ objectName: "proxySettingsPage"
+ title: qsTr("Proxy settings")
+ backButtonObjectName: "proxySettingsBackButton"
+
+ property var settingsModel: optionsModel
+ property var coreSettingsModel: settingsModel.coreSettings
+ readonly property var proxySetting: coreSettingsModel.entry("proxy")
+ readonly property var onionSetting: coreSettingsModel.entry("onion")
+ property bool draftProxyEnabled: false
+ property string draftProxyAddress: ""
+ property string draftProxyValidationError: ""
+ property bool draftTorEnabled: false
+ property string draftTorAddress: ""
+ property string draftTorValidationError: ""
+ readonly property bool proxyDraftDirty: draftProxyEnabled !== proxySetting.enabled
+ || draftProxyAddress !== displayAddress(proxySetting)
+ || draftTorEnabled !== onionSetting.enabled
+ || draftTorAddress !== displayAddress(onionSetting)
+ readonly property bool proxyDraftValid: !draftProxyEnabled || draftProxyValidationError.length === 0
+ readonly property bool torDraftValid: !draftTorEnabled || draftTorValidationError.length === 0
+ readonly property bool canSaveProxyDraft: proxyDraftDirty && proxyDraftValid && torDraftValid
+
+ signal closeRequested()
+
+ function displayAddress(setting) {
+ return setting.address.length > 0 ? setting.address : setting.defaultAddress()
+ }
+
+ function validateAddress(setting, address) {
+ return setting.validate(address.trim())
+ }
+
+ function resetProxyDraft() {
+ root.draftProxyEnabled = root.proxySetting.enabled
+ root.draftProxyAddress = root.displayAddress(root.proxySetting)
+ root.draftProxyValidationError = root.validateAddress(root.proxySetting, root.draftProxyAddress)
+ root.draftTorEnabled = root.onionSetting.enabled
+ root.draftTorAddress = root.displayAddress(root.onionSetting)
+ root.draftTorValidationError = root.validateAddress(root.onionSetting, root.draftTorAddress)
+ }
+
+ function updateProxyAddress(address) {
+ root.draftProxyAddress = address
+ root.draftProxyValidationError = root.validateAddress(root.proxySetting, address)
+ }
+
+ function updateTorAddress(address) {
+ root.draftTorAddress = address
+ root.draftTorValidationError = root.validateAddress(root.onionSetting, address)
+ }
+
+ function commitProxyDraftEntry(setting, enabled, address, validationError) {
+ const trimmedAddress = address.trim()
+ if (!setting.canEdit) return true
+ if (validationError.length === 0 && trimmedAddress !== setting.address) {
+ if (!setting.commitAddress(trimmedAddress)) return false
+ }
+ if (setting.enabled !== enabled) {
+ setting.enabled = enabled
+ if (setting.enabled !== enabled) return false
+ }
+ return true
+ }
+
+ function commitProxyDraft() {
+ if (!root.canSaveProxyDraft) return false
+ if (!root.commitProxyDraftEntry(
+ root.proxySetting,
+ root.draftProxyEnabled,
+ root.draftProxyAddress,
+ root.draftProxyValidationError)) return false
+ if (!root.commitProxyDraftEntry(
+ root.onionSetting,
+ root.draftTorEnabled,
+ root.draftTorAddress,
+ root.draftTorValidationError)) return false
+ root.resetProxyDraft()
+ return true
+ }
+
+ function save() {
+ if (!root.commitProxyDraft()) return
+ root.closeRequested()
+ }
+
+ function requestBack() {
+ if (root.proxyDraftDirty) {
+ discardProxyChangesPopup.open()
+ return
+ }
+ root.closeRequested()
+ }
+
+ onBack: root.requestBack()
+ Component.onCompleted: root.resetProxyDraft()
+
+ rightItem: NavButton {
+ objectName: "proxySettingsSaveButton"
+ text: qsTr("Save")
+ enabled: root.canSaveProxyDraft
+ onClicked: root.save()
+ }
+
+ SettingsRestartNotice {
+ objectName: "proxyRestartNotice"
+ visible: root.settingsModel.proxySettingsDirty
+ Layout.fillWidth: true
+ }
+
+ FormSection {
+ objectName: "defaultProxySection"
+ Layout.fillWidth: true
+ title: qsTr("Default proxy")
+ description: qsTr("Route peer connections through a SOCKS5 proxy. IPv4, IPv6, and Tor connections are supported.")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Enable")
+ supportingText: root.proxySetting.infoText
+ enabled: root.proxySetting.canEdit
+ trailingItem: OptionSwitch {
+ objectName: "proxyEnableSwitch"
+ checked: root.draftProxyEnabled
+ onToggled: root.draftProxyEnabled = checked
+ }
+ }
+
+ TextFieldRow {
+ id: proxyAddressRow
+ objectName: "proxyAddressRow"
+ Layout.fillWidth: true
+ title: qsTr("Proxy location")
+ enabled: root.draftProxyEnabled && root.proxySetting.canEdit
+ fieldObjectName: "proxyAddressInput"
+ fieldWidth: 220
+ text: root.draftProxyAddress
+ placeholderText: root.proxySetting.defaultAddress()
+ errorText: root.draftProxyEnabled ? root.draftProxyValidationError : ""
+ showDivider: false
+ onTextEdited: function(text) { root.updateProxyAddress(text) }
+ onEditingFinished: {
+ root.updateProxyAddress(proxyAddressRow.text)
+ if (root.draftProxyValidationError.length === 0) {
+ root.draftProxyAddress = proxyAddressRow.text.trim()
+ }
+ }
+ }
+ }
+
+ FormSection {
+ objectName: "torProxySection"
+ Layout.fillWidth: true
+ title: qsTr("Tor proxy")
+ description: qsTr("Route Tor connections through a dedicated SOCKS5 proxy.")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Enable")
+ supportingText: root.onionSetting.infoText
+ enabled: root.onionSetting.canEdit
+ trailingItem: OptionSwitch {
+ objectName: "torEnableSwitch"
+ checked: root.draftTorEnabled
+ onToggled: root.draftTorEnabled = checked
+ }
+ }
+
+ TextFieldRow {
+ id: torAddressRow
+ objectName: "torAddressRow"
+ Layout.fillWidth: true
+ title: qsTr("Proxy location")
+ enabled: root.draftTorEnabled && root.onionSetting.canEdit
+ fieldObjectName: "torAddressInput"
+ fieldWidth: 220
+ text: root.draftTorAddress
+ placeholderText: root.onionSetting.defaultAddress()
+ errorText: root.draftTorEnabled ? root.draftTorValidationError : ""
+ showDivider: false
+ onTextEdited: function(text) { root.updateTorAddress(text) }
+ onEditingFinished: {
+ root.updateTorAddress(torAddressRow.text)
+ if (root.draftTorValidationError.length === 0) {
+ root.draftTorAddress = torAddressRow.text.trim()
+ }
+ }
+ }
+ }
+
+ AlertPopup {
+ id: discardProxyChangesPopup
+ objectName: "discardProxyChangesPopup"
+ parent: Overlay.overlay
+ title: qsTr("Discard changes?")
+ message: qsTr("This will discard your proxy settings changes.")
+ messageObjectName: "discardProxyChangesMessage"
+
+ AlertAction {
+ text: qsTr("Cancel")
+ role: AlertAction.Cancel
+ buttonObjectName: "discardProxyChangesCancelButton"
+ }
+
+ AlertAction {
+ text: qsTr("Discard")
+ role: AlertAction.Destructive
+ buttonObjectName: "discardProxyChangesConfirmButton"
+ onTriggered: {
+ root.resetProxyDraft()
+ root.closeRequested()
+ }
+ }
+ }
+}
diff --git a/qml/pages/settings/RpcConsoleSettingsPage.qml b/qml/pages/settings/RpcConsoleSettingsPage.qml
new file mode 100644
index 0000000000..796d7be84f
--- /dev/null
+++ b/qml/pages/settings/RpcConsoleSettingsPage.qml
@@ -0,0 +1,52 @@
+// Copyright (c) 2026 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 "../../controls"
+import "../node" as NodePages
+
+Page {
+ id: root
+ objectName: "rpcConsoleSettingsPage"
+
+ property string walletName: ""
+ property real maximumContentWidth: 840
+ property real contentHorizontalPadding: width >= 900 ? 56 : width >= 640 ? 40 : 24
+ readonly property alias consoleItem: rpcConsole
+
+ background: null
+ padding: 0
+ clip: true
+
+ header: SettingsHeader {
+ objectName: "rpcConsoleHeader"
+ title: qsTr("RPC console")
+ showBackButton: false
+ }
+
+ Item {
+ id: contentFrame
+ anchors {
+ top: parent.top
+ bottom: parent.bottom
+ horizontalCenter: parent.horizontalCenter
+ topMargin: 20
+ bottomMargin: 20
+ }
+ width: Math.max(0, Math.min(
+ parent.width - root.contentHorizontalPadding * 2,
+ root.maximumContentWidth))
+
+ NodePages.CommandConsole {
+ id: rpcConsole
+ objectName: "rpcConsole"
+ anchors.fill: parent
+ showHeader: false
+ tabActive: root.visible
+ walletName: root.walletName
+ }
+ }
+}
diff --git a/qml/pages/settings/SettingsBlockClockDisplayMode.qml b/qml/pages/settings/SettingsBlockClockDisplayMode.qml
deleted file mode 100644
index 852e2add26..0000000000
--- a/qml/pages/settings/SettingsBlockClockDisplayMode.qml
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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"
-
-Page {
- signal back
-
- id: root
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: SettingsHeader {
- title: qsTr("Block clock display mode")
- onBack: root.back()
- }
- BlockClockDisplayMode {
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- }
-}
\ No newline at end of file
diff --git a/qml/pages/settings/SettingsDebugLog.qml b/qml/pages/settings/SettingsDebugLog.qml
index ea18a082df..a370c1d977 100644
--- a/qml/pages/settings/SettingsDebugLog.qml
+++ b/qml/pages/settings/SettingsDebugLog.qml
@@ -15,12 +15,26 @@ Page {
id: root
objectName: "settingsDebugLog"
background: null
+ padding: 0
property int pendingNewLines: 0
property int displayedLines: 0
+ property real maximumContentWidth: 600
+ property real contentHorizontalPadding: 20
+ property bool ownsDebugLogActivity: false
onPendingNewLinesChanged: if (pendingNewLines > 0) displayedLines = pendingNewLines
property bool userIsScrolled: false
+ function updateDebugLogActivity() {
+ if (root.visible) {
+ debugLogModel.active = true
+ root.ownsDebugLogActivity = true
+ } else {
+ if (root.ownsDebugLogActivity) debugLogModel.active = false
+ root.ownsDebugLogActivity = false
+ }
+ }
+
Connections {
target: debugLogModel
function onNewLinesAdded(count) {
@@ -41,7 +55,7 @@ Page {
Timer {
interval: 60000
repeat: true
- running: true
+ running: root.visible
onTriggered: debugLogModel.updateRelativeTimes()
}
@@ -91,7 +105,9 @@ Page {
ColumnLayout {
id: contentLayout
objectName: "debugLogContentLayout"
- width: Math.max(0, Math.min(parent.width - 40, 600))
+ width: Math.max(0, Math.min(
+ parent.width - root.contentHorizontalPadding * 2,
+ root.maximumContentWidth))
anchors {
top: parent.top
bottom: parent.bottom
@@ -132,10 +148,9 @@ Page {
color: Theme.color.neutral9
placeholderTextColor: Theme.color.neutral5
placeholderText: qsTr("Search...")
- // The page is unloaded whenever another Settings section is
- // selected, while the C++ model intentionally retains its
- // filter. Mirror that retained value on re-entry so the field
- // and the rows cannot disagree.
+ // The C++ model intentionally retains its filter while this
+ // page is cached or closed. Mirror that retained value so the
+ // field and rows cannot disagree when the page is shown.
text: debugLogModel.filter
verticalAlignment: TextInput.AlignVCenter
selectByMouse: true
@@ -328,6 +343,9 @@ Page {
}
}
- Component.onCompleted: debugLogModel.active = true
- Component.onDestruction: debugLogModel.active = false
+ Component.onCompleted: root.updateDebugLogActivity()
+ onVisibleChanged: root.updateDebugLogActivity()
+ Component.onDestruction: {
+ if (root.ownsDebugLogActivity) debugLogModel.active = false
+ }
}
diff --git a/qml/pages/settings/SettingsDesignSystem.qml b/qml/pages/settings/SettingsDesignSystem.qml
index 8c504c0f80..942d724541 100644
--- a/qml/pages/settings/SettingsDesignSystem.qml
+++ b/qml/pages/settings/SettingsDesignSystem.qml
@@ -3,166 +3,415 @@
// 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"
-
-Page {
- signal back
+SettingsPage {
id: root
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
+ title: qsTr("Design system")
readonly property var typographyRoles: [
- { name: "display", group: "Headers" },
- { name: "headline", group: "Headers" },
- { name: "title", group: "Headers" },
- { name: "subtitle", group: "Headers" },
- { name: "heading", group: "Headers" },
- { name: "subheading", group: "Headers" },
- { name: "lead", group: "Body" },
- { name: "bodyLarge", group: "Body" },
- { name: "body", group: "Body" },
- { name: "description", group: "Body" },
- { name: "caption", group: "Body" },
- { name: "button", group: "Controls" },
- { name: "buttonStrong", group: "Controls" },
- { name: "monoLead", group: "Mono" },
- { name: "monoBody", group: "Mono" },
- { name: "monoDescription", group: "Mono" },
- { name: "monoCaption", group: "Mono" }
+ {
+ name: "display",
+ group: "Headers"
+ },
+ {
+ name: "headline",
+ group: "Headers"
+ },
+ {
+ name: "title",
+ group: "Headers"
+ },
+ {
+ name: "subtitle",
+ group: "Headers"
+ },
+ {
+ name: "heading",
+ group: "Headers"
+ },
+ {
+ name: "subheading",
+ group: "Headers"
+ },
+ {
+ name: "lead",
+ group: "Body"
+ },
+ {
+ name: "bodyLarge",
+ group: "Body"
+ },
+ {
+ name: "body",
+ group: "Body"
+ },
+ {
+ name: "description",
+ group: "Body"
+ },
+ {
+ name: "caption",
+ group: "Body"
+ },
+ {
+ name: "button",
+ group: "Controls"
+ },
+ {
+ name: "buttonStrong",
+ group: "Controls"
+ },
+ {
+ name: "monoLead",
+ group: "Mono"
+ },
+ {
+ name: "monoBody",
+ group: "Mono"
+ },
+ {
+ name: "monoDescription",
+ group: "Mono"
+ },
+ {
+ name: "monoCaption",
+ group: "Mono"
+ }
]
- readonly property var paletteTokens: [
- "background", "white",
- "orange", "orangeLight1", "orangeLight2",
- "red", "green", "blue", "amber", "purple",
- "neutral0", "neutral1", "neutral2", "neutral3", "neutral4",
- "neutral5", "neutral6", "neutral7", "neutral8", "neutral9"
- ]
+ readonly property var paletteTokens: ["background", "white", "orange", "orangeLight1", "orangeLight2", "red", "green", "blue", "amber", "purple", "neutral0", "neutral1", "neutral2", "neutral3", "neutral4", "neutral5", "neutral6", "neutral7", "neutral8", "neutral9"]
- header: SettingsHeader {
- title: qsTr("Design system")
- onBack: root.back()
+ property string exampleLanguage: "en"
+ property string exampleBlockClockMode: "compact"
+ property bool exampleStartupEnabled: true
+ property string exampleBlockStorageLimit: "2"
+ property string exampleProxyAddress: "127.0.0.1:9050"
+ property url lastExampleLink: ""
+
+ // ── Form controls ───────────────────────────────────────
+ PageHeading {
+ Layout.fillWidth: true
+ title: qsTr("General")
+ description: qsTr("Generic form and navigation components using the active Theme tokens.")
}
- Flickable {
- anchors.fill: parent
- contentWidth: width
- contentHeight: contentColumn.height
- clip: true
+ FormSection {
+ objectName: "designSystemAppearanceSection"
+ Layout.fillWidth: true
+ title: qsTr("Appearance")
- ColumnLayout {
- id: contentColumn
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- spacing: 24
+ FormRow {
+ objectName: "designSystemThemeRow"
+ Layout.fillWidth: true
+ title: qsTr("Theme")
+ description: qsTr("Choose the application appearance.")
+ trailingItem: SegmentedPicker {
+ implicitWidth: 190
+ implicitHeight: 36
+ model: [qsTr("Light"), qsTr("Dark")]
+ currentIndex: Theme.dark ? 1 : 0
+ onSelected: function (index, option) {
+ Theme.dark = index === 1;
+ }
+ }
+ }
- // ── Typography ──────────────────────────────────────────
- Text {
- Layout.topMargin: 8
- Layout.fillWidth: true
- font: Theme.text.title.font
- color: Theme.color.neutral9
- text: qsTr("Typography")
+ FormRow {
+ objectName: "designSystemLanguageRow"
+ Layout.fillWidth: true
+ title: qsTr("Language")
+ description: qsTr("Choose the language used throughout the app.")
+ showDivider: false
+ trailingItem: PopupPicker {
+ objectName: "designSystemLanguagePicker"
+ embedded: true
+ minimumMenuWidth: 180
+ currentValue: root.exampleLanguage
+ model: [
+ {
+ text: qsTr("English"),
+ value: "en"
+ },
+ {
+ text: qsTr("Deutsch"),
+ value: "de"
+ },
+ {
+ text: qsTr("Español"),
+ value: "es"
+ }
+ ]
+ onActivated: function (value) {
+ root.exampleLanguage = value;
+ }
}
+ }
+ }
- Repeater {
- model: root.typographyRoles
- delegate: ColumnLayout {
- Layout.fillWidth: true
- spacing: 4
+ FormSection {
+ objectName: "designSystemBehaviorSection"
+ Layout.fillWidth: true
+ title: qsTr("Behavior")
+ description: qsTr("Rows can host any existing control without owning its state.")
- Text {
- Layout.fillWidth: true
- font: Theme.text[modelData.name].font
- lineHeight: Theme.text[modelData.name].lineHeight
- lineHeightMode: Text.FixedHeight
- color: Theme.color.neutral9
- text: modelData.name
- elide: Text.ElideRight
- }
- Text {
- Layout.fillWidth: true
- font: Theme.text.caption.font
- color: Theme.color.neutral6
- text: Theme.text[modelData.name].family + " " +
- Theme.text[modelData.name].styleName + " · " +
- Theme.text[modelData.name].pixelSize + "/" +
- Theme.text[modelData.name].lineHeight + " · " +
- modelData.group
- }
- Rectangle {
- Layout.topMargin: 8
- Layout.fillWidth: true
- height: 1
- color: Theme.color.neutral3
+ FormRow {
+ objectName: "designSystemBlockClockRow"
+ Layout.fillWidth: true
+ title: qsTr("Block status size")
+ description: qsTr("Set the scale used for the block status display.")
+ trailingItem: SegmentedPicker {
+ implicitWidth: 220
+ implicitHeight: 36
+ model: [
+ {
+ text: qsTr("Compact"),
+ value: "compact"
+ },
+ {
+ text: qsTr("Showcase"),
+ value: "showcase"
}
+ ]
+ currentIndex: root.exampleBlockClockMode === "compact" ? 0 : 1
+ onSelected: function (index, option) {
+ root.exampleBlockClockMode = option.value;
}
}
+ }
+
+ FormRow {
+ objectName: "designSystemStartupRow"
+ Layout.fillWidth: true
+ title: qsTr("Open at login")
+ description: qsTr("Start the app after signing in.")
+ showDivider: false
+ trailingItem: OptionSwitch {
+ objectName: "designSystemStartupSwitch"
+ checked: root.exampleStartupEnabled
+ onToggled: root.exampleStartupEnabled = checked
+ }
+ }
+ }
+
+ FormSection {
+ objectName: "designSystemNavigationSection"
+ Layout.fillWidth: true
+ title: qsTr("Navigation rows")
+ description: qsTr("Use ListRow for destinations and disclosure actions.")
+
+ ListRow {
+ objectName: "designSystemSelectedListRow"
+ Layout.fillWidth: true
+ title: qsTr("Selected destination")
+ description: qsTr("Selection and keyboard focus are independent states.")
+ selected: true
+ showsDisclosureIndicator: true
+ disclosureIndicatorColor: Theme.color.orange
+ }
+
+ ListRow {
+ objectName: "designSystemDisclosureListRow"
+ Layout.fillWidth: true
+ title: qsTr("Advanced options")
+ description: qsTr("Open another page for settings that need more space.")
+ showDivider: false
+ showsDisclosureIndicator: true
+ }
+ }
+
+ FormSection {
+ objectName: "designSystemValueSection"
+ Layout.fillWidth: true
+ title: qsTr("Values and links")
+ description: qsTr("Use value rows for read-only data and link rows for caller-owned navigation.")
+
+ LinkRow {
+ objectName: "designSystemWebsiteRow"
+ Layout.fillWidth: true
+ title: qsTr("Website")
+ value: "bitcoincore.org"
+ link: "https://bitcoincore.org"
+ onActivated: function (link) {
+ root.lastExampleLink = link;
+ }
+ }
+
+ LinkRow {
+ objectName: "designSystemSourceRow"
+ Layout.fillWidth: true
+ title: qsTr("Source code")
+ value: "github.com/bitcoin/bitcoin"
+ link: "https://github.com/bitcoin/bitcoin"
+ onActivated: function (link) {
+ root.lastExampleLink = link;
+ }
+ }
+
+ ValueRow {
+ objectName: "designSystemVersionRow"
+ Layout.fillWidth: true
+ title: qsTr("Version")
+ value: "v31.99.0-unk"
+ showDivider: false
+ }
+ }
+
+ FormSection {
+ objectName: "designSystemFieldSection"
+ Layout.fillWidth: true
+ title: qsTr("Inline fields and details")
+ description: qsTr("Use compact trailing editors for short values and body content for long details.")
+
+ TextFieldRow {
+ objectName: "designSystemBlockStorageRow"
+ fieldObjectName: "designSystemBlockStorageField"
+ Layout.fillWidth: true
+ title: qsTr("Block storage limit (GB)")
+ fieldWidth: 72
+ text: root.exampleBlockStorageLimit
+ validator: IntValidator {
+ bottom: 1
+ }
+ onTextEdited: function (text) {
+ root.exampleBlockStorageLimit = text;
+ }
+ }
+
+ TextFieldRow {
+ objectName: "designSystemProxyLocationRow"
+ fieldObjectName: "designSystemProxyLocationField"
+ Layout.fillWidth: true
+ title: qsTr("Proxy location")
+ fieldWidth: 200
+ text: root.exampleProxyAddress
+ onTextEdited: function (text) {
+ root.exampleProxyAddress = text;
+ }
+ }
+
+ FormRow {
+ objectName: "designSystemDataDirectoryRow"
+ Layout.fillWidth: true
+ title: qsTr("Data directory")
+ supportingText: qsTr("Selected before startup. The data directory cannot be changed while the node is running.")
+ showDivider: false
+ bodyItem: CoreText {
+ objectName: "designSystemDataDirectoryValue"
+ Layout.fillWidth: true
+ text: "/Users/example/Bitcoin"
+ color: Theme.color.neutral7
+ font: Theme.text.caption.font
+ lineHeight: Theme.text.caption.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+ }
+
+ // ── Typography ──────────────────────────────────────────
+ Text {
+ Layout.topMargin: 8
+ Layout.fillWidth: true
+ font: Theme.text.title.font
+ color: Theme.color.neutral9
+ text: qsTr("Typography")
+ }
+
+ Repeater {
+ model: root.typographyRoles
+ delegate: ColumnLayout {
+ id: typographySample
+ required property var modelData
+ Layout.fillWidth: true
+ spacing: 4
- // ── Colors ──────────────────────────────────────────────
Text {
- Layout.topMargin: 16
Layout.fillWidth: true
- font: Theme.text.title.font
+ font: Theme.text[typographySample.modelData.name].font
+ lineHeight: Theme.text[typographySample.modelData.name].lineHeight
+ lineHeightMode: Text.FixedHeight
color: Theme.color.neutral9
- text: qsTr("Colors")
+ text: typographySample.modelData.name
+ elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
font: Theme.text.caption.font
color: Theme.color.neutral6
- text: qsTr("Palette tokens for the active theme. Toggle Theme to compare.")
- wrapMode: Text.WordWrap
+ text: Theme.text[typographySample.modelData.name].family + " " + Theme.text[typographySample.modelData.name].styleName + " · " + Theme.text[typographySample.modelData.name].pixelSize + "/" + Theme.text[typographySample.modelData.name].lineHeight + " · " + typographySample.modelData.group
}
+ Rectangle {
+ Layout.topMargin: 8
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Theme.color.neutral3
+ }
+ }
+ }
- GridLayout {
+ // ── Colors ──────────────────────────────────────────────
+ Text {
+ Layout.topMargin: 16
+ Layout.fillWidth: true
+ font: Theme.text.title.font
+ color: Theme.color.neutral9
+ text: qsTr("Colors")
+ }
+ Text {
+ Layout.fillWidth: true
+ font: Theme.text.caption.font
+ color: Theme.color.neutral6
+ text: qsTr("Palette tokens for the active theme. Toggle Theme to compare.")
+ wrapMode: Text.WordWrap
+ }
+
+ GridLayout {
+ Layout.fillWidth: true
+ columns: 2
+ columnSpacing: 12
+ rowSpacing: 8
+
+ Repeater {
+ model: root.paletteTokens
+ delegate: RowLayout {
+ id: paletteSample
+ required property string modelData
Layout.fillWidth: true
- columns: 2
- columnSpacing: 12
- rowSpacing: 8
+ spacing: 10
- Repeater {
- model: root.paletteTokens
- delegate: RowLayout {
+ Rectangle {
+ Layout.preferredWidth: 32
+ Layout.preferredHeight: 32
+ radius: 4
+ color: Theme.color[paletteSample.modelData]
+ border.color: Theme.color.neutral4
+ border.width: 1
+ }
+ ColumnLayout {
+ Layout.fillWidth: true
+ spacing: 0
+ Text {
Layout.fillWidth: true
- spacing: 10
-
- Rectangle {
- Layout.preferredWidth: 32
- Layout.preferredHeight: 32
- radius: 4
- color: Theme.color[modelData]
- border.color: Theme.color.neutral4
- border.width: 1
- }
- ColumnLayout {
- Layout.fillWidth: true
- spacing: 0
- Text {
- Layout.fillWidth: true
- font: Theme.text.description.font
- color: Theme.color.neutral9
- text: modelData
- elide: Text.ElideRight
- }
- Text {
- Layout.fillWidth: true
- font: Theme.text.caption.font
- color: Theme.color.neutral6
- text: Theme.color[modelData].toString().toUpperCase()
- }
- }
+ font: Theme.text.description.font
+ color: Theme.color.neutral9
+ text: paletteSample.modelData
+ elide: Text.ElideRight
+ }
+ Text {
+ Layout.fillWidth: true
+ font: Theme.text.caption.font
+ color: Theme.color.neutral6
+ text: Theme.color[paletteSample.modelData].toString().toUpperCase()
}
}
}
-
- Item { Layout.preferredHeight: 24 }
}
}
+
+ Item {
+ Layout.preferredHeight: 24
+ }
}
diff --git a/qml/pages/settings/SettingsDisplay.qml b/qml/pages/settings/SettingsDisplay.qml
deleted file mode 100644
index 8b2685af7b..0000000000
--- a/qml/pages/settings/SettingsDisplay.qml
+++ /dev/null
@@ -1,264 +0,0 @@
-// 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 {
- signal back
- property bool showBackButton: true
-
- id: root
-
- PageStack {
- id: displaySettingsView
- anchors.fill: parent
-
- initialItem: Page {
- id: displaySettings
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: SettingsHeader {
- title: qsTr("Display")
- showBackButton: root.showBackButton
- backButtonObjectName: "settingsDisplayBack"
- onBack: root.back()
- }
- ColumnLayout {
- spacing: 4
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- Setting {
- id: gotoTheme
- objectName: "gotoTheme"
- Layout.fillWidth: true
- header: qsTr("Theme")
- actionItem: CaretRightIcon {
- color: gotoTheme.stateColor
- }
- onClicked: {
- displaySettingsView.push(theme_page)
- }
- }
- Separator { Layout.fillWidth: true }
- Setting {
- id: gotoBlockClockSize
- Layout.fillWidth: true
- header: qsTr("Block status size")
- actionItem: CaretRightIcon {
- color: gotoBlockClockSize.stateColor
- }
- onClicked: {
- displaySettingsView.push(blockclocksize_page)
- }
- }
- Separator { Layout.fillWidth: true }
- Setting {
- id: gotoDisplayUnit
- objectName: "gotoDisplayUnit"
- Layout.fillWidth: true
- header: qsTr("Display unit")
- actionItem: CaretRightIcon {
- color: gotoDisplayUnit.stateColor
- }
- onClicked: {
- displaySettingsView.push(displayunit_page)
- }
- }
- Separator { Layout.fillWidth: true }
- Setting {
- id: gotoLanguage
- objectName: "gotoLanguage"
- readonly property var settingStatus: (optionsModel.coreSettingStatuses || ({})).lang || ({})
- Layout.fillWidth: true
- header: qsTr("Language")
- state: settingStatus.canEdit === false ? "DISABLED" : "FILLED"
- infoText: settingStatus.infoText || ""
- showInfoText: infoText.length > 0
- actionItem: CaretRightIcon {
- color: gotoLanguage.stateColor
- }
- onClicked: {
- displaySettingsView.push(language_page)
- }
- }
- Separator { Layout.fillWidth: true }
- Setting {
- id: gotoThirdPartyUrls
- objectName: "gotoThirdPartyTransactionUrls"
- Layout.fillWidth: true
- header: qsTr("Third-party transaction URLs")
- actionItem: CaretRightIcon {
- color: gotoThirdPartyUrls.stateColor
- }
- onClicked: displaySettingsView.push(third_party_urls_page)
- }
- Separator { Layout.fillWidth: true }
- Setting {
- id: gotoMoneyFont
- objectName: "gotoMoneyFont"
- Layout.fillWidth: true
- header: qsTr("Money font")
- actionItem: CaretRightIcon {
- color: gotoMoneyFont.stateColor
- }
- onClicked: displaySettingsView.push(money_font_page)
- }
- }
- }
- }
- Component {
- id: theme_page
- SettingsTheme {
- onBack: {
- displaySettingsView.pop()
- }
- onDesignSystemRequested: {
- displaySettingsView.push(design_system_page)
- }
- }
- }
- Component {
- id: blockclocksize_page
- SettingsBlockClockDisplayMode {
- onBack: {
- displaySettingsView.pop()
- }
- }
- }
- Component {
- id: design_system_page
- SettingsDesignSystem {
- onBack: {
- displaySettingsView.pop()
- }
- }
- }
- Component {
- id: displayunit_page
- SettingsDisplayUnit {
- onBack: {
- displaySettingsView.pop()
- }
- }
- }
- Component {
- id: language_page
- SettingsLanguage {
- onBack: {
- displaySettingsView.pop()
- }
- }
- }
- Component {
- id: third_party_urls_page
- Page {
- background: null
- implicitWidth: 450
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: NavigationBar2 {
- leftItem: NavButton {
- iconSource: "image://images/caret-left"
- text: qsTr("Back")
- onClicked: displaySettingsView.pop()
- }
- centerItem: Header {
- headerBold: true
- headerSize: 18
- header: qsTr("Transaction URLs")
- }
- }
-
- ColumnLayout {
- spacing: 15
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
-
- Header {
- Layout.fillWidth: true
- center: false
- header: qsTr("Third-party transaction URLs")
- headerSize: 18
- description: qsTr("Use %s for the transaction hash. Separate multiple URLs with |.")
- descriptionSize: 15
- }
-
- CoreTextField {
- objectName: "thirdPartyTransactionUrlsInput"
- Layout.fillWidth: true
- text: optionsModel.thirdPartyTransactionUrls
- placeholderText: "https://example.com/tx/%s"
- onEditingFinished: optionsModel.thirdPartyTransactionUrls = text
- }
- }
- }
- }
- Component {
- id: money_font_page
- Page {
- background: null
- implicitWidth: 450
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: NavigationBar2 {
- leftItem: NavButton {
- iconSource: "image://images/caret-left"
- text: qsTr("Back")
- onClicked: displaySettingsView.pop()
- }
- centerItem: Header {
- headerBold: true
- headerSize: 18
- header: qsTr("Money font")
- }
- }
-
- ColumnLayout {
- spacing: 15
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
-
- OptionButton {
- objectName: "moneyFontEmbedded"
- Layout.fillWidth: true
- text: qsTr("Embedded fixed-width font")
- description: "111.11111111 BTC"
- checked: optionsModel.moneyFontChoice === "embedded"
- onClicked: optionsModel.moneyFontChoice = "embedded"
- }
-
- OptionButton {
- objectName: "moneyFontSystem"
- Layout.fillWidth: true
- text: qsTr("System fixed-width font")
- description: "111.11111111 BTC"
- checked: optionsModel.moneyFontChoice === "best_system"
- onClicked: optionsModel.moneyFontChoice = "best_system"
- }
-
- CoreText {
- objectName: "moneyFontPreview"
- Layout.fillWidth: true
- text: "111.11111111 BTC"
- color: Theme.color.neutral9
- horizontalAlignment: Text.AlignHCenter
- font.family: optionsModel.moneyFont.family
- font.weight: optionsModel.moneyFont.weight
- font.pixelSize: 20
- }
- }
- }
- }
-}
diff --git a/qml/pages/settings/SettingsDisplayUnit.qml b/qml/pages/settings/SettingsDisplayUnit.qml
deleted file mode 100644
index 61c492ae2f..0000000000
--- a/qml/pages/settings/SettingsDisplayUnit.qml
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2026 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"
-
-Page {
- id: root
- signal back
-
- objectName: "settingsDisplayUnitPage"
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: SettingsHeader {
- title: qsTr("Display unit")
- backButtonObjectName: "settingsDisplayUnitBack"
- onBack: root.back()
- }
-
- ColumnLayout {
- spacing: 15
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
-
- OptionButton {
- objectName: "displayUnitBTC"
- Layout.fillWidth: true
- text: qsTr("BTC")
- description: qsTr("8 decimal places (0.00000001 BTC = 1 sat)")
- checked: optionsModel.displayUnit === 0
- onClicked: optionsModel.displayUnit = 0
- }
-
- OptionButton {
- objectName: "displayUnitMBTC"
- Layout.fillWidth: true
- text: qsTr("mBTC")
- description: qsTr("5 decimal places (0.00001 mBTC = 1 sat)")
- checked: optionsModel.displayUnit === 1
- onClicked: optionsModel.displayUnit = 1
- }
-
- OptionButton {
- objectName: "displayUnitUBTC"
- Layout.fillWidth: true
- text: qsTr("bits")
- description: qsTr("2 decimal places (0.01 bits = 1 sat)")
- checked: optionsModel.displayUnit === 2
- onClicked: optionsModel.displayUnit = 2
- }
-
- OptionButton {
- objectName: "displayUnitSAT"
- Layout.fillWidth: true
- text: qsTr("sat")
- description: qsTr("Satoshi, the smallest unit (1 sat = 0.00000001 BTC)")
- checked: optionsModel.displayUnit === 3
- onClicked: optionsModel.displayUnit = 3
- }
- }
-}
diff --git a/qml/pages/settings/SettingsTheme.qml b/qml/pages/settings/SettingsTheme.qml
deleted file mode 100644
index c6e43af9a3..0000000000
--- a/qml/pages/settings/SettingsTheme.qml
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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"
-
-Page {
- signal back
- signal designSystemRequested
-
- id: root
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: SettingsHeader {
- title: qsTr("Theme")
- onBack: root.back()
- }
- ThemeSettings {
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- onDesignSystemRequested: root.designSystemRequested()
- }
-}
\ No newline at end of file
diff --git a/qml/pages/settings/SettingsWallet.qml b/qml/pages/settings/SettingsWallet.qml
deleted file mode 100644
index ba55892808..0000000000
--- a/qml/pages/settings/SettingsWallet.qml
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (c) 2026 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"
-
-Page {
- id: root
- objectName: "settingsWallet"
-
- signal back
- property bool showBackButton: true
- readonly property int maximumContentWidth: 450
-
- background: null
- contentWidth: root.maximumContentWidth
-
- header: SettingsHeader {
- title: qsTr("External signer")
- showBackButton: root.showBackButton
- backButtonObjectName: "settingsWalletBack"
- onBack: root.back()
- }
-
- ScrollView {
- anchors.fill: parent
- contentWidth: width
- clip: true
-
- ColumnLayout {
- width: Math.min(parent.width, root.maximumContentWidth)
- anchors.horizontalCenter: parent.horizontalCenter
- spacing: 0
-
- SettingsRestartNotice {
- objectName: "walletRestartNotice"
- visible: optionsModel.walletSettingsDirty
- Layout.fillWidth: true
- Layout.topMargin: 10
- Layout.bottomMargin: 20
- }
-
- WalletSettings {
- Layout.fillWidth: true
- }
- }
- }
-}
diff --git a/qml/pages/settings/SettingsWindowBehavior.qml b/qml/pages/settings/SettingsWindowBehavior.qml
deleted file mode 100644
index 9d33d203d9..0000000000
--- a/qml/pages/settings/SettingsWindowBehavior.qml
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) 2021-2026 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 {
- id: root
- objectName: "windowBehaviorPage"
- signal back
- property bool showBackButton: true
-
- property var windowBehaviorModel: desktopWindowBehaviorModel
-
- Page {
- anchors.fill: parent
- background: null
- leftPadding: 20
- rightPadding: 20
- topPadding: 30
-
- header: SettingsHeader {
- title: qsTr("Window behavior")
- showBackButton: root.showBackButton
- backButtonObjectName: "windowBehaviorBack"
- onBack: root.back()
- }
-
- ColumnLayout {
- spacing: 4
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
-
- Setting {
- id: showTrayIconSetting
- Layout.fillWidth: true
- header: qsTr("Show tray icon")
- description: qsTr("Keep the app available in the system tray")
- disabled: !windowBehaviorModel.desktopPlatform
- actionItem: OptionSwitch {
- objectName: "showTrayIconSwitch"
- checked: windowBehaviorModel.showTrayIcon
- onToggled: windowBehaviorModel.showTrayIcon = checked
- }
- onClicked: windowBehaviorModel.showTrayIcon = !windowBehaviorModel.showTrayIcon
- }
-
- Separator { Layout.fillWidth: true }
-
- Setting {
- id: minimizeToTraySetting
- Layout.fillWidth: true
- header: qsTr("Minimize to tray")
- description: qsTr("Hide window to tray when minimized")
- disabled: !windowBehaviorModel.desktopPlatform ||
- !windowBehaviorModel.showTrayIcon
- actionItem: OptionSwitch {
- objectName: "minimizeToTraySwitch"
- checked: windowBehaviorModel.minimizeToTray
- enabled: windowBehaviorModel.desktopPlatform &&
- windowBehaviorModel.showTrayIcon
- onToggled: windowBehaviorModel.minimizeToTray = checked
- }
- onClicked: windowBehaviorModel.minimizeToTray = !windowBehaviorModel.minimizeToTray
- }
-
- Separator { Layout.fillWidth: true }
-
- Setting {
- id: minimizeOnCloseSetting
- Layout.fillWidth: true
- header: qsTr("Minimize on close")
- description: qsTr("Keep node running when the window is closed")
- disabled: !windowBehaviorModel.desktopPlatform
- actionItem: OptionSwitch {
- objectName: "minimizeOnCloseSwitch"
- checked: windowBehaviorModel.minimizeOnClose
- enabled: windowBehaviorModel.desktopPlatform
- onToggled: windowBehaviorModel.minimizeOnClose = checked
- }
- onClicked: windowBehaviorModel.minimizeOnClose = !windowBehaviorModel.minimizeOnClose
- }
- }
- }
-}
diff --git a/qml/pages/settings/StorageSettingsPage.qml b/qml/pages/settings/StorageSettingsPage.qml
new file mode 100644
index 0000000000..edf61e64e8
--- /dev/null
+++ b/qml/pages/settings/StorageSettingsPage.qml
@@ -0,0 +1,122 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+import "../../controls"
+import "../../components"
+
+SettingsPage {
+ id: root
+ objectName: "storageSettingsPage"
+ title: qsTr("Storage")
+ showBackButton: false
+
+ property var settingsModel: optionsModel
+ property var coreSettingsModel: settingsModel.coreSettings
+ readonly property var pruneSetting: coreSettingsModel.entry("prune")
+ readonly property bool hasStorageResult: root.settingsModel
+ && root.settingsModel["storageAvailableText"] !== undefined
+ && root.settingsModel.storageAvailableText.length > 0
+ && !root.settingsModel.storageCheckPending
+ readonly property int availableStorageGB: root.hasStorageResult
+ ? root.settingsModel.storageAvailableGB
+ : 0
+ readonly property int assumedChainstateSizeGB: root.settingsModel
+ && root.settingsModel["assumedChainstateSize"] !== undefined
+ ? root.settingsModel.assumedChainstateSize
+ : 0
+ readonly property int maxPruneSizeGB: root.hasStorageResult
+ ? Math.max(0, root.availableStorageGB - root.assumedChainstateSizeGB)
+ : 0
+ property string pruneTargetText: String(pruneSetting.value)
+ property string pruneTargetError: ""
+
+ function validatePruneTarget(value) {
+ if (isNaN(value) || value < 1) {
+ return qsTr("Choose a storage limit of at least 1 GB.")
+ }
+ if (root.hasStorageResult && value > root.maxPruneSizeGB) {
+ if (root.maxPruneSizeGB < 1) {
+ return qsTr("There is not enough available storage for reduced storage in this data directory.")
+ }
+ return qsTr("Choose a value between 1 GB and %1 GB for this data directory.").arg(root.maxPruneSizeGB)
+ }
+ return ""
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Block storage")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Store recent blocks only")
+ supportingText: root.pruneSetting.infoText
+ enabled: root.pruneSetting.canEdit
+ trailingItem: OptionSwitch {
+ objectName: "pruneSwitch"
+ checked: root.pruneSetting.enabled
+ onToggled: root.pruneSetting.enabled = checked
+ }
+ }
+
+ TextFieldRow {
+ id: pruneTargetRow
+ Layout.fillWidth: true
+ title: qsTr("Block storage limit (GB)")
+ enabled: root.pruneSetting.enabled && root.pruneSetting.canEdit
+ fieldObjectName: "pruneTargetInput"
+ fieldWidth: 80
+ text: root.pruneTargetText
+ validator: IntValidator { bottom: 1 }
+ errorText: root.pruneTargetError
+ supportingText: root.pruneTargetError.length === 0 ? root.pruneSetting.infoText : ""
+ showDivider: false
+ onTextEdited: function(text) {
+ root.pruneTargetText = text
+ root.pruneTargetError = ""
+ }
+ onEditingFinished: {
+ const parsed = parseInt(pruneTargetRow.text)
+ root.pruneTargetError = root.validatePruneTarget(parsed)
+ if (root.pruneTargetError.length === 0) {
+ root.pruneSetting.value = parsed
+ root.pruneTargetText = String(parsed)
+ }
+ }
+ }
+ }
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Data directory")
+ description: qsTr("Selected before startup. The data directory cannot be changed while the node is running.")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Location")
+ showDivider: false
+ bodyItem: CoreText {
+ objectName: "dataDirectoryValue"
+ Layout.fillWidth: true
+ text: root.settingsModel.dataDir
+ color: Theme.color.neutral7
+ font: Theme.text.caption.font
+ lineHeight: Theme.text.caption.lineHeight
+ lineHeightMode: Text.FixedHeight
+ horizontalAlignment: Text.AlignLeft
+ wrap: true
+ }
+ }
+ }
+
+ SettingsRestartNotice {
+ objectName: "storageRestartNotice"
+ visible: root.settingsModel.storageSettingsDirty
+ Layout.fillWidth: true
+ Layout.maximumWidth: root.contentLayout.width
+ }
+}
diff --git a/qml/pages/settings/WalletSectionPage.qml b/qml/pages/settings/WalletSectionPage.qml
new file mode 100644
index 0000000000..397dd1d057
--- /dev/null
+++ b/qml/pages/settings/WalletSectionPage.qml
@@ -0,0 +1,210 @@
+// Copyright (c) 2026 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.Dialogs
+import QtQuick.Layouts 1.15
+
+import "../../controls"
+
+SettingsPage {
+ id: root
+ objectName: "walletSettingsPage"
+ title: qsTr("Wallet settings")
+ showBackButton: false
+
+ property var wallet: walletController.selectedWallet
+ property string errorText: ""
+ property string pendingDisplayName: root.wallet ? root.wallet.displayName : ""
+ readonly property bool walletLoaded: walletController.isWalletLoaded
+ readonly property bool canManagePassphrase: root.wallet !== null && root.wallet.canManagePassphrase
+
+ signal selectWalletRequested()
+ signal passwordRequested()
+ signal signVerifyMessageRequested()
+ signal addressesRequested()
+
+ function backupFileName() {
+ const walletName = root.wallet && root.wallet.name.length > 0
+ ? root.wallet.name.replace(/[\\/]/g, "_")
+ : "wallet"
+ return walletName + ".bak"
+ }
+
+ function backupDefaultFileUrl() {
+ return "file://" + walletController.homePath() + "/" + root.backupFileName()
+ }
+
+ function resolvedBackupPath(rawPath) {
+ let normalized = walletController.normalizeWalletPath(rawPath)
+ if (normalized.length === 0) return ""
+
+ const hasKnownSuffix = /\.(bak|dat)$/i.test(normalized)
+ if (walletController.walletPathExists(normalized) && !hasKnownSuffix) {
+ normalized += "/" + root.backupFileName()
+ } else if (!hasKnownSuffix) {
+ normalized += ".bak"
+ }
+ return normalized
+ }
+
+ function startBackup() {
+ if (!root.wallet) return
+ root.errorText = ""
+ root.wallet.clearSettingsError()
+ if (backupAutomationPath.text.length > 0) {
+ const automatedPath = root.resolvedBackupPath(backupAutomationPath.text)
+ backupAutomationPath.text = ""
+ if (!root.wallet.backupWallet(automatedPath)) root.errorText = root.wallet.settingsError
+ return
+ }
+ backupDialog.open()
+ }
+
+ FileDialog {
+ id: backupDialog
+ fileMode: FileDialog.SaveFile
+ currentFolder: "file://" + walletController.homePath()
+ currentFile: root.backupDefaultFileUrl()
+ defaultSuffix: "bak"
+ nameFilters: [qsTr("Wallet backup files (*.bak *.dat)"), qsTr("All files (*)")]
+ onAccepted: {
+ if (backupDialog.selectedFile.toString().length === 0) return
+ const normalized = root.resolvedBackupPath(backupDialog.selectedFile.toString())
+ if (!root.wallet.backupWallet(normalized)) root.errorText = root.wallet.settingsError
+ else root.errorText = ""
+ }
+ }
+
+ TextField {
+ id: backupAutomationPath
+ objectName: "walletSettingsBackupPathField"
+ visible: false
+ }
+
+ Connections {
+ target: root.wallet
+
+ function onSettingsErrorChanged() {
+ root.errorText = root.wallet ? root.wallet.settingsError : ""
+ }
+
+ function onDisplayNameChanged() {
+ root.pendingDisplayName = root.wallet ? root.wallet.displayName : ""
+ }
+ }
+
+ Connections {
+ target: walletController
+
+ function onSelectedWalletChanged() {
+ root.pendingDisplayName = root.wallet ? root.wallet.displayName : ""
+ root.errorText = ""
+ }
+ }
+
+ PageHeading {
+ visible: !root.walletLoaded
+ Layout.fillWidth: true
+ title: qsTr("No wallet selected")
+ description: qsTr("Select a wallet to manage wallet-specific settings.")
+ }
+
+ OutlineButton {
+ visible: !root.walletLoaded
+ Layout.preferredWidth: 220
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Select wallet")
+ onClicked: root.selectWalletRequested()
+ }
+
+ FormSection {
+ objectName: "walletInfoSection"
+ visible: root.walletLoaded
+ Layout.fillWidth: true
+ title: qsTr("Wallet info")
+
+ TextFieldRow {
+ Layout.fillWidth: true
+ title: qsTr("Name")
+ fieldObjectName: "walletNameInput"
+ fieldWidth: 220
+ text: root.pendingDisplayName
+ onTextEdited: function(text) { root.pendingDisplayName = text }
+ onEditingFinished: {
+ if (!root.wallet) return
+ if (!walletController.setWalletDisplayName(root.wallet.name, root.pendingDisplayName)) {
+ root.errorText = root.wallet.settingsError
+ }
+ }
+ }
+
+ ValueRow {
+ Layout.fillWidth: true
+ title: qsTr("Key scheme")
+ value: root.wallet ? root.wallet.keyScheme : ""
+ }
+
+ ValueRow {
+ Layout.fillWidth: true
+ title: qsTr("Private keys")
+ value: root.wallet ? root.wallet.privateKeysStatus : ""
+ }
+
+ ValueRow {
+ Layout.fillWidth: true
+ title: qsTr("External signer")
+ value: root.wallet ? root.wallet.externalSignerStatus : ""
+ showDivider: false
+ }
+ }
+
+ FormSection {
+ objectName: "walletActionsSection"
+ visible: root.walletLoaded
+ Layout.fillWidth: true
+ title: qsTr("Wallet actions")
+
+ ListRow {
+ objectName: "walletAddressesRow"
+ Layout.fillWidth: true
+ title: qsTr("Addresses")
+ showsDisclosureIndicator: true
+ onClicked: root.addressesRequested()
+ }
+
+ ListRow {
+ objectName: "walletPasswordRow"
+ visible: root.canManagePassphrase
+ Layout.fillWidth: true
+ title: root.wallet && root.wallet.isEncrypted ? qsTr("Update password") : qsTr("Set password")
+ showsDisclosureIndicator: true
+ onClicked: root.passwordRequested()
+ }
+
+ ListRow {
+ objectName: "walletBackupRow"
+ Layout.fillWidth: true
+ title: qsTr("Back up wallet")
+ showsDisclosureIndicator: true
+ onClicked: root.startBackup()
+ }
+
+ ListRow {
+ objectName: "walletSignVerifyMessageRow"
+ Layout.fillWidth: true
+ title: qsTr("Sign or verify message")
+ showDivider: false
+ showsDisclosureIndicator: true
+ onClicked: root.signVerifyMessageRequested()
+ }
+ }
+
+ FormRow {
+ visible: root.errorText.length > 0
+ Layout.fillWidth: true
+ errorText: root.errorText
+ }
+}
diff --git a/qml/pages/settings/WindowBehaviorSettingsPage.qml b/qml/pages/settings/WindowBehaviorSettingsPage.qml
new file mode 100644
index 0000000000..940cb0e433
--- /dev/null
+++ b/qml/pages/settings/WindowBehaviorSettingsPage.qml
@@ -0,0 +1,59 @@
+// Copyright (c) 2026 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.Layouts 1.15
+
+import "../../controls"
+
+SettingsPage {
+ id: root
+ objectName: "windowBehaviorSettingsPage"
+ title: qsTr("Window behavior")
+ showBackButton: false
+
+ property var windowBehaviorModel: desktopWindowBehaviorModel
+
+ FormSection {
+ Layout.fillWidth: true
+ title: qsTr("Window behavior")
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Show tray icon")
+ description: qsTr("Keep the app available in the system tray.")
+ enabled: root.windowBehaviorModel.desktopPlatform
+ trailingItem: OptionSwitch {
+ objectName: "showTrayIconSwitch"
+ checked: root.windowBehaviorModel.showTrayIcon
+ onToggled: root.windowBehaviorModel.showTrayIcon = checked
+ }
+ }
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Minimize to tray")
+ description: qsTr("Hide the window in the tray when minimized.")
+ enabled: root.windowBehaviorModel.desktopPlatform
+ && root.windowBehaviorModel.showTrayIcon
+ trailingItem: OptionSwitch {
+ objectName: "minimizeToTraySwitch"
+ checked: root.windowBehaviorModel.minimizeToTray
+ onToggled: root.windowBehaviorModel.minimizeToTray = checked
+ }
+ }
+
+ FormRow {
+ Layout.fillWidth: true
+ title: qsTr("Minimize on close")
+ description: qsTr("Keep the node running when the window is closed.")
+ enabled: root.windowBehaviorModel.desktopPlatform
+ showDivider: false
+ trailingItem: OptionSwitch {
+ objectName: "minimizeOnCloseSwitch"
+ checked: root.windowBehaviorModel.minimizeOnClose
+ onToggled: root.windowBehaviorModel.minimizeOnClose = checked
+ }
+ }
+ }
+}
diff --git a/qml/pages/wallet/DesktopWallets.qml b/qml/pages/wallet/DesktopWallets.qml
index a369e83604..a32c2f5e31 100644
--- a/qml/pages/wallet/DesktopWallets.qml
+++ b/qml/pages/wallet/DesktopWallets.qml
@@ -57,11 +57,16 @@ Page {
}
}
+ function openSettingsRoute(route) {
+ settingsLoader.pendingRoute = route
+ settingsTabButton.checked = true
+ Qt.callLater(settingsLoader.applyPendingRoute)
+ }
+
Connections {
target: walletController
function onOpenWalletSettingsRequested() {
- settingsTabButton.checked = true
- nodeSettings.openWalletSettings()
+ root.openSettingsRoute("wallet")
}
function onOpenReceiveRequested() {
receiveTabButton.checked = true
@@ -243,31 +248,13 @@ Page {
text: qsTr("Peers")
}
}
- NavigationTab {
- id: consoleTabButton
- objectName: "consoleTabButton"
- iconSource: "image://images/console"
- iconColor: Theme.color.neutral7
- iconSize: 24
- Layout.preferredWidth: 30
- property int index: 5
- ButtonGroup.group: navigationTabs
-
- Tooltip {
- anchors.top: consoleTabButton.bottom
- anchors.topMargin: -5
- anchors.horizontalCenter: consoleTabButton.horizontalCenter
- visible: consoleTabButton.hovered
- text: qsTr("Console")
- }
- }
NavigationTab {
id: settingsTabButton
objectName: "desktopWalletSettingsTabButton"
iconSource: "image://images/gear-outline"
iconColor: Theme.color.neutral7
Layout.preferredWidth: 30
- property int index: 6
+ property int index: 5
ButtonGroup.group: navigationTabs
Tooltip {
@@ -304,8 +291,7 @@ Page {
}
RequestPayment {
onAddressHistoryRequested: {
- settingsTabButton.checked = true
- nodeSettings.openWalletAddressHistory()
+ root.openSettingsRoute("addresses")
}
}
Item {
@@ -348,19 +334,33 @@ Page {
}
}
}
- CommandConsole {
- showHeader: false
- tabActive: consoleTabButton.checked
- walletName: walletController.isWalletLoaded && walletController.selectedWallet
- ? walletController.selectedWallet.name
- : ""
- }
- NodeSettings {
- id: nodeSettings
- showDoneButton: false
- onSelectWalletRequested: root.openWalletSelection()
- onReceiveRequested: {
- receiveTabButton.checked = true
+ Item {
+ Loader {
+ id: settingsLoader
+ objectName: "settingsLoader"
+ anchors.fill: parent
+ property bool retainItem: false
+ property string pendingRoute: ""
+
+ function applyPendingRoute() {
+ if (!item || pendingRoute.length === 0) return
+ if (pendingRoute === "addresses") item.openWalletAddressHistory()
+ else item.selectSection(pendingRoute)
+ pendingRoute = ""
+ }
+
+ // Create Settings on first use, then retain its navigation
+ // stacks while the parent tab item is hidden.
+ active: settingsTabButton.checked || retainItem
+ onLoaded: {
+ retainItem = true
+ Qt.callLater(applyPendingRoute)
+ }
+ sourceComponent: SettingsView {
+ showDoneButton: false
+ onSelectWalletRequested: root.openWalletSelection()
+ onReceiveRequested: receiveTabButton.checked = true
+ }
}
}
}
diff --git a/qml/pages/wallet/WalletSettings.qml b/qml/pages/wallet/WalletSettings.qml
deleted file mode 100644
index ab91761bf3..0000000000
--- a/qml/pages/wallet/WalletSettings.qml
+++ /dev/null
@@ -1,352 +0,0 @@
-// Copyright (c) 2026 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 QtQuick.Dialogs
-
-import org.bitcoincore.qt 1.0
-
-import "../../controls"
-
-Page {
- id: root
- objectName: "walletSettingsPage"
-
- property WalletQmlModel wallet: walletController.selectedWallet
- property string errorText: ""
- property bool editingName: false
- property string pendingDisplayName: ""
-
- property bool showBackButton: true
-
- signal back()
- signal selectWalletRequested()
- signal passwordRequested()
- signal signVerifyMessageRequested()
- signal addressesRequested()
-
- readonly property bool walletLoaded: walletController.isWalletLoaded
- readonly property bool canManagePassphrase: root.wallet !== null && root.wallet.canManagePassphrase
-
- background: null
-
- function backupDefaultFileUrl() {
- const wallet_name = root.wallet && root.wallet.name.length > 0
- ? root.wallet.name.replace(/[\\/]/g, "_")
- : "wallet"
- return "file://" + walletController.homePath() + "/" + wallet_name + ".bak"
- }
-
- function backupFileName() {
- const wallet_name = root.wallet && root.wallet.name.length > 0
- ? root.wallet.name.replace(/[\\/]/g, "_")
- : "wallet"
- return wallet_name + ".bak"
- }
-
- function resolvedBackupPath(rawPath) {
- let normalized = walletController.normalizeWalletPath(rawPath)
- if (normalized.length === 0) {
- return ""
- }
-
- const hasKnownSuffix = /\.(bak|dat)$/i.test(normalized)
- if (walletController.walletPathExists(normalized) && !hasKnownSuffix) {
- normalized = normalized + "/" + root.backupFileName()
- } else if (!hasKnownSuffix) {
- normalized = normalized + ".bak"
- }
-
- return normalized
- }
-
- function startBackup() {
- if (!root.wallet) {
- return
- }
- root.errorText = ""
- root.wallet.clearSettingsError()
- if (backupAutomationPath.text.length > 0) {
- const automatedPath = root.resolvedBackupPath(backupAutomationPath.text)
- backupAutomationPath.text = ""
- if (!root.wallet.backupWallet(automatedPath)) {
- root.errorText = root.wallet.settingsError
- }
- return
- }
- backupDialog.open()
- }
-
- function beginNameEdit() {
- if (!root.wallet) {
- return
- }
- root.pendingDisplayName = root.wallet.displayName
- root.editingName = true
- }
-
- function cancelNameEdit() {
- root.pendingDisplayName = root.wallet ? root.wallet.displayName : ""
- root.editingName = false
- }
-
- function confirmNameEdit() {
- if (!root.wallet) {
- return
- }
- if (walletController.setWalletDisplayName(root.wallet.name, root.pendingDisplayName)) {
- root.editingName = false
- }
- }
-
- header: SettingsHeader {
- title: qsTr("Wallet settings")
- showBackButton: root.showBackButton
- backButtonObjectName: "walletSettingsBackButton"
- onBack: root.back()
- }
-
- FileDialog {
- id: backupDialog
- fileMode: FileDialog.SaveFile
- currentFolder: "file://" + walletController.homePath()
- currentFile: root.backupDefaultFileUrl()
- defaultSuffix: "bak"
- nameFilters: [qsTr("Wallet backup files (*.bak *.dat)"), qsTr("All files (*)")]
- onAccepted: {
- if (backupDialog.selectedFile.toString().length === 0) {
- return
- }
- const normalized = root.resolvedBackupPath(backupDialog.selectedFile.toString())
- if (!root.wallet.backupWallet(normalized)) {
- root.errorText = root.wallet.settingsError
- } else {
- root.errorText = ""
- }
- }
- }
-
- // Hidden automation hook so tests can inject a backup destination.
- TextField {
- id: backupAutomationPath
- objectName: "walletSettingsBackupPathField"
- visible: false
- }
-
- Connections {
- target: root.wallet
- function onSettingsErrorChanged() {
- root.errorText = root.wallet ? root.wallet.settingsError : ""
- }
- function onDisplayNameChanged() {
- if (!root.editingName) {
- root.pendingDisplayName = root.wallet ? root.wallet.displayName : ""
- }
- }
- }
-
- Connections {
- target: walletController
- function onSelectedWalletChanged() {
- root.editingName = false
- root.pendingDisplayName = root.wallet ? root.wallet.displayName : ""
- }
- }
-
- ColumnLayout {
- visible: !root.walletLoaded
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: parent.top
- spacing: 24
-
- Header {
- objectName: "walletSettingsEmptyHeader"
- Layout.fillWidth: true
- header: qsTr("No wallet selected")
- headerBold: true
- headerSize: 28
- description: qsTr("Select a wallet to manage wallet-specific settings.")
- }
-
- OutlineButton {
- objectName: "walletSettingsSelectWalletButton"
- Layout.preferredWidth: 220
- Layout.alignment: Qt.AlignCenter
- text: qsTr("Select wallet")
- onClicked: root.selectWalletRequested()
- }
- }
-
- ColumnLayout {
- visible: root.walletLoaded
- width: Math.min(parent.width, 450)
- anchors.horizontalCenter: parent.horizontalCenter
- anchors.top: parent.top
- spacing: 0
-
- EditableKeyValueRow {
- objectName: "walletSettingsNameRow"
- Layout.fillWidth: true
- Layout.topMargin: 12
- Layout.bottomMargin: 15
- keyObjectName: "walletSettingsNameKey"
- valueObjectName: "walletSettingsNameValue"
- editFieldObjectName: "walletSettingsNameEditField"
- editButtonObjectName: "walletSettingsNameEditButton"
- cancelButtonObjectName: "walletSettingsNameCancelButton"
- confirmButtonObjectName: "walletSettingsNameConfirmButton"
- label: qsTr("Name")
- displayValue: root.wallet ? root.wallet.displayName : ""
- editValue: root.pendingDisplayName
- editing: root.editingName
- onEditRequested: root.beginNameEdit()
- onCancelRequested: root.cancelNameEdit()
- onConfirmRequested: root.confirmNameEdit()
- onEditValueEdited: value => root.pendingDisplayName = value
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- KeyValueRow {
- keyWidth: 150
- Layout.fillWidth: true
- Layout.topMargin: 15
- Layout.bottomMargin: 15
- key: KeyText {
- objectName: "walletSettingsKeySchemeKey"
- text: qsTr("Key scheme")
- }
- value: ValueText {
- objectName: "walletSettingsKeySchemeValue"
- text: root.wallet ? root.wallet.keyScheme : ""
- }
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- KeyValueRow {
- keyWidth: 150
- Layout.fillWidth: true
- Layout.topMargin: 15
- Layout.bottomMargin: 15
- key: KeyText {
- objectName: "walletSettingsPrivateKeysKey"
- text: qsTr("Private keys")
- }
- value: ValueText {
- objectName: "walletSettingsPrivateKeysValue"
- text: root.wallet ? root.wallet.privateKeysStatus : ""
- }
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- KeyValueRow {
- keyWidth: 150
- Layout.fillWidth: true
- Layout.topMargin: 15
- Layout.bottomMargin: 15
- key: KeyText {
- objectName: "walletSettingsExternalSignerKey"
- text: qsTr("External signer")
- }
- value: ValueText {
- objectName: "walletSettingsExternalSignerValue"
- text: root.wallet ? root.wallet.externalSignerStatus : ""
- }
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- Setting {
- id: addressesSetting
- objectName: "settingsAddresses"
- Layout.fillWidth: true
- header: qsTr("Addresses")
- actionItem: CaretRightIcon {
- color: addressesSetting.stateColor
- }
- onClicked: root.addressesRequested()
- }
-
- Rectangle {
- objectName: "walletSettingsPasswordDivider"
- visible: root.canManagePassphrase
- Layout.fillWidth: true
- height: 1
- color: Theme.color.neutral4
- }
-
- Setting {
- id: passwordSetting
- objectName: "walletSettingsPasswordRow"
- visible: root.canManagePassphrase
- Layout.fillWidth: true
- header: root.wallet && root.wallet.isEncrypted ? qsTr("Update password") : qsTr("Set password")
- actionItem: CaretRightIcon {
- color: passwordSetting.stateColor
- }
- onClicked: root.passwordRequested()
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- Setting {
- id: backupSetting
- objectName: "walletSettingsBackupRow"
- Layout.fillWidth: true
- header: qsTr("Back up wallet")
- actionItem: CaretRightIcon {
- color: backupSetting.stateColor
- }
- onClicked: root.startBackup()
- }
-
- Rectangle { Layout.fillWidth: true; height: 1; color: Theme.color.neutral4 }
-
- Setting {
- id: signVerifyMessageSetting
- objectName: "walletSettingsSignVerifyMessageRow"
- Layout.fillWidth: true
- header: qsTr("Sign or verify message")
- actionItem: CaretRightIcon {
- color: signVerifyMessageSetting.stateColor
- }
- onClicked: root.signVerifyMessageRequested()
- }
-
- CoreText {
- objectName: "walletSettingsErrorText"
- Layout.fillWidth: true
- Layout.topMargin: 12
- visible: text.length > 0
- text: root.errorText
- color: Theme.color.red
- font.pixelSize: 15
- horizontalAlignment: Text.AlignLeft
- wrapMode: Text.WordWrap
- }
- }
-
- component KeyText: CoreText {
- color: Theme.color.neutral7
- font.pixelSize: 18
- fontStyleName: "Regular"
- wrap: false
- horizontalAlignment: Qt.AlignLeft
- verticalAlignment: Text.AlignVCenter
- }
-
- component ValueText: CoreText {
- color: Theme.color.neutral9
- font.pixelSize: 18
- fontStyleName: "Regular"
- horizontalAlignment: Qt.AlignRight
- verticalAlignment: Text.AlignVCenter
- wrapMode: Text.WordWrap
- }
-}
diff --git a/test/functional/qml_driver.py b/test/functional/qml_driver.py
index 8816281e78..603fa82d02 100644
--- a/test/functional/qml_driver.py
+++ b/test/functional/qml_driver.py
@@ -317,12 +317,12 @@ def set_clipboard_text(self, text):
def settle(
self,
timeout_ms=5000,
- stack_view_names=("mainPageStack", "createWalletWizard", "nodeSettingsStack"),
+ stack_view_names=("mainPageStack", "createWalletWizard", "settingsNavigationStack_wallet"),
):
"""Wait for relevant StackView transitions to finish.
The wallet flow transitions run through the app's main page stack and,
- once opened, the nested create-wallet wizard stack and settings stack.
+ once opened, the nested create-wallet wizard and wallet-settings stacks.
Waiting for their `busy` property to become false is more reliable than
sleeping. Missing stack views are ignored so this remains safe before
nested flows have been created.
diff --git a/test/functional/qml_test_activity_filter_export.py b/test/functional/qml_test_activity_filter_export.py
index 457ddacfe3..799f988ccb 100644
--- a/test/functional/qml_test_activity_filter_export.py
+++ b/test/functional/qml_test_activity_filter_export.py
@@ -221,11 +221,11 @@ def run_test(save_screenshots=False, screenshot_root=None):
checkpoints.checkpoint("search by request label applied", gui)
gui.click("desktopWalletSettingsTabButton")
- gui.wait_for_property("settings_display", "visible", True, timeout_ms=5000)
- gui.click("settings_display")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
+ gui.wait_for_property("settingsSidebar_display", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_display")
+ gui.wait_for_page("displayUnitPicker", timeout_ms=5000)
+ gui.click("displayUnitPickerButton")
+ gui.wait_for_page("displayUnitSAT", timeout_ms=5000)
gui.click("displayUnitSAT")
checkpoints.checkpoint("display unit switched to sats", gui)
diff --git a/test/functional/qml_test_addresses.py b/test/functional/qml_test_addresses.py
index 22b5154f1b..810b421f5a 100755
--- a/test/functional/qml_test_addresses.py
+++ b/test/functional/qml_test_addresses.py
@@ -90,10 +90,10 @@ def open_address_list_from_settings(gui):
gui.click("desktopWalletSettingsTabButton")
gui.wait_for_property("desktopWalletSettingsTabButton", "checked", True, timeout_ms=5000)
gui.settle()
- gui.wait_for_property("settings_wallet", "visible", True, timeout_ms=5000)
- gui.click("settings_wallet")
+ gui.wait_for_property("settingsSidebar_wallet", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_wallet")
gui.wait_for_property("walletSettingsPage", "visible", True, timeout_ms=5000)
- gui.click("settingsAddresses")
+ gui.click("walletAddressesRow")
gui.wait_for_property("addressListPage", "visible", True, timeout_ms=10000)
diff --git a/test/functional/qml_test_blocksonly_settings.py b/test/functional/qml_test_blocksonly_settings.py
index f9214a3672..638dc639db 100644
--- a/test/functional/qml_test_blocksonly_settings.py
+++ b/test/functional/qml_test_blocksonly_settings.py
@@ -23,17 +23,13 @@ def run_tests():
complete_onboarding(gui)
gui.wait_for_page("nodeSettingsButton", timeout_ms=30000)
gui.click("nodeSettingsButton")
- # Node settings now uses a sidebar layout that lands on the About
- # section; each sidebar row has objectName "settings_".
- gui.wait_for_page("settings_about", timeout_ms=10000)
+ gui.wait_for_page("settingsSidebar_about", timeout_ms=10000)
# The Mempool Information sidebar row is gated on
# nodeModel.mempoolInformationAvailable, which is false in -blocksonly
# mode, so the row must be hidden.
- mempool_visible = gui.get_property("settings_mempool", "visible")
- assert mempool_visible is False, (
- "Mempool Information settings row should be hidden in -blocksonly mode, "
- f"got {mempool_visible!r}"
+ assert not gui.object_exists("settingsSidebar_mempool"), (
+ "Mempool Information settings row should not be instantiated in -blocksonly mode"
)
print("Blocksonly settings smoke test PASSED")
diff --git a/test/functional/qml_test_console.py b/test/functional/qml_test_console.py
index 339cb431fd..97590ad375 100644
--- a/test/functional/qml_test_console.py
+++ b/test/functional/qml_test_console.py
@@ -5,7 +5,7 @@
"""End-to-end tests for the RPC command console.
Starts the GUI as a regtest node (no peers needed), completes onboarding,
-then navigates to Settings → Console and exercises command execution.
+then navigates to Settings → RPC console and exercises command execution.
The console is only reachable after the node has fully started. We use a
generous wait_for_page timeout (~90 s) for the initial node-runner screen
@@ -39,17 +39,20 @@
def navigate_to_console(gui):
- """From the NodeRunner main screen, navigate to the Console page.
+ """From the NodeRunner main screen, navigate to the RPC console settings page.
Waits for the node-runner screen (which only appears once the node is
- running), then clicks the console icon button in the header.
+ running), then opens Settings and selects RPC console in the sidebar.
"""
gui.wait_for_page(
- "consoleTabButton",
+ "nodeSettingsButton",
timeout_ms=NODE_RUNNING_TIMEOUT_MS,
)
- gui.click("consoleTabButton")
- gui.wait_for_page("commandConsole", timeout_ms=5000)
+ gui.click("nodeSettingsButton")
+ gui.wait_for_property("settingsSidebar_rpc-console", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_rpc-console")
+ gui.wait_for_page("rpcConsoleSettingsPage", timeout_ms=5000)
+ gui.wait_for_page("rpcConsole", timeout_ms=5000)
# The command input auto-focuses on open (desktop), mirroring Core's
# RPCConsole, so the user can type immediately.
gui.wait_for_property("consoleInput", "activeFocus", True, timeout_ms=5000)
@@ -66,14 +69,14 @@ def assert_close(actual, expected, label, tolerance=1):
def submit_console_command(gui, command):
gui.set_text("consoleInput", command)
- gui.invoke("commandConsole", "runHighlightedOrSubmit")
+ gui.invoke("rpcConsole", "runHighlightedOrSubmit")
def test_console_input_bar_matches_design(gui):
"""Console input bar follows the Figma Console input component geometry."""
print("\n── test_console_input_bar_matches_design ───────────────────────")
- root_width = gui.get_property("commandConsole", "width")
+ root_width = gui.get_property("rpcConsole", "width")
row_x = gui.get_property("consoleInputRow", "x")
row_width = gui.get_property("consoleInputRow", "width")
row_height = gui.get_property("consoleInputRow", "height")
@@ -104,19 +107,19 @@ def test_console_input_bar_matches_design(gui):
assert_close(action_height, 20, "console action cluster height")
assert_close(content_x + action_x, row_width - 95, "console action cluster right alignment")
assert gui.get_property("consoleInput", "placeholderText") == "Enter command..."
- assert gui.get_property("commandConsole", "searchMode") is False
+ assert gui.get_property("rpcConsole", "searchMode") is False
gui.click("consoleModeToggleButton")
- gui.wait_for_property("commandConsole", "searchMode", True, timeout_ms=3000)
+ gui.wait_for_property("rpcConsole", "searchMode", True, timeout_ms=3000)
assert gui.get_property("consoleInput", "placeholderText") == "Search..."
gui.click("consoleFontIncreaseButton")
- assert gui.get_property("commandConsole", "outputFontPixelSize") == 14
+ assert gui.get_property("rpcConsole", "outputFontPixelSize") == 14
gui.click("consoleFontDecreaseButton")
- assert gui.get_property("commandConsole", "outputFontPixelSize") == 13
+ assert gui.get_property("rpcConsole", "outputFontPixelSize") == 13
gui.click("consoleModeToggleButton")
- gui.wait_for_property("commandConsole", "searchMode", False, timeout_ms=3000)
+ gui.wait_for_property("rpcConsole", "searchMode", False, timeout_ms=3000)
assert gui.get_property("consoleInput", "placeholderText") == "Enter command..."
print(" PASSED: console input bar geometry and controls match the design component")
@@ -137,8 +140,8 @@ def test_console_output_rows_match_design(gui):
"""Console output rows follow the Figma Console entry component geometry."""
print("\n── test_console_output_rows_match_design ───────────────────────")
- gui.wait_for_property("commandConsole", "outputCount", lambda v: v >= 1, timeout_ms=3000)
- root_width = gui.get_property("commandConsole", "width")
+ gui.wait_for_property("rpcConsole", "outputCount", lambda v: v >= 1, timeout_ms=3000)
+ root_width = gui.get_property("rpcConsole", "width")
column_width = root_width - 40
assert_close(gui.get_property("consoleOutputArea_contentColumn", "x"), 20, "console output column x")
@@ -152,10 +155,10 @@ def test_console_output_rows_match_design(gui):
assert "Use ↑↓ arrows" in welcome_text
assert "help-console" in welcome_text
- count_before = gui.get_property("commandConsole", "outputCount")
+ count_before = gui.get_property("rpcConsole", "outputCount")
submit_console_command(gui, "getblockcount")
- gui.wait_for_property("commandConsole", "executing", False, timeout_ms=10000)
- gui.wait_for_property("commandConsole", "outputCount", count_before + 2, timeout_ms=3000)
+ gui.wait_for_property("rpcConsole", "executing", False, timeout_ms=10000)
+ gui.wait_for_property("rpcConsole", "outputCount", count_before + 2, timeout_ms=3000)
request_index = count_before
reply_index = count_before + 1
@@ -176,13 +179,13 @@ def test_execute_getblockcount(gui):
"""Execute getblockcount and verify a request + reply pair appears (no error row)."""
print("\n── test_execute_getblockcount ──────────────────────────────────")
- count_before = gui.get_property("commandConsole", "outputCount")
+ count_before = gui.get_property("rpcConsole", "outputCount")
submit_console_command(gui, "getblockcount")
# Wait for execution to complete.
- gui.wait_for_property("commandConsole", "executing", False, timeout_ms=10000)
+ gui.wait_for_property("rpcConsole", "executing", False, timeout_ms=10000)
- count_after = gui.get_property("commandConsole", "outputCount")
+ count_after = gui.get_property("rpcConsole", "outputCount")
# Expect exactly 2 new rows: one CMD_REQUEST (command echo) and one
# CMD_REPLY (the numeric block count). An error would add a third row.
assert count_after == count_before + 2, (
@@ -196,12 +199,12 @@ def test_execute_help(gui):
"""Execute 'help' and verify output rows appear."""
print("\n── test_execute_help ───────────────────────────────────────────")
- count_before = gui.get_property("commandConsole", "outputCount")
+ count_before = gui.get_property("rpcConsole", "outputCount")
submit_console_command(gui, "help")
- gui.wait_for_property("commandConsole", "executing", False, timeout_ms=10000)
+ gui.wait_for_property("rpcConsole", "executing", False, timeout_ms=10000)
- count_after = gui.get_property("commandConsole", "outputCount")
+ count_after = gui.get_property("rpcConsole", "outputCount")
assert count_after > count_before, (
f"Expected output rows after help (before={count_before}, after={count_after})"
)
@@ -212,13 +215,13 @@ def test_execute_invalid_command(gui):
"""Execute an unknown command and verify the submit button re-enables and output appears."""
print("\n── test_execute_invalid_command ────────────────────────────────")
- count_before = gui.get_property("commandConsole", "outputCount")
+ count_before = gui.get_property("rpcConsole", "outputCount")
submit_console_command(gui, "thiscommanddoesnotexist")
# Wait for execution to complete (button stays disabled since input was cleared).
- gui.wait_for_property("commandConsole", "executing", False, timeout_ms=10000)
+ gui.wait_for_property("rpcConsole", "executing", False, timeout_ms=10000)
- count_after = gui.get_property("commandConsole", "outputCount")
+ count_after = gui.get_property("rpcConsole", "outputCount")
assert count_after > count_before, (
f"Expected error output rows after invalid command (before={count_before}, after={count_after})"
)
@@ -275,10 +278,10 @@ def test_autocomplete_help_variants(gui):
def test_back_navigation(gui):
- """Navigate back from the Console page and verify we return to NodeRunner."""
+ """Close Settings from the RPC console and verify we return to NodeRunner."""
print("\n── test_back_navigation ────────────────────────────────────────")
- gui.click("consoleBackButton")
+ gui.click("settingsDoneButton")
gui.wait_for_page("nodeRunner", timeout_ms=5000)
print(" PASSED: back navigation returned to NodeRunner")
@@ -288,12 +291,12 @@ def test_clear_button_restores_welcome_output(gui):
print("\n── test_clear_button_restores_welcome_output ───────────────────")
gui.set_text("consoleInput", "")
- assert gui.get_property("commandConsole", "outputCount") > 0
+ assert gui.get_property("rpcConsole", "outputCount") > 0
welcome_time_before = gui.get_text("consoleOutputArea_left_0")
time.sleep(1.1)
gui.click("consoleClearButton")
- gui.wait_for_property("commandConsole", "outputCount", 1, timeout_ms=3000)
+ gui.wait_for_property("rpcConsole", "outputCount", 1, timeout_ms=3000)
welcome_time_after = gui.get_text("consoleOutputArea_left_0")
assert re.fullmatch(r"\d\d:\d\d:\d\d", welcome_time_after), (
diff --git a/test/functional/qml_test_debug_log.py b/test/functional/qml_test_debug_log.py
index a5e8d747b2..05be208788 100644
--- a/test/functional/qml_test_debug_log.py
+++ b/test/functional/qml_test_debug_log.py
@@ -113,10 +113,9 @@ def navigate_to_debug_log(gui):
"""
gui.wait_for_page("nodeRunner", timeout_ms=10000)
gui.click("nodeSettingsButton")
- gui.wait_for_page("nodeSettingsStack", timeout_ms=5000)
- # Debug Log is a sidebar section (settings_debuglog) in the desktop layout.
- gui.wait_for_property("settings_debuglog", "visible", True, timeout_ms=5000)
- gui.click("settings_debuglog")
+ gui.wait_for_page("settingsView", timeout_ms=5000)
+ gui.wait_for_property("settingsSidebar_debug-log", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_debug-log")
gui.wait_for_page("settingsDebugLog", timeout_ms=5000)
@@ -179,7 +178,16 @@ def test_search_layout_matches_design(gui):
)
page_width = gui.get_property("settingsDebugLog", "width")
content_width = gui.get_property("debugLogContentLayout", "width")
- expected_content_width = max(0, min(page_width - 40, 600))
+ content_horizontal_padding = gui.get_property(
+ "settingsDebugLog", "contentHorizontalPadding"
+ )
+ maximum_content_width = gui.get_property(
+ "settingsDebugLog", "maximumContentWidth"
+ )
+ expected_content_width = max(
+ 0,
+ min(page_width - content_horizontal_padding * 2, maximum_content_width),
+ )
assert_close(content_width, expected_content_width,
"debug log content max width")
@@ -399,7 +407,7 @@ def test_load_more_at_bottom(gui, current_count):
def test_close_settings(gui):
"""Clicking Done exits the desktop settings shell."""
print("\n── test_close_settings ───────────────────────────────────────────")
- gui.click("nodeSettingsDoneButton")
+ gui.click("settingsDoneButton")
gui.wait_for_page("nodeSettingsButton", timeout_ms=5000)
print(" PASSED: Done closed node settings")
diff --git a/test/functional/qml_test_disablewallet_boot.py b/test/functional/qml_test_disablewallet_boot.py
index 6590749ec0..3d6924a7ee 100755
--- a/test/functional/qml_test_disablewallet_boot.py
+++ b/test/functional/qml_test_disablewallet_boot.py
@@ -88,7 +88,7 @@ def checkpoint(self, label, gui=None):
def open_node_settings(gui):
gui.click("nodeSettingsButton")
- gui.wait_for_page("settings_about", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.wait_for_page("settingsSidebar_about", timeout_ms=SETTINGS_TIMEOUT_MS)
def prepend_config_line(datadir, line):
@@ -128,10 +128,8 @@ def assert_wallet_ui_absent(gui):
f"{sorted(unexpected)}"
)
- wallet_settings_visible = gui.get_property("settings_wallet", "visible")
- assert wallet_settings_visible is False, (
- "Wallet settings row should be hidden in -disablewallet mode, "
- f"got {wallet_settings_visible!r}"
+ assert not gui.object_exists("settingsSidebar_wallet"), (
+ "Wallet settings row should not be instantiated in -disablewallet mode"
)
@@ -161,59 +159,55 @@ def assert_wallet_boot(gui):
def walk_about_settings(gui, checkpoints):
print(" Opening About settings (sidebar)")
- gui.click("settings_about")
- gui.wait_for_page("settingsAbout", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("settingsSidebar_about")
+ gui.wait_for_page("aboutSettingsPage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("about settings opened", gui)
- gui.click("gotoDeveloperSetting")
+ gui.click("aboutDeveloperRow")
gui.wait_for_page("settingsDeveloper", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("developer settings opened", gui)
gui.click("settingsDeveloperBack")
- gui.wait_for_page("settingsAbout", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.wait_for_page("aboutSettingsPage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("returned from developer settings", gui)
def walk_display_settings(gui, checkpoints):
print(" Opening Display settings (sidebar)")
- gui.click("settings_display")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("settingsSidebar_display")
+ gui.wait_for_page("displayUnitPicker", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("display settings opened", gui)
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=SETTINGS_TIMEOUT_MS)
- checkpoints.checkpoint("display unit settings opened", gui)
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("displayUnitPickerButton")
+ gui.wait_for_page("displayUnitPickerMenu", timeout_ms=SETTINGS_TIMEOUT_MS)
+ checkpoints.checkpoint("display unit picker opened", gui)
+ gui.click("displayUnitBTC")
- gui.click("gotoLanguage")
+ gui.click("displayLanguageRow")
gui.wait_for_page("settingsLanguagePage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("language settings opened", gui)
gui.click("settingsLanguageBack")
- gui.wait_for_page("gotoLanguage", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.wait_for_page("displayLanguageRow", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("returned from display sub-pages", gui)
def walk_storage_settings(gui, checkpoints):
print(" Opening Storage settings (sidebar)")
- gui.click("settings_storage")
- # currentSection is the sidebar row index; Storage sits at row 4 in the
- # grouped sidebar order (Wallet, External Signer, Display, Window Behavior,
- # Storage, ...).
- gui.wait_for_property("nodeSettingsStack", "currentSection", 4, timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("settingsSidebar_storage")
+ gui.wait_for_page("storageSettingsPage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("storage settings opened", gui)
def walk_connection_settings(gui, checkpoints):
print(" Opening Connection settings (sidebar)")
- gui.click("settings_connection")
- gui.wait_for_page("gotoProxy", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("settingsSidebar_connection")
+ gui.wait_for_page("proxySettingsRow", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("connection settings opened", gui)
- gui.click("gotoProxy")
- gui.wait_for_page("settingsProxy", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("proxySettingsRow")
+ gui.wait_for_page("proxySettingsPage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("proxy settings opened", gui)
- gui.click("settingsProxyBack")
- gui.wait_for_page("gotoProxy", timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("proxySettingsBackButton")
+ gui.wait_for_page("proxySettingsRow", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("returned from proxy settings", gui)
@@ -229,14 +223,14 @@ def walk_peers(gui, checkpoints):
def walk_network_traffic_settings(gui, checkpoints):
print(" Opening Network Traffic settings (sidebar)")
- gui.click("settings_networktraffic")
- gui.wait_for_property("nodeSettingsStack", "currentSection", 6, timeout_ms=SETTINGS_TIMEOUT_MS)
+ gui.click("settingsSidebar_network-traffic")
+ gui.wait_for_page("networkTrafficSettingsPage", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("network traffic settings opened", gui)
def walk_debug_log_settings(gui, checkpoints):
print(" Opening Debug Log settings (sidebar)")
- gui.click("settings_debuglog")
+ gui.click("settingsSidebar_debug-log")
gui.wait_for_page("debugLogSearchField", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("debug log settings opened", gui)
@@ -266,7 +260,7 @@ def run_node_only_flow(harness, checkpoints, *, full_walk):
walk_network_traffic_settings(gui, checkpoints)
walk_debug_log_settings(gui, checkpoints)
- gui.click("nodeSettingsDoneButton")
+ gui.click("settingsDoneButton")
gui.wait_for_page("nodeSettingsButton", timeout_ms=SETTINGS_TIMEOUT_MS)
checkpoints.checkpoint("node settings closed", gui)
diff --git a/test/functional/qml_test_external_signer.py b/test/functional/qml_test_external_signer.py
index 60c7d9135c..ee0fb4dc13 100644
--- a/test/functional/qml_test_external_signer.py
+++ b/test/functional/qml_test_external_signer.py
@@ -238,27 +238,28 @@ def ensure_desktop_wallets_visible(gui):
def open_wallet_settings(gui):
ensure_desktop_wallets_visible(gui)
gui.click("desktopWalletSettingsTabButton")
- # External-signer config moved into the node-settings sidebar's
- # "External Signer" section (objectName settings_externalsigner), whose page
- # hosts externalSignerPathInput.
- gui.wait_for_property("settings_externalsigner", "visible", True, timeout_ms=10000)
- gui.click("settings_externalsigner")
+ # External-signer configuration lives in its own redesigned sidebar page.
+ gui.wait_for_property("settingsSidebar_external-signer", "visible", True, timeout_ms=10000)
+ gui.click("settingsSidebar_external-signer")
gui.wait_for_property("externalSignerPathInput", "visible", True, timeout_ms=10000)
def open_selected_wallet_settings(gui):
ensure_desktop_wallets_visible(gui)
gui.click("desktopWalletSettingsTabButton")
- # Per-wallet settings live under the sidebar "Wallet" section
- # (objectName settings_wallet), which opens walletSettingsPage.
- gui.wait_for_property("settings_wallet", "visible", True, timeout_ms=10000)
- gui.click("settings_wallet")
+ # Per-wallet settings live under the sidebar Wallet section.
+ gui.wait_for_property("settingsSidebar_wallet", "visible", True, timeout_ms=10000)
+ gui.click("settingsSidebar_wallet")
try:
gui.wait_for_page("walletSettingsPage", timeout_ms=1000)
except QmlDriverError:
- # The wallet section is a PageStack; if a prior step left it on a
- # sub-page, pop back to the wallet settings root.
- for back_button in ("walletSettingsBackButton", "settingsWalletBack"):
+ # If a prior step left the preserved wallet stack on a subpage, return
+ # to the redesigned wallet settings root.
+ for back_button in (
+ "walletPasswordBackButton",
+ "addressListBackButton",
+ "signVerifyMessageBackButton",
+ ):
try:
if gui.get_property(back_button, "visible") is True:
gui.click(back_button)
@@ -505,8 +506,8 @@ def run_test(args):
configure_external_signer_via_gui(harness, checkpoints, signer_path)
wallet_name = create_and_verify_external_wallet(harness, checkpoints)
open_selected_wallet_settings(harness.driver)
- harness.driver.wait_for_property("walletSettingsPasswordRow", "visible", False, timeout_ms=10000)
- harness.driver.wait_for_property("walletSettingsBackupRow", "visible", True, timeout_ms=10000)
+ harness.driver.wait_for_property("walletPasswordRow", "visible", False, timeout_ms=10000)
+ harness.driver.wait_for_property("walletBackupRow", "visible", True, timeout_ms=10000)
checkpoints.checkpoint("external signer wallet hides password settings", harness.driver)
create_wallet(harness.gui_rpc_port, "miner", load_on_startup=False)
diff --git a/test/functional/qml_test_password_wallet.py b/test/functional/qml_test_password_wallet.py
index 1b52fad0c4..9d0b1f8b11 100644
--- a/test/functional/qml_test_password_wallet.py
+++ b/test/functional/qml_test_password_wallet.py
@@ -234,8 +234,8 @@ def close_wallet_from_selector(gui, wallet_name):
def open_wallet_settings_page(gui):
gui.click("desktopWalletSettingsTabButton")
- gui.wait_for_property("settings_wallet", "visible", True, timeout_ms=10000)
- gui.click("settings_wallet")
+ gui.wait_for_property("settingsSidebar_wallet", "visible", True, timeout_ms=10000)
+ gui.click("settingsSidebar_wallet")
gui.wait_for_page("walletSettingsPage", timeout_ms=10000)
@@ -551,8 +551,8 @@ def case_close_loaded_wallet_from_selector(harness, checkpoints):
remaining_wallet = next(name for name in wallet_names if name != selected_wallet)
open_wallet_settings_page(gui)
- gui.wait_for_property("walletSettingsPasswordRow", "visible", True, timeout_ms=10000)
- gui.wait_for_property("walletSettingsBackupRow", "visible", True, timeout_ms=10000)
+ gui.wait_for_property("walletPasswordRow", "visible", True, timeout_ms=10000)
+ gui.wait_for_property("walletBackupRow", "visible", True, timeout_ms=10000)
checkpoints.checkpoint("wallet settings opened", gui)
open_wallet_selector(gui)
diff --git a/test/functional/qml_test_peers.py b/test/functional/qml_test_peers.py
index c20c864ff3..12dad9ad46 100644
--- a/test/functional/qml_test_peers.py
+++ b/test/functional/qml_test_peers.py
@@ -464,22 +464,22 @@ def navigate_to_peers(gui):
# Peers moved out of node settings into a dedicated NodeRunner header tab.
gui.click("peersTabButton")
gui.wait_for_page("peers")
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
-def _wait_for_node_settings_idle(gui, timeout_ms=PEER_ACTION_TIMEOUT_SECS * 1000) -> None:
+def _wait_for_page_stack_idle(gui, timeout_ms=PEER_ACTION_TIMEOUT_SECS * 1000) -> None:
"""Wait until page-stack transitions to/from the Peers page have settled.
- Peers moved out of the NodeSettings stack onto the main page stack, so wait
+ Peers live on the main page stack, so wait
on the app's stack views via settle() (missing stacks are ignored)."""
gui.settle(timeout_ms=timeout_ms)
def _open_peer_details(gui, node_id: int) -> None:
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
gui.click(f"peerListItem_{node_id}")
gui.wait_for_page("peerDetails", timeout_ms=8000)
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
print(f" Opened PeerDetails for node id={node_id}")
@@ -538,7 +538,7 @@ def test_ban_peer(gui, harness, node_id, duration_secs, duration_label):
# Wait for PeerDetails to navigate back via its onDisconnected handler.
gui.wait_for_page("peers", timeout_ms=PEER_ACTION_TIMEOUT_SECS * 1000)
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
def test_unban_peer(gui, harness):
@@ -547,14 +547,14 @@ def test_unban_peer(gui, harness):
# StackView disables input during transitions (500ms pop animation for
# PeerDetails→Peers). Wait for it to finish; pushing BannedPeers while
# the StackView is busy is silently ignored by Qt.
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
gui.wait_for_property("viewBannedPeersButton", "enabled", True,
timeout_ms=PEER_ACTION_TIMEOUT_SECS * 1000)
# The ban list button is in the Peers page footer.
gui.click("viewBannedPeersButton")
gui.wait_for_page("bannedPeers", timeout_ms=8000)
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
print(" Navigated to BannedPeers page")
gui.click("unbanButton_0")
@@ -747,10 +747,10 @@ def run_tests():
# The 1-year ban from the last iteration is still in the ban list.
test_unban_peer(gui, harness)
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
gui.click("bannedPeersBackButton")
gui.wait_for_page("peers")
- _wait_for_node_settings_idle(gui)
+ _wait_for_page_stack_idle(gui)
assert not gui.get_property("viewBannedPeersButton", "visible"), \
"viewBannedPeersButton should be hidden after UI unban"
diff --git a/test/functional/qml_test_proxy.py b/test/functional/qml_test_proxy.py
index 9a3253bec7..29e847072d 100644
--- a/test/functional/qml_test_proxy.py
+++ b/test/functional/qml_test_proxy.py
@@ -36,27 +36,27 @@ def navigate_to_proxy_settings(gui):
gui.click("nodeSettingsButton")
gui.settle()
- gui.wait_for_property("settings_connection", "visible", True, timeout_ms=5000)
- gui.click("settings_connection")
- gui.wait_for_page("gotoProxy", timeout_ms=5000)
- gui.click("gotoProxy")
- gui.wait_for_page("settingsProxy", timeout_ms=5000)
+ gui.wait_for_property("settingsSidebar_connection", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_connection")
+ gui.wait_for_page("proxySettingsRow", timeout_ms=5000)
+ gui.click("proxySettingsRow")
+ gui.wait_for_page("proxySettingsPage", timeout_ms=5000)
print(" Navigated to Proxy Settings page.")
def leave_proxy_settings_with_done(gui):
"""Commit draft proxy settings and return to Connection settings."""
- gui.wait_for_property("settingsProxyDone", "enabled", True, timeout_ms=2000)
- gui.click("settingsProxyDone")
- gui.wait_for_page("gotoProxy", timeout_ms=5000)
+ gui.wait_for_property("proxySettingsSaveButton", "enabled", True, timeout_ms=2000)
+ gui.click("proxySettingsSaveButton")
+ gui.wait_for_page("proxySettingsRow", timeout_ms=5000)
def navigate_back_from_connection_settings(gui):
"""Navigate back from Connection settings to the runtime settings shell."""
- if gui.object_exists("nodeSettingsDoneButton"):
- gui.click("nodeSettingsDoneButton")
+ if gui.object_exists("settingsDoneButton"):
+ gui.click("settingsDoneButton")
else:
- gui.click("settingsConnectionBack")
+ gui.click("activityTabButton")
gui.settle()
if gui.object_exists("desktopWalletSettingsTabButton"):
gui.wait_for_property("desktopWalletSettingsTabButton", "visible", True, timeout_ms=5000)
@@ -72,9 +72,9 @@ def test_default_proxy_toggle(gui):
checked = gui.get_property("proxyEnableSwitch", "checked")
assert not checked, f"Expected proxy disabled by default, got checked={checked}"
- dirty = gui.get_property("settingsProxy", "proxySettingsDirty")
+ dirty = gui.get_property("proxyRestartNotice", "visible")
assert not dirty, "Expected proxySettingsDirty=False before any change"
- draft_dirty = gui.get_property("settingsProxy", "proxyDraftDirty")
+ draft_dirty = gui.get_property("proxySettingsPage", "proxyDraftDirty")
assert not draft_dirty, "Expected proxyDraftDirty=False before any change"
# Enable proxy.
@@ -84,8 +84,8 @@ def test_default_proxy_toggle(gui):
assert checked, "Expected proxyEnableSwitch to be checked after click"
print(" Default proxy toggled ON: OK")
- gui.wait_for_property("settingsProxy", "proxyDraftDirty", True, timeout_ms=2000)
- dirty = gui.get_property("settingsProxy", "proxySettingsDirty")
+ gui.wait_for_property("proxySettingsPage", "proxyDraftDirty", True, timeout_ms=2000)
+ dirty = gui.get_property("proxyRestartNotice", "visible")
assert not dirty, "Expected proxySettingsDirty=False before pressing Done"
print(" Proxy edit is draft-only before Done: OK")
@@ -96,7 +96,7 @@ def test_default_proxy_toggle(gui):
assert not checked, "Expected proxyEnableSwitch to be unchecked after second click"
print(" Default proxy toggled OFF: OK")
- gui.wait_for_property("settingsProxy", "proxyDraftDirty", False, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "proxyDraftDirty", False, timeout_ms=2000)
print(" proxyDraftDirty=False after reverting proxy change: OK")
@@ -111,15 +111,12 @@ def test_proxy_valid_address(gui):
gui.wait_for_property("proxyAddressInput", "enabled", True, timeout_ms=2000)
gui.set_text("proxyAddressInput", "")
gui.wait_for_property("proxyAddressInput", "text", "", timeout_ms=2000)
- gui.click("proxyAddressSetting")
+ gui.invoke("proxyAddressInput", "forceActiveFocus")
gui.wait_for_property("proxyAddressInput", "activeFocus", True, timeout_ms=2000)
gui.type_text("proxyAddressInput", "10.0.0.1:9050")
gui.wait_for_property("proxyAddressInput", "text", "10.0.0.1:9050", timeout_ms=2000)
- gui.wait_for_property("proxyAddressInput", "validInput", True, timeout_ms=2000)
-
- valid = gui.get_property("proxyAddressInput", "validInput")
- assert valid, f"Expected '10.0.0.1:9050' to pass validation, got validInput={valid}"
- gui.wait_for_property("settingsProxyDone", "enabled", True, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "draftProxyValidationError", "", timeout_ms=2000)
+ gui.wait_for_property("proxySettingsSaveButton", "enabled", True, timeout_ms=2000)
print(" Valid address accepted: OK")
@@ -134,17 +131,19 @@ def test_proxy_invalid_address(gui):
gui.wait_for_property("proxyAddressInput", "enabled", True, timeout_ms=2000)
# Enter an address with invalid IP octets.
gui.set_text("proxyAddressInput", "999.999.999.999:9050")
- gui.wait_for_property("proxyAddressInput", "validInput", False, timeout_ms=2000)
-
- valid = gui.get_property("proxyAddressInput", "validInput")
- assert not valid, f"Expected invalid address to fail validation, got validInput={valid}"
- gui.wait_for_property("settingsProxyDone", "enabled", False, timeout_ms=2000)
+ gui.wait_for_property(
+ "proxySettingsPage",
+ "draftProxyValidationError",
+ lambda error: len(error) > 0,
+ timeout_ms=2000,
+ )
+ gui.wait_for_property("proxySettingsSaveButton", "enabled", False, timeout_ms=2000)
print(" Invalid address rejected: OK")
# Restore to a valid address for subsequent tests.
gui.set_text("proxyAddressInput", "127.0.0.1:9050")
- gui.wait_for_property("proxyAddressInput", "validInput", True, timeout_ms=2000)
- gui.wait_for_property("settingsProxyDone", "enabled", True, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "draftProxyValidationError", "", timeout_ms=2000)
+ gui.wait_for_property("proxySettingsSaveButton", "enabled", True, timeout_ms=2000)
def test_tor_proxy_toggle(gui):
@@ -180,25 +179,25 @@ def test_back_discards_proxy_draft(gui):
gui.wait_for_property("proxyAddressInput", "enabled", True, timeout_ms=2000)
gui.set_text("proxyAddressInput", "10.0.0.5:9050")
gui.wait_for_property("proxyAddressInput", "text", "10.0.0.5:9050", timeout_ms=2000)
- gui.wait_for_property("settingsProxy", "proxyDraftDirty", True, timeout_ms=2000)
- dirty = gui.get_property("settingsProxy", "proxySettingsDirty")
+ gui.wait_for_property("proxySettingsPage", "proxyDraftDirty", True, timeout_ms=2000)
+ dirty = gui.get_property("proxyRestartNotice", "visible")
assert not dirty, "Expected model to remain unchanged before pressing Done"
- gui.click("settingsProxyBack")
+ gui.click("proxySettingsBackButton")
gui.wait_for_property("discardProxyChangesPopup", "visible", True, timeout_ms=2000)
gui.click("discardProxyChangesCancelButton")
gui.wait_for_property("discardProxyChangesPopup", "visible", False, timeout_ms=2000)
gui.wait_for_property("proxyAddressInput", "text", "10.0.0.5:9050", timeout_ms=2000)
print(" Back cancellation keeps draft changes: OK")
- gui.click("settingsProxyBack")
+ gui.click("proxySettingsBackButton")
gui.wait_for_property("discardProxyChangesPopup", "visible", True, timeout_ms=2000)
gui.click("discardProxyChangesConfirmButton")
- gui.wait_for_page("gotoProxy", timeout_ms=5000)
- gui.click("gotoProxy")
- gui.wait_for_page("settingsProxy", timeout_ms=5000)
+ gui.wait_for_page("proxySettingsRow", timeout_ms=5000)
+ gui.click("proxySettingsRow")
+ gui.wait_for_page("proxySettingsPage", timeout_ms=5000)
gui.wait_for_property("proxyEnableSwitch", "checked", False, timeout_ms=2000)
- gui.wait_for_property("settingsProxy", "proxyDraftDirty", False, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "proxyDraftDirty", False, timeout_ms=2000)
print(" Back discard leaves persisted settings unchanged: OK")
@@ -265,14 +264,14 @@ def run_tests():
gui.wait_for_property("proxyEnableSwitch", "checked", True, timeout_ms=2000)
gui.wait_for_property("proxyAddressInput", "enabled", True, timeout_ms=2000)
gui.set_text("proxyAddressInput", "10.0.0.1:9050")
- gui.wait_for_property("proxyAddressInput", "validInput", True, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "draftProxyValidationError", "", timeout_ms=2000)
if not gui.get_property("torEnableSwitch", "checked"):
gui.click("torEnableSwitch")
gui.wait_for_property("torEnableSwitch", "checked", True, timeout_ms=2000)
gui.wait_for_property("torAddressInput", "enabled", True, timeout_ms=2000)
gui.set_text("torAddressInput", "127.0.0.1:9150")
- gui.wait_for_property("torAddressInput", "validInput", True, timeout_ms=2000)
+ gui.wait_for_property("proxySettingsPage", "draftTorValidationError", "", timeout_ms=2000)
leave_proxy_settings_with_done(gui)
navigate_back_from_connection_settings(gui)
diff --git a/test/functional/qml_test_send_receive.py b/test/functional/qml_test_send_receive.py
index 585e12232e..7e3db51cd7 100644
--- a/test/functional/qml_test_send_receive.py
+++ b/test/functional/qml_test_send_receive.py
@@ -395,6 +395,11 @@ def run_test(*, save_screenshots=False, screenshot_root=None):
gui.click("activityTypeFilterButton")
gui.click("activityTypeSent")
gui.wait_for_property("activityFilterProxyModel", "count", 1, timeout_ms=20000)
+ wait_until(
+ lambda: gui.get_list_item_property("activityListView", 0, "amount") != "",
+ timeout=10,
+ description="sent Activity row delegate",
+ )
activity_amount_text = gui.get_list_item_property("activityListView", 0, "amount")
activity_amount_sats = amount_text_to_sats(activity_amount_text)
assert activity_amount_text.startswith("-"), (
diff --git a/test/functional/qml_test_settings_display.py b/test/functional/qml_test_settings_display.py
index bfc4d708da..99007656c0 100644
--- a/test/functional/qml_test_settings_display.py
+++ b/test/functional/qml_test_settings_display.py
@@ -2,24 +2,12 @@
# Copyright (c) 2026 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
-"""End-to-end tests for the Display settings page.
-
-Tests language selection, display unit switching (BTC / SAT), and the
-"Ask before opening links" toggle.
-
-These tests run post-onboarding and do not require a peer connection, but
-they do require the node to start up, so generous wait timeouts are used.
-
-This test requires:
- - bitcoin-core-app built with -DENABLE_TEST_AUTOMATION=ON
-"""
+"""End-to-end tests for the redesigned Display settings page."""
import shutil
import sys
-import time
from qml_test_harness import (
- GUI_STARTUP_TIMEOUT,
QmlTestHarness,
complete_onboarding,
dump_qml_tree,
@@ -27,118 +15,68 @@
)
from qml_driver import QmlDriverError
-# The node must start up before post-onboarding pages are interactive.
-# Use a generous timeout for waits that follow onboarding completion.
-POST_ONBOARDING_TIMEOUT_MS = 30000
+
+POST_ONBOARDING_TIMEOUT_MS = 30_000
DISPLAY_SETTING_ROWS = (
- "gotoTheme",
- "gotoDisplayUnit",
- "gotoLanguage",
- "gotoThirdPartyTransactionUrls",
- "gotoMoneyFont",
+ "displayThemeRow",
+ "displayBlockStatusSizeRow",
+ "displayMoneyFontRow",
+ "displayUnitRow",
+ "displayLanguageRow",
+ "displayTransactionUrlsRow",
)
-# ── Navigation helpers ────────────────────────────────────────────────────────
-
def navigate_to_display_settings(gui):
- """From the NodeRunner main screen, navigate to the Display settings page."""
+ """Open Settings and select Display from the sidebar."""
gui.click("nodeSettingsButton")
- # Display is a sidebar section (settings_display) in the desktop layout.
- gui.wait_for_property("settings_display", "visible", True, timeout_ms=5000)
- gui.click("settings_display")
- # SettingsDisplay is identified by the presence of gotoDisplayUnit.
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
- print(" Navigated to Display settings page")
+ gui.wait_for_property("settingsSidebar_display", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_display")
+ gui.wait_for_page("displaySettingsPage", timeout_ms=5000)
+ gui.wait_for_page("displayUnitPicker", timeout_ms=5000)
+ print(" Navigated to redesigned Display settings page")
def assert_display_rows_have_no_descriptions(gui):
- """The top-level Display page follows the single-line row design."""
for row in DISPLAY_SETTING_ROWS:
description = gui.get_property(row, "description")
assert description == "", f"{row} should not show subtext, got: {description!r}"
-def reset_display_unit_to_btc(gui):
- """Reset the persisted display unit to BTC.
-
- Precondition: caller is on the SettingsDisplay page with gotoDisplayUnit
- visible. Callers should wrap invocations in `try/except QmlDriverError:
- pass` for best-effort teardown that does not mask the original test failure.
- """
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- gui.click("displayUnitBTC")
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
+def select_display_unit(gui, item_name, expected_text):
+ gui.click("displayUnitPickerButton")
+ gui.wait_for_property(item_name, "visible", True, timeout_ms=3000)
+ gui.click(item_name)
+ gui.wait_for_property(
+ "displayUnitPicker", "currentText", expected_text, timeout_ms=3000
+ )
-def reset_language_to_system_default(gui):
- """Reset the persisted language to the System default (empty tag).
-
- Precondition: caller is on the SettingsDisplay page with gotoLanguage
- visible. The helper navigates into SettingsLanguage, picks the empty-tag
- delegate, and waits to return to SettingsDisplay. Callers should wrap
- invocations in `try/except QmlDriverError: pass` for best-effort teardown
- that does not mask the original test failure.
- """
- gui.click("gotoLanguage")
+def select_language(gui, search_text, item_name):
+ gui.click("displayLanguageRow")
gui.wait_for_page("settingsLanguagePage", timeout_ms=5000)
- gui.wait_for_page("language_", timeout_ms=3000) # wait for delegate to render
- gui.click("language_") # objectName: "language_" + "" = "language_"
- gui.wait_for_page("gotoLanguage", timeout_ms=5000)
+ if search_text:
+ gui.set_text("languageSearch", search_text)
+ gui.wait_for_page(item_name, timeout_ms=3000)
+ gui.click(item_name)
+ gui.wait_for_page("displayLanguageRow", timeout_ms=5000)
-# ── Individual test cases ─────────────────────────────────────────────────────
+def reset_display_unit_to_btc(gui):
+ select_display_unit(gui, "displayUnitBTC", "BTC")
+
+
+def reset_language_to_system_default(gui):
+ select_language(gui, "", "language_")
+
def test_display_unit_selection(gui):
- """Select SAT on the Display unit page and verify it is reflected."""
print("\n── test_display_unit_selection ───────────────────────────────")
-
try:
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- print(" Navigated to SettingsDisplayUnit page")
-
- # If SAT is selected (state persists across runs), switch to BTC first.
- # We must navigate to a fresh page after the switch because clicking a
- # checkable OptionButton breaks its declarative `checked:` binding.
- if gui.get_property("displayUnitSAT", "checked"):
- gui.click("displayUnitBTC")
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- print(" Switched to BTC starting state")
-
- btc_checked = gui.get_property("displayUnitBTC", "checked")
- sat_checked = gui.get_property("displayUnitSAT", "checked")
- assert btc_checked, (
- f"BTC should be checked at test start, got btc={btc_checked} sat={sat_checked}"
- )
- assert not sat_checked, (
- f"SAT should not be checked at test start, got sat={sat_checked}"
- )
- print(f" Starting state: BTC={btc_checked}, SAT={sat_checked} PASSED")
-
- # Select SAT.
- gui.click("displayUnitSAT")
- sat_after = gui.get_property("displayUnitSAT", "checked")
- btc_after = gui.get_property("displayUnitBTC", "checked")
- assert sat_after, f"SAT should be checked after clicking, got sat={sat_after}"
- assert not btc_after, f"BTC should be unchecked after selecting SAT, got btc={btc_after}"
- print(f" After SAT selection: SAT={sat_after}, BTC={btc_after} PASSED")
-
- # Go back and reset to BTC for future runs.
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
-
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- gui.click("displayUnitBTC")
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
- print(" Reset display unit to BTC PASSED")
+ reset_display_unit_to_btc(gui)
+ select_display_unit(gui, "displayUnitSAT", "sat")
+ assert gui.get_property("displayUnitPicker", "currentValue") == 3
+ print(" Display unit changed from BTC to sat PASSED")
finally:
try:
reset_display_unit_to_btc(gui)
@@ -147,61 +85,17 @@ def test_display_unit_selection(gui):
def test_language_selection(gui):
- """Select Spanish and verify translated headers update."""
print("\n── test_language_selection ───────────────────────────────────")
-
try:
- gui.click("gotoLanguage")
- gui.wait_for_page("settingsLanguagePage", timeout_ms=5000)
- print(" Navigated to SettingsLanguage page")
-
- # Filter the list to Spanish so the delegate is rendered by the ListView.
- gui.set_text("languageSearch", "español")
- gui.wait_for_page("language_es", timeout_ms=3000)
- gui.click("language_es")
- # Selecting a language navigates back to SettingsDisplay automatically.
- gui.wait_for_page("gotoLanguage", timeout_ms=5000)
- print(" Selected Spanish (es) and returned to Display settings")
-
+ select_language(gui, "español", "language_es")
assert_display_rows_have_no_descriptions(gui)
- # Verify translation propagated to other row headers on this page.
- lang_header = gui.get_property("gotoLanguage", "header")
- assert lang_header == "Idioma", (
- f"'Language' row header should be 'Idioma' in Spanish, got: {lang_header!r}"
- )
- print(f" Language row header translated: {lang_header!r} PASSED")
-
- unit_header = gui.get_property("gotoDisplayUnit", "header")
- assert unit_header == "Unidad de visualización", (
- f"'Display unit' row header should be translated in Spanish, got: {unit_header!r}"
- )
- print(f" Display unit row header translated: {unit_header!r} PASSED")
-
- # Reset to System default (empty tag).
- gui.click("gotoLanguage")
- gui.wait_for_page("settingsLanguagePage", timeout_ms=5000)
- gui.wait_for_page("language_", timeout_ms=3000) # wait for delegate to render
- gui.click("language_") # objectName: "language_" + "" = "language_"
- gui.wait_for_page("gotoLanguage", timeout_ms=5000)
-
- assert_display_rows_have_no_descriptions(gui)
- print(" Reset to System default PASSED")
-
- # Verify English headers are restored after reset.
- lang_header_reset = gui.get_property("gotoLanguage", "header")
- assert lang_header_reset == "Language", (
- f"'Language' header should be restored to English after reset, got: {lang_header_reset!r}"
- )
- unit_header_reset = gui.get_property("gotoDisplayUnit", "header")
- assert unit_header_reset == "Display unit", (
- f"'Display unit' header should be restored to English after reset, got: {unit_header_reset!r}"
- )
- print(f" Headers restored to English PASSED")
+ language_title = gui.get_property("displayLanguageRow", "title")
+ unit_title = gui.get_property("displayUnitRow", "title")
+ assert language_title == "Idioma", language_title
+ assert unit_title == "Unidad de visualización", unit_title
+ print(" Spanish translated the inline Display rows PASSED")
finally:
- # Best-effort: if the test failed mid-flow the persisted language may
- # still be Spanish. Reset to System default so the restart phase starts
- # from a known state within this test's temporary QSettings sandbox.
try:
reset_language_to_system_default(gui)
except QmlDriverError:
@@ -209,137 +103,68 @@ def test_language_selection(gui):
def test_settings_persistence(datadir):
- """Restart the app without -resetguisettings and verify settings persisted.
-
- Issue #512 requires: change unit/language → restart → verify persisted.
- """
print("\n── test_settings_persistence ─────────────────────────────────")
-
- harness2 = QmlTestHarness(
+ harness = QmlTestHarness(
extra_args=["-disablewallet"],
reset_settings=False,
datadir=datadir,
)
try:
- harness2.start()
- gui2 = harness2.driver
+ harness.start()
+ gui = harness.driver
+ gui.wait_for_page("nodeSettingsButton", timeout_ms=POST_ONBOARDING_TIMEOUT_MS)
+ navigate_to_display_settings(gui)
- try:
- # Runtime restart tests launch as onboarded so they stay focused on
- # display setting persistence, not first-run onboarding.
- gui2.wait_for_page("nodeSettingsButton", timeout_ms=POST_ONBOARDING_TIMEOUT_MS)
- print(" Reached NodeRunner main screen after restart")
-
- navigate_to_display_settings(gui2)
-
- # Verify SAT is still selected.
- gui2.click("gotoDisplayUnit")
- gui2.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- sat_persisted = gui2.get_property("displayUnitSAT", "checked")
- assert sat_persisted, (
- f"SAT should still be selected after restart, got checked={sat_persisted}"
- )
- print(" Display unit (SAT) persisted across restart PASSED")
- gui2.click("settingsDisplayUnitBack")
- gui2.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
-
- # Verify Spanish is still selected through translated Display rows.
- lang_header = gui2.get_property("gotoLanguage", "header")
- assert lang_header == "Idioma", (
- f"'Language' row header should be Spanish after restart, got: {lang_header!r}"
- )
- unit_header = gui2.get_property("gotoDisplayUnit", "header")
- assert unit_header == "Unidad de visualización", (
- f"'Display unit' row header should be Spanish after restart, got: {unit_header!r}"
- )
- assert_display_rows_have_no_descriptions(gui2)
- print(" Language (Español) persisted across restart PASSED")
- finally:
- # Best-effort: reset persisted settings to defaults before the
- # harness shuts down. Values are flushed by QSettings on app exit,
- # so this must run before harness2.stop(). Swallow driver errors
- # so a mid-test failure is not masked.
- try:
- reset_display_unit_to_btc(gui2)
- except QmlDriverError:
- pass
- try:
- reset_language_to_system_default(gui2)
- except QmlDriverError:
- pass
+ assert gui.get_property("displayUnitPicker", "currentValue") == 3
+ assert gui.get_property("displayLanguageRow", "title") == "Idioma"
+ assert gui.get_property("displayUnitRow", "title") == "Unidad de visualización"
+ print(" Display unit and language persisted across restart PASSED")
+ reset_display_unit_to_btc(gui)
+ reset_language_to_system_default(gui)
finally:
- harness2.stop()
-
+ harness.stop()
-# ── Main ──────────────────────────────────────────────────────────────────────
def run_tests():
args = parse_args()
harness = QmlTestHarness(socket_path=args.socket_path, extra_args=["-disablewallet"])
+ datadir = None
+ tmpdir = None
try:
harness.start()
gui = harness.driver
-
- # Complete onboarding to reach the main node screen.
complete_onboarding(gui)
-
- # Wait for the NodeRunner main screen.
- # Uses a generous timeout because the node starts up after onboarding.
gui.wait_for_page("nodeSettingsButton", timeout_ms=POST_ONBOARDING_TIMEOUT_MS)
- print("Reached NodeRunner main screen")
navigate_to_display_settings(gui)
assert_display_rows_have_no_descriptions(gui)
-
test_display_unit_selection(gui)
test_language_selection(gui)
- # Set known state for persistence test: SAT + Spanish.
- print("\n── Setting up state for persistence test ─────────────────────")
- gui.click("gotoDisplayUnit")
- gui.wait_for_page("settingsDisplayUnitPage", timeout_ms=5000)
- gui.click("displayUnitSAT")
- gui.click("settingsDisplayUnitBack")
- gui.wait_for_page("gotoDisplayUnit", timeout_ms=5000)
- gui.click("gotoLanguage")
- gui.wait_for_page("settingsLanguagePage", timeout_ms=5000)
- gui.set_text("languageSearch", "español")
- gui.wait_for_page("language_es", timeout_ms=3000)
- gui.click("language_es")
- gui.wait_for_page("gotoLanguage", timeout_ms=5000)
- print(" State set: SAT + Spanish")
-
+ select_display_unit(gui, "displayUnitSAT", "sat")
+ select_language(gui, "español", "language_es")
datadir = harness.datadir
tmpdir = harness.tmpdir
-
- except Exception as e:
- print(f"\nFAILED: {e}", file=sys.stderr)
- import traceback
- traceback.print_exc()
+ except Exception as error:
+ print(f"\nFAILED: {error}", file=sys.stderr)
if harness.driver:
dump_qml_tree(harness.driver)
- sys.exit(1)
+ raise
finally:
- # Keep the datadir on disk so the second harness can reuse it.
harness.stop(cleanup=False)
- # Phase 2: restart without -resetguisettings and verify persistence.
try:
test_settings_persistence(datadir)
- except Exception as e:
- print(f"\nFAILED: {e}", file=sys.stderr)
- import traceback
- traceback.print_exc()
- sys.exit(1)
finally:
if tmpdir:
shutil.rmtree(tmpdir, ignore_errors=True)
- print("\n" + "=" * 60)
- print("All display settings tests PASSED")
- print("=" * 60)
+ print("\nAll display settings tests PASSED")
-if __name__ == '__main__':
- run_tests()
+if __name__ == "__main__":
+ try:
+ run_tests()
+ except Exception:
+ sys.exit(1)
diff --git a/test/functional/qml_test_tray.py b/test/functional/qml_test_tray.py
index 0eb5c725df..19ba2dc736 100644
--- a/test/functional/qml_test_tray.py
+++ b/test/functional/qml_test_tray.py
@@ -46,9 +46,9 @@ def navigate_to_window_behavior(gui):
"""Open Settings then navigate to the Window Behavior page."""
gui.click("nodeSettingsButton")
# Wait for the settings list with the Window Behavior entry.
- gui.wait_for_page("settings_windowbehavior", timeout_ms=5000)
- gui.click("settings_windowbehavior")
- gui.wait_for_page("windowBehaviorPage", timeout_ms=5000)
+ gui.wait_for_page("settingsSidebar_window-behavior", timeout_ms=5000)
+ gui.click("settingsSidebar_window-behavior")
+ gui.wait_for_page("windowBehaviorSettingsPage", timeout_ms=5000)
def run_tests():
@@ -126,9 +126,9 @@ def run_tests():
# showTrayIcon defaults to true; the others default to false.
print("Test 3: Verify default switch states ...")
expected_defaults = {
- "showTrayIconSwitch": True, # show tray icon is on by default
- "minimizeToTraySwitch": False, # minimize-to-tray is off by default
- "minimizeOnCloseSwitch": False, # minimize-on-close is off by default
+ "showTrayIconSwitch": True,
+ "minimizeToTraySwitch": False,
+ "minimizeOnCloseSwitch": False,
}
for switch_name, expected in expected_defaults.items():
checked = gui.get_property(switch_name, "checked")
@@ -141,7 +141,7 @@ def run_tests():
print(f" -> {switch_name}.checked == {str(expected).lower()} ✓")
# ── Tests 4–5: showTrayIcon toggle round-trip ─────────────────────────
- # Run the toggle tests while only one windowBehaviorPage instance is in
+ # Run the toggle tests while only one Window Behavior page instance is in
# the StackView (before the back/re-open cycle), so objectName lookups
# are unambiguous.
#
@@ -149,26 +149,26 @@ def run_tests():
# disabled) is already covered by the C++ unit tests in
# test_desktopwindowbehaviormodel.cpp. Here we only verify that the
# toggle round-trip works correctly via the UI on the offscreen backend.
- print("Test 4: showTrayIconSwitch toggles off and the model reflects the change ...")
+ print("Test 4: Show tray icon toggles off and the model reflects the change ...")
gui.click("showTrayIconSwitch")
gui.wait_for_property("showTrayIconSwitch", "checked", False, timeout_ms=2000)
- print(" -> showTrayIconSwitch clicked off ✓")
+ print(" -> Show tray icon clicked off ✓")
- print("Test 5: showTrayIconSwitch toggles back on ...")
+ print("Test 5: Show tray icon toggles back on ...")
gui.click("showTrayIconSwitch")
gui.wait_for_property("showTrayIconSwitch", "checked", True, timeout_ms=2000)
- print(" -> showTrayIconSwitch restored to on ✓")
+ print(" -> Show tray icon restored to on ✓")
# ── Test 6: Sidebar navigation after interaction ──────────────────────
print("Test 6: Sidebar navigation still works after interacting with Window Behavior ...")
- gui.click("settings_about")
- gui.wait_for_page("settingsAbout", timeout_ms=5000)
+ gui.click("settingsSidebar_about")
+ gui.wait_for_page("aboutSettingsPage", timeout_ms=5000)
print(" -> switched to About section ✓")
# ── Test 7: Re-open page (round-trip) ─────────────────────────────────
print("Test 7: Re-open Window Behavior page (round-trip) ...")
- gui.click("settings_windowbehavior")
- gui.wait_for_page("windowBehaviorPage", timeout_ms=5000)
+ gui.click("settingsSidebar_window-behavior")
+ gui.wait_for_page("windowBehaviorSettingsPage", timeout_ms=5000)
print(" -> re-opened Window Behavior ✓")
# ── Test 8: Close with minimizeOnClose keeps app alive ────────────────
diff --git a/test/functional/qml_test_wallet_settings.py b/test/functional/qml_test_wallet_settings.py
index 82149b7ae9..ef3e665cfa 100644
--- a/test/functional/qml_test_wallet_settings.py
+++ b/test/functional/qml_test_wallet_settings.py
@@ -44,7 +44,7 @@ def make_screenshot_root():
class CheckpointRecorder:
- STACK_VIEW_NAMES = ("mainPageStack", "createWalletWizard", "nodeSettingsStack")
+ STACK_VIEW_NAMES = ("mainPageStack", "createWalletWizard", "settingsNavigationStack_wallet")
def __init__(self, case_name, save_screenshots, screenshot_root):
self.case_name = case_name
@@ -146,8 +146,8 @@ def load_wallet(gui, harness, wallet_name):
def open_wallet_settings(gui):
gui.click("desktopWalletSettingsTabButton")
gui.settle()
- gui.wait_for_property("settings_wallet", "visible", True, timeout_ms=5000)
- gui.click("settings_wallet")
+ gui.wait_for_property("settingsSidebar_wallet", "visible", True, timeout_ms=5000)
+ gui.click("settingsSidebar_wallet")
gui.wait_for_page("walletSettingsPage", timeout_ms=10000)
@@ -241,23 +241,9 @@ def case_rename_persists_across_restart(harness, checkpoints):
open_wallet_settings(gui)
checkpoints.checkpoint("wallet settings opened", gui)
- # Regression: the divider between Addresses and Set password used
- # height: visible ? 1 : 0, which left it laid out at height 0 even though it
- # was visible, so the line never rendered. On a passphrase-managed wallet it
- # must have a real, non-zero height.
- gui.wait_for_property("walletSettingsPasswordDivider", "visible", True, timeout_ms=5000)
- divider_height = gui.get_property("walletSettingsPasswordDivider", "height")
- assert divider_height and divider_height > 0, (
- f"Addresses/Set password divider should render with a non-zero height, got {divider_height!r}"
- )
- checkpoints.checkpoint("password divider renders", gui)
-
- gui.click("walletSettingsNameEditButton")
- gui.wait_for_property("walletSettingsNameEditField", "visible", True, timeout_ms=5000)
- gui.set_text("walletSettingsNameEditField", display_name)
- gui.wait_for_property("walletSettingsNameConfirmButton", "enabled", True, timeout_ms=5000)
- gui.click("walletSettingsNameConfirmButton")
- gui.wait_for_property("walletSettingsNameValue", "text", display_name, timeout_ms=5000)
+ gui.wait_for_property("walletNameInput", "visible", True, timeout_ms=5000)
+ gui.set_text("walletNameInput", display_name)
+ gui.invoke("walletNameInput", "editingFinished")
gui.wait_for_property("walletBadge", "text", display_name, timeout_ms=5000)
checkpoints.checkpoint("wallet renamed", gui)
@@ -291,9 +277,11 @@ def case_backup_uses_automation_path(harness, checkpoints):
checkpoints.checkpoint("wallet settings opened", gui)
gui.set_text("walletSettingsBackupPathField", backup_dir)
- gui.click("walletSettingsBackupRow")
+ gui.click("walletBackupRow")
wait_for_file(backup_path)
- assert gui.get_text("walletSettingsErrorText") == "", "Backup should not surface an error"
+ assert gui.get_property("walletSettingsPage", "errorText") == "", (
+ "Backup should not surface an error"
+ )
checkpoints.checkpoint("wallet backup created", gui)
@@ -316,7 +304,7 @@ def case_sign_verify_message(harness, checkpoints):
address = rpc_call(harness.gui_rpc_port, "getnewaddress", ["", "legacy"], wallet=wallet_name)
open_wallet_settings(gui)
- gui.click("walletSettingsSignVerifyMessageRow")
+ gui.click("walletSignVerifyMessageRow")
gui.wait_for_page("signVerifyMessagePage", timeout_ms=10000)
checkpoints.checkpoint("sign verify message page opened", gui)
@@ -365,7 +353,7 @@ def case_subpages_close_when_wallet_becomes_unselected(harness, checkpoints):
checkpoints.checkpoint("managed wallet loaded", gui)
open_wallet_settings(gui)
- gui.click("walletSettingsPasswordRow")
+ gui.click("walletPasswordRow")
gui.wait_for_page("walletPasswordSettingsPage", timeout_ms=10000)
checkpoints.checkpoint("password subpage opened", gui)
@@ -380,7 +368,7 @@ def case_subpages_close_when_wallet_becomes_unselected(harness, checkpoints):
select_wallet(gui, wallet_name)
wait_for_wallet_ready(harness, gui)
gui.wait_for_property("walletBadge", "noWalletLoaded", False, timeout_ms=5000)
- gui.wait_for_property("walletSettingsNameRow", "visible", True, timeout_ms=5000)
+ gui.wait_for_property("walletNameInput", "visible", True, timeout_ms=5000)
checkpoints.checkpoint("wallet reselected from settings page", gui)
@@ -405,7 +393,7 @@ def case_password_page_closes_when_selected_wallet_changes(harness, checkpoints)
checkpoints.checkpoint("first wallet selected", gui)
open_wallet_settings(gui)
- gui.click("walletSettingsPasswordRow")
+ gui.click("walletPasswordRow")
gui.wait_for_page("walletPasswordSettingsPage", timeout_ms=10000)
checkpoints.checkpoint("password subpage opened for first wallet", gui)
@@ -432,7 +420,7 @@ def case_wrong_current_password_clears_current_field(harness, checkpoints):
checkpoints.checkpoint("managed wallet loaded", gui)
open_wallet_settings(gui)
- gui.click("walletSettingsPasswordRow")
+ gui.click("walletPasswordRow")
gui.wait_for_page("walletPasswordSettingsPage", timeout_ms=10000)
gui.set_text("walletPasswordCurrentField", "wrong password")
diff --git a/test/qml/bitcoin_qmltests.qrc b/test/qml/bitcoin_qmltests.qrc
index c82b227ecd..aa1287754f 100644
--- a/test/qml/bitcoin_qmltests.qrc
+++ b/test/qml/bitcoin_qmltests.qrc
@@ -17,19 +17,18 @@
tst_createwalletwizard.qml
tst_debuglogoutputview.qml
tst_desktopwallets.qml
- tst_displaysettings.qml
tst_dropdownbutton.qml
tst_externalsignerreviewactions.qml
tst_feeselection.qml
tst_importwalletoptions.qml
+ tst_formcontrols.qml
tst_mainrouting.qml
tst_mempoolinformationrows.qml
- tst_mempoolinformationsettings.qml
tst_navbutton.qml
tst_nodefeedback.qml
- tst_nodesettings.qml
tst_onboarding_datadir.qml
tst_peeractions.qml
+ tst_popuppicker.qml
tst_proxylocationinput.qml
tst_requestpayment.qml
tst_rightcontenticon.qml
@@ -37,15 +36,13 @@
tst_send.qml
tst_setting.qml
tst_settingsheader.qml
+ tst_settingsnavigation.qml
tst_settingsstatus.qml
- tst_settingswallet.qml
- tst_settingswindowbehavior.qml
tst_utils.qml
tst_valueinput.qml
tst_walletpassphrasepopup.qml
tst_walletpasswordsettings.qml
tst_walletselect.qml
- tst_walletsettings.qml
tst_wallettypelistitem.qml
tst_watchonlyxpub.qml
diff --git a/test/qml/qml_tests_main.cpp b/test/qml/qml_tests_main.cpp
index f3949f0936..8185d5d38e 100644
--- a/test/qml/qml_tests_main.cpp
+++ b/test/qml/qml_tests_main.cpp
@@ -1432,7 +1432,7 @@ class MockWalletController : public QObject
Q_EMIT walletMigrationSucceeded();
}
Q_INVOKABLE void requestOpenWalletSettings() { Q_EMIT openWalletSettingsRequested(); }
- void setSelectedWalletObject(QObject* wallet)
+ Q_INVOKABLE void setSelectedWalletObject(QObject* wallet)
{
if (m_selected_wallet == wallet) return;
m_selected_wallet = wallet;
@@ -1974,6 +1974,7 @@ class MockOptionsModel : public QObject
Q_INVOKABLE bool commitProxyLocation(const QString&) { return true; }
Q_INVOKABLE bool commitTorLocation(const QString&) { return true; }
Q_INVOKABLE QString defaultProxyAddress() const { return QStringLiteral("127.0.0.1:9050"); }
+ Q_INVOKABLE QString externalSignerPathValidationError(const QString&) const { return {}; }
QObject* coreSettings() { return &m_core_settings; }
QVariantMap coreSettingStatuses() const {
QVariantMap statuses;
diff --git a/test/qml/tst_contextmenu.qml b/test/qml/tst_contextmenu.qml
index 19caf68d72..9763d2fdf8 100644
--- a/test/qml/tst_contextmenu.qml
+++ b/test/qml/tst_contextmenu.qml
@@ -87,6 +87,7 @@ TestCase {
function test_empty_menu_clamps_to_min_width() {
const menu = openMenu(emptyMenuComponent)
compare(menu.implicitWidth, menu.minMenuWidth)
+ compare(menu.background.color, Theme.color.neutral1)
}
function test_escape_closes_focused_menu() {
diff --git a/test/qml/tst_contextmenubutton.qml b/test/qml/tst_contextmenubutton.qml
index 13c93bd79f..6cfb2ba5bb 100644
--- a/test/qml/tst_contextmenubutton.qml
+++ b/test/qml/tst_contextmenubutton.qml
@@ -47,7 +47,12 @@ TestCase {
compare(button.role, ContextMenuButton.Normal)
compare(button.autoClose, true)
compare(button.focusPolicy, Qt.StrongFocus)
+ compare(button.hoverBackgroundColor, Theme.color.neutral3)
verify(button.hoverEnabled)
+
+ button.forceActiveFocus(Qt.TabFocusReason)
+ tryCompare(button, "visualFocus", true)
+ compare(button.background.color, Theme.color.neutral3)
}
function test_destructive_role_marker() {
diff --git a/test/qml/tst_desktopwallets.qml b/test/qml/tst_desktopwallets.qml
index eacdeaec6d..2694dcd816 100644
--- a/test/qml/tst_desktopwallets.qml
+++ b/test/qml/tst_desktopwallets.qml
@@ -26,6 +26,7 @@ TestCase {
walletController.initialized = true
walletController.isWalletLoaded = true
walletController.noWalletsFound = false
+ walletController.setSelectedWalletObject(testWalletModel)
walletListModel.reset()
}
@@ -82,7 +83,6 @@ TestCase {
const tabs = [
findChild(page, "blockClockTabButton"),
findChild(page, "peersTabButton"),
- findChild(page, "consoleTabButton"),
findChild(page, "desktopWalletSettingsTabButton")
]
@@ -93,28 +93,32 @@ TestCase {
}
compare(tabs[1].iconSize, 24)
- compare(tabs[2].iconSize, 24)
- compare(tabs[3].iconSize, 30)
+ compare(tabs[2].iconSize, 30)
+ compare(tabs[2].iconSource, "image://images/gear-outline")
+ compare(findChild(page, "consoleTabButton"), null)
+ compare(findChild(page, "desktopWalletSettingsPreviewTabButton"), null)
}
- function test_console_autocomplete_closes_when_switching_tabs() {
+ function test_settings_is_lazilyLoadedAndRetained() {
const page = createDesktopWallets()
- const consoleTab = findChild(page, "consoleTabButton")
- const activityTab = findChild(page, "activityTabButton")
- const popup = findChild(page, "consoleAutocompletePopup")
-
- verify(consoleTab !== null)
- verify(activityTab !== null)
- verify(popup !== null)
-
- consoleTab.checked = true
- tryCompare(consoleTab, "checked", true)
- popup.open()
- tryCompare(popup, "visible", true)
+ const settingsTab = findChild(page, "desktopWalletSettingsTabButton")
+ const settingsLoader = findChild(page, "settingsLoader")
- activityTab.checked = true
- tryCompare(activityTab, "checked", true)
- tryCompare(popup, "visible", false)
+ verify(settingsTab !== null)
+ verify(settingsLoader !== null)
+ compare(settingsLoader.active, false)
+ compare(settingsLoader.item, null)
+
+ settingsTab.checked = true
+ tryCompare(settingsTab, "checked", true)
+ tryCompare(settingsLoader, "active", true)
+ tryVerify(function() { return settingsLoader.item !== null })
+ compare(settingsLoader.item.objectName, "settingsView")
+ const settingsView = settingsLoader.item
+
+ settingsTab.checked = false
+ compare(settingsLoader.item, settingsView)
+ compare(settingsLoader.active, true)
}
function test_receive_options_view_address_history_opens_settings_address_stack() {
@@ -139,13 +143,15 @@ TestCase {
verify(settingsTab !== null)
compare(settingsTab.checked, true)
- const settingsPage = findChild(page, "nodeSettingsStack")
+ const settingsPage = findChild(page, "settingsView")
verify(settingsPage !== null)
- tryVerify(function() { return findChild(page, "walletSettingsStack") !== null })
- const walletStack = findChild(page, "walletSettingsStack")
- tryCompare(walletStack, "depth", 2)
- compare(walletStack.currentItem.objectName, "addressListPage")
+ const settingsContainer = findChild(page, "settingsPageContainer")
+ verify(settingsContainer !== null)
+ tryCompare(settingsPage, "selectedSectionId", "wallet")
+ tryCompare(settingsContainer, "currentSectionId", "wallet")
+ tryCompare(settingsContainer, "depth", 2)
+ compare(settingsContainer.currentItem.objectName, "addressListPage")
verify(findChild(page, "walletSettingsPage") !== null)
}
}
diff --git a/test/qml/tst_displaysettings.qml b/test/qml/tst_displaysettings.qml
deleted file mode 100644
index 9eeed90673..0000000000
--- a/test/qml/tst_displaysettings.qml
+++ /dev/null
@@ -1,204 +0,0 @@
-// Copyright (c) 2026 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 QtTest 1.2
-import "../../qml/controls"
-
-TestCase {
- name: "DisplaySettings"
- when: windowShown
- width: 600
- height: 800
-
- // Minimal component exercising display-unit OptionButton binding logic.
- // Uses OptionButton directly (no NavButton / org.bitcoincore.qt dependency)
- // to test the optionsModel.displayUnit binding in isolation.
- // ButtonGroup is intentionally omitted: the declarative 'checked:' bindings
- // already model mutual exclusion through optionsModel, and ButtonGroup's
- // managed-checked behavior conflicts with declarative bindings in tests.
- Component {
- id: displayUnitButtons
- Column {
- OptionButton {
- objectName: "displayUnitBTC"
- text: "BTC"
- checked: optionsModel.displayUnit === 0
- onClicked: optionsModel.displayUnit = 0
- }
- OptionButton {
- objectName: "displayUnitMBTC"
- text: "mBTC"
- checked: optionsModel.displayUnit === 1
- onClicked: optionsModel.displayUnit = 1
- }
- OptionButton {
- objectName: "displayUnitUBTC"
- text: "bits"
- checked: optionsModel.displayUnit === 2
- onClicked: optionsModel.displayUnit = 2
- }
- OptionButton {
- objectName: "displayUnitSAT"
- text: "sat"
- checked: optionsModel.displayUnit === 3
- onClicked: optionsModel.displayUnit = 3
- }
- }
- }
-
- function test_displayUnit_BTC_button_checked_by_default() {
- optionsModel.displayUnit = 0
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const btcBtn = findChild(obj, "displayUnitBTC")
- verify(btcBtn !== null)
- compare(btcBtn.checked, true)
-
- const satBtn = findChild(obj, "displayUnitSAT")
- verify(satBtn !== null)
- compare(satBtn.checked, false)
- }
-
- function test_displayUnit_SAT_button_updates_on_model_change() {
- optionsModel.displayUnit = 3
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const satBtn = findChild(obj, "displayUnitSAT")
- verify(satBtn !== null)
- compare(satBtn.checked, true)
-
- const btcBtn = findChild(obj, "displayUnitBTC")
- verify(btcBtn !== null)
- compare(btcBtn.checked, false)
-
- // Reset
- optionsModel.displayUnit = 0
- }
-
- function test_displayUnit_clicking_SAT_updates_model() {
- optionsModel.displayUnit = 0
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const satBtn = findChild(obj, "displayUnitSAT")
- verify(satBtn !== null)
-
- // Invoke the onClicked handler directly to simulate a user press.
- satBtn.clicked()
- compare(optionsModel.displayUnit, 3)
-
- // Reset
- optionsModel.displayUnit = 0
- }
-
- function test_displayUnit_clicking_BTC_after_SAT_resets_model() {
- optionsModel.displayUnit = 3
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const btcBtn = findChild(obj, "displayUnitBTC")
- verify(btcBtn !== null)
- compare(btcBtn.checked, false)
-
- btcBtn.clicked()
- compare(optionsModel.displayUnit, 0)
- }
-
- function test_displayUnit_clicking_mBTC_updates_model() {
- optionsModel.displayUnit = 0
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const mbtcBtn = findChild(obj, "displayUnitMBTC")
- verify(mbtcBtn !== null)
-
- mbtcBtn.clicked()
- compare(optionsModel.displayUnit, 1)
-
- optionsModel.displayUnit = 0
- }
-
- function test_displayUnit_clicking_bits_updates_model() {
- optionsModel.displayUnit = 0
- const obj = createTemporaryObject(displayUnitButtons, this)
- verify(obj !== null)
-
- const ubtcBtn = findChild(obj, "displayUnitUBTC")
- verify(ubtcBtn !== null)
-
- ubtcBtn.clicked()
- compare(optionsModel.displayUnit, 2)
-
- optionsModel.displayUnit = 0
- }
-
- // Mirrors the balance suffix expression in WalletBadge.qml.
- // balanceSatoshi=1000 → plural "sats"; balanceSatoshi=1 → singular "sat".
- Component {
- id: balanceSuffixComponent
- Text {
- property string balance: "1 000"
- property var balanceSatoshi: 1000
- text: balance + " " + optionsModel.displayUnitLabelForAmount(balanceSatoshi)
- }
- }
-
- function test_walletBadge_suffix_is_sats_in_sat_mode() {
- optionsModel.displayUnit = 3
- const obj = createTemporaryObject(balanceSuffixComponent, this)
- verify(obj !== null)
- compare(obj.text, "1 000 sats")
- optionsModel.displayUnit = 0
- }
-
- function test_walletBadge_suffix_is_sat_singular_in_sat_mode() {
- optionsModel.displayUnit = 3
- const obj = createTemporaryObject(balanceSuffixComponent, this)
- verify(obj !== null)
- obj.balanceSatoshi = 1
- compare(obj.text, "1 000 sat")
- optionsModel.displayUnit = 0
- }
-
- function test_walletBadge_suffix_is_btc_symbol_in_btc_mode() {
- optionsModel.displayUnit = 0
- const obj = createTemporaryObject(balanceSuffixComponent, this)
- verify(obj !== null)
- compare(obj.text, "1 000 â‚¿")
- }
-
- // Tests displayUnitLabelForAmount pluralization logic.
- function test_displayUnitLabelForAmount_singular_in_sat_mode() {
- optionsModel.displayUnit = 3
- compare(optionsModel.displayUnitLabelForAmount(1), "sat")
- compare(optionsModel.displayUnitLabelForAmount(-1), "sat")
- optionsModel.displayUnit = 0
- }
-
- function test_displayUnitLabelForAmount_plural_in_sat_mode() {
- optionsModel.displayUnit = 3
- compare(optionsModel.displayUnitLabelForAmount(0), "sats")
- compare(optionsModel.displayUnitLabelForAmount(2), "sats")
- compare(optionsModel.displayUnitLabelForAmount(1000), "sats")
- optionsModel.displayUnit = 0
- }
-
- function test_displayUnitLabelForAmount_btc_symbol_in_btc_mode() {
- optionsModel.displayUnit = 0
- compare(optionsModel.displayUnitLabelForAmount(1), "â‚¿")
- compare(optionsModel.displayUnitLabelForAmount(1000), "â‚¿")
- }
-
- function test_displayUnitLabelForAmount_mbtc_and_bits() {
- optionsModel.displayUnit = 1
- compare(optionsModel.displayUnitLabelForAmount(1000), "mBTC")
- optionsModel.displayUnit = 2
- compare(optionsModel.displayUnitLabelForAmount(1000), "bits")
- optionsModel.displayUnit = 0
- }
-}
diff --git a/test/qml/tst_formcontrols.qml b/test/qml/tst_formcontrols.qml
new file mode 100644
index 0000000000..2f301f384c
--- /dev/null
+++ b/test/qml/tst_formcontrols.qml
@@ -0,0 +1,396 @@
+// Copyright (c) 2026 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.Layouts 1.15
+import QtTest 1.2
+import "../../qml/controls"
+import "../../qml/pages/settings"
+
+TestCase {
+ id: testCase
+ name: "FormControls"
+ when: windowShown
+ width: 640
+ height: 640
+
+ Item {
+ id: host
+ anchors.fill: parent
+ }
+
+ Component {
+ id: formRowComponent
+
+ FormRow {
+ objectName: "exampleRow"
+ width: 480
+ title: "Theme"
+ description: "Choose the application appearance."
+ supportingText: "Managed by the application."
+ trailingItem: OptionSwitch {
+ objectName: "exampleSwitch"
+ checked: true
+ }
+ }
+ }
+
+ Component {
+ id: formSectionComponent
+
+ FormSection {
+ objectName: "exampleSection"
+ width: 480
+ title: "Appearance"
+ footerText: "Changes apply immediately."
+
+ FormRow {
+ Layout.fillWidth: true
+ title: "First"
+ }
+
+ FormRow {
+ Layout.fillWidth: true
+ title: "Second"
+ showDivider: false
+ }
+ }
+ }
+
+ Component {
+ id: listRowComponent
+
+ ListRow {
+ objectName: "exampleDisclosureRow"
+ width: 480
+ title: "Display"
+ selected: true
+ showsDisclosureIndicator: true
+ disclosureIndicatorObjectName: "exampleDisclosureRowDisclosureIndicator"
+ }
+ }
+
+ Component {
+ id: pageHeadingComponent
+
+ PageHeading {
+ objectName: "exampleHeading"
+ width: 480
+ title: "General"
+ description: "Customize the application."
+ }
+ }
+
+ Component {
+ id: popupPickerComponent
+
+ PopupPicker {
+ objectName: "examplePicker"
+ width: 180
+ currentValue: "light"
+ model: [
+ { text: "Light", value: "light" },
+ { text: "Dark", value: "dark" }
+ ]
+ }
+ }
+
+ Component {
+ id: designSystemPageComponent
+
+ SettingsDesignSystem {
+ width: 600
+ height: 900
+ }
+ }
+
+ Component {
+ id: settingsPageComponent
+
+ SettingsPage {
+ objectName: "exampleSettingsPage"
+ width: 720
+ height: 640
+ title: "Settings"
+ backButtonObjectName: "exampleSettingsBack"
+ maximumContentWidth: 420
+ contentSpacing: 12
+ rightItem: NavButton {
+ objectName: "exampleSettingsAction"
+ text: "Done"
+ }
+
+ FormSection {
+ objectName: "exampleSettingsSection"
+ Layout.fillWidth: true
+ title: "General"
+
+ FormRow {
+ Layout.fillWidth: true
+ title: "Example"
+ showDivider: false
+ }
+ }
+ }
+ }
+
+ Component {
+ id: valueRowComponent
+
+ ValueRow {
+ objectName: "exampleValueRow"
+ width: 480
+ title: "Version"
+ value: "v31.99.0-unk"
+ }
+ }
+
+ Component {
+ id: linkRowComponent
+
+ LinkRow {
+ objectName: "exampleLinkRow"
+ width: 480
+ title: "Website"
+ value: "bitcoincore.org"
+ link: "https://bitcoincore.org"
+ }
+ }
+
+ Component {
+ id: textFieldRowComponent
+
+ TextFieldRow {
+ objectName: "exampleTextFieldRow"
+ fieldObjectName: "exampleTextField"
+ width: 480
+ title: "Proxy location"
+ text: "127.0.0.1:9050"
+ }
+ }
+
+ Component {
+ id: bodyRowComponent
+
+ FormRow {
+ objectName: "exampleBodyRow"
+ width: 480
+ title: "Data directory"
+ bodyItem: CoreText {
+ objectName: "exampleBodyContent"
+ Layout.fillWidth: true
+ text: "/Users/example/Bitcoin"
+ }
+ }
+ }
+
+ function test_formRowLoadsAndDisablesTrailingControl() {
+ const row = createTemporaryObject(formRowComponent, host)
+ verify(row !== null)
+ tryVerify(function() { return row.loadedTrailingItem !== null })
+ compare(row.loadedTrailingItem.objectName, "exampleSwitch")
+ compare(row.loadedTrailingItem.enabled, true)
+
+ row.enabled = false
+ compare(row.loadedTrailingItem.enabled, false)
+ tryCompare(findChild(row, "exampleRowTitle"), "color", Theme.color.neutral4)
+ }
+
+ function test_formSectionOwnsCardAndContent() {
+ const section = createTemporaryObject(formSectionComponent, host)
+ verify(section !== null)
+ const card = findChild(section, "exampleSectionCard")
+ verify(card !== null)
+ compare(card.color, Theme.color.neutral1)
+ compare(card.radius, 16)
+ const footer = findChild(section, "exampleSectionFooter")
+ verify(footer !== null)
+ compare(footer.text, "Changes apply immediately.")
+ compare(footer.font.pixelSize, Theme.text.caption.font.pixelSize)
+ verify(section.implicitHeight > 0)
+ }
+
+ function test_listRowSelectionAndActivation() {
+ const row = createTemporaryObject(listRowComponent, host)
+ verify(row !== null)
+ compare(row.selected, true)
+ compare(row.background.color, row.selectedBackgroundColor)
+ compare(row.cornerRadius, 16)
+ compare(row.background.radius, 16)
+ const disclosureIndicator = findChild(row, "exampleDisclosureRowDisclosureIndicator")
+ verify(disclosureIndicator !== null)
+ compare(disclosureIndicator.size, 14)
+
+ let clickCount = 0
+ row.clicked.connect(function() { clickCount += 1 })
+ row.clicked()
+ compare(clickCount, 1)
+ }
+
+ function test_pageHeadingUsesThemeTypography() {
+ const heading = createTemporaryObject(pageHeadingComponent, host)
+ verify(heading !== null)
+ const title = findChild(heading, "exampleHeadingTitle")
+ const description = findChild(heading, "exampleHeadingDescription")
+ verify(title !== null)
+ verify(description !== null)
+ compare(title.font.pixelSize, Theme.text.headline.pixelSize)
+ compare(description.font.pixelSize, Theme.text.description.font.pixelSize)
+ }
+
+ function test_popupPickerMapsValuesAndLeavesStateCallerOwned() {
+ const picker = createTemporaryObject(popupPickerComponent, host)
+ verify(picker !== null)
+ compare(picker.currentText, "Light")
+ const button = findChild(picker, "examplePickerButton")
+ const menu = findChild(picker, "examplePickerMenu")
+ verify(button !== null)
+ verify(menu !== null)
+ compare(button.defaultBgColor, Theme.color.background)
+ compare(button.hoverBgColor, Theme.color.neutral2)
+ compare(menu.backgroundColor, Theme.color.neutral1)
+
+ picker.embedded = true
+ compare(button.defaultBgColor, Theme.color.neutral2)
+ compare(button.hoverBgColor, Theme.color.neutral3)
+ compare(menu.backgroundColor, Theme.color.neutral2)
+
+ picker.currentValue = "dark"
+ compare(picker.currentText, "Dark")
+
+ let activatedValue = ""
+ picker.activated.connect(function(value) { activatedValue = value })
+ tryVerify(function() { return picker.itemAtIndex(0) !== null })
+ picker.itemAtIndex(0).triggered()
+
+ compare(activatedValue, "light")
+ compare(picker.currentValue, "dark")
+ }
+
+ function test_designSystemPageShowsGenericControlExamples() {
+ const page = createTemporaryObject(designSystemPageComponent, host)
+ verify(page !== null)
+ verify(findChild(page, "settingsPageContentLayout") !== null)
+ verify(findChild(page, "designSystemAppearanceSection") !== null)
+ verify(findChild(page, "designSystemThemeRow") !== null)
+ verify(findChild(page, "designSystemLanguagePicker") !== null)
+ verify(findChild(page, "designSystemBehaviorSection") !== null)
+ verify(findChild(page, "designSystemNavigationSection") !== null)
+ verify(findChild(page, "designSystemValueSection") !== null)
+ verify(findChild(page, "designSystemWebsiteRow") !== null)
+ verify(findChild(page, "designSystemVersionRow") !== null)
+ verify(findChild(page, "designSystemFieldSection") !== null)
+ verify(findChild(page, "designSystemBlockStorageField") !== null)
+ verify(findChild(page, "designSystemProxyLocationField") !== null)
+ verify(findChild(page, "designSystemDataDirectoryValue") !== null)
+ }
+
+ function test_settingsPageOwnsNavigationAndConstrainedScrollableContent() {
+ const page = createTemporaryObject(settingsPageComponent, host)
+ verify(page !== null)
+ compare(page.pageHeader.title, "Settings")
+ compare(page.pageHeader.backButtonObjectName, "exampleSettingsBack")
+ verify(findChild(page, "exampleSettingsAction") !== null)
+ verify(findChild(page, "exampleSettingsSection") !== null)
+ compare(page.contentLayout.width, 420)
+ compare(page.contentLayout.spacing, 12)
+ compare(page.scrollView.contentWidth, page.scrollView.availableWidth)
+
+ let backCount = 0
+ page.back.connect(function() { backCount += 1 })
+ page.pageHeader.back()
+ compare(backCount, 1)
+ }
+
+ function test_settingsPageKeepsImplicitWidthIndependentOfLayout() {
+ const page = createTemporaryObject(settingsPageComponent, host)
+ verify(page !== null)
+
+ const implicitWidthBefore = page.implicitWidth
+ const widthBefore = page.width
+ page.width = widthBefore / 2
+
+ verify(page.width !== widthBefore)
+ compare(page.implicitWidth, implicitWidthBefore)
+ }
+
+ function test_valueRowDisplaysCallerOwnedValue() {
+ const row = createTemporaryObject(valueRowComponent, host)
+ verify(row !== null)
+ const value = findChild(row, "exampleValueRowValue")
+ verify(value !== null)
+ compare(value.text, "v31.99.0-unk")
+
+ row.value = "v32.0"
+ compare(value.text, "v32.0")
+ }
+
+ function test_pageHeadingCentersProminentDescription() {
+ const heading = createTemporaryObject(pageHeadingComponent, host)
+ verify(heading !== null)
+ const description = findChild(heading, "exampleHeadingDescription")
+ verify(description !== null)
+ compare(heading.descriptionTextStyle.font.pixelSize, Theme.text.description.font.pixelSize)
+ compare(description.font.pixelSize, Theme.text.description.font.pixelSize)
+ compare(description.horizontalAlignment, Text.AlignHCenter)
+ }
+
+ function test_linkRowEmitsWithoutOpeningTheUrl() {
+ const row = createTemporaryObject(linkRowComponent, host)
+ verify(row !== null)
+
+ let activatedLink = ""
+ row.activated.connect(function(link) { activatedLink = link.toString() })
+ row.clicked()
+
+ compare(activatedLink, "https://bitcoincore.org")
+ compare(findChild(row, "exampleLinkRowValue").text, "bitcoincore.org")
+ }
+
+ function test_textFieldRowUsesCompactTrailingEditor() {
+ const row = createTemporaryObject(textFieldRowComponent, host)
+ verify(row !== null)
+ verify(row.field !== null)
+ compare(row.field.objectName, "exampleTextField")
+ compare(row.field, row.loadedTrailingItem)
+ compare(row.loadedBodyItem, null)
+ compare(row.text, "127.0.0.1:9050")
+ compare(row.field.horizontalAlignment, Text.AlignRight)
+ compare(row.focusBorderColor, Theme.color.orange)
+ compare(row.field.background.border.color, Theme.color.orange)
+
+ row.text = "127.0.0.1:9150"
+ compare(row.field.text, "127.0.0.1:9150")
+ }
+
+ function test_textFieldRowResignsFocusWhenAccepted() {
+ const row = createTemporaryObject(textFieldRowComponent, host)
+ verify(row !== null)
+ verify(row.field !== null)
+
+ let acceptedCount = 0
+ row.accepted.connect(function() { acceptedCount += 1 })
+
+ row.field.forceActiveFocus()
+ verify(row.field.activeFocus)
+ keyClick(Qt.Key_Return)
+ tryCompare(row.field, "activeFocus", false)
+ compare(acceptedCount, 1)
+
+ row.field.forceActiveFocus()
+ verify(row.field.activeFocus)
+ keyClick(Qt.Key_Enter)
+ tryCompare(row.field, "activeFocus", false)
+ compare(acceptedCount, 2)
+ }
+
+ function test_formRowAcceptsFullWidthBodyContent() {
+ const row = createTemporaryObject(bodyRowComponent, host)
+ verify(row !== null)
+ verify(row.loadedBodyItem !== null)
+ compare(row.loadedBodyItem.objectName, "exampleBodyContent")
+ compare(row.loadedBodyItem.text, "/Users/example/Bitcoin")
+ verify(row.implicitHeight > row.minimumRowHeight)
+ }
+}
diff --git a/test/qml/tst_mempoolinformationsettings.qml b/test/qml/tst_mempoolinformationsettings.qml
deleted file mode 100644
index f45e6095b5..0000000000
--- a/test/qml/tst_mempoolinformationsettings.qml
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright (c) 2026 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.Window 2.15
-import QtTest 1.2
-import "../../qml/pages/node"
-
-TestCase {
- name: "MempoolInformationSettings"
- when: windowShown
- width: 520
- height: 720
-
- Window {
- id: testWindow
- width: 520
- height: 720
- visible: true
- }
-
- Component {
- id: mempoolInformationSettingsComponent
-
- MempoolInformationSettings {
- width: 460
- height: 680
- }
- }
-
- function init() {
- nodeModel.resetMempoolInfoPollingTestState()
- optionsModel.mempoolSettingsDirty = false
- }
-
- function createMempoolInformationSettingsPage() {
- const page = createTemporaryObject(mempoolInformationSettingsComponent, testWindow.contentItem)
- verify(page !== null)
- page.visible = false
- wait(0)
- page.visible = true
- wait(0)
- compare(page.visible, true)
- return page
- }
-
- function test_polling_activity_tracks_page_visibility() {
- const page = createMempoolInformationSettingsPage()
- compare(nodeModel.mempoolInfoPollingActive, true)
-
- page.visible = false
- compare(nodeModel.mempoolInfoPollingActive, false)
-
- page.visible = true
- compare(nodeModel.mempoolInfoPollingActive, true)
- }
-
- function test_polling_activity_stops_on_page_destruction() {
- const page = createMempoolInformationSettingsPage()
- compare(nodeModel.mempoolInfoPollingActive, true)
-
- page.destroy()
- wait(0)
- compare(nodeModel.mempoolInfoPollingActive, false)
- }
-
- function test_restart_notice_hidden_when_mempool_settings_unchanged() {
- const page = createMempoolInformationSettingsPage()
- const notice = findChild(page, "mempoolRestartNotice")
- verify(notice !== null)
- compare(notice.visible, false)
- }
-
- function test_restart_notice_visible_when_mempool_settings_changed() {
- optionsModel.mempoolSettingsDirty = true
- const page = createMempoolInformationSettingsPage()
- const notice = findChild(page, "mempoolRestartNotice")
- verify(notice !== null)
- compare(notice.visible, true)
- }
-}
diff --git a/test/qml/tst_nodefeedback.qml b/test/qml/tst_nodefeedback.qml
index f2bc312f99..29b63c78ad 100644
--- a/test/qml/tst_nodefeedback.qml
+++ b/test/qml/tst_nodefeedback.qml
@@ -172,6 +172,7 @@ TestCase {
verify(warningButton !== null)
verify(infoButton !== null)
verify(settingsButton !== null)
+ compare(findChild(runner, "consoleTabButton"), null)
tryCompare(warningButton, "visible", true)
compare(warningButton.height, 34)
diff --git a/test/qml/tst_nodesettings.qml b/test/qml/tst_nodesettings.qml
deleted file mode 100644
index cd3e649665..0000000000
--- a/test/qml/tst_nodesettings.qml
+++ /dev/null
@@ -1,279 +0,0 @@
-// Copyright (c) 2026 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.Window 2.15
-import QtTest 1.2
-import org.bitcoincore.qt 1.0
-import "../../qml/pages/node"
-
-TestCase {
- name: "NodeSettings"
- when: windowShown
- width: 520
- height: 720
-
- Window {
- id: testWindow
- width: 520
- height: 720
- visible: true
- }
-
- Component {
- id: nodeSettingsComponent
-
- NodeSettings {
- width: 460
- height: 680
- }
- }
-
- Component {
- id: subPageComponent
- Item {}
- }
-
- function init() {
- nodeModel.mempoolInformationAvailable = true
- AppMode.walletEnabled = true
- AppMode.isDesktop = true
- testNetworkTrafficTower.active = false
- testDebugLogModel.active = false
- testDebugLogModel.filter = ""
- testDebugLogModel.resetForTest(0, false)
- }
-
- function createNodeSettingsPage() {
- const page = createTemporaryObject(nodeSettingsComponent, testWindow.contentItem)
- verify(page !== null)
- wait(0)
- return page
- }
-
- function test_mempool_information_row_visible_when_available() {
- const page = createNodeSettingsPage()
- const row = findChild(page, "settings_mempool")
- verify(row !== null)
- compare(row.visible, true)
- }
-
- function test_mempool_information_row_hidden_when_unavailable() {
- nodeModel.mempoolInformationAvailable = false
-
- const page = createNodeSettingsPage()
- const row = findChild(page, "settings_mempool")
- verify(row !== null)
- compare(row.visible, false)
- }
-
- function test_sidebar_section_switching() {
- const page = createNodeSettingsPage()
-
- // currentSection is the sidebar row index in the grouped order:
- // Wallet(0), External Signer(1), Display(2), Window Behavior(3),
- // Storage(4), Connection(5), Network Traffic(6), Mempool(7),
- // Debug Log(8), About(9). With the wallet enabled the page lands on
- // the first visible row, Wallet.
- compare(page.currentSection, 0)
-
- const displayItem = findChild(page, "settings_display")
- verify(displayItem !== null)
- mouseClick(displayItem, displayItem.width / 2, displayItem.height / 2)
- compare(page.currentSection, 2)
-
- const connectionItem = findChild(page, "settings_connection")
- verify(connectionItem !== null)
- mouseClick(connectionItem, connectionItem.width / 2, connectionItem.height / 2)
- compare(page.currentSection, 5)
- }
-
- function test_network_traffic_only_publishes_while_selected() {
- const page = createNodeSettingsPage()
-
- compare(testNetworkTrafficTower.active, false)
- verify(findChild(page, "networkTrafficPage") === null)
-
- const networkTrafficItem = findChild(page, "settings_networktraffic")
- verify(networkTrafficItem !== null)
- mouseClick(networkTrafficItem, networkTrafficItem.width / 2, networkTrafficItem.height / 2)
- tryCompare(page, "currentSection", 6)
- tryCompare(testNetworkTrafficTower, "active", true)
- verify(findChild(page, "networkTrafficPage") !== null)
-
- // Leaving Settings unloads both graphs and suppresses worker snapshots,
- // while the C++ sampler continues retaining raw history off-thread.
- page.visible = false
- tryCompare(testNetworkTrafficTower, "active", false)
- tryVerify(function() { return findChild(page, "networkTrafficPage") === null })
-
- page.visible = true
- tryCompare(testNetworkTrafficTower, "active", true)
- verify(findChild(page, "networkTrafficPage") !== null)
-
- const displayItem = findChild(page, "settings_display")
- verify(displayItem !== null)
- mouseClick(displayItem, displayItem.width / 2, displayItem.height / 2)
- tryCompare(page, "currentSection", 2)
- tryCompare(testNetworkTrafficTower, "active", false)
- tryVerify(function() { return findChild(page, "networkTrafficPage") === null })
- }
-
- function test_debug_log_only_active_while_selected() {
- const page = createNodeSettingsPage()
-
- compare(testDebugLogModel.active, false)
- verify(findChild(page, "settingsDebugLog") === null)
-
- const debugLogItem = findChild(page, "settings_debuglog")
- verify(debugLogItem !== null)
- mouseClick(debugLogItem, debugLogItem.width / 2, debugLogItem.height / 2)
- tryCompare(page, "currentSection", 8)
- tryCompare(testDebugLogModel, "active", true)
- verify(findChild(page, "settingsDebugLog") !== null)
-
- // DesktopWallets keeps NodeSettings in its outer StackLayout. Leaving
- // Settings for Send must unload the debug log even if it remains the
- // selected Settings section.
- page.visible = false
- tryCompare(testDebugLogModel, "active", false)
- tryVerify(function() { return findChild(page, "settingsDebugLog") === null })
-
- page.visible = true
- tryCompare(testDebugLogModel, "active", true)
- verify(findChild(page, "settingsDebugLog") !== null)
-
- const displayItem = findChild(page, "settings_display")
- verify(displayItem !== null)
- mouseClick(displayItem, displayItem.width / 2, displayItem.height / 2)
- tryCompare(page, "currentSection", 2)
- tryCompare(testDebugLogModel, "active", false)
- tryVerify(function() { return findChild(page, "settingsDebugLog") === null })
- }
-
- function test_debug_log_load_more_appends_without_jumping_to_new_bottom() {
- testDebugLogModel.resetForTest(100, true)
- const page = createNodeSettingsPage()
-
- const debugLogItem = findChild(page, "settings_debuglog")
- verify(debugLogItem !== null)
- mouseClick(debugLogItem, debugLogItem.width / 2, debugLogItem.height / 2)
- tryCompare(page, "currentSection", 8)
-
- const logView = findChild(page, "debugLogListView")
- const loadMoreButton = findChild(page, "debugLogLoadMoreButton")
- verify(logView !== null)
- verify(loadMoreButton !== null)
- tryCompare(logView, "count", 100)
-
- logView.scrollToBottom()
- tryCompare(logView, "atBottom", true)
- tryCompare(loadMoreButton, "visible", true)
- const anchoredContentY = logView.contentY
- mouseClick(loadMoreButton, loadMoreButton.width / 2, loadMoreButton.height / 2)
-
- tryCompare(testDebugLogModel, "loadMoreCalls", 1)
- tryCompare(logView, "count", 120)
- tryCompare(loadMoreButton, "visible", false)
- verify(Math.abs(logView.contentY - anchoredContentY) < 0.5,
- "Loading older rows should leave the previous bottom entries anchored: before="
- + anchoredContentY + ", after=" + logView.contentY
- + ", contentHeight=" + logView.contentHeight)
- compare(logView.atBottom, false)
- }
-
- function test_debug_log_filter_matches_model_after_page_reentry() {
- testDebugLogModel.filter = "retained filter"
- const page = createNodeSettingsPage()
-
- const debugLogItem = findChild(page, "settings_debuglog")
- verify(debugLogItem !== null)
- mouseClick(debugLogItem, debugLogItem.width / 2, debugLogItem.height / 2)
- tryCompare(page, "currentSection", 8)
-
- let searchField = findChild(page, "debugLogSearchField")
- verify(searchField !== null)
- compare(searchField.text, "retained filter")
- searchField.text = "updated filter"
- tryCompare(testDebugLogModel, "filter", "updated filter", 1000)
-
- const displayItem = findChild(page, "settings_display")
- verify(displayItem !== null)
- mouseClick(displayItem, displayItem.width / 2, displayItem.height / 2)
- tryCompare(page, "currentSection", 2)
- tryVerify(function() { return findChild(page, "settingsDebugLog") === null })
-
- mouseClick(debugLogItem, debugLogItem.width / 2, debugLogItem.height / 2)
- tryCompare(page, "currentSection", 8)
- searchField = findChild(page, "debugLogSearchField")
- verify(searchField !== null)
- compare(searchField.text, "updated filter")
- }
- function test_wallet_section_hidden_when_disabled() {
- AppMode.walletEnabled = false
-
- const page = createNodeSettingsPage()
- const walletItem = findChild(page, "settings_wallet")
- verify(walletItem !== null)
- compare(walletItem.visible, false)
-
- const signerItem = findChild(page, "settings_externalsigner")
- verify(signerItem !== null)
- compare(signerItem.visible, false)
- }
-
- function test_window_behavior_hidden_on_non_desktop() {
- AppMode.isDesktop = false
-
- const page = createNodeSettingsPage()
- const windowItem = findChild(page, "settings_windowbehavior")
- verify(windowItem !== null)
- compare(windowItem.visible, false)
- }
-
- function test_wallet_settings_back_button_stays_hidden_when_subpage_open() {
- const page = createNodeSettingsPage()
-
- const walletSettingsPage = findChild(page, "walletSettingsPage")
- verify(walletSettingsPage !== null)
- const walletStack = findChild(page, "walletSettingsStack")
- verify(walletStack !== null)
-
- // The wallet settings page is reached from the sidebar and has no back
- // button of its own. Pushing a sub-page must not turn it on: binding it
- // to depth > 1 flashed the back button on this page during the push
- // transition.
- compare(walletSettingsPage.showBackButton, false)
- walletStack.push(subPageComponent)
- wait(0)
- verify(walletStack.depth > 1)
- compare(walletSettingsPage.showBackButton, false)
- }
-
- function test_section_pages_keep_implicit_width_independent_of_layout() {
- const page = createNodeSettingsPage()
-
- const sections = [
- { index: 1, name: "settingsWallet" },
- { index: 4, name: "settingsStoragePage" },
- { index: 7, name: "mempoolInformationSettingsPage" }
- ]
-
- for (let i = 0; i < sections.length; ++i) {
- page.currentSection = sections[i].index
- wait(0)
-
- const sectionPage = findChild(page, sections[i].name)
- verify(sectionPage !== null, sections[i].name + " was not created")
-
- const implicitWidthBefore = sectionPage.implicitWidth
- const widthBefore = sectionPage.width
- sectionPage.width = widthBefore / 2
- verify(sectionPage.width !== widthBefore,
- sections[i].name + " ignored the width it was given")
- compare(sectionPage.implicitWidth, implicitWidthBefore,
- sections[i].name + " implicit width followed the width it was given")
- }
- }
-}
diff --git a/test/qml/tst_popuppicker.qml b/test/qml/tst_popuppicker.qml
new file mode 100644
index 0000000000..a2d30cf4a8
--- /dev/null
+++ b/test/qml/tst_popuppicker.qml
@@ -0,0 +1,74 @@
+// Copyright (c) 2026 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 QtTest 1.2
+import "../../qml/controls"
+
+TestCase {
+ name: "PopupPicker"
+ when: windowShown
+ width: 500
+ height: 300
+
+ Item {
+ id: host
+ width: parent.width
+ height: parent.height
+ }
+
+ Component {
+ id: adaptivePickerComponent
+
+ Item {
+ property alias picker: picker
+ property string selectedValue: "embedded"
+
+ PopupPicker {
+ id: picker
+ objectName: "adaptivePopupPicker"
+ currentValue: parent.selectedValue
+ selectionIconSource: ""
+ model: [
+ { text: "Roboto Mono", value: "embedded" },
+ { text: "System Monospace", value: "best_system" }
+ ]
+ }
+ }
+ }
+
+ function test_closed_chip_tracks_current_label_width() {
+ const pickerHost = createTemporaryObject(adaptivePickerComponent, host)
+ verify(pickerHost !== null)
+
+ const picker = pickerHost.picker
+ tryCompare(picker, "currentText", "Roboto Mono")
+ const shortWidth = picker.implicitWidth
+ verify(shortWidth > 0)
+ verify(shortWidth < picker.minimumMenuWidth)
+
+ pickerHost.selectedValue = "best_system"
+ tryCompare(picker, "currentText", "System Monospace")
+ tryVerify(function() { return picker.implicitWidth > shortWidth })
+
+ pickerHost.selectedValue = "embedded"
+ tryCompare(picker, "currentText", "Roboto Mono")
+ tryCompare(picker, "implicitWidth", shortWidth)
+ }
+
+ function test_menu_width_is_independent_from_closed_chip_width() {
+ const pickerHost = createTemporaryObject(adaptivePickerComponent, host)
+ verify(pickerHost !== null)
+
+ const picker = pickerHost.picker
+ const menu = findChild(pickerHost, "adaptivePopupPickerMenu")
+ verify(menu !== null)
+ verify(picker.implicitWidth < picker.minimumMenuWidth)
+
+ picker.open()
+ tryCompare(menu, "opened", true)
+ verify(menu.width >= picker.minimumMenuWidth)
+ picker.close()
+ }
+}
diff --git a/test/qml/tst_settingsheader.qml b/test/qml/tst_settingsheader.qml
index f916a77ed4..528432d56e 100644
--- a/test/qml/tst_settingsheader.qml
+++ b/test/qml/tst_settingsheader.qml
@@ -43,6 +43,15 @@ TestCase {
}
}
+ Component {
+ id: compactBackHeader
+ SettingsHeader {
+ width: 400
+ title: "Display"
+ backButtonObjectName: "compactSettingsBack"
+ }
+ }
+
// Regression: the right section must show its actions once visible. The
// previous binding also read contentItem.visible, the child's *effective*
// visibility, which includes the section's own Pane. Built while the parent
@@ -69,4 +78,20 @@ TestCase {
compare(backButton.iconSource.toString(), "image://images/caret-left")
tryVerify(function() { return backButton.width > 40 })
}
+
+ function test_compactBackIconKeepsSizeAcrossThemeChanges() {
+ const originalDark = Theme.dark
+ const header = createTemporaryObject(compactBackHeader, testCase)
+ verify(header !== null)
+ const icon = findChild(header, "settingsHeaderBackIcon")
+ verify(icon !== null)
+
+ compare(icon.width, 24)
+ compare(icon.height, 24)
+
+ Theme.dark = !originalDark
+ tryCompare(icon, "width", 24)
+ tryCompare(icon, "height", 24)
+ Theme.dark = originalDark
+ }
}
diff --git a/test/qml/tst_settingsnavigation.qml b/test/qml/tst_settingsnavigation.qml
new file mode 100644
index 0000000000..eba8c89a10
--- /dev/null
+++ b/test/qml/tst_settingsnavigation.qml
@@ -0,0 +1,800 @@
+// Copyright (c) 2026 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.Window 2.15
+import QtTest 1.2
+
+import org.bitcoincore.qt 1.0
+
+import "../../qml/components"
+import "../../qml/controls"
+
+TestCase {
+ id: testCase
+ name: "SettingsNavigation"
+ when: windowShown
+ width: 720
+ height: 640
+
+ property int firstCreated: 0
+ property int firstDestroyed: 0
+ property int pushedCreated: 0
+ property int pushedDestroyed: 0
+ property int secondCreated: 0
+ property int secondDestroyed: 0
+
+ Item {
+ id: host
+ anchors.fill: parent
+ }
+
+ Window {
+ id: settingsWindow
+ width: 900
+ height: 700
+ visible: true
+ }
+
+ Component {
+ id: containerComponent
+
+ SettingsPageContainer {
+ width: 480
+ height: 560
+ }
+ }
+
+ Component {
+ id: sidebarComponent
+
+ SettingsSidebar {
+ width: 190
+ height: 300
+ currentSectionId: "display"
+ groupTitles: ({
+ "wallet": "Wallet",
+ "general": "General",
+ "network": "Network"
+ })
+ model: [
+ { id: "wallet", label: "Wallet", group: "wallet" },
+ { id: "display", label: "Display", group: "general" },
+ { id: "hidden", label: "Hidden", group: "general", visible: false },
+ { id: "connection", label: "Connection", group: "network" }
+ ]
+ }
+ }
+
+ Component {
+ id: settingsViewComponent
+
+ SettingsView {
+ width: 900
+ height: 700
+ selectedSectionId: "display"
+ }
+ }
+
+ Component {
+ id: firstPage
+
+ Item {
+ objectName: "firstSettingsPage"
+ Component.onCompleted: testCase.firstCreated += 1
+ Component.onDestruction: testCase.firstDestroyed += 1
+ }
+ }
+
+ Component {
+ id: pushedPage
+
+ Item {
+ objectName: "pushedSettingsPage"
+ Component.onCompleted: testCase.pushedCreated += 1
+ Component.onDestruction: testCase.pushedDestroyed += 1
+ }
+ }
+
+ Component {
+ id: secondPage
+
+ Item {
+ objectName: "secondSettingsPage"
+ Component.onCompleted: testCase.secondCreated += 1
+ Component.onDestruction: testCase.secondDestroyed += 1
+ }
+ }
+
+ function init() {
+ firstCreated = 0
+ firstDestroyed = 0
+ pushedCreated = 0
+ pushedDestroyed = 0
+ secondCreated = 0
+ secondDestroyed = 0
+ AppMode.walletEnabled = true
+ AppMode.isDesktop = true
+ nodeModel.mempoolInformationAvailable = true
+ Theme.dark = true
+ Theme.blockclocksize = 5 / 12
+ optionsModel.displayUnit = 0
+ optionsModel.moneyFontChoice = "embedded"
+ optionsModel.maxMempoolSizeMB = 300
+ optionsModel.storageSettingsDirty = false
+ optionsModel.mempoolSettingsDirty = false
+ nodeModel.resetMempoolInfoPollingTestState()
+ optionsModel.clearCoreSettingStatusesForTest()
+ walletController.reset()
+ walletController.setWalletLoaded(true)
+ walletController.selectedWallet.isEncrypted = false
+ const proxySetting = optionsModel.coreSettings.entry("proxy")
+ const onionSetting = optionsModel.coreSettings.entry("onion")
+ proxySetting.enabled = false
+ proxySetting.address = proxySetting.defaultAddress()
+ onionSetting.enabled = false
+ onionSetting.address = onionSetting.defaultAddress()
+ testNetworkTrafficTower.active = false
+ testDebugLogModel.active = false
+ }
+
+ function test_sidebarFiltersGroupsAndEmitsStableSectionId() {
+ const sidebar = createTemporaryObject(sidebarComponent, host)
+ verify(sidebar !== null)
+ compare(sidebar.visibleSections.length, 3)
+ verify(findChild(sidebar, "settingsSidebar_wallet") !== null)
+ verify(findChild(sidebar, "settingsSidebar_display") !== null)
+ verify(findChild(sidebar, "settingsSidebar_hidden") === null)
+ const walletGroup = findChild(sidebar, "settingsSidebarGroup_wallet")
+ const generalGroup = findChild(sidebar, "settingsSidebarGroup_general")
+ const networkGroup = findChild(sidebar, "settingsSidebarGroup_network")
+ verify(walletGroup !== null)
+ verify(generalGroup !== null)
+ verify(networkGroup !== null)
+ compare(walletGroup.text, "Wallet")
+ compare(generalGroup.text, "General")
+ compare(networkGroup.text, "Network")
+ compare(walletGroup.font.styleName, "Semi Bold")
+
+ let activatedSection = ""
+ sidebar.sectionActivated.connect(function(sectionId) {
+ activatedSection = sectionId
+ })
+ const connection = findChild(sidebar, "settingsSidebar_connection")
+ verify(connection !== null)
+ connection.clicked()
+ compare(activatedSection, "connection")
+ }
+
+ function test_settingsViewAppliesRuntimeVisibilityGates() {
+ AppMode.walletEnabled = false
+ AppMode.isDesktop = false
+ nodeModel.mempoolInformationAvailable = false
+
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ compare(view.sectionIsVisible("wallet"), false)
+ compare(view.sectionIsVisible("external-signer"), false)
+ compare(view.sectionIsVisible("window-behavior"), false)
+ compare(view.sectionIsVisible("mempool"), false)
+ verify(findChild(view, "settingsSidebar_wallet") === null)
+ verify(findChild(view, "settingsSidebar_external-signer") === null)
+ verify(findChild(view, "settingsSidebar_window-behavior") === null)
+ verify(findChild(view, "settingsSidebar_mempool") === null)
+
+ compare(view.sectionIsVisible("display"), true)
+ compare(view.selectedSectionId, "display")
+ verify(findChild(view, "settingsSidebar_display") !== null)
+ }
+
+ function test_containerLazilyCachesAndRestoresEachSectionStack() {
+ const container = createTemporaryObject(containerComponent, host)
+ verify(container !== null)
+
+ container.showSection("first", firstPage)
+ compare(container.currentSectionId, "first")
+ compare(container.depth, 1)
+ compare(firstCreated, 1)
+ compare(firstDestroyed, 0)
+
+ container.push(pushedPage)
+ tryCompare(container, "depth", 2)
+ const pushedItem = container.currentItem
+ compare(firstDestroyed, 0)
+ compare(pushedCreated, 1)
+ compare(pushedDestroyed, 0)
+
+ container.showSection("second", secondPage)
+ compare(container.currentSectionId, "second")
+ compare(container.depth, 1)
+ compare(firstDestroyed, 0)
+ compare(pushedDestroyed, 0)
+ compare(secondCreated, 1)
+
+ container.showSection("first", firstPage)
+ compare(container.currentSectionId, "first")
+ compare(container.depth, 2)
+ compare(container.currentItem, pushedItem)
+ compare(firstCreated, 1)
+ compare(pushedCreated, 1)
+ compare(secondDestroyed, 0)
+
+ container.clear()
+ compare(container.depth, 0)
+ tryCompare(testCase, "firstDestroyed", 1)
+ tryCompare(testCase, "pushedDestroyed", 1)
+ tryCompare(testCase, "secondDestroyed", 1)
+ }
+
+ function test_selectingCurrentSectionDoesNotReloadItsStack() {
+ const container = createTemporaryObject(containerComponent, host)
+ verify(container !== null)
+
+ container.showSection("first", firstPage)
+ container.push(pushedPage)
+ tryCompare(container, "depth", 2)
+
+ container.showSection("first", firstPage)
+ compare(container.depth, 2)
+ compare(firstCreated, 1)
+ compare(firstDestroyed, 0)
+ compare(pushedCreated, 1)
+ compare(pushedDestroyed, 0)
+ }
+
+ function test_settingsViewLazilyCachesSectionsAndIdlesExpensivePages() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ compare(view.visible, true)
+ verify(view.componentForSection("display") !== null)
+ compare(view.selectedSectionId, "display")
+ tryCompare(view.pageContainer, "depth", 1)
+ const displayPage = findChild(view, "displaySettingsPage")
+ verify(displayPage !== null)
+ verify(findChild(view, "networkTrafficSettingsPage") === null)
+ verify(findChild(view, "settingsDebugLog") === null)
+ compare(testNetworkTrafficTower.active, false)
+ compare(testDebugLogModel.active, false)
+
+ view.selectSection("network-traffic")
+ tryCompare(view, "selectedSectionId", "network-traffic")
+ compare(findChild(view, "displaySettingsPage"), displayPage)
+ const networkTrafficPage = findChild(view, "networkTrafficSettingsPage")
+ verify(networkTrafficPage !== null)
+ tryCompare(testNetworkTrafficTower, "active", true)
+ const networkTrafficHeading = findChild(view, "networkTrafficHeading")
+ const networkTrafficDescription = findChild(view, "networkTrafficHeadingDescription")
+ const networkTrafficSection = findChild(view, "networkTrafficSection")
+ const networkTrafficRangePicker = findChild(view, "networkTrafficRangePicker")
+ const networkTrafficReceivedGraph = findChild(view, "networkTrafficReceivedGraph")
+ const networkTrafficSentRow = findChild(view, "networkTrafficSentRow")
+ const networkTrafficSentGraph = findChild(view, "networkTrafficSentGraph")
+ verify(networkTrafficHeading !== null)
+ verify(networkTrafficDescription !== null)
+ verify(networkTrafficSection !== null)
+ verify(networkTrafficRangePicker !== null)
+ verify(networkTrafficReceivedGraph !== null)
+ verify(networkTrafficSentRow !== null)
+ verify(networkTrafficSentGraph !== null)
+ compare(networkTrafficHeading.descriptionTextStyle.font.pixelSize,
+ Theme.text.description.font.pixelSize)
+ compare(networkTrafficDescription.horizontalAlignment, Text.AlignHCenter)
+ compare(networkTrafficSection.backgroundColor, Theme.color.neutral1)
+ compare(networkTrafficSentRow.bottomPadding, 16)
+ compare(networkTrafficRangePicker.model.length, 4)
+ networkTrafficRangePicker.selected(1, networkTrafficRangePicker.model[1])
+ compare(testNetworkTrafficTower.lastFilterWindowSize, 360)
+
+ view.selectSection("debug-log")
+ compare(findChild(view, "networkTrafficSettingsPage"), networkTrafficPage)
+ tryCompare(testNetworkTrafficTower, "active", false)
+ const debugLogPage = findChild(view, "settingsDebugLog")
+ verify(debugLogPage !== null)
+ tryCompare(testDebugLogModel, "active", true)
+
+ view.selectSection("network-traffic")
+ compare(findChild(view, "networkTrafficSettingsPage"), networkTrafficPage)
+ compare(findChild(view, "settingsDebugLog"), debugLogPage)
+ compare(networkTrafficPage.trafficGraphScale, 3600)
+ tryCompare(testNetworkTrafficTower, "active", true)
+ tryCompare(testDebugLogModel, "active", false)
+
+ view.selectSection("debug-log")
+ compare(findChild(view, "settingsDebugLog"), debugLogPage)
+ tryCompare(testNetworkTrafficTower, "active", false)
+ tryCompare(testDebugLogModel, "active", true)
+
+ view.selectSection("about")
+ tryCompare(testDebugLogModel, "active", false)
+ verify(findChild(view, "aboutSettingsPage") !== null)
+ compare(findChild(view, "settingsDebugLog"), debugLogPage)
+
+ view.visible = false
+ compare(view.pageContainer.depth, 1)
+ tryCompare(testDebugLogModel, "active", false)
+ tryCompare(testNetworkTrafficTower, "active", false)
+ compare(findChild(view, "settingsDebugLog"), debugLogPage)
+ compare(findChild(view, "networkTrafficSettingsPage"), networkTrafficPage)
+
+ view.visible = true
+ compare(view.pageContainer.depth, 1)
+ verify(findChild(view, "aboutSettingsPage") !== null)
+ compare(findChild(view, "settingsDebugLog"), debugLogPage)
+ compare(findChild(view, "networkTrafficSettingsPage"), networkTrafficPage)
+ tryCompare(testDebugLogModel, "active", false)
+ tryCompare(testNetworkTrafficTower, "active", false)
+ }
+
+ function test_settingsViewPreservesAddressStackWhileHidden() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ view.openWalletAddressHistory()
+ tryCompare(view.pageContainer, "depth", 2)
+ const addressPage = view.pageContainer.currentItem
+ compare(addressPage.objectName, "addressListPage")
+
+ view.visible = false
+ compare(view.pageContainer.depth, 2)
+ compare(view.pageContainer.currentItem, addressPage)
+
+ view.visible = true
+ compare(view.pageContainer.depth, 2)
+ compare(view.pageContainer.currentItem, addressPage)
+ }
+
+ function test_walletStackResetsWhenSelectedWalletChangesWhileHidden() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ view.selectSection("wallet")
+ const setPasswordRow = findChild(view, "walletPasswordRow")
+ verify(setPasswordRow !== null)
+ compare(setPasswordRow.title, "Set password")
+ setPasswordRow.clicked()
+ tryCompare(view.pageContainer, "depth", 2)
+ const setPasswordPage = findChild(view, "walletPasswordSettingsPage")
+ verify(setPasswordPage !== null)
+ compare(setPasswordPage.updating, false)
+
+ view.selectSection("display")
+ compare(view.selectedSectionId, "display")
+ walletController.selectedWallet.isEncrypted = true
+ walletController.setSelectedWallet("encrypted-wallet")
+ compare(view.selectedSectionId, "display")
+ compare(view.pageContainer.sectionDepth("wallet"), 1)
+
+ view.selectSection("wallet")
+ tryCompare(view.pageContainer, "depth", 1)
+ const updatePasswordRow = findChild(view, "walletPasswordRow")
+ verify(updatePasswordRow !== null)
+ compare(updatePasswordRow.title, "Update password")
+ updatePasswordRow.clicked()
+ tryCompare(view.pageContainer, "depth", 2)
+ const updatePasswordPage = findChild(view, "walletPasswordSettingsPage")
+ verify(updatePasswordPage !== null)
+ compare(updatePasswordPage.updating, true)
+ }
+
+ function test_walletStackResetsWhenWalletUnloadsWhileHidden() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ view.selectSection("wallet")
+ const passwordRow = findChild(view, "walletPasswordRow")
+ verify(passwordRow !== null)
+ passwordRow.clicked()
+ tryCompare(view.pageContainer, "depth", 2)
+ verify(findChild(view, "walletPasswordSettingsPage") !== null)
+
+ view.selectSection("display")
+ compare(view.selectedSectionId, "display")
+ walletController.setWalletLoaded(false)
+ compare(view.selectedSectionId, "display")
+ compare(view.pageContainer.sectionDepth("wallet"), 1)
+
+ view.selectSection("wallet")
+ tryCompare(view.pageContainer, "depth", 1)
+ compare(view.pageContainer.currentItem.objectName, "walletSettingsPage")
+ }
+
+ function test_settingsViewPinsSidebarAndLetsPageContainerGrow() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ const sidebarSurface = findChild(view, "settingsSidebarSurface")
+ const sidebarHeading = findChild(view, "settingsSidebarHeading")
+ const displayPage = findChild(view, "displaySettingsPage")
+ verify(sidebarSurface !== null)
+ verify(sidebarHeading !== null)
+ verify(displayPage !== null)
+
+ tryCompare(sidebarSurface, "x", 0)
+ tryCompare(sidebarSurface, "width", view.sidebarWidth)
+ compare(sidebarSurface.color, Theme.color.neutral1)
+ compare(sidebarHeading.text, "Settings")
+ compare(sidebarHeading.font.pixelSize, Theme.text.display.font.pixelSize)
+ compare(sidebarHeading.horizontalAlignment, Text.AlignLeft)
+ tryCompare(view.pageContainer, "x", view.sidebarWidth)
+ tryCompare(view.pageContainer, "width", view.width - view.sidebarWidth)
+
+ verify(displayPage.contentHorizontalPadding >= 24)
+ verify(displayPage.contentLayout.width <= displayPage.maximumContentWidth)
+ verify(displayPage.contentLayout.width < view.pageContainer.width)
+ }
+
+ function test_dataHeavyPagesUseAvailableWidthWithResponsivePadding() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ view.width = 1500
+
+ view.selectSection("network-traffic")
+ const networkTrafficPage = findChild(view, "networkTrafficSettingsPage")
+ verify(networkTrafficPage !== null)
+ verify(networkTrafficPage.contentHorizontalPadding >= 24)
+ tryCompare(networkTrafficPage.contentLayout, "width",
+ networkTrafficPage.scrollView.availableWidth
+ - networkTrafficPage.contentHorizontalPadding * 2)
+ verify(networkTrafficPage.contentLayout.width > 840)
+
+ view.selectSection("debug-log")
+ const debugLogPage = findChild(view, "settingsDebugLog")
+ const debugLogContent = findChild(view, "debugLogContentLayout")
+ verify(debugLogPage !== null)
+ verify(debugLogContent !== null)
+ verify(debugLogPage.contentHorizontalPadding >= 24)
+ tryCompare(debugLogContent, "width",
+ debugLogPage.width - debugLogPage.contentHorizontalPadding * 2)
+ verify(debugLogContent.width > 840)
+ }
+
+ function test_settingsViewGroupsRelatedDestinations() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ compare(view.sectionForId("wallet").label, "Wallet settings")
+ compare(view.sectionForId("wallet").group, "wallet")
+ compare(view.sectionForId("storage").group, "general")
+ compare(view.sectionForId("network-traffic").group, "network")
+ compare(view.sectionForId("mempool").group, "advanced")
+ compare(view.sectionForId("rpc-console").group, "advanced")
+ compare(view.sectionForId("debug-log").group, "advanced")
+ compare(view.groupTitles.wallet, "Wallet")
+ compare(view.groupTitles.general, "General")
+ compare(view.groupTitles.network, "Network")
+ compare(view.groupTitles.advanced, "Advanced")
+ }
+
+ function test_rpcConsoleUsesSettingsContainerAndTracksVisibility() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ view.selectSection("rpc-console")
+
+ const page = findChild(view, "rpcConsoleSettingsPage")
+ const header = findChild(view, "rpcConsoleHeader")
+ const rpcConsole = findChild(view, "rpcConsole")
+ verify(page !== null)
+ verify(header !== null)
+ verify(rpcConsole !== null)
+ compare(header.title, "RPC console")
+ compare(header.showBackButton, false)
+ compare(page.maximumContentWidth, 840)
+ verify(page.contentHorizontalPadding >= 24)
+ compare(rpcConsole.showHeader, false)
+ compare(rpcConsole.tabActive, true)
+
+ view.selectSection("about")
+ tryCompare(rpcConsole, "tabActive", false)
+ }
+
+ function test_displayPageUsesInlineGenericPickers() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ const themePicker = findChild(view, "displayThemePicker")
+ const blockStatusSizePicker = findChild(view, "displayBlockStatusSizePicker")
+ const moneyFontPicker = findChild(view, "displayMoneyFontPicker")
+ const displayUnitPicker = findChild(view, "displayUnitPicker")
+ const languageDisclosure = findChild(view, "displayLanguageDisclosureIndicator")
+ const developerSection = findChild(view, "displayDeveloperSection")
+ const designSystemRow = findChild(view, "displayDesignSystemRow")
+
+ verify(themePicker !== null)
+ verify(blockStatusSizePicker !== null)
+ verify(moneyFontPicker !== null)
+ verify(displayUnitPicker !== null)
+ verify(languageDisclosure !== null)
+ verify(developerSection !== null)
+ verify(designSystemRow !== null)
+ compare(languageDisclosure.size, 14)
+ compare(developerSection.visible, BuildInfo.isDebug)
+ compare(displayUnitPicker.subtitleRole, "description")
+ compare(displayUnitPicker.minimumMenuWidth, 400)
+ tryVerify(function() { return displayUnitPicker.itemAtIndex(3) !== null })
+ compare(displayUnitPicker.itemAtIndex(0).subtitle,
+ "8 decimal places (0.00000001 BTC = 1 sat)")
+ compare(displayUnitPicker.itemAtIndex(1).subtitle,
+ "5 decimal places (0.00001 mBTC = 1 sat)")
+ compare(displayUnitPicker.itemAtIndex(2).subtitle,
+ "2 decimal places (0.01 bits = 1 sat)")
+ compare(displayUnitPicker.itemAtIndex(3).subtitle,
+ "Satoshi, the smallest unit (1 sat = 0.00000001 BTC)")
+
+ themePicker.selected(0, "Light")
+ compare(Theme.dark, false)
+
+ compare(blockStatusSizePicker.subtitleRole, "description")
+ compare(blockStatusSizePicker.iconRole, "icon")
+ compare(blockStatusSizePicker.iconSize, 40)
+ compare(blockStatusSizePicker.minimumMenuWidth, 520)
+ compare(blockStatusSizePicker.currentText, "Compact")
+ tryVerify(function() { return blockStatusSizePicker.itemAtIndex(1) !== null })
+ compare(blockStatusSizePicker.itemAtIndex(0).subtitle,
+ "For personal use on a computer or smartphone.")
+ compare(blockStatusSizePicker.itemAtIndex(1).subtitle,
+ "A larger block clock for public display on a tablet or other large screen.")
+ compare(blockStatusSizePicker.itemAtIndex(0).rowIconSource.toString(),
+ "image://images/blockclock-size-compact")
+ compare(blockStatusSizePicker.itemAtIndex(1).rowIconSource.toString(),
+ "image://images/blockclock-size-showcase")
+ compare(blockStatusSizePicker.itemAtIndex(0).implicitHeight, 52)
+ compare(blockStatusSizePicker.itemAtIndex(1).implicitHeight, 52)
+ blockStatusSizePicker.activated(1 / 2)
+ compare(Theme.blockclocksize, 1 / 2)
+ compare(blockStatusSizePicker.currentText, "Showcase")
+
+ compare(moneyFontPicker.subtitleRole, "description")
+ compare(moneyFontPicker.minimumMenuWidth, 400)
+ compare(moneyFontPicker.currentText, "Roboto Mono")
+ tryVerify(function() { return moneyFontPicker.itemAtIndex(1) !== null })
+ compare(moneyFontPicker.itemAtIndex(0).subtitle, "Included with Bitcoin Core")
+ compare(moneyFontPicker.itemAtIndex(1).subtitle,
+ "Uses your operating system’s default monospaced font")
+ const embeddedMoneyFontWidth = moneyFontPicker.width
+ moneyFontPicker.activated("best_system")
+ compare(optionsModel.moneyFontChoice, "best_system")
+ compare(moneyFontPicker.currentText, "System Monospace")
+ tryVerify(function() { return moneyFontPicker.width > embeddedMoneyFontWidth })
+ moneyFontPicker.activated("embedded")
+ compare(optionsModel.moneyFontChoice, "embedded")
+ compare(moneyFontPicker.currentText, "Roboto Mono")
+ tryCompare(moneyFontPicker, "width", embeddedMoneyFontWidth)
+
+ const displayUnits = [
+ { value: 1, text: "mBTC" },
+ { value: 2, text: "bits" },
+ { value: 3, text: "sat" },
+ { value: 0, text: "BTC" }
+ ]
+ for (let index = 0; index < displayUnits.length; ++index) {
+ const unit = displayUnits[index]
+ displayUnitPicker.activated(unit.value)
+ compare(optionsModel.displayUnit, unit.value)
+ compare(displayUnitPicker.currentValue, unit.value)
+ compare(displayUnitPicker.currentText, unit.text)
+ }
+
+ designSystemRow.clicked()
+ tryCompare(view.pageContainer, "depth", 2)
+ verify(findChild(view, "displayDesignSystemPage") !== null)
+ }
+
+ function test_mempoolPageUsesStandardFormRows() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ view.selectSection("mempool")
+
+ const transactionsRow = findChild(view, "mempoolTransactionsRow")
+ const memoryUsedRow = findChild(view, "mempoolMemoryUsedRow")
+ const sizeLimitRow = findChild(view, "mempoolSizeLimitRow")
+ const sizeLimitInput = findChild(view, "mempoolSizeLimitInput")
+
+ verify(transactionsRow !== null)
+ verify(memoryUsedRow !== null)
+ verify(sizeLimitRow !== null)
+ verify(sizeLimitInput !== null)
+ compare(transactionsRow.titleTextStyle.font.pixelSize, Theme.text.description.font.pixelSize)
+ compare(transactionsRow.valueTextStyle.font.pixelSize, Theme.text.description.font.pixelSize)
+ compare(memoryUsedRow.valueTextStyle.font.pixelSize, Theme.text.description.font.pixelSize)
+
+ sizeLimitRow.text = "512"
+ sizeLimitRow.editingFinished()
+ compare(optionsModel.maxMempoolSizeMB, 512)
+ compare(sizeLimitRow.errorText, "")
+
+ sizeLimitRow.text = "0"
+ sizeLimitRow.editingFinished()
+ compare(optionsModel.maxMempoolSizeMB, 512)
+ verify(sizeLimitRow.errorText.length > 0)
+ }
+
+ function test_mempoolPollingAndRestartNoticeFollowVisibility() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ compare(nodeModel.mempoolInfoPollingActive, false)
+
+ view.selectSection("mempool")
+ const mempoolPage = findChild(view, "mempoolSettingsPage")
+ const restartNotice = findChild(view, "mempoolRestartNotice")
+ verify(mempoolPage !== null)
+ verify(restartNotice !== null)
+ tryCompare(nodeModel, "mempoolInfoPollingActive", true)
+ compare(restartNotice.visible, false)
+
+ optionsModel.mempoolSettingsDirty = true
+ tryCompare(restartNotice, "visible", true)
+
+ view.selectSection("display")
+ tryCompare(nodeModel, "mempoolInfoPollingActive", false)
+ compare(findChild(view, "mempoolSettingsPage"), mempoolPage)
+
+ view.selectSection("mempool")
+ tryCompare(nodeModel, "mempoolInfoPollingActive", true)
+ view.visible = false
+ tryCompare(nodeModel, "mempoolInfoPollingActive", false)
+ view.visible = true
+ tryCompare(nodeModel, "mempoolInfoPollingActive", true)
+
+ view.destroy()
+ wait(0)
+ compare(nodeModel.mempoolInfoPollingActive, false)
+ }
+
+ function test_connectionProxyPageUsesRedesignedFormAndDraftCommit() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+ view.selectSection("connection")
+
+ const proxySettingsRow = findChild(view, "proxySettingsRow")
+ verify(proxySettingsRow !== null)
+ proxySettingsRow.clicked()
+ tryCompare(view.pageContainer, "depth", 2)
+
+ const proxyPage = findChild(view, "proxySettingsPage")
+ const defaultProxySection = findChild(view, "defaultProxySection")
+ const torProxySection = findChild(view, "torProxySection")
+ const proxySwitch = findChild(view, "proxyEnableSwitch")
+ const proxyAddressRow = findChild(view, "proxyAddressRow")
+ const saveButton = findChild(view, "proxySettingsSaveButton")
+
+ verify(proxyPage !== null)
+ verify(defaultProxySection !== null)
+ verify(torProxySection !== null)
+ verify(proxySwitch !== null)
+ verify(proxyAddressRow !== null)
+ verify(saveButton !== null)
+ compare(saveButton.text, "Save")
+ compare(saveButton.enabled, false)
+ compare(proxyAddressRow.titleTextStyle.font.pixelSize, Theme.text.description.font.pixelSize)
+ compare(proxyAddressRow.enabled, false)
+
+ proxySwitch.checked = true
+ proxySwitch.toggled()
+ compare(proxyPage.draftProxyEnabled, true)
+ compare(proxyAddressRow.enabled, true)
+
+ proxySwitch.checked = false
+ proxySwitch.toggled()
+ compare(proxyPage.proxyDraftDirty, false)
+ compare(saveButton.enabled, false)
+
+ proxySwitch.checked = true
+ proxySwitch.toggled()
+ compare(proxyPage.proxyDraftDirty, true)
+ compare(saveButton.enabled, true)
+
+ proxyAddressRow.text = ""
+ proxyAddressRow.textEdited(proxyAddressRow.text)
+ verify(proxyPage.draftProxyValidationError.length > 0)
+ compare(saveButton.enabled, false)
+
+ proxyAddressRow.text = "10.0.0.1:9050"
+ proxyAddressRow.textEdited(proxyAddressRow.text)
+ compare(proxyPage.proxyDraftDirty, true)
+ compare(proxyPage.draftProxyValidationError, "")
+ compare(saveButton.enabled, true)
+
+ proxyPage.back()
+ tryCompare(view.pageContainer, "depth", 2)
+ const discardPopup = findChild(settingsWindow.contentItem, "discardProxyChangesPopup")
+ verify(discardPopup !== null)
+ tryCompare(discardPopup, "opened", true)
+ const cancelButton = findChild(settingsWindow.contentItem, "discardProxyChangesCancelButton")
+ verify(cancelButton !== null)
+ cancelButton.clicked()
+ tryCompare(discardPopup, "opened", false)
+
+ saveButton.clicked()
+ tryCompare(view.pageContainer, "depth", 1)
+ const proxySetting = optionsModel.coreSettings.entry("proxy")
+ compare(proxySetting.enabled, true)
+ compare(proxySetting.address, "10.0.0.1:9050")
+ }
+
+ function test_settingsViewCanInstantiateEveryVisibleTopLevelDestination() {
+ const view = createTemporaryObject(settingsViewComponent, settingsWindow.contentItem)
+ verify(view !== null)
+
+ const destinations = [
+ { id: "wallet", objectName: "walletSettingsPage" },
+ { id: "external-signer", objectName: "externalSignerSettingsPage" },
+ { id: "display", objectName: "displaySettingsPage" },
+ { id: "window-behavior", objectName: "windowBehaviorSettingsPage" },
+ { id: "storage", objectName: "storageSettingsPage" },
+ { id: "connection", objectName: "connectionSettingsPage" },
+ { id: "network-traffic", objectName: "networkTrafficSettingsPage" },
+ { id: "mempool", objectName: "mempoolSettingsPage" },
+ { id: "rpc-console", objectName: "rpcConsoleSettingsPage" },
+ { id: "debug-log", objectName: "settingsDebugLog" },
+ { id: "about", objectName: "aboutSettingsPage" }
+ ]
+
+ for (let index = 0; index < destinations.length; ++index) {
+ const destination = destinations[index]
+ view.selectSection(destination.id, true)
+ compare(view.selectedSectionId, destination.id)
+ tryCompare(view.pageContainer, "depth", 1)
+ verify(findChild(view, destination.objectName) !== null,
+ "Expected instantiated destination " + destination.id)
+ if (destination.id === "wallet") {
+ const walletInfoSection = findChild(view, "walletInfoSection")
+ const walletActionsSection = findChild(view, "walletActionsSection")
+ verify(walletInfoSection !== null)
+ verify(walletActionsSection !== null)
+ compare(walletInfoSection.title, "Wallet info")
+ compare(walletActionsSection.title, "Wallet actions")
+ }
+ if (destination.id === "external-signer") {
+ const signerPage = findChild(view, "externalSignerSettingsPage")
+ const introduction = findChild(view, "externalSignerIntroduction")
+ const signerSection = findChild(view, "externalSignerPathSection")
+ const signerPathRow = findChild(view, "externalSignerPathRow")
+ const signerFooter = findChild(view, "externalSignerPathSectionFooter")
+ const signerPathInput = findChild(view, "externalSignerPathInput")
+ const signerPathFocusBorder = findChild(view, "externalSignerPathFocusBorder")
+ const signerStatusIndicator = findChild(view, "externalSignerStatusIndicator")
+ const signerStatusText = findChild(view, "externalSignerStatusText")
+ const checkDeviceButton = findChild(view, "externalSignerCheckDeviceButton")
+ verify(signerPage !== null)
+ verify(introduction !== null)
+ verify(signerSection !== null)
+ verify(signerPathRow !== null)
+ verify(signerFooter !== null)
+ verify(signerPathInput !== null)
+ verify(signerPathFocusBorder !== null)
+ verify(signerStatusIndicator !== null)
+ verify(signerStatusText !== null)
+ verify(checkDeviceButton !== null)
+ compare(introduction.title, "")
+ compare(introduction.description, "Connect a hardware wallet or another external signing tool.")
+ compare(signerPage.maximumContentWidth, 840)
+ compare(signerSection.title, "Signer path")
+ compare(signerPathRow.topPadding, 16)
+ compare(signerPathRow.bottomPadding, 16)
+ compare(signerFooter.text,
+ "The add wallet flow can offer external wallets when exactly one supported signer is connected.")
+ compare(signerStatusIndicator.color, Theme.color.red)
+ compare(checkDeviceButton.text, "Check device")
+ compare(signerPathInput.implicitHeight, 37)
+ compare(signerPathInput.leftPadding, 15)
+ compare(signerPathInput.rightPadding, 10)
+ compare(signerPathInput.background.color, Theme.color.neutral2)
+ compare(signerPathInput.background.radius, 5)
+ compare(signerPathFocusBorder.border.color, Theme.color.orange)
+ compare(signerStatusIndicator.width, 10)
+ compare(signerStatusIndicator.height, 10)
+ }
+ if (destination.id === "about") {
+ const versionRow = findChild(view, "aboutVersionRow")
+ const versionValue = findChild(view, "aboutVersionRowValue")
+ verify(versionRow !== null)
+ verify(versionValue !== null)
+ compare(versionRow.value, BuildInfo.fullClientVersion)
+ compare(versionValue.text, BuildInfo.fullClientVersion)
+ verify(versionValue.text.length > 0)
+ }
+ }
+ }
+}
diff --git a/test/qml/tst_settingswallet.qml b/test/qml/tst_settingswallet.qml
deleted file mode 100644
index d29a720d6b..0000000000
--- a/test/qml/tst_settingswallet.qml
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright (c) 2026 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.Window 2.15
-import QtTest 1.2
-import "../../qml/pages/settings"
-
-TestCase {
- name: "SettingsWallet"
- when: windowShown
- width: 520
- height: 720
-
- Window {
- id: testWindow
- width: 520
- height: 720
- visible: true
- }
-
- Component {
- id: settingsWalletComponent
-
- SettingsWallet {
- width: 460
- height: 680
- }
- }
-
- function init() {
- optionsModel.walletSettingsDirty = false
- }
-
- function createSettingsWalletPage() {
- const page = createTemporaryObject(settingsWalletComponent, testWindow.contentItem)
- verify(page !== null)
- return page
- }
-
- function test_restart_notice_hidden_when_wallet_settings_unchanged() {
- const page = createSettingsWalletPage()
- const notice = findChild(page, "walletRestartNotice")
- verify(notice !== null)
- compare(notice.visible, false)
- }
-
- function test_restart_notice_visible_when_wallet_settings_changed() {
- optionsModel.walletSettingsDirty = true
- const page = createSettingsWalletPage()
- const notice = findChild(page, "walletRestartNotice")
- verify(notice !== null)
- compare(notice.visible, true)
- }
-}
diff --git a/test/qml/tst_settingswindowbehavior.qml b/test/qml/tst_settingswindowbehavior.qml
deleted file mode 100644
index f346c9f351..0000000000
--- a/test/qml/tst_settingswindowbehavior.qml
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright (c) 2026 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 QtTest 1.2
-
-TestCase {
- name: "SettingsWindowBehavior"
-
- function init() {
- desktopWindowBehaviorModel.showTrayIcon = true
- desktopWindowBehaviorModel.minimizeToTray = false
- desktopWindowBehaviorModel.minimizeOnClose = false
- }
-
- function test_desktopPlatform_isTrue() {
- compare(desktopWindowBehaviorModel.desktopPlatform, true)
- }
-
- function test_showTrayIcon_defaultsTrue() {
- compare(desktopWindowBehaviorModel.showTrayIcon, true)
- }
-
- function test_minimizeToTray_defaultsFalse() {
- compare(desktopWindowBehaviorModel.minimizeToTray, false)
- }
-
- function test_minimizeOnClose_defaultsFalse() {
- compare(desktopWindowBehaviorModel.minimizeOnClose, false)
- }
-
- function test_showTrayIcon_toggleRoundTrip() {
- desktopWindowBehaviorModel.showTrayIcon = false
- compare(desktopWindowBehaviorModel.showTrayIcon, false)
- desktopWindowBehaviorModel.showTrayIcon = true
- compare(desktopWindowBehaviorModel.showTrayIcon, true)
- }
-
- function test_minimizeToTray_cascadesWhenTrayDisabled() {
- desktopWindowBehaviorModel.minimizeToTray = true
- compare(desktopWindowBehaviorModel.minimizeToTray, true)
- desktopWindowBehaviorModel.showTrayIcon = false
- compare(desktopWindowBehaviorModel.minimizeToTray, false)
- }
-
- function test_minimizeToTray_blockedWithoutTray() {
- desktopWindowBehaviorModel.showTrayIcon = false
- desktopWindowBehaviorModel.minimizeToTray = true
- compare(desktopWindowBehaviorModel.minimizeToTray, false)
- }
-
- function test_minimizeOnClose_independentOfTray() {
- desktopWindowBehaviorModel.showTrayIcon = false
- desktopWindowBehaviorModel.minimizeOnClose = true
- compare(desktopWindowBehaviorModel.minimizeOnClose, true)
- }
-
- function test_shouldHideToTrayOnMinimize_requiresAllConditions() {
- compare(desktopWindowBehaviorModel.shouldHideToTrayOnMinimize(), false)
- desktopWindowBehaviorModel.minimizeToTray = true
- compare(desktopWindowBehaviorModel.shouldHideToTrayOnMinimize(), true)
- desktopWindowBehaviorModel.showTrayIcon = false
- compare(desktopWindowBehaviorModel.shouldHideToTrayOnMinimize(), false)
- }
-
- function test_shouldMinimizeWindowOnClose() {
- compare(desktopWindowBehaviorModel.shouldMinimizeWindowOnClose(), false)
- desktopWindowBehaviorModel.minimizeOnClose = true
- compare(desktopWindowBehaviorModel.shouldMinimizeWindowOnClose(), true)
- }
-}
diff --git a/test/qml/tst_walletsettings.qml b/test/qml/tst_walletsettings.qml
deleted file mode 100644
index 3359f364c3..0000000000
--- a/test/qml/tst_walletsettings.qml
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (c) 2026 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 QtTest 1.2
-import "../../qml/pages/wallet"
-
-TestCase {
- name: "WalletSettings"
- when: windowShown
- width: 520
- height: 720
-
- Component {
- id: walletSettingsComponent
-
- WalletSettings {
- width: 460
- height: 680
- }
- }
-
- function init() {
- testWalletModel.resetWalletSettingsTestState()
- }
-
- function createWalletSettingsPage() {
- const page = createTemporaryObject(walletSettingsComponent, this)
- verify(page !== null)
- return page
- }
-
- function test_wallet_settings_password_and_backup_actions_remain_available() {
- const page = createWalletSettingsPage()
- let passwordRequests = 0
- let addressRequests = 0
- page.passwordRequested.connect(function() {
- ++passwordRequests
- })
- page.addressesRequested.connect(function() {
- ++addressRequests
- })
-
- const addressesRow = findChild(page, "settingsAddresses")
- verify(addressesRow !== null)
- addressesRow.clicked()
- compare(addressRequests, 1)
-
- const passwordRow = findChild(page, "walletSettingsPasswordRow")
- verify(passwordRow !== null)
- passwordRow.clicked()
- compare(passwordRequests, 1)
-
- const backupPathField = findChild(page, "walletSettingsBackupPathField")
- verify(backupPathField !== null)
- backupPathField.text = "/tmp/qml-wallet-settings-test.bak"
-
- const backupRow = findChild(page, "walletSettingsBackupRow")
- verify(backupRow !== null)
- backupRow.clicked()
- compare(testWalletModel.backupWalletCalls, 1)
- compare(testWalletModel.lastBackupPath, "/tmp/qml-wallet-settings-test.bak")
- }
-
- function test_wallet_settings_hides_password_action_for_external_signer_wallet() {
- testWalletModel.setExternalSignerWalletSettingsTestState()
- const page = createWalletSettingsPage()
-
- const passwordRow = findChild(page, "walletSettingsPasswordRow")
- verify(passwordRow !== null)
- compare(passwordRow.visible, false)
-
- const backupRow = findChild(page, "walletSettingsBackupRow")
- verify(backupRow !== null)
- }
-}