Files
transmission/qt/NativeIcon.cc
Mike Gelfand 887473cade Fix missing Qt client icons on Windows (#7931)
* Enable FreeType and Harfbuzz for Windows release builds

This fixes icon font glyphs rendering in Qt client.

* Bump Qt to 6.10.1

* Use pixel sizes for icon font glyphs rendering on Windows

The glyphs otherwise turn out of a smaller, non-standard size which
makes them less crisp.
2025-12-14 20:39:02 +00:00

487 lines
15 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#include "NativeIcon.h"
#include <optional>
#include <string_view>
#include <QChar>
#include <QFontDatabase>
#include <QOperatingSystemVersion>
#include <QStyle>
#include <QtGui/QFont>
#include <QtGui/QGuiApplication> // qApp
#include <QtGui/QIcon>
#include <QtGui/QPainter>
#include <QtGui/QPalette>
#include <QtGui/QPixmap>
#include <small/set.hpp>
#if defined(Q_OS_MAC)
extern QPixmap loadSFSymbol(QString symbol_name, int pixel_size);
#endif
namespace icons
{
namespace
{
// https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-ui-symbol-font
auto const Win10IconFamily = QStringLiteral("Segoe MDL2 Assets");
// https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font
auto const Win11IconFamily = QStringLiteral("Segoe Fluent Icons");
// Define these two macros to force a specific icon icon during development.
// Their EULA doesn't allow redistribution but does allow using them
// during design/develop/testing.
// 1. Snag the ttf you want to use (Win 10 uses https://aka.ms/SegoeFonts,
// Win 11 uses https://aka.ms/SegoeFluentIcons).
// 2. Add it to application.qrc
// 3. Set these two macros accordingly
// #define DEV_FORCE_FONT_FAMILY Win11IconFamily
// #define DEV_FORCE_FONT_RESOURCE QStringLiteral(":devonly/segoe_fluent_icons.ttf")
QString getWindowsFontFamily()
{
#ifdef DEV_FORCE_FONT_FAMILY
return DEV_FORCE_FONT_FAMILY;
#else
if (QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Windows, 11))
return Win11IconFamily;
if (QOperatingSystemVersion::current() >= QOperatingSystemVersion(QOperatingSystemVersion::Windows, 10))
return Win10IconFamily;
return {};
#endif
}
void ensureFontsLoaded()
{
#ifdef DEV_FORCE_FONT_RESOURCE
[[maybe_unused]] static auto const font_id = QFontDatabase::addApplicationFont(DEV_FORCE_FONT_RESOURCE);
#endif
}
QPixmap makeIconFromCodepoint(QString const family, QChar const codepoint, int const pixel_size)
{
auto font = QFont{ family };
if (!QFontMetrics{ font }.inFont(codepoint))
return {};
font.setPixelSize(pixel_size);
// FIXME: HDPI, pixel size vs point size?
auto const rect = QRect{ 0, 0, pixel_size, pixel_size };
auto pixmap = QPixmap{ rect.size() };
pixmap.fill(Qt::transparent);
auto painter = QPainter{ &pixmap };
painter.setFont(font);
painter.setBrush(Qt::NoBrush);
painter.setPen(qApp->palette().color(QPalette::ButtonText));
painter.setRenderHint(QPainter::TextAntialiasing);
auto br = QRect{};
painter.drawText(rect, Qt::AlignCenter, QString{ codepoint }, &br);
painter.end();
return pixmap;
}
struct Info
{
std::string_view sf_symbol_name;
char16_t segoe_codepoint = {};
std::string_view xdg_icon_name;
std::optional<QStyle::StandardPixmap> fallback;
bool ok_in_gnome_menus = false;
};
/**
* # Choosing Icons
*
* Use icons that follow the per-platform guidelines below.
* If there's not a match, don't give that Type an icon on that platform.
*
* ## Windows
*
* https://learn.microsoft.com/en-us/windows/apps/design/controls/menus
* Consider providing menu item icons for:
* - The most commonly used items.
* - Menu items whose icon is standard or well known.
* - Menu items whose icon well illustrates what the command does.
* Don't feel obligated to provide icons for commands that don't have
* a standard visualization. Cryptic icons aren't helpful, create visual
* clutter, and prevent users from focusing on the important menu items.
*
* ## macOS
*
* https://developer.apple.com/design/human-interface-guidelines/menus
* Represent menu item actions with familiar icons. Icons help people
* recognize common actions throughout your app. Use the same icons as
* the system to represent actions such as Copy, Share, and Delete,
* wherever they appear. For a list of icons that represent common
* actions, see Standard icons.
* Dont display an icon if you cant find one that clearly represents
* the menu item. Not all menu items need an icon. Be careful when adding
* icons for custom menu items to avoid confusion with other existing
* actions, and dont add icons just for the sake of ornamentation.
*
* ## GNOME
*
* https://discourse.gnome.org/t/principle-of-icons-in-menus/4803
* We do not “block” icons in menus; icons are generally reserved
* for “nouns”, or “objects”—for instance: website favicons in
* bookmark menus, or file-type icons—instead of having them for
* “verbs”, or “actions”—for instance: save, copy, print, etc.
*
* ## KDE
*
* https://develop.kde.org/hig/icons/#icons-for-menu-items-and-buttons-with-text
* Set an icon on every button and menu item, making sure not to
* use the same icon for multiple visible buttons or menu items.
* Choose different icons, or use more specific ones to disambiguate.
*
* ## QStyle::StandardPixmap
*
* This is an extremely limited icon set. Use with caution.
* https://github.com/transmission/transmission/pull/2283 has galleries.
* This is used as a fallback to ensure all toolbar acitions have icons,
* even on very old Windows / macOS systems lacking Segoe / SF Symbols.
*/
[[nodiscard]] constexpr Info getInfo(Type const type)
{
auto sf_symbol_name = std::string_view{};
auto xdg_icon_name = std::string_view{};
auto segoe_codepoint = char16_t{};
auto fallback = std::optional<QStyle::StandardPixmap>{};
auto ok_in_gnome_menus = false;
switch (type)
{
case Type::AddTracker:
sf_symbol_name = "plus";
segoe_codepoint = 0xE710U; // Add
xdg_icon_name = "list-add";
break;
case Type::EditTrackers:
sf_symbol_name = "pencil";
segoe_codepoint = 0xE70FU; // Edit
xdg_icon_name = "document-edit";
break;
case Type::RemoveTracker:
sf_symbol_name = "minus";
segoe_codepoint = 0xE738U; // Remove
xdg_icon_name = "list-remove";
break;
case Type::AddTorrentFromFile:
sf_symbol_name = "folder";
segoe_codepoint = 0xE8E5U; // OpenFile
xdg_icon_name = "document-open";
fallback = QStyle::SP_FileIcon;
break;
case Type::AddTorrentFromURL:
sf_symbol_name = "network";
segoe_codepoint = 0xE774U; // Globe
xdg_icon_name = "network-workgroup";
fallback = QStyle::SP_DriveNetIcon;
break;
case Type::CreateNewTorrent:
sf_symbol_name = "plus";
segoe_codepoint = 0xE710U; // Add
xdg_icon_name = "document-new";
fallback = QStyle::SP_FileIcon;
break;
case Type::OpenTorrentDetails:
sf_symbol_name = "doc.text.magnifyingglass";
segoe_codepoint = 0xE946U; // Info
xdg_icon_name = "document-properties";
ok_in_gnome_menus = true;
fallback = QStyle::SP_FileDialogContentsView;
break;
case Type::OpenTorrentLocalFolder:
sf_symbol_name = "folder";
segoe_codepoint = 0xED25U; // OpenFolderHorizontal
xdg_icon_name = "folder-open";
fallback = QStyle::SP_DirOpenIcon;
break;
case Type::StartTorrent:
sf_symbol_name = "play";
segoe_codepoint = 0xE768U; // Play
xdg_icon_name = "media-playback-start";
fallback = QStyle::SP_MediaPlay;
break;
case Type::StartTorrentNow:
sf_symbol_name = "forward";
segoe_codepoint = 0xEB9DU; // FastForward
xdg_icon_name = "media-seek-forward";
fallback = QStyle::SP_MediaSeekForward;
break;
case Type::RemoveTorrent:
sf_symbol_name = "minus";
segoe_codepoint = 0xE738U; // Remove
xdg_icon_name = "list-remove";
fallback = QStyle::SP_DialogCancelButton;
break;
case Type::RemoveTorrentAndDeleteData:
sf_symbol_name = "trash";
segoe_codepoint = 0xE74DU; // Delete
xdg_icon_name = "edit-delete";
fallback = QStyle::SP_TrashIcon;
break;
case Type::SetTorrentLocation:
sf_symbol_name = "arrow.up.and.down.and.arrow.left.and.right";
segoe_codepoint = 0xE7C2U; // Move
xdg_icon_name = "edit-find";
break;
case Type::CopyMagnetLinkToClipboard:
sf_symbol_name = "clipboard";
segoe_codepoint = 0xE8C8U; // Copy
xdg_icon_name = "edit-copy";
break;
case Type::SelectAll:
sf_symbol_name = "checkmark.square";
segoe_codepoint = 0xE8B3U; // SelectAll
xdg_icon_name = "edit-select-all";
break;
case Type::DeselectAll:
sf_symbol_name = "square";
segoe_codepoint = 0xE739U; // Checkbox
xdg_icon_name = "edit-select-none";
break;
case Type::Statistics:
sf_symbol_name = "chart.bar";
segoe_codepoint = 0xED5EU; // Ruler
xdg_icon_name = "info";
ok_in_gnome_menus = true;
break;
case Type::Donate:
sf_symbol_name = "heart";
segoe_codepoint = 0xEB51U; // Heart
xdg_icon_name = "donate";
break;
case Type::Settings:
sf_symbol_name = "gearshape";
segoe_codepoint = 0xE713U; // Settings
xdg_icon_name = "preferences-system";
ok_in_gnome_menus = true;
break;
case Type::QuitApp:
sf_symbol_name = "power";
segoe_codepoint = 0xE7E8U; // PowerButton
xdg_icon_name = "application-exit";
break;
case Type::About:
sf_symbol_name = "info.circle";
segoe_codepoint = 0xE946U; // Info
xdg_icon_name = "help-about";
fallback = QStyle::SP_MessageBoxInformation;
ok_in_gnome_menus = true;
break;
case Type::Help:
sf_symbol_name = "questionmark.circle";
segoe_codepoint = 0xE897U; // Help
xdg_icon_name = "help-faq";
fallback = QStyle::SP_DialogHelpButton;
ok_in_gnome_menus = true;
break;
case Type::QueueMoveTop:
sf_symbol_name = "arrow.up.to.line";
segoe_codepoint = 0xEDDBU; // CaretUpSolid8
xdg_icon_name = "go-top";
break;
case Type::QueueMoveUp:
sf_symbol_name = "arrow.up";
segoe_codepoint = 0xEDD7U; // CaretUp8
xdg_icon_name = "go-up";
break;
case Type::QueueMoveDown:
sf_symbol_name = "arrow.down";
segoe_codepoint = 0xEDD8U; // CaretDown8
xdg_icon_name = "go-down";
break;
case Type::QueueMoveBottom:
sf_symbol_name = "arrow.down.to.line";
segoe_codepoint = 0xEDDCU; // CaretDownSolid8
xdg_icon_name = "go-bottom";
break;
case Type::NetworkIdle:
xdg_icon_name = "network-idle";
break;
case Type::NetworkReceive:
sf_symbol_name = "arrow.down.circle";
segoe_codepoint = 0xE896U; // Download
xdg_icon_name = "network-receive";
break;
case Type::NetworkTransmit:
sf_symbol_name = "arrow.up.circle";
segoe_codepoint = 0xE898U; // Upload
xdg_icon_name = "network-transmit";
break;
case Type::NetworkTransmitReceive:
sf_symbol_name = "arrow.up.arrow.down.circle";
segoe_codepoint = 0xE174U; // UploadDownload
xdg_icon_name = "network-transmit-receive";
break;
case Type::NetworkError:
sf_symbol_name = "wifi.exclamationmark";
segoe_codepoint = 0xE783U; // Error
xdg_icon_name = "network-error";
fallback = QStyle::SP_MessageBoxCritical;
break;
case Type::TorrentStateActive:
sf_symbol_name = "play";
segoe_codepoint = 0xE768U; // Play
xdg_icon_name = "media-playback-start";
fallback = QStyle::SP_MediaPlay;
break;
case Type::TorrentStateSeeding:
sf_symbol_name = "chevron.up";
segoe_codepoint = 0xE70EU; // ChevronUp
xdg_icon_name = "go-up";
fallback = QStyle::SP_ArrowUp;
break;
case Type::TorrentStateDownloading:
sf_symbol_name = "chevron.down";
segoe_codepoint = 0xE70DU; // ChevronDown
xdg_icon_name = "go-down";
fallback = QStyle::SP_ArrowDown;
break;
case Type::PauseTorrent:
[[fallthrough]];
case Type::TorrentStatePaused:
sf_symbol_name = "pause";
segoe_codepoint = 0xE769U; // Pause
xdg_icon_name = "media-playback-pause";
fallback = QStyle::SP_MediaPause;
break;
case Type::VerifyTorrent:
[[fallthrough]];
case Type::TorrentStateVerifying:
sf_symbol_name = "arrow.clockwise";
segoe_codepoint = 0xE72CU; // Refresh
xdg_icon_name = "view-refresh";
fallback = QStyle::SP_BrowserReload;
break;
case Type::TorrentErrorEmblem:
[[fallthrough]];
case Type::TorrentStateError:
sf_symbol_name = "xmark.circle";
segoe_codepoint = 0xEB90U; // StatusErrorFull
xdg_icon_name = "dialog-error";
fallback = QStyle::SP_MessageBoxWarning;
break;
}
return { sf_symbol_name, segoe_codepoint, xdg_icon_name, fallback, ok_in_gnome_menus };
}
} // namespace
QIcon icon(Type const type, QStyle const* const style)
{
ensureFontsLoaded();
auto const pixel_sizes = small::max_size_set<int, 7U>{
style->pixelMetric(QStyle::PM_ButtonIconSize), style->pixelMetric(QStyle::PM_LargeIconSize),
style->pixelMetric(QStyle::PM_ListViewIconSize), style->pixelMetric(QStyle::PM_MessageBoxIconSize),
style->pixelMetric(QStyle::PM_SmallIconSize), style->pixelMetric(QStyle::PM_TabBarIconSize),
style->pixelMetric(QStyle::PM_ToolBarIconSize)
};
auto const info = getInfo(type);
#if defined(Q_OS_MAC)
if (auto const key = info.sf_symbol_name; !std::empty(key))
{
auto icon = QIcon{};
auto const name = QString::fromUtf8(std::data(key), std::size(key));
for (int const pixel_size : pixel_sizes)
if (auto const pixmap = loadSFSymbol(name, pixel_size); !pixmap.isNull())
icon.addPixmap(pixmap);
if (!icon.isNull())
return icon;
}
#endif
if (auto const key = info.segoe_codepoint)
{
if (auto const family = getWindowsFontFamily(); !family.isEmpty())
{
auto icon = QIcon{};
auto const ch = QChar{ key };
for (int const pixel_size : pixel_sizes)
if (auto pixmap = makeIconFromCodepoint(family, ch, pixel_size); !pixmap.isNull())
icon.addPixmap(pixmap);
if (!icon.isNull())
return icon;
}
}
if (auto const key = info.xdg_icon_name; !std::empty(key))
{
auto const name = QString::fromUtf8(std::data(key), std::size(key));
if (auto icon = QIcon::fromTheme(name); !icon.isNull())
return icon;
if (auto icon = QIcon::fromTheme(name + QStringLiteral("-symbolic")); !icon.isNull())
return icon;
}
if (info.fallback)
return style->standardIcon(*info.fallback);
return {};
}
[[nodiscard]] bool shouldBeShownInMenu(Type type)
{
static bool const force_icons = !qgetenv("TR_SHOW_MENU_ICONS").isEmpty();
static bool const is_gnome = qgetenv("XDG_CURRENT_DESKTOP").contains("GNOME");
return force_icons || !is_gnome || getInfo(type).ok_in_gnome_menus;
}
} // namespace icons