diff --git a/contrib/devtools/gen_macos_icons.py b/contrib/devtools/gen_macos_icons.py new file mode 100755 index 000000000000..dd0758874c3b --- /dev/null +++ b/contrib/devtools/gen_macos_icons.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# Copyright (c) 2026 The Dash Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import os +import platform +import shutil +import subprocess +import sys +import tempfile + +# Assuming 1024x1024 canvas, the squircle content area is ~864x864 with +# ~80px transparent padding on each side +CONTENT_RATIO = 864 / 1024 + +DIR_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) +DIR_SRC = os.path.join(DIR_ROOT, "src", "qt", "res", "src") +DIR_OUT = os.path.join(DIR_ROOT, "src", "qt", "res", "icons") + +# Icon Composer exports to runtime icon map +ICONS = [ + ("macos_devnet.png", "dash_macos_devnet.png"), + ("macos_mainnet.png", "dash_macos_mainnet.png"), + ("macos_regtest.png", "dash_macos_regtest.png"), + ("macos_testnet.png", "dash_macos_testnet.png"), +] +TRAY = os.path.join(DIR_SRC, "tray.svg") + +# Canvas to filename mapping for bundle icon +ICNS_MAP = [ + (16, "icon_16x16.png"), + (32, "icon_16x16@2x.png"), + (32, "icon_32x32.png"), + (64, "icon_32x32@2x.png"), + (128, "icon_128x128.png"), + (256, "icon_128x128@2x.png"), + (256, "icon_256x256.png"), + (512, "icon_256x256@2x.png"), + (512, "icon_512x512.png"), + (1024, "icon_512x512@2x.png"), +] + +# Maximum height of canvas is 22pt, we use max height instead of recommended +# 16pt canvas to prevent the icon from looking undersized due to icon width. +# See https://bjango.com/articles/designingmenubarextras/ +TRAY_MAP = [ + (22, "dash_macos_tray.png"), + (44, "dash_macos_tray@2x.png") +] + + +def sips_resample_padded(src, dst, canvas_size): + content_size = max(round(canvas_size * CONTENT_RATIO), 1) + subprocess.check_call( + ["sips", "-z", str(content_size), str(content_size), "-p", str(canvas_size), str(canvas_size), src, "--out", dst], + stdout=subprocess.DEVNULL, + ) + + +def sips_svg_to_png(svg_path, png_path, height): + subprocess.check_call( + ["sips", "-s", "format", "png", "--resampleHeight", str(height), svg_path, "--out", png_path], + stdout=subprocess.DEVNULL, + ) + + +def generate_icns(tmpdir): + iconset = os.path.join(tmpdir, "dash.iconset") + os.makedirs(iconset) + + src_main = os.path.join(DIR_SRC, ICONS[1][0]) + for canvas_px, filename in ICNS_MAP: + sips_resample_padded(src_main, os.path.join(iconset, filename), canvas_px) + + icns_out = os.path.join(DIR_OUT, "dash.icns") + subprocess.check_call(["iconutil", "-c", "icns", iconset, "-o", icns_out]) + print(f"Created: {icns_out}") + + +def check_source(path): + if not os.path.isfile(path): + sys.exit(f"Error: Source image not found: {path}") + + +def main(): + if platform.system() != "Darwin": + sys.exit("Error: This script requires macOS (needs sips, iconutil).") + + for tool in ("sips", "iconutil"): + if shutil.which(tool) is None: + sys.exit(f"Error: '{tool}' not found. Install Xcode command-line tools.") + + check_source(TRAY) + for src_name, _ in ICONS: + check_source(os.path.join(DIR_SRC, src_name)) + + os.makedirs(DIR_OUT, exist_ok=True) + + # Generate bundle icon + with tempfile.TemporaryDirectory(prefix="dash_icons_") as tmpdir: + generate_icns(tmpdir) + + # Generate runtime icons + for src_name, dst_name in ICONS: + src = os.path.join(DIR_SRC, src_name) + dst = os.path.join(DIR_OUT, dst_name) + sips_resample_padded(src, dst, 256) + print(f"Created: {dst}") + + # Generate tray icons + for canvas_px, filename in TRAY_MAP: + dst = os.path.join(DIR_OUT, filename) + sips_svg_to_png(TRAY, dst, canvas_px) + print(f"Created: {dst}") + +if __name__ == "__main__": + main() diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 970c478a41fb..04652688dbef 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -216,6 +216,12 @@ QT_RES_ICONS = \ qt/res/icons/connect4_16.png \ qt/res/icons/dash.ico \ qt/res/icons/dash.png \ + qt/res/icons/dash_macos_devnet.png \ + qt/res/icons/dash_macos_mainnet.png \ + qt/res/icons/dash_macos_regtest.png \ + qt/res/icons/dash_macos_testnet.png \ + qt/res/icons/dash_macos_tray.png \ + qt/res/icons/dash_macos_tray@2x.png \ qt/res/icons/dash_testnet.ico \ qt/res/icons/editcopy.png \ qt/res/icons/editpaste.png \ diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index a8fa60cce807..d0d6ed9f64ec 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -116,8 +116,16 @@ BitcoinGUI::BitcoinGUI(interfaces::Node& node, const NetworkStyle* networkStyle, #ifdef ENABLE_WALLET enableWallet = WalletModel::isWalletEnabled(); #endif // ENABLE_WALLET - QApplication::setWindowIcon(m_network_style->getTrayAndWindowIcon()); - setWindowIcon(m_network_style->getTrayAndWindowIcon()); + + QIcon icon{m_network_style->getTrayAndWindowIcon()}; +#ifdef Q_OS_MACOS + if (auto macos_icon{m_network_style->getMacIcon()}) { + icon = macos_icon.value(); + } +#endif // Q_OS_MACOS + QApplication::setWindowIcon(icon); + setWindowIcon(icon); + updateWindowTitle(); rpcConsole = new RPCConsole(node, this, enableWallet ? Qt::Window : Qt::Widget); @@ -1113,7 +1121,13 @@ void BitcoinGUI::createTrayIcon() assert(QSystemTrayIcon::isSystemTrayAvailable()); if (QSystemTrayIcon::isSystemTrayAvailable()) { - trayIcon = new QSystemTrayIcon(m_network_style->getTrayAndWindowIcon(), this); + QIcon icon{m_network_style->getTrayAndWindowIcon()}; +#ifdef Q_OS_MACOS + if (auto macos_tray{m_network_style->getMacTray()}) { + icon = macos_tray.value(); + } +#endif // Q_OS_MACOS + trayIcon = new QSystemTrayIcon(icon, this); QString toolTip = tr("%1 client").arg(PACKAGE_NAME) + " " + m_network_style->getTitleAddText(); trayIcon->setToolTip(toolTip); } diff --git a/src/qt/dash.qrc b/src/qt/dash.qrc index 20621ba996b9..137f02d78a0f 100644 --- a/src/qt/dash.qrc +++ b/src/qt/dash.qrc @@ -1,6 +1,12 @@ res/icons/dash.png + res/icons/dash_macos_devnet.png + res/icons/dash_macos_mainnet.png + res/icons/dash_macos_regtest.png + res/icons/dash_macos_testnet.png + res/icons/dash_macos_tray.png + res/icons/dash_macos_tray@2x.png res/icons/warning.png res/icons/address-book.png res/icons/connect1_16.png diff --git a/src/qt/networkstyle.cpp b/src/qt/networkstyle.cpp index 703efdf7c4bc..e76808614f0e 100644 --- a/src/qt/networkstyle.cpp +++ b/src/qt/networkstyle.cpp @@ -21,12 +21,13 @@ static const struct { const char *appName; const int iconColorHueShift; const int iconColorSaturationReduction; + const char *macIconPath; const std::string titleAddText; } network_styles[] = { - {"main", QAPP_APP_NAME_DEFAULT, 0, 0, ""}, - {"test", QAPP_APP_NAME_TESTNET, 190, 20, ""}, - {"devnet", QAPP_APP_NAME_DEVNET, 35, 15, "[devnet: %s]"}, - {"regtest", QAPP_APP_NAME_REGTEST, 160, 30, ""} + {"main", QAPP_APP_NAME_DEFAULT, 0, 0, ":/icons/dash_macos_mainnet", ""}, + {"test", QAPP_APP_NAME_TESTNET, 190, 20, ":/icons/dash_macos_testnet", ""}, + {"devnet", QAPP_APP_NAME_DEVNET, 35, 15, ":/icons/dash_macos_devnet", "[devnet: %s]"}, + {"regtest", QAPP_APP_NAME_REGTEST, 160, 30, ":/icons/dash_macos_regtest", ""}, }; void NetworkStyle::rotateColor(QColor& col, const int iconColorHueShift, const int iconColorSaturationReduction) @@ -62,7 +63,8 @@ void NetworkStyle::rotateColors(QImage& img, const int iconColorHueShift, const } // titleAddText needs to be const char* for tr() -NetworkStyle::NetworkStyle(const QString &_appName, const int iconColorHueShift, const int iconColorSaturationReduction, const char *_titleAddText): +NetworkStyle::NetworkStyle(const QString &_appName, const int iconColorHueShift, const int iconColorSaturationReduction, + const char *_macIconPath, const char *_titleAddText): appName(_appName), titleAddText(qApp->translate("SplashScreen", _titleAddText)), badgeColor(QColor(0, 141, 228)) // default badge color is the original Dash's blue, regardless of the current theme @@ -86,6 +88,14 @@ NetworkStyle::NetworkStyle(const QString &_appName, const int iconColorHueShift, appIcon = QIcon(appIconPixmap); trayAndWindowIcon = QIcon(appIconPixmap.scaled(QSize(256,256))); splashImage = QPixmap(":/images/splash"); + +#ifdef Q_OS_MACOS + if (_macIconPath) { + m_macos_icon = QIcon(QPixmap(_macIconPath)); + } + m_macos_tray = QIcon(QPixmap(":/icons/dash_macos_tray")); + m_macos_tray->setIsMask(true); +#endif // Q_OS_MACOS } const NetworkStyle* NetworkStyle::instantiate(const std::string& networkId) @@ -107,6 +117,7 @@ const NetworkStyle* NetworkStyle::instantiate(const std::string& networkId) appName.c_str(), network_style.iconColorHueShift, network_style.iconColorSaturationReduction, + network_style.macIconPath, titleAddText.c_str()); } } diff --git a/src/qt/networkstyle.h b/src/qt/networkstyle.h index 05d1b9bb688a..f9d872e37f46 100644 --- a/src/qt/networkstyle.h +++ b/src/qt/networkstyle.h @@ -10,6 +10,8 @@ #include #include +#include + /* Coin network-specific GUI style information */ class NetworkStyle { @@ -23,9 +25,14 @@ class NetworkStyle const QIcon &getTrayAndWindowIcon() const { return trayAndWindowIcon; } const QString &getTitleAddText() const { return titleAddText; } const QColor &getBadgeColor() const { return badgeColor; } +#ifdef Q_OS_MACOS + std::optional getMacIcon() const { return m_macos_icon; } + std::optional getMacTray() const { return m_macos_tray; } +#endif // Q_OS_MACOS private: - NetworkStyle(const QString &appName, const int iconColorHueShift, const int iconColorSaturationReduction, const char *titleAddText); + NetworkStyle(const QString &appName, const int iconColorHueShift, const int iconColorSaturationReduction, + const char *macIconPath, const char *titleAddText); QString appName; QIcon appIcon; @@ -33,6 +40,10 @@ class NetworkStyle QIcon trayAndWindowIcon; QString titleAddText; QColor badgeColor; +#ifdef Q_OS_MACOS + std::optional m_macos_icon; + std::optional m_macos_tray; +#endif // Q_OS_MACOS void rotateColor(QColor& col, const int iconColorHueShift, const int iconColorSaturationReduction); void rotateColors(QImage& img, const int iconColorHueShift, const int iconColorSaturationReduction); diff --git a/src/qt/res/icons/dash.icns b/src/qt/res/icons/dash.icns index 0408f496d504..d20cd901245e 100644 Binary files a/src/qt/res/icons/dash.icns and b/src/qt/res/icons/dash.icns differ diff --git a/src/qt/res/icons/dash_macos_devnet.png b/src/qt/res/icons/dash_macos_devnet.png new file mode 100644 index 000000000000..54bf0ff2c6af Binary files /dev/null and b/src/qt/res/icons/dash_macos_devnet.png differ diff --git a/src/qt/res/icons/dash_macos_mainnet.png b/src/qt/res/icons/dash_macos_mainnet.png new file mode 100644 index 000000000000..736a8cbd2e31 Binary files /dev/null and b/src/qt/res/icons/dash_macos_mainnet.png differ diff --git a/src/qt/res/icons/dash_macos_regtest.png b/src/qt/res/icons/dash_macos_regtest.png new file mode 100644 index 000000000000..d014645ded50 Binary files /dev/null and b/src/qt/res/icons/dash_macos_regtest.png differ diff --git a/src/qt/res/icons/dash_macos_testnet.png b/src/qt/res/icons/dash_macos_testnet.png new file mode 100644 index 000000000000..d2fb13d0f8fe Binary files /dev/null and b/src/qt/res/icons/dash_macos_testnet.png differ diff --git a/src/qt/res/icons/dash_macos_tray.png b/src/qt/res/icons/dash_macos_tray.png new file mode 100644 index 000000000000..0fab35e5e3a9 Binary files /dev/null and b/src/qt/res/icons/dash_macos_tray.png differ diff --git a/src/qt/res/icons/dash_macos_tray@2x.png b/src/qt/res/icons/dash_macos_tray@2x.png new file mode 100644 index 000000000000..a022ffffdd47 Binary files /dev/null and b/src/qt/res/icons/dash_macos_tray@2x.png differ diff --git a/src/qt/res/src/macos_devnet.png b/src/qt/res/src/macos_devnet.png new file mode 100644 index 000000000000..01bd1e834710 Binary files /dev/null and b/src/qt/res/src/macos_devnet.png differ diff --git a/src/qt/res/src/macos_mainnet.png b/src/qt/res/src/macos_mainnet.png new file mode 100644 index 000000000000..4b2bb976b4f3 Binary files /dev/null and b/src/qt/res/src/macos_mainnet.png differ diff --git a/src/qt/res/src/macos_regtest.png b/src/qt/res/src/macos_regtest.png new file mode 100644 index 000000000000..0e39319b1ee0 Binary files /dev/null and b/src/qt/res/src/macos_regtest.png differ diff --git a/src/qt/res/src/macos_testnet.png b/src/qt/res/src/macos_testnet.png new file mode 100644 index 000000000000..02069c7a9c84 Binary files /dev/null and b/src/qt/res/src/macos_testnet.png differ diff --git a/src/qt/res/src/tray.svg b/src/qt/res/src/tray.svg new file mode 100644 index 000000000000..d262ccc6dd9f --- /dev/null +++ b/src/qt/res/src/tray.svg @@ -0,0 +1,5 @@ + + + + +