Skip to content
Merged
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
118 changes: 118 additions & 0 deletions contrib/devtools/gen_macos_icons.py
Original file line number Diff line number Diff line change
@@ -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()
6 changes: 6 additions & 0 deletions src/Makefile.qt.include
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
20 changes: 17 additions & 3 deletions src/qt/bitcoingui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 6 additions & 0 deletions src/qt/dash.qrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/icons">
<file alias="dash">res/icons/dash.png</file>
<file alias="dash_macos_devnet">res/icons/dash_macos_devnet.png</file>
<file alias="dash_macos_mainnet">res/icons/dash_macos_mainnet.png</file>
<file alias="dash_macos_regtest">res/icons/dash_macos_regtest.png</file>
<file alias="dash_macos_testnet">res/icons/dash_macos_testnet.png</file>
<file alias="dash_macos_tray">res/icons/dash_macos_tray.png</file>
<file alias="dash_macos_tray@2x">res/icons/dash_macos_tray@2x.png</file>
<file alias="warning">res/icons/warning.png</file>
<file alias="address-book">res/icons/address-book.png</file>
<file alias="connect_1">res/icons/connect1_16.png</file>
Expand Down
21 changes: 16 additions & 5 deletions src/qt/networkstyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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);
Comment on lines +93 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the networkstyle.cpp file around the specified lines
head -120 src/qt/networkstyle.cpp | tail -40

Repository: dashpay/dash

Length of output: 1630


🏁 Script executed:

# Get the complete context around lines 93-97
sed -n '80,110p' src/qt/networkstyle.cpp

Repository: dashpay/dash

Length of output: 1217


🏁 Script executed:

# Check the getter implementations for the macOS icons
rg -n 'getMacIcon|getMacTray|m_macos_icon|m_macos_tray' src/qt/networkstyle.cpp -A 3

Repository: dashpay/dash

Length of output: 257


🏁 Script executed:

# Run the verification scripts from the review comment to see how icons are consumed
rg -nP -C3 '\bgetMacIcon\s*\(|\bgetMacTray\s*\(' --type=cpp --type=h src/qt

Repository: dashpay/dash

Length of output: 1409


🏁 Script executed:

# Check if call sites verify isNull before using the icons
rg -nP -C3 '\bisNull\s*\(\)' --type=cpp --type=h src/qt

Repository: dashpay/dash

Length of output: 4222


🏁 Script executed:

# Check the header file to understand the optional types and getter signatures
rg -n 'getMacIcon|getMacTray|m_macos_icon|m_macos_tray' src/qt/networkstyle.h -B 2 -A 2

Repository: dashpay/dash

Length of output: 476


Guard against null icon loads before populating optionals.

At Line 93-97, icon load success is not validated. If resource loading fails, QIcon instances are created with null pixmaps but still stored in the optional, allowing has_value() checks at call sites (bitcoingui.cpp:122, 1126) to incorrectly treat failed loads as success. Additionally, Line 97 calls ->setIsMask(true) on the unconditionally-stored optional, relying on undefined behavior if loading fails.

Validate icon state with isNull() before populating the optionals, and defer non-default operations (e.g., setIsMask) until after successful load.

Proposed hardening patch
 `#ifdef` Q_OS_MACOS
-    if (_macIconPath) {
-        m_macos_icon = QIcon(QPixmap(_macIconPath));
+    if (_macIconPath) {
+        const QIcon mac_icon(QString::fromUtf8(_macIconPath));
+        if (!mac_icon.isNull()) {
+            m_macos_icon = mac_icon;
+        }
     }
-    m_macos_tray = QIcon(QPixmap(":/icons/dash_macos_tray"));
-    m_macos_tray->setIsMask(true);
+    const QIcon mac_tray_icon(QStringLiteral(":/icons/dash_macos_tray"));
+    if (!mac_tray_icon.isNull()) {
+        mac_tray_icon.setIsMask(true);
+        m_macos_tray = mac_tray_icon;
+    }
 `#endif` // Q_OS_MACOS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (_macIconPath) {
m_macos_icon = QIcon(QPixmap(_macIconPath));
}
m_macos_tray = QIcon(QPixmap(":/icons/dash_macos_tray"));
m_macos_tray->setIsMask(true);
`#ifdef` Q_OS_MACOS
if (_macIconPath) {
const QIcon mac_icon(QString::fromUtf8(_macIconPath));
if (!mac_icon.isNull()) {
m_macos_icon = mac_icon;
}
}
QIcon mac_tray_icon(QStringLiteral(":/icons/dash_macos_tray"));
if (!mac_tray_icon.isNull()) {
mac_tray_icon.setIsMask(true);
m_macos_tray = mac_tray_icon;
}
`#endif` // Q_OS_MACOS
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/qt/networkstyle.cpp` around lines 93 - 97, The code unconditionally
constructs and stores QIcon optionals (m_macos_icon, m_macos_tray) from QPixmap
resources and then calls setIsMask on the tray icon, but it doesn't check for
failed loads; update the logic to load QPixmap from _macIconPath and from
":/icons/dash_macos_tray", call isNull() on each QPixmap, and only emplace the
corresponding optional (m_macos_icon, m_macos_tray) when the pixmap is valid;
after emplacing m_macos_tray, then call setIsMask(true) on the stored QIcon (not
on a possibly-empty optional) so setIsMask is only invoked after a successful
load.

#endif // Q_OS_MACOS
}

const NetworkStyle* NetworkStyle::instantiate(const std::string& networkId)
Expand All @@ -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());
}
}
Expand Down
13 changes: 12 additions & 1 deletion src/qt/networkstyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <QPixmap>
#include <QString>

#include <optional>

/* Coin network-specific GUI style information */
class NetworkStyle
{
Expand All @@ -23,16 +25,25 @@ 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<QIcon> getMacIcon() const { return m_macos_icon; }
std::optional<QIcon> 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;
QPixmap splashImage;
QIcon trayAndWindowIcon;
QString titleAddText;
QColor badgeColor;
#ifdef Q_OS_MACOS
std::optional<QIcon> m_macos_icon;
std::optional<QIcon> 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);
Expand Down
Binary file modified src/qt/res/icons/dash.icns
Binary file not shown.
Binary file added src/qt/res/icons/dash_macos_devnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/icons/dash_macos_mainnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/icons/dash_macos_regtest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/icons/dash_macos_testnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/icons/dash_macos_tray.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/icons/dash_macos_tray@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/src/macos_devnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/src/macos_mainnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/src/macos_regtest.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/qt/res/src/macos_testnet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/qt/res/src/tray.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading