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:
parent
fd37e1dc9f
commit
a77b49406c
85
src/cascadia/TerminalApp/TabHeaderControl.cpp
Normal file
85
src/cascadia/TerminalApp/TabHeaderControl.cpp
Normal 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());
|
||||
}
|
||||
}
|
37
src/cascadia/TerminalApp/TabHeaderControl.h
Normal file
37
src/cascadia/TerminalApp/TabHeaderControl.h
Normal 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);
|
||||
}
|
18
src/cascadia/TerminalApp/TabHeaderControl.idl
Normal file
18
src/cascadia/TerminalApp/TabHeaderControl.idl
Normal 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;
|
||||
}
|
||||
}
|
30
src/cascadia/TerminalApp/TabHeaderControl.xaml
Normal file
30
src/cascadia/TerminalApp/TabHeaderControl.xaml
Normal 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=""
|
||||
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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue