mirror of
https://github.com/transmission/transmission.git
synced 2025-12-20 02:18:42 +00:00
* chore: savepoint * chore: code style * refactor: add std::string_view constructor for NativeIcon::Spec * chore: add TODO comment * feat: honor per-desktop HIG on when to show menu icons * chore: remove Faenza system-run icon unused sinceb58e95910b* chore: remove Faenza view-refresh icon not needed due tob58e95910b* chore: remove Faenza media-playback-pause icon not needed due tob58e95910b* chore: remove Faenza media-playback-start icon not needed due tob58e95910b* chore: add a safeguard against merging with incomplete TODO items * feat: add more icons refactor: remove some tracer cerr statements * refactor: remove IconCache use from MainWindow * chore: remove Faenza icon set * chore: re-enable remote session network icon * fix: FTBFS on Windows * refactor: use symbolic names for Segoe icons * docs: add links to Segoe MDL2 Assets icon list * chore: savepoint segoe icons work still a WIP; includes test code that should not ship * feat: use segoe::FastForward for action_StartNow feat: use segoe::Move for action_SetLocation refactor: make it easier for devs to force a font at compile time for development work segoe license does not allow bundling but does allow dev work chore: code_style.sh * refactor: remove unused addEmblem() * docs: add code comment on how to force an icon font * fix: Win 10, 11 icons play nicely with dark mode * chore: savepoint add draft of SF Symbol -> QPixmap loader * chore: remove dangling font reference from qrc file * fix: FTBFS * refactor: use bribri code for NSImage -> QPixmap * feat: support dark, light mode when rendering SF Symbol monochrome icons * fixup! feat: support dark, light mode when rendering SF Symbol monochrome icons fix: fail gracefully on macOS 11 * chore: code style * chore: tweak some SF Symbol icon choices * chore: consistent uppercase for hex segoe QChars * chore: undefine DEV_FORCE_FONT_FAMILY and DEV_FORCE_FONT_RESOURCE * chore: savepoint * refactor: clean up NativeIcon impl * refactor: remove unused MenuMode::Other * refactor: DRY in FilterBar::createActivityCombo() * chore: remove obsolete code comment * refactor: rename icons::Facet as icons::Type * fix: oops * refactor: minor cleanup * fix: tyop * chore: remove unused #includes * fix: add modes for some icons * refactor: tweak some icon choices on macOS * fix: ensure icons are visible on File, Help menus fix: remove unused local variable * refactor: tweak some icon choices for XDG * refactor: remove the fallback QStyle::StandardPixmaps These interfere with deciding whether an icon is well-defined and unambiguous as per the macOS and Windows HIG guidelines. If a standard or unambiguous icon exists in the native icon sets, specify it with an SF Symbols name, a Segoe codepoint, or XDG standard icon name. Otherwise, leave those fields blank. * refactor: remove unused #includes * docs: add "choosing icons" section in NativeIcons.cc * refactor: simplify icons::shouldBeShownInMenu() * refactor: reduce unnecessary code shear from main * refactor: make TorrentDelegate::warning_emblem_ const * refactor: extract-method MainWindow::updateActionIcons() * feat: update MainWindow icons when light/dark theme changes * feat: restore the QStyle::StandardPixmaps as fallbacks Can be used on older Windows / macOS if Segoe or SF Symbols are unavailable * refactor: add button text for add/edit/remove tracker buttons QStyle::StandardPixmap doesn't have good icons for these, so let's ensure that these buttons have visible text. * fix: building NativeIconMac.mm on mac even if not clang * chore: iwyu in new code * docs: tweak the "Choosing Icons" comments again * fix: handle changed QStyles in icons::icon() do not cache point_sizes set between calls refactor: const correctness * fixup! refactor: simplify icons::shouldBeShownInMenu() refactor: minor code tweak, declare vars in order that they are used
485 lines
15 KiB
C++
485 lines
15 KiB
C++
// 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 point_size)
|
||
{
|
||
auto const font = QFont{ family, point_size - 8 };
|
||
if (!QFontMetrics{ font }.inFont(codepoint))
|
||
return {};
|
||
|
||
// FIXME: HDPI, pixel size vs point size?
|
||
auto const rect = QRect{ 0, 0, point_size, point_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.
|
||
* Don’t display an icon if you can’t 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 don’t 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 point_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 point_size : point_sizes)
|
||
if (auto const pixmap = loadSFSymbol(name, point_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 point_size : point_sizes)
|
||
if (auto pixmap = makeIconFromCodepoint(family, ch, point_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
|