Skip to content

Commit

Permalink
Show indicator of bell in tab (#8637)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
PankajBhojwani authored Jan 22, 2021
1 parent acf36d0 commit 02fd7a0
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 15 deletions.
35 changes: 26 additions & 9 deletions src/cascadia/TerminalApp/Pane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<int>(paneProfile.BellStyle()) != 0)
{
const auto soundAlias = reinterpret_cast<LPCTSTR>(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<LPCTSTR>(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));
}
}
}
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1421,6 +1436,7 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> 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;
Expand Down Expand Up @@ -2070,4 +2086,5 @@ std::optional<SplitState> Pane::PreCalculateAutoSplit(const std::shared_ptr<Pane
}

DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DEFINE_EVENT(Pane, PaneRaiseVisualBell, _PaneRaiseVisualBellHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DEFINE_EVENT(Pane, LostFocus, _LostFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DEFINE_EVENT(Pane, PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>);
6 changes: 5 additions & 1 deletion src/cascadia/TerminalApp/Pane.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ class Pane : public std::enable_shared_from_this<Pane>

WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>);
DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(PaneRaiseVisualBell, _PaneRaiseVisualBellHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(LostFocus, _LostFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
DECLARE_EVENT(PaneRaiseBell, _PaneRaiseBellHandlers, winrt::Windows::Foundation::EventHandler<bool>);

private:
struct SnapSizeResult;
Expand Down Expand Up @@ -111,6 +112,7 @@ class Pane : public std::enable_shared_from_this<Pane>
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{};

Expand Down Expand Up @@ -144,6 +146,8 @@ class Pane : public std::enable_shared_from_this<Pane>
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<float, float> _CalcChildrenSizes(const float fullSize) const;
SnapChildrenSizeResult _CalcSnappedChildrenSizes(const bool widthOrHeight, const float fullSize) const;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TabHeaderControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalApp/TabHeaderControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/TabHeaderControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ the MIT License. See LICENSE in the project root for license information. -->
<!--We want the progress ring to 'replace' the tab icon, but we don't have control
over the tab icon here (the tab view item does) - so we hide the tab icon there
and use a negative margin for the progress ring here to put it where the icon would be-->
<FontIcon x:Name="HeaderBellIndicator"
FontFamily="Segoe MDL2 Assets"
Visibility="{x:Bind BellIndicator, Mode=OneWay}"
Glyph="&#xEA8F;"
FontSize="12"
Margin="0,0,8,0"/>
<FontIcon x:Name="HeaderZoomIcon"
FontFamily="Segoe MDL2 Assets"
Visibility="{x:Bind IsPaneZoomed, Mode=OneWay}"
Expand Down
102 changes: 97 additions & 5 deletions src/cascadia/TerminalApp/TerminalTab.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ namespace winrt::TerminalApp::implementation
TabViewItem().Header(_headerControl);
}

// Method Description:
// - Called when the timer for the bell indicator in the tab header fires
// - Removes the bell indicator from the tab header
// Arguments:
// - sender, e: not used
void TerminalTab::_BellIndicatorTimerTick(Windows::Foundation::IInspectable const& /*sender*/, Windows::Foundation::IInspectable const& /*e*/)
{
ShowBellIndicator(false);
// Just do a sanity check that the timer still exists before we stop it
if (_bellIndicatorTimer.has_value())
{
_bellIndicatorTimer->Stop();
_bellIndicatorTimer = std::nullopt;
}
}

// Method Description:
// - Initializes a TabViewItem for this Tab instance.
// Arguments:
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Pane> /*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;
}
});

Expand Down Expand Up @@ -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();
}
}
}
});
}
Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalApp/TerminalTab.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -103,6 +106,9 @@ namespace winrt::TerminalApp::implementation

winrt::TerminalApp::ShortcutActionDispatch _dispatch;

std::optional<Windows::UI::Xaml::DispatcherTimer> _bellIndicatorTimer;
void _BellIndicatorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);

void _MakeTabViewItem();

winrt::fire_and_forget _UpdateHeaderControlMaxWidth();
Expand Down

0 comments on commit 02fd7a0

Please sign in to comment.