diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index a9f1767e13d..fe6fc8df0b7 100644 --- a/src/cascadia/TerminalApp/Pane.cpp +++ b/src/cascadia/TerminalApp/Pane.cpp @@ -59,6 +59,7 @@ Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocus // Register an event with the control to have it inform us when it gains focus. _gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler }); + _lostFocusRevoker = control.LostFocus(winrt::auto_revoke, { this, &Pane::_ControlLostFocusHandler }); // When our border is tapped, make sure to transfer focus to our control. // LOAD-BEARING: This will NOT work if the border's BorderBrush is set to @@ -367,15 +368,18 @@ void Pane::_ControlWarningBellHandler(const winrt::Windows::Foundation::IInspect auto paneProfile = settings.FindProfile(_profile.value()); if (paneProfile) { - if (WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) + // We don't want to do anything if nothing is set, so check for that first + if (static_cast(paneProfile.BellStyle()) != 0) { - const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); - PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); - } - if (WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Visual)) - { - // Bubble this event up to app host, starting with bubbling to the hosting tab - _PaneRaiseVisualBellHandlers(nullptr); + if (WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Audible)) + { + // Audible is set, play the sound + const auto soundAlias = reinterpret_cast(SND_ALIAS_SYSTEMHAND); + PlaySound(soundAlias, NULL, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY); + } + + // raise the event with the bool value corresponding to the visual flag + _PaneRaiseBellHandlers(nullptr, WI_IsFlagSet(paneProfile.BellStyle(), winrt::Microsoft::Terminal::Settings::Model::BellStyle::Visual)); } } } @@ -394,6 +398,16 @@ void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable cons _GotFocusHandlers(shared_from_this()); } +// Event Description: +// - Called when our control loses focus. We'll use this to trigger our LostFocus +// callback. The tab that's hosting us should have registered a callback which +// can be used to update its own internal focus state +void Pane::_ControlLostFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */, + RoutedEventArgs const& /* args */) +{ + _LostFocusHandlers(shared_from_this()); +} + // Method Description: // - Fire our Closed event to tell our parent that we should be removed. // Arguments: @@ -703,6 +717,7 @@ void Pane::_CloseChild(const bool closeFirst) // re-attach our handler for the control's GotFocus event. _gotFocusRevoker = _control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler }); + _lostFocusRevoker = _control.LostFocus(winrt::auto_revoke, { this, &Pane::_ControlLostFocusHandler }); // If we're inheriting the "last active" state from one of our children, // focus our control now. This should trigger our own GotFocus event. @@ -1408,6 +1423,7 @@ std::pair, std::shared_ptr> Pane::_Split(SplitState // control telling us that it's now focused, we want it telling its new // parent. _gotFocusRevoker.revoke(); + _lostFocusRevoker.revoke(); _splitState = actualSplitType; _desiredSplitPosition = 1.0f - splitSize; @@ -2057,4 +2073,5 @@ std::optional Pane::PreCalculateAutoSplit(const std::shared_ptr>); -DEFINE_EVENT(Pane, PaneRaiseVisualBell, _PaneRaiseVisualBellHandlers, winrt::delegate>); +DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate>); +DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 01e6cc237c8..0fd84210b38 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -83,7 +83,8 @@ class Pane : public std::enable_shared_from_this WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler); DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate>); - DECLARE_EVENT(PaneRaiseVisualBell, _PaneRaiseVisualBellHandlers, winrt::delegate>); + DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate>); + DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler); private: struct SnapSizeResult; @@ -111,6 +112,7 @@ class Pane : public std::enable_shared_from_this winrt::event_token _warningBellToken{ 0 }; winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker; + winrt::Windows::UI::Xaml::UIElement::LostFocus_revoker _lostFocusRevoker; std::shared_mutex _createCloseLock{}; @@ -144,6 +146,8 @@ class Pane : public std::enable_shared_from_this winrt::Windows::Foundation::IInspectable const& e); void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void _ControlLostFocusHandler(winrt::Windows::Foundation::IInspectable const& sender, + winrt::Windows::UI::Xaml::RoutedEventArgs const& e); std::pair _CalcChildrenSizes(const float fullSize) const; SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const; diff --git a/src/cascadia/TerminalApp/TabHeaderControl.h b/src/cascadia/TerminalApp/TabHeaderControl.h index 39744b2bcb0..12ff4262d54 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.h +++ b/src/cascadia/TerminalApp/TabHeaderControl.h @@ -25,6 +25,7 @@ namespace winrt::TerminalApp::implementation OBSERVABLE_GETSET_PROPERTY(bool, IsPaneZoomed, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(bool, IsProgressRingActive, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(bool, IsProgressRingIndeterminate, _PropertyChangedHandlers); + OBSERVABLE_GETSET_PROPERTY(bool, BellIndicator, _PropertyChangedHandlers); OBSERVABLE_GETSET_PROPERTY(uint32_t, ProgressValue, _PropertyChangedHandlers); private: diff --git a/src/cascadia/TerminalApp/TabHeaderControl.idl b/src/cascadia/TerminalApp/TabHeaderControl.idl index 0fc8fc2da3e..49cead65646 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.idl +++ b/src/cascadia/TerminalApp/TabHeaderControl.idl @@ -11,6 +11,7 @@ namespace TerminalApp Boolean IsPaneZoomed { get; set; }; Boolean IsProgressRingActive { get; set; }; Boolean IsProgressRingIndeterminate { get; set; }; + Boolean BellIndicator { get; set; }; UInt32 ProgressValue { get; set; }; TabHeaderControl(); diff --git a/src/cascadia/TerminalApp/TabHeaderControl.xaml b/src/cascadia/TerminalApp/TabHeaderControl.xaml index 65e8b5ee7b9..1bb8e7ab76a 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.xaml +++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml @@ -26,6 +26,12 @@ the MIT License. See LICENSE in the project root for license information. --> + Stop(); + _bellIndicatorTimer = std::nullopt; + } + } + // Method Description: // - Initializes a TabViewItem for this Tab instance. // Arguments: @@ -129,6 +145,11 @@ namespace winrt::TerminalApp::implementation lastFocusedControl.Focus(_focusState); lastFocusedControl.TaskbarProgressChanged(); } + // When we gain focus, remove the bell indicator if it is active + if (_headerControl.BellIndicator()) + { + ShowBellIndicator(false); + } } } @@ -236,6 +257,44 @@ namespace winrt::TerminalApp::implementation } } + // Method Description: + // - Hide or show the bell indicator in the tab header + // Arguments: + // - show: if true, we show the indicator; if false, we hide the indicator + winrt::fire_and_forget TerminalTab::ShowBellIndicator(const bool show) + { + auto weakThis{ get_weak() }; + + co_await winrt::resume_foreground(TabViewItem().Dispatcher()); + + if (auto tab{ weakThis.get() }) + { + tab->_headerControl.BellIndicator(show); + } + } + + // Method Description: + // - Activates the timer for the bell indicator in the tab + // - Called if a bell raised when the tab already has focus + winrt::fire_and_forget TerminalTab::ActivateBellIndicatorTimer() + { + auto weakThis{ get_weak() }; + + co_await winrt::resume_foreground(TabViewItem().Dispatcher()); + + if (auto tab{ weakThis.get() }) + { + if (!tab->_bellIndicatorTimer.has_value()) + { + DispatcherTimer bellIndicatorTimer; + bellIndicatorTimer.Interval(std::chrono::milliseconds(2000)); + bellIndicatorTimer.Tick({ get_weak(), &TerminalTab::_BellIndicatorTimerTick }); + bellIndicatorTimer.Start(); + tab->_bellIndicatorTimer.emplace(std::move(bellIndicatorTimer)); + } + } + } + // Method Description: // - Gets the title string of the last focused terminal control in our tree. // Returns the empty string if there is no such control. @@ -564,10 +623,30 @@ namespace winrt::TerminalApp::implementation // Do nothing if the Tab's lifetime is expired or pane isn't new. auto tab{ weakThis.get() }; - if (tab && sender != tab->_activePane) + if (tab) { - tab->_UpdateActivePane(sender); - tab->_RecalculateAndApplyTabColor(); + if (sender != tab->_activePane) + { + tab->_UpdateActivePane(sender); + tab->_RecalculateAndApplyTabColor(); + } + tab->_focusState = WUX::FocusState::Programmatic; + // This tab has gained focus, remove the bell indicator if it is active + if (tab->_headerControl.BellIndicator()) + { + tab->ShowBellIndicator(false); + } + } + }); + + pane->LostFocus([weakThis](std::shared_ptr /*sender*/) { + // Do nothing if the Tab's lifetime is expired or pane isn't new. + auto tab{ weakThis.get() }; + + if (tab) + { + // update this tab's focus state + tab->_focusState = WUX::FocusState::Unfocused; } }); @@ -601,10 +680,23 @@ namespace winrt::TerminalApp::implementation // Add a PaneRaiseVisualBell event handler to the Pane. When the pane emits this event, // we need to bubble it all the way to app host. In this part of the chain we bubble it // from the hosting tab to the page. - pane->PaneRaiseVisualBell([weakThis](auto&& /*s*/) { + pane->PaneRaiseBell([weakThis](auto&& /*s*/, auto&& visual) { if (auto tab{ weakThis.get() }) { - tab->_TabRaiseVisualBellHandlers(); + if (visual) + { + tab->_TabRaiseVisualBellHandlers(); + + tab->ShowBellIndicator(true); + + // If this tab is focused, activate the bell indicator timer, which will + // remove the bell indicator once it fires + // (otherwise, the indicator is removed when the tab gets focus) + if (tab->_focusState != WUX::FocusState::Unfocused) + { + tab->ActivateBellIndicatorTimer(); + } + } } }); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 1ae7ad2e6dd..e66da3278b1 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -38,6 +38,9 @@ namespace winrt::TerminalApp::implementation winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath); winrt::fire_and_forget HideIcon(const bool hide); + winrt::fire_and_forget ShowBellIndicator(const bool show); + winrt::fire_and_forget ActivateBellIndicatorTimer(); + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; winrt::Microsoft::Terminal::Settings::Model::SplitState PreCalculateAutoSplit(winrt::Windows::Foundation::Size rootSize) const; bool PreCalculateCanSplit(winrt::Microsoft::Terminal::Settings::Model::SplitState splitType, @@ -100,6 +103,9 @@ namespace winrt::TerminalApp::implementation winrt::TerminalApp::ShortcutActionDispatch _dispatch; + std::optional _bellIndicatorTimer; + void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); + void _MakeTabViewItem(); void _SetToolTip(const winrt::hstring& tabTitle);