diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 18023d517..696f9da66 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -2,11 +2,13 @@ ACCEPTFILES ACCESSDENIED alignas alignof +APPLYTOSUBMENUS bitfield bitfields BUILDBRANCH BUILDMSG BUILDNUMBER +BYPOSITION charconv CLASSNOTAVAILABLE cmdletbinding @@ -78,6 +80,9 @@ llu localtime lround LSHIFT +MENUCOMMAND +MENUDATA +MENUINFO memicmp mptt mov @@ -94,6 +99,7 @@ NOCHANGEDIR NOPROGRESS NOREDIRECTIONBITMAP NOREPEAT +NOTIFYBYPOS NOTIFYICON NOTIFYICONDATA ntprivapi @@ -141,6 +147,7 @@ Stubless Subheader Subpage syscall +TASKBARCREATED TBPF THEMECHANGED tlg diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 1ee31b9e0..8ed94f9d4 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -2801,6 +2801,7 @@ xes xff XFile XFORM +xIcon XManifest XMath XMFLOAT @@ -2833,6 +2834,7 @@ YCast YCENTER YCount YDPI +yIcon yml YOffset YPosition diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index 2c736dd2f..72ca7293e 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -1201,6 +1201,16 @@ "minimum": 0, "type": [ "integer", "string" ], "deprecated": true + }, + "minimizeToTray": { + "default": "false", + "description": "When set to true, minimizing a Terminal window will no longer appear in the taskbar. Instead, a Terminal icon will appear in the system tray through which the user can access their windows.", + "type": "boolean" + }, + "alwaysShowTrayIcon": { + "default": "false", + "description": "When set to true, the Terminal's tray icon will always be shown in the system tray.", + "type": "boolean" }, "actions": { "description": "Properties are specific to each custom action.", diff --git a/src/cascadia/Remoting/Monarch.cpp b/src/cascadia/Remoting/Monarch.cpp index a5444e3d4..05f987661 100644 --- a/src/cascadia/Remoting/Monarch.cpp +++ b/src/cascadia/Remoting/Monarch.cpp @@ -28,8 +28,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation CATCH_LOG(); } - // This is a private constructor to be used in unit tests, where we don't - // want each Monarch to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Monarch::Monarch(const uint64_t testPID) : _ourPID{ testPID } { @@ -78,6 +80,9 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation peasant.IdentifyWindowsRequested({ this, &Monarch::_identifyWindows }); peasant.RenameRequested({ this, &Monarch::_renameRequested }); + peasant.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + peasant.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); + _peasants[newPeasantsId] = peasant; TraceLoggingWrite(g_hRemotingProvider, @@ -738,20 +743,30 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation try { args.FoundMatch(false); + + // If a WindowID is provided from the args, use that first. uint64_t windowId = 0; - // If no name was provided, then just summon the MRU window. - if (searchedForName.empty()) + if (args.WindowID()) { - // Use the value of the `desktop` arg to determine if we should - // limit to the current desktop (desktop:onCurrent) or not - // (desktop:any or desktop:toCurrent) - windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + windowId = args.WindowID().Value(); } else { - // Try to find a peasant that currently has this name - windowId = _lookupPeasantIdForName(searchedForName); + // If no name was provided, then just summon the MRU window. + if (searchedForName.empty()) + { + // Use the value of the `desktop` arg to determine if we should + // limit to the current desktop (desktop:onCurrent) or not + // (desktop:any or desktop:toCurrent) + windowId = _getMostRecentPeasantID(args.OnCurrentDesktop(), false); + } + else + { + // Try to find a peasant that currently has this name + windowId = _lookupPeasantIdForName(searchedForName); + } } + if (auto targetPeasant{ _getPeasant(windowId) }) { targetPeasant.Summon(args.SummonBehavior()); @@ -789,4 +804,56 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } } + + // Method Description: + // - This method creates a map of peasant IDs to peasant names + // while removing dead peasants. + // Arguments: + // - + // Return Value: + // - A map of peasant IDs to their names. + Windows::Foundation::Collections::IMapView Monarch::GetPeasantNames() + { + auto names = winrt::single_threaded_map(); + + std::vector peasantsToErase{}; + for (const auto& [id, p] : _peasants) + { + try + { + names.Insert(id, p.WindowName()); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + peasantsToErase.push_back(id); + } + } + + // Remove the dead peasants we came across while iterating. + for (const auto& id : peasantsToErase) + { + _peasants.erase(id); + _clearOldMruEntries(id); + } + + return names.GetView(); + } + + void Monarch::SummonAllWindows() + { + auto callback = [](auto&& p, auto&& /*id*/) { + SummonWindowBehavior args{}; + args.ToggleVisibility(false); + p.Summon(args); + }; + auto onError = [](auto&& id) { + TraceLoggingWrite(g_hRemotingProvider, + "Monarch_SummonAll_Failed", + TraceLoggingInt64(id, "peasantID", "The ID of the peasant which we could not summon"), + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + }; + _forAllPeasantsIgnoringTheDead(callback, onError); + } } diff --git a/src/cascadia/Remoting/Monarch.h b/src/cascadia/Remoting/Monarch.h index 9dc8fc77e..2aa30dab4 100644 --- a/src/cascadia/Remoting/Monarch.h +++ b/src/cascadia/Remoting/Monarch.h @@ -41,6 +41,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation struct Monarch : public MonarchT { Monarch(); + Monarch(const uint64_t testPID); ~Monarch(); uint64_t GetPID(); @@ -51,10 +52,14 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void HandleActivatePeasant(const winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs& args); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: - Monarch(const uint64_t testPID); uint64_t _ourPID; uint64_t _nextPeasantID{ 1 }; diff --git a/src/cascadia/Remoting/Monarch.idl b/src/cascadia/Remoting/Monarch.idl index d87d780b3..f4e8bd8b4 100644 --- a/src/cascadia/Remoting/Monarch.idl +++ b/src/cascadia/Remoting/Monarch.idl @@ -28,6 +28,7 @@ namespace Microsoft.Terminal.Remoting Boolean FoundMatch; SummonWindowBehavior SummonBehavior; + Windows.Foundation.IReference WindowID; } @@ -40,6 +41,11 @@ namespace Microsoft.Terminal.Remoting void HandleActivatePeasant(WindowActivatedArgs args); void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + Windows.Foundation.Collections.IMapView GetPeasantNames { get; }; + event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/Remoting/Peasant.cpp b/src/cascadia/Remoting/Peasant.cpp index 58a71acba..a9787f597 100644 --- a/src/cascadia/Remoting/Peasant.cpp +++ b/src/cascadia/Remoting/Peasant.cpp @@ -20,8 +20,10 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { } - // This is a private constructor to be used in unit tests, where we don't - // want each Peasant to necessarily use the current PID. + // This constructor is intended to be used in unit tests, + // but we need to make it public in order to use make_self + // in the tests. It's not exposed through the idl though + // so it's not _truly_ fully public which should be acceptable. Peasant::Peasant(const uint64_t testPID) : _ourPID{ testPID } { @@ -31,6 +33,7 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation { _id = id; } + uint64_t Peasant::GetID() { return _id; @@ -222,4 +225,36 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), TraceLoggingKeyword(TIL_KEYWORD_TRACE)); } + + void Peasant::RequestShowTrayIcon() + { + try + { + _ShowTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestShowTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } + + void Peasant::RequestHideTrayIcon() + { + try + { + _HideTrayIconRequestedHandlers(*this, nullptr); + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + } + TraceLoggingWrite(g_hRemotingProvider, + "Peasant_RequestHideTrayIcon", + TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE), + TraceLoggingKeyword(TIL_KEYWORD_TRACE)); + } } diff --git a/src/cascadia/Remoting/Peasant.h b/src/cascadia/Remoting/Peasant.h index 7b5b44e81..6093cd835 100644 --- a/src/cascadia/Remoting/Peasant.h +++ b/src/cascadia/Remoting/Peasant.h @@ -28,6 +28,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation void RequestIdentifyWindows(); void DisplayWindowId(); void RequestRename(const winrt::Microsoft::Terminal::Remoting::RenameRequestArgs& args); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); winrt::Microsoft::Terminal::Remoting::WindowActivatedArgs GetLastActivatedArgs(); @@ -40,6 +42,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: Peasant(const uint64_t testPID); diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl index bc36e3f30..454d748cf 100644 --- a/src/cascadia/Remoting/Peasant.idl +++ b/src/cascadia/Remoting/Peasant.idl @@ -64,6 +64,8 @@ namespace Microsoft.Terminal.Remoting void RequestIdentifyWindows(); // Tells us to raise a IdentifyWindowsRequested void RequestRename(RenameRequestArgs args); // Tells us to raise a RenameRequested void Summon(SummonWindowBehavior behavior); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); event Windows.Foundation.TypedEventHandler WindowActivated; event Windows.Foundation.TypedEventHandler ExecuteCommandlineRequested; @@ -71,6 +73,8 @@ namespace Microsoft.Terminal.Remoting event Windows.Foundation.TypedEventHandler DisplayWindowIdRequested; event Windows.Foundation.TypedEventHandler RenameRequested; event Windows.Foundation.TypedEventHandler SummonRequested; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; [default_interface] runtimeclass Peasant : IPeasant diff --git a/src/cascadia/Remoting/SummonWindowSelectionArgs.h b/src/cascadia/Remoting/SummonWindowSelectionArgs.h index 2696925ef..efbf72a3b 100644 --- a/src/cascadia/Remoting/SummonWindowSelectionArgs.h +++ b/src/cascadia/Remoting/SummonWindowSelectionArgs.h @@ -34,6 +34,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation WINRT_PROPERTY(bool, FoundMatch, false); WINRT_PROPERTY(bool, OnCurrentDesktop, false); WINRT_PROPERTY(SummonWindowBehavior, SummonBehavior); + + WINRT_PROPERTY(Windows::Foundation::IReference, WindowID); }; } diff --git a/src/cascadia/Remoting/WindowManager.cpp b/src/cascadia/Remoting/WindowManager.cpp index 88e0adbf0..551bd0930 100644 --- a/src/cascadia/Remoting/WindowManager.cpp +++ b/src/cascadia/Remoting/WindowManager.cpp @@ -254,6 +254,8 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation // window, and when the current monarch dies. _monarch.FindTargetWindowRequested({ this, &WindowManager::_raiseFindTargetWindowRequested }); + _monarch.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequestedHandlers(*this, nullptr); }); + _monarch.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequestedHandlers(*this, nullptr); }); _BecameMonarchHandlers(*this, nullptr); } @@ -509,4 +511,54 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation _monarch.SummonWindow(args); } + void WindowManager::SummonAllWindows() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + _monarch.SummonAllWindows(); + } + } + + Windows::Foundation::Collections::IMapView WindowManager::GetPeasantNames() + { + // We should only get called when we're the monarch since the monarch + // is the only one that knows about all peasants. + return _monarch.GetPeasantNames(); + } + + // Method Description: + // - Ask the monarch to show a tray icon. + // Arguments: + // - + // Return Value: + // - + void WindowManager::RequestShowTrayIcon() + { + _peasant.RequestShowTrayIcon(); + } + + // Method Description: + // - Ask the monarch to hide its tray icon. + // Arguments: + // - + // Return Value: + // - + void WindowManager::RequestHideTrayIcon() + { + _peasant.RequestHideTrayIcon(); + } + + bool WindowManager::DoesQuakeWindowExist() + { + const auto names = GetPeasantNames(); + for (const auto [id, name] : names) + { + if (name == QuakeWindowName) + { + return true; + } + } + return false; + } + } diff --git a/src/cascadia/Remoting/WindowManager.h b/src/cascadia/Remoting/WindowManager.h index 0b87075f9..8169f3737 100644 --- a/src/cascadia/Remoting/WindowManager.h +++ b/src/cascadia/Remoting/WindowManager.h @@ -40,8 +40,17 @@ namespace winrt::Microsoft::Terminal::Remoting::implementation bool IsMonarch(); void SummonWindow(const Remoting::SummonWindowSelectionArgs& args); + void SummonAllWindows(); + Windows::Foundation::Collections::IMapView GetPeasantNames(); + + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); + bool DoesQuakeWindowExist(); + TYPED_EVENT(FindTargetWindowRequested, winrt::Windows::Foundation::IInspectable, winrt::Microsoft::Terminal::Remoting::FindTargetWindowArgs); TYPED_EVENT(BecameMonarch, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); private: bool _shouldCreateWindow{ false }; diff --git a/src/cascadia/Remoting/WindowManager.idl b/src/cascadia/Remoting/WindowManager.idl index 547b96f9a..ca1f9f747 100644 --- a/src/cascadia/Remoting/WindowManager.idl +++ b/src/cascadia/Remoting/WindowManager.idl @@ -12,7 +12,14 @@ namespace Microsoft.Terminal.Remoting IPeasant CurrentWindow(); Boolean IsMonarch { get; }; void SummonWindow(SummonWindowSelectionArgs args); + void SummonAllWindows(); + void RequestShowTrayIcon(); + void RequestHideTrayIcon(); + Boolean DoesQuakeWindowExist(); + Windows.Foundation.Collections.IMapView GetPeasantNames(); event Windows.Foundation.TypedEventHandler FindTargetWindowRequested; event Windows.Foundation.TypedEventHandler BecameMonarch; + event Windows.Foundation.TypedEventHandler ShowTrayIconRequested; + event Windows.Foundation.TypedEventHandler HideTrayIconRequested; }; } diff --git a/src/cascadia/TerminalApp/AppLogic.cpp b/src/cascadia/TerminalApp/AppLogic.cpp index 420630774..106da0fbd 100644 --- a/src/cascadia/TerminalApp/AppLogic.cpp +++ b/src/cascadia/TerminalApp/AppLogic.cpp @@ -1442,4 +1442,39 @@ namespace winrt::TerminalApp::implementation return _root->IsQuakeWindow(); } + bool AppLogic::GetMinimizeToTray() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().MinimizeToTray(); + } + else + { + return false; + } + } + + bool AppLogic::GetAlwaysShowTrayIcon() + { + if constexpr (Feature_TrayIcon::IsEnabled()) + { + if (!_loadedInitialSettings) + { + // Load settings if we haven't already + LoadSettings(); + } + + return _settings.GlobalSettings().AlwaysShowTrayIcon(); + } + else + { + return false; + } + } } diff --git a/src/cascadia/TerminalApp/AppLogic.h b/src/cascadia/TerminalApp/AppLogic.h index 4131d28f1..dbaf301c3 100644 --- a/src/cascadia/TerminalApp/AppLogic.h +++ b/src/cascadia/TerminalApp/AppLogic.h @@ -92,6 +92,9 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::TaskbarState TaskbarState(); + bool GetMinimizeToTray(); + bool GetAlwaysShowTrayIcon(); + winrt::Windows::Foundation::IAsyncOperation ShowDialog(winrt::Windows::UI::Xaml::Controls::ContentDialog dialog); Windows::Foundation::Collections::IMapView GlobalHotkeys(); diff --git a/src/cascadia/TerminalApp/AppLogic.idl b/src/cascadia/TerminalApp/AppLogic.idl index 0b8891038..1f7675ad4 100644 --- a/src/cascadia/TerminalApp/AppLogic.idl +++ b/src/cascadia/TerminalApp/AppLogic.idl @@ -70,6 +70,9 @@ namespace TerminalApp TaskbarState TaskbarState{ get; }; + Boolean GetMinimizeToTray(); + Boolean GetAlwaysShowTrayIcon(); + FindTargetWindowResult FindTargetWindow(String[] args); Windows.Foundation.Collections.IMapView GlobalHotkeys(); diff --git a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw index a64a239df..f9e8cbf12 100644 --- a/src/cascadia/TerminalApp/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/en-US/Resources.resw @@ -653,6 +653,14 @@ Command Palette + + Focus Terminal + This is displayed as a label for the context menu item that focuses the terminal. + + + Windows + This is displayed as a label for the context menu item that holds the submenu of available windows. + Open in Windows Terminal (Dev) {Locked} The dev build will never be seen in multiple languages diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp index 466031b05..aeca8499d 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.cpp @@ -48,6 +48,8 @@ static constexpr std::string_view StartupActionsKey{ "startupActions" }; static constexpr std::string_view FocusFollowMouseKey{ "focusFollowMouse" }; static constexpr std::string_view WindowingBehaviorKey{ "windowingBehavior" }; static constexpr std::string_view TrimBlockSelectionKey{ "trimBlockSelection" }; +static constexpr std::string_view AlwaysShowTrayIconKey{ "alwaysShowTrayIcon" }; +static constexpr std::string_view MinimizeToTrayKey{ "minimizeToTray" }; static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" }; @@ -129,6 +131,8 @@ winrt::com_ptr GlobalAppSettings::Copy() const globals->_WindowingBehavior = _WindowingBehavior; globals->_TrimBlockSelection = _TrimBlockSelection; globals->_DetectURLs = _DetectURLs; + globals->_MinimizeToTray = _MinimizeToTray; + globals->_AlwaysShowTrayIcon = _AlwaysShowTrayIcon; globals->_UnparsedDefaultProfile = _UnparsedDefaultProfile; globals->_validDefaultProfile = _validDefaultProfile; @@ -319,6 +323,10 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) JsonUtils::GetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::GetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + + JsonUtils::GetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); + // This is a helper lambda to get the keybindings and commands out of both // and array of objects. We'll use this twice, once on the legacy // `keybindings` key, and again on the newer `bindings` key. @@ -414,6 +422,8 @@ Json::Value GlobalAppSettings::ToJson() const JsonUtils::SetValueForKey(json, WindowingBehaviorKey, _WindowingBehavior); JsonUtils::SetValueForKey(json, TrimBlockSelectionKey, _TrimBlockSelection); JsonUtils::SetValueForKey(json, DetectURLsKey, _DetectURLs); + JsonUtils::SetValueForKey(json, MinimizeToTrayKey, _MinimizeToTray); + JsonUtils::SetValueForKey(json, AlwaysShowTrayIconKey, _AlwaysShowTrayIcon); // clang-format on json[JsonKey(ActionsKey)] = _actionMap->ToJson(); diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h index 7fa8c7878..9a1536a10 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.h @@ -98,6 +98,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation INHERITABLE_SETTING(Model::GlobalAppSettings, Model::WindowingMode, WindowingBehavior, Model::WindowingMode::UseNew); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, TrimBlockSelection, false); INHERITABLE_SETTING(Model::GlobalAppSettings, bool, DetectURLs, true); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, MinimizeToTray, false); + INHERITABLE_SETTING(Model::GlobalAppSettings, bool, AlwaysShowTrayIcon, false); private: guid _defaultProfile; diff --git a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl index 1fac7f66b..e9d1fccad 100644 --- a/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl +++ b/src/cascadia/TerminalSettingsModel/GlobalAppSettings.idl @@ -73,6 +73,8 @@ namespace Microsoft.Terminal.Settings.Model INHERITABLE_SETTING(WindowingMode, WindowingBehavior); INHERITABLE_SETTING(Boolean, TrimBlockSelection); INHERITABLE_SETTING(Boolean, DetectURLs); + INHERITABLE_SETTING(Boolean, MinimizeToTray); + INHERITABLE_SETTING(Boolean, AlwaysShowTrayIcon); Windows.Foundation.Collections.IMapView ColorSchemes(); void AddColorScheme(ColorScheme scheme); diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw index 4ea614c25..7f30440e6 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw @@ -436,4 +436,7 @@ Windows Console Host Name describing the usage of the classic windows console as the terminal UI. (`conhost.exe`) + + Minimize current window to tray + diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json index 8cfb25cd3..c87895d9b 100644 --- a/src/cascadia/TerminalSettingsModel/defaults.json +++ b/src/cascadia/TerminalSettingsModel/defaults.json @@ -29,6 +29,8 @@ "disableAnimations": false, "startupActions": "", "focusFollowMouse": false, + "minimizeToTray": false, + "alwaysShowTrayIcon": false, "profiles": [ diff --git a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp index 28b502927..d7d21b0db 100644 --- a/src/cascadia/UnitTests_Remoting/RemotingTests.cpp +++ b/src/cascadia/UnitTests_Remoting/RemotingTests.cpp @@ -71,12 +71,16 @@ namespace RemotingUnitTests Remoting::WindowActivatedArgs GetLastActivatedArgs() { throw winrt::hresult_error{}; } void RequestRename(const Remoting::RenameRequestArgs& /*args*/) { throw winrt::hresult_error{}; } void Summon(const Remoting::SummonWindowBehavior& /*args*/) { throw winrt::hresult_error{}; }; + void RequestShowTrayIcon() { throw winrt::hresult_error{}; }; + void RequestHideTrayIcon() { throw winrt::hresult_error{}; }; TYPED_EVENT(WindowActivated, winrt::Windows::Foundation::IInspectable, Remoting::WindowActivatedArgs); TYPED_EVENT(ExecuteCommandlineRequested, winrt::Windows::Foundation::IInspectable, Remoting::CommandlineArgs); TYPED_EVENT(IdentifyWindowsRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(DisplayWindowIdRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); TYPED_EVENT(RenameRequested, winrt::Windows::Foundation::IInspectable, Remoting::RenameRequestArgs); TYPED_EVENT(SummonRequested, winrt::Windows::Foundation::IInspectable, Remoting::SummonWindowBehavior); + TYPED_EVENT(ShowTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); + TYPED_EVENT(HideTrayIconRequested, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable); }; class RemotingTests diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index a89cc3756..356d1d5c4 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -11,8 +11,6 @@ #include "VirtualDesktopUtils.h" #include "icon.h" -#include - using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Composition; using namespace winrt::Windows::UI::Xaml; @@ -65,6 +63,8 @@ AppHost::AppHost() noexcept : // Update our own internal state tracking if we're in quake mode or not. _IsQuakeWindowChanged(nullptr, nullptr); + _window->SetMinimizeToTrayBehavior(_logic.GetMinimizeToTray()); + // Tell the window to callback to us when it's about to handle a WM_CREATE auto pfn = std::bind(&AppHost::_HandleCreateWindow, this, @@ -80,15 +80,9 @@ AppHost::AppHost() noexcept : _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _window->WindowActivated({ this, &AppHost::_WindowActivated }); _window->HotkeyPressed({ this, &AppHost::_GlobalHotkeyPressed }); - _window->NotifyTrayIconPressed({ this, &AppHost::_HandleTrayIconPressed }); _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); - if (_window->IsQuakeWindow()) - { - _UpdateTrayIcon(); - } - _windowManager.BecameMonarch({ this, &AppHost::_BecomeMonarch }); if (_windowManager.IsMonarch()) { @@ -98,13 +92,12 @@ AppHost::AppHost() noexcept : AppHost::~AppHost() { - // destruction order is important for proper teardown here - if (_trayIconData) + if (_window->IsQuakeWindow()) { - Shell_NotifyIcon(NIM_DELETE, &_trayIconData.value()); - _trayIconData.reset(); + _windowManager.RequestHideTrayIcon(); } + // destruction order is important for proper teardown here _window = nullptr; _app.Close(); _app = nullptr; @@ -662,6 +655,20 @@ void AppHost::_BecomeMonarch(const winrt::Windows::Foundation::IInspectable& /*s const winrt::Windows::Foundation::IInspectable& /*args*/) { _setupGlobalHotkeys(); + + // The monarch is just going to be THE listener for inbound connections. + _listenForInboundConnections(); + + if (_windowManager.DoesQuakeWindowExist() || + _window->IsQuakeWindow() || + (_logic.GetAlwaysShowTrayIcon() || _logic.GetMinimizeToTray())) + { + _CreateTrayIcon(); + } + + // These events are coming from peasants that become or un-become quake windows. + _windowManager.ShowTrayIconRequested([this](auto&&, auto&&) { _ShowTrayIconRequested(); }); + _windowManager.HideTrayIconRequested([this](auto&&, auto&&) { _HideTrayIconRequested(); }); } void AppHost::_listenForInboundConnections() @@ -947,24 +954,50 @@ void AppHost::_HandleSettingsChanged(const winrt::Windows::Foundation::IInspecta const winrt::Windows::Foundation::IInspectable& /*args*/) { _setupGlobalHotkeys(); + + // If we're monarch, we need to check some conditions to show the tray icon. + // If there's a Quake window somewhere, we'll want to keep the tray icon. + // There's two settings - MinimizeToTray and AlwaysShowTrayIcon. If either + // one of them are true, we want to make sure there's a tray icon. + // If both are false, we want to remove our icon from the tray. + // When we remove our icon from the tray, we'll also want to re-summon + // any hidden windows, but right now we're not keeping track of who's hidden, + // so just summon them all. Tracking the work to do a "summon all minimized" in + // GH#10448 + if (_windowManager.IsMonarch()) + { + if (!_windowManager.DoesQuakeWindowExist()) + { + if (!_trayIcon && (_logic.GetMinimizeToTray() || _logic.GetAlwaysShowTrayIcon())) + { + _CreateTrayIcon(); + } + else if (_trayIcon && !_logic.GetMinimizeToTray() && !_logic.GetAlwaysShowTrayIcon()) + { + _windowManager.SummonAllWindows(); + _DestroyTrayIcon(); + } + } + } + + _window->SetMinimizeToTrayBehavior(_logic.GetMinimizeToTray()); } void AppHost::_IsQuakeWindowChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { - if (_window->IsQuakeWindow() && !_logic.IsQuakeWindow()) + // We want the quake window to be accessible through the tray icon. + // This means if there's a quake window _somewhere_, we want the tray icon + // to show regardless of the tray icon settings. + // This also means we'll need to destroy the tray icon if it was created + // specifically for the quake window. If not, it should not be destroyed. + if (!_window->IsQuakeWindow() && _logic.IsQuakeWindow()) { - // If we're exiting quake mode, we should make our - // tray icon disappear. - if (_trayIconData) - { - Shell_NotifyIcon(NIM_DELETE, &_trayIconData.value()); - _trayIconData.reset(); - } + _ShowTrayIconRequested(); } - else if (!_window->IsQuakeWindow() && _logic.IsQuakeWindow()) + else if (_window->IsQuakeWindow() && !_logic.IsQuakeWindow()) { - _UpdateTrayIcon(); + _HideTrayIconRequested(); } _window->IsQuakeWindow(_logic.IsQuakeWindow()); @@ -981,55 +1014,84 @@ void AppHost::_SummonWindowRequested(const winrt::Windows::Foundation::IInspecta _HandleSummon(sender, summonArgs); } -void AppHost::_HandleTrayIconPressed() +// Method Description: +// - Creates a Tray Icon and hooks up its handlers +// Arguments: +// - +// Return Value: +// - +void AppHost::_CreateTrayIcon() { - // Currently scoping "minimize to tray" to only - // the quake window. - if (_logic.IsQuakeWindow()) + if constexpr (Feature_TrayIcon::IsEnabled()) { - const Remoting::SummonWindowBehavior summonArgs{}; - summonArgs.DropdownDuration(200); - _window->SummonWindow(summonArgs); + _trayIcon = std::make_unique(_window->GetHandle()); + + // Hookup the handlers, save the tokens for revoking if settings change. + _ReAddTrayIconToken = _window->NotifyReAddTrayIcon([this]() { _trayIcon->ReAddTrayIcon(); }); + _TrayIconPressedToken = _window->NotifyTrayIconPressed([this]() { _trayIcon->TrayIconPressed(); }); + _ShowTrayContextMenuToken = _window->NotifyShowTrayContextMenu([this](til::point coord) { _trayIcon->ShowTrayContextMenu(coord, _windowManager.GetPeasantNames()); }); + _TrayMenuItemSelectedToken = _window->NotifyTrayMenuItemSelected([this](HMENU hm, UINT idx) { _trayIcon->TrayMenuItemSelected(hm, idx); }); + _trayIcon->SummonWindowRequested([this](auto& args) { _windowManager.SummonWindow(args); }); } } // Method Description: -// - Creates and adds an icon to the notification tray. +// - Deletes our tray icon if we have one. // Arguments: -// - +// - // Return Value: // - -void AppHost::_UpdateTrayIcon() +void AppHost::_DestroyTrayIcon() { - if (!_trayIconData && _window->GetHandle()) + if constexpr (Feature_TrayIcon::IsEnabled()) { - NOTIFYICONDATA nid{}; + _window->NotifyReAddTrayIcon(_ReAddTrayIconToken); + _window->NotifyTrayIconPressed(_TrayIconPressedToken); + _window->NotifyShowTrayContextMenu(_ShowTrayContextMenuToken); + _window->NotifyTrayMenuItemSelected(_TrayMenuItemSelectedToken); - // This HWND will receive the callbacks sent by the tray icon. - nid.hWnd = _window->GetHandle(); - - // App-defined identifier of the icon. The HWND and ID are used - // to identify which icon to operate on when calling Shell_NotifyIcon. - // Multiple icons can be associated with one HWND, but here we're only - // going to be showing one so the ID doesn't really matter. - nid.uID = 1; - - nid.uCallbackMessage = CM_NOTIFY_FROM_TRAY; - - ScopedResourceLoader cascadiaLoader{ L"Resources" }; - - nid.hIcon = static_cast(GetActiveAppIconHandle(ICON_SMALL)); - StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), cascadiaLoader.GetLocalizedString(L"AppName").c_str()); - nid.uFlags = NIF_MESSAGE | NIF_SHOWTIP | NIF_TIP | NIF_ICON; - Shell_NotifyIcon(NIM_ADD, &nid); - - // For whatever reason, the NIM_ADD call doesn't seem to set the version - // properly, resulting in us being unable to receive the expected notification - // events. We actually have to make a separate NIM_SETVERSION call for it to - // work properly. - nid.uVersion = NOTIFYICON_VERSION_4; - Shell_NotifyIcon(NIM_SETVERSION, &nid); - - _trayIconData = nid; + _trayIcon->RemoveIconFromTray(); + _trayIcon = nullptr; + } +} + +winrt::fire_and_forget AppHost::_ShowTrayIconRequested() +{ + if constexpr (Feature_TrayIcon::IsEnabled()) + { + co_await winrt::resume_background(); + if (_windowManager.IsMonarch()) + { + if (!_trayIcon) + { + _CreateTrayIcon(); + } + } + else + { + _windowManager.RequestShowTrayIcon(); + } + } +} + +winrt::fire_and_forget AppHost::_HideTrayIconRequested() +{ + if constexpr (Feature_TrayIcon::IsEnabled()) + { + co_await winrt::resume_background(); + if (_windowManager.IsMonarch()) + { + // Destroy it only if our settings allow it + if (_trayIcon && + !_logic.GetAlwaysShowTrayIcon() && + !_logic.GetMinimizeToTray()) + { + _DestroyTrayIcon(); + } + } + else + { + _windowManager.RequestHideTrayIcon(); + } } } diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 5f6d10ca5..89a4c96d9 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -2,8 +2,8 @@ // Licensed under the MIT license. #include "pch.h" - #include "NonClientIslandWindow.h" +#include "TrayIcon.h" class AppHost { @@ -84,8 +84,13 @@ private: void _SummonWindowRequested(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - void _UpdateTrayIcon(); - void _HandleTrayIconPressed(); - - std::optional _trayIconData; + void _CreateTrayIcon(); + void _DestroyTrayIcon(); + winrt::fire_and_forget _ShowTrayIconRequested(); + winrt::fire_and_forget _HideTrayIconRequested(); + std::unique_ptr _trayIcon; + winrt::event_token _ReAddTrayIconToken; + winrt::event_token _TrayIconPressedToken; + winrt::event_token _ShowTrayContextMenuToken; + winrt::event_token _TrayMenuItemSelectedToken; }; diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index 731dfa168..616f80036 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -6,6 +6,7 @@ #include "../types/inc/Viewport.hpp" #include "resource.h" #include "icon.h" +#include "TrayIcon.h" extern "C" IMAGE_DOS_HEADER __ImageBase; @@ -22,6 +23,8 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers; #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS" +const UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); + IslandWindow::IslandWindow() noexcept : _interopWindowHandle{ nullptr }, _rootGrid{ nullptr }, @@ -453,7 +456,6 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize { if (wparam == SIZE_MINIMIZED && _isQuakeWindow) { - _NotifyWindowHiddenHandlers(); ShowWindow(GetHandle(), SW_HIDE); return 0; } @@ -573,9 +575,30 @@ long IslandWindow::_calculateTotalSize(const bool isWidth, const long clientSize _NotifyTrayIconPressedHandlers(); return 0; } + case WM_CONTEXTMENU: + { + const til::point eventPoint{ GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) }; + _NotifyShowTrayContextMenuHandlers(eventPoint); + return 0; + } } break; } + case WM_MENUCOMMAND: + { + _NotifyTrayMenuItemSelectedHandlers((HMENU)lparam, (UINT)wparam); + return 0; + } + default: + // We'll want to receive this message when explorer.exe restarts + // so that we can re-add our icon to the tray. + // This unfortunately isn't a switch case because we register the + // message at runtime. + if (message == WM_TASKBARCREATED) + { + _NotifyReAddTrayIconHandlers(); + return 0; + } } // TODO: handle messages here... @@ -600,6 +623,10 @@ void IslandWindow::OnResize(const UINT width, const UINT height) void IslandWindow::OnMinimize() { // TODO GH#1989 Stop rendering island content when the app is minimized. + if (_minimizeToTray) + { + HideWindow(); + } } // Method Description: @@ -1603,5 +1630,15 @@ til::rectangle IslandWindow::_getQuakeModeSize(HMONITOR hmon) return til::rectangle{ origin, dimensions }; } +void IslandWindow::HideWindow() +{ + ShowWindow(GetHandle(), SW_HIDE); +} + +void IslandWindow::SetMinimizeToTrayBehavior(bool minimizeToTray) noexcept +{ + _minimizeToTray = minimizeToTray; +} + DEFINE_EVENT(IslandWindow, DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>); DEFINE_EVENT(IslandWindow, WindowCloseButtonClicked, _windowCloseButtonClickedHandler, winrt::delegate<>); diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index bea360862..1f78c8e1a 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -47,13 +47,19 @@ public: bool IsQuakeWindow() const noexcept; void IsQuakeWindow(bool isQuakeWindow) noexcept; + void HideWindow(); + + void SetMinimizeToTrayBehavior(bool minimizeToTray) noexcept; + DECLARE_EVENT(DragRegionClicked, _DragRegionClickedHandlers, winrt::delegate<>); DECLARE_EVENT(WindowCloseButtonClicked, _windowCloseButtonClickedHandler, winrt::delegate<>); WINRT_CALLBACK(MouseScrolled, winrt::delegate); WINRT_CALLBACK(WindowActivated, winrt::delegate); WINRT_CALLBACK(HotkeyPressed, winrt::delegate); WINRT_CALLBACK(NotifyTrayIconPressed, winrt::delegate); - WINRT_CALLBACK(NotifyWindowHidden, winrt::delegate); + WINRT_CALLBACK(NotifyShowTrayContextMenu, winrt::delegate); + WINRT_CALLBACK(NotifyTrayMenuItemSelected, winrt::delegate); + WINRT_CALLBACK(NotifyReAddTrayIcon, winrt::delegate); protected: void ForceResize() @@ -116,6 +122,8 @@ protected: void _summonWindowRoutineBody(winrt::Microsoft::Terminal::Remoting::SummonWindowBehavior args); + bool _minimizeToTray{ false }; + private: // This minimum width allows for width the tabs fit static constexpr long minimumWidth = 460L; diff --git a/src/cascadia/WindowsTerminal/TrayIcon.cpp b/src/cascadia/WindowsTerminal/TrayIcon.cpp new file mode 100644 index 000000000..83bc04343 --- /dev/null +++ b/src/cascadia/WindowsTerminal/TrayIcon.cpp @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "icon.h" +#include "TrayIcon.h" +#include "CustomWindowMessages.h" + +#include +#include + +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Microsoft::Terminal; + +TrayIcon::TrayIcon(const HWND owningHwnd) : + _owningHwnd{ owningHwnd } +{ + CreateTrayIcon(); +} + +TrayIcon::~TrayIcon() +{ + RemoveIconFromTray(); +} + +// Method Description: +// - Creates and adds an icon to the notification tray. +// If an icon already exists, update the HWND associated +// to the icon with this window's HWND. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::CreateTrayIcon() +{ + NOTIFYICONDATA nid{}; + nid.cbSize = sizeof(NOTIFYICONDATA); + + // This HWND will receive the callbacks sent by the tray icon. + nid.hWnd = _owningHwnd; + + // App-defined identifier of the icon. The HWND and ID are used + // to identify which icon to operate on when calling Shell_NotifyIcon. + // Multiple icons can be associated with one HWND, but here we're only + // going to be showing one so the ID doesn't really matter. + nid.uID = 1; + + nid.uCallbackMessage = CM_NOTIFY_FROM_TRAY; + + // AppName happens to be in CascadiaPackage's Resources. + ScopedResourceLoader loader{ L"Resources" }; + const auto appNameLoc = loader.GetLocalizedString(L"AppName"); + + nid.hIcon = static_cast(GetActiveAppIconHandle(true)); + StringCchCopy(nid.szTip, ARRAYSIZE(nid.szTip), appNameLoc.c_str()); + nid.uFlags = NIF_MESSAGE | NIF_SHOWTIP | NIF_TIP | NIF_ICON; + Shell_NotifyIcon(NIM_ADD, &nid); + + // For whatever reason, the NIM_ADD call doesn't seem to set the version + // properly, resulting in us being unable to receive the expected notification + // events. We actually have to make a separate NIM_SETVERSION call for it to + // work properly. + nid.uVersion = NOTIFYICON_VERSION_4; + Shell_NotifyIcon(NIM_SETVERSION, &nid); + + _trayIconData = nid; +} + +// Method Description: +// - This creates our context menu and displays it at the given +// screen coordinates. +// Arguments: +// - coord: The coordinates where we should be showing the context menu. +// - peasants: The map of all peasants that should be available in the context menu. +// Return Value: +// - +void TrayIcon::ShowTrayContextMenu(const til::point coord, + IMapView peasants) +{ + if (const auto hMenu = _CreateTrayContextMenu(peasants)) + { + // We'll need to set our window to the foreground before calling + // TrackPopupMenuEx or else the menu won't dismiss when clicking away. + SetForegroundWindow(_owningHwnd); + + // User can select menu items with the left and right buttons. + UINT uFlags = TPM_RIGHTBUTTON; + + // Nonzero if drop-down menus are right-aligned with the corresponding menu-bar item + // 0 if the menus are left-aligned. + if (GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0) + { + uFlags |= TPM_RIGHTALIGN; + } + else + { + uFlags |= TPM_LEFTALIGN; + } + + TrackPopupMenuEx(hMenu, uFlags, gsl::narrow_cast(coord.x()), gsl::narrow_cast(coord.y()), _owningHwnd, NULL); + } +} + +// Method Description: +// - This creates the context menu for our tray icon. +// Arguments: +// - peasants: A map of all peasants' ID to their window name. +// Return Value: +// - The handle to the newly created context menu. +HMENU TrayIcon::_CreateTrayContextMenu(IMapView peasants) +{ + auto hMenu = CreatePopupMenu(); + if (hMenu) + { + MENUINFO mi{}; + mi.cbSize = sizeof(MENUINFO); + mi.fMask = MIM_STYLE | MIM_APPLYTOSUBMENUS | MIM_MENUDATA; + mi.dwStyle = MNS_NOTIFYBYPOS; + mi.dwMenuData = NULL; + SetMenuInfo(hMenu, &mi); + + // Focus Current Terminal Window + AppendMenu(hMenu, MF_STRING, gsl::narrow(TrayMenuItemAction::FocusTerminal), RS_(L"TrayIconFocusTerminal").c_str()); + AppendMenu(hMenu, MF_SEPARATOR, 0, L""); + + // Submenu for Windows + if (auto submenu = CreatePopupMenu()) + { + const auto locWindow = RS_(L"WindowIdLabel"); + const auto locUnnamed = RS_(L"UnnamedWindowName"); + for (const auto [id, name] : peasants) + { + winrt::hstring displayText = name; + if (name.empty()) + { + displayText = fmt::format(L"{} {} - <{}>", locWindow, id, locUnnamed); + } + + AppendMenu(submenu, MF_STRING, gsl::narrow(id), displayText.c_str()); + } + + MENUINFO submenuInfo{}; + submenuInfo.cbSize = sizeof(MENUINFO); + submenuInfo.fMask = MIM_MENUDATA; + submenuInfo.dwStyle = MNS_NOTIFYBYPOS; + submenuInfo.dwMenuData = (UINT_PTR)TrayMenuItemAction::SummonWindow; + SetMenuInfo(submenu, &submenuInfo); + + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)submenu, RS_(L"TrayIconWindowSubmenu").c_str()); + } + } + return hMenu; +} + +// Method Description: +// - This is the handler for when one of the menu items are selected within +// the tray icon's context menu. +// Arguments: +// - menu: The handle to the menu that holds the menu item that was selected. +// - menuItemIndex: The index of the menu item within the given menu. +// Return Value: +// - +void TrayIcon::TrayMenuItemSelected(const HMENU menu, const UINT menuItemIndex) +{ + // Check the menu's data for a specific action. + MENUINFO mi{}; + mi.cbSize = sizeof(MENUINFO); + mi.fMask = MIM_MENUDATA; + GetMenuInfo(menu, &mi); + if (mi.dwMenuData) + { + if (gsl::narrow(mi.dwMenuData) == TrayMenuItemAction::SummonWindow) + { + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.WindowID(GetMenuItemID(menu, menuItemIndex)); + args.SummonBehavior().ToggleVisibility(false); + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + _SummonWindowRequestedHandlers(args); + return; + } + } + + // Now check the menu item itself for an action. + const auto action = gsl::narrow(GetMenuItemID(menu, menuItemIndex)); + switch (action) + { + case TrayMenuItemAction::FocusTerminal: + { + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.SummonBehavior().ToggleVisibility(false); + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + _SummonWindowRequestedHandlers(args); + break; + } + } +} + +// Method Description: +// - This is the handler for when the tray icon itself is left-clicked. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::TrayIconPressed() +{ + // No name in the args means summon the mru window. + winrt::Microsoft::Terminal::Remoting::SummonWindowSelectionArgs args{}; + args.SummonBehavior().MoveToCurrentDesktop(false); + args.SummonBehavior().ToMonitor(Remoting::MonitorBehavior::InPlace); + args.SummonBehavior().ToggleVisibility(false); + _SummonWindowRequestedHandlers(args); +} + +// Method Description: +// - Re-add a tray icon using our currently saved tray icon data. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::ReAddTrayIcon() +{ + Shell_NotifyIcon(NIM_ADD, &_trayIconData); + Shell_NotifyIcon(NIM_SETVERSION, &_trayIconData); +} + +// Method Description: +// - Deletes our tray icon. +// Arguments: +// - +// Return Value: +// - +void TrayIcon::RemoveIconFromTray() +{ + Shell_NotifyIcon(NIM_DELETE, &_trayIconData); +} diff --git a/src/cascadia/WindowsTerminal/TrayIcon.h b/src/cascadia/WindowsTerminal/TrayIcon.h new file mode 100644 index 000000000..aa58d17f1 --- /dev/null +++ b/src/cascadia/WindowsTerminal/TrayIcon.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "../cascadia/inc/cppwinrt_utils.h" + +// This enumerates all the possible actions +// that our tray icon context menu could do. +enum class TrayMenuItemAction +{ + FocusTerminal, // Focus the MRU terminal. + SummonWindow +}; + +class TrayIcon +{ +public: + TrayIcon() = delete; + TrayIcon(const HWND owningHwnd); + ~TrayIcon(); + + void CreateTrayIcon(); + void RemoveIconFromTray(); + void ReAddTrayIcon(); + + void TrayIconPressed(); + void ShowTrayContextMenu(const til::point coord, winrt::Windows::Foundation::Collections::IMapView peasants); + void TrayMenuItemSelected(const HMENU menu, const UINT menuItemIndex); + + WINRT_CALLBACK(SummonWindowRequested, winrt::delegate); + +private: + HMENU _CreateTrayContextMenu(winrt::Windows::Foundation::Collections::IMapView peasants); + + HWND _owningHwnd; + NOTIFYICONDATA _trayIconData; +}; diff --git a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj index 16ff12257..b20f6fd07 100644 --- a/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj +++ b/src/cascadia/WindowsTerminal/WindowsTerminal.vcxproj @@ -47,6 +47,7 @@ + @@ -57,6 +58,7 @@ + @@ -175,4 +177,4 @@ - + \ No newline at end of file diff --git a/src/cascadia/WindowsTerminal/icon.cpp b/src/cascadia/WindowsTerminal/icon.cpp index 4706894ad..687644aee 100644 --- a/src/cascadia/WindowsTerminal/icon.cpp +++ b/src/cascadia/WindowsTerminal/icon.cpp @@ -27,12 +27,14 @@ static int _GetActiveAppIconResource() return iconResource; } -HANDLE GetActiveAppIconHandle(int size) +// There's only two possible sizes - ICON_SMALL and ICON_BIG. +// So, use true for smallIcon if you want small and false for big. +HANDLE GetActiveAppIconHandle(bool smallIcon) { auto iconResource{ MAKEINTRESOURCEW(_GetActiveAppIconResource()) }; - const auto smXIcon = size == ICON_SMALL ? SM_CXSMICON : SM_CXICON; - const auto smYIcon = size == ICON_SMALL ? SM_CYSMICON : SM_CYICON; + const auto smXIcon = smallIcon ? SM_CXSMICON : SM_CXICON; + const auto smYIcon = smallIcon ? SM_CYSMICON : SM_CYICON; // These handles are loaded with LR_SHARED, so they are safe to "leak". HANDLE hIcon{ LoadImageW(wil::GetModuleInstanceHandle(), iconResource, IMAGE_ICON, GetSystemMetrics(smXIcon), GetSystemMetrics(smYIcon), LR_SHARED) }; @@ -43,11 +45,11 @@ HANDLE GetActiveAppIconHandle(int size) void UpdateWindowIconForActiveMetrics(HWND window) { - if (auto smallIcon = GetActiveAppIconHandle(ICON_SMALL)) + if (auto smallIcon = GetActiveAppIconHandle(true)) { SendMessageW(window, WM_SETICON, ICON_SMALL, reinterpret_cast(smallIcon)); } - if (auto largeIcon = GetActiveAppIconHandle(ICON_BIG)) + if (auto largeIcon = GetActiveAppIconHandle(false)) { SendMessageW(window, WM_SETICON, ICON_BIG, reinterpret_cast(largeIcon)); } diff --git a/src/cascadia/WindowsTerminal/icon.h b/src/cascadia/WindowsTerminal/icon.h index ed4fb5587..84be4e9c1 100644 --- a/src/cascadia/WindowsTerminal/icon.h +++ b/src/cascadia/WindowsTerminal/icon.h @@ -3,5 +3,5 @@ #pragma once -HANDLE GetActiveAppIconHandle(const int size); +HANDLE GetActiveAppIconHandle(bool smallIcon); void UpdateWindowIconForActiveMetrics(HWND window); diff --git a/src/cascadia/WindowsTerminal/main.cpp b/src/cascadia/WindowsTerminal/main.cpp index 7fb2523a5..ddac884fa 100644 --- a/src/cascadia/WindowsTerminal/main.cpp +++ b/src/cascadia/WindowsTerminal/main.cpp @@ -22,6 +22,15 @@ TRACELOGGING_DEFINE_PROVIDER( (0x56c06166, 0x2e2e, 0x5f4d, 0x7f, 0xf3, 0x74, 0xf4, 0xb7, 0x8c, 0x87, 0xd6), TraceLoggingOptionMicrosoftTelemetry()); +// !! BODGY !! +// Manually use the resources from TerminalApp as our resources. +// The WindowsTerminal project doesn't actually build a Resources.resw file, but +// we still need to be able to localize strings for the tray icon menu. Anything +// you want localized for WindowsTerminal.exe should be stuck in +// ...\TerminalApp\Resources\en-US\Resources.resw +#include +UTILS_DEFINE_LIBRARY_RESOURCE_SCOPE(L"TerminalApp/Resources"); + // Routine Description: // - Takes an image architecture and locates a string resource that maps to that architecture. // Arguments: diff --git a/src/features.xml b/src/features.xml index 42ff2e5f9..c50399f4a 100644 --- a/src/features.xml +++ b/src/features.xml @@ -64,6 +64,13 @@ + + Feature_TrayIcon + Controls whether the Tray Icon and related settings (aka. MinimizeToTray and AlwaysShowTrayIcon) are enabled + AlwaysEnabled + + + Feature_ShowProfileDefaultsInSettings Whether to show the "defaults" page in the Terminal settings UI