From 9292f1e015d47d9592e0c99ea4313d2451ea8064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nizamet=20O=CC=88zkan?= Date: Sun, 30 Aug 2026 15:49:21 +0300 Subject: [PATCH] qt: Improve GUI appearance on macOS 26 --- src/qt/bitcoin.cpp | 111 ++++++++++++++++++++++++++++++ src/qt/bitcoin.qrc | 4 ++ src/qt/bitcoingui.cpp | 14 ++++ src/qt/res/icons/spin-down.png | Bin 0 -> 240 bytes src/qt/res/icons/spin-down@2x.png | Bin 0 -> 343 bytes src/qt/res/icons/spin-up.png | Bin 0 -> 250 bytes src/qt/res/icons/spin-up@2x.png | Bin 0 -> 322 bytes src/qt/res/src/spin-down.svg | 3 + src/qt/res/src/spin-up.svg | 3 + src/qt/rpcconsole.cpp | 42 ++++++++++- src/qt/test/apptests.cpp | 52 ++++++++++++++ 11 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/qt/res/icons/spin-down.png create mode 100644 src/qt/res/icons/spin-down@2x.png create mode 100644 src/qt/res/icons/spin-up.png create mode 100644 src/qt/res/icons/spin-up@2x.png create mode 100644 src/qt/res/src/spin-down.svg create mode 100644 src/qt/res/src/spin-up.svg diff --git a/src/qt/bitcoin.cpp b/src/qt/bitcoin.cpp index 97eb957c93e..829885cc8a8 100644 --- a/src/qt/bitcoin.cpp +++ b/src/qt/bitcoin.cpp @@ -48,12 +48,23 @@ #include #include +#ifdef Q_OS_MACOS +#include +#endif #include #include +#include #include #include +#ifdef Q_OS_MACOS +#include +#endif #include #include +#include +#ifdef Q_OS_MACOS +#include +#endif #include #include #include @@ -72,6 +83,99 @@ Q_DECLARE_METATYPE(wallet::AddressPurpose) using util::MakeUnorderedList; +#ifdef Q_OS_MACOS +namespace { +constexpr auto MACOS_26_TEXT_FIELD_PROPERTY{"macos26RoundedTextField"}; +constexpr auto MACOS_26_TEXT_FIELD_STYLE{R"( +QLineEdit[macos26RoundedTextField="true"] { + background-color: palette(base); + border: 1px solid palette(mid); + border-radius: 7px; + padding: 2px 7px; +} + +QLineEdit[macos26RoundedTextField="true"]:focus { + border-color: palette(highlight); +} + +QAbstractSpinBox[macos26RoundedTextField="true"] { + background-color: palette(base); + border: 1px solid palette(mid); + border-radius: 7px; + padding: 2px 18px 2px 7px; +} + +QAbstractSpinBox[macos26RoundedTextField="true"]:focus { + border-color: palette(highlight); +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::up-button, +QAbstractSpinBox[macos26RoundedTextField="true"]::down-button { + subcontrol-origin: border; + width: 18px; + border: 0; + background-color: transparent; +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::up-button { + subcontrol-position: top right; + border-top-right-radius: 7px; +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::down-button { + subcontrol-position: bottom right; + border-bottom-right-radius: 7px; +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::up-button:hover, +QAbstractSpinBox[macos26RoundedTextField="true"]::down-button:hover { + background-color: palette(midlight); +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::up-button:pressed, +QAbstractSpinBox[macos26RoundedTextField="true"]::down-button:pressed { + background-color: palette(mid); +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::up-arrow { + image: url(:/icons/spin_up); + width: 8px; + height: 5px; +} + +QAbstractSpinBox[macos26RoundedTextField="true"]::down-arrow { + image: url(:/icons/spin_down); + width: 8px; + height: 5px; +} + +)"}; + +class MacOS26TextFieldStyleFilter final : public QObject +{ +public: + using QObject::QObject; + +protected: + bool eventFilter(QObject* object, QEvent* event) override + { + if (event->type() == QEvent::Polish) { + if (auto* line_edit = qobject_cast(object)) { + line_edit->setAttribute(Qt::WA_MacShowFocusRect, false); + if (!qobject_cast(line_edit->parentWidget())) { + line_edit->setProperty(MACOS_26_TEXT_FIELD_PROPERTY, true); + } + } else if (auto* spin_box = qobject_cast(object)) { + spin_box->setAttribute(Qt::WA_MacShowFocusRect, false); + spin_box->setProperty(MACOS_26_TEXT_FIELD_PROPERTY, true); + } + } + return QObject::eventFilter(object, event); + } +}; +} // namespace +#endif + static void RegisterMetaTypes() { // Register meta types used for QMetaObject::invokeMethod and Qt::QueuedConnection @@ -203,6 +307,13 @@ BitcoinApplication::BitcoinApplication() // Qt runs setlocale(LC_ALL, "") on initialization. RegisterMetaTypes(); setQuitOnLastWindowClosed(false); +#ifdef Q_OS_MACOS + const QOperatingSystemVersion macos_26{QOperatingSystemVersion::MacOS, 26}; + if (QOperatingSystemVersion::current() >= macos_26) { + installEventFilter(new MacOS26TextFieldStyleFilter(this)); + setStyleSheet(QString::fromLatin1(MACOS_26_TEXT_FIELD_STYLE) + styleSheet()); + } +#endif } void BitcoinApplication::setupPlatformStyle() diff --git a/src/qt/bitcoin.qrc b/src/qt/bitcoin.qrc index fed373e551c..d648af2811e 100644 --- a/src/qt/bitcoin.qrc +++ b/src/qt/bitcoin.qrc @@ -39,6 +39,10 @@ res/icons/fontbigger.png res/icons/fontsmaller.png res/icons/chevron.png + res/icons/spin-up.png + res/icons/spin-up@2x.png + res/icons/spin-down.png + res/icons/spin-down@2x.png res/icons/transaction_abandoned.png res/icons/hd_enabled.png res/icons/hd_disabled.png diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index f5ed2ed004d..bacdf555532 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -55,6 +55,9 @@ #include #include #include +#ifdef Q_OS_MACOS +#include +#endif #include #include #include @@ -148,6 +151,17 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const PlatformStyle *_platformSty // Create the toolbars createToolBars(); +#ifdef Q_OS_MACOS + const QOperatingSystemVersion macos_26{QOperatingSystemVersion::MacOS, 26}; + if (QOperatingSystemVersion::current() >= macos_26) { + setUnifiedTitleAndToolBarOnMac(true); + if (appToolBar) { + appToolBar->setFloatable(false); + appToolBar->setAllowedAreas(Qt::TopToolBarArea); + } + } +#endif + // Create system tray icon and notification if (QSystemTrayIcon::isSystemTrayAvailable()) { createTrayIcon(); diff --git a/src/qt/res/icons/spin-down.png b/src/qt/res/icons/spin-down.png new file mode 100644 index 0000000000000000000000000000000000000000..436c5d9977af173565c48ed333490ef4c90af666 GIT binary patch literal 240 zcmeAS@N?(olHy`uVBq!ia0vp^96-#N^5+IfWVg?501&j>LK$;OGwtxvP%dvnN&SnKE%oW-H7)V8Vx;Tb# z$R@veJ9o+fAPA3{W6tSum@&YH|HRKDEGj=FBqi0qee+w_da~(;g5d@k4*{hE7ry+r zcb4rqe}W@XT>QD2aTALJ!!nK|Y?>^dO*b?cQuMFn?XX}77F4{)KlyJV$Tm+`KbLh* G2~7Z0xIB>n literal 0 HcmV?d00001 diff --git a/src/qt/res/icons/spin-down@2x.png b/src/qt/res/icons/spin-down@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..377922403e1232c528eb4c561eb0ecbdc2dcac11 GIT binary patch literal 343 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~g!3HGv?z=Y!NHG=%xjQkeJ16rJ$Z<)H@J#dd zWzYh$IT#q*GZ|PwN`P1jh#44|7cep~18GK(*a9ZFtiS?hI2&XegBi$Ln2r;B5V zhG=h)H(!$j&r)N#GyV!yl8>3pHieoy^u!%>);N5KRs5qIlhyepo^wNzgpKm!?pI%E z)A$rBa$HmHLA}>2PVsE@a*6hbQL?9|!auELKt3Ylrv!y_zCC zQ??*tq2%B596>Mcto5Cg`n=q5`ku5fW(OsgcTc8A?sGW$sXdT&UJY|fTlO-+MQzJK Op7M0{b6Mw<&;$Vbn{ajj literal 0 HcmV?d00001 diff --git a/src/qt/res/icons/spin-up.png b/src/qt/res/icons/spin-up.png new file mode 100644 index 0000000000000000000000000000000000000000..b1ca99a1e6720e0709bfcb77352ba72b89c6f154 GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^96-#N^5+IfWVg?501&j>LK$;OGwtxvP%dvnN&SnKE%oW-H7)Ygfx;Tb# z$R;N^Fzwwt`ICc$XGf#tv6pY>#+*KJ;s)!Hd3kwp|I6Oqs^d^$4`Y~^lJV>R{DpiP z+vZQ4xR7&MhhU0Iz;%}iDTRf3#vbBMhyT~8Dg3|u|NsBbiIWsq70f(%I_#Mk76=)y Tt6ajY1G3lC)z4*}Q$iB}&uK(| literal 0 HcmV?d00001 diff --git a/src/qt/res/icons/spin-up@2x.png b/src/qt/res/icons/spin-up@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..54eeee50c4d497cd263dae9d0e2c1a1e3f2ac9ac GIT binary patch literal 322 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~g!3HGv?z=Y!NHG=%xjQkeJ16rJ$Z<)H@J#dd zWzYh$IT#q*GZ|PwN`P1jh#44|7cep~18GK(*a9ZFtiS?hI2&XegBi$H3Pr;B5V zhG=ra0p?x1f6F>Z^b4`6#Z0oboxEzs%*b!e1|7ZLvc_hd6WG+$j)l#TQC42OdG+e& z?_+mwlW*%hn8Cw*!St}mBO{ih3?*m(|NFcC-~aiE@*c(~6mr@Q@E%yUbBW4&-X}Q| zJ{L+dnK4XHoALRhbV2-q9$o!O@^56FIK9Ku8UHx3vIVCg!0GbJBbpQYW literal 0 HcmV?d00001 diff --git a/src/qt/res/src/spin-down.svg b/src/qt/res/src/spin-down.svg new file mode 100644 index 00000000000..a1b7b36644f --- /dev/null +++ b/src/qt/res/src/spin-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qt/res/src/spin-up.svg b/src/qt/res/src/spin-up.svg new file mode 100644 index 00000000000..e4424464396 --- /dev/null +++ b/src/qt/res/src/spin-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/qt/rpcconsole.cpp b/src/qt/rpcconsole.cpp index ef53dfd103c..c6de11f1371 100644 --- a/src/qt/rpcconsole.cpp +++ b/src/qt/rpcconsole.cpp @@ -39,6 +39,10 @@ #include #include #include +#ifdef Q_OS_MACOS +#include +#include +#endif #include #include #include @@ -71,6 +75,35 @@ const struct { namespace { +#ifdef Q_OS_MACOS +constexpr auto MACOS_26_TAB_STYLE{R"( +QTabBar { + background-color: palette(button); + border: 1px solid palette(mid); + border-radius: 7px; +} + +QTabBar::tab { + background-color: transparent; + color: palette(button-text); + border: 0; + margin: 0; + padding: 3px 12px; +} + +QTabBar::tab:hover { + background-color: palette(midlight); + border-radius: 6px; +} + +QTabBar::tab:selected { + background-color: palette(highlight); + color: palette(highlighted-text); + border-radius: 6px; +} +)"}; +#endif + // don't add private key handling cmd's to the history const QStringList historyFilter = QStringList() << "createwallet" @@ -82,7 +115,7 @@ const QStringList historyFilter = QStringList() << "walletpassphrasechange" << "encryptwallet"; -} +} // namespace /* Object for executing console RPC commands in a separate thread. */ @@ -447,6 +480,13 @@ RPCConsole::RPCConsole(interfaces::Node& node, const PlatformStyle *_platformSty platformStyle(_platformStyle) { ui->setupUi(this); +#ifdef Q_OS_MACOS + const QOperatingSystemVersion macos_26{QOperatingSystemVersion::MacOS, 26}; + if (QOperatingSystemVersion::current() >= macos_26) { + QTabBar* const tab_bar = ui->tabWidget->tabBar(); + tab_bar->setStyleSheet(QString::fromLatin1(MACOS_26_TAB_STYLE)); + } +#endif QSettings settings; #ifdef ENABLE_WALLET if (WalletModel::isWalletEnabled()) { diff --git a/src/qt/test/apptests.cpp b/src/qt/test/apptests.cpp index 7802696d5f3..7244a329e30 100644 --- a/src/qt/test/apptests.cpp +++ b/src/qt/test/apptests.cpp @@ -2,6 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include // IWYU pragma: keep + #include #include @@ -15,7 +17,15 @@ #include #include +#include #include +#ifdef Q_OS_MACOS +#include +#include +#ifdef ENABLE_WALLET +#include +#endif +#endif #include #include #include @@ -34,11 +44,40 @@ QString FindInConsole(const QString& output, const QString& pattern) return re.match(output).captured(1); } +void CheckPngResource(const QString& path, const QSize& pixel_size, qreal device_pixel_ratio) +{ + QImageReader reader{path, QByteArrayLiteral("png")}; + reader.setAutoDetectImageFormat(false); + QVERIFY(reader.canRead()); + const QImage image{reader.read()}; + QVERIFY(!image.isNull()); + QCOMPARE(image.size(), pixel_size); + QCOMPARE(image.devicePixelRatio(), device_pixel_ratio); + QVERIFY(image.hasAlphaChannel()); +} + //! Call getblockchaininfo RPC and check first field of JSON output. void TestRpcCommand(RPCConsole* console) { QTextEdit* messagesWidget = console->findChild("messagesWidget"); QLineEdit* lineEdit = console->findChild("lineEdit"); + + CheckPngResource(QStringLiteral(":/icons/spin_up"), QSize(8, 5), 1.0); + CheckPngResource(QStringLiteral(":/icons/spin_up@2x"), QSize(16, 10), 2.0); + CheckPngResource(QStringLiteral(":/icons/spin_down"), QSize(8, 5), 1.0); + CheckPngResource(QStringLiteral(":/icons/spin_down@2x"), QSize(16, 10), 2.0); +#ifdef Q_OS_MACOS + const QOperatingSystemVersion macos_26{QOperatingSystemVersion::MacOS, 26}; + if (QOperatingSystemVersion::current() >= macos_26) { + QVERIFY(!lineEdit->testAttribute(Qt::WA_MacShowFocusRect)); + QVERIFY(lineEdit->property("macos26RoundedTextField").toBool()); + + QSpinBox spin_box; + spin_box.ensurePolished(); + QVERIFY(!spin_box.testAttribute(Qt::WA_MacShowFocusRect)); + QVERIFY(spin_box.property("macos26RoundedTextField").toBool()); + } +#endif QSignalSpy mw_spy(messagesWidget, &QTextEdit::textChanged); QVERIFY(mw_spy.isValid()); QTest::keyClicks(lineEdit, "getblockchaininfo"); @@ -76,6 +115,19 @@ void AppTests::appTests() void AppTests::guiTests(BitcoinGUI* window) { HandleCallback callback{"guiTests", *this}; +#ifdef Q_OS_MACOS + const QOperatingSystemVersion macos_26{QOperatingSystemVersion::MacOS, 26}; + const bool expects_unified_toolbar{QOperatingSystemVersion::current() >= macos_26}; + QCOMPARE(window->unifiedTitleAndToolBarOnMac(), expects_unified_toolbar); +#ifdef ENABLE_WALLET + if (expects_unified_toolbar) { + QToolBar* const toolbar{window->findChild()}; + QVERIFY(toolbar); + QVERIFY(!toolbar->isFloatable()); + QCOMPARE(toolbar->allowedAreas(), Qt::TopToolBarArea); + } +#endif +#endif connect(window, &BitcoinGUI::consoleShown, this, &AppTests::consoleTests); expectCallback("consoleTests"); QAction* action = window->findChild("openRPCConsoleAction");