diff --git a/src/cascadia/TerminalApp/TabHeaderControl.cpp b/src/cascadia/TerminalApp/TabHeaderControl.cpp new file mode 100644 index 000000000..4612ea7e4 --- /dev/null +++ b/src/cascadia/TerminalApp/TabHeaderControl.cpp @@ -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()); + } +} diff --git a/src/cascadia/TerminalApp/TabHeaderControl.h b/src/cascadia/TerminalApp/TabHeaderControl.h new file mode 100644 index 000000000..367ce4317 --- /dev/null +++ b/src/cascadia/TerminalApp/TabHeaderControl.h @@ -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(); + 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); +} diff --git a/src/cascadia/TerminalApp/TabHeaderControl.idl b/src/cascadia/TerminalApp/TabHeaderControl.idl new file mode 100644 index 000000000..a9dd3f571 --- /dev/null +++ b/src/cascadia/TerminalApp/TabHeaderControl.idl @@ -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; + } +} diff --git a/src/cascadia/TerminalApp/TabHeaderControl.xaml b/src/cascadia/TerminalApp/TabHeaderControl.xaml new file mode 100644 index 000000000..4c09b3b0d --- /dev/null +++ b/src/cascadia/TerminalApp/TabHeaderControl.xaml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj index f653d7d42..bffa63b7a 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj @@ -53,6 +53,9 @@ Designer + + + Designer Designer @@ -89,6 +92,9 @@ TabRowControl.xaml + + TabHeaderControl.xaml + HighlightedTextControl.xaml @@ -154,6 +160,9 @@ TabRowControl.xaml + + TabHeaderControl.xaml + HighlightedTextControl.xaml @@ -230,6 +239,10 @@ TabRowControl.xaml Code + + TabHeaderControl.xaml + Code + HighlightedTextControl.xaml Code diff --git a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters index 8f89e5829..3d424c399 100644 --- a/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters +++ b/src/cascadia/TerminalApp/TerminalAppLib.vcxproj.filters @@ -106,6 +106,9 @@ controls + + controls + controls diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 91ce8e76e..e50c5de89 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -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 // - 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: - // - - // Return Value: - // - - 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: - // - - void TerminalTab::_ConstructTabRenameBox(const winrt::hstring& tabText) - { - if (TabViewItem().Header().try_as()) - { - 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() }; - 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() }; - 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() }; - 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()); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 773be21e8..b228b45a1 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -81,6 +81,7 @@ namespace winrt::TerminalApp::implementation std::optional _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); winrt::hstring _GetActiveTitle() const; - void _UpdateTabHeader(); - void _ConstructTabRenameBox(const winrt::hstring& tabText); void _RecalculateAndApplyTabColor(); void _ApplyTabColor(const winrt::Windows::UI::Color& color);