diff --git a/src/core/Application.cpp b/src/core/Application.cpp index 2215cee..cad3ed6 100644 --- a/src/core/Application.cpp +++ b/src/core/Application.cpp @@ -243,7 +243,7 @@ void Application::LoadConfiguration() { static_cast(ReadIniInt(L"General", L"PrevDesktopKey", VK_OEM_3))); VirtualDesktopSwitcher::SetPinAllDesktopsKey( static_cast(ReadIniInt(L"General", L"PinAllDesktopsKey", 'D'))); - for (int i = 0; i < static_cast(kMaxDesktops); ++i) { + for (int i = 0; i < kMaxDesktops; ++i) { std::wstring keyName = L"DesktopKey" + std::to_wstring(i + 1); VirtualDesktopSwitcher::SetDesktopKey(i, static_cast(ReadIniInt(L"General", keyName, '1' + i))); @@ -410,6 +410,8 @@ void Application::SyncDesktopState() { if (m_pOverlay) { m_pOverlay->ShowTemporarily(); } } + m_pTrayIcon->UpdateTrayIcon(currentDesktop + 1); + if (m_pOverlay) { auto emptyMask = m_switcher->GetDesktopEmptyMask(); m_pOverlay->SetDesktopState(desktopCount, currentDesktop, emptyMask); diff --git a/src/core/IndicatorConfig.h b/src/core/IndicatorConfig.h index 5d1a093..015dd41 100644 --- a/src/core/IndicatorConfig.h +++ b/src/core/IndicatorConfig.h @@ -30,6 +30,12 @@ enum class DragSwitchMode : std::uint8_t { Count = 4 }; +enum class TrayIconMode : std::uint8_t { + Icon = 0, + Number = 1, + Count = 2 +}; + struct WindowRatio { float x = 0.5f; float y = 0.0f; diff --git a/src/core/VirtualDesktopHelper.cpp b/src/core/VirtualDesktopHelper.cpp index 5cb6336..b079703 100644 --- a/src/core/VirtualDesktopHelper.cpp +++ b/src/core/VirtualDesktopHelper.cpp @@ -436,7 +436,7 @@ std::array VirtualDesktopHelper::GetDesktopEmptyMask() const } } - for (UINT i = 0; i < desktopCount && i < kMaxDesktops; ++i) { + for (UINT i = 0; i < desktopCount && i < static_cast(kMaxDesktops); ++i) { emptyMask.at(i) = (windowCounts.at(i) == 0); } return emptyMask; diff --git a/src/core/VirtualDesktopSwitcher.cpp b/src/core/VirtualDesktopSwitcher.cpp index 9561e2e..ffc25f2 100644 --- a/src/core/VirtualDesktopSwitcher.cpp +++ b/src/core/VirtualDesktopSwitcher.cpp @@ -88,7 +88,7 @@ LRESULT CALLBACK VirtualDesktopSwitcher::LowLevelKeyboardProc(int nCode, WPARAM bool match = held != 0 && held == s_modMask; if (match) { - for (int i = 0; i < static_cast(kMaxDesktops); ++i) { + for (int i = 0; i < kMaxDesktops; ++i) { if (pKeyboard->vkCode == s_desktopKeys.at(i)) { PostMessage(s_active->m_hwnd, WM_SWITCH_DESKTOP, i, 0); return 1; diff --git a/src/core/VirtualDesktopSwitcher.h b/src/core/VirtualDesktopSwitcher.h index b3c09d8..595e72d 100644 --- a/src/core/VirtualDesktopSwitcher.h +++ b/src/core/VirtualDesktopSwitcher.h @@ -43,7 +43,7 @@ class VirtualDesktopSwitcher { static void SetPrevDesktopKey(uint8_t vk) { s_prevDesktopKey = vk; } static void SetPinAllDesktopsKey(uint8_t vk) { s_pinAllDesktopsKey = vk; } static void SetDesktopKey(int index, uint8_t vk) { - if (index >= 0 && index < static_cast(kMaxDesktops)) { s_desktopKeys.at(index) = vk; } + if (index >= 0 && index < kMaxDesktops) { s_desktopKeys.at(index) = vk; } } [[nodiscard]] int GetDesktopCount() const; diff --git a/src/ui/DesktopIndicator.cpp b/src/ui/DesktopIndicator.cpp index d27dabd..6468b8e 100644 --- a/src/ui/DesktopIndicator.cpp +++ b/src/ui/DesktopIndicator.cpp @@ -563,7 +563,7 @@ void DesktopIndicator::RebuildText() { return; } std::wstring text; - for (int i = 0; i < m_desktopCount && i < static_cast(kMaxDesktops); ++i) { + for (int i = 0; i < m_desktopCount && i < kMaxDesktops; ++i) { text += (i == m_currentDesktop) ? m_pCfg->currentSymbol : m_emptyDesktops.at(i) ? m_pCfg->emptySymbol : m_pCfg->otherSymbol; diff --git a/src/ui/TrayIcon.cpp b/src/ui/TrayIcon.cpp index 8a8f70f..97e89a3 100644 --- a/src/ui/TrayIcon.cpp +++ b/src/ui/TrayIcon.cpp @@ -1,7 +1,10 @@ #include "TrayIcon.h" +#include #include +#include + #include "core/IndicatorConfig.h" #include "util/AutoStart.h" #include "util/ConfigIni.h" @@ -10,22 +13,22 @@ namespace { -constexpr UINT WM_TRAY_EXIT = WM_USER + 3; -constexpr UINT WM_TRAY_TOGGLE_AUTOSTART = WM_USER + 4; -constexpr UINT WM_TRAY_SETTINGS = WM_USER + 6; -constexpr UINT WM_TRAY_ABOUT = WM_USER + 7; -constexpr UINT WM_TRAY_RUNAS_ADMIN = WM_USER + 8; -constexpr UINT WM_TRAY_ANIM_MODE = WM_USER + 9; -constexpr UINT WM_TRAY_TOGGLE_SHOW = WM_USER + 10; -constexpr UINT WM_TRAY_RESET = WM_USER + 11; -constexpr UINT WM_TRAY_AUTO_CONTRAST = WM_USER + 12; -constexpr UINT WM_TRAY_AUTO_FOCUS = WM_USER + 13; -constexpr UINT CMD_SHOW_MODE_BASE = WM_USER + 300; -constexpr UINT CMD_SHOW_MODE_CUSTOM = CMD_SHOW_MODE_BASE + static_cast(ShowMode::Count); -constexpr UINT CMD_POSITION_BASE = WM_USER + 200; -constexpr UINT CMD_POSITION_CUSTOM = CMD_POSITION_BASE + static_cast(PositionPreset::Count) + 1; -constexpr UINT CMD_DRAG_MODE_BASE = WM_USER + 400; -constexpr UINT CMD_DRAG_MODE_CUSTOM = CMD_DRAG_MODE_BASE + static_cast(DragSwitchMode::Count); +constexpr UINT CMD_TRAY_EXIT = WM_USER + 3; +constexpr UINT CMD_TRAY_TOGGLE_AUTOSTART = WM_USER + 4; +constexpr UINT CMD_TRAY_SETTINGS = WM_USER + 6; +constexpr UINT CMD_TRAY_ABOUT = WM_USER + 7; +constexpr UINT CMD_TRAY_RUNAS_ADMIN = WM_USER + 8; +constexpr UINT CMD_TRAY_ANIM_MODE = WM_USER + 9; +constexpr UINT CMD_TRAY_TOGGLE_SHOW = WM_USER + 10; +constexpr UINT CMD_TRAY_RESET = WM_USER + 11; +constexpr UINT CMD_TRAY_AUTO_CONTRAST = WM_USER + 12; +constexpr UINT CMD_TRAY_AUTO_FOCUS = WM_USER + 13; +constexpr UINT CMD_SHOW_MODE_BASE = WM_USER + 300; +constexpr UINT CMD_SHOW_MODE_CUSTOM = CMD_SHOW_MODE_BASE + static_cast(ShowMode::Count); +constexpr UINT CMD_POSITION_BASE = WM_USER + 200; +constexpr UINT CMD_POSITION_CUSTOM = CMD_POSITION_BASE + static_cast(PositionPreset::Count) + 1; +constexpr UINT CMD_DRAG_MODE_BASE = WM_USER + 400; +constexpr UINT CMD_DRAG_MODE_CUSTOM = CMD_DRAG_MODE_BASE + static_cast(DragSwitchMode::Count); constexpr std::array kShowModeKeys = { L"Menu.AlwaysShow", @@ -53,19 +56,64 @@ constexpr std::array kDragModeKeys = { L"Menu.DragNever", }; -void DrawSwatchRect(HDC hdc, RECT rect, const std::wstring &hex) { +} // namespace + +// GdiplusGuard RAII:GDI+ 生命周期由 TrayIcon 持有(构造启动、析构关闭) +class GdiplusGuard { +public: + GdiplusGuard() { + Gdiplus::GdiplusStartupInput input; + if (Gdiplus::GdiplusStartup(&m_token, &input, nullptr) != Gdiplus::Ok) { + m_token = 0; + } + } + ~GdiplusGuard() { + if (m_token != 0) { + Gdiplus::GdiplusShutdown(m_token); + } + } + GdiplusGuard(const GdiplusGuard &) = delete; + GdiplusGuard &operator=(const GdiplusGuard &) = delete; + GdiplusGuard(GdiplusGuard &&) = delete; + GdiplusGuard &operator=(GdiplusGuard &&) = delete; + + [[nodiscard]] bool Ok() const { return m_token != 0; } + +private: + ULONG_PTR m_token = 0; +}; + +namespace { + +void DrawSwatchRect(HDC hdc, RECT rect, const std::wstring &hex, GdiplusGuard &gdiplus) { auto colors = ParseMultiColorString(hex); - if (colors.count >= 2) { - const int sw = rect.right - rect.left; - const int sh = rect.bottom - rect.top; - for (int sx = 0; sx < sw; ++sx) { - COLORREF col = InterpolateGradientColor(colors.colors.data(), colors.count, static_cast(sx) / static_cast(sw)); - for (int sy = 0; sy < sh; ++sy) { - SetPixel(hdc, rect.left + sx, rect.top + sy, col); + if (colors.count >= 2 && gdiplus.Ok()) { + std::array gdColors{}; + for (size_t i = 0; i < colors.count; ++i) { + gdColors[i] = Gdiplus::Color(255, GetRValue(colors.colors[i]), + GetGValue(colors.colors[i]), GetBValue(colors.colors[i])); + } + Gdiplus::LinearGradientBrush brush( + Gdiplus::RectF(static_cast(rect.left), static_cast(rect.top), + static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)), + gdColors[0], gdColors[colors.count - 1], Gdiplus::LinearGradientModeHorizontal); + if (colors.count > 2) { + std::array positions{}; + for (size_t i = 0; i < colors.count; ++i) { + positions[i] = static_cast(i) / static_cast(colors.count - 1); } + brush.SetInterpolationColors(gdColors.data(), positions.data(), static_cast(colors.count)); + } + { + Gdiplus::Graphics g(hdc); + g.FillRectangle(&brush, + static_cast(rect.left), static_cast(rect.top), + static_cast(rect.right - rect.left), + static_cast(rect.bottom - rect.top)); } - } else if (colors.count == 1) { + } else if (colors.count >= 1) { HBRUSH hb = CreateSolidBrush(colors.colors[0]); FillRect(hdc, &rect, hb); DeleteObject(hb); @@ -83,6 +131,8 @@ void DrawSwatchRect(HDC hdc, RECT rect, const std::wstring &hex) { } // namespace +TrayIcon::TrayIcon() : m_gdiplus(std::make_unique()) {} + TrayIcon::~TrayIcon() { if (m_nid.hWnd != nullptr) { Shell_NotifyIconW(NIM_DELETE, &m_nid); @@ -90,6 +140,12 @@ TrayIcon::~TrayIcon() { if (m_hMenu != nullptr) { DestroyMenu(m_hMenu); } + for (int i = 0; i <= kMaxDesktops; ++i) { + if (m_hNumberIcons[i] != nullptr) { + DestroyIcon(m_hNumberIcons[i]); + m_hNumberIcons[i] = nullptr; + } + } } void TrayIcon::BuildMenu() { @@ -134,39 +190,67 @@ void TrayIcon::BuildMenu() { } AppendMenuW(hColorMenu, MF_SEPARATOR, 0, nullptr); bool animOn = ReadIniInt(L"Display", L"AnimMode", 1) != 0; - AppendMenuW(hColorMenu, MF_STRING | (animOn ? MF_CHECKED : 0), WM_TRAY_ANIM_MODE, Lang::Get(L"Menu.BreathingRing")); + AppendMenuW(hColorMenu, MF_STRING | (animOn ? MF_CHECKED : 0), CMD_TRAY_ANIM_MODE, Lang::Get(L"Menu.BreathingRing")); bool autoContrastOn = ReadIniInt(L"Display", L"AutoContrast", 1) != 0; - AppendMenuW(hColorMenu, MF_STRING | (autoContrastOn ? MF_CHECKED : 0), WM_TRAY_AUTO_CONTRAST, Lang::Get(L"Menu.AutoContrast")); + AppendMenuW(hColorMenu, MF_STRING | (autoContrastOn ? MF_CHECKED : 0), CMD_TRAY_AUTO_CONTRAST, Lang::Get(L"Menu.AutoContrast")); AppendMenuW(m_hMenu, MF_POPUP, reinterpret_cast(hColorMenu), Lang::Get(L"Menu.Color")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) - AppendMenuW(m_hMenu, MF_STRING, WM_TRAY_SETTINGS, Lang::Get(L"Menu.Settings")); + HMENU hTrayIconMenu = CreatePopupMenu(); + int curIconMode = ReadIniInt(L"Display", L"TrayIconMode", static_cast(TrayIconMode::Icon)); + AppendMenuW(hTrayIconMenu, MF_STRING | (curIconMode == static_cast(TrayIconMode::Icon) ? MF_CHECKED : 0), + CMD_TRAY_ICON_MODE_ICON, Lang::Get(L"Menu.TrayIconIcon")); + AppendMenuW(hTrayIconMenu, MF_STRING | (curIconMode == static_cast(TrayIconMode::Number) ? MF_CHECKED : 0), + CMD_TRAY_ICON_MODE_NUMBER, Lang::Get(L"Menu.TrayIconNumber")); + AppendMenuW(hTrayIconMenu, MF_SEPARATOR, 0, nullptr); + + HMENU hNumColorMenu = CreatePopupMenu(); + std::wstring curNumColor = ReadIniString(L"Display", L"TrayNumberColor", L"#2997FF"); + UINT numColorIdx = 0; + for (UINT i = 0; i < static_cast(kPredefinedColors.size()); ++i) { + if (std::wstring(kPredefinedColors.at(i)).find(L'_') != std::wstring::npos) { continue; } + MENUITEMINFOW mii = {}; + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_ID | MIIM_FTYPE | MIIM_STATE; + mii.fType = MFT_OWNERDRAW; + mii.wID = CMD_TRAY_NUMBER_COLOR_BASE + i; + if (wcscmp(kPredefinedColors.at(i), curNumColor.c_str()) == 0) { + mii.fState = MFS_CHECKED; + } + InsertMenuItemW(hNumColorMenu, numColorIdx++, TRUE, &mii); + } + AppendMenuW(hTrayIconMenu, MF_POPUP, reinterpret_cast(hNumColorMenu), Lang::Get(L"Menu.TrayNumberColor")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + AppendMenuW(m_hMenu, MF_POPUP, reinterpret_cast(hTrayIconMenu), Lang::Get(L"Menu.TrayIcon")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + AppendMenuW(m_hMenu, MF_STRING, CMD_TRAY_SETTINGS, Lang::Get(L"Menu.Settings")); AppendMenuW(m_hMenu, MF_SEPARATOR, 0, nullptr); HMENU hLangMenu = CreatePopupMenu(); - AppendMenuW(hLangMenu, MF_STRING | (Lang::Current() == LangType::Chinese ? MF_CHECKED : 0), WM_TRAY_LANG_CHINESE, Lang::Get(L"Menu.LangChinese")); - AppendMenuW(hLangMenu, MF_STRING | (Lang::Current() == LangType::English ? MF_CHECKED : 0), WM_TRAY_LANG_ENGLISH, Lang::Get(L"Menu.LangEnglish")); + AppendMenuW(hLangMenu, MF_STRING | (Lang::Current() == LangType::Chinese ? MF_CHECKED : 0), CMD_TRAY_LANG_CHINESE, Lang::Get(L"Menu.LangChinese")); + AppendMenuW(hLangMenu, MF_STRING | (Lang::Current() == LangType::English ? MF_CHECKED : 0), CMD_TRAY_LANG_ENGLISH, Lang::Get(L"Menu.LangEnglish")); AppendMenuW(m_hMenu, MF_POPUP, reinterpret_cast(hLangMenu), Lang::Get(L"Menu.Language")); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) bool autoFocusOn = ReadIniInt(L"Display", L"AutoFocus", 1) != 0; AppendMenuW(m_hMenu, MF_STRING | (autoFocusOn ? MF_CHECKED : 0), - WM_TRAY_AUTO_FOCUS, Lang::Get(L"Menu.AutoFocus")); + CMD_TRAY_AUTO_FOCUS, Lang::Get(L"Menu.AutoFocus")); bool runAsAdmin = ReadIniInt(L"General", L"RunAsAdmin", 0) != 0; AppendMenuW(m_hMenu, MF_STRING | (runAsAdmin ? MF_CHECKED : MF_UNCHECKED), - WM_TRAY_RUNAS_ADMIN, Lang::Get(L"Menu.RunAsAdmin")); + CMD_TRAY_RUNAS_ADMIN, Lang::Get(L"Menu.RunAsAdmin")); AppendMenuW(m_hMenu, MF_STRING | (m_autoStartEnabled ? MF_CHECKED : MF_UNCHECKED), - WM_TRAY_TOGGLE_AUTOSTART, Lang::Get(L"Menu.AutoStart")); + CMD_TRAY_TOGGLE_AUTOSTART, Lang::Get(L"Menu.AutoStart")); AppendMenuW(m_hMenu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(m_hMenu, MF_STRING, WM_TRAY_RESET, Lang::Get(L"Menu.Reset")); - AppendMenuW(m_hMenu, MF_STRING, WM_TRAY_ABOUT, Lang::Get(L"Menu.About")); + AppendMenuW(m_hMenu, MF_STRING, CMD_TRAY_RESET, Lang::Get(L"Menu.Reset")); + AppendMenuW(m_hMenu, MF_STRING, CMD_TRAY_ABOUT, Lang::Get(L"Menu.About")); AppendMenuW(m_hMenu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(m_hMenu, MF_STRING, WM_TRAY_EXIT, Lang::Get(L"Menu.Exit")); + AppendMenuW(m_hMenu, MF_STRING, CMD_TRAY_EXIT, Lang::Get(L"Menu.Exit")); } bool TrayIcon::Initialize(HWND hwnd, HINSTANCE hInstance) { - HICON hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(101)); + m_hInstance = hInstance; + + HICON hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(kTrayDefaultIconResource)); if (hIcon == nullptr) { hIcon = LoadIcon(nullptr, IDI_APPLICATION); } @@ -178,6 +262,15 @@ bool TrayIcon::Initialize(HWND hwnd, HINSTANCE hInstance) { m_nid.uCallbackMessage = WM_TRAYICON; m_nid.hIcon = hIcon; + int modeVal = ReadIniInt(L"Display", L"TrayIconMode", static_cast(TrayIconMode::Icon)); + m_iconMode = (modeVal >= 0 && modeVal < static_cast(TrayIconMode::Count)) + ? static_cast(modeVal) + : TrayIconMode::Icon; + + std::wstring numColor = ReadIniString(L"Display", L"TrayNumberColor", L"#2997FF"); + auto colorArr = ParseMultiColorString(numColor); + m_numberColor = (colorArr.count > 0) ? colorArr.colors[0] : RGB(41, 151, 255); + UpdateTooltip(Lang::Get(L"Tray.DefaultTip")); if (Shell_NotifyIconW(NIM_ADD, &m_nid) == 0) { @@ -203,7 +296,7 @@ void TrayIcon::HandleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) TrackPopupMenu(m_hMenu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, nullptr); PostMessage(hwnd, WM_NULL, 0, 0); } else if (lParam == WM_LBUTTONDBLCLK) { - PostMessage(hwnd, WM_COMMAND, WM_TRAY_TOGGLE_SHOW, 0); + PostMessage(hwnd, WM_COMMAND, CMD_TRAY_TOGGLE_SHOW, 0); } break; @@ -221,7 +314,6 @@ void TrayIcon::HandleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) HGDIOBJ old = SelectObject(hdc, hFont); TEXTMETRICW tm{}; GetTextMetricsW(hdc, &tm); - m_menuAveWidth = tm.tmAveCharWidth; SelectObject(hdc, old); DeleteObject(hFont); ReleaseDC(hwnd, hdc); @@ -235,7 +327,12 @@ void TrayIcon::HandleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) case WM_DRAWITEM: { auto *dis = LParamToPtr(lParam); if (dis->CtlType == ODT_MENU) { - DrawColorSwatch(dis); + if (dis->itemID >= CMD_TRAY_NUMBER_COLOR_BASE + && dis->itemID < CMD_TRAY_NUMBER_COLOR_BASE + static_cast(kPredefinedColors.size())) { + DrawColorSwatch(dis, CMD_TRAY_NUMBER_COLOR_BASE); + } else { + DrawColorSwatch(dis, CMD_COLOR_OPTIONS_BASE); + } } break; } @@ -324,6 +421,37 @@ void TrayIcon::HandleColorCommand(int index) { if (m_colorFn) { m_colorFn(kPredefinedColors.at(index)); } } +void TrayIcon::SetTrayIconMode(TrayIconMode mode) { + if (m_iconMode == mode) { return; } + m_iconMode = mode; + WriteIniInt(L"Display", L"TrayIconMode", static_cast(mode)); + m_nTrayNumber = -1; + if (mode == TrayIconMode::Icon) { + UpdateTrayIcon(1); + } +} + +void TrayIcon::SetNumberColor(const std::wstring &hexColor) { + auto colorArr = ParseMultiColorString(hexColor); + m_numberColor = (colorArr.count > 0) ? colorArr.colors[0] : RGB(41, 151, 255); + WriteIniString(L"Display", L"TrayNumberColor", hexColor); + for (auto &hIcon : m_hNumberIcons) { + if (hIcon != nullptr) { + DestroyIcon(hIcon); + hIcon = nullptr; + } + } + if (m_iconMode == TrayIconMode::Number && m_nTrayNumber >= 1) { + int num = m_nTrayNumber; + m_nTrayNumber = -1; + UpdateTrayIcon(num); + } +} + +void TrayIcon::HandleNumberColorCommand(int colorIndex) { + SetNumberColor(kPredefinedColors.at(colorIndex)); +} + void TrayIcon::HandleCommand(WPARAM wParam) { const UINT cmd = LOWORD(wParam); @@ -343,42 +471,48 @@ void TrayIcon::HandleCommand(WPARAM wParam) { HandleColorCommand(static_cast(cmd - CMD_COLOR_OPTIONS_BASE)); return; } + if (cmd >= CMD_TRAY_NUMBER_COLOR_BASE && cmd < CMD_TRAY_NUMBER_COLOR_BASE + static_cast(kPredefinedColors.size())) { + HandleNumberColorCommand(static_cast(cmd - CMD_TRAY_NUMBER_COLOR_BASE)); + return; + } switch (cmd) { - case WM_TRAY_EXIT: HandleExit(); break; - case WM_TRAY_TOGGLE_AUTOSTART: HandleToggleAutoStart(); break; - case WM_TRAY_RUNAS_ADMIN: HandleRunAsAdmin(); break; + case CMD_TRAY_EXIT: HandleExit(); break; + case CMD_TRAY_TOGGLE_AUTOSTART: HandleToggleAutoStart(); break; + case CMD_TRAY_RUNAS_ADMIN: HandleRunAsAdmin(); break; + case CMD_TRAY_ICON_MODE_ICON: SetTrayIconMode(TrayIconMode::Icon); break; + case CMD_TRAY_ICON_MODE_NUMBER: SetTrayIconMode(TrayIconMode::Number); break; case CMD_POSITION_CUSTOM: m_activePositionPreset = PositionPreset::Custom; if (m_editModeFn) { m_editModeFn(); } break; - case WM_TRAY_SETTINGS: + case CMD_TRAY_SETTINGS: if (m_settingsFn) { m_settingsFn(); } break; - case WM_TRAY_LANG_CHINESE: + case CMD_TRAY_LANG_CHINESE: Lang::Set(LangType::Chinese); WriteIniInt(L"General", L"Language", 0); UpdateTooltip(Lang::Get(L"Tray.DefaultTip")); break; - case WM_TRAY_LANG_ENGLISH: + case CMD_TRAY_LANG_ENGLISH: Lang::Set(LangType::English); WriteIniInt(L"General", L"Language", 1); UpdateTooltip(Lang::Get(L"Tray.DefaultTip")); break; - case WM_TRAY_RESET: HandleReset(); break; - case WM_TRAY_ABOUT: + case CMD_TRAY_RESET: HandleReset(); break; + case CMD_TRAY_ABOUT: if (m_aboutFn) { m_aboutFn(); } break; - case WM_TRAY_ANIM_MODE: HandleAnimMode(); break; - case WM_TRAY_AUTO_CONTRAST: HandleAutoContrast(); break; - case WM_TRAY_AUTO_FOCUS: HandleAutoFocus(); break; - case WM_TRAY_TOGGLE_SHOW: HandleToggleShow(); break; + case CMD_TRAY_ANIM_MODE: HandleAnimMode(); break; + case CMD_TRAY_AUTO_CONTRAST: HandleAutoContrast(); break; + case CMD_TRAY_AUTO_FOCUS: HandleAutoFocus(); break; + case CMD_TRAY_TOGGLE_SHOW: HandleToggleShow(); break; default: break; } } -void TrayIcon::DrawColorSwatch(LPDRAWITEMSTRUCT dis) const { - const int colorIndex = static_cast(dis->itemID - CMD_COLOR_OPTIONS_BASE); +void TrayIcon::DrawColorSwatch(LPDRAWITEMSTRUCT dis, UINT base) const { + const int colorIndex = static_cast(dis->itemID - base); if (colorIndex < 0 || static_cast(colorIndex) >= kPredefinedColors.size()) { return; } @@ -401,35 +535,109 @@ void TrayIcon::DrawColorSwatch(LPDRAWITEMSTRUCT dis) const { } // Color swatch - const int labelW = m_menuAveWidth * 3; - const int pad = MulDiv(3, m_dpi, 96); + const int pad = MulDiv(3, m_dpi, 96); RECT cr = dis->rcItem; - cr.left += labelW + pad; + cr.left += pad; cr.top += MulDiv(1, m_dpi, 96); cr.bottom -= MulDiv(1, m_dpi, 96); cr.right = dis->rcItem.right - pad; - DrawSwatchRect(dis->hDC, cr, kPredefinedColors.at(colorIndex)); - - // Number label - const std::wstring numStr = std::to_wstring(colorIndex + 1); - RECT nr = dis->rcItem; - nr.left += pad; - nr.right = dis->rcItem.left + labelW; - SetBkMode(dis->hDC, TRANSPARENT); - SetTextColor(dis->hDC, (isSel != 0) ? GetSysColor(COLOR_HIGHLIGHTTEXT) : GetSysColor(COLOR_MENUTEXT)); - DrawTextW(dis->hDC, numStr.data(), -1, &nr, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); + DrawSwatchRect(dis->hDC, cr, kPredefinedColors.at(colorIndex), *m_gdiplus); } bool TrayIcon::Reinitialize() { if (m_nid.hWnd != nullptr) { Shell_NotifyIconW(NIM_DELETE, &m_nid); } - return Shell_NotifyIconW(NIM_ADD, &m_nid) != 0; + const bool ok = Shell_NotifyIconW(NIM_ADD, &m_nid) != 0; + m_nTrayNumber = -1; // 强制下一次同步重新应用桌面编号图标 + return ok; } void TrayIcon::UpdateTooltip(const std::wstring &tooltip) { wcsncpy_s(m_nid.szTip, tooltip.c_str(), _TRUNCATE); Shell_NotifyIconW(NIM_MODIFY, &m_nid); } + +void TrayIcon::UpdateTrayIcon(int displayNumber) { + if (m_iconMode == TrayIconMode::Icon) { + if (m_nTrayNumber == 0) { return; } + HICON hIcon = LoadIcon(m_hInstance, MAKEINTRESOURCE(kTrayDefaultIconResource)); + NOTIFYICONDATAW nid = {}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_nid.hWnd; + nid.uID = m_nid.uID; + nid.uFlags = NIF_ICON; + nid.hIcon = hIcon; + Shell_NotifyIconW(NIM_MODIFY, &nid); + m_nid.hIcon = hIcon; + m_nTrayNumber = 0; + return; + } + + if (displayNumber == m_nTrayNumber) { + return; + } + + NOTIFYICONDATAW nid = {}; + nid.cbSize = sizeof(nid); + nid.hWnd = m_nid.hWnd; + nid.uID = m_nid.uID; + nid.uFlags = NIF_ICON; + nid.hIcon = GetTrayIconForNumber(displayNumber); + Shell_NotifyIconW(NIM_MODIFY, &nid); + m_nid.hIcon = nid.hIcon; + m_nTrayNumber = displayNumber; +} + +HICON TrayIcon::GetTrayIconForNumber(int nDisplay) { + if (nDisplay >= 1 && nDisplay <= kMaxDesktops) { + HICON hIcon = GetNumberIcon(nDisplay); + if (hIcon != nullptr) { + return hIcon; + } + } + return LoadIcon(m_hInstance, MAKEINTRESOURCE(kTrayDefaultIconResource)); +} + +HICON TrayIcon::GetNumberIcon(int nNumber) { + if (nNumber < 1 || nNumber > kMaxDesktops) { + return nullptr; + } + if (m_hNumberIcons[nNumber] == nullptr) { + m_hNumberIcons[nNumber] = CreateNumberIcon(nNumber, m_numberColor, *m_gdiplus); + } + return m_hNumberIcons[nNumber]; +} + +HICON TrayIcon::CreateNumberIcon(int nNumber, COLORREF color, GdiplusGuard &gdiplus) { + if (!gdiplus.Ok()) { + return nullptr; + } + + int nSize = std::clamp(GetSystemMetrics(SM_CXSMICON), 16, 64); + + Gdiplus::Bitmap bmp(nSize, nSize, PixelFormat32bppARGB); + Gdiplus::Graphics graphics(&bmp); + graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); + graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAliasGridFit); + + std::wstring szNum = std::to_wstring(nNumber); + + Gdiplus::FontFamily fontFamily(L"Segoe UI"); + Gdiplus::Font font(&fontFamily, static_cast(nSize) * 0.85f, + Gdiplus::FontStyleBold, Gdiplus::UnitPixel); + Gdiplus::SolidBrush brushText(Gdiplus::Color(255, GetRValue(color), GetGValue(color), GetBValue(color))); + Gdiplus::StringFormat format; + format.SetAlignment(Gdiplus::StringAlignmentCenter); + format.SetLineAlignment(Gdiplus::StringAlignmentCenter); + Gdiplus::RectF rectText(0.0f, 0.0f, static_cast(nSize), static_cast(nSize)); + graphics.DrawString(szNum.c_str(), -1, &font, rectText, &format, &brushText); + + HICON hIcon = nullptr; + if (bmp.GetHICON(&hIcon) != Gdiplus::Ok) { + hIcon = nullptr; + } + return hIcon; +} diff --git a/src/ui/TrayIcon.h b/src/ui/TrayIcon.h index 67c0fb2..74dcb9b 100644 --- a/src/ui/TrayIcon.h +++ b/src/ui/TrayIcon.h @@ -4,15 +4,24 @@ #include +#include #include +#include #include #include "core/IndicatorConfig.h" +#include "util/Utils.h" -constexpr UINT WM_TRAYICON = WM_USER + 2; -constexpr UINT WM_TRAY_LANG_CHINESE = WM_USER + 50; -constexpr UINT WM_TRAY_LANG_ENGLISH = WM_USER + 51; -constexpr UINT CMD_COLOR_OPTIONS_BASE = WM_USER + 100; +constexpr UINT WM_TRAYICON = WM_USER + 2; +constexpr UINT CMD_TRAY_LANG_CHINESE = WM_USER + 50; +constexpr UINT CMD_TRAY_LANG_ENGLISH = WM_USER + 51; +constexpr UINT CMD_TRAY_ICON_MODE_ICON = WM_USER + 60; +constexpr UINT CMD_TRAY_ICON_MODE_NUMBER = WM_USER + 61; +constexpr UINT CMD_COLOR_OPTIONS_BASE = WM_USER + 100; +constexpr UINT CMD_TRAY_NUMBER_COLOR_BASE = WM_USER + 500; + +constexpr int kTrayDefaultIconResource = 101; +class GdiplusGuard; class TrayIcon { public: @@ -21,13 +30,16 @@ class TrayIcon { TrayIcon(TrayIcon &&) = delete; TrayIcon &operator=(TrayIcon &&) = delete; - TrayIcon() = default; + TrayIcon(); ~TrayIcon(); bool Initialize(HWND hwnd, HINSTANCE hInstance); bool Reinitialize(); void HandleMessage(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); void UpdateTooltip(const std::wstring &tooltip); + void UpdateTrayIcon(int displayNumber); + void SetTrayIconMode(TrayIconMode mode); + void SetNumberColor(const std::wstring &hexColor); void SetActivePositionPreset(PositionPreset preset) { m_activePositionPreset = preset; } void SetEditModeCallback(std::function cb) { m_editModeFn = std::move(cb); } @@ -42,12 +54,17 @@ class TrayIcon { void SetDragSwitchModeCallback(std::function cb) { m_dragModeFn = std::move(cb); } private: - NOTIFYICONDATAW m_nid{}; - HMENU m_hMenu = nullptr; - bool m_autoStartEnabled = false; - PositionPreset m_activePositionPreset = PositionPreset::TopCenter; - int m_menuAveWidth = 6; - int m_dpi = 96; + NOTIFYICONDATAW m_nid{}; + std::unique_ptr m_gdiplus; + HINSTANCE m_hInstance = nullptr; + HMENU m_hMenu = nullptr; + bool m_autoStartEnabled = false; + PositionPreset m_activePositionPreset = PositionPreset::TopCenter; + int m_dpi = 96; + int m_nTrayNumber = -1; + TrayIconMode m_iconMode = TrayIconMode::Icon; + COLORREF m_numberColor = RGB(41, 151, 255); + std::array m_hNumberIcons{}; std::function m_colorFn; std::function m_editModeFn; @@ -60,9 +77,12 @@ class TrayIcon { std::function m_autoFocusFn; std::function m_dragModeFn; - void BuildMenu(); - void HandleCommand(WPARAM wParam); - void DrawColorSwatch(LPDRAWITEMSTRUCT dis) const; + void BuildMenu(); + void HandleCommand(WPARAM wParam); + void DrawColorSwatch(LPDRAWITEMSTRUCT dis, UINT base) const; + HICON GetNumberIcon(int nNumber); + HICON GetTrayIconForNumber(int nDisplay); + static HICON CreateNumberIcon(int nNumber, COLORREF color, GdiplusGuard &gdiplus); static void HandleRunAsAdmin(); static void HandleReset(); @@ -74,6 +94,7 @@ class TrayIcon { void HandleToggleShow(); void HandleToggleAutoStart(); void HandleShowModeCommand(int mode); + void HandleNumberColorCommand(int colorIndex); void HandlePositionCommand(PositionPreset preset); void HandleColorCommand(int index); }; diff --git a/src/util/LangData_en.inc b/src/util/LangData_en.inc index 42eced1..5bcaa72 100644 --- a/src/util/LangData_en.inc +++ b/src/util/LangData_en.inc @@ -52,6 +52,10 @@ std::pair{L"Menu.DragAlt", L"Hold Alt to Switch"}, std::pair{L"Menu.DragCtrl", L"Hold Ctrl to Switch"}, std::pair{L"Menu.DragNever", L"Never Switch"}, std::pair{L"Menu.AutoFocus", L"Cross-Monitor Focus"}, +std::pair{L"Menu.TrayIcon", L"Tray Icon"}, +std::pair{L"Menu.TrayIconIcon", L"Icon"}, +std::pair{L"Menu.TrayIconNumber", L"Desktop Number"}, +std::pair{L"Menu.TrayNumberColor", L"Number Color"}, std::pair{L"Tray.DefaultTip", L"Virtual Desktop Switcher"}, std::pair{L"Tooltip.Desktops", L"Desktops: "}, std::pair{L"Tooltip.Current", L"Current: "}, diff --git a/src/util/LangData_zh.inc b/src/util/LangData_zh.inc index 55e3540..a038944 100644 --- a/src/util/LangData_zh.inc +++ b/src/util/LangData_zh.inc @@ -52,6 +52,10 @@ std::pair{L"Menu.DragAlt", L"按住 Alt 键切换"}, std::pair{L"Menu.DragCtrl", L"按住 Ctrl 键切换"}, std::pair{L"Menu.DragNever", L"不切换"}, std::pair{L"Menu.AutoFocus", L"跨屏自动聚焦"}, +std::pair{L"Menu.TrayIcon", L"托盘图标"}, +std::pair{L"Menu.TrayIconIcon", L"图标"}, +std::pair{L"Menu.TrayIconNumber", L"桌面编号"}, +std::pair{L"Menu.TrayNumberColor", L"编号颜色"}, std::pair{L"Tray.DefaultTip", L"虚拟桌面切换器"}, std::pair{L"Tooltip.Desktops", L"桌面:"}, std::pair{L"Tooltip.Current", L"当前:"}, diff --git a/src/util/Utils.h b/src/util/Utils.h index 0556716..905d3aa 100644 --- a/src/util/Utils.h +++ b/src/util/Utils.h @@ -7,18 +7,23 @@ // --- Constants --- -constexpr size_t kMaxDesktops = 9; +constexpr int kMaxDesktops = 9; constexpr std::array kPredefinedColors = { L"#FFFFFF", L"#E3E3E5", L"#000000", - L"#FF5F5F", - L"#F6ABB7", - L"#FB7FA4", - L"#F59E0B", + L"#FF3B30", + L"#FF6B6B", + L"#FF9500", + L"#FFCC00", L"#22C55E", + L"#4ECDC4", + L"#2997FF", + L"#5856D6", L"#8771C6", + L"#FF2D55", + L"#FB7FA4", L"#FF9A9E_#FECFEF", L"#FEA5B7_#FFDE9B", L"#A8EDEA_#FED6E3", diff --git a/xmake.lua b/xmake.lua index 48c7526..75ac084 100644 --- a/xmake.lua +++ b/xmake.lua @@ -20,6 +20,6 @@ target("VirtualDesktopSwitcher") add_defines("NOMINMAX") add_cxflags("/GR-") add_ldflags("/SUBSYSTEM:WINDOWS") - add_syslinks("shell32", "user32", "gdi32", "advapi32", "comctl32", "ole32", "dwmapi", "shcore") + add_syslinks("shell32", "user32", "gdi32", "advapi32", "comctl32", "ole32", "dwmapi", "shcore", "gdiplus") add_packages("stb")