Decouple "Active Terminal" and "Focused Control" (#3540)

## Summary of the Pull Request

Unties the concept of "focused control" from "active control".

Previously, we were exclusively using the "Focused" state of `TermControl`s to determine which one was active. This was fraught with gotchas - if anything else became focused, then suddenly there was _no_ pane focused in the Tab. This happened especially frequently if the user clicked on a tab to focus the window. Furthermore, in experimental branches with more UI added to the Terminal (such as [dev/migrie/f/2046-command-palette](https://github.com/microsoft/terminal/tree/dev/migrie/f/2046-command-palette)), when these UIs were added to the Terminal, they'd take focus, which again meant that there was no focused pane.

This fixes these issue by having each Tab manually track which Pane is active in that tab. The Tab is now the arbiter of who in the tree is "active". Panes still track this state, for them to be able to MoveFocus appropriately. 

It also contains a related fix to prevent the tab separator from stealing focus from the TermControl. This required us to set the color of the un-focused Pane border to some color other that Transparent, so I went with the TabViewBackground. Panes now look like the following:

![image](https://user-images.githubusercontent.com/18356694/68697343-41ea2380-0544-11ea-8218-601b57fdd835.png)


## References

See also: #2046

## PR Checklist
* [x] Closes #1205
* [x] Closes #522
* [x] Closes #999
* [x] I work here
* [😢] Tests added/passed
* [n/a] Requires documentation to be updated

## Validation Steps Performed

Tested manually opening panes, closing panes, clicking around panes, the whole dance.

---------------------------------------------------

* this is janky but is close for some reason?

* This is _almost_ right to solve #1205

  If I want to double up and also fix #522 (which I do), then I need to also
  * when a tab GetsFocus, send the focus instead to the Pane
  * When the border is clicked on, focus that pane's control

  And like a lot of cleanup, because this is horrifying

* hey this autorevoker is really nice

* Encapsulate Pane::pfnGotFocus

* Propogate the events back up on close

* Encapsulate Tab::pfnFocusChanged, and clean up TerminalPage a bit

* Mostly just code cleanup, commenting

* This works to hittest on the borders

  If the border is `Transparent`, then it can't hittest for Tapped events, and it'll fall through (to someone)

  THis at least works, but looks garish

* Match the pane border to the TabViewHeader

* Fix a bit of dead code and a bad copy-pasta

* This _works_ to use a winrt event, but it's dirty

* Clean up everything from the winrt::event debacle.

* This is dead code that shouldn't have been there

* Turn Tab's callback into a winrt::event as well
This commit is contained in:
Mike Griese 2019-11-18 15:41:25 -06:00 committed by GitHub
parent cc8faaf04f
commit 9ed3da8b3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 313 additions and 183 deletions

View file

@ -215,7 +215,7 @@ namespace winrt::TerminalApp::implementation
{
if (const auto& realArgs = args.ActionArgs().try_as<TerminalApp::AdjustFontSizeArgs>())
{
const auto termControl = _GetFocusedControl();
const auto termControl = _GetActiveControl();
termControl.AdjustFontSize(realArgs.Delta());
args.Handled(true);
}

View file

@ -18,10 +18,11 @@ static const int CombinedPaneBorderSize = 2 * PaneBorderSize;
static const float Half = 0.50f;
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_focusedBorderBrush = { nullptr };
winrt::Windows::UI::Xaml::Media::SolidColorBrush Pane::s_unfocusedBorderBrush = { nullptr };
Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocused) :
_control{ control },
_lastFocused{ lastFocused },
_lastActive{ lastFocused },
_profile{ profile }
{
_root.Children().Append(_border);
@ -29,39 +30,25 @@ Pane::Pane(const GUID& profile, const TermControl& control, const bool lastFocus
_connectionClosedToken = _control.ConnectionClosed({ this, &Pane::_ControlClosedHandler });
// Set the background of the pane to match that of the theme's default grid
// background. This way, we'll match the small underline under the tabs, and
// the UI will be consistent on bot light and dark modes.
const auto res = Application::Current().Resources();
const auto key = winrt::box_value(L"BackgroundGridThemeStyle");
if (res.HasKey(key))
// On the first Pane's creation, lookup resources we'll use to theme the
// Pane, including the brushed to use for the focused/unfocused border
// color.
if (s_focusedBorderBrush == nullptr || s_unfocusedBorderBrush == nullptr)
{
const auto g = res.Lookup(key);
const auto style = g.try_as<winrt::Windows::UI::Xaml::Style>();
// try_as fails by returning nullptr
if (style)
{
_root.Style(style);
}
_SetupResources();
}
if (s_focusedBorderBrush == nullptr)
{
const auto accentColorKey = winrt::box_value(L"SystemAccentColor");
if (res.HasKey(accentColorKey))
{
const auto colorFromResources = res.Lookup(accentColorKey);
// If SystemAccentColor is _not_ a Color for some reason, use
// Transparent as the color, so we don't do this process again on
// the next pane (by leaving s_focusedBorderBrush nullptr)
auto actualColor = winrt::unbox_value_or<Color>(colorFromResources, Colors::Transparent());
s_focusedBorderBrush = SolidColorBrush(actualColor);
}
else
{
s_focusedBorderBrush = SolidColorBrush{ Colors::Transparent() };
}
}
// Register an event with the control to have it inform us when it gains focus.
_gotFocusRevoker = control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
// 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
// Colors::Transparent! The border won't get Tapped events, and they'll fall
// through to something else.
_border.Tapped([this](auto&, auto& e) {
_FocusFirstChild();
e.Handled(true);
});
}
// Method Description:
@ -187,8 +174,8 @@ bool Pane::ResizePane(const Direction& direction)
// If it is, and the requested resize direction matches our separator, then
// we're the pane that needs to adjust its separator.
// If our separator is the wrong direction, then we can't handle it.
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastFocused;
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastFocused;
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive;
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive;
if (firstIsFocused || secondIsFocused)
{
return _Resize(direction);
@ -246,7 +233,7 @@ bool Pane::_NavigateFocus(const Direction& direction)
// Transfer focus to our child, and update the focus of our tree.
newlyFocusedChild->_FocusFirstChild();
UpdateFocus();
UpdateVisuals();
return true;
}
@ -276,8 +263,8 @@ bool Pane::NavigateFocus(const Direction& direction)
// Check if either our first or second child is the currently focused leaf.
// If it is, and the requested move direction matches our separator, then
// we're the pane that needs to handle this focus move.
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastFocused;
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastFocused;
const bool firstIsFocused = _firstChild->_IsLeaf() && _firstChild->_lastActive;
const bool secondIsFocused = _secondChild->_IsLeaf() && _secondChild->_lastActive;
if (firstIsFocused || secondIsFocused)
{
return _NavigateFocus(direction);
@ -370,37 +357,63 @@ Controls::Grid Pane::GetRootElement()
// not currently focused.
// Return Value:
// - nullptr if we're a leaf and unfocused, or no children were marked
// `_lastFocused`, else returns this
std::shared_ptr<Pane> Pane::GetFocusedPane()
// `_lastActive`, else returns this
std::shared_ptr<Pane> Pane::GetActivePane()
{
if (_IsLeaf())
{
return _lastFocused ? shared_from_this() : nullptr;
return _lastActive ? shared_from_this() : nullptr;
}
auto firstFocused = _firstChild->GetFocusedPane();
auto firstFocused = _firstChild->GetActivePane();
if (firstFocused != nullptr)
{
return firstFocused;
}
return _secondChild->GetFocusedPane();
return _secondChild->GetActivePane();
}
// Method Description:
// - Returns nullptr if no children of this pane were the last control to be
// focused, or the TermControl that _was_ the last control to be focused (if
// there was one).
// - This control might not currently be focused, if the tab itself is not
// currently focused.
// - Gets the TermControl of this pane. If this Pane is not a leaf, this will return nullptr.
// Arguments:
// - <none>
// Return Value:
// - nullptr if no children were marked `_lastFocused`, else the TermControl
// that was last focused.
TermControl Pane::GetFocusedTerminalControl()
// - nullptr if this Pane is a parent, otherwise the TermControl of this Pane.
TermControl Pane::GetTerminalControl()
{
auto lastFocused = GetFocusedPane();
return lastFocused ? lastFocused->_control : nullptr;
return _IsLeaf() ? _control : nullptr;
}
// Method Description:
// - Recursively remove the "Active" state from this Pane and all it's children.
// - Updates our visuals to match our new state, including highlighting our borders.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Pane::ClearActive()
{
_lastActive = false;
if (!_IsLeaf())
{
_firstChild->ClearActive();
_secondChild->ClearActive();
}
UpdateVisuals();
}
// Method Description:
// - Sets the "Active" state on this Pane. Only one Pane in a tree of Panes
// should be "active", and that pane should be a leaf.
// - Updates our visuals to match our new state, including highlighting our borders.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Pane::SetActive()
{
_lastActive = true;
UpdateVisuals();
}
// Method Description:
@ -414,7 +427,7 @@ TermControl Pane::GetFocusedTerminalControl()
// focused, else the GUID of the profile of the last control to be focused
std::optional<GUID> Pane::GetFocusedProfile()
{
auto lastFocused = GetFocusedPane();
auto lastFocused = GetActivePane();
return lastFocused ? lastFocused->_profile : std::nullopt;
}
@ -426,7 +439,7 @@ std::optional<GUID> Pane::GetFocusedProfile()
// - true iff we were the last pane focused in this tree of panes.
bool Pane::WasLastFocused() const noexcept
{
return _lastFocused;
return _lastActive;
}
// Method Description:
@ -453,38 +466,21 @@ bool Pane::_HasFocusedChild() const noexcept
// We're intentionally making this one giant expression, so the compiler
// will skip the following lookups if one of the lookups before it returns
// true
return (_control && _control.FocusState() != FocusState::Unfocused) ||
return (_control && _lastActive) ||
(_firstChild && _firstChild->_HasFocusedChild()) ||
(_secondChild && _secondChild->_HasFocusedChild());
}
// Method Description:
// - Update the focus state of this pane, and all its descendants.
// * If this is a leaf node, and our control is actively focused, we'll mark
// ourselves as the _lastFocused.
// * If we're not a leaf, we'll recurse on our children to check them.
// - Update the focus state of this pane. We'll make sure to colorize our
// borders depending on if we are the active pane or not.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Pane::UpdateFocus()
void Pane::UpdateVisuals()
{
if (_IsLeaf())
{
const auto controlFocused = _control &&
_control.FocusState() != FocusState::Unfocused;
_lastFocused = controlFocused;
_border.BorderBrush(_lastFocused ? s_focusedBorderBrush : nullptr);
}
else
{
_lastFocused = false;
_firstChild->UpdateFocus();
_secondChild->UpdateFocus();
}
_border.BorderBrush(_lastActive ? s_focusedBorderBrush : s_unfocusedBorderBrush);
}
// Method Description:
@ -587,7 +583,7 @@ void Pane::_CloseChild(const bool closeFirst)
// If either of our children was focused, we want to take that focus from
// them.
_lastFocused = _firstChild->_lastFocused || _secondChild->_lastFocused;
_lastActive = _firstChild->_lastActive || _secondChild->_lastActive;
// Remove all the ui elements of our children. This'll make sure we can
// re-attach the TermControl to our Grid.
@ -606,13 +602,22 @@ void Pane::_CloseChild(const bool closeFirst)
_root.Children().Append(_border);
_border.Child(_control);
if (_lastFocused)
// Make sure to set our _splitState before focusing the control. If you
// fail to do this, when the tab handles the GotFocus event and asks us
// what our active control is, we won't technically be a "leaf", and
// GetTerminalControl will return null.
_splitState = SplitState::None;
// re-attach our handler for the control's GotFocus event.
_gotFocusRevoker = _control.GotFocus(winrt::auto_revoke, { this, &Pane::_ControlGotFocusHandler });
// If we're inheriting the "last active" state from one of our children,
// focus our control now. This should trigger our own GotFocus event.
if (_lastActive)
{
_control.Focus(FocusState::Programmatic);
}
_splitState = SplitState::None;
_UpdateBorders();
// Release our children.
@ -696,7 +701,7 @@ void Pane::_CloseChild(const bool closeFirst)
_secondChild->_UpdateBorders();
// If the closed child was focused, transfer the focus to it's first sibling.
if (closedChild->_lastFocused)
if (closedChild->_lastActive)
{
_FocusFirstChild();
}
@ -892,24 +897,24 @@ bool Pane::CanSplit(SplitState splitType)
// - profile: The profile GUID to associate with the newly created pane.
// - control: A TermControl to use in the new pane.
// Return Value:
// - <none>
void Pane::Split(SplitState splitType, const GUID& profile, const TermControl& control)
// - The two newly created Panes
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::Split(SplitState splitType, const GUID& profile, const TermControl& control)
{
if (!_IsLeaf())
{
if (_firstChild->_HasFocusedChild())
{
_firstChild->Split(splitType, profile, control);
return _firstChild->Split(splitType, profile, control);
}
else if (_secondChild->_HasFocusedChild())
{
_secondChild->Split(splitType, profile, control);
return _secondChild->Split(splitType, profile, control);
}
return;
return { nullptr, nullptr };
}
_Split(splitType, profile, control);
return _Split(splitType, profile, control);
}
// Method Description:
@ -952,8 +957,8 @@ bool Pane::_CanSplit(SplitState splitType)
// - profile: The profile GUID to associate with the newly created pane.
// - control: A TermControl to use in the new pane.
// Return Value:
// - <none>
void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& control)
// - The two newly created Panes
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitState splitType, const GUID& profile, const TermControl& control)
{
// Lock the create/close lock so that another operation won't concurrently
// modify our tree
@ -963,6 +968,11 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl&
_control.ConnectionClosed(_connectionClosedToken);
_connectionClosedToken.value = 0;
// Remove our old GotFocus handler from the control. We don't what the
// control telling us that it's now focused, we want it telling its new
// parent.
_gotFocusRevoker.revoke();
_splitState = splitType;
_firstPercent = { Half };
@ -991,7 +1001,9 @@ void Pane::_Split(SplitState splitType, const GUID& profile, const TermControl&
// Register event handlers on our children to handle their Close events
_SetupChildCloseHandlers();
_lastFocused = false;
_lastActive = false;
return { _firstChild, _secondChild };
}
// Method Description:
@ -1053,4 +1065,64 @@ Size Pane::_GetMinSize() const
}
}
// Event Description:
// - Called when our control gains focus. We'll use this to trigger our GotFocus
// callback. The tab that's hosting us should have registered a callback which
// can be used to mark us as active.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void Pane::_ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& /* sender */,
RoutedEventArgs const& /* args */)
{
_GotFocusHandlers(shared_from_this());
}
// Function Description:
// - Attempts to load some XAML resources that the Pane will need. This includes:
// * The Color we'll use for active Panes's borders - SystemAccentColor
// * The Brush we'll use for inactive Panes - TabViewBackground (to match the
// color of the titlebar)
// Arguments:
// - <none>
// Return Value:
// - <none>
void Pane::_SetupResources()
{
const auto res = Application::Current().Resources();
const auto accentColorKey = winrt::box_value(L"SystemAccentColor");
if (res.HasKey(accentColorKey))
{
const auto colorFromResources = res.Lookup(accentColorKey);
// If SystemAccentColor is _not_ a Color for some reason, use
// Transparent as the color, so we don't do this process again on
// the next pane (by leaving s_focusedBorderBrush nullptr)
auto actualColor = winrt::unbox_value_or<Color>(colorFromResources, Colors::Black());
s_focusedBorderBrush = SolidColorBrush(actualColor);
}
else
{
// DON'T use Transparent here - if it's "Transparent", then it won't
// be able to hittest for clicks, and then clicking on the border
// will eat focus.
s_focusedBorderBrush = SolidColorBrush{ Colors::Black() };
}
const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground");
if (res.HasKey(accentColorKey))
{
winrt::Windows::Foundation::IInspectable obj = res.Lookup(tabViewBackgroundKey);
s_unfocusedBorderBrush = obj.try_as<winrt::Windows::UI::Xaml::Media::SolidColorBrush>();
}
else
{
// DON'T use Transparent here - if it's "Transparent", then it won't
// be able to hittest for clicks, and then clicking on the border
// will eat focus.
s_unfocusedBorderBrush = SolidColorBrush{ Colors::Black() };
}
}
DEFINE_EVENT(Pane, Closed, _closedHandlers, ConnectionClosedEventArgs);
DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);

View file

@ -43,34 +43,43 @@ public:
Horizontal = 2
};
Pane(const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control, const bool lastFocused = false);
Pane(const GUID& profile,
const winrt::Microsoft::Terminal::TerminalControl::TermControl& control,
const bool lastFocused = false);
std::shared_ptr<Pane> GetFocusedPane();
winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl();
std::shared_ptr<Pane> GetActivePane();
winrt::Microsoft::Terminal::TerminalControl::TermControl GetTerminalControl();
std::optional<GUID> GetFocusedProfile();
winrt::Windows::UI::Xaml::Controls::Grid GetRootElement();
bool WasLastFocused() const noexcept;
void UpdateFocus();
void UpdateVisuals();
void ClearActive();
void SetActive();
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile);
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings,
const GUID& profile);
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
bool ResizePane(const winrt::TerminalApp::Direction& direction);
bool NavigateFocus(const winrt::TerminalApp::Direction& direction);
bool CanSplit(SplitState splitType);
void Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Split(SplitState splitType,
const GUID& profile,
const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void Close();
DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs);
DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
private:
winrt::Windows::UI::Xaml::Controls::Grid _root{};
winrt::Windows::UI::Xaml::Controls::Border _border{};
winrt::Microsoft::Terminal::TerminalControl::TermControl _control{ nullptr };
static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_focusedBorderBrush;
static winrt::Windows::UI::Xaml::Media::SolidColorBrush s_unfocusedBorderBrush;
std::shared_ptr<Pane> _firstChild{ nullptr };
std::shared_ptr<Pane> _secondChild{ nullptr };
@ -78,12 +87,14 @@ private:
std::optional<float> _firstPercent{ std::nullopt };
std::optional<float> _secondPercent{ std::nullopt };
bool _lastFocused{ false };
bool _lastActive{ false };
std::optional<GUID> _profile{ std::nullopt };
winrt::event_token _connectionClosedToken{ 0 };
winrt::event_token _firstClosedToken{ 0 };
winrt::event_token _secondClosedToken{ 0 };
winrt::Windows::UI::Xaml::UIElement::GotFocus_revoker _gotFocusRevoker;
std::shared_mutex _createCloseLock{};
Borders _borders{ Borders::None };
@ -93,7 +104,10 @@ private:
void _SetupChildCloseHandlers();
bool _CanSplit(SplitState splitType);
void _Split(SplitState splitType, const GUID& profile, const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> _Split(SplitState splitType,
const GUID& profile,
const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void _CreateRowColDefinitions(const winrt::Windows::Foundation::Size& rootSize);
void _CreateSplitContent();
void _ApplySplitDefinitions();
@ -110,6 +124,8 @@ private:
std::pair<float, float> _GetPaneSizes(const float& fullSize);
winrt::Windows::Foundation::Size _GetMinSize() const;
void _ControlGotFocusHandler(winrt::Windows::Foundation::IInspectable const& sender,
winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
// Function Description:
// - Returns true if the given direction can be used with the given split
@ -144,4 +160,6 @@ private:
}
return false;
}
static void _SetupResources();
};

View file

@ -23,6 +23,12 @@ Tab::Tab(const GUID& profile, const TermControl& control)
_closedHandlers();
});
_activePane = _rootPane;
_AttachEventHandlersToPane(_rootPane);
_AttachEventHandlersToControl(control);
_MakeTabViewItem();
}
@ -47,9 +53,9 @@ UIElement Tab::GetRootElement()
// Return Value:
// - nullptr if no children were marked `_lastFocused`, else the TermControl
// that was last focused.
TermControl Tab::GetFocusedTerminalControl()
TermControl Tab::GetActiveTerminalControl() const
{
return _rootPane->GetFocusedTerminalControl();
return _activePane->GetTerminalControl();
}
winrt::MUX::Controls::TabViewItem Tab::GetTabViewItem()
@ -99,7 +105,7 @@ void Tab::SetFocused(const bool focused)
// focused, else the GUID of the profile of the last control to be focused
std::optional<GUID> Tab::GetFocusedProfile() const noexcept
{
return _rootPane->GetFocusedProfile();
return _activePane->GetFocusedProfile();
}
// Method Description:
@ -124,27 +130,13 @@ void Tab::_Focus()
{
_focused = true;
auto lastFocusedControl = _rootPane->GetFocusedTerminalControl();
auto lastFocusedControl = GetActiveTerminalControl();
if (lastFocusedControl)
{
lastFocusedControl.Focus(FocusState::Programmatic);
}
}
// Method Description:
// - Update the focus state of this tab's tree of panes. If one of the controls
// under this tab is focused, then it will be marked as the last focused. If
// there are no focused panes, then there will not be a last focused control
// when this returns.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Tab::UpdateFocus()
{
_rootPane->UpdateFocus();
}
void Tab::UpdateIcon(const winrt::hstring iconPath)
{
// Don't reload our icon if it hasn't changed.
@ -167,9 +159,9 @@ void Tab::UpdateIcon(const winrt::hstring iconPath)
// - <none>
// Return Value:
// - the title string of the last focused terminal control in our tree.
winrt::hstring Tab::GetFocusedTitle() const
winrt::hstring Tab::GetActiveTitle() const
{
const auto lastFocusedControl = _rootPane->GetFocusedTerminalControl();
const auto lastFocusedControl = GetActiveTerminalControl();
return lastFocusedControl ? lastFocusedControl.Title() : L"";
}
@ -198,7 +190,7 @@ void Tab::SetTabText(const winrt::hstring& text)
// - <none>
void Tab::Scroll(const int delta)
{
auto control = GetFocusedTerminalControl();
auto control = GetActiveTerminalControl();
control.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [control, delta]() {
const auto currentOffset = control.GetScrollOffset();
control.KeyboardScrollViewport(currentOffset + delta);
@ -213,7 +205,7 @@ void Tab::Scroll(const int delta)
// - True if the focused pane can be split. False otherwise.
bool Tab::CanSplitPane(Pane::SplitState splitType)
{
return _rootPane->CanSplit(splitType);
return _activePane->CanSplit(splitType);
}
// Method Description:
@ -227,7 +219,14 @@ bool Tab::CanSplitPane(Pane::SplitState splitType)
// - <none>
void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl& control)
{
_rootPane->Split(splitType, profile, control);
auto [first, second] = _activePane->Split(splitType, profile, control);
_AttachEventHandlersToControl(control);
// Add a event handlers to the new panes' GotFocus event. When the pane
// gains focus, we'll mark it as the new active pane.
_AttachEventHandlersToPane(first);
_AttachEventHandlersToPane(second);
}
// Method Description:
@ -239,6 +238,8 @@ void Tab::SplitPane(Pane::SplitState splitType, const GUID& profile, TermControl
// - <none>
void Tab::ResizeContent(const winrt::Windows::Foundation::Size& newSize)
{
// NOTE: This _must_ be called on the root pane, so that it can propogate
// throughout the entire tree.
_rootPane->ResizeContent(newSize);
}
@ -251,6 +252,8 @@ void Tab::ResizeContent(const winrt::Windows::Foundation::Size& newSize)
// - <none>
void Tab::ResizePane(const winrt::TerminalApp::Direction& direction)
{
// NOTE: This _must_ be called on the root pane, so that it can propogate
// throughout the entire tree.
_rootPane->ResizePane(direction);
}
@ -263,6 +266,8 @@ void Tab::ResizePane(const winrt::TerminalApp::Direction& direction)
// - <none>
void Tab::NavigateFocus(const winrt::TerminalApp::Direction& direction)
{
// NOTE: This _must_ be called on the root pane, so that it can propogate
// throughout the entire tree.
_rootPane->NavigateFocus(direction);
}
@ -276,8 +281,59 @@ void Tab::NavigateFocus(const winrt::TerminalApp::Direction& direction)
// - <none>
void Tab::ClosePane()
{
auto focused = _rootPane->GetFocusedPane();
focused->Close();
_activePane->Close();
}
// Method Description:
// - Register any event handlers that we may need with the given TermControl.
// This should be called on each and every TermControl that we add to the tree
// of Panes in this tab. We'll add events too:
// * notify us when the control's title changed, so we can update our own
// title (if necessary)
// Arguments:
// - control: the TermControl to add events to.
// Return Value:
// - <none>
void Tab::_AttachEventHandlersToControl(const TermControl& control)
{
control.TitleChanged([this](auto newTitle) {
// The title of the control changed, but not necessarily the title
// of the tab. Get the title of the active pane of the tab, and set
// the tab's text to the active panes' text.
auto newTabTitle = GetActiveTitle();
SetTabText(newTabTitle);
});
}
// Method Description:
// - Add an event handler to this pane's GotFocus event. When that pane gains
// focus, we'll mark it as the new active pane. We'll also query the title of
// that pane when it's focused to set our own text, and finally, we'll trigger
// our own ActivePaneChanged event.
// Arguments:
// - <none>
// Return Value:
// - <none>
void Tab::_AttachEventHandlersToPane(std::shared_ptr<Pane> pane)
{
pane->GotFocus([this](std::shared_ptr<Pane> sender) {
// Do nothing if it's the same pane as before.
if (sender == _activePane)
{
return;
}
// Clear the active state of the entire tree, and mark only the sender as active.
_rootPane->ClearActive();
_activePane = sender;
_activePane->SetActive();
// Update our own title text to match the newly-active pane.
SetTabText(GetActiveTitle());
// Raise our own ActivePaneChanged event.
_ActivePaneChangedHandlers();
});
}
DEFINE_EVENT(Tab, Closed, _closedHandlers, ConnectionClosedEventArgs);
DEFINE_EVENT(Tab, ActivePaneChanged, _ActivePaneChangedHandlers, winrt::delegate<>);

View file

@ -12,7 +12,7 @@ public:
winrt::Microsoft::UI::Xaml::Controls::TabViewItem GetTabViewItem();
winrt::Windows::UI::Xaml::UIElement GetRootElement();
winrt::Microsoft::Terminal::TerminalControl::TermControl GetFocusedTerminalControl();
winrt::Microsoft::Terminal::TerminalControl::TermControl GetActiveTerminalControl() const;
std::optional<GUID> GetFocusedProfile() const noexcept;
bool IsFocused() const noexcept;
@ -23,7 +23,6 @@ public:
bool CanSplitPane(Pane::SplitState splitType);
void SplitPane(Pane::SplitState splitType, const GUID& profile, winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void UpdateFocus();
void UpdateIcon(const winrt::hstring iconPath);
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
@ -31,15 +30,17 @@ public:
void NavigateFocus(const winrt::TerminalApp::Direction& direction);
void UpdateSettings(const winrt::Microsoft::Terminal::Settings::TerminalSettings& settings, const GUID& profile);
winrt::hstring GetFocusedTitle() const;
winrt::hstring GetActiveTitle() const;
void SetTabText(const winrt::hstring& text);
void ClosePane();
DECLARE_EVENT(Closed, _closedHandlers, winrt::Microsoft::Terminal::TerminalControl::ConnectionClosedEventArgs);
DECLARE_EVENT(ActivePaneChanged, _ActivePaneChangedHandlers, winrt::delegate<>);
private:
std::shared_ptr<Pane> _rootPane{ nullptr };
std::shared_ptr<Pane> _activePane{ nullptr };
winrt::hstring _lastIconPath{};
bool _focused{ false };
@ -47,4 +48,7 @@ private:
void _MakeTabViewItem();
void _Focus();
void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void _AttachEventHandlersToPane(std::shared_ptr<Pane> pane);
};

View file

@ -416,15 +416,31 @@ namespace winrt::TerminalApp::implementation
// Add the new tab to the list of our tabs.
auto newTab = _tabs.emplace_back(std::make_shared<Tab>(profileGuid, term));
const auto* const profile = _settings->FindProfile(profileGuid);
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(term, newTab);
// Don't capture a strong ref to the tab. If the tab is removed as this
// is called, we don't really care anymore about handling the event.
std::weak_ptr<Tab> weakTabPtr = newTab;
// When the tab's active pane changes, we'll want to lookup a new icon
// for it, and possibly propogate the title up to the window.
newTab->ActivePaneChanged([this, weakTabPtr]() {
if (auto tab = weakTabPtr.lock())
{
// Possibly update the icon of the tab.
_UpdateTabIcon(tab);
// Possibly update the title of the tab, window to match the newly
// focused pane.
_UpdateTitle(tab);
}
});
auto tabViewItem = newTab->GetTabViewItem();
_tabView.TabItems().Append(tabViewItem);
// Set this profile's tab to the icon the user specified
// Set this tab's icon to the icon from the user's profile
const auto* const profile = _settings->FindProfile(profileGuid);
if (profile != nullptr && profile->HasIcon())
{
newTab->UpdateIcon(profile->GetExpandedIconPath());
@ -582,15 +598,14 @@ namespace winrt::TerminalApp::implementation
}
// Method Description:
// - Get the title of the currently focused terminal control, and set it's
// tab's text to that text. If this tab is the focused tab, then also
// bubble this title to any listeners of our TitleChanged event.
// - Get the title of the currently focused terminal control. If this tab is
// the focused tab, then also bubble this title to any listeners of our
// TitleChanged event.
// Arguments:
// - tab: the Tab to update the title for.
void TerminalPage::_UpdateTitle(std::shared_ptr<Tab> tab)
{
auto newTabTitle = tab->GetFocusedTitle();
tab->SetTabText(newTabTitle);
auto newTabTitle = tab->GetActiveTitle();
if (_settings->GlobalSettings().GetShowTitleInTitlebar() &&
tab->IsFocused())
@ -706,9 +721,6 @@ namespace winrt::TerminalApp::implementation
// handle. This includes:
// * the Copy and Paste events, for setting and retrieving clipboard data
// on the right thread
// * the TitleChanged event, for changing the text of the tab
// * the GotFocus event, for changing the title/icon in the tab when a new
// control is focused
// Arguments:
// - term: The newly created TermControl to connect the events for
// - hostingTab: The Tab that's hosting this TermControl instance
@ -720,38 +732,6 @@ namespace winrt::TerminalApp::implementation
// Add an event handler when the terminal wants to paste data from the Clipboard.
term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler });
// Don't capture a strong ref to the tab. If the tab is removed as this
// is called, we don't really care anymore about handling the event.
std::weak_ptr<Tab> weakTabPtr = hostingTab;
term.TitleChanged([this, weakTabPtr](auto newTitle) {
auto tab = weakTabPtr.lock();
if (!tab)
{
return;
}
// The title of the control changed, but not necessarily the title
// of the tab. Get the title of the focused pane of the tab, and set
// the tab's text to the focused panes' text.
_UpdateTitle(tab);
});
term.GotFocus([this, weakTabPtr](auto&&, auto&&) {
auto tab = weakTabPtr.lock();
if (!tab)
{
return;
}
// Update the focus of the tab's panes
tab->UpdateFocus();
// Possibly update the title of the tab, window to match the newly
// focused pane.
_UpdateTitle(tab);
// Possibly update the icon of the tab.
_UpdateTabIcon(tab);
});
}
// Method Description:
@ -796,11 +776,11 @@ namespace winrt::TerminalApp::implementation
_tabs[focusedTabIndex]->NavigateFocus(direction);
}
winrt::Microsoft::Terminal::TerminalControl::TermControl TerminalPage::_GetFocusedControl()
winrt::Microsoft::Terminal::TerminalControl::TermControl TerminalPage::_GetActiveControl()
{
int focusedTabIndex = _GetFocusedTabIndex();
auto focusedTab = _tabs[focusedTabIndex];
return focusedTab->GetFocusedTerminalControl();
return focusedTab->GetActiveTerminalControl();
}
// Method Description:
@ -977,7 +957,7 @@ namespace winrt::TerminalApp::implementation
{
delta = std::clamp(delta, -1, 1);
const auto focusedTabIndex = _GetFocusedTabIndex();
const auto control = _GetFocusedControl();
const auto control = _GetActiveControl();
const auto termHeight = control.GetViewHeight();
_tabs[focusedTabIndex]->Scroll(termHeight * delta);
}
@ -998,7 +978,7 @@ namespace winrt::TerminalApp::implementation
{
try
{
if (auto focusedControl{ _GetFocusedControl() })
if (auto focusedControl{ _GetActiveControl() })
{
return focusedControl.Title();
}
@ -1161,7 +1141,7 @@ namespace winrt::TerminalApp::implementation
// - true iff we we able to copy text (if a selection was active)
bool TerminalPage::_CopyText(const bool trimTrailingWhitespace)
{
const auto control = _GetFocusedControl();
const auto control = _GetActiveControl();
return control.CopySelectionToClipboard(trimTrailingWhitespace);
}
@ -1169,7 +1149,7 @@ namespace winrt::TerminalApp::implementation
// - Paste text from the Windows Clipboard to the focused terminal
void TerminalPage::_PasteText()
{
const auto control = _GetFocusedControl();
const auto control = _GetActiveControl();
control.PasteTextFromClipboard();
}

View file

@ -94,7 +94,7 @@ namespace winrt::TerminalApp::implementation
bool _SelectTab(const int tabIndex);
void _MoveFocus(const Direction& direction);
winrt::Microsoft::Terminal::TerminalControl::TermControl _GetFocusedControl();
winrt::Microsoft::Terminal::TerminalControl::TermControl _GetActiveControl();
int _GetFocusedTabIndex() const;
void _SetFocusedTabIndex(int tabIndex);
void _CloseFocusedTab();

View file

@ -63,13 +63,13 @@ private:
// signatures and define them both for you, because they don't really vary from
// event to event.
// Use this in a classes header if you have a Windows.Foundation.TypedEventHandler
#define TYPED_EVENT(name, sender, args) \
public: \
winrt::event_token name(Windows::Foundation::TypedEventHandler<sender, args> const& handler) { return _##name##Handlers.add(handler); } \
void name(winrt::event_token const& token) noexcept { _##name##Handlers.remove(token); } \
\
private: \
winrt::event<Windows::Foundation::TypedEventHandler<sender, args>> _##name##Handlers;
#define TYPED_EVENT(name, sender, args) \
public: \
winrt::event_token name(winrt::Windows::Foundation::TypedEventHandler<sender, args> const& handler) { return _##name##Handlers.add(handler); } \
void name(winrt::event_token const& token) noexcept { _##name##Handlers.remove(token); } \
\
private: \
winrt::event<winrt::Windows::Foundation::TypedEventHandler<sender, args>> _##name##Handlers;
// This is a helper macro for both declaring the signature and body of an event
// which is exposed by one class, but actually handled entirely by one of the