Make tab header a custom control (#8227)

This PR makes the Header of TabViewItem a custom user control.

## Validation Steps Performed
Manual testing

Closes #8201
This commit is contained in:
PankajBhojwani 2020-11-20 12:16:38 -05:00 committed by GitHub
parent fd37e1dc9f
commit a77b49406c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 205 additions and 168 deletions

View file

@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TabHeaderControl.h"
#include "TabHeaderControl.g.cpp"
using namespace winrt;
using namespace winrt::Microsoft::UI::Xaml;
namespace winrt::TerminalApp::implementation
{
TabHeaderControl::TabHeaderControl()
{
InitializeComponent();
// We'll only process the KeyUp event if we received an initial KeyDown event first.
// Avoids issue immediately closing the tab rename when we see the enter KeyUp event that was
// sent to the command palette to trigger the openTabRenamer action in the first place.
HeaderRenamerTextBox().KeyDown([&](auto&&, auto&&) {
_receivedKeyDown = true;
});
// NOTE: (Preview)KeyDown does not work here. If you use that, we'll
// remove the TextBox from the UI tree, then the following KeyUp
// will bubble to the NewTabButton, which we don't want to have
// happen.
HeaderRenamerTextBox().KeyUp([&](auto&&, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) {
if (_receivedKeyDown)
{
if (e.OriginalKey() == Windows::System::VirtualKey::Enter)
{
// User is done making changes, close the rename box
_CloseRenameBox();
}
else if (e.OriginalKey() == Windows::System::VirtualKey::Escape)
{
// User wants to discard the changes they made,
// reset the rename box text to the old text and close the rename box
HeaderRenamerTextBox().Text(Title());
_CloseRenameBox();
}
}
});
}
// Method Description:
// - Show the tab rename box for the user to rename the tab title
// - We automatically use the previous title as the initial text of the box
void TabHeaderControl::BeginRename()
{
_receivedKeyDown = false;
HeaderTextBlock().Visibility(Windows::UI::Xaml::Visibility::Collapsed);
HeaderRenamerTextBox().Visibility(Windows::UI::Xaml::Visibility::Visible);
HeaderRenamerTextBox().Text(Title());
HeaderRenamerTextBox().SelectAll();
HeaderRenamerTextBox().Focus(Windows::UI::Xaml::FocusState::Programmatic);
}
// Method Description:
// - Event handler for when the rename box loses focus
// - When the rename box loses focus, we use the text in it as the new title
// (i.e. we commit the change instead of cancelling it)
void TabHeaderControl::RenameBoxLostFocusHandler(Windows::Foundation::IInspectable const& /*sender*/,
Windows::UI::Xaml::RoutedEventArgs const& /*e*/)
{
_CloseRenameBox();
}
// Method Description:
// - Hides the rename box and displays the title text block
// - Sends an event to the hosting tab to let them know we wish to change the title
// to whatever is in the renamer box right now - the tab will process that event
// and tell us to update our title
void TabHeaderControl::_CloseRenameBox()
{
HeaderRenamerTextBox().Visibility(Windows::UI::Xaml::Visibility::Collapsed);
HeaderTextBlock().Visibility(Windows::UI::Xaml::Visibility::Visible);
_TitleChangeRequestedHandlers(HeaderRenamerTextBox().Text());
}
}

View file

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "winrt/Microsoft.UI.Xaml.Controls.h"
#include "inc/cppwinrt_utils.h"
#include "TabHeaderControl.g.h"
namespace winrt::TerminalApp::implementation
{
struct TabHeaderControl : TabHeaderControlT<TabHeaderControl>
{
TabHeaderControl();
void BeginRename();
void RenameBoxLostFocusHandler(winrt::Windows::Foundation::IInspectable const& sender,
winrt::Windows::UI::Xaml::RoutedEventArgs const& e);
WINRT_CALLBACK(TitleChangeRequested, TerminalApp::TitleChangeRequestedArgs);
WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
OBSERVABLE_GETSET_PROPERTY(winrt::hstring, Title, _PropertyChangedHandlers);
OBSERVABLE_GETSET_PROPERTY(bool, IsPaneZoomed, _PropertyChangedHandlers);
private:
bool _receivedKeyDown{ false };
void _CloseRenameBox();
};
}
namespace winrt::TerminalApp::factory_implementation
{
BASIC_FACTORY(TabHeaderControl);
}

View file

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
namespace TerminalApp
{
delegate void TitleChangeRequestedArgs(String title);
[default_interface] runtimeclass TabHeaderControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged
{
String Title { get; set; };
Boolean IsPaneZoomed { get; set; };
TabHeaderControl();
void BeginRename();
event TitleChangeRequestedArgs TitleChangeRequested;
}
}

View file

@ -0,0 +1,30 @@
<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under
the MIT License. See LICENSE in the project root for license information. -->
<UserControl
x:Class="TerminalApp.TabHeaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TerminalApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel x:Name="HeaderStackPanel"
Orientation="Horizontal">
<FontIcon x:Name="HeaderZoomIcon"
FontFamily="Segoe MDL2 Assets"
Visibility="{x:Bind IsPaneZoomed, Mode=OneWay}"
Glyph="&#xE8A3;"
FontSize="12"
Margin="0,0,8,0"/>
<TextBlock x:Name="HeaderTextBlock"
Visibility="Visible"
Text="{x:Bind Title, Mode=OneWay}"/>
<TextBox x:Name="HeaderRenamerTextBox"
Visibility="Collapsed"
MinHeight="0"
Padding="4,0,4,0"
Margin="0,-8,0,-8"
LostFocus="RenameBoxLostFocusHandler"/>
</StackPanel>
</UserControl>

View file

@ -53,6 +53,9 @@
</Page>
<Page Include="TabRowControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="TabHeaderControl.xaml">
<SubType>Designer</SubType>
</Page>
<Page Include="HighlightedTextControl.xaml">
<SubType>Designer</SubType>
@ -89,6 +92,9 @@
<ClInclude Include="TabRowControl.h">
<DependentUpon>TabRowControl.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="TabHeaderControl.h">
<DependentUpon>TabHeaderControl.xaml</DependentUpon>
</ClInclude>
<ClInclude Include="HighlightedTextControl.h">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClInclude>
@ -154,6 +160,9 @@
<ClCompile Include="TabRowControl.cpp">
<DependentUpon>TabRowControl.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="TabHeaderControl.cpp">
<DependentUpon>TabHeaderControl.xaml</DependentUpon>
</ClCompile>
<ClCompile Include="HighlightedTextControl.cpp">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
</ClCompile>
@ -230,6 +239,10 @@
<DependentUpon>TabRowControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="TabHeaderControl.idl">
<DependentUpon>TabHeaderControl.xaml</DependentUpon>
<SubType>Code</SubType>
</Midl>
<Midl Include="HighlightedTextControl.idl">
<DependentUpon>HighlightedTextControl.xaml</DependentUpon>
<SubType>Code</SubType>

View file

@ -106,6 +106,9 @@
<Page Include="TabRowControl.xaml">
<Filter>controls</Filter>
</Page>
<Page Include="TabHeaderControl.xaml">
<Filter>controls</Filter>
</Page>
<Page Include="TerminalPage.xaml">
<Filter>controls</Filter>
</Page>

View file

@ -37,6 +37,17 @@ namespace winrt::TerminalApp::implementation
_MakeTabViewItem();
_CreateContextMenu();
// Add an event handler for the header control to tell us when they want their title to change
_headerControl.TitleChangeRequested([weakThis = get_weak()](auto&& title) {
if (auto tab{ weakThis.get() })
{
tab->SetTabText(title);
}
});
// Use our header control as the TabViewItem's header
TabViewItem().Header(_headerControl);
}
// Method Description:
@ -211,14 +222,15 @@ namespace winrt::TerminalApp::implementation
co_await winrt::resume_foreground(TabViewItem().Dispatcher());
if (auto tab{ weakThis.get() })
{
const auto activeTitle = _GetActiveTitle();
// Bubble our current tab text to anyone who's listening for changes.
Title(_GetActiveTitle());
Title(activeTitle);
// Update SwitchToTab command's name
SwitchToTabCommand().Name(Title());
// Update the UI to reflect the changed
_UpdateTabHeader();
// Update the control to reflect the changed title
_headerControl.Title(activeTitle);
}
}
@ -368,9 +380,7 @@ namespace winrt::TerminalApp::implementation
// - <none>
void TerminalTab::ActivateTabRenamer()
{
_inRename = true;
_receivedKeyDown = false;
_UpdateTabHeader();
_headerControl.BeginRename();
}
// Method Description:
@ -580,164 +590,6 @@ namespace winrt::TerminalApp::implementation
TabViewItem().ContextFlyout(newTabFlyout);
}
// Method Description:
// - This will update the contents of our TabViewItem for our current state.
// - If we're not in a rename, we'll set the Header of the TabViewItem to
// simply our current tab text (either the runtime tab text or the
// active terminal's text).
// - If we're in a rename, then we'll set the Header to a TextBox with the
// current tab text. The user can then use that TextBox to set a string
// to use as an override for the tab's text.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalTab::_UpdateTabHeader()
{
winrt::hstring tabText{ Title() };
if (!_inRename)
{
if (_zoomedPane)
{
Controls::StackPanel sp;
sp.Orientation(Controls::Orientation::Horizontal);
Controls::FontIcon ico;
ico.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" });
ico.Glyph(L"\xE8A3"); // "ZoomIn", a magnifying glass with a '+' in it.
ico.FontSize(12);
ico.Margin(ThicknessHelper::FromLengths(0, 0, 8, 0));
sp.Children().Append(ico);
Controls::TextBlock tb;
tb.Text(tabText);
sp.Children().Append(tb);
TabViewItem().Header(sp);
}
else
{
// If we're not currently in the process of renaming the tab,
// then just set the tab's text to whatever our active title is.
TabViewItem().Header(winrt::box_value(tabText));
}
}
else
{
_ConstructTabRenameBox(tabText);
}
}
// Method Description:
// - Create a new TextBox to use as the control for renaming the tab text.
// If the text box is already created, then this will do nothing, and
// leave the current box unmodified.
// Arguments:
// - tabText: This should be the text to initialize the rename text box with.
// Return Value:
// - <none>
void TerminalTab::_ConstructTabRenameBox(const winrt::hstring& tabText)
{
if (TabViewItem().Header().try_as<Controls::TextBox>())
{
return;
}
Controls::TextBox tabTextBox;
tabTextBox.Text(tabText);
// The TextBox has a MinHeight already set by default, which is
// larger than we want. Get rid of it.
tabTextBox.MinHeight(0);
// Also get rid of the internal padding on the text box, between the
// border and the text content, on the top and bottom. This will
// help the box fit within the bounds of the tab.
Thickness internalPadding = ThicknessHelper::FromLengths(4, 0, 4, 0);
tabTextBox.Padding(internalPadding);
// Make the margin (0, -8, 0, -8), to counteract the padding that
// the TabViewItem has.
//
// This is maybe a bit fragile, as the actual value might not be exactly
// (0, 8, 0, 8), but using TabViewItemHeaderPadding to look up the real
// value at runtime didn't work. So this is good enough for now.
Thickness negativeMargins = ThicknessHelper::FromLengths(0, -8, 0, -8);
tabTextBox.Margin(negativeMargins);
// Set up some event handlers on the text box. We need three of them:
// * A LostFocus event, so when the TextBox loses focus, we'll
// remove it and return to just the text on the tab.
// * A KeyUp event, to be able to submit the tab text on Enter or
// dismiss the text box on Escape
// * A LayoutUpdated event, so that we can auto-focus the text box
// when it's added to the tree.
auto weakThis{ get_weak() };
// When the text box loses focus, update the tab title of our tab.
// - If there are any contents in the box, we'll use that value as
// the new "runtime text", which will override any text set by the
// application.
// - If the text box is empty, we'll reset the "runtime text", and
// return to using the active terminal's title.
tabTextBox.LostFocus([weakThis](const IInspectable& sender, auto&&) {
auto tab{ weakThis.get() };
auto textBox{ sender.try_as<Controls::TextBox>() };
if (tab && textBox)
{
tab->_runtimeTabText = textBox.Text();
tab->_inRename = false;
tab->UpdateTitle();
}
});
// We'll only process the KeyUp event if we received an initial KeyDown event first.
// Avoids issue immediately closing the tab rename when we see the enter KeyUp event that was
// sent to the command palette to trigger the openTabRenamer action in the first place.
tabTextBox.KeyDown([weakThis](const IInspectable&, Input::KeyRoutedEventArgs const&) {
auto tab{ weakThis.get() };
tab->_receivedKeyDown = true;
});
// NOTE: (Preview)KeyDown does not work here. If you use that, we'll
// remove the TextBox from the UI tree, then the following KeyUp
// will bubble to the NewTabButton, which we don't want to have
// happen.
tabTextBox.KeyUp([weakThis](const IInspectable& sender, Input::KeyRoutedEventArgs const& e) {
auto tab{ weakThis.get() };
auto textBox{ sender.try_as<Controls::TextBox>() };
if (tab && textBox && tab->_receivedKeyDown)
{
switch (e.OriginalKey())
{
case VirtualKey::Enter:
tab->_runtimeTabText = textBox.Text();
[[fallthrough]];
case VirtualKey::Escape:
e.Handled(true);
textBox.Text(tab->_runtimeTabText);
tab->_inRename = false;
tab->UpdateTitle();
break;
}
}
});
// As soon as the text box is added to the UI tree, focus it. We can't focus it till it's in the tree.
_tabRenameBoxLayoutUpdatedRevoker = tabTextBox.LayoutUpdated(winrt::auto_revoke, [this](auto&&, auto&&) {
// Curiously, the sender for this event is null, so we have to
// get the TextBox from the Tab's Header().
auto textBox{ TabViewItem().Header().try_as<Controls::TextBox>() };
if (textBox)
{
textBox.SelectAll();
textBox.Focus(FocusState::Programmatic);
}
// Only let this succeed once.
_tabRenameBoxLayoutUpdatedRevoker.revoke();
});
TabViewItem().Header(tabTextBox);
}
// Method Description:
// Returns the tab color, if any
// Arguments:
@ -1012,7 +864,7 @@ namespace winrt::TerminalApp::implementation
_zoomedPane = _activePane;
_rootPane->Maximize(_zoomedPane);
// Update the tab header to show the magnifying glass
_UpdateTabHeader();
_headerControl.IsPaneZoomed(true);
Content(_zoomedPane->GetRootElement());
}
void TerminalTab::ExitZoom()
@ -1020,7 +872,7 @@ namespace winrt::TerminalApp::implementation
_rootPane->Restore(_zoomedPane);
_zoomedPane = nullptr;
// Update the tab header to hide the magnifying glass
_UpdateTabHeader();
_headerControl.IsPaneZoomed(false);
Content(_rootPane->GetRootElement());
}

View file

@ -81,6 +81,7 @@ namespace winrt::TerminalApp::implementation
std::optional<winrt::Windows::UI::Color> _runtimeTabColor{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeOtherTabsMenuItem{};
winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _closeTabsAfterMenuItem{};
winrt::TerminalApp::TabHeaderControl _headerControl{};
bool _receivedKeyDown{ false };
@ -104,8 +105,6 @@ namespace winrt::TerminalApp::implementation
void _UpdateActivePane(std::shared_ptr<Pane> pane);
winrt::hstring _GetActiveTitle() const;
void _UpdateTabHeader();
void _ConstructTabRenameBox(const winrt::hstring& tabText);
void _RecalculateAndApplyTabColor();
void _ApplyTabColor(const winrt::Windows::UI::Color& color);