Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ find_package(Qt 6.2 MODULE REQUIRED
find_package(QRencode MODULE REQUIRED)

option(ENABLE_TEST_AUTOMATION "Enable test automation bridge for QML UI testing" OFF)
option(ENABLE_TABVIEW_SHELL "Enable the adaptive sidebar/tab-bar shell" OFF)

# Do not build any executable targets from bitcoin submodule
set(BUILD_BENCH OFF)
Expand Down Expand Up @@ -106,6 +107,9 @@ target_compile_definitions(bitcoinqml
if(ENABLE_TEST_AUTOMATION)
target_compile_definitions(bitcoinqml PUBLIC ENABLE_TEST_AUTOMATION)
endif()
if(ENABLE_TABVIEW_SHELL)
target_compile_definitions(bitcoinqml PUBLIC ENABLE_TABVIEW_SHELL)
endif()
target_include_directories(bitcoinqml
PRIVATE
# to keep the convention of #include <qml/*.h>
Expand Down
41 changes: 41 additions & 0 deletions qml/appmode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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.

#include <qml/appmode.h>

#include <qml/models/settings_keys.h>

#include <QSettings>

namespace {
constexpr bool AdaptiveSidebarLayoutAvailable()
{
#ifdef ENABLE_TABVIEW_SHELL
return true;
#else
return false;
#endif
}
} // namespace

AppMode::AppMode(Mode mode, bool wallet_enabled)
: m_mode(mode)
, m_wallet_enabled(wallet_enabled)
, m_adaptive_sidebar_layout_available(AdaptiveSidebarLayoutAvailable())
{
QSettings settings;
m_adaptive_sidebar_layout = m_adaptive_sidebar_layout_available
&& settings.value(SettingsKeys::ADAPTIVE_SIDEBAR_LAYOUT, false).toBool();
}

void AppMode::setAdaptiveSidebarLayout(bool enabled)
{
if (enabled && !m_adaptive_sidebar_layout_available) return;
if (m_adaptive_sidebar_layout == enabled) return;

m_adaptive_sidebar_layout = enabled;
QSettings settings;
settings.setValue(SettingsKeys::ADAPTIVE_SIDEBAR_LAYOUT, enabled);
Q_EMIT adaptiveSidebarLayoutChanged();
}
23 changes: 10 additions & 13 deletions qml/appmode.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class AppMode : public QObject
Q_PROPERTY(bool isDesktop READ isDesktop NOTIFY modeChanged)
Q_PROPERTY(bool isMobile READ isMobile NOTIFY modeChanged)
Q_PROPERTY(bool walletEnabled READ walletEnabled NOTIFY walletEnabledChanged)
Q_PROPERTY(bool adaptiveSidebarLayoutAvailable READ adaptiveSidebarLayoutAvailable CONSTANT)
Q_PROPERTY(bool adaptiveSidebarLayout READ adaptiveSidebarLayout WRITE setAdaptiveSidebarLayout NOTIFY adaptiveSidebarLayoutChanged)
Q_PROPERTY(QString state READ state NOTIFY modeChanged)

public:
Expand All @@ -21,15 +23,14 @@ class AppMode : public QObject
MOBILE
};

explicit AppMode(Mode mode, bool wallet_enabled)
: m_mode(mode)
, m_wallet_enabled(wallet_enabled)
{
}
explicit AppMode(Mode mode, bool wallet_enabled);

bool isMobile() { return m_mode == MOBILE; }
bool isDesktop() { return m_mode == DESKTOP; }
bool walletEnabled() { return m_wallet_enabled; }
bool adaptiveSidebarLayoutAvailable() const { return m_adaptive_sidebar_layout_available; }
bool adaptiveSidebarLayout() const { return m_adaptive_sidebar_layout; }
void setAdaptiveSidebarLayout(bool enabled);
void setWalletEnabled(bool wallet_enabled)
{
if (m_wallet_enabled == wallet_enabled) return;
Expand All @@ -38,24 +39,20 @@ class AppMode : public QObject
}
QString state()
{
switch (m_mode) {
case MOBILE:
return "MOBILE";
case DESKTOP:
return "DESKTOP";
default:
return "DESKTOP";
}
return m_mode == MOBILE ? "MOBILE" : "DESKTOP";
}
Mode mode() const { return m_mode; }

Q_SIGNALS:
void modeChanged();
void walletEnabledChanged();
void adaptiveSidebarLayoutChanged();

private:
const Mode m_mode;
bool m_wallet_enabled;
const bool m_adaptive_sidebar_layout_available;
bool m_adaptive_sidebar_layout{false};
};

#endif // BITCOIN_QML_APPMODE_H
21 changes: 21 additions & 0 deletions qml/bitcoin_qml.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,20 @@
<file>controls/SegmentedPicker.qml</file>
<file>controls/Setting.qml</file>
<file>controls/SettingsHeader.qml</file>
<file>controls/SizeClass.qml</file>
<file>controls/Skeleton.qml</file>
<file>controls/SpinningIndicator.qml</file>
<file>controls/Tab.qml</file>
<file>controls/TabSection.qml</file>
<file>controls/TabView.qml</file>
<file>controls/TextButton.qml</file>
<file>controls/Theme.qml</file>
<file>controls/ToggleButton.qml</file>
<file>controls/utils.js</file>
<file>controls/ValueInput.qml</file>
<file>controls/WalletTypeListItem.qml</file>
<file>pages/initerrormessage.qml</file>
<file>pages/MainShell.qml</file>
<file>pages/MainWindow.qml</file>
<file>pages/preinit.qml</file>
<file>pages/node/BannedPeers.qml</file>
Expand All @@ -123,6 +128,7 @@
<file>pages/settings/SettingsAbout.qml</file>
<file>pages/settings/SettingsDisplayUnit.qml</file>
<file>pages/settings/SettingsLanguage.qml</file>
<file>pages/settings/SettingsLayout.qml</file>
<file>pages/settings/SettingsWindowBehavior.qml</file>
<file>pages/settings/SettingsBlockClockDisplayMode.qml</file>
<file>pages/settings/SettingsConnection.qml</file>
Expand Down Expand Up @@ -162,6 +168,7 @@
<file>pages/wallet/SpeedUpOverlay.qml</file>
<file>pages/wallet/SignVerifyMessage.qml</file>
<file>pages/wallet/WalletBadge.qml</file>
<file>pages/wallet/WalletContainer.qml</file>
<file>pages/wallet/WalletPasswordSettings.qml</file>
<file>pages/wallet/WalletSettings.qml</file>
<file>pages/wallet/WalletSelect.qml</file>
Expand Down Expand Up @@ -228,6 +235,20 @@
<file alias="singlesig-wallet">res/icons/singlesig-wallet.png</file>
<file alias="storage-dark">res/icons/storage-dark.png</file>
<file alias="storage-light">res/icons/storage-light.png</file>
<file alias="tab-add">res/icons/plus.svg</file>
<file alias="tab-add-filled">res/icons/plus-filled.svg</file>
<file alias="tab-console">res/icons/console.svg</file>
<file alias="tab-console-filled">res/icons/console-filled.svg</file>
<file alias="tab-contacts">res/icons/contacts.svg</file>
<file alias="tab-contacts-filled">res/icons/contacts-filled.svg</file>
<file alias="tab-node">res/icons/node-hardware.svg</file>
<file alias="tab-node-filled">res/icons/node-hardware-filled.svg</file>
<file alias="tab-peers">res/icons/node-3-connections.svg</file>
<file alias="tab-peers-filled">res/icons/node-3-connections-filled.svg</file>
<file alias="tab-settings">res/icons/gear.svg</file>
<file alias="tab-settings-filled">res/icons/gear-filled.svg</file>
<file alias="tab-wallet">res/icons/wallet.svg</file>
<file alias="tab-wallet-filled">res/icons/wallet-filled.svg</file>
<file alias="tooltip-arrow-dark">res/icons/tooltip-arrow-dark.png</file>
<file alias="tooltip-arrow-light">res/icons/tooltip-arrow-light.png</file>
<file alias="triangle-down">res/icons/triangle-down.png</file>
Expand Down
2 changes: 1 addition & 1 deletion qml/controls/NavButton.qml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ AbstractButton {
states: [
State {
name: "DEFAULT"
PropertyChanges { target: bg; color: Theme.color.background }
PropertyChanges { target: bg; color: "transparent" }
},
State {
name: "HOVER"
Expand Down
17 changes: 17 additions & 0 deletions qml/controls/SizeClass.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 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.

pragma Singleton
import QtQuick 2.15

QtObject {
readonly property int compact: 0
readonly property int regular: 1

property real compactWidthMax: 600
property real compactHeightMax: 500

function widthClassFor(w) { return w <= compactWidthMax ? compact : regular }
function heightClassFor(h) { return h <= compactHeightMax ? compact : regular }
}
26 changes: 26 additions & 0 deletions qml/controls/Tab.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// 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

QtObject {
readonly property string kind: "tab"
property string title: ""
property string iconSource: ""
property string activeIconSource: ""
property Component contentComponent: null
property url contentUrl: ""
property bool enabled: true
property int displayModes: TabView.Sidebar | TabView.TabBar
property bool pinToBottom: false
property bool dimmed: false

// Action-only tabs do not switch the active content when clicked;
// instead they fire `triggered()` for the host to handle (e.g. open a popup).
property bool actionOnly: false
signal triggered()

// Fired when this tab becomes the current tab (initial load + every switch).
signal selected()
}
46 changes: 46 additions & 0 deletions qml/controls/TabSection.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 QtQml.Models 2.15

QtObject {
id: section

readonly property string kind: "section"
property string title: ""
property bool enabled: true
property int displayModes: TabView.Sidebar | TabView.TabBar
default property list<QtObject> tabs

// Optional model + delegate for dynamically-generated tabs.
// Static `tabs` (declared as children) and dynamic tabs are combined
// into `effectiveTabs` which TabView iterates.
property var model: null
property Component delegate: null

property var _dynamicTabs: []
// Dynamic (model-driven) tabs come first; declarative static `tabs` follow.
// This lets authors add trailing items like "+ Add Wallet" after the dynamic list.
readonly property var effectiveTabs: _dynamicTabs.concat(tabs)

property Instantiator _instantiator: Instantiator {
active: section.model !== null && section.delegate !== null
model: section.model
delegate: section.delegate
onObjectAdded: (idx, obj) => section._refreshDynamic()
onObjectRemoved: (idx, obj) => section._refreshDynamic()
}

function _refreshDynamic() {
var arr = []
for (var i = 0; i < _instantiator.count; ++i) {
var instance = _instantiator.objectAt(i)
if (instance)
arr.push(instance)

}
_dynamicTabs = arr
}
}
Loading
Loading