mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 00:48:23 -06:00
Between fmt 7.1.3 and 11.0.2 a lot has happened. `wchar_t` support is now more limited and implicit conversions don't work anymore. Furthermore, even the non-`FMT_COMPILE` API is now compile-time checked and so it fails to work in our UI code which passes `hstring` format strings which aren't implicitly convertible to the expected type. `fmt::runtime` was introduced for this but it also fails to work for `hstring` parameters. To solve this, a new `RS_fmt` macro was added to abstract the added `std::wstring_view` casting away. Finally, some additional changes to reduce `stringstream` usage have been made, whenever `format_to`, etc., is available. This mostly affects `ActionArgs.cpp`. Closes #16000 ## Validation Steps Performed * Compiles ✅ * Settings page opens ✅
1117 lines
43 KiB
C++
1117 lines
43 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
//
|
|
// This file contains much of the code related to tab management for the
|
|
// TerminalPage. Things like opening new tabs, selecting different tabs,
|
|
// switching tabs, should all be handled in this file. Hypothetically, in the
|
|
// future, the contents of this file could be moved to a separate class
|
|
// entirely.
|
|
//
|
|
|
|
#include "pch.h"
|
|
#include "TerminalPage.h"
|
|
#include "Utils.h"
|
|
#include "../../types/inc/utils.hpp"
|
|
#include "../../inc/til/string.h"
|
|
#include <til/io.h>
|
|
|
|
#include <LibraryResources.h>
|
|
|
|
#include "TabRowControl.h"
|
|
#include "ColorHelper.h"
|
|
#include "DebugTapConnection.h"
|
|
#include "..\TerminalSettingsModel\FileUtils.h"
|
|
|
|
#include <shlobj.h>
|
|
|
|
using namespace winrt;
|
|
using namespace winrt::Windows::Foundation::Collections;
|
|
using namespace winrt::Windows::UI::Xaml;
|
|
using namespace winrt::Windows::UI::Xaml::Controls;
|
|
using namespace winrt::Windows::UI::Core;
|
|
using namespace winrt::Windows::System;
|
|
using namespace winrt::Windows::ApplicationModel::DataTransfer;
|
|
using namespace winrt::Windows::UI::Text;
|
|
using namespace winrt::Windows::Storage;
|
|
using namespace winrt::Windows::Storage::Pickers;
|
|
using namespace winrt::Windows::Storage::Provider;
|
|
using namespace winrt::Microsoft::Terminal;
|
|
using namespace winrt::Microsoft::Terminal::Control;
|
|
using namespace winrt::Microsoft::Terminal::TerminalConnection;
|
|
using namespace winrt::Microsoft::Terminal::Settings::Model;
|
|
using namespace ::TerminalApp;
|
|
using namespace ::Microsoft::Console;
|
|
|
|
namespace winrt
|
|
{
|
|
namespace MUX = Microsoft::UI::Xaml;
|
|
namespace WUX = Windows::UI::Xaml;
|
|
using IInspectable = Windows::Foundation::IInspectable;
|
|
}
|
|
|
|
namespace winrt::TerminalApp::implementation
|
|
{
|
|
// Method Description:
|
|
// - Open a new tab. This will create the TerminalControl hosting the
|
|
// terminal, and add a new Tab to our list of tabs. The method can
|
|
// optionally be provided a NewTerminalArgs, which will be used to create
|
|
// a tab using the values in that object.
|
|
// Arguments:
|
|
// - newTerminalArgs: An object that may contain a blob of parameters to
|
|
// control which profile is created and with possible other
|
|
// configurations. See TerminalSettings::CreateWithNewTerminalArgs for more details.
|
|
// - existingConnection: An optional connection that is already established to a PTY
|
|
// for this tab to host instead of creating one.
|
|
// If not defined, the tab will create the connection.
|
|
HRESULT TerminalPage::_OpenNewTab(const INewContentArgs& newContentArgs)
|
|
try
|
|
{
|
|
if (const auto& newTerminalArgs{ newContentArgs.try_as<NewTerminalArgs>() })
|
|
{
|
|
const auto profile{ _settings.GetProfileForArgs(newTerminalArgs) };
|
|
// GH#11114: GetProfileForArgs can return null if the index is higher
|
|
// than the number of available profiles.
|
|
if (!profile)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
const auto settings{ TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings) };
|
|
|
|
// Try to handle auto-elevation
|
|
if (_maybeElevate(newTerminalArgs, settings, profile))
|
|
{
|
|
return S_OK;
|
|
}
|
|
// We can't go in the other direction (elevated->unelevated)
|
|
// unfortunately. This seems to be due to Centennial quirks. It works
|
|
// unpackaged, but not packaged.
|
|
}
|
|
|
|
// This call to _MakePane won't return nullptr, we already checked that
|
|
// case above with the _maybeElevate call.
|
|
_CreateNewTabFromPane(_MakePane(newContentArgs, nullptr));
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN();
|
|
|
|
// Method Description:
|
|
// - Sets up state, event handlers, etc on a tab object that was just made.
|
|
// Arguments:
|
|
// - newTabImpl: the uninitialized tab.
|
|
// - insertPosition: Optional parameter to indicate the position of tab.
|
|
void TerminalPage::_InitializeTab(winrt::com_ptr<TerminalTab> newTabImpl, uint32_t insertPosition)
|
|
{
|
|
newTabImpl->Initialize();
|
|
|
|
// If insert position is not passed, calculate it
|
|
if (insertPosition == -1)
|
|
{
|
|
insertPosition = _tabs.Size();
|
|
if (_settings.GlobalSettings().NewTabPosition() == NewTabPosition::AfterCurrentTab)
|
|
{
|
|
auto currentTabIndex = _GetFocusedTabIndex();
|
|
if (currentTabIndex.has_value())
|
|
{
|
|
insertPosition = currentTabIndex.value() + 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the new tab to the list of our tabs.
|
|
_tabs.InsertAt(insertPosition, *newTabImpl);
|
|
_mruTabs.Append(*newTabImpl);
|
|
|
|
newTabImpl->SetDispatch(*_actionDispatch);
|
|
newTabImpl->SetActionMap(_settings.ActionMap());
|
|
|
|
// Give the tab its index in the _tabs vector so it can manage its own SwitchToTab command.
|
|
_UpdateTabIndices();
|
|
|
|
// Hookup our event handlers to the new terminal
|
|
_RegisterTabEvents(*newTabImpl);
|
|
|
|
// 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.
|
|
auto weakTab = make_weak(newTabImpl);
|
|
|
|
// When the tab's active pane changes, we'll want to lookup a new icon
|
|
// for it. The Title change will be propagated upwards through the tab's
|
|
// PropertyChanged event handler.
|
|
newTabImpl->ActivePaneChanged({ get_weak(), &TerminalPage::_activePaneChanged });
|
|
|
|
// The RaiseVisualBell event has been bubbled up to here from the pane,
|
|
// the next part of the chain is bubbling up to app logic, which will
|
|
// forward it to app host.
|
|
newTabImpl->TabRaiseVisualBell([weakTab, weakThis{ get_weak() }]() {
|
|
auto page{ weakThis.get() };
|
|
auto tab{ weakTab.get() };
|
|
|
|
if (page && tab)
|
|
{
|
|
page->RaiseVisualBell.raise(nullptr, nullptr);
|
|
}
|
|
});
|
|
|
|
auto tabViewItem = newTabImpl->TabViewItem();
|
|
_tabView.TabItems().InsertAt(insertPosition, tabViewItem);
|
|
|
|
// Set this tab's icon to the icon from the content
|
|
_UpdateTabIcon(*newTabImpl);
|
|
|
|
tabViewItem.PointerReleased({ this, &TerminalPage::_OnTabClick });
|
|
|
|
// When the tab requests close, try to close it (prompt for approval, if required)
|
|
newTabImpl->CloseRequested([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
|
|
auto page{ weakThis.get() };
|
|
auto tab{ weakTab.get() };
|
|
|
|
if (page && tab)
|
|
{
|
|
page->_HandleCloseTabRequested(*tab);
|
|
}
|
|
});
|
|
|
|
// When the tab is closed, remove it from our list of tabs.
|
|
newTabImpl->Closed([weakTab, weakThis{ get_weak() }](auto&& /*s*/, auto&& /*e*/) {
|
|
const auto page = weakThis.get();
|
|
const auto tab = weakTab.get();
|
|
|
|
if (page && tab)
|
|
{
|
|
page->_RemoveTab(*tab);
|
|
}
|
|
});
|
|
|
|
// The tab might want us to toss focus into the control, especially when
|
|
// transient UIs (like the context menu, or the renamer) are dismissed.
|
|
newTabImpl->RequestFocusActiveControl([weakThis{ get_weak() }]() {
|
|
if (const auto page{ weakThis.get() })
|
|
{
|
|
page->_FocusCurrentTab(false);
|
|
}
|
|
});
|
|
|
|
// This kicks off TabView::SelectionChanged, in response to which
|
|
// we'll attach the terminal's Xaml control to the Xaml root.
|
|
_tabView.SelectedItem(tabViewItem);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Create a new tab using a specified pane as the root.
|
|
// Arguments:
|
|
// - pane: The pane to use as the root.
|
|
// - insertPosition: Optional parameter to indicate the position of tab.
|
|
TerminalApp::TerminalTab TerminalPage::_CreateNewTabFromPane(std::shared_ptr<Pane> pane, uint32_t insertPosition)
|
|
{
|
|
if (pane)
|
|
{
|
|
auto newTabImpl = winrt::make_self<TerminalTab>(pane);
|
|
_InitializeTab(newTabImpl, insertPosition);
|
|
return *newTabImpl;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Get the icon of the currently focused terminal control, and set its
|
|
// tab's icon to that icon.
|
|
// Arguments:
|
|
// - tab: the Tab to update the title for.
|
|
void TerminalPage::_UpdateTabIcon(TerminalTab& tab)
|
|
{
|
|
if (const auto content{ tab.GetActiveContent() })
|
|
{
|
|
const auto& icon{ content.Icon() };
|
|
const auto theme = _settings.GlobalSettings().CurrentTheme();
|
|
const auto iconStyle = (theme && theme.Tab()) ? theme.Tab().IconStyle() : IconStyle::Default;
|
|
|
|
tab.UpdateIcon(icon, iconStyle);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Handle changes to the tab width set by the user
|
|
void TerminalPage::_UpdateTabWidthMode()
|
|
{
|
|
_tabView.TabWidthMode(_settings.GlobalSettings().TabWidthMode());
|
|
}
|
|
|
|
// Method Description:
|
|
// - Handle changes in tab layout.
|
|
void TerminalPage::_UpdateTabView()
|
|
{
|
|
// Never show the tab row when we're fullscreen. Otherwise:
|
|
// Show tabs when there's more than 1, or the user has chosen to always
|
|
// show the tab bar.
|
|
const auto isVisible = (!_isFullscreen && !_isInFocusMode) &&
|
|
(_settings.GlobalSettings().ShowTabsInTitlebar() ||
|
|
(_tabs.Size() > 1) ||
|
|
_settings.GlobalSettings().AlwaysShowTabs());
|
|
|
|
if (_tabView)
|
|
{
|
|
// collapse/show the tabs themselves
|
|
_tabView.Visibility(isVisible ? Visibility::Visible : Visibility::Collapsed);
|
|
}
|
|
if (_tabRow)
|
|
{
|
|
// collapse/show the row that the tabs are in.
|
|
// NaN is the special value XAML uses for "Auto" sizing.
|
|
_tabRow.Height(isVisible ? NAN : 0);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Duplicates the current focused tab
|
|
void TerminalPage::_DuplicateFocusedTab()
|
|
{
|
|
if (const auto terminalTab{ _GetFocusedTabImpl() })
|
|
{
|
|
_DuplicateTab(*terminalTab);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Duplicates specified tab
|
|
// Arguments:
|
|
// - tab: tab to duplicate
|
|
void TerminalPage::_DuplicateTab(const TerminalTab& tab)
|
|
{
|
|
try
|
|
{
|
|
// TODO: GH#5047 - We're duplicating the whole profile, which might
|
|
// be a dangling reference to old settings.
|
|
//
|
|
// In the future, it may be preferable to just duplicate the
|
|
// current control's live settings (which will include changes
|
|
// made through VT).
|
|
uint32_t insertPosition = _tabs.Size();
|
|
if (_settings.GlobalSettings().NewTabPosition() == NewTabPosition::AfterCurrentTab)
|
|
{
|
|
insertPosition = tab.TabViewIndex() + 1;
|
|
}
|
|
_CreateNewTabFromPane(_MakePane(nullptr, tab, nullptr), insertPosition);
|
|
|
|
const auto runtimeTabText{ tab.GetTabText() };
|
|
if (!runtimeTabText.empty())
|
|
{
|
|
if (auto newTab{ _GetFocusedTabImpl() })
|
|
{
|
|
newTab->SetTabText(runtimeTabText);
|
|
}
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Exports the content of the Terminal Buffer inside the tab
|
|
// Arguments:
|
|
// - tab: tab to export
|
|
winrt::fire_and_forget TerminalPage::_ExportTab(const TerminalTab& tab, winrt::hstring filepath)
|
|
{
|
|
// This will be used to set up the file picker "filter", to select .txt
|
|
// files by default.
|
|
static constexpr COMDLG_FILTERSPEC supportedFileTypes[] = {
|
|
{ L"Text Files (*.txt)", L"*.txt" },
|
|
{ L"All Files (*.*)", L"*.*" }
|
|
};
|
|
// An arbitrary GUID to associate with all instances of this
|
|
// dialog, so they all re-open in the same path as they were
|
|
// open before:
|
|
static constexpr winrt::guid clientGuidExportFile{ 0xF6AF20BB, 0x0800, 0x48E6, { 0xB0, 0x17, 0xA1, 0x4C, 0xD8, 0x73, 0xDD, 0x58 } };
|
|
|
|
try
|
|
{
|
|
if (const auto control{ tab.GetActiveTerminalControl() })
|
|
{
|
|
auto path = filepath;
|
|
|
|
if (path.empty())
|
|
{
|
|
// GH#11356 - we can't use the UWP apis for writing the file,
|
|
// because they don't work elevated (shocker) So just use the
|
|
// shell32 file picker manually.
|
|
std::wstring filename{ tab.Title() };
|
|
filename = til::clean_filename(filename);
|
|
path = co_await SaveFilePicker(*_hostingHwnd, [filename = std::move(filename)](auto&& dialog) {
|
|
THROW_IF_FAILED(dialog->SetClientGuid(clientGuidExportFile));
|
|
try
|
|
{
|
|
// Default to the Downloads folder
|
|
auto folderShellItem{ winrt::capture<IShellItem>(&SHGetKnownFolderItem, FOLDERID_Downloads, KF_FLAG_DEFAULT, nullptr) };
|
|
dialog->SetDefaultFolder(folderShellItem.get());
|
|
}
|
|
CATCH_LOG(); // non-fatal
|
|
THROW_IF_FAILED(dialog->SetFileTypes(ARRAYSIZE(supportedFileTypes), supportedFileTypes));
|
|
THROW_IF_FAILED(dialog->SetFileTypeIndex(1)); // the array is 1-indexed
|
|
THROW_IF_FAILED(dialog->SetDefaultExtension(L"txt"));
|
|
|
|
// Default to using the tab title as the file name
|
|
THROW_IF_FAILED(dialog->SetFileName((filename + L".txt").c_str()));
|
|
});
|
|
}
|
|
else
|
|
{
|
|
// The file picker isn't going to give us paths with
|
|
// environment variables, but the user might have set one in
|
|
// the settings. Expand those here.
|
|
|
|
path = winrt::hstring{ wil::ExpandEnvironmentStringsW<std::wstring>(path.c_str()) };
|
|
}
|
|
|
|
if (!path.empty())
|
|
{
|
|
const auto buffer = control.ReadEntireBuffer();
|
|
til::io::write_utf8_string_to_file_atomic(std::filesystem::path{ std::wstring_view{ path } }, til::u16u8(buffer));
|
|
}
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Record the configuration information of the last closed thing .
|
|
// - Will occasionally prune the list so it doesn't grow infinitely.
|
|
// Arguments:
|
|
// - args: the list of actions to take to remake the pane/tab
|
|
void TerminalPage::_AddPreviouslyClosedPaneOrTab(std::vector<ActionAndArgs>&& args)
|
|
{
|
|
// Just make sure we don't get infinitely large, but still
|
|
// maintain a large replay buffer.
|
|
if (const auto size = _previouslyClosedPanesAndTabs.size(); size > 150)
|
|
{
|
|
const auto it = _previouslyClosedPanesAndTabs.begin();
|
|
// delete 50 at a time so that we don't have to do an erase
|
|
// of the buffer every time when at capacity.
|
|
_previouslyClosedPanesAndTabs.erase(it, it + (size - 100));
|
|
}
|
|
|
|
_previouslyClosedPanesAndTabs.emplace_back(args);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Removes the tab (both TerminalControl and XAML) after prompting for approval
|
|
// Arguments:
|
|
// - tab: the tab to remove
|
|
winrt::Windows::Foundation::IAsyncAction TerminalPage::_HandleCloseTabRequested(winrt::TerminalApp::TabBase tab)
|
|
{
|
|
if (tab.ReadOnly())
|
|
{
|
|
auto warningResult = co_await _ShowCloseReadOnlyDialog();
|
|
|
|
// If the user didn't explicitly click on close tab - leave
|
|
if (warningResult != ContentDialogResult::Primary)
|
|
{
|
|
co_return;
|
|
}
|
|
}
|
|
|
|
auto t = winrt::get_self<implementation::TabBase>(tab);
|
|
auto actions = t->BuildStartupActions(BuildStartupKind::None);
|
|
_AddPreviouslyClosedPaneOrTab(std::move(actions));
|
|
|
|
_RemoveTab(tab);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Removes the tab (both TerminalControl and XAML)
|
|
// Arguments:
|
|
// - tab: the tab to remove
|
|
void TerminalPage::_RemoveTab(const winrt::TerminalApp::TabBase& tab)
|
|
{
|
|
uint32_t tabIndex{};
|
|
if (!_tabs.IndexOf(tab, tabIndex))
|
|
{
|
|
// The tab is already removed
|
|
return;
|
|
}
|
|
|
|
// We use _removing flag to suppress _OnTabSelectionChanged events
|
|
// that might get triggered while removing
|
|
_removing = true;
|
|
auto unsetRemoving = wil::scope_exit([&]() noexcept { _removing = false; });
|
|
|
|
const auto focusedTabIndex{ _GetFocusedTabIndex() };
|
|
|
|
// Removing the tab from the collection should destroy its control and disconnect its connection,
|
|
// but it doesn't always do so. The UI tree may still be holding the control and preventing its destruction.
|
|
tab.Shutdown();
|
|
|
|
uint32_t mruIndex{};
|
|
if (_mruTabs.IndexOf(tab, mruIndex))
|
|
{
|
|
_mruTabs.RemoveAt(mruIndex);
|
|
}
|
|
|
|
if (tab == _settingsTab)
|
|
{
|
|
_settingsTab = nullptr;
|
|
}
|
|
|
|
if (_stashed.draggedTab && *_stashed.draggedTab == tab)
|
|
{
|
|
_stashed.draggedTab = nullptr;
|
|
}
|
|
|
|
_tabs.RemoveAt(tabIndex);
|
|
_tabView.TabItems().RemoveAt(tabIndex);
|
|
_UpdateTabIndices();
|
|
|
|
// To close the window here, we need to close the hosting window.
|
|
if (_tabs.Size() == 0)
|
|
{
|
|
// If we are supposed to save state, make sure we clear it out
|
|
// if the user manually closed all tabs.
|
|
// Do this only if we are the last window; the monarch will notice
|
|
// we are missing and remove us that way otherwise.
|
|
CloseWindowRequested.raise(*this, nullptr);
|
|
}
|
|
else if (focusedTabIndex.has_value() && focusedTabIndex.value() == gsl::narrow_cast<uint32_t>(tabIndex))
|
|
{
|
|
// Manually select the new tab to get focus, rather than relying on TabView since:
|
|
// 1. We want to customize this behavior (e.g., use MRU logic)
|
|
// 2. In fullscreen (GH#5799) and focus (GH#7916) modes the _OnTabItemsChanged is not fired
|
|
// 3. When rearranging tabs (GH#7916) _OnTabItemsChanged is suppressed
|
|
const auto tabSwitchMode = _settings.GlobalSettings().TabSwitcherMode();
|
|
|
|
if (tabSwitchMode == TabSwitcherMode::MostRecentlyUsed)
|
|
{
|
|
const auto newSelectedTab = _mruTabs.GetAt(0);
|
|
_UpdatedSelectedTab(newSelectedTab);
|
|
_tabView.SelectedItem(newSelectedTab.TabViewItem());
|
|
}
|
|
else
|
|
{
|
|
// We can't use
|
|
// auto selectedIndex = _tabView.SelectedIndex();
|
|
// Because this will always return -1 in this scenario unfortunately.
|
|
//
|
|
// So, what we're going to try to do is move the focus to the tab
|
|
// to the left, within the bounds of how many tabs we have.
|
|
//
|
|
// EX: we have 4 tabs: [A, B, C, D]. If we close:
|
|
// * A (tabIndex=0): We'll want to focus tab B (now in index 0)
|
|
// * B (tabIndex=1): We'll want to focus tab A (now in index 0)
|
|
// * C (tabIndex=2): We'll want to focus tab B (now in index 1)
|
|
// * D (tabIndex=3): We'll want to focus tab C (now in index 2)
|
|
const auto newSelectedIndex = std::clamp<int32_t>(tabIndex - 1, 0, _tabs.Size() - 1);
|
|
// _UpdatedSelectedTab will do the work of setting up the new tab as
|
|
// the focused one, and unfocusing all the others.
|
|
auto newSelectedTab{ _tabs.GetAt(newSelectedIndex) };
|
|
_UpdatedSelectedTab(newSelectedTab);
|
|
|
|
// Also, we need to _manually_ set the SelectedItem of the tabView
|
|
// here. If we don't, then the TabView will technically not have a
|
|
// selected item at all, which can make things like ClosePane not
|
|
// work correctly.
|
|
_tabView.SelectedItem(newSelectedTab.TabViewItem());
|
|
}
|
|
}
|
|
|
|
// GH#5559 - If we were in the middle of a drag/drop, end it by clearing
|
|
// out our state.
|
|
if (_rearranging)
|
|
{
|
|
_rearranging = false;
|
|
_rearrangeFrom = std::nullopt;
|
|
_rearrangeTo = std::nullopt;
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets focus to the tab to the right or left the currently selected tab.
|
|
void TerminalPage::_SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference<Microsoft::Terminal::Settings::Model::TabSwitcherMode>& customTabSwitcherMode)
|
|
{
|
|
const auto index{ _GetFocusedTabIndex().value_or(0) };
|
|
const auto tabSwitchMode = customTabSwitcherMode ? customTabSwitcherMode.Value() : _settings.GlobalSettings().TabSwitcherMode();
|
|
if (tabSwitchMode == TabSwitcherMode::Disabled)
|
|
{
|
|
auto tabCount = _tabs.Size();
|
|
// Wraparound math. By adding tabCount and then calculating
|
|
// modulo tabCount, we clamp the values to the range [0,
|
|
// tabCount) while still supporting moving leftward from 0 to
|
|
// tabCount - 1.
|
|
const auto newTabIndex = ((tabCount + index + (bMoveRight ? 1 : -1)) % tabCount);
|
|
_SelectTab(newTabIndex);
|
|
}
|
|
else
|
|
{
|
|
const auto p = LoadCommandPalette();
|
|
p.SetTabs(_tabs, _mruTabs);
|
|
|
|
// Otherwise, set up the tab switcher in the selected mode, with
|
|
// the given ordering, and make it visible.
|
|
p.EnableTabSwitcherMode(index, tabSwitchMode);
|
|
p.Visibility(Visibility::Visible);
|
|
p.SelectNextItem(bMoveRight);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Sets focus to the desired tab. Returns false if the provided tabIndex
|
|
// is greater than the number of tabs we have.
|
|
// - During startup, we'll immediately set the selected tab as focused.
|
|
// - After startup, we'll dispatch an async method to set the selected
|
|
// item of the TabView, which will then also trigger a
|
|
// TabView::SelectionChanged, handled in
|
|
// TerminalPage::_OnTabSelectionChanged
|
|
// Return Value:
|
|
// true iff we were able to select that tab index, false otherwise
|
|
bool TerminalPage::_SelectTab(uint32_t tabIndex)
|
|
{
|
|
// GH#9369 - if the argument is out of range, then clamp to the number
|
|
// of available tabs. Previously, we'd just silently do nothing if the
|
|
// value was greater than the number of tabs.
|
|
tabIndex = std::clamp(tabIndex, 0u, _tabs.Size() - 1);
|
|
|
|
auto tab{ _tabs.GetAt(tabIndex) };
|
|
// GH#11107 - Always just set the item directly first so that if
|
|
// tab movement is done as part of multiple actions following calls
|
|
// to _GetFocusedTab will return the correct tab.
|
|
_tabView.SelectedItem(tab.TabViewItem());
|
|
|
|
if (_startupState == StartupState::InStartup)
|
|
{
|
|
_UpdatedSelectedTab(tab);
|
|
}
|
|
else
|
|
{
|
|
_SetFocusedTab(tab);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - This method is called once a tab was selected in tab switcher
|
|
// We'll use this event to select the relevant tab
|
|
// Arguments:
|
|
// - tab - tab to select
|
|
// Return Value:
|
|
// - <none>
|
|
void TerminalPage::_OnSwitchToTabRequested(const IInspectable& /*sender*/, const winrt::TerminalApp::TabBase& tab)
|
|
{
|
|
uint32_t index{};
|
|
if (_tabs.IndexOf(tab, index))
|
|
{
|
|
_SelectTab(index);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns the index in our list of tabs of the currently focused tab. If
|
|
// no tab is currently selected, returns nullopt.
|
|
// Return Value:
|
|
// - the index of the currently focused tab if there is one, else nullopt
|
|
std::optional<uint32_t> TerminalPage::_GetFocusedTabIndex() const noexcept
|
|
{
|
|
// GH#1117: This is a workaround because _tabView.SelectedIndex()
|
|
// sometimes return incorrect result after removing some tabs
|
|
uint32_t focusedIndex;
|
|
if (_tabView.TabItems().IndexOf(_tabView.SelectedItem(), focusedIndex))
|
|
{
|
|
return focusedIndex;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Returns the index in our list of tabs of the currently focused tab. If
|
|
// no tab is currently selected, returns nullopt.
|
|
// Return Value:
|
|
// - the index of the currently focused tab if there is one, else nullopt
|
|
std::optional<uint32_t> TerminalPage::_GetTabIndex(const TerminalApp::TabBase& tab) const noexcept
|
|
{
|
|
uint32_t i;
|
|
if (_tabs.IndexOf(tab, i))
|
|
{
|
|
return i;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Method Description:
|
|
// - returns the currently focused tab. This might return null,
|
|
// so make sure to check the result!
|
|
winrt::TerminalApp::TabBase TerminalPage::_GetFocusedTab() const noexcept
|
|
{
|
|
if (auto index{ _GetFocusedTabIndex() })
|
|
{
|
|
return _tabs.GetAt(*index);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - returns a com_ptr to the currently focused tab implementation. This might return null,
|
|
// so make sure to check the result!
|
|
winrt::com_ptr<TerminalTab> TerminalPage::_GetFocusedTabImpl() const noexcept
|
|
{
|
|
if (auto tab{ _GetFocusedTab() })
|
|
{
|
|
return _GetTerminalTabImpl(tab);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - returns a tab corresponding to a view item. This might return null,
|
|
// so make sure to check the result!
|
|
winrt::TerminalApp::TabBase TerminalPage::_GetTabByTabViewItem(const Microsoft::UI::Xaml::Controls::TabViewItem& tabViewItem) const noexcept
|
|
{
|
|
uint32_t tabIndexFromControl{};
|
|
const auto items{ _tabView.TabItems() };
|
|
if (items.IndexOf(tabViewItem, tabIndexFromControl) && tabIndexFromControl < _tabs.Size())
|
|
{
|
|
// If IndexOf returns true, we've actually got an index
|
|
return _tabs.GetAt(tabIndexFromControl);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Method Description:
|
|
// - An async method for changing the focused tab on the UI thread. This
|
|
// method will _only_ set the selected item of the TabView, which will
|
|
// then also trigger a TabView::SelectionChanged event, which we'll handle
|
|
// in TerminalPage::_OnTabSelectionChanged, where we'll mark the new tab
|
|
// as focused.
|
|
// Arguments:
|
|
// - tab: tab to focus.
|
|
// Return Value:
|
|
// - <none>
|
|
winrt::fire_and_forget TerminalPage::_SetFocusedTab(const winrt::TerminalApp::TabBase tab)
|
|
{
|
|
// GH#1117: This is a workaround because _tabView.SelectedIndex(tabIndex)
|
|
// sometimes set focus to an incorrect tab after removing some tabs
|
|
auto weakThis{ get_weak() };
|
|
|
|
if (!_tabView.Dispatcher().HasThreadAccess())
|
|
{
|
|
co_await winrt::resume_foreground(_tabView.Dispatcher());
|
|
}
|
|
|
|
if (auto page{ weakThis.get() })
|
|
{
|
|
// Make sure the tab was not removed
|
|
uint32_t tabIndex{};
|
|
if (_tabs.IndexOf(tab, tabIndex))
|
|
{
|
|
_tabView.SelectedItem(tab.TabViewItem());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Disables read-only mode on pane if the user wishes to close it and read-only mode is enabled.
|
|
// Arguments:
|
|
// - pane: the pane that is about to be closed.
|
|
// Return Value:
|
|
// - bool indicating whether the (read-only) pane can be closed.
|
|
winrt::Windows::Foundation::IAsyncOperation<bool> TerminalPage::_PaneConfirmCloseReadOnly(std::shared_ptr<Pane> pane)
|
|
{
|
|
if (pane->ContainsReadOnly())
|
|
{
|
|
auto warningResult = co_await _ShowCloseReadOnlyDialog();
|
|
|
|
// If the user didn't explicitly click on close tab - leave
|
|
if (warningResult != ContentDialogResult::Primary)
|
|
{
|
|
co_return false;
|
|
}
|
|
|
|
// Clean read-only mode to prevent additional prompt if closing the pane triggers closing of a hosting tab
|
|
pane->WalkTree([](const auto& p) {
|
|
if (const auto control{ p->GetTerminalControl() })
|
|
{
|
|
if (control.ReadOnly())
|
|
{
|
|
control.ToggleReadOnly();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
co_return true;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Removes the pane from the tab it belongs to.
|
|
// Arguments:
|
|
// - pane: the pane to close.
|
|
void TerminalPage::_HandleClosePaneRequested(std::shared_ptr<Pane> pane)
|
|
{
|
|
// Build the list of actions to recreate the closed pane,
|
|
// BuildStartupActions returns the "first" pane and the rest of
|
|
// its actions are assuming that first pane has been created first.
|
|
// This doesn't handle refocusing anything in particular, the
|
|
// result will be that the last pane created is focused. In the
|
|
// case of a single pane that is the desired behavior anyways.
|
|
auto state = pane->BuildStartupActions(0, 1, BuildStartupKind::None);
|
|
{
|
|
ActionAndArgs splitPaneAction{};
|
|
splitPaneAction.Action(ShortcutAction::SplitPane);
|
|
SplitPaneArgs splitPaneArgs{ SplitDirection::Automatic, state.firstPane->GetTerminalArgsForPane(BuildStartupKind::None) };
|
|
splitPaneAction.Args(splitPaneArgs);
|
|
|
|
state.args.emplace(state.args.begin(), std::move(splitPaneAction));
|
|
}
|
|
_AddPreviouslyClosedPaneOrTab(std::move(state.args));
|
|
|
|
// If specified, detach before closing to directly update the pane structure
|
|
pane->Close();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Close the currently focused pane. If the pane is the last pane in the
|
|
// tab, the tab will also be closed. This will happen when we handle the
|
|
// tab's Closed event.
|
|
winrt::fire_and_forget TerminalPage::_CloseFocusedPane()
|
|
{
|
|
if (const auto terminalTab{ _GetFocusedTabImpl() })
|
|
{
|
|
_UnZoomIfNeeded();
|
|
|
|
if (const auto pane{ terminalTab->GetActivePane() })
|
|
{
|
|
if (co_await _PaneConfirmCloseReadOnly(pane))
|
|
{
|
|
_HandleClosePaneRequested(pane);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Close all panes with the given IDs sequentially.
|
|
// Arguments:
|
|
// - weakTab: weak reference to the tab that the pane belongs to.
|
|
// - paneIds: collection of the IDs of the panes that are marked for removal.
|
|
void TerminalPage::_ClosePanes(weak_ref<TerminalTab> weakTab, std::vector<uint32_t> paneIds)
|
|
{
|
|
if (auto strongTab{ weakTab.get() })
|
|
{
|
|
// Close all unfocused panes one by one
|
|
while (!paneIds.empty())
|
|
{
|
|
const auto id = paneIds.back();
|
|
paneIds.pop_back();
|
|
|
|
if (const auto pane{ strongTab->GetRootPane()->FindPane(id) })
|
|
{
|
|
pane->ClosedByParent([ids{ std::move(paneIds) }, weakThis{ get_weak() }, weakTab]() {
|
|
if (auto strongThis{ weakThis.get() })
|
|
{
|
|
strongThis->_ClosePanes(weakTab, std::move(ids));
|
|
}
|
|
});
|
|
|
|
// Close the pane which will eventually trigger the closed by parent event
|
|
_HandleClosePaneRequested(pane);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Close the tab at the given index.
|
|
void TerminalPage::_CloseTabAtIndex(uint32_t index)
|
|
{
|
|
if (index >= _tabs.Size())
|
|
{
|
|
return;
|
|
}
|
|
if (auto tab{ _tabs.GetAt(index) })
|
|
{
|
|
_HandleCloseTabRequested(tab);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Closes provided tabs one by one
|
|
// Arguments:
|
|
// - tabs - tabs to remove
|
|
winrt::fire_and_forget TerminalPage::_RemoveTabs(const std::vector<winrt::TerminalApp::TabBase> tabs)
|
|
{
|
|
for (auto& tab : tabs)
|
|
{
|
|
co_await _HandleCloseTabRequested(tab);
|
|
}
|
|
}
|
|
// Method Description:
|
|
// - Responds to changes in the TabView's item list by changing the
|
|
// tabview's visibility.
|
|
// - This method is also invoked when tabs are dragged / dropped as part of
|
|
// tab reordering and this method hands that case as well in concert with
|
|
// TabDragStarting and TabDragCompleted handlers that are set up in
|
|
// TerminalPage::Create()
|
|
// Arguments:
|
|
// - sender: the control that originated this event
|
|
// - eventArgs: the event's constituent arguments
|
|
void TerminalPage::_OnTabItemsChanged(const IInspectable& /*sender*/, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs)
|
|
{
|
|
if (_rearranging)
|
|
{
|
|
if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemRemoved)
|
|
{
|
|
_rearrangeFrom = eventArgs.Index();
|
|
}
|
|
|
|
if (eventArgs.CollectionChange() == Windows::Foundation::Collections::CollectionChange::ItemInserted)
|
|
{
|
|
_rearrangeTo = eventArgs.Index();
|
|
}
|
|
}
|
|
|
|
if (const auto p = CommandPaletteElement())
|
|
{
|
|
p.Visibility(Visibility::Collapsed);
|
|
}
|
|
_UpdateTabView();
|
|
}
|
|
|
|
// Method Description:
|
|
// - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click
|
|
// Arguments:
|
|
// - sender: the control that originated this event (TabViewItem)
|
|
// - eventArgs: the event's constituent arguments
|
|
void TerminalPage::_OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs)
|
|
{
|
|
if (eventArgs.GetCurrentPoint(*this).Properties().IsMiddleButtonPressed())
|
|
{
|
|
const auto tabViewItem = sender.try_as<MUX::Controls::TabViewItem>();
|
|
if (auto tab{ _GetTabByTabViewItem(tabViewItem) })
|
|
{
|
|
_HandleCloseTabRequested(tab);
|
|
}
|
|
eventArgs.Handled(true);
|
|
}
|
|
else if (eventArgs.GetCurrentPoint(*this).Properties().IsRightButtonPressed())
|
|
{
|
|
eventArgs.Handled(true);
|
|
}
|
|
}
|
|
|
|
void TerminalPage::_UpdatedSelectedTab(const winrt::TerminalApp::TabBase& tab)
|
|
{
|
|
// Unfocus all the tabs.
|
|
for (const auto& tab : _tabs)
|
|
{
|
|
tab.Focus(FocusState::Unfocused);
|
|
}
|
|
|
|
try
|
|
{
|
|
_tabContent.Children().Clear();
|
|
_tabContent.Children().Append(tab.Content());
|
|
|
|
// GH#7409: If the tab switcher is open, then we _don't_ want to
|
|
// automatically focus the new tab here. The tab switcher wants
|
|
// to be able to "preview" the selected tab as the user tabs
|
|
// through the menu, but if we toss the focus to the control
|
|
// here, then the user won't be able to navigate the ATS any
|
|
// longer.
|
|
//
|
|
// When the tab switcher is eventually dismissed, the focus will
|
|
// get tossed back to the focused terminal control, so we don't
|
|
// need to worry about focus getting lost.
|
|
const auto p = CommandPaletteElement();
|
|
if (!p || p.Visibility() != Visibility::Visible)
|
|
{
|
|
tab.Focus(FocusState::Programmatic);
|
|
_UpdateMRUTab(tab);
|
|
_updateAllTabCloseButtons();
|
|
}
|
|
|
|
tab.TabViewItem().StartBringIntoView();
|
|
|
|
// Raise an event that our title changed
|
|
if (_settings.GlobalSettings().ShowTitleInTitlebar())
|
|
{
|
|
TitleChanged.raise(*this, tab.Title());
|
|
}
|
|
|
|
_updateThemeColors();
|
|
|
|
auto tab_impl = _GetTerminalTabImpl(tab);
|
|
if (tab_impl)
|
|
{
|
|
auto profile = tab_impl->GetFocusedProfile();
|
|
_UpdateBackground(profile);
|
|
}
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
void TerminalPage::_UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile)
|
|
{
|
|
if (profile && _settings.GlobalSettings().UseBackgroundImageForWindow())
|
|
{
|
|
_SetBackgroundImage(profile.DefaultAppearance());
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Responds to the TabView control's Selection Changed event (to move a
|
|
// new terminal control into focus) when not in in the middle of a tab rearrangement.
|
|
// Arguments:
|
|
// - sender: the control that originated this event
|
|
// - eventArgs: the event's constituent arguments
|
|
void TerminalPage::_OnTabSelectionChanged(const IInspectable& sender, const WUX::Controls::SelectionChangedEventArgs& /*eventArgs*/)
|
|
{
|
|
if (!_rearranging && !_removing)
|
|
{
|
|
auto tabView = sender.as<MUX::Controls::TabView>();
|
|
auto selectedIndex = tabView.SelectedIndex();
|
|
if (selectedIndex >= 0 && selectedIndex < gsl::narrow_cast<int32_t>(_tabs.Size()))
|
|
{
|
|
const auto tab{ _tabs.GetAt(selectedIndex) };
|
|
_UpdatedSelectedTab(tab);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates all tabs with their current index in _tabs.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - <none>
|
|
void TerminalPage::_UpdateTabIndices()
|
|
{
|
|
const auto size = _tabs.Size();
|
|
for (uint32_t i = 0; i < size; ++i)
|
|
{
|
|
auto tab{ _tabs.GetAt(i) };
|
|
auto tabImpl{ winrt::get_self<TabBase>(tab) };
|
|
tabImpl->UpdateTabViewIndex(i, size);
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Bumps the tab in its in-order index up to the top of the mru list.
|
|
// Arguments:
|
|
// - tab: tab to bump.
|
|
// Return Value:
|
|
// - <none>
|
|
void TerminalPage::_UpdateMRUTab(const winrt::TerminalApp::TabBase& tab)
|
|
{
|
|
uint32_t mruIndex;
|
|
if (_mruTabs.IndexOf(tab, mruIndex))
|
|
{
|
|
if (mruIndex > 0)
|
|
{
|
|
_mruTabs.RemoveAt(mruIndex);
|
|
_mruTabs.InsertAt(0, tab);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method Description:
|
|
// - Moves the tab to another index in the tabs row (if required).
|
|
// Arguments:
|
|
// - currentTabIndex: the current index of the tab to move
|
|
// - suggestedNewTabIndex: the new index of the tab, might get clamped to fit int the tabs row boundaries
|
|
// Return Value:
|
|
// - <none>
|
|
void TerminalPage::_TryMoveTab(const uint32_t currentTabIndex,
|
|
const int32_t suggestedNewTabIndex)
|
|
{
|
|
auto newTabIndex = gsl::narrow_cast<uint32_t>(std::clamp<int32_t>(suggestedNewTabIndex, 0, _tabs.Size() - 1));
|
|
if (currentTabIndex != newTabIndex)
|
|
{
|
|
auto tab = _tabs.GetAt(currentTabIndex);
|
|
auto tabViewItem = tab.TabViewItem();
|
|
_tabs.RemoveAt(currentTabIndex);
|
|
_tabs.InsertAt(newTabIndex, tab);
|
|
_UpdateTabIndices();
|
|
|
|
_tabView.TabItems().RemoveAt(currentTabIndex);
|
|
_tabView.TabItems().InsertAt(newTabIndex, tabViewItem);
|
|
_tabView.SelectedItem(tabViewItem);
|
|
|
|
if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this))
|
|
{
|
|
const auto tabTitle = tab.Title();
|
|
autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted,
|
|
Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent,
|
|
RS_fmt(L"TerminalPage_TabMovedAnnouncement_Direction", tabTitle, newTabIndex + 1),
|
|
L"TerminalPageMoveTabWithDirection" /* unique name for this notification category */);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TerminalPage::_TabDragStarted(const IInspectable& /*sender*/,
|
|
const IInspectable& /*eventArgs*/)
|
|
{
|
|
_rearranging = true;
|
|
_rearrangeFrom = std::nullopt;
|
|
_rearrangeTo = std::nullopt;
|
|
}
|
|
|
|
void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/,
|
|
const IInspectable& /*eventArgs*/)
|
|
{
|
|
auto& from{ _rearrangeFrom };
|
|
auto& to{ _rearrangeTo };
|
|
|
|
if (from.has_value() && to.has_value() && to != from)
|
|
{
|
|
try
|
|
{
|
|
auto& tabs{ _tabs };
|
|
auto tab = tabs.GetAt(from.value());
|
|
tabs.RemoveAt(from.value());
|
|
tabs.InsertAt(to.value(), tab);
|
|
_UpdateTabIndices();
|
|
}
|
|
CATCH_LOG();
|
|
}
|
|
|
|
_rearranging = false;
|
|
|
|
if (to.has_value() &&
|
|
*to < gsl::narrow_cast<int32_t>(TabRow().TabView().TabItems().Size()))
|
|
{
|
|
// Selecting the dropped tab
|
|
TabRow().TabView().SelectedIndex(to.value());
|
|
}
|
|
|
|
from = std::nullopt;
|
|
to = std::nullopt;
|
|
}
|
|
|
|
void TerminalPage::_DismissTabContextMenus()
|
|
{
|
|
for (const auto& tab : _tabs)
|
|
{
|
|
if (tab.TabViewItem().ContextFlyout())
|
|
{
|
|
tab.TabViewItem().ContextFlyout().Hide();
|
|
}
|
|
}
|
|
}
|
|
|
|
void TerminalPage::_FocusCurrentTab(const bool focusAlways)
|
|
{
|
|
// We don't want to set focus on the tab if fly-out is open as it will
|
|
// be closed TODO GH#5400: consider checking we are not in the opening
|
|
// state, by hooking both Opening and Open events
|
|
if (focusAlways || !_newTabButton.Flyout().IsOpen())
|
|
{
|
|
// Return focus to the active control
|
|
if (auto tab{ _GetFocusedTab() })
|
|
{
|
|
tab.Focus(FocusState::Programmatic);
|
|
_UpdateMRUTab(tab);
|
|
_updateAllTabCloseButtons();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TerminalPage::_HasMultipleTabs() const
|
|
{
|
|
return _tabs.Size() > 1;
|
|
}
|
|
}
|