From 02fd7a0c15791fc26fc2bab8dd39ff8360d144d9 Mon Sep 17 00:00:00 2001 From: PankajBhojwani Date: Fri, 22 Jan 2021 15:48:20 -0800 Subject: [PATCH] Show indicator of bell in tab (#8637) When we emit a BEL (visual or audible), show an indicator in the tab header If the tab the BEL is coming from is not focused when the BEL is raised, the indicator in its header will be removed when the tab gains focus. If the tab was already focused when the BEL was emitted, then the indicator goes away after 2 seconds. Closes #8106 --- src/cascadia/TerminalApp/Pane.cpp | 35 ++++-- src/cascadia/TerminalApp/Pane.h | 6 +- src/cascadia/TerminalApp/TabHeaderControl.h | 1 + src/cascadia/TerminalApp/TabHeaderControl.idl | 1 + .../TerminalApp/TabHeaderControl.xaml | 6 ++ src/cascadia/TerminalApp/TerminalTab.cpp | 102 +++++++++++++++++- src/cascadia/TerminalApp/TerminalTab.h | 6 ++ 7 files changed, 142 insertions(+), 15 deletions(-) diff --git a/src/cascadia/TerminalApp/Pane.cpp b/src/cascadia/TerminalApp/Pane.cpp index 914be085e8e..91c751ca7f1 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: @@ -716,6 +730,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. @@ -1421,6 +1436,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; @@ -2070,4 +2086,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 41c81fb783d..7cc5bcd055e 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.h +++ b/src/cascadia/TerminalApp/TabHeaderControl.h @@ -26,6 +26,7 @@ namespace winrt::TerminalApp::implementation OBSERVABLE_GETSET_PROPERTY(double, RenamerMaxWidth, _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 808b72067c4..745acdeea84 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.idl +++ b/src/cascadia/TerminalApp/TabHeaderControl.idl @@ -12,6 +12,7 @@ namespace TerminalApp Double RenamerMaxWidth { 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 ba8350d3b12..2ca1e56004f 100644 --- a/src/cascadia/TerminalApp/TabHeaderControl.xaml +++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml @@ -43,6 +43,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: @@ -145,6 +161,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); + } } } @@ -255,6 +276,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. @@ -583,10 +642,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; } }); @@ -620,10 +699,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 eee3c1e3a59..7bab3ac5c15 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -41,6 +41,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, @@ -103,6 +106,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(); winrt::fire_and_forget _UpdateHeaderControlMaxWidth();