From 1b5b1a045a3afa6a0df890fca44a8152211d6c67 Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Thu, 19 Mar 2026 14:40:57 +0800 Subject: [PATCH 1/6] Add tab drag-drop helper tests Extract reusable insertion-index helpers for multi-tab drag/drop and cover them with focused unit tests. This chunk was validated by building TerminalApp.UnitTests and running TabDragDropHelpersTests (5/5 passed). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cascadia/TerminalApp/TabDragDropHelpers.h | 46 ++++++++++++++ .../ut_app/TabDragDropHelpersTests.cpp | 61 +++++++++++++++++++ .../ut_app/TerminalApp.UnitTests.vcxproj | 1 + 3 files changed, 108 insertions(+) create mode 100644 src/cascadia/TerminalApp/TabDragDropHelpers.h create mode 100644 src/cascadia/ut_app/TabDragDropHelpersTests.cpp diff --git a/src/cascadia/TerminalApp/TabDragDropHelpers.h b/src/cascadia/TerminalApp/TabDragDropHelpers.h new file mode 100644 index 00000000000..8e738c6afbf --- /dev/null +++ b/src/cascadia/TerminalApp/TabDragDropHelpers.h @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include +#include +#include + +namespace TerminalApp::TabDragDrop +{ + // Computes the destination insertion index after removing the dragged tabs from + // their current positions. `sortedDraggedIndices` must be sorted ascending. + inline uint32_t ComputeAdjustedInsertIndex(const uint32_t suggestedIndex, + const uint32_t tabCount, + const std::vector& sortedDraggedIndices) + { + auto insertIndex = std::min(suggestedIndex, tabCount); + for (const auto index : sortedDraggedIndices) + { + if (index < insertIndex) + { + insertIndex--; + } + } + + return insertIndex; + } + + inline bool AreContiguous(const std::vector& sortedIndices) + { + if (sortedIndices.empty()) + { + return false; + } + + return std::adjacent_find(sortedIndices.begin(), sortedIndices.end(), [](const auto lhs, const auto rhs) { + return rhs != lhs + 1; + }) == sortedIndices.end(); + } + + inline bool IsNoOpMove(const std::vector& sortedDraggedIndices, const uint32_t insertIndex) + { + return AreContiguous(sortedDraggedIndices) && sortedDraggedIndices.front() == insertIndex; + } +} diff --git a/src/cascadia/ut_app/TabDragDropHelpersTests.cpp b/src/cascadia/ut_app/TabDragDropHelpersTests.cpp new file mode 100644 index 00000000000..171bd5f49c9 --- /dev/null +++ b/src/cascadia/ut_app/TabDragDropHelpersTests.cpp @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" +#include "../TerminalApp/TabDragDropHelpers.h" + +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +namespace TerminalAppUnitTests +{ + class TabDragDropHelpersTests + { + BEGIN_TEST_CLASS(TabDragDropHelpersTests) + END_TEST_CLASS() + + TEST_METHOD(ComputeAdjustedInsertIndexShiftsForRemovedTabs); + TEST_METHOD(ComputeAdjustedInsertIndexClampsToTabCount); + TEST_METHOD(ComputeAdjustedInsertIndexNoShiftWhenDraggedTabsOnOrAfterInsert); + TEST_METHOD(IsNoOpMoveOnlyForContiguousBlockAtInsertIndex); + TEST_METHOD(IsNoOpMoveFalseForEmptySelection); + }; + + void TabDragDropHelpersTests::ComputeAdjustedInsertIndexShiftsForRemovedTabs() + { + const std::vector dragged{ 1, 3 }; + const auto actual = TerminalApp::TabDragDrop::ComputeAdjustedInsertIndex(4, 5, dragged); + VERIFY_ARE_EQUAL(3u, actual); + } + + void TabDragDropHelpersTests::ComputeAdjustedInsertIndexClampsToTabCount() + { + const std::vector dragged{ 0 }; + const auto actual = TerminalApp::TabDragDrop::ComputeAdjustedInsertIndex(99, 5, dragged); + VERIFY_ARE_EQUAL(4u, actual); + } + + void TabDragDropHelpersTests::ComputeAdjustedInsertIndexNoShiftWhenDraggedTabsOnOrAfterInsert() + { + const std::vector dragged{ 3, 4 }; + const auto actual = TerminalApp::TabDragDrop::ComputeAdjustedInsertIndex(3, 5, dragged); + VERIFY_ARE_EQUAL(3u, actual); + } + + void TabDragDropHelpersTests::IsNoOpMoveOnlyForContiguousBlockAtInsertIndex() + { + const std::vector contiguous{ 2, 3, 4 }; + const std::vector nonContiguous{ 1, 3, 4 }; + + VERIFY_IS_TRUE(TerminalApp::TabDragDrop::IsNoOpMove(contiguous, 2)); + VERIFY_IS_FALSE(TerminalApp::TabDragDrop::IsNoOpMove(contiguous, 1)); + VERIFY_IS_FALSE(TerminalApp::TabDragDrop::IsNoOpMove(nonContiguous, 1)); + } + + void TabDragDropHelpersTests::IsNoOpMoveFalseForEmptySelection() + { + const std::vector empty{}; + VERIFY_IS_FALSE(TerminalApp::TabDragDrop::IsNoOpMove(empty, 0)); + } +} diff --git a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj index 1eba2400bb6..efc2fa70cb8 100644 --- a/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj +++ b/src/cascadia/ut_app/TerminalApp.UnitTests.vcxproj @@ -23,6 +23,7 @@ + Create From f31d8a50bf1a3846dcd9456af2954a5ef0a9080f Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Thu, 19 Mar 2026 22:00:36 +0800 Subject: [PATCH 2/6] Implement multi-tab drag/drop flows Add the product plumbing and LocalTests coverage for multi-tab selection, block reordering, startup action export, and cross-window attach behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LocalTests_TerminalApp/TabTests.cpp | 512 +++++++++++++++++- src/cascadia/TerminalApp/Tab.cpp | 27 + src/cascadia/TerminalApp/Tab.h | 3 + src/cascadia/TerminalApp/TabManagement.cpp | 279 +++++++++- src/cascadia/TerminalApp/TerminalPage.cpp | 97 +++- src/cascadia/TerminalApp/TerminalPage.h | 18 +- 6 files changed, 883 insertions(+), 53 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 8272be34ed2..7b8a286f72b 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -12,6 +12,7 @@ #include "../TerminalApp/CommandPalette.h" #include "../TerminalApp/ContentManager.h" #include "CppWinrtTailored.h" +#include using namespace Microsoft::Console; using namespace TerminalApp; @@ -78,6 +79,10 @@ namespace TerminalAppLocalTests TEST_METHOD(TryDuplicateBadTab); TEST_METHOD(TryDuplicateBadPane); + TEST_METHOD(TrackSelectedTabsVisualState); + TEST_METHOD(MoveMultipleTabsAsBlock); + TEST_METHOD(BuildStartupActionsForMultipleTabs); + TEST_METHOD(AttachContentInsertsDraggedTabsAfterTargetIndex); TEST_METHOD(TryZoomPane); TEST_METHOD(MoveFocusFromZoomedPane); @@ -108,9 +113,15 @@ namespace TerminalAppLocalTests } private: + void _primeApplicationResourcesForTabTests(); void _initializeTerminalPage(winrt::com_ptr& page, - CascadiaSettings initialSettings); - winrt::com_ptr _commonSetup(); + CascadiaSettings initialSettings, + winrt::TerminalApp::ContentManager contentManager = nullptr); + void _yieldToLowPriorityDispatcher(const winrt::com_ptr& page); + void _ensureStableBaselineTab(const winrt::com_ptr& page); + void _waitForTabCount(const winrt::com_ptr& page, uint32_t expectedCount); + void _openProfileTab(const winrt::com_ptr& page, int32_t profileIndex, uint32_t expectedCount); + winrt::com_ptr _commonSetup(winrt::TerminalApp::ContentManager contentManager = nullptr); winrt::com_ptr _windowProperties; winrt::com_ptr _contentManager; }; @@ -122,6 +133,28 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } + static NewTerminalArgs _profileArgs(const int32_t profileIndex) + { + auto index = profileIndex; + return NewTerminalArgs{ index }; + } + + void TabTests::_yieldToLowPriorityDispatcher(const winrt::com_ptr& page) + { + VERIFY_IS_NOT_NULL(page); + + details::Event completedEvent; + VERIFY_IS_TRUE(completedEvent.IsValid()); + + auto action = page->Dispatcher().RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Low, [] {}); + action.Completed([&completedEvent](auto&&, auto&&) { + completedEvent.Set(); + return S_OK; + }); + + VERIFY_SUCCEEDED(completedEvent.Wait()); + } + void TabTests::EnsureTestsActivate() { // This test was originally used to ensure that XAML Islands was @@ -171,6 +204,101 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(result); } + void TabTests::_waitForTabCount(const winrt::com_ptr& page, const uint32_t expectedCount) + { + VERIFY_IS_NOT_NULL(page); + + uint32_t actualCount{}; + uint32_t actualTabItemCount{}; + for (auto attempt = 0; attempt < 20; ++attempt) + { + const auto result = RunOnUIThread([&]() { + page->_tabView.UpdateLayout(); + actualCount = page->_tabs.Size(); + actualTabItemCount = page->_tabView.TabItems().Size(); + }); + VERIFY_SUCCEEDED(result); + + if (actualCount == expectedCount && actualTabItemCount == expectedCount) + { + return; + } + + Sleep(50); + } + + VERIFY_ARE_EQUAL(expectedCount, actualCount); + VERIFY_ARE_EQUAL(expectedCount, actualTabItemCount); + } + + void TabTests::_ensureStableBaselineTab(const winrt::com_ptr& page) + { + VERIFY_IS_NOT_NULL(page); + + uint32_t latestCount{}; + bool sawZero = false; + + for (auto attempt = 0; attempt < 20; ++attempt) + { + _yieldToLowPriorityDispatcher(page); + + auto result = RunOnUIThread([&]() { + latestCount = page->_tabs.Size(); + }); + VERIFY_SUCCEEDED(result); + + if (latestCount == 0) + { + sawZero = true; + break; + } + + Sleep(50); + } + + Log::Comment(NoThrowString().Format(L"Stable baseline final count=%u sawZero=%d", latestCount, sawZero ? 1 : 0)); + if (sawZero) + { + _openProfileTab(page, 0, 1); + } + } + + void TabTests::_openProfileTab(const winrt::com_ptr& page, const int32_t profileIndex, const uint32_t expectedCount) + { + VERIFY_IS_NOT_NULL(page); + + _yieldToLowPriorityDispatcher(page); + + TestOnUIThread([&]() { + auto beforeCount = page->_tabs.Size(); + + if (profileIndex != 0 && (beforeCount + 1) < expectedCount) + { + const auto baselineHr = page->_OpenNewTab(_profileArgs(0)); + const auto baselineCount = page->_tabs.Size(); + Log::Comment(NoThrowString().Format( + L"_OpenNewTab repaired missing baseline -> hr=0x%08x before=%u after=%u", + static_cast(baselineHr), + beforeCount, + baselineCount)); + VERIFY_ARE_EQUAL(S_OK, baselineHr); + beforeCount = baselineCount; + } + + const auto hr = page->_OpenNewTab(_profileArgs(profileIndex)); + const auto afterCount = page->_tabs.Size(); + Log::Comment(NoThrowString().Format( + L"_OpenNewTab(profileIndex=%d) -> hr=0x%08x before=%u after=%u", + profileIndex, + static_cast(hr), + beforeCount, + afterCount)); + VERIFY_ARE_EQUAL(S_OK, hr); + }); + + _waitForTabCount(page, expectedCount); + } + void TabTests::CreateTerminalMuxXamlType() { winrt::com_ptr tabRowControl{ nullptr }; @@ -186,6 +314,8 @@ namespace TerminalAppLocalTests { winrt::com_ptr page{ nullptr }; + _primeApplicationResourcesForTabTests(); + _windowProperties = winrt::make_self(); winrt::TerminalApp::WindowProperties props = *_windowProperties; @@ -197,6 +327,64 @@ namespace TerminalAppLocalTests VERIFY_IS_NOT_NULL(page); }); VERIFY_SUCCEEDED(result); + + _yieldToLowPriorityDispatcher(page); + } + + void TabTests::_primeApplicationResourcesForTabTests() + { + const auto result = RunOnUIThread([]() { + const auto app = winrt::Windows::UI::Xaml::Application::Current(); + VERIFY_IS_NOT_NULL(app); + + // Keep priming intentionally minimal for LocalTests: avoid pulling in + // full MUX dictionaries, but provide common TabView keys used during + // TerminalPage InitializeComponent. + auto resources = app.Resources(); + const auto ensureBrush = [&](const wchar_t* key) { + const auto boxedKey = winrt::box_value(winrt::hstring{ key }); + if (!resources.HasKey(boxedKey)) + { + resources.Insert(boxedKey, winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() }); + } + }; + const auto ensureDouble = [&](const wchar_t* key, const double value) { + const auto boxedKey = winrt::box_value(winrt::hstring{ key }); + if (!resources.HasKey(boxedKey)) + { + resources.Insert(boxedKey, winrt::box_value(value)); + } + }; + const auto ensureCornerRadius = [&](const wchar_t* key) { + const auto boxedKey = winrt::box_value(winrt::hstring{ key }); + if (!resources.HasKey(boxedKey)) + { + resources.Insert(boxedKey, winrt::box_value(winrt::Windows::UI::Xaml::CornerRadius{ 0, 0, 0, 0 })); + } + }; + const auto ensureThickness = [&](const wchar_t* key) { + const auto boxedKey = winrt::box_value(winrt::hstring{ key }); + if (!resources.HasKey(boxedKey)) + { + resources.Insert(boxedKey, winrt::box_value(winrt::Windows::UI::Xaml::Thickness{ 0, 0, 0, 0 })); + } + }; + const auto ensureStyle = [&](const wchar_t* key, const winrt::Windows::UI::Xaml::Interop::TypeName& targetType) { + const auto boxedKey = winrt::box_value(winrt::hstring{ key }); + if (!resources.HasKey(boxedKey)) + { + winrt::Windows::UI::Xaml::Style style; + style.TargetType(targetType); + resources.Insert(boxedKey, style); + } + }; + + ensureBrush(L"TabViewBackground"); + Log::Comment(NoThrowString().Format(L"Resource probe: TabViewBackground=%d CardStrokeColorDefaultBrush=%d", + resources.HasKey(winrt::box_value(L"TabViewBackground")), + resources.HasKey(winrt::box_value(L"CardStrokeColorDefaultBrush")))); + }); + VERIFY_SUCCEEDED(result); } // Method Description: @@ -225,8 +413,11 @@ namespace TerminalAppLocalTests // Return Value: // - void TabTests::_initializeTerminalPage(winrt::com_ptr& page, - CascadiaSettings initialSettings) + CascadiaSettings initialSettings, + winrt::TerminalApp::ContentManager contentManager) { + _primeApplicationResourcesForTabTests(); + // This is super wacky, but we can't just initialize the // com_ptr in the lambda and assign it back out of // the lambda. We'll crash trying to get a weak_ref to the TerminalPage @@ -239,13 +430,150 @@ namespace TerminalAppLocalTests _windowProperties = winrt::make_self(); winrt::TerminalApp::WindowProperties props = *_windowProperties; - _contentManager = winrt::make_self(); - winrt::TerminalApp::ContentManager contentManager = *_contentManager; + if (!contentManager) + { + _contentManager = winrt::make_self(); + contentManager = *_contentManager; + } + else + { + _contentManager.copy_from(winrt::get_self(contentManager)); + } Log::Comment(NoThrowString().Format(L"Construct the TerminalPage")); auto result = RunOnUIThread([&projectedPage, &page, initialSettings, props, contentManager]() { - projectedPage = winrt::TerminalApp::TerminalPage(props, contentManager); - page.copy_from(winrt::get_self(projectedPage)); - page->_settings = initialSettings; + const auto app = winrt::Windows::UI::Xaml::Application::Current(); + VERIFY_IS_NOT_NULL(app); + auto resources = app.Resources(); + const auto donorResources = winrt::Microsoft::UI::Xaml::Controls::XamlControlsResources{}; + std::unordered_set injectedKeys; + + const auto tryCopyFromDonor = [&](const winrt::hstring& key, winrt::Windows::Foundation::IInspectable& value) { + const auto keyObj = winrt::box_value(key); + if (donorResources.HasKey(keyObj)) + { + value = donorResources.Lookup(keyObj); + Log::Comment(NoThrowString().Format(L"Donor hit root key '%s'", key.c_str())); + return true; + } + + const auto donorThemes = donorResources.ThemeDictionaries(); + for (const auto& theme : { L"Default", L"Light", L"Dark", L"HighContrast" }) + { + const auto themeObj = winrt::box_value(theme); + if (donorThemes.HasKey(themeObj)) + { + const auto themeDictionary = donorThemes.Lookup(themeObj).as(); + if (themeDictionary.HasKey(keyObj)) + { + value = themeDictionary.Lookup(keyObj); + Log::Comment(NoThrowString().Format(L"Donor hit theme '%s' key '%s'", theme, key.c_str())); + return true; + } + } + } + return false; + }; + + const auto insertFallbackForKey = [&](const std::wstring& key) { + const auto keyH = winrt::hstring{ key }; + const auto keyObj = winrt::box_value(keyH); + if (resources.HasKey(keyObj)) + { + return; + } + + winrt::Windows::Foundation::IInspectable value{ nullptr }; + if (!tryCopyFromDonor(keyH, value)) + { + if (key.find(L"Thickness") != std::wstring::npos || key.find(L"Padding") != std::wstring::npos || key.find(L"Margin") != std::wstring::npos) + { + value = winrt::box_value(winrt::Windows::UI::Xaml::Thickness{ 0, 0, 0, 0 }); + } + else if (key.find(L"CornerRadius") != std::wstring::npos || key.find(L"Radius") != std::wstring::npos) + { + value = winrt::box_value(winrt::Windows::UI::Xaml::CornerRadius{ 0, 0, 0, 0 }); + } + else if (key.find(L"Style") != std::wstring::npos) + { + winrt::Windows::UI::Xaml::Style style; + if (key.find(L"RepeatButtonStyle") != std::wstring::npos) + { + style.TargetType(winrt::xaml_typename()); + } + else if (key.find(L"ButtonStyle") != std::wstring::npos) + { + style.TargetType(winrt::xaml_typename()); + } + else + { + style.TargetType(winrt::xaml_typename()); + } + value = style; + } + else if (key.find(L"FontSize") != std::wstring::npos || + key.find(L"Width") != std::wstring::npos || + key.find(L"Height") != std::wstring::npos || + key.find(L"Min") != std::wstring::npos || + key.find(L"Max") != std::wstring::npos || + key.find(L"Opacity") != std::wstring::npos) + { + value = winrt::box_value(0.0); + } + else + { + value = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Transparent() }; + } + } + + resources.Insert(keyObj, value); + }; + + for (auto attempt = 0; attempt < 512; attempt++) + { + try + { + projectedPage = winrt::TerminalApp::TerminalPage(props, contentManager); + page.copy_from(winrt::get_self(projectedPage)); + page->_settings = initialSettings; + return; + } + catch (const winrt::hresult_error& ex) + { + std::wstring message = ex.message().c_str(); + constexpr std::wstring_view marker{ L"Cannot find a Resource with the Name/Key " }; + const auto markerPos = message.find(marker); + if (markerPos == std::wstring::npos) + { + throw; + } + + const auto keyStart = markerPos + marker.size(); + const auto keyEnd = message.find(L" [", keyStart); + std::wstring key = message.substr(keyStart, keyEnd == std::wstring::npos ? std::wstring::npos : keyEnd - keyStart); + while (!key.empty() && iswspace(key.front())) + { + key.erase(key.begin()); + } + while (!key.empty() && iswspace(key.back())) + { + key.pop_back(); + } + + if (key.empty()) + { + throw; + } + + if (!injectedKeys.contains(key)) + { + Log::Comment(NoThrowString().Format(L"Injecting fallback resource for '%s'", key.c_str())); + injectedKeys.insert(key); + } + insertFallbackForKey(key); + } + } + + VERIFY_FAIL(L"Failed to initialize TerminalPage after fallback resource retries."); }); VERIFY_SUCCEEDED(result); @@ -266,23 +594,26 @@ namespace TerminalAppLocalTests result = RunOnUIThread([&page]() { VERIFY_IS_NOT_NULL(page); VERIFY_IS_NOT_NULL(page->_settings); - page->Create(); - Log::Comment(L"Create()'d the page successfully"); // Build a NewTab action, to make sure we start with one. The real // Terminal will always get one from AppCommandlineArgs. NewTerminalArgs newTerminalArgs{}; NewTabArgs args{ newTerminalArgs }; ActionAndArgs newTabAction{ ShortcutAction::NewTab, args }; - // push the arg onto the front - page->_startupActions.push_back(std::move(newTabAction)); - Log::Comment(L"Added a single newTab action"); + page->SetStartupActions({ std::move(newTabAction) }); + Log::Comment(L"Configured a single startup newTab action"); + + page->Create(); + Log::Comment(L"Create()'d the page successfully"); auto app = ::winrt::Windows::UI::Xaml::Application::Current(); winrt::TerminalApp::TerminalPage pp = *page; winrt::Windows::UI::Xaml::Window::Current().Content(pp); winrt::Windows::UI::Xaml::Window::Current().Activate(); + pp.UpdateLayout(); + page->_tabRow.UpdateLayout(); + page->_tabContent.UpdateLayout(); }); VERIFY_SUCCEEDED(result); @@ -290,14 +621,45 @@ namespace TerminalAppLocalTests VERIFY_SUCCEEDED(waitForInitEvent.Wait()); Log::Comment(L"...Done"); - result = RunOnUIThread([&page]() { + uint32_t tabCount{}; + uint32_t tabItemCount{}; + bool hasSelectedItem{}; + for (auto attempt = 0; attempt < 20; ++attempt) + { + result = RunOnUIThread([&]() { + tabCount = page->_tabs.Size(); + tabItemCount = page->_tabView.TabItems().Size(); + hasSelectedItem = static_cast(page->_tabView.SelectedItem()); + }); + VERIFY_SUCCEEDED(result); + + if (tabCount > 0) + { + break; + } + + Sleep(50); + } + + result = RunOnUIThread([&page, tabCount, tabItemCount, hasSelectedItem]() { // In the real app, this isn't a problem, but doesn't happen // reliably in the unit tests. Log::Comment(L"Ensure we set the first tab as the selected one."); + Log::Comment(NoThrowString().Format( + L"Selection sync state: tabs=%u tabItems=%u selectedItem=%d", + tabCount, + tabItemCount, + hasSelectedItem ? 1 : 0)); + VERIFY_IS_TRUE(page->_tabs.Size() > 0); auto tab = page->_tabs.GetAt(0); auto tabImpl = page->_GetTabImpl(tab); + VERIFY_IS_NOT_NULL(tabImpl); + VERIFY_IS_TRUE(static_cast(tabImpl->TabViewItem())); + Log::Comment(L"About to assign TabView.SelectedItem"); page->_tabView.SelectedItem(tabImpl->TabViewItem()); + Log::Comment(L"About to call _UpdatedSelectedTab"); page->_UpdatedSelectedTab(tab); + Log::Comment(L"Selected first tab successfully"); }); VERIFY_SUCCEEDED(result); } @@ -561,12 +923,12 @@ namespace TerminalAppLocalTests // - // Return Value: // - The initialized TerminalPage, ready to use. - winrt::com_ptr TabTests::_commonSetup() + winrt::com_ptr TabTests::_commonSetup(winrt::TerminalApp::ContentManager contentManager) { static constexpr std::wstring_view settingsJson0{ LR"( { "defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}", - "showTabsInTitlebar": false, + "showTabsInTitlebar": true, "profiles": [ { "name" : "profile0", @@ -679,14 +1041,121 @@ namespace TerminalAppLocalTests // implementation _from_ the winrt object. This seems to work, even if // it's weird. winrt::com_ptr page{ nullptr }; - _initializeTerminalPage(page, settings0); + _initializeTerminalPage(page, settings0, contentManager); + _ensureStableBaselineTab(page); - auto result = RunOnUIThread([&page]() { - VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); + return page; + } + + void TabTests::TrackSelectedTabsVisualState() + { + auto page = _commonSetup(); + VERIFY_IS_NOT_NULL(page); + + _openProfileTab(page, 1, 2); + _openProfileTab(page, 2, 3); + _openProfileTab(page, 3, 4); + + TestOnUIThread([&page]() { + const auto tab0 = page->_tabs.GetAt(0); + const auto tab2 = page->_tabs.GetAt(2); + page->_SetSelectedTabs({ tab0, tab2 }, tab0); + + VERIFY_ARE_EQUAL(2u, page->_selectedTabs.size()); + VERIFY_IS_TRUE(page->_selectionAnchor == tab0); + VERIFY_IS_TRUE(page->_GetTabImpl(tab0)->IsMultiSelected()); + VERIFY_IS_FALSE(page->_GetTabImpl(page->_tabs.GetAt(1))->IsMultiSelected()); + VERIFY_IS_TRUE(page->_GetTabImpl(tab2)->IsMultiSelected()); + + page->_RemoveSelectedTab(tab0); + VERIFY_ARE_EQUAL(1u, page->_selectedTabs.size()); + VERIFY_IS_TRUE(page->_selectionAnchor == tab2); + VERIFY_IS_FALSE(page->_GetTabImpl(tab0)->IsMultiSelected()); + VERIFY_IS_TRUE(page->_GetTabImpl(tab2)->IsMultiSelected()); }); - VERIFY_SUCCEEDED(result); + } - return page; + void TabTests::MoveMultipleTabsAsBlock() + { + auto page = _commonSetup(); + VERIFY_IS_NOT_NULL(page); + + _openProfileTab(page, 1, 2); + _openProfileTab(page, 2, 3); + _openProfileTab(page, 3, 4); + + TestOnUIThread([&page]() { + const auto tab1 = page->_tabs.GetAt(1); + const auto tab2 = page->_tabs.GetAt(2); + page->_MoveTabsToIndex({ tab1, tab2 }, 4); + + VERIFY_ARE_EQUAL(L"Profile 0", page->_tabs.GetAt(0).Title()); + VERIFY_ARE_EQUAL(L"Profile 3", page->_tabs.GetAt(1).Title()); + VERIFY_ARE_EQUAL(L"Profile 1", page->_tabs.GetAt(2).Title()); + VERIFY_ARE_EQUAL(L"Profile 2", page->_tabs.GetAt(3).Title()); + + const auto focusedIndex = page->_GetFocusedTabIndex(); + VERIFY_IS_TRUE(focusedIndex.has_value()); + VERIFY_ARE_EQUAL(2u, focusedIndex.value()); + }); + } + + void TabTests::BuildStartupActionsForMultipleTabs() + { + auto page = _commonSetup(); + VERIFY_IS_NOT_NULL(page); + + _openProfileTab(page, 1, 2); + _openProfileTab(page, 2, 3); + + TestOnUIThread([&page]() { + const auto actions = page->_BuildStartupActionsForTabs({ page->_tabs.GetAt(0), page->_tabs.GetAt(2) }); + const auto newTabCount = std::count_if(actions.begin(), actions.end(), [](const auto& action) { + return action.Action() == ShortcutAction::NewTab; + }); + + VERIFY_ARE_EQUAL(2u, gsl::narrow_cast(newTabCount)); + VERIFY_IS_FALSE(actions.empty()); + VERIFY_ARE_EQUAL(ShortcutAction::NewTab, actions.front().Action()); + VERIFY_IS_TRUE(std::count_if(actions.begin(), actions.end(), [](const auto& action) { + return action.Action() == ShortcutAction::SwitchToTab; + }) == 0); + }); + } + + void TabTests::AttachContentInsertsDraggedTabsAfterTargetIndex() + { + const auto sharedContentManager = winrt::make(); + auto sourcePage = _commonSetup(sharedContentManager); + auto destinationPage = _commonSetup(sharedContentManager); + VERIFY_IS_NOT_NULL(sourcePage); + VERIFY_IS_NOT_NULL(destinationPage); + + _ensureStableBaselineTab(sourcePage); + _ensureStableBaselineTab(destinationPage); + + _openProfileTab(sourcePage, 1, 2); + _openProfileTab(sourcePage, 2, 3); + _waitForTabCount(destinationPage, 1); + + TestOnUIThread([&]() { + const auto startupActions = sourcePage->_BuildStartupActionsForTabs({ sourcePage->_tabs.GetAt(1), sourcePage->_tabs.GetAt(2) }); + auto serializedActions = winrt::single_threaded_vector(); + for (const auto& action : startupActions) + { + serializedActions.Append(action); + } + + destinationPage->AttachContent(serializedActions, 1); + }); + + _waitForTabCount(destinationPage, 3); + + TestOnUIThread([&]() { + VERIFY_ARE_EQUAL(L"Profile 0", destinationPage->_tabs.GetAt(0).Title()); + VERIFY_ARE_EQUAL(L"Profile 1", destinationPage->_tabs.GetAt(1).Title()); + VERIFY_ARE_EQUAL(L"Profile 2", destinationPage->_tabs.GetAt(2).Title()); + }); } void TabTests::TryZoomPane() @@ -1579,3 +2048,4 @@ namespace TerminalAppLocalTests } } + diff --git a/src/cascadia/TerminalApp/Tab.cpp b/src/cascadia/TerminalApp/Tab.cpp index 4bbf58e50ad..7930207d93e 100644 --- a/src/cascadia/TerminalApp/Tab.cpp +++ b/src/cascadia/TerminalApp/Tab.cpp @@ -502,6 +502,33 @@ namespace winrt::TerminalApp::implementation _UpdateToolTip(); } + void Tab::SetMultiSelected(const bool multiSelected) + { + ASSERT_UI_THREAD(); + _multiSelected = multiSelected; + + if (multiSelected) + { + auto accentBrush = TabViewItem().Resources().TryLookup(box_value(L"SystemControlForegroundAccentBrush")).try_as(); + if (!accentBrush) + { + accentBrush = Media::SolidColorBrush{ winrt::Windows::UI::Colors::DodgerBlue() }; + } + TabViewItem().BorderBrush(accentBrush); + TabViewItem().BorderThickness(ThicknessHelper::FromUniformLength(1.0)); + } + else + { + TabViewItem().BorderBrush(nullptr); + TabViewItem().BorderThickness(ThicknessHelper::FromUniformLength(0.0)); + } + } + + bool Tab::IsMultiSelected() const noexcept + { + return _multiSelected; + } + // Method Description: // - Move the viewport of the terminal up or down a number of lines. Negative // values of `delta` will move the view up, and positive values will move diff --git a/src/cascadia/TerminalApp/Tab.h b/src/cascadia/TerminalApp/Tab.h index 65d4e574ede..771fdf032b0 100644 --- a/src/cascadia/TerminalApp/Tab.h +++ b/src/cascadia/TerminalApp/Tab.h @@ -60,6 +60,8 @@ namespace winrt::TerminalApp::implementation void UpdateSettings(const winrt::Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); void UpdateTitle(); + void SetMultiSelected(bool multiSelected); + bool IsMultiSelected() const noexcept; void Close(); void Shutdown(); @@ -154,6 +156,7 @@ namespace winrt::TerminalApp::implementation til::color _tabRowColor; Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility _closeButtonVisibility{ Microsoft::Terminal::Settings::Model::TabCloseButtonVisibility::Always }; + bool _multiSelected{ false }; std::shared_ptr _rootPane{ nullptr }; std::shared_ptr _activePane{ nullptr }; diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 21e31fb6dd3..b4a603bd896 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -10,6 +10,7 @@ #include "pch.h" #include "TerminalPage.h" +#include "TabDragDropHelpers.h" #include "Utils.h" #include "../../types/inc/utils.hpp" #include "../../inc/til/string.h" @@ -453,9 +454,18 @@ namespace winrt::TerminalApp::implementation _settingsTab = nullptr; } - if (_stashed.draggedTab && *_stashed.draggedTab == tab) + _RemoveSelectedTab(tab); + + if (!_stashed.draggedTabs.empty()) { - _stashed.draggedTab = nullptr; + std::erase_if(_stashed.draggedTabs, [&](const auto& draggedTab) { + return draggedTab == tab; + }); + + if (_stashed.dragAnchor == tab) + { + _stashed.dragAnchor = nullptr; + } } _tabs.RemoveAt(tabIndex); @@ -897,6 +907,13 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& e) { + const auto tab = _GetTabByTabViewItem(sender); + if (tab && + e.GetCurrentPoint(nullptr).Properties().IsLeftButtonPressed()) + { + _UpdateSelectionFromPointer(tab); + } + if (!_tabItemMiddleClickHookEnabled || !e.GetCurrentPoint(nullptr).Properties().IsMiddleButtonPressed()) { return; @@ -1039,6 +1056,10 @@ namespace winrt::TerminalApp::implementation { const auto tab{ _tabs.GetAt(selectedIndex) }; _UpdatedSelectedTab(tab); + if (_selectedTabs.empty() && _TabSupportsMultiSelection(tab)) + { + _SetSelectedTabs({ tab }, tab); + } } } } @@ -1079,6 +1100,258 @@ namespace winrt::TerminalApp::implementation } } + bool TerminalPage::_TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept + { + return tab && _GetTabImpl(tab) != nullptr; + } + + bool TerminalPage::_IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept + { + return std::ranges::any_of(_selectedTabs, [&](const auto& selectedTab) { + return selectedTab == tab; + }); + } + + void TerminalPage::_ApplyMultiSelectionVisuals() + { + for (const auto& tab : _tabs) + { + if (const auto tabImpl{ _GetTabImpl(tab) }) + { + tabImpl->SetMultiSelected(_IsTabSelected(tab)); + } + } + } + + void TerminalPage::_SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor) + { + std::vector sanitizedTabs{}; + sanitizedTabs.reserve(tabs.size()); + + for (const auto& tab : tabs) + { + if (_TabSupportsMultiSelection(tab) && + !std::ranges::any_of(sanitizedTabs, [&](const auto& existingTab) { return existingTab == tab; })) + { + sanitizedTabs.emplace_back(tab); + } + } + + _selectedTabs = std::move(sanitizedTabs); + if (_TabSupportsMultiSelection(anchor)) + { + _selectionAnchor = anchor; + } + else if (_selectedTabs.empty()) + { + _selectionAnchor = nullptr; + } + else if (!_TabSupportsMultiSelection(_selectionAnchor) || !_IsTabSelected(_selectionAnchor)) + { + _selectionAnchor = _selectedTabs.front(); + } + + _ApplyMultiSelectionVisuals(); + } + + void TerminalPage::_RemoveSelectedTab(const winrt::TerminalApp::Tab& tab) + { + auto eraseIt = std::remove_if(_selectedTabs.begin(), _selectedTabs.end(), [&](const auto& selectedTab) { + return selectedTab == tab; + }); + if (eraseIt != _selectedTabs.end()) + { + _selectedTabs.erase(eraseIt, _selectedTabs.end()); + } + + if (_selectionAnchor == tab) + { + _selectionAnchor = !_selectedTabs.empty() ? _selectedTabs.front() : nullptr; + } + + _ApplyMultiSelectionVisuals(); + } + + std::vector TerminalPage::_GetSelectedTabsInDisplayOrder() const + { + std::vector selectedTabs{}; + selectedTabs.reserve(_selectedTabs.size()); + + for (const auto& tab : _tabs) + { + if (_IsTabSelected(tab)) + { + selectedTabs.emplace_back(tab); + } + } + + return selectedTabs; + } + + std::vector TerminalPage::_GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const + { + std::vector tabs{}; + const auto startIndex = _GetTabIndex(start); + const auto endIndex = _GetTabIndex(end); + if (!startIndex.has_value() || !endIndex.has_value()) + { + return tabs; + } + + const auto first = std::min(startIndex.value(), endIndex.value()); + const auto last = std::max(startIndex.value(), endIndex.value()); + tabs.reserve(last - first + 1); + + for (auto i = first; i <= last; ++i) + { + const auto tab = _tabs.GetAt(i); + if (_TabSupportsMultiSelection(tab)) + { + tabs.emplace_back(tab); + } + } + + return tabs; + } + + void TerminalPage::_UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab) + { + const bool ctrlPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_CONTROL)), 0x8000); + const bool shiftPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_SHIFT)), 0x8000); + + if (!_TabSupportsMultiSelection(tab)) + { + _SetSelectedTabs({}, nullptr); + return; + } + + if (shiftPressed) + { + const auto anchor = _TabSupportsMultiSelection(_selectionAnchor) ? + _selectionAnchor : + (_TabSupportsMultiSelection(_GetFocusedTab()) ? _GetFocusedTab() : tab); + auto range = _GetTabRange(anchor, tab); + if (range.empty()) + { + range.emplace_back(tab); + } + _SetSelectedTabs(std::move(range), anchor); + return; + } + + if (ctrlPressed) + { + auto tabs = _GetSelectedTabsInDisplayOrder(); + const auto selected = _IsTabSelected(tab); + if (selected) + { + if (tabs.size() > 1) + { + std::erase_if(tabs, [&](const auto& selectedTab) { return selectedTab == tab; }); + } + } + else + { + tabs.emplace_back(tab); + } + _SetSelectedTabs(std::move(tabs), tab); + return; + } + + if (!_IsTabSelected(tab) || _selectedTabs.size() <= 1) + { + _SetSelectedTabs({ tab }, tab); + } + else + { + _selectionAnchor = tab; + } + } + + void TerminalPage::_MoveTabsToIndex(const std::vector& tabs, const uint32_t suggestedNewTabIndex) + { + if (tabs.empty() || _tabs.Size() == 0) + { + return; + } + + auto orderedTabs = tabs; + std::ranges::sort(orderedTabs, [&](const auto& lhs, const auto& rhs) { + return _GetTabIndex(lhs).value_or(0) < _GetTabIndex(rhs).value_or(0); + }); + + std::vector indices{}; + indices.reserve(orderedTabs.size()); + for (const auto& tab : orderedTabs) + { + if (const auto currentIndex = _GetTabIndex(tab)) + { + indices.emplace_back(*currentIndex); + } + } + + if (indices.empty()) + { + return; + } + + const auto insertIndex = ::TerminalApp::TabDragDrop::ComputeAdjustedInsertIndex(suggestedNewTabIndex, _tabs.Size(), indices); + if (::TerminalApp::TabDragDrop::IsNoOpMove(indices, insertIndex)) + { + _tabView.SelectedItem(orderedTabs.front().TabViewItem()); + return; + } + + std::vector tabItems{}; + tabItems.reserve(orderedTabs.size()); + for (const auto& tab : orderedTabs) + { + tabItems.emplace_back(tab.TabViewItem()); + } + + for (auto it = indices.rbegin(); it != indices.rend(); ++it) + { + _tabs.RemoveAt(*it); + _tabView.TabItems().RemoveAt(*it); + } + + for (uint32_t i = 0; i < orderedTabs.size(); ++i) + { + _tabs.InsertAt(insertIndex + i, orderedTabs[i]); + _tabView.TabItems().InsertAt(insertIndex + i, tabItems[i]); + } + + _UpdateTabIndices(); + _tabView.SelectedItem(orderedTabs.front().TabViewItem()); + } + + std::vector TerminalPage::_CollectNewTabs(const std::vector& existingTabs) const + { + std::vector newTabs{}; + for (const auto& tab : _tabs) + { + if (!std::ranges::any_of(existingTabs, [&](const auto& existingTab) { return existingTab == tab; })) + { + newTabs.emplace_back(tab); + } + } + return newTabs; + } + + std::vector TerminalPage::_BuildStartupActionsForTabs(const std::vector& tabs) const + { + std::vector actions{}; + for (const auto& tab : tabs) + { + if (const auto tabImpl{ _GetTabImpl(tab) }) + { + auto tabActions = tabImpl->BuildStartupActions(BuildStartupKind::Content); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); + } + } + return actions; + } + // Method Description: // - Moves the tab to another index in the tabs row (if required). // Arguments: @@ -1141,6 +1414,8 @@ namespace winrt::TerminalApp::implementation } _rearranging = false; + _stashed.draggedTabs.clear(); + _stashed.dragAnchor = nullptr; if (to.has_value() && *to < gsl::narrow_cast(TabRow().TabView().TabItems().Size())) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index e98de6b9957..15acdb80411 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -2647,6 +2647,13 @@ namespace winrt::TerminalApp::implementation return; } + std::vector existingTabs{}; + existingTabs.reserve(_tabs.Size()); + for (const auto& tab : _tabs) + { + existingTabs.emplace_back(tab); + } + const auto& firstAction = args.GetAt(0); const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; @@ -2673,17 +2680,12 @@ namespace winrt::TerminalApp::implementation // made into position. if (!firstIsSplitPane && tabIndex != -1) { - // Move the currently active tab to the requested index Use the - // currently focused tab index, because we don't know if the new tab - // opened at the end of the list, or adjacent to the previously - // active tab. This is affected by the user's "newTabPosition" - // setting. - if (const auto focusedTabIndex = _GetFocusedTabIndex()) + const auto newTabs = _CollectNewTabs(existingTabs); + if (!newTabs.empty()) { - const auto source = *focusedTabIndex; - _TryMoveTab(source, tabIndex); + _MoveTabsToIndex(newTabs, tabIndex); + _SetSelectedTabs(newTabs, newTabs.front()); } - // else: This shouldn't really be possible, because the tab we _just_ opened should be active. } } @@ -5537,16 +5539,21 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) { - // Get the tab impl from this event. const auto eventTab = e.Tab(); - const auto tabBase = _GetTabByTabViewItem(eventTab); - winrt::com_ptr tabImpl; - tabImpl.copy_from(winrt::get_self(tabBase)); - if (tabImpl) + const auto draggedTab = _GetTabByTabViewItem(eventTab); + if (draggedTab) { - // First: stash the tab we started dragging. - // We're going to be asked for this. - _stashed.draggedTab = tabImpl; + auto draggedTabs = _IsTabSelected(draggedTab) ? _GetSelectedTabsInDisplayOrder() : + std::vector{}; + if (draggedTabs.empty() || + !std::ranges::any_of(draggedTabs, [&](const auto& tab) { return tab == draggedTab; })) + { + draggedTabs = { draggedTab }; + _SetSelectedTabs(draggedTabs, draggedTab); + } + + _stashed.draggedTabs = std::move(draggedTabs); + _stashed.dragAnchor = draggedTab; // Stash the offset from where we started the drag to the // tab's origin. We'll use that offset in the future to help @@ -5653,6 +5660,11 @@ namespace winrt::TerminalApp::implementation } } + if (index < 0) + { + index = gsl::narrow_cast(_tabView.TabItems().Size()); + } + // `this` is safe to use const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); @@ -5676,12 +5688,12 @@ namespace winrt::TerminalApp::implementation { return; } - if (!_stashed.draggedTab) + if (_stashed.draggedTabs.empty()) { return; } - _sendDraggedTabToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); + _sendDraggedTabsToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); } void TerminalPage::_onTabDroppedOutside(winrt::IInspectable /*sender*/, @@ -5695,7 +5707,7 @@ namespace winrt::TerminalApp::implementation // invoke a moveTab action with the target window being -1. That will // force the creation of a new window. - if (!_stashed.draggedTab) + if (_stashed.draggedTabs.empty()) { return; } @@ -5711,19 +5723,48 @@ namespace winrt::TerminalApp::implementation pointerPoint.X - _stashed.dragOffset.X, pointerPoint.Y - _stashed.dragOffset.Y, }; - _sendDraggedTabToWindow(winrt::hstring{ L"-1" }, 0, adjusted); + _sendDraggedTabsToWindow(winrt::hstring{ L"-1" }, 0, adjusted); } - void TerminalPage::_sendDraggedTabToWindow(const winrt::hstring& windowId, - const uint32_t tabIndex, - std::optional dragPoint) + void TerminalPage::_sendDraggedTabsToWindow(const winrt::hstring& windowId, + const uint32_t tabIndex, + std::optional dragPoint) { - auto startupActions = _stashed.draggedTab->BuildStartupActions(BuildStartupKind::Content); - _DetachTabFromWindow(_stashed.draggedTab); + if (_stashed.draggedTabs.empty()) + { + return; + } + + auto draggedTabs = _stashed.draggedTabs; + auto startupActions = _BuildStartupActionsForTabs(draggedTabs); + if (dragPoint.has_value() && draggedTabs.size() > 1 && _stashed.dragAnchor) + { + const auto draggedAnchorIt = std::ranges::find_if(draggedTabs, [&](const auto& tab) { + return tab == _stashed.dragAnchor; + }); + if (draggedAnchorIt != draggedTabs.end()) + { + ActionAndArgs switchToTabAction{}; + switchToTabAction.Action(ShortcutAction::SwitchToTab); + switchToTabAction.Args(SwitchToTabArgs{ gsl::narrow_cast(std::distance(draggedTabs.begin(), draggedAnchorIt)) }); + startupActions.emplace_back(std::move(switchToTabAction)); + } + } + + for (const auto& tab : draggedTabs) + { + if (const auto tabImpl{ _GetTabImpl(tab) }) + { + _DetachTabFromWindow(tabImpl); + } + } _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); - // _RemoveTab will make sure to null out the _stashed.draggedTab - _RemoveTab(*_stashed.draggedTab); + + for (auto it = draggedTabs.rbegin(); it != draggedTabs.rend(); ++it) + { + _RemoveTab(*it); + } } /// diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 4b48cc0e9d9..55de993ebc3 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -249,6 +249,8 @@ namespace winrt::TerminalApp::implementation std::optional _rearrangeFrom{}; std::optional _rearrangeTo{}; bool _removing{ false }; + std::vector _selectedTabs{}; + winrt::TerminalApp::Tab _selectionAnchor{ nullptr }; bool _activated{ false }; bool _visible{ true }; @@ -287,7 +289,8 @@ namespace winrt::TerminalApp::implementation struct StashedDragData { - winrt::com_ptr draggedTab{ nullptr }; + std::vector draggedTabs{}; + winrt::TerminalApp::Tab dragAnchor{ nullptr }; winrt::Windows::Foundation::Point dragOffset{ 0, 0 }; } _stashed; @@ -495,6 +498,17 @@ namespace winrt::TerminalApp::implementation static uint32_t _ReadSystemRowsToScroll(); void _UpdateMRUTab(const winrt::TerminalApp::Tab& tab); + bool _TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept; + bool _IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept; + void _SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor = nullptr); + void _RemoveSelectedTab(const winrt::TerminalApp::Tab& tab); + std::vector _GetSelectedTabsInDisplayOrder() const; + std::vector _GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const; + void _ApplyMultiSelectionVisuals(); + void _UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab); + void _MoveTabsToIndex(const std::vector& tabs, uint32_t suggestedNewTabIndex); + std::vector _CollectNewTabs(const std::vector& existingTabs) const; + std::vector _BuildStartupActionsForTabs(const std::vector& tabs) const; void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex); @@ -558,7 +572,7 @@ namespace winrt::TerminalApp::implementation const winrt::hstring& windowName, const uint32_t tabIndex, const std::optional& dragPoint = std::nullopt); - void _sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); + void _sendDraggedTabsToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); From 3382c4d738282d3b1490b1fe38573889e2a685ed Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Tue, 24 Mar 2026 09:47:56 +0800 Subject: [PATCH 3/6] Stabilize tab LocalTests Require stable baseline tab observations in the tab LocalTests and surface UI thread exceptions explicitly so the multi-tab drag cases fail diagnostically instead of flaking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../LocalTests_TerminalApp/CppWinrtTailored.h | 26 ++++++- .../LocalTests_TerminalApp/TabTests.cpp | 69 +++++++++++++++---- 2 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/CppWinrtTailored.h b/src/cascadia/LocalTests_TerminalApp/CppWinrtTailored.h index a2d8cbf1c22..9ddcd74ff06 100644 --- a/src/cascadia/LocalTests_TerminalApp/CppWinrtTailored.h +++ b/src/cascadia/LocalTests_TerminalApp/CppWinrtTailored.h @@ -85,7 +85,31 @@ HRESULT RunOnUIThread(const TFunction& function) auto asyncAction = d.RunAsync(winrt::Windows::UI::Core::CoreDispatcherPriority::Normal, [&invokeResult, &function]() { - invokeResult = WEX::SafeInvoke([&]() -> bool { function(); return true; }); + try + { + function(); + invokeResult = S_OK; + } + catch (const winrt::hresult_error& ex) + { + invokeResult = ex.code(); + WEX::Logging::Log::Comment(WEX::Common::NoThrowString().Format( + L"RunOnUIThread caught winrt::hresult_error: 0x%08x (%s)", + static_cast(invokeResult), + ex.message().c_str())); + } + catch (const std::exception& ex) + { + invokeResult = E_FAIL; + WEX::Logging::Log::Comment(WEX::Common::NoThrowString().Format( + L"RunOnUIThread caught std::exception: %hs", + ex.what())); + } + catch (...) + { + invokeResult = E_UNEXPECTED; + WEX::Logging::Log::Comment(L"RunOnUIThread caught unknown exception"); + } }); asyncAction.Completed([&completedEvent](auto&&, auto&&) { diff --git a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp index 7b8a286f72b..b276e697038 100644 --- a/src/cascadia/LocalTests_TerminalApp/TabTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/TabTests.cpp @@ -210,8 +210,11 @@ namespace TerminalAppLocalTests uint32_t actualCount{}; uint32_t actualTabItemCount{}; + uint32_t consecutiveMatches{}; for (auto attempt = 0; attempt < 20; ++attempt) { + _yieldToLowPriorityDispatcher(page); + const auto result = RunOnUIThread([&]() { page->_tabView.UpdateLayout(); actualCount = page->_tabs.Size(); @@ -221,7 +224,14 @@ namespace TerminalAppLocalTests if (actualCount == expectedCount && actualTabItemCount == expectedCount) { - return; + if (++consecutiveMatches >= 2) + { + return; + } + } + else + { + consecutiveMatches = 0; } Sleep(50); @@ -236,31 +246,45 @@ namespace TerminalAppLocalTests VERIFY_IS_NOT_NULL(page); uint32_t latestCount{}; - bool sawZero = false; + uint32_t stableBaselineSamples{}; + uint32_t repairs{}; - for (auto attempt = 0; attempt < 20; ++attempt) + for (auto attempt = 0; attempt < 40; ++attempt) { _yieldToLowPriorityDispatcher(page); auto result = RunOnUIThread([&]() { + page->_tabView.UpdateLayout(); latestCount = page->_tabs.Size(); }); VERIFY_SUCCEEDED(result); if (latestCount == 0) { - sawZero = true; - break; + stableBaselineSamples = 0; + ++repairs; + _openProfileTab(page, 0, 1); + continue; + } + + if (latestCount == 1) + { + if (++stableBaselineSamples >= 3) + { + Log::Comment(NoThrowString().Format(L"Stable baseline final count=%u repairs=%u", latestCount, repairs)); + return; + } + } + else + { + stableBaselineSamples = 0; } Sleep(50); } - Log::Comment(NoThrowString().Format(L"Stable baseline final count=%u sawZero=%d", latestCount, sawZero ? 1 : 0)); - if (sawZero) - { - _openProfileTab(page, 0, 1); - } + Log::Comment(NoThrowString().Format(L"Stable baseline final count=%u repairs=%u stableSamples=%u", latestCount, repairs, stableBaselineSamples)); + VERIFY_ARE_EQUAL(1u, latestCount); } void TabTests::_openProfileTab(const winrt::com_ptr& page, const int32_t profileIndex, const uint32_t expectedCount) @@ -641,7 +665,11 @@ namespace TerminalAppLocalTests Sleep(50); } - result = RunOnUIThread([&page, tabCount, tabItemCount, hasSelectedItem]() { + const auto selectFirstTab = [&page]() { + const auto tabCount = page->_tabs.Size(); + const auto tabItemCount = page->_tabView.TabItems().Size(); + const auto hasSelectedItem = static_cast(page->_tabView.SelectedItem()); + // In the real app, this isn't a problem, but doesn't happen // reliably in the unit tests. Log::Comment(L"Ensure we set the first tab as the selected one."); @@ -660,7 +688,22 @@ namespace TerminalAppLocalTests Log::Comment(L"About to call _UpdatedSelectedTab"); page->_UpdatedSelectedTab(tab); Log::Comment(L"Selected first tab successfully"); - }); + }; + + result = RunOnUIThread(selectFirstTab); + VERIFY_SUCCEEDED(result); + + _ensureStableBaselineTab(page); + _waitForTabCount(page, 1); + + result = RunOnUIThread(selectFirstTab); + VERIFY_SUCCEEDED(result); + + _yieldToLowPriorityDispatcher(page); + _ensureStableBaselineTab(page); + _waitForTabCount(page, 1); + + result = RunOnUIThread(selectFirstTab); VERIFY_SUCCEEDED(result); } @@ -700,6 +743,8 @@ namespace TerminalAppLocalTests // it's weird. winrt::com_ptr page{ nullptr }; _initializeTerminalPage(page, settings0); + _ensureStableBaselineTab(page); + _waitForTabCount(page, 1); auto result = RunOnUIThread([&page]() { VERIFY_ARE_EQUAL(1u, page->_tabs.Size()); From 278af5c59df3ef881aeeaaf4d061b9bdd7774c24 Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Tue, 24 Mar 2026 09:48:11 +0800 Subject: [PATCH 4/6] Add UIA coverage for multi-window tab drag Preserve multi-tab drag state through cross-window drops, add a minimal test-only tab-range selection hook, and extend the UIA smoke harness so it can validate tear-out and attach flows across real top- level windows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cascadia/TerminalApp/TabManagement.cpp | 132 +- src/cascadia/TerminalApp/TerminalPage.cpp | 11698 ++++++++-------- src/cascadia/TerminalApp/TerminalPage.h | 1207 +- src/cascadia/TerminalApp/TerminalWindow.cpp | 5 + src/cascadia/TerminalApp/TerminalWindow.h | 1 + src/cascadia/TerminalApp/TerminalWindow.idl | 1 + src/cascadia/WindowsTerminal/AppHost.cpp | 15 + src/cascadia/WindowsTerminal/AppHost.h | 1 + src/cascadia/WindowsTerminal/BaseWindow.h | 1 + src/cascadia/WindowsTerminal/IslandWindow.cpp | 14 + src/cascadia/WindowsTerminal/IslandWindow.h | 334 +- .../Common/NativeMethods.cs | 28 + .../Elements/TerminalApp.cs | 1148 +- .../WindowsTerminal_UIATests/SmokeTests.cs | 85 + 14 files changed, 8053 insertions(+), 6617 deletions(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index b4a603bd896..71f5d7223f5 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -48,6 +48,71 @@ namespace winrt using IInspectable = Windows::Foundation::IInspectable; } +namespace +{ + void _appendUiaDragLog(const wchar_t* message) noexcept + { + std::wstring buffer(MAX_PATH, L'\0'); + const auto length = GetEnvironmentVariableW(L"WT_UIA_DRAG_LOG", buffer.data(), gsl::narrow_cast(buffer.size())); + if (length == 0 || length >= buffer.size()) + { + return; + } + + buffer.resize(length); + + const wil::unique_hfile file{ CreateFileW(buffer.c_str(), + FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr) }; + if (!file) + { + return; + } + + const auto line = winrt::to_string(std::wstring{ message } + L"\r\n"); + DWORD bytesWritten = 0; + WriteFile(file.get(), line.data(), gsl::narrow_cast(line.size()), &bytesWritten, nullptr); + } + + std::wstring _tabTitleForUiaLog(const winrt::TerminalApp::Tab& tab) + { + if (!tab) + { + return L""; + } + + const auto title = tab.Title(); + return title.empty() ? L"" : std::wstring{ title }; + } + + void _appendUiaSelectionLog(const std::vector& tabs, const winrt::TerminalApp::Tab& anchor) noexcept + { + std::wstring message{ L"_SetSelectedTabs: count=" }; + message += std::to_wstring(tabs.size()); + message += L", anchor="; + message += _tabTitleForUiaLog(anchor); + message += L", tabs=["; + + bool first = true; + for (const auto& tab : tabs) + { + if (!first) + { + message += L", "; + } + first = false; + message += _tabTitleForUiaLog(tab); + } + + message += L"]"; + _appendUiaDragLog(message.c_str()); + } +} + namespace winrt::TerminalApp::implementation { // Method Description: @@ -1151,6 +1216,7 @@ namespace winrt::TerminalApp::implementation _selectionAnchor = _selectedTabs.front(); } + _appendUiaSelectionLog(_selectedTabs, _selectionAnchor); _ApplyMultiSelectionVisuals(); } @@ -1214,10 +1280,59 @@ namespace winrt::TerminalApp::implementation return tabs; } + bool TerminalPage::SelectTabRangeForTesting(const uint32_t startIndex, const uint32_t endIndex) + { + const auto tabCount = _tabs.Size(); + if (tabCount == 0 || startIndex >= tabCount || endIndex >= tabCount) + { + const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: invalid range {}-{} count={}"), + startIndex, + endIndex, + tabCount); + _appendUiaDragLog(message.c_str()); + return false; + } + + const auto startTab = _tabs.GetAt(startIndex); + const auto endTab = _tabs.GetAt(endIndex); + if (!_TabSupportsMultiSelection(startTab) || !_TabSupportsMultiSelection(endTab)) + { + const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: unsupported tabs start={} end={}"), + _tabTitleForUiaLog(startTab), + _tabTitleForUiaLog(endTab)); + _appendUiaDragLog(message.c_str()); + return false; + } + + _SelectTab(startIndex); + auto range = _GetTabRange(startTab, endTab); + if (range.empty()) + { + const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: empty range start={} end={}"), + _tabTitleForUiaLog(startTab), + _tabTitleForUiaLog(endTab)); + _appendUiaDragLog(message.c_str()); + return false; + } + + const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: start={} end={}"), + _tabTitleForUiaLog(startTab), + _tabTitleForUiaLog(endTab)); + _appendUiaDragLog(message.c_str()); + _SetSelectedTabs(std::move(range), startTab); + return true; + } + void TerminalPage::_UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab) { const bool ctrlPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_CONTROL)), 0x8000); const bool shiftPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_SHIFT)), 0x8000); + const auto clickedTitle = _tabTitleForUiaLog(tab); + const auto pointerMessage = til::hstring_format(FMT_COMPILE(L"_UpdateSelectionFromPointer: tab={}, ctrl={}, shift={}"), + clickedTitle, + ctrlPressed ? 1 : 0, + shiftPressed ? 1 : 0); + _appendUiaDragLog(pointerMessage.c_str()); if (!_TabSupportsMultiSelection(tab)) { @@ -1389,14 +1504,17 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_TabDragStarted(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) { + _appendUiaDragLog(L"_TabDragStarted"); _rearranging = true; _rearrangeFrom = std::nullopt; _rearrangeTo = std::nullopt; } void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/, - const IInspectable& /*eventArgs*/) + const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs) { + const auto dropResultLog = std::wstring{ L"_TabDragCompleted: dropResult=" } + std::to_wstring(static_cast(eventArgs.DropResult())); + _appendUiaDragLog(dropResultLog.c_str()); auto& from{ _rearrangeFrom }; auto& to{ _rearrangeTo }; @@ -1414,8 +1532,16 @@ namespace winrt::TerminalApp::implementation } _rearranging = false; - _stashed.draggedTabs.clear(); - _stashed.dragAnchor = nullptr; + if (from.has_value() || to.has_value()) + { + _appendUiaDragLog(L"_TabDragCompleted: clearing stashed drag data after in-window reorder"); + _stashed.draggedTabs.clear(); + _stashed.dragAnchor = nullptr; + } + else + { + _appendUiaDragLog(L"_TabDragCompleted: preserving stashed drag data for post-complete drop handlers"); + } if (to.has_value() && *to < gsl::narrow_cast(TabRow().TabView().TabItems().Size())) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 15acdb80411..42739842ea9 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1,5814 +1,5884 @@ - -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "TerminalPage.h" - -#include -#include -#include -#include -#include - -#include "../../types/inc/ColorFix.hpp" -#include "../../types/inc/utils.hpp" -#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" -#include "App.h" -#include "DebugTapConnection.h" -#include "MarkdownPaneContent.h" -#include "Remoting.h" -#include "ScratchpadContent.h" -#include "SettingsPaneContent.h" -#include "SnippetsPaneContent.h" -#include "TabRowControl.h" -#include "TerminalSettingsCache.h" - -#include "LaunchPositionRequest.g.cpp" -#include "RenameWindowRequestedArgs.g.cpp" -#include "RequestMoveContentArgs.g.cpp" -#include "TerminalPage.g.cpp" - -using namespace winrt; -using namespace winrt::Microsoft::Management::Deployment; -using namespace winrt::Microsoft::Terminal::Control; -using namespace winrt::Microsoft::Terminal::Settings::Model; -using namespace winrt::Microsoft::Terminal::TerminalConnection; -using namespace winrt::Microsoft::Terminal; -using namespace winrt::Windows::ApplicationModel::DataTransfer; -using namespace winrt::Windows::Foundation::Collections; -using namespace winrt::Windows::System; -using namespace winrt::Windows::UI; -using namespace winrt::Windows::UI::Core; -using namespace winrt::Windows::UI::Text; -using namespace winrt::Windows::UI::Xaml::Controls; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Xaml::Media; -using namespace ::TerminalApp; -using namespace ::Microsoft::Console; -using namespace ::Microsoft::Terminal::Core; -using namespace std::chrono_literals; - -#define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action }); - -namespace winrt -{ - namespace MUX = Microsoft::UI::Xaml; - namespace WUX = Windows::UI::Xaml; - using IInspectable = Windows::Foundation::IInspectable; - using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers; -} - -namespace clipboard -{ - static SRWLOCK lock = SRWLOCK_INIT; - - struct ClipboardHandle - { - explicit ClipboardHandle(bool open) : - _open{ open } - { - } - - ~ClipboardHandle() - { - if (_open) - { - ReleaseSRWLockExclusive(&lock); - CloseClipboard(); - } - } - - explicit operator bool() const noexcept - { - return _open; - } - - private: - bool _open = false; - }; - - ClipboardHandle open(HWND hwnd) - { - // Turns out, OpenClipboard/CloseClipboard are not thread-safe whatsoever, - // and on CloseClipboard, the GetClipboardData handle may get freed. - // The problem is that WinUI also uses OpenClipboard (through WinRT which uses OLE), - // and so even with this mutex we can still crash randomly if you copy something via WinUI. - // Makes you wonder how many Windows apps are subtly broken, huh. - AcquireSRWLockExclusive(&lock); - - bool success = false; - - // OpenClipboard may fail to acquire the internal lock --> retry. - for (DWORD sleep = 10;; sleep *= 2) - { - if (OpenClipboard(hwnd)) - { - success = true; - break; - } - // 10 iterations - if (sleep > 10000) - { - break; - } - Sleep(sleep); - } - - if (!success) - { - ReleaseSRWLockExclusive(&lock); - } - - return ClipboardHandle{ success }; - } - - void write(wil::zwstring_view text, std::string_view html, std::string_view rtf) - { - static const auto regular = [](const UINT format, const void* src, const size_t bytes) { - wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) }; - - const auto locked = GlobalLock(handle.get()); - memcpy(locked, src, bytes); - GlobalUnlock(handle.get()); - - THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get())); - handle.release(); - }; - static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) { - const auto id = RegisterClipboardFormatW(format); - if (!id) - { - LOG_LAST_ERROR(); - return; - } - regular(id, src, bytes); - }; - - EmptyClipboard(); - - if (!text.empty()) - { - // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats - // CF_UNICODETEXT: [...] A null character signals the end of the data. - // --> We add +1 to the length. This works because .c_str() is null-terminated. - regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); - } - - if (!html.empty()) - { - registered(L"HTML Format", html.data(), html.size()); - } - - if (!rtf.empty()) - { - registered(L"Rich Text Format", rtf.data(), rtf.size()); - } - } - - winrt::hstring read() - { - // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically. - if (const auto handle = GetClipboardData(CF_UNICODETEXT)) - { - const wil::unique_hglobal_locked lock{ handle }; - const auto str = static_cast(lock.get()); - if (!str) - { - return {}; - } - - const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); - const auto len = wcsnlen(str, maxLen); - return winrt::hstring{ str, gsl::narrow_cast(len) }; - } - - // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others). - if (const auto handle = GetClipboardData(CF_HDROP)) - { - const wil::unique_hglobal_locked lock{ handle }; - const auto drop = static_cast(lock.get()); - if (!drop) - { - return {}; - } - - const auto cap = DragQueryFileW(drop, 0, nullptr, 0); - if (cap == 0) - { - return {}; - } - - auto buffer = winrt::impl::hstring_builder{ cap }; - const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); - if (len == 0) - { - return {}; - } - - return buffer.to_hstring(); - } - - return {}; - } -} // namespace clipboard - -namespace winrt::TerminalApp::implementation -{ - TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : - _tabs{ winrt::single_threaded_observable_vector() }, - _mruTabs{ winrt::single_threaded_observable_vector() }, - _manager{ manager }, - _hostingHwnd{}, - _WindowProperties{ std::move(properties) } - { - InitializeComponent(); - _WindowProperties.PropertyChanged({ get_weak(), &TerminalPage::_windowPropertyChanged }); - } - - // Method Description: - // - implements the IInitializeWithWindow interface from shobjidl_core. - // - We're going to use this HWND as the owner for the ConPTY windows, via - // ConptyConnection::ReparentWindow. We need this for applications that - // call GetConsoleWindow, and attempt to open a MessageBox for the - // console. By marking the conpty windows as owned by the Terminal HWND, - // the message box will be owned by the Terminal window as well. - // - see GH#2988 - HRESULT TerminalPage::Initialize(HWND hwnd) - { - if (!_hostingHwnd.has_value()) - { - // GH#13211 - if we haven't yet set the owning hwnd, reparent all the controls now. - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { - if (const auto& term{ pane->GetTerminalControl() }) - { - term.OwningHwnd(reinterpret_cast(hwnd)); - } - }); - } - // We don't need to worry about resetting the owning hwnd for the - // SUI here. GH#13211 only repros for a defterm connection, where - // the tab is spawned before the window is created. It's not - // possible to make a SUI tab like that, before the window is - // created. The SUI could be spawned as a part of a window restore, - // but that would still work fine. The window would be created - // before restoring previous tabs in that scenario. - } - } - - _hostingHwnd = hwnd; - return S_OK; - } - - // INVARIANT: This needs to be called on OUR UI thread! - void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) - { - assert(Dispatcher().HasThreadAccess()); - if (_settings == nullptr) - { - // Create this only on the first time we load the settings. - _terminalSettingsCache = std::make_shared(settings); - } - _settings = settings; - - // Make sure to call SetCommands before _RefreshUIForSettingsReload. - // SetCommands will make sure the KeyChordText of Commands is updated, which needs - // to happen before the Settings UI is reloaded and tries to re-read those values. - if (const auto p = CommandPaletteElement()) - { - p.SetActionMap(_settings.ActionMap()); - } - - if (needRefreshUI) - { - _RefreshUIForSettingsReload(); - } - - // Upon settings update we reload the system settings for scrolling as well. - // TODO: consider reloading this value periodically. - _systemRowsToScroll = _ReadSystemRowsToScroll(); - } - - bool TerminalPage::IsRunningElevated() const noexcept - { - // GH#2455 - Make sure to try/catch calls to Application::Current, - // because that _won't_ be an instance of TerminalApp::App in the - // LocalTests - try - { - return Application::Current().as().Logic().IsRunningElevated(); - } - CATCH_LOG(); - return false; - } - bool TerminalPage::CanDragDrop() const noexcept - { - try - { - return Application::Current().as().Logic().CanDragDrop(); - } - CATCH_LOG(); - return true; - } - - void TerminalPage::Create() - { - // Hookup the key bindings - _HookupKeyBindings(_settings.ActionMap()); - - _tabContent = this->TabContent(); - _tabRow = this->TabRow(); - _tabView = _tabRow.TabView(); - _rearranging = false; - - const auto canDragDrop = CanDragDrop(); - - _tabView.CanReorderTabs(canDragDrop); - _tabView.CanDragTabs(canDragDrop); - _tabView.TabDragStarting({ get_weak(), &TerminalPage::_TabDragStarted }); - _tabView.TabDragCompleted({ get_weak(), &TerminalPage::_TabDragCompleted }); - - auto tabRowImpl = winrt::get_self(_tabRow); - _newTabButton = tabRowImpl->NewTabButton(); - - if (_settings.GlobalSettings().ShowTabsInTitlebar()) - { - // Remove the TabView from the page. We'll hang on to it, we need to - // put it in the titlebar. - uint32_t index = 0; - if (this->Root().Children().IndexOf(_tabRow, index)) - { - this->Root().Children().RemoveAt(index); - } - - // Inform the host that our titlebar content has changed. - SetTitleBarContent.raise(*this, _tabRow); - - // GH#13143 Manually set the tab row's background to transparent here. - // - // We're doing it this way because ThemeResources are tricky. We - // default in XAML to using the appropriate ThemeResource background - // color for our TabRow. When tabs in the titlebar are _disabled_, - // this will ensure that the tab row has the correct theme-dependent - // value. When tabs in the titlebar are _enabled_ (the default), - // we'll switch the BG to Transparent, to let the Titlebar Control's - // background be used as the BG for the tab row. - // - // We can't do it the other way around (default to Transparent, only - // switch to a color when disabling tabs in the titlebar), because - // looking up the correct ThemeResource from and App dictionary is a - // capital-H Hard problem. - const auto transparent = Media::SolidColorBrush(); - transparent.Color(Windows::UI::Colors::Transparent()); - _tabRow.Background(transparent); - } - _updateThemeColors(); - - // Initialize the state of the CloseButtonOverlayMode property of - // our TabView, to match the tab.showCloseButton property in the theme. - if (const auto theme = _settings.GlobalSettings().CurrentTheme()) - { - const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always; - - _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; - - switch (visibility) - { - case Settings::Model::TabCloseButtonVisibility::Never: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); - break; - case Settings::Model::TabCloseButtonVisibility::Hover: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); - break; - default: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); - break; - } - } - - // Hookup our event handlers to the ShortcutActionDispatch - _RegisterActionCallbacks(); - - //Event Bindings (Early) - _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuDefaultButtonClicked", - TraceLoggingDescription("Event emitted when the default button from the new tab split button is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); - } - }); - _newTabButton.Drop({ get_weak(), &TerminalPage::_NewTerminalByDrop }); - _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); - _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); - _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); - - _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); - _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); - _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop }); - _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside }); - - _CreateNewTabFlyout(); - - _UpdateTabWidthMode(); - - // Settings AllowDependentAnimations will affect whether animations are - // enabled application-wide, so we don't need to check it each time we - // want to create an animation. - WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); - - // Once the page is actually laid out on the screen, trigger all our - // startup actions. Things like Panes need to know at least how big the - // window will be, so they can subdivide that space. - // - // _OnFirstLayout will remove this handler so it doesn't get called more than once. - _layoutUpdatedRevoker = _tabContent.LayoutUpdated(winrt::auto_revoke, { this, &TerminalPage::_OnFirstLayout }); - - _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); - _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); - - // DON'T set up Toasts/TeachingTips here. They should be loaded and - // initialized the first time they're opened, in whatever method opens - // them. - - _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); - - _adjustProcessPriorityThrottled = std::make_shared>( - DispatcherQueue::GetForCurrentThread(), - til::throttled_func_options{ - .delay = std::chrono::milliseconds{ 100 }, - .debounce = true, - .trailing = true, - }, - [=]() { - _adjustProcessPriority(); - }); - } - - Windows::UI::Xaml::Automation::Peers::AutomationPeer TerminalPage::OnCreateAutomationPeer() - { - return Automation::Peers::FrameworkElementAutomationPeer(*this); - } - - // Method Description: - // - This is a bit of trickiness: If we're running unelevated, and the user - // passed in only --elevate actions, the we don't _actually_ want to - // restore the layouts here. We're not _actually_ about to create the - // window. We're simply going to toss the commandlines - // Arguments: - // - - // Return Value: - // - true if we're not elevated but all relevant pane-spawning actions are elevated - bool TerminalPage::ShouldImmediatelyHandoffToElevated(const CascadiaSettings& settings) const - { - if (_startupActions.empty() || _startupConnection || IsRunningElevated()) - { - // No point in handing off if we got no startup actions, or we're already elevated. - // Also, we shouldn't need to elevate handoff ConPTY connections. - assert(!_startupConnection); - return false; - } - - // Check that there's at least one action that's not just an elevated newTab action. - for (const auto& action : _startupActions) - { - // Only new terminal panes will be requesting elevation. - NewTerminalArgs newTerminalArgs{ nullptr }; - - if (action.Action() == ShortcutAction::NewTab) - { - const auto& args{ action.Args().try_as() }; - if (args) - { - newTerminalArgs = args.ContentArgs().try_as(); - } - else - { - // This was a nt action that didn't have any args. The default - // profile may want to be elevated, so don't just early return. - } - } - else if (action.Action() == ShortcutAction::SplitPane) - { - const auto& args{ action.Args().try_as() }; - if (args) - { - newTerminalArgs = args.ContentArgs().try_as(); - } - else - { - // This was a nt action that didn't have any args. The default - // profile may want to be elevated, so don't just early return. - } - } - else - { - // This was not a new tab or split pane action. - // This doesn't affect the outcome - continue; - } - - // It's possible that newTerminalArgs is null here. - // GetProfileForArgs should be resilient to that. - const auto profile{ settings.GetProfileForArgs(newTerminalArgs) }; - if (profile.Elevate()) - { - continue; - } - - // The profile didn't want to be elevated, and we aren't elevated. - // We're going to open at least one tab, so return false. - return false; - } - return true; - } - - // Method Description: - // - Escape hatch for immediately dispatching requests to elevated windows - // when first launched. At this point in startup, the window doesn't exist - // yet, XAML hasn't been started, but we need to dispatch these actions. - // We can't just go through ProcessStartupActions, because that processes - // the actions async using the XAML dispatcher (which doesn't exist yet) - // - DON'T CALL THIS if you haven't already checked - // ShouldImmediatelyHandoffToElevated. If you're thinking about calling - // this outside of the one place it's used, that's probably the wrong - // solution. - // Arguments: - // - settings: the settings we should use for dispatching these actions. At - // this point in startup, we hadn't otherwise been initialized with these, - // so use them now. - // Return Value: - // - - void TerminalPage::HandoffToElevated(const CascadiaSettings& settings) - { - if (_startupActions.empty()) - { - return; - } - - // Hookup our event handlers to the ShortcutActionDispatch - _settings = settings; - _HookupKeyBindings(_settings.ActionMap()); - _RegisterActionCallbacks(); - - for (const auto& action : _startupActions) - { - // only process new tabs and split panes. They're all going to the elevated window anyways. - if (action.Action() == ShortcutAction::NewTab || action.Action() == ShortcutAction::SplitPane) - { - _actionDispatch->DoAction(action); - } - } - } - - safe_void_coroutine TerminalPage::_NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e) - try - { - const auto data = e.DataView(); - if (!data.Contains(StandardDataFormats::StorageItems())) - { - co_return; - } - - const auto weakThis = get_weak(); - const auto items = co_await data.GetStorageItemsAsync(); - const auto strongThis = weakThis.get(); - if (!strongThis) - { - co_return; - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabByDragDrop", - TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - for (const auto& item : items) - { - auto directory = item.Path(); - - std::filesystem::path path(std::wstring_view{ directory }); - if (!std::filesystem::is_directory(path)) - { - directory = winrt::hstring{ path.parent_path().native() }; - } - - NewTerminalArgs args; - args.StartingDirectory(directory); - _OpenNewTerminalViaDropdown(args); - } - } - CATCH_LOG() - - // Method Description: - // - This method is called once command palette action was chosen for dispatching - // We'll use this event to dispatch this command. - // Arguments: - // - command - command to dispatch - // Return Value: - // - - void TerminalPage::_OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command) - { - const auto& actionAndArgs = command.ActionAndArgs(); - _actionDispatch->DoAction(sender, actionAndArgs); - } - - // Method Description: - // - This method is called once command palette command line was chosen for execution - // We'll use this event to create a command line execution command and dispatch it. - // Arguments: - // - command - command to dispatch - // Return Value: - // - - void TerminalPage::_OnCommandLineExecutionRequested(const IInspectable& /*sender*/, const winrt::hstring& commandLine) - { - ExecuteCommandlineArgs args{ commandLine }; - ActionAndArgs actionAndArgs{ ShortcutAction::ExecuteCommandline, args }; - _actionDispatch->DoAction(actionAndArgs); - } - - // Method Description: - // - This method is called once on startup, on the first LayoutUpdated event. - // We'll use this event to know that we have an ActualWidth and - // ActualHeight, so we can now attempt to process our list of startup - // actions. - // - We'll remove this event handler when the event is first handled. - // - If there are no startup actions, we'll open a single tab with the - // default profile. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_OnFirstLayout(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) - { - // Only let this succeed once. - _layoutUpdatedRevoker.revoke(); - - // This event fires every time the layout changes, but it is always the - // last one to fire in any layout change chain. That gives us great - // flexibility in finding the right point at which to initialize our - // renderer (and our terminal). Any earlier than the last layout update - // and we may not know the terminal's starting size. - if (_startupState == StartupState::NotInitialized) - { - _startupState = StartupState::InStartup; - - if (_startupConnection) - { - CreateTabFromConnection(std::move(_startupConnection)); - } - else if (!_startupActions.empty()) - { - ProcessStartupActions(std::move(_startupActions)); - } - - _CompleteInitialization(); - } - } - - // Method Description: - // - Process all the startup actions in the provided list of startup - // actions. We'll do this all at once here. - // Arguments: - // - actions: a winrt vector of actions to process. Note that this must NOT - // be an IVector&, because we need the collection to be accessible on the - // other side of the co_await. - // - initial: if true, we're parsing these args during startup, and we - // should fire an Initialized event. - // - cwd: If not empty, we should try switching to this provided directory - // while processing these actions. This will allow something like `wt -w 0 - // nt -d .` from inside another directory to work as expected. - // Return Value: - // - - safe_void_coroutine TerminalPage::ProcessStartupActions(std::vector actions, const winrt::hstring cwd, const winrt::hstring env) - { - const auto strong = get_strong(); - - // If the caller provided a CWD, "switch" to that directory, then switch - // back once we're done. - auto originalVirtualCwd{ _WindowProperties.VirtualWorkingDirectory() }; - auto originalVirtualEnv{ _WindowProperties.VirtualEnvVars() }; - auto restoreCwd = wil::scope_exit([&]() { - if (!cwd.empty()) - { - // ignore errors, we'll just power on through. We'd rather do - // something rather than fail silently if the directory doesn't - // actually exist. - _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd); - _WindowProperties.VirtualEnvVars(originalVirtualEnv); - } - }); - if (!cwd.empty()) - { - _WindowProperties.VirtualWorkingDirectory(cwd); - _WindowProperties.VirtualEnvVars(env); - } - - // The current TerminalWindow & TerminalPage architecture is rather instable - // and fails to start up if the first tab isn't created synchronously. - // - // While that's a fair assumption in on itself, simultaneously WinUI will - // not assign tab contents a size if they're not shown at least once, - // which we need however in order to initialize ControlCore with a size. - // - // So, we do two things here: - // * DO NOT suspend if this is the first tab. - // * DO suspend between the creation of panes (or tabs) in order to allow - // WinUI to layout the new controls and for ControlCore to get a size. - // - // This same logic is also applied to CreateTabFromConnection. - // - // See GH#13136. - auto suspend = _tabs.Size() > 0; - - for (size_t i = 0; i < actions.size(); ++i) - { - if (suspend) - { - co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); - } - - _actionDispatch->DoAction(actions[i]); - suspend = true; - } - - // GH#6586: now that we're done processing all startup commands, - // focus the active control. This will work as expected for both - // commandline invocations and for `wt` action invocations. - if (const auto& tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto& content{ tabImpl->GetActiveContent() }) - { - content.Focus(FocusState::Programmatic); - } - } - } - - safe_void_coroutine TerminalPage::CreateTabFromConnection(ITerminalConnection connection) - { - const auto strong = get_strong(); - - // This is the exact same logic as in ProcessStartupActions. - if (_tabs.Size() > 0) - { - co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); - } - - NewTerminalArgs newTerminalArgs; - - if (const auto conpty = connection.try_as()) - { - newTerminalArgs.Commandline(conpty.Commandline()); - newTerminalArgs.TabTitle(conpty.StartingTitle()); - } - - // GH #12370: We absolutely cannot allow a defterm connection to - // auto-elevate. Defterm doesn't work for elevated scenarios in the - // first place. If we try accepting the connection, the spawning an - // elevated version of the Terminal with that profile... that's a - // recipe for disaster. We won't ever open up a tab in this window. - newTerminalArgs.Elevate(false); - - const auto newPane = _MakePane(newTerminalArgs, nullptr, std::move(connection)); - newPane->WalkTree([](const auto& pane) { - pane->FinalizeConfigurationGivenDefault(); - }); - _CreateNewTabFromPane(newPane); - } - - // Method Description: - // - Perform and steps that need to be done once our initial state is all - // set up. This includes entering fullscreen mode and firing our - // Initialized event. - // Arguments: - // - - // Return Value: - // - - safe_void_coroutine TerminalPage::_CompleteInitialization() - { - _startupState = StartupState::Initialized; - - // GH#632 - It's possible that the user tried to create the terminal - // with only one tab, with only an elevated profile. If that happens, - // we'll create _another_ process to host the elevated version of that - // profile. This can happen from the jumplist, or if the default profile - // is `elevate:true`, or from the commandline. - // - // However, we need to make sure to close this window in that scenario. - // Since there aren't any _tabs_ in this window, we won't ever get a - // closed event. So do it manually. - // - // GH#12267: Make sure that we don't instantly close ourselves when - // we're readying to accept a defterm connection. In that case, we don't - // have a tab yet, but will once we're initialized. - if (_tabs.Size() == 0) - { - CloseWindowRequested.raise(*this, nullptr); - co_return; - } - else - { - // GH#11561: When we start up, our window is initially just a frame - // with a transparent content area. We're gonna do all this startup - // init on the UI thread, so the UI won't actually paint till it's - // all done. This results in a few frames where the frame is - // visible, before the page paints for the first time, before any - // tabs appears, etc. - // - // To mitigate this, we're gonna wait for the UI thread to finish - // everything it's gotta do for the initial init, and _then_ fire - // our Initialized event. By waiting for everything else to finish - // (CoreDispatcherPriority::Low), we let all the tabs and panes - // actually get created. In the window layer, we're gonna cloak the - // window till this event is fired, so we don't actually see this - // frame until we're actually all ready to go. - // - // This will result in the window seemingly not loading as fast, but - // it will actually take exactly the same amount of time before it's - // usable. - // - // We also experimented with drawing a solid BG color before the - // initialization is finished. However, there are still a few frames - // after the frame is displayed before the XAML content first draws, - // so that didn't actually resolve any issues. - Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weak = get_weak()]() { - if (auto self{ weak.get() }) - { - self->Initialized.raise(*self, nullptr); - } - }); - } - } - - // Method Description: - // - Show a dialog with "About" information. Displays the app's Display - // Name, version, getting started link, source code link, documentation link, release - // Notes link, send feedback link and privacy policy link. - void TerminalPage::_ShowAboutDialog() - { - _ShowDialogHelper(L"AboutDialog"); - } - - winrt::hstring TerminalPage::ApplicationDisplayName() - { - return CascadiaSettings::ApplicationDisplayName(); - } - - winrt::hstring TerminalPage::ApplicationVersion() - { - return CascadiaSettings::ApplicationVersion(); - } - - // Method Description: - // - Helper to show a content dialog - // - We only open a content dialog if there isn't one open already - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowDialogHelper(const std::wstring_view& name) - { - if (auto presenter{ _dialogPresenter.get() }) - { - co_return co_await presenter.ShowDialog(FindName(name).try_as()); - } - co_return ContentDialogResult::None; - } - - // Method Description: - // - Displays a dialog to warn the user that they are about to close all open windows. - // Once the user clicks the OK button, shut down the application. - // If cancel is clicked, the dialog will close. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowQuitDialog() - { - return _ShowDialogHelper(L"QuitDialog"); - } - - // Method Description: - // - Displays a dialog for warnings found while closing the terminal app using - // key binding with multiple tabs opened. Display messages to warn user - // that more than 1 tab is opened, and once the user clicks the OK button, remove - // all the tabs and shut down and app. If cancel is clicked, the dialog will close - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseWarningDialog() - { - return _ShowDialogHelper(L"CloseAllDialog"); - } - - // Method Description: - // - Displays a dialog for warnings found while closing the terminal tab marked as read-only - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseReadOnlyDialog() - { - return _ShowDialogHelper(L"CloseReadOnlyDialog"); - } - - // Method Description: - // - Displays a dialog to warn the user about the fact that the text that - // they are trying to paste contains the "new line" character which can - // have the effect of starting commands without the user's knowledge if - // it is pasted on a shell where the "new line" character marks the end - // of a command. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowMultiLinePasteWarningDialog() - { - return _ShowDialogHelper(L"MultiLinePasteDialog"); - } - - // Method Description: - // - Displays a dialog to warn the user about the fact that the text that - // they are trying to paste is very long, in case they did not mean to - // paste it but pressed the paste shortcut by accident. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowLargePasteWarningDialog() - { - return _ShowDialogHelper(L"LargePasteDialog"); - } - - // Method Description: - // - Builds the flyout (dropdown) attached to the new tab button, and - // attaches it to the button. Populates the flyout with one entry per - // Profile, displaying the profile's name. Clicking each flyout item will - // open a new tab with that profile. - // Below the profiles are the static menu items: settings, command palette - void TerminalPage::_CreateNewTabFlyout() - { - auto newTabFlyout = WUX::Controls::MenuFlyout{}; - newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft); - - // Create profile entries from the NewTabMenu configuration using a - // recursive helper function. This returns a std::vector of FlyoutItemBases, - // that we then add to our Flyout. - auto entries = _settings.GlobalSettings().NewTabMenu(); - auto items = _CreateNewTabFlyoutItems(entries); - for (const auto& item : items) - { - newTabFlyout.Items().Append(item); - } - - // add menu separator - auto separatorItem = WUX::Controls::MenuFlyoutSeparator{}; - newTabFlyout.Items().Append(separatorItem); - - // add static items - { - // Create the settings button. - auto settingsItem = WUX::Controls::MenuFlyoutItem{}; - settingsItem.Text(RS_(L"SettingsMenuItem")); - const auto settingsToolTip = RS_(L"SettingsToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(settingsItem, box_value(settingsToolTip)); - Automation::AutomationProperties::SetHelpText(settingsItem, settingsToolTip); - - WUX::Controls::SymbolIcon ico{}; - ico.Symbol(WUX::Controls::Symbol::Setting); - settingsItem.Icon(ico); - - settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); - newTabFlyout.Items().Append(settingsItem); - - auto actionMap = _settings.ActionMap(); - const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenSettingsUI") }; - if (settingsKeyChord) - { - _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); - } - - // Create the command palette button. - auto commandPaletteFlyout = WUX::Controls::MenuFlyoutItem{}; - commandPaletteFlyout.Text(RS_(L"CommandPaletteMenuItem")); - const auto commandPaletteToolTip = RS_(L"CommandPaletteToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(commandPaletteFlyout, box_value(commandPaletteToolTip)); - Automation::AutomationProperties::SetHelpText(commandPaletteFlyout, commandPaletteToolTip); - - WUX::Controls::FontIcon commandPaletteIcon{}; - commandPaletteIcon.Glyph(L"\xE945"); - commandPaletteIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); - commandPaletteFlyout.Icon(commandPaletteIcon); - - commandPaletteFlyout.Click({ this, &TerminalPage::_CommandPaletteButtonOnClick }); - newTabFlyout.Items().Append(commandPaletteFlyout); - - const auto commandPaletteKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.ToggleCommandPalette") }; - if (commandPaletteKeyChord) - { - _SetAcceleratorForMenuItem(commandPaletteFlyout, commandPaletteKeyChord); - } - - // Create the about button. - auto aboutFlyout = WUX::Controls::MenuFlyoutItem{}; - aboutFlyout.Text(RS_(L"AboutMenuItem")); - const auto aboutToolTip = RS_(L"AboutToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(aboutFlyout, box_value(aboutToolTip)); - Automation::AutomationProperties::SetHelpText(aboutFlyout, aboutToolTip); - - WUX::Controls::SymbolIcon aboutIcon{}; - aboutIcon.Symbol(WUX::Controls::Symbol::Help); - aboutFlyout.Icon(aboutIcon); - - aboutFlyout.Click({ this, &TerminalPage::_AboutButtonOnClick }); - newTabFlyout.Items().Append(aboutFlyout); - } - - // Before opening the fly-out set focus on the current tab - // so no matter how fly-out is closed later on the focus will return to some tab. - // We cannot do it on closing because if the window loses focus (alt+tab) - // the closing event is not fired. - // It is important to set the focus on the tab - // Since the previous focus location might be discarded in the background, - // e.g., the command palette will be dismissed by the menu, - // and then closing the fly-out will move the focus to wrong location. - newTabFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - page->_FocusCurrentTab(true); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuOpened", - TraceLoggingDescription("Event emitted when the new tab menu is opened"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }); - // Necessary for fly-out sub items to get focus on a tab before collapsing. Related to #15049 - newTabFlyout.Closing([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - if (!page->_commandPaletteIs(Visibility::Visible)) - { - page->_FocusCurrentTab(true); - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuClosed", - TraceLoggingDescription("Event emitted when the new tab menu is closed"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }); - _newTabButton.Flyout(newTabFlyout); - } - - // Method Description: - // - For a given list of tab menu entries, this method will create the corresponding - // list of flyout items. This is a recursive method that calls itself when it comes - // across a folder entry. - std::vector TerminalPage::_CreateNewTabFlyoutItems(IVector entries) - { - std::vector items; - - if (entries == nullptr || entries.Size() == 0) - { - return items; - } - - for (const auto& entry : entries) - { - if (entry == nullptr) - { - continue; - } - - switch (entry.Type()) - { - case NewTabMenuEntryType::Separator: - { - items.push_back(WUX::Controls::MenuFlyoutSeparator{}); - break; - } - // A folder has a custom name and icon, and has a number of entries that require - // us to call this method recursively. - case NewTabMenuEntryType::Folder: - { - const auto folderEntry = entry.as(); - const auto folderEntries = folderEntry.Entries(); - - // If the folder is empty, we should skip the entry if AllowEmpty is false, or - // when the folder should inline. - // The IsEmpty check includes semantics for nested (empty) folders - if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) - { - break; - } - - // Recursively generate flyout items - auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries); - - // If the folder should auto-inline and there is only one item, do so. - if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1) - { - for (auto const& folderEntryItem : folderEntryItems) - { - items.push_back(folderEntryItem); - } - - break; - } - - // Otherwise, create a flyout - auto folderItem = WUX::Controls::MenuFlyoutSubItem{}; - folderItem.Text(folderEntry.Name()); - - auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon().Resolved()); - folderItem.Icon(icon); - - for (const auto& folderEntryItem : folderEntryItems) - { - folderItem.Items().Append(folderEntryItem); - } - - // If the folder is empty, and by now we know we set AllowEmpty to true, - // create a placeholder item here - if (folderEntries.Size() == 0) - { - auto placeholder = WUX::Controls::MenuFlyoutItem{}; - placeholder.Text(RS_(L"NewTabMenuFolderEmpty")); - placeholder.IsEnabled(false); - - folderItem.Items().Append(placeholder); - } - - items.push_back(folderItem); - break; - } - // Any "collection entry" will simply make us add each profile in the collection - // separately. This collection is stored as a map , so the correct - // profile index is already known. - case NewTabMenuEntryType::RemainingProfiles: - case NewTabMenuEntryType::MatchProfiles: - { - const auto remainingProfilesEntry = entry.as(); - if (remainingProfilesEntry.Profiles() == nullptr) - { - break; - } - - for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles()) - { - items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex, {})); - } - - break; - } - // A single profile, the profile index is also given in the entry - case NewTabMenuEntryType::Profile: - { - const auto profileEntry = entry.as(); - if (profileEntry.Profile() == nullptr) - { - break; - } - - auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex(), profileEntry.Icon().Resolved()); - items.push_back(profileItem); - break; - } - case NewTabMenuEntryType::Action: - { - const auto actionEntry = entry.as(); - const auto actionId = actionEntry.ActionId(); - if (_settings.ActionMap().GetActionByID(actionId)) - { - auto actionItem = _CreateNewTabFlyoutAction(actionId, actionEntry.Icon().Resolved()); - items.push_back(actionItem); - } - - break; - } - } - } - - return items; - } - - // Method Description: - // - This method creates a flyout menu item for a given profile with the given index. - // It makes sure to set the correct icon, keybinding, and click-action. - WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex, const winrt::hstring& iconPathOverride) - { - auto profileMenuItem = WUX::Controls::MenuFlyoutItem{}; - - // Add the keyboard shortcuts based on the number of profiles defined - // Look for a keychord that is bound to the equivalent - // NewTab(ProfileIndex=N) action - NewTerminalArgs newTerminalArgs{ profileIndex }; - NewTabArgs newTabArgs{ newTerminalArgs }; - const auto id = fmt::format(FMT_COMPILE(L"Terminal.OpenNewTabProfile{}"), profileIndex); - const auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(id) }; - - // make sure we find one to display - if (profileKeyChord) - { - _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord); - } - - auto profileName = profile.Name(); - profileMenuItem.Text(profileName); - - // If a custom icon path has been specified, set it as the icon for - // this flyout item. Otherwise, if an icon is set for this profile, set that icon - // for this flyout item. - const auto& iconPath = iconPathOverride.empty() ? profile.Icon().Resolved() : iconPathOverride; - if (!iconPath.empty()) - { - const auto icon = _CreateNewTabFlyoutIcon(iconPath); - profileMenuItem.Icon(icon); - } - - if (profile.Guid() == _settings.GlobalSettings().DefaultProfile()) - { - // Contrast the default profile with others in font weight. - profileMenuItem.FontWeight(FontWeights::Bold()); - } - - auto newTabRun = WUX::Documents::Run(); - newTabRun.Text(RS_(L"NewTabRun/Text")); - auto newPaneRun = WUX::Documents::Run(); - newPaneRun.Text(RS_(L"NewPaneRun/Text")); - newPaneRun.FontStyle(FontStyle::Italic); - auto newWindowRun = WUX::Documents::Run(); - newWindowRun.Text(RS_(L"NewWindowRun/Text")); - newWindowRun.FontStyle(FontStyle::Italic); - auto elevatedRun = WUX::Documents::Run(); - elevatedRun.Text(RS_(L"ElevatedRun/Text")); - elevatedRun.FontStyle(FontStyle::Italic); - - auto textBlock = WUX::Controls::TextBlock{}; - textBlock.Inlines().Append(newTabRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(newPaneRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(newWindowRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(elevatedRun); - - auto toolTip = WUX::Controls::ToolTip{}; - toolTip.Content(textBlock); - WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip); - - profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Profile", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - NewTerminalArgs newTerminalArgs{ profileIndex }; - page->_OpenNewTerminalViaDropdown(newTerminalArgs); - } - }); - - // Using the static method on the base class seems to do what we want in terms of placement. - WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(profileMenuItem, _CreateRunAsAdminFlyout(profileIndex)); - - // Since we are not setting the ContextFlyout property of the item we have to handle the ContextRequested event - // and rely on the base class to show our menu. - profileMenuItem.ContextRequested([profileMenuItem](auto&&, auto&&) { - WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(profileMenuItem); - }); - - return profileMenuItem; - } - - // Method Description: - // - This method creates a flyout menu item for a given action - // It makes sure to set the correct icon, keybinding, and click-action. - WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride) - { - auto actionMenuItem = WUX::Controls::MenuFlyoutItem{}; - const auto action{ _settings.ActionMap().GetActionByID(actionId) }; - const auto actionKeyChord{ _settings.ActionMap().GetKeyBindingForAction(actionId) }; - - if (actionKeyChord) - { - _SetAcceleratorForMenuItem(actionMenuItem, actionKeyChord); - } - - actionMenuItem.Text(action.Name()); - - // If a custom icon path has been specified, set it as the icon for - // this flyout item. Otherwise, if an icon is set for this action, set that icon - // for this flyout item. - const auto& iconPath = iconPathOverride.empty() ? action.Icon().Resolved() : iconPathOverride; - if (!iconPath.empty()) - { - const auto icon = _CreateNewTabFlyoutIcon(iconPath); - actionMenuItem.Icon(icon); - } - - actionMenuItem.Click([action, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Action", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - page->_actionDispatch->DoAction(action.ActionAndArgs()); - } - }); - - return actionMenuItem; - } - - // Method Description: - // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and - // MenuFlyoutSubItems - IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource) - { - if (iconSource.empty()) - { - return nullptr; - } - - auto icon = UI::IconPathConverter::IconWUX(iconSource); - Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw); - - return icon; - } - - // Function Description: - // Called when the openNewTabDropdown keybinding is used. - // Shows the dropdown flyout. - void TerminalPage::_OpenNewTabDropdown() - { - _newTabButton.Flyout().ShowAt(_newTabButton); - } - - void TerminalPage::_OpenNewTerminalViaDropdown(const NewTerminalArgs newTerminalArgs) - { - // if alt is pressed, open a pane - const auto window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); - const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - const auto ctrlState{ window.GetKeyState(VirtualKey::Control) }; - const auto rCtrlState = window.GetKeyState(VirtualKey::RightControl); - const auto lCtrlState = window.GetKeyState(VirtualKey::LeftControl); - const auto ctrlPressed{ WI_IsFlagSet(ctrlState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rCtrlState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lCtrlState, CoreVirtualKeyStates::Down) }; - - // Check for DebugTap - auto debugTap = this->_settings.GlobalSettings().DebugFeaturesEnabled() && - WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto dispatchToElevatedWindow = ctrlPressed && !IsRunningElevated(); - - auto sessionType = ""; - if ((shiftPressed || dispatchToElevatedWindow) && !debugTap) - { - // Manually fill in the evaluated profile. - if (newTerminalArgs.ProfileIndex() != nullptr) - { - // We want to promote the index to a GUID because there is no "launch to profile index" command. - const auto profile = _settings.GetProfileForArgs(newTerminalArgs); - if (profile) - { - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); - newTerminalArgs.StartingDirectory(_evaluatePathForCwd(profile.EvaluatedStartingDirectory())); - } - } - - if (dispatchToElevatedWindow) - { - _OpenElevatedWT(newTerminalArgs); - sessionType = "ElevatedWindow"; - } - else - { - _OpenNewWindow(newTerminalArgs); - sessionType = "Window"; - } - } - else - { - const auto newPane = _MakePane(newTerminalArgs); - // If the newTerminalArgs caused us to open an elevated window - // instead of creating a pane, it may have returned nullptr. Just do - // nothing then. - if (!newPane) - { - return; - } - if (altPressed && !debugTap) - { - this->_SplitPane(_GetFocusedTabImpl(), - SplitDirection::Automatic, - 0.5f, - newPane); - sessionType = "Pane"; - } - else - { - _CreateNewTabFromPane(newPane); - sessionType = "Tab"; - } - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuCreatedNewTerminalSession", - TraceLoggingDescription("Event emitted when a new terminal was created via the new tab menu"), - TraceLoggingValue(NumberOfTabs(), "NewTabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue(sessionType, "SessionType", "The type of session that was created"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - std::wstring TerminalPage::_evaluatePathForCwd(const std::wstring_view path) - { - return Utils::EvaluateStartingDirectory(_WindowProperties.VirtualWorkingDirectory(), path); - } - - // Method Description: - // - Creates a new connection based on the profile settings - // Arguments: - // - the profile we want the settings from - // - the terminal settings - // Return value: - // - the desired connection - TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile, - IControlSettings settings, - const bool inheritCursor) - { - static const auto textMeasurement = [&]() -> std::wstring_view { - switch (_settings.GlobalSettings().TextMeasurement()) - { - case TextMeasurement::Graphemes: - return L"graphemes"; - case TextMeasurement::Wcswidth: - return L"wcswidth"; - case TextMeasurement::Console: - return L"console"; - default: - return {}; - } - }(); - static const auto ambiguousIsWide = [&]() -> bool { - return _settings.GlobalSettings().AmbiguousWidth() == AmbiguousWidth::Wide; - }(); - - TerminalConnection::ITerminalConnection connection{ nullptr }; - - auto connectionType = profile.ConnectionType(); - Windows::Foundation::Collections::ValueSet valueSet; - - if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && - TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) - { - connection = TerminalConnection::AzureConnection{}; - valueSet = TerminalConnection::ConptyConnection::CreateSettings(winrt::hstring{}, - L".", - L"Azure", - false, - L"", - nullptr, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid(), - profile.Guid()); - } - - else - { - auto settingsInternal{ winrt::get_self(settings) }; - const auto environment = settingsInternal->EnvironmentVariables(); - - // Update the path to be relative to whatever our CWD is. - // - // Refer to the examples in - // https://en.cppreference.com/w/cpp/filesystem/path/append - // - // We need to do this here, to ensure we tell the ConptyConnection - // the correct starting path. If we're being invoked from another - // terminal instance (e.g. `wt -w 0 -d .`), then we have switched our - // CWD to the provided path. We should treat the StartingDirectory - // as relative to the current CWD. - // - // The connection must be informed of the current CWD on - // construction, because the connection might not spawn the child - // process until later, on another thread, after we've already - // restored the CWD to its original value. - auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) }; - connection = TerminalConnection::ConptyConnection{}; - valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), - newWorkingDirectory, - settings.StartingTitle(), - settingsInternal->ReloadEnvironmentVariables(), - _WindowProperties.VirtualEnvVars(), - environment, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid(), - profile.Guid()); - - if (inheritCursor) - { - valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true)); - } - } - - if (!textMeasurement.empty()) - { - valueSet.Insert(L"textMeasurement", Windows::Foundation::PropertyValue::CreateString(textMeasurement)); - } - if (ambiguousIsWide) - { - valueSet.Insert(L"ambiguousIsWide", Windows::Foundation::PropertyValue::CreateBoolean(true)); - } - - if (const auto id = settings.SessionId(); id != winrt::guid{}) - { - valueSet.Insert(L"sessionId", Windows::Foundation::PropertyValue::CreateGuid(id)); - } - - connection.Initialize(valueSet); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "ConnectionCreated", - TraceLoggingDescription("Event emitted upon the creation of a connection"), - TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"), - TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"), - TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - return connection; - } - - TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent) - { - if (paneContent == nullptr) - { - return nullptr; - } - - const auto& control{ paneContent.GetTermControl() }; - if (control == nullptr) - { - return nullptr; - } - const auto& connection = control.Connection(); - auto profile{ paneContent.GetProfile() }; - - Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; - - if (profile) - { - // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. - profile = GetClosestProfileForDuplicationOfProfile(profile); - controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); - - // Replace the Starting directory with the CWD, if given - const auto workingDirectory = control.WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) - { - controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); - } - - // To facilitate restarting defterm connections: grab the original - // commandline out of the connection and shove that back into the - // settings. - if (const auto& conpty{ connection.try_as() }) - { - controlSettings.DefaultSettings()->Commandline(conpty.Commandline()); - } - } - - return _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), true); - } - - // Method Description: - // - Called when the settings button is clicked. Launches a background - // thread to open the settings file in the default JSON editor. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_SettingsButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - const auto window = CoreWindow::GetForCurrentThread(); - - // check alt state - const auto rAltState{ window.GetKeyState(VirtualKey::RightMenu) }; - const auto lAltState{ window.GetKeyState(VirtualKey::LeftMenu) }; - const auto altPressed{ WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down) }; - - // check shift state - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto lShiftState{ window.GetKeyState(VirtualKey::LeftShift) }; - const auto rShiftState{ window.GetKeyState(VirtualKey::RightShift) }; - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - auto target{ SettingsTarget::SettingsUI }; - if (shiftPressed) - { - target = SettingsTarget::SettingsFile; - } - else if (altPressed) - { - target = SettingsTarget::DefaultsFile; - } - - const auto targetAsString = [&target]() { - switch (target) - { - case SettingsTarget::SettingsFile: - return "SettingsFile"; - case SettingsTarget::DefaultsFile: - return "DefaultsFile"; - case SettingsTarget::SettingsUI: - default: - return "UI"; - } - }(); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Settings", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingValue(targetAsString, "SettingsTarget", "The target settings file or UI"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - _LaunchSettings(target); - } - - // Method Description: - // - Called when the command palette button is clicked. Opens the command palette. - void TerminalPage::_CommandPaletteButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - auto p = LoadCommandPalette(); - p.EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); - p.Visibility(Visibility::Visible); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("CommandPalette", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - // Method Description: - // - Called when the about button is clicked. See _ShowAboutDialog for more info. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_AboutButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - _ShowAboutDialog(); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("About", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - // Method Description: - // - Called when the users pressed keyBindings while CommandPaletteElement is open. - // - As of GH#8480, this is also bound to the TabRowControl's KeyUp event. - // That should only fire when focus is in the tab row, which is hard to - // do. Notably, that's possible: - // - When you have enough tabs to make the little scroll arrows appear, - // click one, then hit tab - // - When Narrator is in Scan mode (which is the a11y bug we're fixing here) - // - This method is effectively an extract of TermControl::_KeyHandler and TermControl::_TryHandleKeyBinding. - // Arguments: - // - e: the KeyRoutedEventArgs containing info about the keystroke. - // Return Value: - // - - void TerminalPage::_KeyDownHandler(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto keyStatus = e.KeyStatus(); - const auto vkey = gsl::narrow_cast(e.OriginalKey()); - const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); - const auto modifiers = _GetPressedModifierKeys(); - - // GH#11076: - // For some weird reason we sometimes receive a WM_KEYDOWN - // message without vkey or scanCode if a user drags a tab. - // The KeyChord constructor has a debug assertion ensuring that all KeyChord - // either have a valid vkey/scanCode. This is important, because this prevents - // accidental insertion of invalid KeyChords into classes like ActionMap. - if (!vkey && !scanCode) - { - return; - } - - // Alt-Numpad# input will send us a character once the user releases - // Alt, so we should be ignoring the individual keydowns. The character - // will be sent through the TSFInputControl. See GH#1401 for more - // details - if (modifiers.IsAltPressed() && (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) - { - return; - } - - // GH#2235: Terminal::Settings hasn't been modified to differentiate - // between AltGr and Ctrl+Alt yet. - // -> Don't check for key bindings if this is an AltGr key combination. - if (modifiers.IsAltGrPressed()) - { - return; - } - - const auto actionMap = _settings.ActionMap(); - if (!actionMap) - { - return; - } - - const auto cmd = actionMap.GetActionByKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - modifiers.IsWinPressed(), - vkey, - scanCode, - }); - if (!cmd) - { - return; - } - - if (!_actionDispatch->DoAction(cmd.ActionAndArgs())) - { - return; - } - - if (_commandPaletteIs(Visibility::Visible) && - cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) - { - CommandPaletteElement().Visibility(Visibility::Collapsed); - } - if (_suggestionsControlIs(Visibility::Visible) && - cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) - { - SuggestionsElement().Visibility(Visibility::Collapsed); - } - - // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". - // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. - // The following is used to manually "consume" such dead keys and clear them from the keyboard state. - _ClearKeyboardState(vkey, scanCode); - e.Handled(true); - } - - bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) - { - const auto modifiers = _GetPressedModifierKeys(); - if (vkey == VK_SPACE && modifiers.IsAltPressed() && down) - { - if (const auto actionMap = _settings.ActionMap()) - { - if (const auto cmd = actionMap.GetActionByKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - modifiers.IsWinPressed(), - gsl::narrow_cast(vkey), - scanCode, - })) - { - return _actionDispatch->DoAction(cmd.ActionAndArgs()); - } - } - } - return false; - } - - // Method Description: - // - Get the modifier keys that are currently pressed. This can be used to - // find out which modifiers (ctrl, alt, shift) are pressed in events that - // don't necessarily include that state. - // - This is a copy of TermControl::_GetPressedModifierKeys. - // Return Value: - // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. - ControlKeyStates TerminalPage::_GetPressedModifierKeys() noexcept - { - const auto window = CoreWindow::GetForCurrentThread(); - // DONT USE - // != CoreVirtualKeyStates::None - // OR - // == CoreVirtualKeyStates::Down - // Sometimes with the key down, the state is Down | Locked. - // Sometimes with the key up, the state is Locked. - // IsFlagSet(Down) is the only correct solution. - - struct KeyModifier - { - VirtualKey vkey; - ControlKeyStates flags; - }; - - constexpr std::array modifiers{ { - { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, - { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, - { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, - { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, - { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, - { VirtualKey::RightWindows, ControlKeyStates::RightWinPressed }, - { VirtualKey::LeftWindows, ControlKeyStates::LeftWinPressed }, - } }; - - ControlKeyStates flags; - - for (const auto& mod : modifiers) - { - const auto state = window.GetKeyState(mod.vkey); - const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); - - if (isDown) - { - flags |= mod.flags; - } - } - - return flags; - } - - // Method Description: - // - Discards currently pressed dead keys. - // - This is a copy of TermControl::_ClearKeyboardState. - // Arguments: - // - vkey: The vkey of the key pressed. - // - scanCode: The scan code of the key pressed. - void TerminalPage::_ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept - { - std::array keyState; - if (!GetKeyboardState(keyState.data())) - { - return; - } - - // As described in "Sometimes you *want* to interfere with the keyboard's state buffer": - // http://archives.miloush.net/michkap/archive/2006/09/10/748775.html - // > "The key here is to keep trying to pass stuff to ToUnicode until -1 is not returned." - std::array buffer; - while (ToUnicodeEx(vkey, scanCode, keyState.data(), buffer.data(), gsl::narrow_cast(buffer.size()), 0b1, nullptr) < 0) - { - } - } - - // Method Description: - // - Configure the AppKeyBindings to use our ShortcutActionDispatch and the updated ActionMap - // as the object to handle dispatching ShortcutAction events. - // Arguments: - // - bindings: An IActionMapView object to wire up with our event handlers - void TerminalPage::_HookupKeyBindings(const IActionMapView& actionMap) noexcept - { - _bindings->SetDispatch(*_actionDispatch); - _bindings->SetActionMap(actionMap); - } - - // Method Description: - // - Register our event handlers with our ShortcutActionDispatch. The - // ShortcutActionDispatch is responsible for raising the appropriate - // events for an ActionAndArgs. WE'll handle each possible event in our - // own way. - // Arguments: - // - - void TerminalPage::_RegisterActionCallbacks() - { - // Hook up the ShortcutActionDispatch object's events to our handlers. - // They should all be hooked up here, regardless of whether or not - // there's an actual keychord for them. -#define ON_ALL_ACTIONS(action) HOOKUP_ACTION(action); - ALL_SHORTCUT_ACTIONS - INTERNAL_SHORTCUT_ACTIONS -#undef ON_ALL_ACTIONS - } - - // Method Description: - // - 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(const Tab& tab) - { - if (tab == _GetFocusedTab()) - { - TitleChanged.raise(*this, nullptr); - } - } - - // Method Description: - // - Connects event handlers to the TermControl for events that we want to - // handle. This includes: - // * the Copy and Paste events, for setting and retrieving clipboard data - // on the right thread - // Arguments: - // - term: The newly created TermControl to connect the events for - void TerminalPage::_RegisterTerminalEvents(TermControl term) - { - term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); - - term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); - term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler }); - - term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); - - // Add an event handler for when the terminal or tab wants to set a - // progress indicator on the taskbar - term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - - term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler }); - - term.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& e) { - if (auto page{ weakThis.get() }) - { - if (e.PropertyName() == L"BackgroundBrush") - { - page->_updateThemeColors(); - } - } - }); - - term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); - term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); - term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); - - // Don't even register for the event if the feature is compiled off. - if constexpr (Feature_ShellCompletions::IsEnabled()) - { - term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); - } - winrt::weak_ref weakTerm{ term }; - term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), false); - } - }); - term.SelectionContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); - } - }); - if constexpr (Feature_QuickFix::IsEnabled()) - { - term.QuickFixMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateQuickFixMenu(weakTerm.get(), sender.try_as()); - } - }); - } - } - - // Method Description: - // - Connects event handlers to the Tab for events that we want to - // handle. This includes: - // * the TitleChanged event, for changing the text of the tab - // * the Color{Selected,Cleared} events to change the color of a tab. - // Arguments: - // - hostingTab: The Tab that's hosting this TermControl instance - void TerminalPage::_RegisterTabEvents(Tab& hostingTab) - { - auto weakTab{ hostingTab.get_weak() }; - auto weakThis{ get_weak() }; - // PropertyChanged is the generic mechanism by which the Tab - // communicates changes to any of its observable properties, including - // the Title - hostingTab.PropertyChanged([weakTab, weakThis](auto&&, const WUX::Data::PropertyChangedEventArgs& args) { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - if (page && tab) - { - const auto propertyName = args.PropertyName(); - if (propertyName == L"Title") - { - page->_UpdateTitle(*tab); - } - else if (propertyName == L"Content") - { - if (*tab == page->_GetFocusedTab()) - { - const auto children = page->_tabContent.Children(); - - children.Clear(); - if (auto content = tab->Content()) - { - page->_tabContent.Children().Append(std::move(content)); - } - - tab->Focus(FocusState::Programmatic); - } - } - } - }); - - // Add an event handler for when the terminal or tab wants to set a - // progress indicator on the taskbar - hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - - hostingTab.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); - } - - // Method Description: - // - Helper to manually exit "zoom" when certain actions take place. - // Anything that modifies the state of the pane tree should probably - // un-zoom the focused pane first, so that the user can see the full pane - // tree again. These actions include: - // * Splitting a new pane - // * Closing a pane - // * Moving focus between panes - // * Resizing a pane - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_UnZoomIfNeeded() - { - if (const auto activeTab{ _GetFocusedTabImpl() }) - { - if (activeTab->IsZoomed()) - { - // Remove the content from the tab first, so Pane::UnZoom can - // re-attach the content to the tree w/in the pane - _tabContent.Children().Clear(); - // In ExitZoom, we'll change the Tab's Content(), triggering the - // content changed event, which will re-attach the tab's new content - // root to the tree. - activeTab->ExitZoom(); - } - } - } - - // Method Description: - // - Attempt to move focus between panes, as to focus the child on - // the other side of the separator. See Pane::NavigateFocus for details. - // - Moves the focus of the currently focused tab. - // Arguments: - // - direction: The direction to move the focus in. - // Return Value: - // - Whether changing the focus succeeded. This allows a keychord to propagate - // to the terminal when no other panes are present (GH#6219) - bool TerminalPage::_MoveFocus(const FocusDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->NavigateFocus(direction); - } - return false; - } - - // Method Description: - // - Attempt to swap the positions of the focused pane with another pane. - // See Pane::SwapPane for details. - // Arguments: - // - direction: The direction to move the focused pane in. - // Return Value: - // - true if panes were swapped. - bool TerminalPage::_SwapPane(const FocusDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - return tabImpl->SwapPane(direction); - } - return false; - } - - TermControl TerminalPage::_GetActiveControl() const - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->GetActiveTerminalControl(); - } - return nullptr; - } - - CommandPalette TerminalPage::LoadCommandPalette() - { - if (const auto p = CommandPaletteElement()) - { - return p; - } - - return _loadCommandPaletteSlowPath(); - } - bool TerminalPage::_commandPaletteIs(WUX::Visibility visibility) - { - const auto p = CommandPaletteElement(); - return p && p.Visibility() == visibility; - } - - CommandPalette TerminalPage::_loadCommandPaletteSlowPath() - { - const auto p = FindName(L"CommandPaletteElement").as(); - - p.SetActionMap(_settings.ActionMap()); - - // When the visibility of the command palette changes to "collapsed", - // the palette has been closed. Toss focus back to the currently active control. - p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - if (_commandPaletteIs(Visibility::Collapsed)) - { - _FocusActiveControl(nullptr, nullptr); - } - }); - p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - p.CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); - p.SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); - p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); - - return p; - } - - SuggestionsControl TerminalPage::LoadSuggestionsUI() - { - if (const auto p = SuggestionsElement()) - { - return p; - } - - return _loadSuggestionsElementSlowPath(); - } - bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) - { - const auto p = SuggestionsElement(); - return p && p.Visibility() == visibility; - } - - SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() - { - const auto p = FindName(L"SuggestionsElement").as(); - - p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - if (SuggestionsElement().Visibility() == Visibility::Collapsed) - { - _FocusActiveControl(nullptr, nullptr); - } - }); - p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); - - return p; - } - - // Method Description: - // - Warn the user that they are about to close all open windows, then - // signal that we want to close everything. - safe_void_coroutine TerminalPage::RequestQuit() - { - if (!_displayingCloseDialog) - { - _displayingCloseDialog = true; - - const auto weak = get_weak(); - auto warningResult = co_await _ShowQuitDialog(); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - _displayingCloseDialog = false; - - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - - QuitRequested.raise(nullptr, nullptr); - } - } - - void TerminalPage::PersistState() - { - // This method may be called for a window even if it hasn't had a tab yet or lost all of them. - // We shouldn't persist such windows. - const auto tabCount = _tabs.Size(); - if (_startupState != StartupState::Initialized || tabCount == 0) - { - return; - } - - std::vector actions; - - for (auto tab : _tabs) - { - auto t = winrt::get_self(tab); - auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist); - actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); - } - - // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. - if (actions.empty()) - { - return; - } - - // if the focused tab was not the last tab, restore that - auto idx = _GetFocusedTabIndex(); - if (idx && idx != tabCount - 1) - { - ActionAndArgs action; - action.Action(ShortcutAction::SwitchToTab); - SwitchToTabArgs switchToTabArgs{ idx.value() }; - action.Args(switchToTabArgs); - - actions.emplace_back(std::move(action)); - } - - // If the user set a custom name, save it - if (const auto& windowName{ _WindowProperties.WindowName() }; !windowName.empty()) - { - ActionAndArgs action; - action.Action(ShortcutAction::RenameWindow); - RenameWindowArgs args{ windowName }; - action.Args(args); - - actions.emplace_back(std::move(action)); - } - - WindowLayout layout; - layout.TabLayout(winrt::single_threaded_vector(std::move(actions))); - - auto mode = LaunchMode::DefaultMode; - WI_SetFlagIf(mode, LaunchMode::FullscreenMode, _isFullscreen); - WI_SetFlagIf(mode, LaunchMode::FocusMode, _isInFocusMode); - WI_SetFlagIf(mode, LaunchMode::MaximizedMode, _isMaximized); - - layout.LaunchMode({ mode }); - - // Only save the content size because the tab size will be added on load. - const auto contentWidth = static_cast(_tabContent.ActualWidth()); - const auto contentHeight = static_cast(_tabContent.ActualHeight()); - const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; - - layout.InitialSize(windowSize); - - // We don't actually know our own position. So we have to ask the window - // layer for that. - const auto launchPosRequest{ winrt::make() }; - RequestLaunchPosition.raise(*this, launchPosRequest); - layout.InitialPosition(launchPosRequest.Position()); - - ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); - } - - // Method Description: - // - Close the terminal app. If there is more - // than one tab opened, show a warning dialog. - safe_void_coroutine TerminalPage::CloseWindow() - { - if (_HasMultipleTabs() && - _settings.GlobalSettings().ConfirmCloseAllTabs() && - !_displayingCloseDialog) - { - if (_newTabButton && _newTabButton.Flyout()) - { - _newTabButton.Flyout().Hide(); - } - _DismissTabContextMenus(); - _displayingCloseDialog = true; - auto warningResult = co_await _ShowCloseWarningDialog(); - _displayingCloseDialog = false; - - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - } - - CloseWindowRequested.raise(*this, nullptr); - } - - std::vector TerminalPage::Panes() const - { - std::vector panes; - - for (const auto tab : _tabs) - { - const auto impl = _GetTabImpl(tab); - if (!impl) - { - continue; - } - - impl->GetRootPane()->WalkTree([&](auto&& pane) { - if (auto content = pane->GetContent()) - { - panes.push_back(std::move(content)); - } - }); - } - - return panes; - } - - // Method Description: - // - Move the viewport of the terminal of the currently focused tab up or - // down a number of lines. - // Arguments: - // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down - // - rowsToScroll: a number of lines to move the viewport. If not provided we will use a system default. - void TerminalPage::_Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - uint32_t realRowsToScroll; - if (rowsToScroll == nullptr) - { - // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page - realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? - tabImpl->GetActiveTerminalControl().ViewHeight() : - _systemRowsToScroll; - } - else - { - // use the custom value specified in the command - realRowsToScroll = rowsToScroll.Value(); - } - auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); - tabImpl->Scroll(scrollDelta); - } - } - - // Method Description: - // - Moves the currently active pane on the currently active tab to the - // specified tab. If the tab index is greater than the number of - // tabs, then a new tab will be created for the pane. Similarly, if a pane - // is the last remaining pane on a tab, that tab will be closed upon moving. - // - No move will occur if the tabIdx is the same as the current tab, or if - // the specified tab is not a host of terminals (such as the settings tab). - // - If the Window is specified, the pane will instead be detached and moved - // to the window with the given name/id. - // Return Value: - // - true if the pane was successfully moved to the new tab. - bool TerminalPage::_MovePane(MovePaneArgs args) - { - const auto tabIdx{ args.TabIndex() }; - const auto windowId{ args.Window() }; - - auto focusedTab{ _GetFocusedTabImpl() }; - - if (!focusedTab) - { - return false; - } - - // If there was a windowId in the action, try to move it to the - // specified window instead of moving it in our tab row. - if (!windowId.empty()) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto pane{ tabImpl->GetActivePane() }) - { - auto startupActions = pane->BuildStartupActions(0, 1, BuildStartupKind::MovePane); - _DetachPaneFromWindow(pane); - _MoveContent(std::move(startupActions.args), windowId, tabIdx); - focusedTab->DetachPane(); - - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - if (windowId == L"new") - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_(L"TerminalPage_PaneMovedAnnouncement_NewWindow"), - L"TerminalPageMovePaneToNewWindow" /* unique name for this notification category */); - } - else - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow2", windowId), - L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); - } - } - return true; - } - } - } - - // If we are trying to move from the current tab to the current tab do nothing. - if (_GetFocusedTabIndex() == tabIdx) - { - return false; - } - - // Moving the pane from the current tab might close it, so get the next - // tab before its index changes. - if (tabIdx < _tabs.Size()) - { - auto targetTab = _GetTabImpl(_tabs.GetAt(tabIdx)); - // if the selected tab is not a host of terminals (e.g. settings) - // don't attempt to add a pane to it. - if (!targetTab) - { - return false; - } - auto pane = focusedTab->DetachPane(); - targetTab->AttachPane(pane); - _SetFocusedTab(*targetTab); - - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - const auto tabTitle = targetTab->Title(); - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingTab", tabTitle), - L"TerminalPageMovePaneToExistingTab" /* unique name for this notification category */); - } - } - else - { - auto pane = focusedTab->DetachPane(); - _CreateNewTabFromPane(pane); - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_(L"TerminalPage_PaneMovedAnnouncement_NewTab"), - L"TerminalPageMovePaneToNewTab" /* unique name for this notification category */); - } - } - - return true; - } - - // Detach a tree of panes from this terminal. Helper used for moving panes - // and tabs to other windows. - void TerminalPage::_DetachPaneFromWindow(std::shared_ptr pane) - { - pane->WalkTree([&](auto p) { - if (const auto& control{ p->GetTerminalControl() }) - { - _manager.Detach(control); - } - }); - } - - void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr& tab) - { - // Detach the root pane, which will act like the whole tab got detached. - if (const auto rootPane = tab->GetRootPane()) - { - _DetachPaneFromWindow(rootPane); - } - } - - // Method Description: - // - Serialize these actions to json, and raise them as a RequestMoveContent - // event. Our Window will raise that to the window manager / monarch, who - // will dispatch this blob of json back to the window that should handle - // this. - // - `actions` will be emptied into a winrt IVector as a part of this method - // and should be expected to be empty after this call. - void TerminalPage::_MoveContent(std::vector&& actions, - const winrt::hstring& windowName, - const uint32_t tabIndex, - const std::optional& dragPoint) - { - const auto winRtActions{ winrt::single_threaded_vector(std::move(actions)) }; - const auto str{ ActionAndArgs::Serialize(winRtActions) }; - const auto request = winrt::make_self(windowName, - str, - tabIndex); - if (dragPoint.has_value()) - { - request->WindowPosition(*dragPoint); - } - RequestMoveContent.raise(*this, *request); - } - - bool TerminalPage::_MoveTab(winrt::com_ptr tab, MoveTabArgs args) - { - if (!tab) - { - return false; - } - - // If there was a windowId in the action, try to move it to the - // specified window instead of moving it in our tab row. - const auto windowId{ args.Window() }; - if (!windowId.empty()) - { - // if the windowId is the same as our name, do nothing - if (windowId == WindowProperties().WindowName() || - windowId == winrt::to_hstring(WindowProperties().WindowId())) - { - return true; - } - - if (tab) - { - auto startupActions = tab->BuildStartupActions(BuildStartupKind::Content); - _DetachTabFromWindow(tab); - _MoveContent(std::move(startupActions), windowId, 0); - _RemoveTab(*tab); - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - const auto tabTitle = tab->Title(); - if (windowId == L"new") - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_TabMovedAnnouncement_NewWindow", tabTitle), - L"TerminalPageMoveTabToNewWindow" /* unique name for this notification category */); - } - else - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_TabMovedAnnouncement_Default", tabTitle, windowId), - L"TerminalPageMoveTabToExistingWindow" /* unique name for this notification category */); - } - } - return true; - } - } - - const auto direction = args.Direction(); - if (direction != MoveTabDirection::None) - { - // Use the requested tab, if provided. Otherwise, use the currently - // focused tab. - const auto tabIndex = til::coalesce(_GetTabIndex(*tab), - _GetFocusedTabIndex()); - if (tabIndex) - { - const auto currentTabIndex = tabIndex.value(); - const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; - _TryMoveTab(currentTabIndex, currentTabIndex + delta); - } - } - - return true; - } - - // 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. - void TerminalPage::_activePaneChanged(winrt::TerminalApp::Tab sender, - Windows::Foundation::IInspectable /*args*/) - { - if (const auto tab{ _GetTabImpl(sender) }) - { - // Possibly update the icon of the tab. - _UpdateTabIcon(*tab); - - _updateThemeColors(); - - // Update the taskbar progress as well. We'll raise our own - // SetTaskbarProgress event here, to get tell the hosting - // application to re-query this value from us. - SetTaskbarProgress.raise(*this, nullptr); - - auto profile = tab->GetFocusedProfile(); - _UpdateBackground(profile); - } - - _adjustProcessPriorityThrottled->Run(); - } - - uint32_t TerminalPage::NumberOfTabs() const - { - return _tabs.Size(); - } - - // Method Description: - // - Called when it is determined that an existing tab or pane should be - // attached to our window. content represents a blob of JSON describing - // some startup actions for rebuilding the specified panes. They will - // include `__content` properties with the GUID of the existing - // ControlInteractivity's we should use, rather than starting new ones. - // - _MakePane is already enlightened to use the ContentId property to - // reattach instead of create new content, so this method simply needs to - // parse the JSON and pump it into our action handler. Almost the same as - // doing something like `wt -w 0 nt`. - void TerminalPage::AttachContent(IVector args, uint32_t tabIndex) - { - if (args == nullptr || - args.Size() == 0) - { - return; - } - - std::vector existingTabs{}; - existingTabs.reserve(_tabs.Size()); - for (const auto& tab : _tabs) - { - existingTabs.emplace_back(tab); - } - - const auto& firstAction = args.GetAt(0); - const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; - - // `splitPane` allows the user to specify which tab to split. In that - // case, split specifically the requested pane. - // - // If there's not enough tabs, then just turn this pane into a new tab. - // - // If the first action is `newTab`, the index is always going to be 0, - // so don't do anything in that case. - if (firstIsSplitPane && tabIndex < _tabs.Size()) - { - _SelectTab(tabIndex); - } - - for (const auto& action : args) - { - _actionDispatch->DoAction(action); - } - - // After handling all the actions, then re-check the tabIndex. We might - // have been called as a part of a tab drag/drop. In that case, the - // tabIndex is actually relevant, and we need to move the tab we just - // made into position. - if (!firstIsSplitPane && tabIndex != -1) - { - const auto newTabs = _CollectNewTabs(existingTabs); - if (!newTabs.empty()) - { - _MoveTabsToIndex(newTabs, tabIndex); - _SetSelectedTabs(newTabs, newTabs.front()); - } - } - } - - // Method Description: - // - Split the focused pane of the given tab, either horizontally or vertically, and place the - // given pane accordingly - // Arguments: - // - tab: The tab that is going to be split. - // - newPane: the pane to add to our tree of panes - // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the - // new pane should be split from its parent. - // - splitSize: the size of the split - void TerminalPage::_SplitPane(const winrt::com_ptr& tab, - const SplitDirection splitDirection, - const float splitSize, - std::shared_ptr newPane) - { - auto activeTab = tab; - // Clever hack for a crash in startup, with multiple sub-commands. Say - // you have the following commandline: - // - // wtd nt -p "elevated cmd" ; sp -p "elevated cmd" ; sp -p "Command Prompt" - // - // Where "elevated cmd" is an elevated profile. - // - // In that scenario, we won't dump off the commandline immediately to an - // elevated window, because it's got the final unelevated split in it. - // However, when we get to that command, there won't be a tab yet. So - // we'd crash right about here. - // - // Instead, let's just promote this first split to be a tab instead. - // Crash avoided, and we don't need to worry about inserting a new-tab - // command in at the start. - if (!tab) - { - if (_tabs.Size() == 0) - { - _CreateNewTabFromPane(newPane); - return; - } - else - { - activeTab = _GetFocusedTabImpl(); - } - } - - // For now, prevent splitting the _settingsTab. We can always revisit this later. - if (*activeTab == _settingsTab) - { - return; - } - - // If the caller is calling us with the return value of _MakePane - // directly, it's possible that nullptr was returned, if the connections - // was supposed to be launched in an elevated window. In that case, do - // nothing here. We don't have a pane with which to create the split. - if (!newPane) - { - return; - } - const auto contentWidth = static_cast(_tabContent.ActualWidth()); - const auto contentHeight = static_cast(_tabContent.ActualHeight()); - const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; - - const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); - if (!realSplitType) - { - return; - } - - _UnZoomIfNeeded(); - auto [original, newGuy] = activeTab->SplitPane(*realSplitType, splitSize, newPane); - - // After GH#6586, the control will no longer focus itself - // automatically when it's finished being laid out. Manually focus - // the control here instead. - if (_startupState == StartupState::Initialized) - { - if (const auto& content{ newGuy->GetContent() }) - { - content.Focus(FocusState::Programmatic); - } - } - } - - // Method Description: - // - Switches the split orientation of the currently focused pane. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_ToggleSplitOrientation() - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - tabImpl->ToggleSplitOrientation(); - } - } - - // Method Description: - // - Attempt to move a separator between panes, as to resize each child on - // either size of the separator. See Pane::ResizePane for details. - // - Moves a separator on the currently focused tab. - // Arguments: - // - direction: The direction to move the separator in. - // Return Value: - // - - void TerminalPage::_ResizePane(const ResizeDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - tabImpl->ResizePane(direction); - } - } - - // Method Description: - // - Move the viewport of the terminal of the currently focused tab up or - // down a page. The page length will be dependent on the terminal view height. - // Arguments: - // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down - void TerminalPage::_ScrollPage(ScrollDirection scrollDirection) - { - // Do nothing if for some reason, there's no terminal tab in focus. We don't want to crash. - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto& control{ _GetActiveControl() }) - { - const auto termHeight = control.ViewHeight(); - auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); - tabImpl->Scroll(scrollDelta); - } - } - } - - void TerminalPage::_ScrollToBufferEdge(ScrollDirection scrollDirection) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - auto scrollDelta = _ComputeScrollDelta(scrollDirection, INT_MAX); - tabImpl->Scroll(scrollDelta); - } - } - - // Method Description: - // - Gets the title of the currently focused terminal control. If there - // isn't a control selected for any reason, returns "Terminal" - // Arguments: - // - - // Return Value: - // - the title of the focused control if there is one, else "Terminal" - hstring TerminalPage::Title() - { - if (_settings.GlobalSettings().ShowTitleInTitlebar()) - { - if (const auto tab{ _GetFocusedTab() }) - { - return tab.Title(); - } - } - return { L"Terminal" }; - } - - // Method Description: - // - Handles the special case of providing a text override for the UI shortcut due to VK_OEM issue. - // Looks at the flags from the KeyChord modifiers and provides a concatenated string value of all - // in the same order that XAML would put them as well. - // Return Value: - // - a string representation of the key modifiers for the shortcut - //NOTE: This needs to be localized with https://github.com/microsoft/terminal/issues/794 if XAML framework issue not resolved before then - static std::wstring _FormatOverrideShortcutText(VirtualKeyModifiers modifiers) - { - std::wstring buffer{ L"" }; - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)) - { - buffer += L"Ctrl+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)) - { - buffer += L"Shift+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)) - { - buffer += L"Alt+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)) - { - buffer += L"Win+"; - } - - return buffer; - } - - // Method Description: - // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. - // Takes into account a special case for an error condition for a comma - // Arguments: - // - MenuFlyoutItem that will be displayed, and a KeyChord to map an accelerator - void TerminalPage::_SetAcceleratorForMenuItem(WUX::Controls::MenuFlyoutItem& menuItem, - const KeyChord& keyChord) - { -#ifdef DEP_MICROSOFT_UI_XAML_708_FIXED - // work around https://github.com/microsoft/microsoft-ui-xaml/issues/708 in case of VK_OEM_COMMA - if (keyChord.Vkey() != VK_OEM_COMMA) - { - // use the XAML shortcut to give us the automatic capabilities - auto menuShortcut = Windows::UI::Xaml::Input::KeyboardAccelerator{}; - - // TODO: Modify this when https://github.com/microsoft/terminal/issues/877 is resolved - menuShortcut.Key(static_cast(keyChord.Vkey())); - - // add the modifiers to the shortcut - menuShortcut.Modifiers(keyChord.Modifiers()); - - // add to the menu - menuItem.KeyboardAccelerators().Append(menuShortcut); - } - else // we've got a comma, so need to just use the alternate method -#endif - { - // extract the modifier and key to a nice format - auto overrideString = _FormatOverrideShortcutText(keyChord.Modifiers()); - auto mappedCh = MapVirtualKeyW(keyChord.Vkey(), MAPVK_VK_TO_CHAR); - if (mappedCh != 0) - { - menuItem.KeyboardAcceleratorTextOverride(overrideString + gsl::narrow_cast(mappedCh)); - } - } - } - - // Method Description: - // - Calculates the appropriate size to snap to in the given direction, for - // the given dimension. If the global setting `snapToGridOnResize` is set - // to `false`, this will just immediately return the provided dimension, - // effectively disabling snapping. - // - See Pane::CalcSnappedDimension - float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const - { - if (_settings && _settings.GlobalSettings().SnapToGridOnResize()) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->CalcSnappedDimension(widthOrHeight, dimension); - } - } - return dimension; - } - - // Function Description: - // - This function is called when the `TermControl` requests that we send - // it the clipboard's content. - // - Retrieves the data from the Windows Clipboard and converts it to text. - // - Shows warnings if the clipboard is too big or contains multiple lines - // of text. - // - Sends the text back to the TermControl through the event's - // `HandleClipboardData` member function. - // - Does some of this in a background thread, as to not hang/crash the UI thread. - // Arguments: - // - eventArgs: the PasteFromClipboard event sent from the TermControl - safe_void_coroutine TerminalPage::_PasteFromClipboardHandler(const IInspectable sender, const PasteFromClipboardEventArgs eventArgs) - try - { - // The old Win32 clipboard API as used below is somewhere in the order of 300-1000x faster than - // the WinRT one on average, depending on CPU load. Don't use the WinRT clipboard API if you can. - const auto weakThis = get_weak(); - const auto dispatcher = Dispatcher(); - const auto globalSettings = _settings.GlobalSettings(); - const auto bracketedPaste = eventArgs.BracketedPasteEnabled(); - const auto sourceId = sender.try_as().Id(); - - // GetClipboardData might block for up to 30s for delay-rendered contents. - co_await winrt::resume_background(); - - winrt::hstring text; - if (const auto clipboard = clipboard::open(nullptr)) - { - text = clipboard::read(); - } - - if (!bracketedPaste && globalSettings.TrimPaste()) - { - text = winrt::hstring{ Utils::TrimPaste(text) }; - } - - if (text.empty()) - { - co_return; - } - - bool warnMultiLine = false; - switch (globalSettings.WarnAboutMultiLinePaste()) - { - case WarnAboutMultiLinePaste::Automatic: - // NOTE that this is unsafe, because a shell that doesn't support bracketed paste - // will allow an attacker to enable the mode, not realize that, and then accept - // the paste as if it was a series of legitimate commands. See GH#13014. - warnMultiLine = !bracketedPaste; - break; - case WarnAboutMultiLinePaste::Always: - warnMultiLine = true; - break; - default: - warnMultiLine = false; - break; - } - - if (warnMultiLine) - { - const std::wstring_view view{ text }; - warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos; - } - - constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB - const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); - - if (warnMultiLine || warnLargeText) - { - co_await wil::resume_foreground(dispatcher); - - if (const auto strongThis = weakThis.get()) - { - // We have to initialize the dialog here to be able to change the text of the text block within it - std::ignore = FindName(L"MultiLinePasteDialog"); - - // WinUI absolutely cannot deal with large amounts of text (at least O(n), possibly O(n^2), - // so we limit the string length here and add an ellipsis if necessary. - auto clipboardText = text; - if (clipboardText.size() > 1024) - { - const std::wstring_view view{ text }; - // Make sure we don't cut in the middle of a surrogate pair - const auto len = til::utf16_iterate_prev(view, 512); - clipboardText = til::hstring_format(FMT_COMPILE(L"{}\n…"), view.substr(0, len)); - } - - ClipboardText().Text(std::move(clipboardText)); - - // The vertical offset on the scrollbar does not reset automatically, so reset it manually - ClipboardContentScrollViewer().ScrollToVerticalOffset(0); - - auto warningResult = ContentDialogResult::Primary; - if (warnMultiLine) - { - warningResult = co_await _ShowMultiLinePasteWarningDialog(); - } - else if (warnLargeText) - { - warningResult = co_await _ShowLargePasteWarningDialog(); - } - - // Clear the clipboard text so it doesn't lie around in memory - ClipboardText().Text({}); - - if (warningResult != ContentDialogResult::Primary) - { - // user rejected the paste - co_return; - } - } - - co_await winrt::resume_background(); - } - - // This will end up calling ConptyConnection::WriteInput which calls WriteFile which may block for - // an indefinite amount of time. Avoid freezes and deadlocks by running this on a background thread. - assert(!dispatcher.HasThreadAccess()); - eventArgs.HandleClipboardData(text); - - // GH#18821: If broadcast input is active, paste the same text into all other - // panes on the tab. We do this here (rather than re-reading the - // clipboard per-pane) so that only one paste warning is shown. - co_await wil::resume_foreground(dispatcher); - if (const auto strongThis = weakThis.get()) - { - if (const auto& tab{ strongThis->_GetFocusedTabImpl() }) - { - if (tab->TabStatus().IsInputBroadcastActive()) - { - tab->GetRootPane()->WalkTree([&](auto&& pane) { - if (const auto control = pane->GetTerminalControl()) - { - if (control.ContentId() != sourceId && !control.ReadOnly()) - { - control.RawWriteString(text); - } - } - }); - } - } - } - } - CATCH_LOG(); - - void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs) - { - try - { - auto parsed = winrt::Windows::Foundation::Uri(eventArgs.Uri()); - if (_IsUriSupported(parsed)) - { - ShellExecute(nullptr, L"open", eventArgs.Uri().c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - else - { - _ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), eventArgs.Uri()); - } - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - _ShowCouldNotOpenDialog(RS_(L"InvalidUriText"), eventArgs.Uri()); - } - } - - // Method Description: - // - Opens up a dialog box explaining why we could not open a URI - // Arguments: - // - The reason (unsupported scheme, invalid uri, potentially more in the future) - // - The uri - void TerminalPage::_ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri) - { - if (auto presenter{ _dialogPresenter.get() }) - { - // FindName needs to be called first to actually load the xaml object - auto unopenedUriDialog = FindName(L"CouldNotOpenUriDialog").try_as(); - - // Insert the reason and the URI - CouldNotOpenUriReason().Text(reason); - UnopenedUri().Text(uri); - - // Show the dialog - presenter.ShowDialog(unopenedUriDialog); - } - } - - // Method Description: - // - Determines if the given URI is currently supported - // Arguments: - // - The parsed URI - // Return value: - // - True if we support it, false otherwise - bool TerminalPage::_IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri) - { - if (parsedUri.SchemeName() == L"http" || parsedUri.SchemeName() == L"https") - { - return true; - } - if (parsedUri.SchemeName() == L"file") - { - const auto host = parsedUri.Host(); - // If no hostname was provided or if the hostname was "localhost", Host() will return an empty string - // and we allow it - if (host == L"") - { - return true; - } - - // GH#10188: WSL paths are okay. We'll let those through. - if (host == L"wsl$" || host == L"wsl.localhost") - { - return true; - } - - // TODO: by the OSC 8 spec, if a hostname (other than localhost) is provided, we _should_ be - // comparing that value against what is returned by GetComputerNameExW and making sure they match. - // However, ShellExecute does not seem to be happy with file URIs of the form - // file://{hostname}/path/to/file.ext - // and so while we could do the hostname matching, we do not know how to actually open the URI - // if its given in that form. So for now we ignore all hostnames other than localhost - return false; - } - - // In this case, the app manually output a URI other than file:// or - // http(s)://. We'll trust the user knows what they're doing when - // clicking on those sorts of links. - // See discussion in GH#7562 for more details. - return true; - } - - // Important! Don't take this eventArgs by reference, we need to extend the - // lifetime of it to the other side of the co_await! - safe_void_coroutine TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, - const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) - { - auto weakThis = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - if (auto page = weakThis.get()) - { - auto message = eventArgs.Message(); - - winrt::hstring title; - - switch (eventArgs.Level()) - { - case NoticeLevel::Debug: - title = RS_(L"NoticeDebug"); //\xebe8 - break; - case NoticeLevel::Info: - title = RS_(L"NoticeInfo"); // \xe946 - break; - case NoticeLevel::Warning: - title = RS_(L"NoticeWarning"); //\xe7ba - break; - case NoticeLevel::Error: - title = RS_(L"NoticeError"); //\xe783 - break; - } - - page->_ShowControlNoticeDialog(title, message); - } - } - - void TerminalPage::_ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message) - { - if (auto presenter{ _dialogPresenter.get() }) - { - // FindName needs to be called first to actually load the xaml object - auto controlNoticeDialog = FindName(L"ControlNoticeDialog").try_as(); - - ControlNoticeDialog().Title(winrt::box_value(title)); - - // Insert the message - NoticeMessage().Text(message); - - // Show the dialog - presenter.ShowDialog(controlNoticeDialog); - } - } - - // Method Description: - // - Copy text from the focused terminal to the Windows Clipboard - // Arguments: - // - dismissSelection: if not enabled, copying text doesn't dismiss the selection - // - singleLine: if enabled, copy contents as a single line of text - // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection - // - formats: dictate which formats need to be copied - // Return Value: - // - true iff we we able to copy text (if a selection was active) - bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats) - { - if (const auto& control{ _GetActiveControl() }) - { - return control.CopySelectionToClipboard(dismissSelection, singleLine, withControlSequences, formats); - } - return false; - } - - // Method Description: - // - Send an event (which will be caught by AppHost) to set the progress indicator on the taskbar - // Arguments: - // - sender (not used) - // - eventArgs: the arguments specifying how to set the progress indicator - safe_void_coroutine TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) - { - const auto weak = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - if (const auto strong = weak.get()) - { - SetTaskbarProgress.raise(*this, nullptr); - } - } - - // Method Description: - // - Send an event (which will be caught by AppHost) to change the show window state of the entire hosting window - // Arguments: - // - sender (not used) - // - args: the arguments specifying how to set the display status to ShowWindow for our window handle - void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) - { - ShowWindowChanged.raise(*this, args); - } - - Windows::Foundation::IAsyncOperation> TerminalPage::_FindPackageAsync(hstring query) - { - const PackageManager packageManager = WindowsPackageManagerFactory::CreatePackageManager(); - PackageCatalogReference catalogRef{ - packageManager.GetPredefinedPackageCatalog(PredefinedPackageCatalog::OpenWindowsCatalog) - }; - catalogRef.PackageCatalogBackgroundUpdateInterval(std::chrono::hours(24)); - - ConnectResult connectResult{ nullptr }; - for (int retries = 0;;) - { - connectResult = catalogRef.Connect(); - if (connectResult.Status() == ConnectResultStatus::Ok) - { - break; - } - - if (++retries == 3) - { - co_return nullptr; - } - } - - PackageCatalog catalog = connectResult.PackageCatalog(); - PackageMatchFilter filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); - filter.Value(query); - filter.Field(PackageMatchField::Command); - filter.Option(PackageFieldMatchOption::Equals); - - FindPackagesOptions options = WindowsPackageManagerFactory::CreateFindPackagesOptions(); - options.Filters().Append(filter); - options.ResultLimit(20); - - const auto result = co_await catalog.FindPackagesAsync(options); - const IVectorView pkgList = result.Matches(); - co_return pkgList; - } - - Windows::Foundation::IAsyncAction TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args) - { - if (!Feature_QuickFix::IsEnabled()) - { - co_return; - } - - const auto weak = get_weak(); - const auto dispatcher = Dispatcher(); - - // All of the code until resume_foreground is static and - // doesn't touch `this`, so we don't need weak/strong_ref. - co_await winrt::resume_background(); - - // no packages were found, nothing to suggest - const auto pkgList = co_await _FindPackageAsync(args.MissingCommand()); - if (!pkgList || pkgList.Size() == 0) - { - co_return; - } - - std::vector suggestions; - suggestions.reserve(pkgList.Size()); - for (const auto& pkg : pkgList) - { - // --id and --source ensure we don't collide with another package catalog - suggestions.emplace_back(fmt::format(FMT_COMPILE(L"winget install --id {} -s winget"), pkg.CatalogPackage().Id())); - } - - co_await wil::resume_foreground(dispatcher); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - auto term = _GetActiveControl(); - if (!term) - { - co_return; - } - term.UpdateWinGetSuggestions(single_threaded_vector(std::move(suggestions))); - term.RefreshQuickFixMenu(); - } - - void TerminalPage::_WindowSizeChanged(const IInspectable sender, const Microsoft::Terminal::Control::WindowSizeChangedEventArgs args) - { - // Raise if: - // - Not in quake mode - // - Not in fullscreen - // - Only one tab exists - // - Only one pane exists - // else: - // - Reset conpty to its original size back - if (!WindowProperties().IsQuakeWindow() && !Fullscreen() && - NumberOfTabs() == 1 && _GetFocusedTabImpl()->GetLeafPaneCount() == 1) - { - WindowSizeChanged.raise(*this, args); - } - else if (const auto& control{ sender.try_as() }) - { - const auto& connection = control.Connection(); - - if (const auto& conpty{ connection.try_as() }) - { - conpty.ResetSize(); - } - } - } - - void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const - { - if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) - { - const auto plain = args.Plain(); - const auto html = args.Html(); - const auto rtf = args.Rtf(); - - clipboard::write( - { plain.data(), plain.size() }, - { reinterpret_cast(html.data()), html.size() }, - { reinterpret_cast(rtf.data()), rtf.size() }); - } - } - - // Method Description: - // - Paste text from the Windows Clipboard to the focused terminal - void TerminalPage::_PasteText() - { - if (const auto& control{ _GetActiveControl() }) - { - control.PasteTextFromClipboard(); - } - } - - // Function Description: - // - Called when the settings button is clicked. ShellExecutes the settings - // file, as to open it in the default editor for .json files. Does this in - // a background thread, as to not hang/crash the UI thread. - safe_void_coroutine TerminalPage::_LaunchSettings(const SettingsTarget target) - { - if (target == SettingsTarget::SettingsUI) - { - OpenSettingsUI(); - } - else - { - // This will switch the execution of the function to a background (not - // UI) thread. This is IMPORTANT, because the Windows.Storage API's - // (used for retrieving the path to the file) will crash on the UI - // thread, because the main thread is a STA. - // - // NOTE: All remaining code of this function doesn't touch `this`, so we don't need weak/strong_ref. - // NOTE NOTE: Don't touch `this` when you make changes here. - co_await winrt::resume_background(); - - auto openFile = [](const auto& filePath) { - HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); - if (static_cast(reinterpret_cast(res)) <= 32) - { - ShellExecute(nullptr, nullptr, L"notepad", filePath.c_str(), nullptr, SW_SHOW); - } - }; - - auto openFolder = [](const auto& filePath) { - HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); - if (static_cast(reinterpret_cast(res)) <= 32) - { - ShellExecute(nullptr, nullptr, L"open", filePath.c_str(), nullptr, SW_SHOW); - } - }; - - switch (target) - { - case SettingsTarget::DefaultsFile: - openFile(CascadiaSettings::DefaultSettingsPath()); - break; - case SettingsTarget::SettingsFile: - openFile(CascadiaSettings::SettingsPath()); - break; - case SettingsTarget::Directory: - openFolder(CascadiaSettings::SettingsDirectory()); - break; - case SettingsTarget::AllFiles: - openFile(CascadiaSettings::DefaultSettingsPath()); - openFile(CascadiaSettings::SettingsPath()); - break; - } - } - } - - // Method Description: - // - Responds to the TabView control's Tab Closing event by removing - // the indicated tab from the set and focusing another one. - // The event is cancelled so App maintains control over the - // items in the tabview. - // Arguments: - // - sender: the control that originated this event - // - eventArgs: the event's constituent arguments - void TerminalPage::_OnTabCloseRequested(const IInspectable& /*sender*/, const MUX::Controls::TabViewTabCloseRequestedEventArgs& eventArgs) - { - const auto tabViewItem = eventArgs.Tab(); - if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) - { - _HandleCloseTabRequested(tab); - } - } - - TermControl TerminalPage::_CreateNewControlAndContent(const Settings::TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) - { - // Do any initialization that needs to apply to _every_ TermControl we - // create here. - const auto content = _manager.CreateCore(*settings.DefaultSettings(), settings.UnfocusedSettings().try_as(), connection); - const TermControl control{ content }; - return _SetupControl(control); - } - - TermControl TerminalPage::_AttachControlToContent(const uint64_t& contentId) - { - if (const auto& content{ _manager.TryLookupCore(contentId) }) - { - // We have to pass in our current keybindings, because that's an - // object that belongs to this TerminalPage, on this thread. If we - // don't, then when we move the content to another thread, and it - // tries to handle a key, it'll callback on the original page's - // stack, inevitably resulting in a wrong_thread - return _SetupControl(TermControl::NewControlByAttachingContent(content)); - } - return nullptr; - } - - TermControl TerminalPage::_SetupControl(const TermControl& term) - { - // GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now. - if (_visible) - { - term.WindowVisibilityChanged(_visible); - } - - // Even in the case of re-attaching content from another window, this - // will correctly update the control's owning HWND - if (_hostingHwnd.has_value()) - { - term.OwningHwnd(reinterpret_cast(*_hostingHwnd)); - } - - term.KeyBindings(*_bindings); - - _RegisterTerminalEvents(term); - return term; - } - - // Method Description: - // - Creates a pane and returns a shared_ptr to it - // - The caller should handle where the pane goes after creation, - // either to split an already existing pane or to create a new tab with it - // Arguments: - // - newTerminalArgs: an object that may contain a blob of parameters to - // control which profile is created and with possible other - // configurations. See CascadiaSettings::BuildSettings for more details. - // - sourceTab: an optional tab reference that indicates that the created - // pane should be a duplicate of the tab's focused pane - // - existingConnection: optionally receives a connection from the outside - // world instead of attempting to create one - // Return Value: - // - If the newTerminalArgs required us to open the pane as a new elevated - // connection, then we'll return nullptr. Otherwise, we'll return a new - // Pane for this connection. - std::shared_ptr TerminalPage::_MakeTerminalPane(const NewTerminalArgs& newTerminalArgs, - const winrt::TerminalApp::Tab& sourceTab, - TerminalConnection::ITerminalConnection existingConnection) - { - // First things first - Check for making a pane from content ID. - if (newTerminalArgs && - newTerminalArgs.ContentId() != 0) - { - // Don't need to worry about duplicating or anything - we'll - // serialize the actual profile's GUID along with the content guid. - const auto& profile = _settings.GetProfileForArgs(newTerminalArgs); - const auto control = _AttachControlToContent(newTerminalArgs.ContentId()); - auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; - return std::make_shared(paneContent); - } - - Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; - Profile profile{ nullptr }; - - if (const auto& tabImpl{ _GetTabImpl(sourceTab) }) - { - profile = tabImpl->GetFocusedProfile(); - if (profile) - { - // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. - profile = GetClosestProfileForDuplicationOfProfile(profile); - controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); - const auto workingDirectory = tabImpl->GetActiveTerminalControl().WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) - { - controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); - } - } - } - if (!profile) - { - profile = _settings.GetProfileForArgs(newTerminalArgs); - controlSettings = Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs); - } - - // Try to handle auto-elevation - if (_maybeElevate(newTerminalArgs, controlSettings, profile)) - { - return nullptr; - } - - const auto sessionId = controlSettings.DefaultSettings()->SessionId(); - const auto hasSessionId = sessionId != winrt::guid{}; - - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), hasSessionId); - if (existingConnection) - { - connection.Resize(controlSettings.DefaultSettings()->InitialRows(), controlSettings.DefaultSettings()->InitialCols()); - } - - TerminalConnection::ITerminalConnection debugConnection{ nullptr }; - if (_settings.GlobalSettings().DebugFeaturesEnabled()) - { - const auto window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const auto bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - if (bothAltsPressed) - { - std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); - } - } - - const auto control = _CreateNewControlAndContent(controlSettings, connection); - - if (hasSessionId) - { - using namespace std::string_view_literals; - - const auto settingsDir = CascadiaSettings::SettingsDirectory(); - const auto admin = IsRunningElevated(); - const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv; - const auto path = fmt::format(FMT_COMPILE(L"{}\\{}{}.txt"), settingsDir, filenamePrefix, sessionId); - control.RestoreFromPath(path); - } - - auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; - - auto resultPane = std::make_shared(paneContent); - - if (debugConnection) // this will only be set if global debugging is on and tap is active - { - auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection); - // Split (auto) with the debug tap. - auto debugContent{ winrt::make(profile, _terminalSettingsCache, newControl) }; - auto debugPane = std::make_shared(debugContent); - - // Since we're doing this split directly on the pane (instead of going through Tab, - // we need to handle the panes 'active' states - - // Set the pane we're splitting to active (otherwise Split will not do anything) - resultPane->SetActive(); - auto [original, _] = resultPane->Split(SplitDirection::Automatic, 0.5f, debugPane); - - // Set the non-debug pane as active - resultPane->ClearActive(); - original->SetActive(); - } - - return resultPane; - } - - // NOTE: callers of _MakePane should be able to accept nullptr as a return - // value gracefully. - std::shared_ptr TerminalPage::_MakePane(const INewContentArgs& contentArgs, - const winrt::TerminalApp::Tab& sourceTab, - TerminalConnection::ITerminalConnection existingConnection) - - { - const auto& newTerminalArgs{ contentArgs.try_as() }; - if (contentArgs == nullptr || newTerminalArgs != nullptr || contentArgs.Type().empty()) - { - // Terminals are of course special, and have to deal with debug taps, duplicating the tab, etc. - return _MakeTerminalPane(newTerminalArgs, sourceTab, existingConnection); - } - - IPaneContent content{ nullptr }; - - const auto& paneType{ contentArgs.Type() }; - if (paneType == L"scratchpad") - { - const auto& scratchPane{ winrt::make_self() }; - - // This is maybe a little wacky - add our key event handler to the pane - // we made. So that we can get actions for keys that the content didn't - // handle. - scratchPane->GetRoot().KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); - - content = *scratchPane; - } - else if (paneType == L"settings") - { - content = _makeSettingsContent(); - } - else if (paneType == L"snippets") - { - // Prevent the user from opening a bunch of snippets panes. - // - // Look at the focused tab, and if it already has one, then just focus it. - if (const auto& focusedTab{ _GetFocusedTabImpl() }) - { - const auto rootPane{ focusedTab->GetRootPane() }; - const bool found = rootPane == nullptr ? false : rootPane->WalkTree([](const auto& p) -> bool { - if (const auto& snippets{ p->GetContent().try_as() }) - { - snippets->Focus(FocusState::Programmatic); - return true; - } - return false; - }); - // Bail out if we already found one. - if (found) - { - return nullptr; - } - } - - const auto& tasksContent{ winrt::make_self() }; - tasksContent->UpdateSettings(_settings); - tasksContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); - tasksContent->DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - if (const auto& termControl{ _GetActiveControl() }) - { - tasksContent->SetLastActiveControl(termControl); - } - - content = *tasksContent; - } - else if (paneType == L"x-markdown") - { - if (Feature_MarkdownPane::IsEnabled()) - { - const auto& markdownContent{ winrt::make_self(L"") }; - markdownContent->UpdateSettings(_settings); - markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); - - // This one doesn't use DispatchCommand, because we don't create - // Command's freely at runtime like we do with just plain old actions. - markdownContent->DispatchActionRequested([weak = get_weak()](const auto& sender, const auto& actionAndArgs) { - if (const auto& page{ weak.get() }) - { - page->_actionDispatch->DoAction(sender, actionAndArgs); - } - }); - if (const auto& termControl{ _GetActiveControl() }) - { - markdownContent->SetLastActiveControl(termControl); - } - - content = *markdownContent; - } - } - - assert(content); - - return std::make_shared(content); - } - - void TerminalPage::_restartPaneConnection( - const TerminalApp::TerminalPaneContent& paneContent, - const winrt::Windows::Foundation::IInspectable&) - { - // Note: callers are likely passing in `nullptr` as the args here, as - // the TermControl.RestartTerminalRequested event doesn't actually pass - // any args upwards itself. If we ever change this, make sure you check - // for nulls - if (const auto& connection{ _duplicateConnectionForRestart(paneContent) }) - { - paneContent.GetTermControl().Connection(connection); - connection.Start(); - } - } - - // Method Description: - // - Sets background image and applies its settings (stretch, opacity and alignment) - // - Checks path validity - // Arguments: - // - newAppearance - // Return Value: - // - - void TerminalPage::_SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance) - { - if (!_settings.GlobalSettings().UseBackgroundImageForWindow()) - { - _tabContent.Background(nullptr); - return; - } - - const auto path = newAppearance.BackgroundImagePath().Resolved(); - if (path.empty()) - { - _tabContent.Background(nullptr); - return; - } - - Windows::Foundation::Uri imageUri{ nullptr }; - try - { - imageUri = Windows::Foundation::Uri{ path }; - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - _tabContent.Background(nullptr); - return; - } - // Check if the image brush is already pointing to the image - // in the modified settings; if it isn't (or isn't there), - // set a new image source for the brush - - auto brush = _tabContent.Background().try_as(); - Media::Imaging::BitmapImage imageSource = brush == nullptr ? nullptr : brush.ImageSource().try_as(); - - if (imageSource == nullptr || - imageSource.UriSource() == nullptr || - !imageSource.UriSource().Equals(imageUri)) - { - Media::ImageBrush b{}; - // Note that BitmapImage handles the image load asynchronously, - // which is especially important since the image - // may well be both large and somewhere out on the - // internet. - Media::Imaging::BitmapImage image(imageUri); - b.ImageSource(image); - _tabContent.Background(b); - } - - // Pull this into a separate block. If the image didn't change, but the - // properties of the image did, we should still update them. - if (const auto newBrush{ _tabContent.Background().try_as() }) - { - newBrush.Stretch(newAppearance.BackgroundImageStretchMode()); - newBrush.Opacity(newAppearance.BackgroundImageOpacity()); - } - } - - // Method Description: - // - Hook up keybindings, and refresh the UI of the terminal. - // This includes update the settings of all the tabs according - // to their profiles, update the title and icon of each tab, and - // finally create the tab flyout - void TerminalPage::_RefreshUIForSettingsReload() - { - // Re-wire the keybindings to their handlers, as we'll have created a - // new AppKeyBindings object. - _HookupKeyBindings(_settings.ActionMap()); - - // Refresh UI elements - - // Recreate the TerminalSettings cache here. We'll use that as we're - // updating terminal panes, so that we don't have to build a _new_ - // TerminalSettings for every profile we update - we can just look them - // up the previous ones we built. - _terminalSettingsCache->Reset(_settings); - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // Let the tab know that there are new settings. It's up to each content to decide what to do with them. - tabImpl->UpdateSettings(_settings); - - // Update the icon of the tab for the currently focused profile in that tab. - // Only do this for TerminalTabs. Other types of tabs won't have multiple panes - // and profiles so the Title and Icon will be set once and only once on init. - _UpdateTabIcon(*tabImpl); - - // Force the TerminalTab to re-grab its currently active control's title. - tabImpl->UpdateTitle(); - } - - auto tabImpl{ winrt::get_self(tab) }; - tabImpl->SetActionMap(_settings.ActionMap()); - } - - if (const auto focusedTab{ _GetFocusedTabImpl() }) - { - if (const auto profile{ focusedTab->GetFocusedProfile() }) - { - _SetBackgroundImage(profile.DefaultAppearance()); - } - } - - // repopulate the new tab button's flyout with entries for each - // profile, which might have changed - _UpdateTabWidthMode(); - _CreateNewTabFlyout(); - - // Reload the current value of alwaysOnTop from the settings file. This - // will let the user hot-reload this setting, but any runtime changes to - // the alwaysOnTop setting will be lost. - _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); - AlwaysOnTopChanged.raise(*this, nullptr); - - _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); - - // Settings AllowDependentAnimations will affect whether animations are - // enabled application-wide, so we don't need to check it each time we - // want to create an animation. - WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); - - _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); - - Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; - _tabView.Background(transparent); - - //////////////////////////////////////////////////////////////////////// - // Begin Theme handling - _updateThemeColors(); - - _updateAllTabCloseButtons(); - - // The user may have changed the "show title in titlebar" setting. - TitleChanged.raise(*this, nullptr); - } - - void TerminalPage::_updateAllTabCloseButtons() - { - // Update the state of the CloseButtonOverlayMode property of - // our TabView, to match the tab.showCloseButton property in the theme. - // - // Also update every tab's individual IsClosable to match the same property. - const auto theme = _settings.GlobalSettings().CurrentTheme(); - const auto visibility = (theme && theme.Tab()) ? - theme.Tab().ShowCloseButton() : - Settings::Model::TabCloseButtonVisibility::Always; - - _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; - - for (const auto& tab : _tabs) - { - tab.CloseButtonVisibility(visibility); - } - - switch (visibility) - { - case Settings::Model::TabCloseButtonVisibility::Never: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); - break; - case Settings::Model::TabCloseButtonVisibility::Hover: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); - break; - case Settings::Model::TabCloseButtonVisibility::ActiveOnly: - default: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); - break; - } - } - - // Method Description: - // - Sets the initial actions to process on startup. We'll make a copy of - // this list, and process these actions when we're loaded. - // - This function will have no effective result after Create() is called. - // Arguments: - // - actions: a list of Actions to process on startup. - // Return Value: - // - - void TerminalPage::SetStartupActions(std::vector actions) - { - _startupActions = std::move(actions); - } - - void TerminalPage::SetStartupConnection(ITerminalConnection connection) - { - _startupConnection = std::move(connection); - } - - winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const - { - return _dialogPresenter.get(); - } - - void TerminalPage::DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter) - { - _dialogPresenter = dialogPresenter; - } - - // Method Description: - // - Get the combined taskbar state for the page. This is the combination of - // all the states of all the tabs, which are themselves a combination of - // all their panes. Taskbar states are given a priority based on the rules - // in: - // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate - // under "How the Taskbar Button Chooses the Progress Indicator for a Group" - // Arguments: - // - - // Return Value: - // - A TaskbarState object representing the combined taskbar state and - // progress percentage of all our tabs. - winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const - { - auto state{ winrt::make() }; - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - auto tabState{ tabImpl->GetCombinedTaskbarState() }; - // lowest priority wins - if (tabState.Priority() < state.Priority()) - { - state = tabState; - } - } - } - - return state; - } - - // Method Description: - // - This is the method that App will call when the titlebar - // has been clicked. It dismisses any open flyouts. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::TitlebarClicked() - { - if (_newTabButton && _newTabButton.Flyout()) - { - _newTabButton.Flyout().Hide(); - } - _DismissTabContextMenus(); - } - - // Method Description: - // - Notifies all attached console controls that the visibility of the - // hosting window has changed. The underlying PTYs may need to know this - // for the proper response to `::GetConsoleWindow()` from a Win32 console app. - // Arguments: - // - showOrHide: Show is true; hide is false. - // Return Value: - // - - void TerminalPage::WindowVisibilityChanged(const bool showOrHide) - { - _visible = showOrHide; - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings - // objects but only have to iterate one time. - tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { - if (auto control = pane->GetTerminalControl()) - { - control.WindowVisibilityChanged(showOrHide); - } - }); - } - } - } - - // Method Description: - // - Called when the user tries to do a search using keybindings. - // This will tell the active terminal control of the passed tab - // to create a search box and enable find process. - // Arguments: - // - tab: the tab where the search box should be created - // Return Value: - // - - void TerminalPage::_Find(const Tab& tab) - { - if (const auto& control{ tab.GetActiveTerminalControl() }) - { - control.CreateSearchBoxControl(); - } - } - - // Method Description: - // - Toggles borderless mode. Hides the tab row, and raises our - // FocusModeChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleFocusMode() - { - SetFocusMode(!_isInFocusMode); - } - - void TerminalPage::SetFocusMode(const bool inFocusMode) - { - const auto newInFocusMode = inFocusMode; - if (newInFocusMode != FocusMode()) - { - _isInFocusMode = newInFocusMode; - _UpdateTabView(); - FocusModeChanged.raise(*this, nullptr); - } - } - - // Method Description: - // - Toggles fullscreen mode. Hides the tab row, and raises our - // FullscreenChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleFullscreen() - { - SetFullscreen(!_isFullscreen); - } - - // Method Description: - // - Toggles always on top mode. Raises our AlwaysOnTopChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleAlwaysOnTop() - { - _isAlwaysOnTop = !_isAlwaysOnTop; - AlwaysOnTopChanged.raise(*this, nullptr); - } - - // Method Description: - // - Sets the tab split button color when a new tab color is selected - // Arguments: - // - color: The color of the newly selected tab, used to properly calculate - // the foreground color of the split button (to match the font - // color of the tab) - // - accentColor: the actual color we are going to use to paint the tab row and - // split button, so that there is some contrast between the tab - // and the non-client are behind it - // Return Value: - // - - void TerminalPage::_SetNewTabButtonColor(const til::color color, const til::color accentColor) - { - constexpr auto lightnessThreshold = 0.6f; - // TODO GH#3327: Look at what to do with the tab button when we have XAML theming - const auto isBrightColor = ColorFix::GetLightness(color) >= lightnessThreshold; - const auto isLightAccentColor = ColorFix::GetLightness(accentColor) >= lightnessThreshold; - const auto hoverColorAdjustment = isLightAccentColor ? -0.05f : 0.05f; - const auto pressedColorAdjustment = isLightAccentColor ? -0.1f : 0.1f; - - const auto foregroundColor = isBrightColor ? Colors::Black() : Colors::White(); - const auto hoverColor = til::color{ ColorFix::AdjustLightness(accentColor, hoverColorAdjustment) }; - const auto pressedColor = til::color{ ColorFix::AdjustLightness(accentColor, pressedColorAdjustment) }; - - Media::SolidColorBrush backgroundBrush{ accentColor }; - Media::SolidColorBrush backgroundHoverBrush{ hoverColor }; - Media::SolidColorBrush backgroundPressedBrush{ pressedColor }; - Media::SolidColorBrush foregroundBrush{ foregroundColor }; - - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackground"), backgroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPointerOver"), backgroundHoverBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPressed"), backgroundPressedBrush); - - // Load bearing: The SplitButton uses SplitButtonForegroundSecondary for - // the secondary button, but {TemplateBinding Foreground} for the - // primary button. - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForeground"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPointerOver"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPressed"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondary"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondaryPressed"), foregroundBrush); - - _newTabButton.Background(backgroundBrush); - _newTabButton.Foreground(foregroundBrush); - - // This is just like what we do in Tab::_RefreshVisualState. We need - // to manually toggle the visual state, so the setters in the visual - // state group will re-apply, and set our currently selected colors in - // the resources. - VisualStateManager::GoToState(_newTabButton, L"FlyoutOpen", true); - VisualStateManager::GoToState(_newTabButton, L"Normal", true); - } - - // Method Description: - // - Clears the tab split button color to a system color - // (or white if none is found) when the tab's color is cleared - // - Clears the tab row color to a system color - // (or white if none is found) when the tab's color is cleared - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_ClearNewTabButtonColor() - { - // TODO GH#3327: Look at what to do with the tab button when we have XAML theming - winrt::hstring keys[] = { - L"SplitButtonBackground", - L"SplitButtonBackgroundPointerOver", - L"SplitButtonBackgroundPressed", - L"SplitButtonForeground", - L"SplitButtonForegroundSecondary", - L"SplitButtonForegroundPointerOver", - L"SplitButtonForegroundPressed", - L"SplitButtonForegroundSecondaryPressed" - }; - - // simply clear any of the colors in the split button's dict - for (auto keyString : keys) - { - auto key = winrt::box_value(keyString); - if (_newTabButton.Resources().HasKey(key)) - { - _newTabButton.Resources().Remove(key); - } - } - - const auto res = Application::Current().Resources(); - - const auto defaultBackgroundKey = winrt::box_value(L"TabViewItemHeaderBackground"); - const auto defaultForegroundKey = winrt::box_value(L"SystemControlForegroundBaseHighBrush"); - winrt::Windows::UI::Xaml::Media::SolidColorBrush backgroundBrush; - winrt::Windows::UI::Xaml::Media::SolidColorBrush foregroundBrush; - - // TODO: Related to GH#3917 - I think if the system is set to "Dark" - // theme, but the app is set to light theme, then this lookup still - // returns to us the dark theme brushes. There's gotta be a way to get - // the right brushes... - // See also GH#5741 - if (res.HasKey(defaultBackgroundKey)) - { - auto obj = res.Lookup(defaultBackgroundKey); - backgroundBrush = obj.try_as(); - } - else - { - backgroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Black() }; - } - - if (res.HasKey(defaultForegroundKey)) - { - auto obj = res.Lookup(defaultForegroundKey); - foregroundBrush = obj.try_as(); - } - else - { - foregroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::White() }; - } - - _newTabButton.Background(backgroundBrush); - _newTabButton.Foreground(foregroundBrush); - } - - // Function Description: - // - This is a helper method to get the commandline out of a - // ExecuteCommandline action, break it into subcommands, and attempt to - // parse it into actions. This is used by _HandleExecuteCommandline for - // processing commandlines in the current WT window. - // Arguments: - // - args: the ExecuteCommandlineArgs to synthesize a list of startup actions for. - // Return Value: - // - an empty list if we failed to parse; otherwise, a list of actions to execute. - std::vector TerminalPage::ConvertExecuteCommandlineToActions(const ExecuteCommandlineArgs& args) - { - ::TerminalApp::AppCommandlineArgs appArgs; - if (appArgs.ParseArgs(args) == 0) - { - return appArgs.GetStartupActions(); - } - - return {}; - } - - void TerminalPage::_FocusActiveControl(IInspectable /*sender*/, - IInspectable /*eventArgs*/) - { - _FocusCurrentTab(false); - } - - bool TerminalPage::FocusMode() const - { - return _isInFocusMode; - } - - bool TerminalPage::Fullscreen() const - { - return _isFullscreen; - } - - // Method Description: - // - Returns true if we're currently in "Always on top" mode. When we're in - // always on top mode, the window should be on top of all other windows. - // If multiple windows are all "always on top", they'll maintain their own - // z-order, with all the windows on top of all other non-topmost windows. - // Arguments: - // - - // Return Value: - // - true if we should be in "always on top" mode - bool TerminalPage::AlwaysOnTop() const - { - return _isAlwaysOnTop; - } - - // Method Description: - // - Returns true if the tab row should be visible when we're in full screen - // state. - // Arguments: - // - - // Return Value: - // - true if the tab row should be visible in full screen state - bool TerminalPage::ShowTabsFullscreen() const - { - return _showTabsFullscreen; - } - - // Method Description: - // - Updates the visibility of the tab row when in fullscreen state. - void TerminalPage::SetShowTabsFullscreen(bool newShowTabsFullscreen) - { - if (_showTabsFullscreen == newShowTabsFullscreen) - { - return; - } - - _showTabsFullscreen = newShowTabsFullscreen; - - // if we're currently in fullscreen, update tab view to make - // sure tabs are given the correct visibility - if (_isFullscreen) - { - _UpdateTabView(); - } - } - - void TerminalPage::SetFullscreen(bool newFullscreen) - { - if (_isFullscreen == newFullscreen) - { - return; - } - _isFullscreen = newFullscreen; - _UpdateTabView(); - FullscreenChanged.raise(*this, nullptr); - } - - // Method Description: - // - Updates the page's state for isMaximized when the window changes externally. - void TerminalPage::Maximized(bool newMaximized) - { - _isMaximized = newMaximized; - } - - // Method Description: - // - Asks the window to change its maximized state. - void TerminalPage::RequestSetMaximized(bool newMaximized) - { - if (_isMaximized == newMaximized) - { - return; - } - _isMaximized = newMaximized; - ChangeMaximizeRequested.raise(*this, nullptr); - } - - TerminalApp::IPaneContent TerminalPage::_makeSettingsContent() - { - if (auto app{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) - { - if (auto appPrivate{ winrt::get_self(app) }) - { - // Lazily load the Settings UI components so that we don't do it on startup. - appPrivate->PrepareForSettingsUI(); - } - } - - // Create the SUI pane content - auto settingsContent{ winrt::make_self(_settings) }; - auto sui = settingsContent->SettingsUI(); - - if (_hostingHwnd) - { - sui.SetHostingWindow(reinterpret_cast(*_hostingHwnd)); - } - - // GH#8767 - let unhandled keys in the SUI try to run commands too. - sui.KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); - - sui.OpenJson([weakThis{ get_weak() }](auto&& /*s*/, winrt::Microsoft::Terminal::Settings::Model::SettingsTarget e) { - if (auto page{ weakThis.get() }) - { - page->_LaunchSettings(e); - } - }); - - sui.ShowLoadWarningsDialog([weakThis{ get_weak() }](auto&& /*s*/, const Windows::Foundation::Collections::IVectorView& warnings) { - if (auto page{ weakThis.get() }) - { - page->ShowLoadWarningsDialog.raise(*page, warnings); - } - }); - - return *settingsContent; - } - - // Method Description: - // - Creates a settings UI tab and focuses it. If there's already a settings UI tab open, - // just focus the existing one. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::OpenSettingsUI() - { - // If we're holding the settings tab's switch command, don't create a new one, switch to the existing one. - if (!_settingsTab) - { - // Create the tab - auto resultPane = std::make_shared(_makeSettingsContent()); - _settingsTab = _CreateNewTabFromPane(resultPane); - } - else - { - _tabView.SelectedItem(_settingsTab.TabViewItem()); - } - } - - // Method Description: - // - Returns a com_ptr to the implementation type of the given tab if it's a Tab. - // If the tab is not a TerminalTab, returns nullptr. - // Arguments: - // - tab: the projected type of a Tab - // Return Value: - // - If the tab is a TerminalTab, a com_ptr to the implementation type. - // If the tab is not a TerminalTab, nullptr - winrt::com_ptr TerminalPage::_GetTabImpl(const TerminalApp::Tab& tab) - { - winrt::com_ptr tabImpl; - tabImpl.copy_from(winrt::get_self(tab)); - return tabImpl; - } - - // Method Description: - // - Computes the delta for scrolling the tab's viewport. - // Arguments: - // - scrollDirection - direction (up / down) to scroll - // - rowsToScroll - the number of rows to scroll - // Return Value: - // - delta - Signed delta, where a negative value means scrolling up. - int TerminalPage::_ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll) - { - return scrollDirection == ScrollUp ? -1 * rowsToScroll : rowsToScroll; - } - - // Method Description: - // - Reads system settings for scrolling (based on the step of the mouse scroll). - // Upon failure fallbacks to default. - // Return Value: - // - The number of rows to scroll or a magic value of WHEEL_PAGESCROLL - // indicating that we need to scroll an entire view height - uint32_t TerminalPage::_ReadSystemRowsToScroll() - { - uint32_t systemRowsToScroll; - if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) - { - LOG_LAST_ERROR(); - - // If SystemParametersInfoW fails, which it shouldn't, fall back to - // Windows' default value. - return DefaultRowsToScroll; - } - - return systemRowsToScroll; - } - - // Method Description: - // - Displays a dialog stating the "Touch Keyboard and Handwriting Panel - // Service" is disabled. - void TerminalPage::ShowKeyboardServiceWarning() const - { - if (!_IsMessageDismissed(InfoBarMessage::KeyboardServiceWarning)) - { - if (const auto keyboardServiceWarningInfoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) - { - keyboardServiceWarningInfoBar.IsOpen(true); - } - } - } - - // Function Description: - // - Helper function to get the OS-localized name for the "Touch Keyboard - // and Handwriting Panel Service". If we can't open up the service for any - // reason, then we'll just return the service's key, "TabletInputService". - // Return Value: - // - The OS-localized name for the TabletInputService - winrt::hstring _getTabletServiceName() - { - wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; - - if (LOG_LAST_ERROR_IF(!hManager.is_valid())) - { - return winrt::hstring{ TabletInputServiceKey }; - } - - DWORD cchBuffer = 0; - const auto ok = GetServiceDisplayNameW(hManager.get(), TabletInputServiceKey.data(), nullptr, &cchBuffer); - - // Windows 11 doesn't have a TabletInputService. - // (It was renamed to TextInputManagementService, because people kept thinking that a - // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) - if (ok || GetLastError() != ERROR_INSUFFICIENT_BUFFER) - { - return winrt::hstring{ TabletInputServiceKey }; - } - - std::wstring buffer; - cchBuffer += 1; // Add space for a null - buffer.resize(cchBuffer); - - if (LOG_LAST_ERROR_IF(!GetServiceDisplayNameW(hManager.get(), - TabletInputServiceKey.data(), - buffer.data(), - &cchBuffer))) - { - return winrt::hstring{ TabletInputServiceKey }; - } - return winrt::hstring{ buffer }; - } - - // Method Description: - // - Return the fully-formed warning message for the - // "KeyboardServiceDisabled" InfoBar. This InfoBar is used to warn the user - // if the keyboard service is disabled, and uses the OS localization for - // the service's actual name. It's bound to the bar in XAML. - // Return Value: - // - The warning message, including the OS-localized service name. - winrt::hstring TerminalPage::KeyboardServiceDisabledText() - { - const auto serviceName{ _getTabletServiceName() }; - const auto text{ RS_fmt(L"KeyboardServiceWarningText", serviceName) }; - return winrt::hstring{ text }; - } - - // Method Description: - // - Update the RequestedTheme of the specified FrameworkElement and all its - // Parent elements. We need to do this so that we can actually theme all - // of the elements of the TeachingTip. See GH#9717 - // Arguments: - // - element: The TeachingTip to set the theme on. - // Return Value: - // - - void TerminalPage::_UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element) - { - auto theme{ _settings.GlobalSettings().CurrentTheme() }; - auto requestedTheme{ theme.RequestedTheme() }; - while (element) - { - element.RequestedTheme(requestedTheme); - element = element.Parent().try_as(); - } - } - - // Method Description: - // - Display the name and ID of this window in a TeachingTip. If the window - // has no name, the name will be presented as "". - // - This can be invoked by either: - // * An identifyWindow action, that displays the info only for the current - // window - // * An identifyWindows action, that displays the info for all windows. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::IdentifyWindow() - { - // If we haven't ever loaded the TeachingTip, then do so now and - // create the toast for it. - if (_windowIdToast == nullptr) - { - if (auto tip{ FindName(L"WindowIdToast").try_as() }) - { - _windowIdToast = std::make_shared(tip); - // IsLightDismissEnabled == true is bugged and poorly interacts with multi-windowing. - // It causes the tip to be immediately dismissed when another tip is opened in another window. - tip.IsLightDismissEnabled(false); - // Make sure to use the weak ref when setting up this callback. - tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); - } - } - _UpdateTeachingTipTheme(WindowIdToast().try_as()); - - if (_windowIdToast != nullptr) - { - _windowIdToast->Open(); - } - } - - void TerminalPage::ShowTerminalWorkingDirectory() - { - // If we haven't ever loaded the TeachingTip, then do so now and - // create the toast for it. - if (_windowCwdToast == nullptr) - { - if (auto tip{ FindName(L"WindowCwdToast").try_as() }) - { - _windowCwdToast = std::make_shared(tip); - // Make sure to use the weak ref when setting up this - // callback. - tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); - } - } - _UpdateTeachingTipTheme(WindowCwdToast().try_as()); - - if (_windowCwdToast != nullptr) - { - _windowCwdToast->Open(); - } - } - - // Method Description: - // - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip. - // - Will raise an event that will bubble up to the monarch, asking if this - // name is acceptable. - // - we'll eventually get called back in TerminalPage::WindowName(hstring). - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_WindowRenamerActionClick(const IInspectable& /*sender*/, - const IInspectable& /*eventArgs*/) - { - auto newName = WindowRenamerTextBox().Text(); - _RequestWindowRename(newName); - } - - void TerminalPage::_RequestWindowRename(const winrt::hstring& newName) - { - auto request = winrt::make(newName); - // The WindowRenamer is _not_ a Toast - we want it to stay open until - // the user dismisses it. - if (WindowRenamer()) - { - WindowRenamer().IsOpen(false); - } - RenameWindowRequested.raise(*this, request); - // We can't just use request.Successful here, because the handler might - // (will) be handling this asynchronously, so when control returns to - // us, this hasn't actually been handled yet. We'll get called back in - // RenameFailed if this fails. - // - // Theoretically we could do a IAsyncOperation kind - // of thing with co_return winrt::make(false). - } - - // Method Description: - // - Used to track if the user pressed enter with the renamer open. If we - // immediately focus it after hitting Enter on the command palette, then - // the Enter keydown will dismiss the command palette and open the - // renamer, and then the enter keyup will go to the renamer. So we need to - // make sure both a down and up go to the renamer. - // Arguments: - // - e: the KeyRoutedEventArgs describing the key that was released - // Return Value: - // - - void TerminalPage::_WindowRenamerKeyDown(const IInspectable& /*sender*/, - const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto key = e.OriginalKey(); - if (key == Windows::System::VirtualKey::Enter) - { - _renamerPressedEnter = true; - } - } - - // Method Description: - // - Manually handle Enter and Escape for committing and dismissing a window - // rename. This is highly similar to the TabHeaderControl's KeyUp handler. - // Arguments: - // - e: the KeyRoutedEventArgs describing the key that was released - // Return Value: - // - - void TerminalPage::_WindowRenamerKeyUp(const IInspectable& sender, - const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto key = e.OriginalKey(); - if (key == Windows::System::VirtualKey::Enter && _renamerPressedEnter) - { - // User is done making changes, close the rename box - _WindowRenamerActionClick(sender, nullptr); - } - else if (key == Windows::System::VirtualKey::Escape) - { - // User wants to discard the changes they made - WindowRenamerTextBox().Text(_WindowProperties.WindowName()); - WindowRenamer().IsOpen(false); - _renamerPressedEnter = false; - } - } - - // Method Description: - // - This function stops people from duplicating the base profile, because - // it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done. - Profile TerminalPage::GetClosestProfileForDuplicationOfProfile(const Profile& profile) const noexcept - { - if (profile == _settings.ProfileDefaults()) - { - return _settings.FindProfile(_settings.GlobalSettings().DefaultProfile()); - } - return profile; - } - - // Function Description: - // - Helper to launch a new WT instance elevated. It'll do this by spawning - // a helper process, who will asking the shell to elevate the process for - // us. This might cause a UAC prompt. The elevation is performed on a - // background thread, as to not block the UI thread. - // Arguments: - // - newTerminalArgs: A NewTerminalArgs describing the terminal instance - // that should be spawned. The Profile should be filled in with the GUID - // of the profile we want to launch. - // Return Value: - // - - // Important: Don't take the param by reference, since we'll be doing work - // on another thread. - void TerminalPage::_OpenElevatedWT(NewTerminalArgs newTerminalArgs) - { - // BODGY - // - // We're going to construct the commandline we want, then toss it to a - // helper process called `elevate-shim.exe` that happens to live next to - // us. elevate-shim.exe will be the one to call ShellExecute with the - // args that we want (to elevate the given profile). - // - // We can't be the one to call ShellExecute ourselves. ShellExecute - // requires that the calling process stays alive until the child is - // spawned. However, in the case of something like `wt -p - // AlwaysElevateMe`, then the original WT will try to ShellExecute a new - // wt.exe (elevated) and immediately exit, preventing ShellExecute from - // successfully spawning the elevated WT. - - std::filesystem::path exePath = wil::GetModuleFileNameW(nullptr); - exePath.replace_filename(L"elevate-shim.exe"); - - // Build the commandline to pass to wt for this set of NewTerminalArgs - auto cmdline{ - fmt::format(FMT_COMPILE(L"new-tab {}"), newTerminalArgs.ToCommandline()) - }; - - wil::unique_process_information pi; - STARTUPINFOW si{}; - si.cb = sizeof(si); - - LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(exePath.c_str(), - cmdline.data(), - nullptr, - nullptr, - FALSE, - 0, - nullptr, - nullptr, - &si, - &pi)); - - // TODO: GH#8592 - It may be useful to pop a Toast here in the original - // Terminal window informing the user that the tab was opened in a new - // window. - } - - // Method Description: - // - If the requested settings want us to elevate this new terminal - // instance, and we're not currently elevated, then open the new terminal - // as an elevated instance (using _OpenElevatedWT). Does nothing if we're - // already elevated, or if the control settings don't want to be elevated. - // Arguments: - // - newTerminalArgs: The NewTerminalArgs for this terminal instance - // - controlSettings: The constructed TerminalSettingsCreateResult for this Terminal instance - // - profile: The Profile we're using to launch this Terminal instance - // Return Value: - // - true iff we tossed this request to an elevated window. Callers can use - // this result to early-return if needed. - bool TerminalPage::_maybeElevate(const NewTerminalArgs& newTerminalArgs, - const Settings::TerminalSettingsCreateResult& controlSettings, - const Profile& profile) - { - // When duplicating a tab there aren't any newTerminalArgs. - if (!newTerminalArgs) - { - return false; - } - - const auto defaultSettings = controlSettings.DefaultSettings(); - - // If we don't even want to elevate we can return early. - // If we're already elevated we can also return, because it doesn't get any more elevated than that. - if (!defaultSettings->Elevate() || IsRunningElevated()) - { - return false; - } - - // Manually set the Profile of the NewTerminalArgs to the guid we've - // resolved to. If there was a profile in the NewTerminalArgs, this - // will be that profile's GUID. If there wasn't, then we'll use - // whatever the default profile's GUID is. - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); - newTerminalArgs.StartingDirectory(_evaluatePathForCwd(defaultSettings->StartingDirectory())); - _OpenElevatedWT(newTerminalArgs); - return true; - } - - // Method Description: - // - Handles the change of connection state. - // If the connection state is failure show information bar suggesting to configure termination behavior - // (unless user asked not to show this message again) - // Arguments: - // - sender: the ICoreState instance containing the connection state - // Return Value: - // - - safe_void_coroutine TerminalPage::_ConnectionStateChangedHandler(const IInspectable& sender, const IInspectable& /*args*/) - { - if (const auto coreState{ sender.try_as() }) - { - const auto newConnectionState = coreState.ConnectionState(); - const auto weak = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - _adjustProcessPriorityThrottled->Run(); - - if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) - { - if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) - { - infoBar.IsOpen(true); - } - } - } - } - - // Method Description: - // - Persists the user's choice not to show information bar guiding to configure termination behavior. - // Then hides this information buffer. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_CloseOnExitInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const - { - _DismissMessage(InfoBarMessage::CloseOnExitInfo); - if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) - { - infoBar.IsOpen(false); - } - } - - // Method Description: - // - Persists the user's choice not to show information bar warning about "Touch keyboard and Handwriting Panel Service" disabled - // Then hides this information buffer. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_KeyboardServiceWarningInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const - { - _DismissMessage(InfoBarMessage::KeyboardServiceWarning); - if (const auto infoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) - { - infoBar.IsOpen(false); - } - } - - // Method Description: - // - Checks whether information bar message was dismissed earlier (in the application state) - // Arguments: - // - message: message to look for in the state - // Return Value: - // - true, if the message was dismissed - bool TerminalPage::_IsMessageDismissed(const InfoBarMessage& message) - { - if (const auto dismissedMessages{ ApplicationState::SharedInstance().DismissedMessages() }) - { - for (const auto& dismissedMessage : dismissedMessages) - { - if (dismissedMessage == message) - { - return true; - } - } - } - return false; - } - - // Method Description: - // - Persists the user's choice to dismiss information bar message (in application state) - // Arguments: - // - message: message to dismiss - // Return Value: - // - - void TerminalPage::_DismissMessage(const InfoBarMessage& message) - { - const auto applicationState = ApplicationState::SharedInstance(); - std::vector messages; - - if (const auto values = applicationState.DismissedMessages()) - { - messages.resize(values.Size()); - values.GetMany(0, messages); - } - - if (std::none_of(messages.begin(), messages.end(), [&](const auto& m) { return m == message; })) - { - messages.emplace_back(message); - } - - applicationState.DismissedMessages(std::move(messages)); - } - - void TerminalPage::_updateThemeColors() - { - if (_settings == nullptr) - { - return; - } - - const auto theme = _settings.GlobalSettings().CurrentTheme(); - auto requestedTheme{ theme.RequestedTheme() }; - - { - _updatePaneResources(requestedTheme); - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // The root pane will propagate the theme change to all its children. - if (const auto& rootPane{ tabImpl->GetRootPane() }) - { - rootPane->UpdateResources(_paneResources); - } - } - } - } - - const auto res = Application::Current().Resources(); - - // Use our helper to lookup the theme-aware version of the resource. - const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); - const auto backgroundSolidBrush = ThemeLookup(res, requestedTheme, tabViewBackgroundKey).as(); - - til::color bgColor = backgroundSolidBrush.Color(); - - Media::Brush terminalBrush{ nullptr }; - if (const auto tab{ _GetFocusedTabImpl() }) - { - if (const auto& pane{ tab->GetActivePane() }) - { - if (const auto& lastContent{ pane->GetLastFocusedContent() }) - { - terminalBrush = lastContent.BackgroundBrush(); - } - } - } - - // GH#19604: Get the theme's tabRow color to use as the acrylic tint. - const auto tabRowBg{ theme.TabRow() ? (_activated ? theme.TabRow().Background() : - theme.TabRow().UnfocusedBackground()) : - ThemeColor{ nullptr } }; - - if (_settings.GlobalSettings().UseAcrylicInTabRow() && (_activated || _settings.GlobalSettings().EnableUnfocusedAcrylic())) - { - if (tabRowBg) - { - bgColor = ThemeColor::ColorFromBrush(tabRowBg.Evaluate(res, terminalBrush, true)); - } - - const auto acrylicBrush = Media::AcrylicBrush(); - acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); - acrylicBrush.FallbackColor(bgColor); - acrylicBrush.TintColor(bgColor); - acrylicBrush.TintOpacity(0.5); - - TitlebarBrush(acrylicBrush); - } - else if (tabRowBg) - { - const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) }; - bgColor = ThemeColor::ColorFromBrush(themeBrush); - // If the tab content returned nullptr for the terminalBrush, we - // _don't_ want to use it as the tab row background. We want to just - // use the default tab row background. - TitlebarBrush(themeBrush ? themeBrush : backgroundSolidBrush); - } - else - { - // Nothing was set in the theme - fall back to our original `TabViewBackground` color. - TitlebarBrush(backgroundSolidBrush); - } - - if (!_settings.GlobalSettings().ShowTabsInTitlebar()) - { - _tabRow.Background(TitlebarBrush()); - } - - // Second: Update the colors of our individual TabViewItems. This - // applies tab.background to the tabs via Tab::ThemeColor. - // - // Do this second, so that we already know the bgColor of the titlebar. - { - const auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; - const auto tabUnfocusedBackground = theme.Tab() ? theme.Tab().UnfocusedBackground() : nullptr; - for (const auto& tab : _tabs) - { - winrt::com_ptr tabImpl; - tabImpl.copy_from(winrt::get_self(tab)); - tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor); - } - } - // Update the new tab button to have better contrast with the new color. - // In theory, it would be convenient to also change these for the - // inactive tabs as well, but we're leaving that as a follow up. - _SetNewTabButtonColor(bgColor, bgColor); - - // Third: the window frame. This is basically the same logic as the tab row background. - // We'll set our `FrameBrush` property, for the window to later use. - const auto windowTheme{ theme.Window() }; - if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() : - windowTheme.UnfocusedFrame()) : - ThemeColor{ nullptr } }) - { - const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) }; - FrameBrush(themeBrush); - } - else - { - // Nothing was set in the theme - fall back to null. The window will - // use that as an indication to use the default window frame. - FrameBrush(nullptr); - } - } - - // Function Description: - // - Attempts to load some XAML resources that Panes will need. This includes: - // * The Color they'll use for active Panes's borders - SystemAccentColor - // * The Brush they'll use for inactive Panes - TabViewBackground (to match the - // color of the titlebar) - // Arguments: - // - requestedTheme: this should be the currently active Theme for the app - // Return Value: - // - - void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) - { - const auto res = Application::Current().Resources(); - const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); - if (res.HasKey(accentColorKey)) - { - const auto colorFromResources = ThemeLookup(res, requestedTheme, 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(colorFromResources, Colors::Black()); - _paneResources.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. - _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; - } - - const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); - if (res.HasKey(unfocusedBorderBrushKey)) - { - // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for - // the requestedTheme, not just the value from the resources (which - // might not respect the settings' requested theme) - auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); - _paneResources.unfocusedBorderBrush = obj.try_as(); - } - 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. - _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; - } - - const auto broadcastColorKey = winrt::box_value(L"BroadcastPaneBorderColor"); - if (res.HasKey(broadcastColorKey)) - { - // MAKE SURE TO USE ThemeLookup - auto obj = ThemeLookup(res, requestedTheme, broadcastColorKey); - _paneResources.broadcastBorderBrush = obj.try_as(); - } - 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. - _paneResources.broadcastBorderBrush = SolidColorBrush{ Colors::Black() }; - } - } - - void TerminalPage::_adjustProcessPriority() const - { - // Windowing is single-threaded, so this will not cause a race condition. - static uint64_t s_lastUpdateHash{ 0 }; - static bool s_supported{ true }; - - if (!s_supported || !_hostingHwnd.has_value()) - { - return; - } - - std::array processes; - auto it = processes.begin(); - const auto end = processes.end(); - - auto&& appendFromControl = [&](auto&& control) { - if (it == end) - { - return; - } - if (control) - { - if (const auto conn{ control.Connection() }) - { - if (const auto pty{ conn.try_as() }) - { - if (const uint64_t process{ pty.RootProcessHandle() }; process != 0) - { - *it++ = reinterpret_cast(process); - } - } - } - } - }; - - auto&& appendFromTab = [&](auto&& tabImpl) { - if (const auto pane{ tabImpl->GetRootPane() }) - { - pane->WalkTree([&](auto&& child) { - if (const auto& control{ child->GetTerminalControl() }) - { - appendFromControl(control); - } - }); - } - }; - - if (!_activated) - { - // When a window is out of focus, we want to attach all of the processes - // under it to the window so they all go into the background at the same time. - for (auto&& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - appendFromTab(tabImpl); - } - } - } - else - { - // When a window is in focus, propagate our foreground boost (if we have one) - // to current all panes in the current tab. - if (auto tabImpl{ _GetFocusedTabImpl() }) - { - appendFromTab(tabImpl); - } - } - - const auto count{ gsl::narrow_cast(it - processes.begin()) }; - const auto hash = til::hash((void*)processes.data(), count * sizeof(HANDLE)); - - if (hash == s_lastUpdateHash) - { - return; - } - - s_lastUpdateHash = hash; - const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); - - if (S_FALSE == hr) - { - // Don't bother trying again or logging. The wrapper tells us it's unsupported. - s_supported = false; - return; - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "CalledNewQoSAPI", - TraceLoggingValue(reinterpret_cast(_hostingHwnd.value()), "hwnd"), - TraceLoggingValue(count), - TraceLoggingHResult(hr)); -#ifdef _DEBUG - OutputDebugStringW(fmt::format(FMT_COMPILE(L"Submitted {} processes to TerminalTrySetWindowAssociatedProcesses; return=0x{:08x}\n"), count, hr).c_str()); -#endif - } - - void TerminalPage::WindowActivated(const bool activated) - { - // Stash if we're activated. Use that when we reload - // the settings, change active panes, etc. - _activated = activated; - _updateThemeColors(); - - _adjustProcessPriorityThrottled->Run(); - - if (const auto& tab{ _GetFocusedTabImpl() }) - { - if (tab->TabStatus().IsInputBroadcastActive()) - { - tab->GetRootPane()->WalkTree([activated](const auto& p) { - if (const auto& control{ p->GetTerminalControl() }) - { - control.CursorVisibility(activated ? - Microsoft::Terminal::Control::CursorDisplayState::Shown : - Microsoft::Terminal::Control::CursorDisplayState::Default); - } - }); - } - } - } - - safe_void_coroutine TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, - const CompletionsChangedEventArgs args) - { - // This won't even get hit if the velocity flag is disabled - we gate - // registering for the event based off of - // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents - - // User must explicitly opt-in on Preview builds - if (!_settings.GlobalSettings().EnableShellCompletionMenu()) - { - co_return; - } - - // Parse the json string into a collection of actions - try - { - auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), - args.ReplacementLength()); - - auto weakThis{ get_weak() }; - Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { - // On the UI thread... - if (const auto& page{ weakThis.get() }) - { - // Open the Suggestions UI with the commands from the control - page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu, L""); - } - }); - } - CATCH_LOG(); - } - - void TerminalPage::_OpenSuggestions( - const TermControl& sender, - IVector commandsCollection, - winrt::TerminalApp::SuggestionsMode mode, - winrt::hstring filterText) - - { - // ON THE UI THREAD - assert(Dispatcher().HasThreadAccess()); - - if (commandsCollection == nullptr) - { - return; - } - if (commandsCollection.Size() == 0) - { - if (const auto p = SuggestionsElement()) - { - p.Visibility(Visibility::Collapsed); - } - return; - } - - const auto& control{ sender ? sender : _GetActiveControl() }; - if (!control) - { - return; - } - - const auto& sxnUi{ LoadSuggestionsUI() }; - - const auto characterSize{ control.CharacterDimensions() }; - // This is in control-relative space. We'll need to convert it to page-relative space. - const auto cursorPos{ control.CursorPositionInDips() }; - const auto controlTransform = control.TransformToVisual(this->Root()); - const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos - const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; - - sxnUi.Open(mode, - commandsCollection, - filterText, - realCursorPos, - windowDimensions, - characterSize.Height); - } - - void TerminalPage::_PopulateContextMenu(const TermControl& control, - const MUX::Controls::CommandBarFlyout& menu, - const bool withSelection) - { - // withSelection can be used to add actions that only appear if there's - // selected text, like "search the web" - - if (!control || !menu) - { - return; - } - - // Helper lambda for dispatching an ActionAndArgs onto the - // ShortcutActionDispatch. Used below to wire up each menu entry to the - // respective action. - - auto weak = get_weak(); - auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { - return [weak, actionAndArgs](auto&&, auto&&) { - if (auto page{ weak.get() }) - { - page->_actionDispatch->DoAction(actionAndArgs); - } - }; - }; - - auto makeItem = [&makeCallback](const winrt::hstring& label, - const winrt::hstring& icon, - const auto& action, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Click(makeCallback(action)); - targetMenu.SecondaryCommands().Append(button); - }; - - auto makeMenuItem = [](const winrt::hstring& label, - const winrt::hstring& icon, - const auto& subMenu, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Flyout(subMenu); - targetMenu.SecondaryCommands().Append(button); - }; - - auto makeContextItem = [&makeCallback](const winrt::hstring& label, - const winrt::hstring& icon, - const winrt::hstring& tooltip, - const auto& action, - const auto& subMenu, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Click(makeCallback(action)); - WUX::Controls::ToolTipService::SetToolTip(button, box_value(tooltip)); - button.ContextFlyout(subMenu); - targetMenu.SecondaryCommands().Append(button); - }; - - const auto focusedProfile = _GetFocusedTabImpl()->GetFocusedProfile(); - auto separatorItem = AppBarSeparator{}; - auto activeProfiles = _settings.ActiveProfiles(); - auto activeProfileCount = gsl::narrow_cast(activeProfiles.Size()); - MUX::Controls::CommandBarFlyout splitPaneMenu{}; - - // Wire up each item to the action that should be performed. By actually - // connecting these to actions, we ensure the implementation is - // consistent. This also leaves room for customizing this menu with - // actions in the future. - - makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }, menu); - - const auto focusedProfileName = focusedProfile.Name(); - const auto focusedProfileIcon = focusedProfile.Icon().Resolved(); - const auto splitPaneDuplicateText = RS_(L"SplitPaneDuplicateText") + L" " + focusedProfileName; // SplitPaneDuplicateText - - const auto splitPaneRightText = RS_(L"SplitPaneRightText"); - const auto splitPaneDownText = RS_(L"SplitPaneDownText"); - const auto splitPaneUpText = RS_(L"SplitPaneUpText"); - const auto splitPaneLeftText = RS_(L"SplitPaneLeftText"); - const auto splitPaneToolTipText = RS_(L"SplitPaneToolTipText"); - - MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; - makeItem(splitPaneRightText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Right, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneDownText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Down, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneUpText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Up, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneLeftText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Left, .5, nullptr } }, splitPaneContextMenu); - - makeContextItem(splitPaneDuplicateText, focusedProfileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Automatic, .5, nullptr } }, splitPaneContextMenu, splitPaneMenu); - - // add menu separator - const auto separatorAutoItem = AppBarSeparator{}; - - splitPaneMenu.SecondaryCommands().Append(separatorAutoItem); - - for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) - { - const auto profile = activeProfiles.GetAt(profileIndex); - const auto profileName = profile.Name(); - const auto profileIcon = profile.Icon().Resolved(); - - NewTerminalArgs args{}; - args.Profile(profileName); - - MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; - makeItem(splitPaneRightText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Right, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneDownText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Down, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneUpText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Up, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneLeftText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Left, .5, args } }, splitPaneContextMenu); - - makeContextItem(profileName, profileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Automatic, .5, args } }, splitPaneContextMenu, splitPaneMenu); - } - - makeMenuItem(RS_(L"SplitPaneText"), L"\xF246", splitPaneMenu, menu); - - // Only wire up "Close Pane" if there's multiple panes. - if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) - { - MUX::Controls::CommandBarFlyout swapPaneMenu{}; - const auto rootPane = _GetFocusedTabImpl()->GetRootPane(); - const auto mruPanes = _GetFocusedTabImpl()->GetMruPanes(); - auto activePane = _GetFocusedTabImpl()->GetActivePane(); - rootPane->WalkTree([&](auto p) { - if (const auto& c{ p->GetTerminalControl() }) - { - if (c == control) - { - activePane = p; - } - } - }); - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Down, mruPanes)) - { - makeItem(RS_(L"SwapPaneDownText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Down } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Right, mruPanes)) - { - makeItem(RS_(L"SwapPaneRightText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Right } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Up, mruPanes)) - { - makeItem(RS_(L"SwapPaneUpText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Up } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Left, mruPanes)) - { - makeItem(RS_(L"SwapPaneLeftText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Left } }, swapPaneMenu); - } - - makeMenuItem(RS_(L"SwapPaneText"), L"\xF1CB", swapPaneMenu, menu); - - makeItem(RS_(L"TogglePaneZoomText"), L"\xE8A3", ActionAndArgs{ ShortcutAction::TogglePaneZoom, nullptr }, menu); - makeItem(RS_(L"CloseOtherPanesText"), L"\xE89F", ActionAndArgs{ ShortcutAction::CloseOtherPanes, nullptr }, menu); - makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }, menu); - } - - if (control.ConnectionState() >= ConnectionState::Closed) - { - makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }, menu); - } - - if (withSelection) - { - makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }, menu); - } - - makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }, menu); - } - - void TerminalPage::_PopulateQuickFixMenu(const TermControl& control, - const Controls::MenuFlyout& menu) - { - if (!control || !menu) - { - return; - } - - // Helper lambda for dispatching a SendInput ActionAndArgs onto the - // ShortcutActionDispatch. Used below to wire up each menu entry to the - // respective action. Then clear the quick fix menu. - auto weak = get_weak(); - auto makeCallback = [weak](const hstring& suggestion) { - return [weak, suggestion](auto&&, auto&&) { - if (auto page{ weak.get() }) - { - const auto actionAndArgs = ActionAndArgs{ ShortcutAction::SendInput, SendInputArgs{ hstring{ L"\u0003" } + suggestion } }; - page->_actionDispatch->DoAction(actionAndArgs); - if (auto ctrl = page->_GetActiveControl()) - { - ctrl.ClearQuickFix(); - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "QuickFixSuggestionUsed", - TraceLoggingDescription("Event emitted when a winget suggestion from is used"), - TraceLoggingValue("QuickFixMenu", "Source"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }; - }; - - // Wire up each item to the action that should be performed. By actually - // connecting these to actions, we ensure the implementation is - // consistent. This also leaves room for customizing this menu with - // actions in the future. - - menu.Items().Clear(); - const auto quickFixes = control.CommandHistory().QuickFixes(); - for (const auto& qf : quickFixes) - { - MenuFlyoutItem item{}; - - auto iconElement = UI::IconPathConverter::IconWUX(L"\ue74c"); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - item.Icon(iconElement); - - item.Text(qf); - item.Click(makeCallback(qf)); - ToolTipService::SetToolTip(item, box_value(qf)); - menu.Items().Append(item); - } - } - - // Handler for our WindowProperties's PropertyChanged event. We'll use this - // to pop the "Identify Window" toast when the user renames our window. - void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) - { - if (args.PropertyName() != L"WindowName") - { - return; - } - - // DON'T display the confirmation if this is the name we were - // given on startup! - if (_startupState == StartupState::Initialized) - { - IdentifyWindow(); - } - } - - void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, - const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) - { - const auto eventTab = e.Tab(); - const auto draggedTab = _GetTabByTabViewItem(eventTab); - if (draggedTab) - { - auto draggedTabs = _IsTabSelected(draggedTab) ? _GetSelectedTabsInDisplayOrder() : - std::vector{}; - if (draggedTabs.empty() || - !std::ranges::any_of(draggedTabs, [&](const auto& tab) { return tab == draggedTab; })) - { - draggedTabs = { draggedTab }; - _SetSelectedTabs(draggedTabs, draggedTab); - } - - _stashed.draggedTabs = std::move(draggedTabs); - _stashed.dragAnchor = draggedTab; - - // Stash the offset from where we started the drag to the - // tab's origin. We'll use that offset in the future to help - // position the dropped window. - const auto inverseScale = 1.0f / static_cast(eventTab.XamlRoot().RasterizationScale()); - POINT cursorPos; - GetCursorPos(&cursorPos); - ScreenToClient(*_hostingHwnd, &cursorPos); - _stashed.dragOffset.X = cursorPos.x * inverseScale; - _stashed.dragOffset.Y = cursorPos.y * inverseScale; - - // Into the DataPackage, let's stash our own window ID. - const auto id{ _WindowProperties.WindowId() }; - - // Get our PID - const auto pid{ GetCurrentProcessId() }; - - e.Data().Properties().Insert(L"windowId", winrt::box_value(id)); - e.Data().Properties().Insert(L"pid", winrt::box_value(pid)); - e.Data().RequestedOperation(DataPackageOperation::Move); - - // The next thing that will happen: - // * Another TerminalPage will get a TabStripDragOver, then get a - // TabStripDrop - // * This will be handled by the _other_ page asking the monarch - // to ask us to send our content to them. - // * We'll get a TabDroppedOutside to indicate that this tab was - // dropped _not_ on a TabView. - // * This will be handled by _onTabDroppedOutside, which will - // raise a MoveContent (to a new window) event. - } - } - - void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::UI::Xaml::DragEventArgs& e) - { - // We must mark that we can accept the drag/drop. The system will never - // call TabStripDrop on us if we don't indicate that we're willing. - const auto& props{ e.DataView().Properties() }; - if (props.HasKey(L"windowId") && - props.HasKey(L"pid") && - (winrt::unbox_value_or(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId())) - { - e.AcceptedOperation(DataPackageOperation::Move); - } - - // You may think to yourself, this is a great place to increase the - // width of the TabView artificially, to make room for the new tab item. - // However, we'll never get a message that the tab left the tab view - // (without being dropped). So there's no good way to resize back down. - } - - // Method Description: - // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage - // to find who the tab came from. We'll then ask the Monarch to ask the - // sender to move that tab to us. - void TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/, - winrt::Windows::UI::Xaml::DragEventArgs e) - { - // Get the PID and make sure it is the same as ours. - if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") }) - { - const auto pid{ winrt::unbox_value_or(pidObj, 0u) }; - if (pid != GetCurrentProcessId()) - { - // The PID doesn't match ours. We can't handle this drop. - return; - } - } - else - { - // No PID? We can't handle this drop. Bail. - return; - } - - const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") }; - if (windowIdObj == nullptr) - { - // No windowId? Bail. - return; - } - const uint64_t src{ winrt::unbox_value(windowIdObj) }; - - // Figure out where in the tab strip we're dropping this tab. Add that - // index to the request. This is largely taken from the WinUI sample - // app. - - // First we need to get the position in the List to drop to - auto index = -1; - - // Determine which items in the list our pointer is between. - for (auto i = 0u; i < _tabView.TabItems().Size(); i++) - { - if (const auto& item{ _tabView.ContainerFromIndex(i).try_as() }) - { - const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab - const auto itemWidth{ item.ActualWidth() }; // The right of the tab - // If the drag point is on the left half of the tab, then insert here. - if (posX < itemWidth / 2) - { - index = i; - break; - } - } - } - - if (index < 0) - { - index = gsl::narrow_cast(_tabView.TabItems().Size()); - } - - // `this` is safe to use - const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); - - // This will go up to the monarch, who will then dispatch the request - // back down to the source TerminalPage, who will then perform a - // RequestMoveContent to move their tab to us. - RequestReceiveContent.raise(*this, *request); - } - - // Method Description: - // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has - // requested that we send our tab to another window. We'll need to - // serialize the tab, and send it to the monarch, who will then send it to - // the destination window. - // - Fortunately, sending the tab is basically just a MoveTab action, so we - // can largely reuse that. - void TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) - { - // validate that we're the source window of the tab in this request - if (args.SourceWindow() != _WindowProperties.WindowId()) - { - return; - } - if (_stashed.draggedTabs.empty()) - { - return; - } - - _sendDraggedTabsToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); - } - - void TerminalPage::_onTabDroppedOutside(winrt::IInspectable /*sender*/, - winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs /*e*/) - { - // Get the current pointer point from the CoreWindow - const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() }; - - // This is called when a tab FROM OUR WINDOW was dropped outside the - // tabview. We already know which tab was being dragged. We'll just - // invoke a moveTab action with the target window being -1. That will - // force the creation of a new window. - - if (_stashed.draggedTabs.empty()) - { - return; - } - - // We need to convert the pointer point to a point that we can use - // to position the new window. We'll use the drag offset from before - // so that the tab in the new window is positioned so that it's - // basically still directly under the cursor. - - // -1 is the magic number for "new window" - // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab. - const winrt::Windows::Foundation::Point adjusted = { - pointerPoint.X - _stashed.dragOffset.X, - pointerPoint.Y - _stashed.dragOffset.Y, - }; - _sendDraggedTabsToWindow(winrt::hstring{ L"-1" }, 0, adjusted); - } - - void TerminalPage::_sendDraggedTabsToWindow(const winrt::hstring& windowId, - const uint32_t tabIndex, - std::optional dragPoint) - { - if (_stashed.draggedTabs.empty()) - { - return; - } - - auto draggedTabs = _stashed.draggedTabs; - auto startupActions = _BuildStartupActionsForTabs(draggedTabs); - if (dragPoint.has_value() && draggedTabs.size() > 1 && _stashed.dragAnchor) - { - const auto draggedAnchorIt = std::ranges::find_if(draggedTabs, [&](const auto& tab) { - return tab == _stashed.dragAnchor; - }); - if (draggedAnchorIt != draggedTabs.end()) - { - ActionAndArgs switchToTabAction{}; - switchToTabAction.Action(ShortcutAction::SwitchToTab); - switchToTabAction.Args(SwitchToTabArgs{ gsl::narrow_cast(std::distance(draggedTabs.begin(), draggedAnchorIt)) }); - startupActions.emplace_back(std::move(switchToTabAction)); - } - } - - for (const auto& tab : draggedTabs) - { - if (const auto tabImpl{ _GetTabImpl(tab) }) - { - _DetachTabFromWindow(tabImpl); - } - } - - _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); - - for (auto it = draggedTabs.rbegin(); it != draggedTabs.rend(); ++it) - { - _RemoveTab(*it); - } - } - - /// - /// Creates a sub flyout menu for profile items in the split button menu that when clicked will show a menu item for - /// Run as Administrator - /// - /// The index for the profileMenuItem - /// MenuFlyout that will show when the context is request on a profileMenuItem - WUX::Controls::MenuFlyout TerminalPage::_CreateRunAsAdminFlyout(int profileIndex) - { - // Create the MenuFlyout and set its placement - WUX::Controls::MenuFlyout profileMenuItemFlyout{}; - profileMenuItemFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight); - - // Create the menu item and an icon to use in the menu - WUX::Controls::MenuFlyoutItem runAsAdminItem{}; - WUX::Controls::FontIcon adminShieldIcon{}; - - adminShieldIcon.Glyph(L"\xEA18"); - adminShieldIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); - - runAsAdminItem.Icon(adminShieldIcon); - runAsAdminItem.Text(RS_(L"RunAsAdminFlyout/Text")); - - // Click handler for the flyout item - runAsAdminItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemElevateSubmenuItemClicked", - TraceLoggingDescription("Event emitted when the elevate submenu item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - NewTerminalArgs args{ profileIndex }; - args.Elevate(true); - page->_OpenNewTerminalViaDropdown(args); - } - }); - - profileMenuItemFlyout.Items().Append(runAsAdminItem); - - return profileMenuItemFlyout; - } -} + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalPage.h" + +#include +#include +#include +#include +#include + +#include "../../types/inc/ColorFix.hpp" +#include "../../types/inc/utils.hpp" +#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" +#include "App.h" +#include "DebugTapConnection.h" +#include "MarkdownPaneContent.h" +#include "Remoting.h" +#include "ScratchpadContent.h" +#include "SettingsPaneContent.h" +#include "SnippetsPaneContent.h" +#include "TabRowControl.h" +#include "TerminalSettingsCache.h" + +#include "LaunchPositionRequest.g.cpp" +#include "RenameWindowRequestedArgs.g.cpp" +#include "RequestMoveContentArgs.g.cpp" +#include "TerminalPage.g.cpp" + +using namespace winrt; +using namespace winrt::Microsoft::Management::Deployment; +using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalConnection; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Text; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Media; +using namespace ::TerminalApp; +using namespace ::Microsoft::Console; +using namespace ::Microsoft::Terminal::Core; +using namespace std::chrono_literals; + +#define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action }); + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; + using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers; +} + +namespace clipboard +{ + static SRWLOCK lock = SRWLOCK_INIT; + + struct ClipboardHandle + { + explicit ClipboardHandle(bool open) : + _open{ open } + { + } + + ~ClipboardHandle() + { + if (_open) + { + ReleaseSRWLockExclusive(&lock); + CloseClipboard(); + } + } + + explicit operator bool() const noexcept + { + return _open; + } + + private: + bool _open = false; + }; + + ClipboardHandle open(HWND hwnd) + { + // Turns out, OpenClipboard/CloseClipboard are not thread-safe whatsoever, + // and on CloseClipboard, the GetClipboardData handle may get freed. + // The problem is that WinUI also uses OpenClipboard (through WinRT which uses OLE), + // and so even with this mutex we can still crash randomly if you copy something via WinUI. + // Makes you wonder how many Windows apps are subtly broken, huh. + AcquireSRWLockExclusive(&lock); + + bool success = false; + + // OpenClipboard may fail to acquire the internal lock --> retry. + for (DWORD sleep = 10;; sleep *= 2) + { + if (OpenClipboard(hwnd)) + { + success = true; + break; + } + // 10 iterations + if (sleep > 10000) + { + break; + } + Sleep(sleep); + } + + if (!success) + { + ReleaseSRWLockExclusive(&lock); + } + + return ClipboardHandle{ success }; + } + + void write(wil::zwstring_view text, std::string_view html, std::string_view rtf) + { + static const auto regular = [](const UINT format, const void* src, const size_t bytes) { + wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) }; + + const auto locked = GlobalLock(handle.get()); + memcpy(locked, src, bytes); + GlobalUnlock(handle.get()); + + THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get())); + handle.release(); + }; + static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) { + const auto id = RegisterClipboardFormatW(format); + if (!id) + { + LOG_LAST_ERROR(); + return; + } + regular(id, src, bytes); + }; + + EmptyClipboard(); + + if (!text.empty()) + { + // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + // CF_UNICODETEXT: [...] A null character signals the end of the data. + // --> We add +1 to the length. This works because .c_str() is null-terminated. + regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); + } + + if (!html.empty()) + { + registered(L"HTML Format", html.data(), html.size()); + } + + if (!rtf.empty()) + { + registered(L"Rich Text Format", rtf.data(), rtf.size()); + } + } + + winrt::hstring read() + { + // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically. + if (const auto handle = GetClipboardData(CF_UNICODETEXT)) + { + const wil::unique_hglobal_locked lock{ handle }; + const auto str = static_cast(lock.get()); + if (!str) + { + return {}; + } + + const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); + const auto len = wcsnlen(str, maxLen); + return winrt::hstring{ str, gsl::narrow_cast(len) }; + } + + // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others). + if (const auto handle = GetClipboardData(CF_HDROP)) + { + const wil::unique_hglobal_locked lock{ handle }; + const auto drop = static_cast(lock.get()); + if (!drop) + { + return {}; + } + + const auto cap = DragQueryFileW(drop, 0, nullptr, 0); + if (cap == 0) + { + return {}; + } + + auto buffer = winrt::impl::hstring_builder{ cap }; + const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); + if (len == 0) + { + return {}; + } + + return buffer.to_hstring(); + } + + return {}; + } +} // namespace clipboard + +namespace +{ + std::wstring_view _uiaDragLogPath() noexcept + { + static const auto path = []() { + std::wstring buffer(MAX_PATH, L'\0'); + const auto length = GetEnvironmentVariableW(L"WT_UIA_DRAG_LOG", buffer.data(), gsl::narrow_cast(buffer.size())); + if (length == 0 || length >= buffer.size()) + { + return std::wstring{}; + } + + buffer.resize(length); + return buffer; + }(); + + return path; + } + + void _appendUiaDragLog(const std::wstring& message) noexcept + { + const auto path = _uiaDragLogPath(); + if (path.empty()) + { + return; + } + + const wil::unique_hfile file{ CreateFileW(path.data(), + FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr) }; + if (!file) + { + return; + } + + SYSTEMTIME timestamp{}; + GetLocalTime(×tamp); + + wchar_t buffer[256]; + const auto written = swprintf_s(buffer, + L"%02u:%02u:%02u.%03u %s\r\n", + timestamp.wHour, + timestamp.wMinute, + timestamp.wSecond, + timestamp.wMilliseconds, + message.c_str()); + if (written <= 0) + { + return; + } + + const auto utf8 = winrt::to_string(std::wstring_view{ buffer, gsl::narrow_cast(written) }); + DWORD bytesWritten = 0; + WriteFile(file.get(), utf8.data(), gsl::narrow_cast(utf8.size()), &bytesWritten, nullptr); + } +} + +namespace winrt::TerminalApp::implementation +{ + TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : + _tabs{ winrt::single_threaded_observable_vector() }, + _mruTabs{ winrt::single_threaded_observable_vector() }, + _manager{ manager }, + _hostingHwnd{}, + _WindowProperties{ std::move(properties) } + { + InitializeComponent(); + _WindowProperties.PropertyChanged({ get_weak(), &TerminalPage::_windowPropertyChanged }); + } + + // Method Description: + // - implements the IInitializeWithWindow interface from shobjidl_core. + // - We're going to use this HWND as the owner for the ConPTY windows, via + // ConptyConnection::ReparentWindow. We need this for applications that + // call GetConsoleWindow, and attempt to open a MessageBox for the + // console. By marking the conpty windows as owned by the Terminal HWND, + // the message box will be owned by the Terminal window as well. + // - see GH#2988 + HRESULT TerminalPage::Initialize(HWND hwnd) + { + if (!_hostingHwnd.has_value()) + { + // GH#13211 - if we haven't yet set the owning hwnd, reparent all the controls now. + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto& term{ pane->GetTerminalControl() }) + { + term.OwningHwnd(reinterpret_cast(hwnd)); + } + }); + } + // We don't need to worry about resetting the owning hwnd for the + // SUI here. GH#13211 only repros for a defterm connection, where + // the tab is spawned before the window is created. It's not + // possible to make a SUI tab like that, before the window is + // created. The SUI could be spawned as a part of a window restore, + // but that would still work fine. The window would be created + // before restoring previous tabs in that scenario. + } + } + + _hostingHwnd = hwnd; + return S_OK; + } + + // INVARIANT: This needs to be called on OUR UI thread! + void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) + { + assert(Dispatcher().HasThreadAccess()); + if (_settings == nullptr) + { + // Create this only on the first time we load the settings. + _terminalSettingsCache = std::make_shared(settings); + } + _settings = settings; + + // Make sure to call SetCommands before _RefreshUIForSettingsReload. + // SetCommands will make sure the KeyChordText of Commands is updated, which needs + // to happen before the Settings UI is reloaded and tries to re-read those values. + if (const auto p = CommandPaletteElement()) + { + p.SetActionMap(_settings.ActionMap()); + } + + if (needRefreshUI) + { + _RefreshUIForSettingsReload(); + } + + // Upon settings update we reload the system settings for scrolling as well. + // TODO: consider reloading this value periodically. + _systemRowsToScroll = _ReadSystemRowsToScroll(); + } + + bool TerminalPage::IsRunningElevated() const noexcept + { + // GH#2455 - Make sure to try/catch calls to Application::Current, + // because that _won't_ be an instance of TerminalApp::App in the + // LocalTests + try + { + return Application::Current().as().Logic().IsRunningElevated(); + } + CATCH_LOG(); + return false; + } + bool TerminalPage::CanDragDrop() const noexcept + { + try + { + return Application::Current().as().Logic().CanDragDrop(); + } + CATCH_LOG(); + return true; + } + + void TerminalPage::Create() + { + // Hookup the key bindings + _HookupKeyBindings(_settings.ActionMap()); + + _tabContent = this->TabContent(); + _tabRow = this->TabRow(); + _tabView = _tabRow.TabView(); + _rearranging = false; + + const auto canDragDrop = CanDragDrop(); + _appendUiaDragLog(std::wstring{ L"Create: canDragDrop=" } + (canDragDrop ? L"true" : L"false")); + + _tabView.CanReorderTabs(canDragDrop); + _tabView.CanDragTabs(canDragDrop); + _tabView.TabDragStarting({ get_weak(), &TerminalPage::_TabDragStarted }); + _tabView.TabDragCompleted({ get_weak(), &TerminalPage::_TabDragCompleted }); + + auto tabRowImpl = winrt::get_self(_tabRow); + _newTabButton = tabRowImpl->NewTabButton(); + + if (_settings.GlobalSettings().ShowTabsInTitlebar()) + { + // Remove the TabView from the page. We'll hang on to it, we need to + // put it in the titlebar. + uint32_t index = 0; + if (this->Root().Children().IndexOf(_tabRow, index)) + { + this->Root().Children().RemoveAt(index); + } + + // Inform the host that our titlebar content has changed. + SetTitleBarContent.raise(*this, _tabRow); + + // GH#13143 Manually set the tab row's background to transparent here. + // + // We're doing it this way because ThemeResources are tricky. We + // default in XAML to using the appropriate ThemeResource background + // color for our TabRow. When tabs in the titlebar are _disabled_, + // this will ensure that the tab row has the correct theme-dependent + // value. When tabs in the titlebar are _enabled_ (the default), + // we'll switch the BG to Transparent, to let the Titlebar Control's + // background be used as the BG for the tab row. + // + // We can't do it the other way around (default to Transparent, only + // switch to a color when disabling tabs in the titlebar), because + // looking up the correct ThemeResource from and App dictionary is a + // capital-H Hard problem. + const auto transparent = Media::SolidColorBrush(); + transparent.Color(Windows::UI::Colors::Transparent()); + _tabRow.Background(transparent); + } + _updateThemeColors(); + + // Initialize the state of the CloseButtonOverlayMode property of + // our TabView, to match the tab.showCloseButton property in the theme. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always; + + _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; + + switch (visibility) + { + case Settings::Model::TabCloseButtonVisibility::Never: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); + break; + case Settings::Model::TabCloseButtonVisibility::Hover: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); + break; + default: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); + break; + } + } + + // Hookup our event handlers to the ShortcutActionDispatch + _RegisterActionCallbacks(); + + //Event Bindings (Early) + _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuDefaultButtonClicked", + TraceLoggingDescription("Event emitted when the default button from the new tab split button is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); + } + }); + _newTabButton.Drop({ get_weak(), &TerminalPage::_NewTerminalByDrop }); + _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); + _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); + _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); + + _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); + _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); + _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop }); + _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside }); + + _CreateNewTabFlyout(); + + _UpdateTabWidthMode(); + + // Settings AllowDependentAnimations will affect whether animations are + // enabled application-wide, so we don't need to check it each time we + // want to create an animation. + WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + // Once the page is actually laid out on the screen, trigger all our + // startup actions. Things like Panes need to know at least how big the + // window will be, so they can subdivide that space. + // + // _OnFirstLayout will remove this handler so it doesn't get called more than once. + _layoutUpdatedRevoker = _tabContent.LayoutUpdated(winrt::auto_revoke, { this, &TerminalPage::_OnFirstLayout }); + + _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); + _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); + + // DON'T set up Toasts/TeachingTips here. They should be loaded and + // initialized the first time they're opened, in whatever method opens + // them. + + _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + + _adjustProcessPriorityThrottled = std::make_shared>( + DispatcherQueue::GetForCurrentThread(), + til::throttled_func_options{ + .delay = std::chrono::milliseconds{ 100 }, + .debounce = true, + .trailing = true, + }, + [=]() { + _adjustProcessPriority(); + }); + } + + Windows::UI::Xaml::Automation::Peers::AutomationPeer TerminalPage::OnCreateAutomationPeer() + { + return Automation::Peers::FrameworkElementAutomationPeer(*this); + } + + // Method Description: + // - This is a bit of trickiness: If we're running unelevated, and the user + // passed in only --elevate actions, the we don't _actually_ want to + // restore the layouts here. We're not _actually_ about to create the + // window. We're simply going to toss the commandlines + // Arguments: + // - + // Return Value: + // - true if we're not elevated but all relevant pane-spawning actions are elevated + bool TerminalPage::ShouldImmediatelyHandoffToElevated(const CascadiaSettings& settings) const + { + if (_startupActions.empty() || _startupConnection || IsRunningElevated()) + { + // No point in handing off if we got no startup actions, or we're already elevated. + // Also, we shouldn't need to elevate handoff ConPTY connections. + assert(!_startupConnection); + return false; + } + + // Check that there's at least one action that's not just an elevated newTab action. + for (const auto& action : _startupActions) + { + // Only new terminal panes will be requesting elevation. + NewTerminalArgs newTerminalArgs{ nullptr }; + + if (action.Action() == ShortcutAction::NewTab) + { + const auto& args{ action.Args().try_as() }; + if (args) + { + newTerminalArgs = args.ContentArgs().try_as(); + } + else + { + // This was a nt action that didn't have any args. The default + // profile may want to be elevated, so don't just early return. + } + } + else if (action.Action() == ShortcutAction::SplitPane) + { + const auto& args{ action.Args().try_as() }; + if (args) + { + newTerminalArgs = args.ContentArgs().try_as(); + } + else + { + // This was a nt action that didn't have any args. The default + // profile may want to be elevated, so don't just early return. + } + } + else + { + // This was not a new tab or split pane action. + // This doesn't affect the outcome + continue; + } + + // It's possible that newTerminalArgs is null here. + // GetProfileForArgs should be resilient to that. + const auto profile{ settings.GetProfileForArgs(newTerminalArgs) }; + if (profile.Elevate()) + { + continue; + } + + // The profile didn't want to be elevated, and we aren't elevated. + // We're going to open at least one tab, so return false. + return false; + } + return true; + } + + // Method Description: + // - Escape hatch for immediately dispatching requests to elevated windows + // when first launched. At this point in startup, the window doesn't exist + // yet, XAML hasn't been started, but we need to dispatch these actions. + // We can't just go through ProcessStartupActions, because that processes + // the actions async using the XAML dispatcher (which doesn't exist yet) + // - DON'T CALL THIS if you haven't already checked + // ShouldImmediatelyHandoffToElevated. If you're thinking about calling + // this outside of the one place it's used, that's probably the wrong + // solution. + // Arguments: + // - settings: the settings we should use for dispatching these actions. At + // this point in startup, we hadn't otherwise been initialized with these, + // so use them now. + // Return Value: + // - + void TerminalPage::HandoffToElevated(const CascadiaSettings& settings) + { + if (_startupActions.empty()) + { + return; + } + + // Hookup our event handlers to the ShortcutActionDispatch + _settings = settings; + _HookupKeyBindings(_settings.ActionMap()); + _RegisterActionCallbacks(); + + for (const auto& action : _startupActions) + { + // only process new tabs and split panes. They're all going to the elevated window anyways. + if (action.Action() == ShortcutAction::NewTab || action.Action() == ShortcutAction::SplitPane) + { + _actionDispatch->DoAction(action); + } + } + } + + safe_void_coroutine TerminalPage::_NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e) + try + { + const auto data = e.DataView(); + if (!data.Contains(StandardDataFormats::StorageItems())) + { + co_return; + } + + const auto weakThis = get_weak(); + const auto items = co_await data.GetStorageItemsAsync(); + const auto strongThis = weakThis.get(); + if (!strongThis) + { + co_return; + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabByDragDrop", + TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + for (const auto& item : items) + { + auto directory = item.Path(); + + std::filesystem::path path(std::wstring_view{ directory }); + if (!std::filesystem::is_directory(path)) + { + directory = winrt::hstring{ path.parent_path().native() }; + } + + NewTerminalArgs args; + args.StartingDirectory(directory); + _OpenNewTerminalViaDropdown(args); + } + } + CATCH_LOG() + + // Method Description: + // - This method is called once command palette action was chosen for dispatching + // We'll use this event to dispatch this command. + // Arguments: + // - command - command to dispatch + // Return Value: + // - + void TerminalPage::_OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command) + { + const auto& actionAndArgs = command.ActionAndArgs(); + _actionDispatch->DoAction(sender, actionAndArgs); + } + + // Method Description: + // - This method is called once command palette command line was chosen for execution + // We'll use this event to create a command line execution command and dispatch it. + // Arguments: + // - command - command to dispatch + // Return Value: + // - + void TerminalPage::_OnCommandLineExecutionRequested(const IInspectable& /*sender*/, const winrt::hstring& commandLine) + { + ExecuteCommandlineArgs args{ commandLine }; + ActionAndArgs actionAndArgs{ ShortcutAction::ExecuteCommandline, args }; + _actionDispatch->DoAction(actionAndArgs); + } + + // Method Description: + // - This method is called once on startup, on the first LayoutUpdated event. + // We'll use this event to know that we have an ActualWidth and + // ActualHeight, so we can now attempt to process our list of startup + // actions. + // - We'll remove this event handler when the event is first handled. + // - If there are no startup actions, we'll open a single tab with the + // default profile. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_OnFirstLayout(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) + { + // Only let this succeed once. + _layoutUpdatedRevoker.revoke(); + + // This event fires every time the layout changes, but it is always the + // last one to fire in any layout change chain. That gives us great + // flexibility in finding the right point at which to initialize our + // renderer (and our terminal). Any earlier than the last layout update + // and we may not know the terminal's starting size. + if (_startupState == StartupState::NotInitialized) + { + _startupState = StartupState::InStartup; + + if (_startupConnection) + { + CreateTabFromConnection(std::move(_startupConnection)); + } + else if (!_startupActions.empty()) + { + ProcessStartupActions(std::move(_startupActions)); + } + + _CompleteInitialization(); + } + } + + // Method Description: + // - Process all the startup actions in the provided list of startup + // actions. We'll do this all at once here. + // Arguments: + // - actions: a winrt vector of actions to process. Note that this must NOT + // be an IVector&, because we need the collection to be accessible on the + // other side of the co_await. + // - initial: if true, we're parsing these args during startup, and we + // should fire an Initialized event. + // - cwd: If not empty, we should try switching to this provided directory + // while processing these actions. This will allow something like `wt -w 0 + // nt -d .` from inside another directory to work as expected. + // Return Value: + // - + safe_void_coroutine TerminalPage::ProcessStartupActions(std::vector actions, const winrt::hstring cwd, const winrt::hstring env) + { + const auto strong = get_strong(); + + // If the caller provided a CWD, "switch" to that directory, then switch + // back once we're done. + auto originalVirtualCwd{ _WindowProperties.VirtualWorkingDirectory() }; + auto originalVirtualEnv{ _WindowProperties.VirtualEnvVars() }; + auto restoreCwd = wil::scope_exit([&]() { + if (!cwd.empty()) + { + // ignore errors, we'll just power on through. We'd rather do + // something rather than fail silently if the directory doesn't + // actually exist. + _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd); + _WindowProperties.VirtualEnvVars(originalVirtualEnv); + } + }); + if (!cwd.empty()) + { + _WindowProperties.VirtualWorkingDirectory(cwd); + _WindowProperties.VirtualEnvVars(env); + } + + // The current TerminalWindow & TerminalPage architecture is rather instable + // and fails to start up if the first tab isn't created synchronously. + // + // While that's a fair assumption in on itself, simultaneously WinUI will + // not assign tab contents a size if they're not shown at least once, + // which we need however in order to initialize ControlCore with a size. + // + // So, we do two things here: + // * DO NOT suspend if this is the first tab. + // * DO suspend between the creation of panes (or tabs) in order to allow + // WinUI to layout the new controls and for ControlCore to get a size. + // + // This same logic is also applied to CreateTabFromConnection. + // + // See GH#13136. + auto suspend = _tabs.Size() > 0; + + for (size_t i = 0; i < actions.size(); ++i) + { + if (suspend) + { + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); + } + + _actionDispatch->DoAction(actions[i]); + suspend = true; + } + + // GH#6586: now that we're done processing all startup commands, + // focus the active control. This will work as expected for both + // commandline invocations and for `wt` action invocations. + if (const auto& tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto& content{ tabImpl->GetActiveContent() }) + { + content.Focus(FocusState::Programmatic); + } + } + } + + safe_void_coroutine TerminalPage::CreateTabFromConnection(ITerminalConnection connection) + { + const auto strong = get_strong(); + + // This is the exact same logic as in ProcessStartupActions. + if (_tabs.Size() > 0) + { + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); + } + + NewTerminalArgs newTerminalArgs; + + if (const auto conpty = connection.try_as()) + { + newTerminalArgs.Commandline(conpty.Commandline()); + newTerminalArgs.TabTitle(conpty.StartingTitle()); + } + + // GH #12370: We absolutely cannot allow a defterm connection to + // auto-elevate. Defterm doesn't work for elevated scenarios in the + // first place. If we try accepting the connection, the spawning an + // elevated version of the Terminal with that profile... that's a + // recipe for disaster. We won't ever open up a tab in this window. + newTerminalArgs.Elevate(false); + + const auto newPane = _MakePane(newTerminalArgs, nullptr, std::move(connection)); + newPane->WalkTree([](const auto& pane) { + pane->FinalizeConfigurationGivenDefault(); + }); + _CreateNewTabFromPane(newPane); + } + + // Method Description: + // - Perform and steps that need to be done once our initial state is all + // set up. This includes entering fullscreen mode and firing our + // Initialized event. + // Arguments: + // - + // Return Value: + // - + safe_void_coroutine TerminalPage::_CompleteInitialization() + { + _startupState = StartupState::Initialized; + + // GH#632 - It's possible that the user tried to create the terminal + // with only one tab, with only an elevated profile. If that happens, + // we'll create _another_ process to host the elevated version of that + // profile. This can happen from the jumplist, or if the default profile + // is `elevate:true`, or from the commandline. + // + // However, we need to make sure to close this window in that scenario. + // Since there aren't any _tabs_ in this window, we won't ever get a + // closed event. So do it manually. + // + // GH#12267: Make sure that we don't instantly close ourselves when + // we're readying to accept a defterm connection. In that case, we don't + // have a tab yet, but will once we're initialized. + if (_tabs.Size() == 0) + { + CloseWindowRequested.raise(*this, nullptr); + co_return; + } + else + { + // GH#11561: When we start up, our window is initially just a frame + // with a transparent content area. We're gonna do all this startup + // init on the UI thread, so the UI won't actually paint till it's + // all done. This results in a few frames where the frame is + // visible, before the page paints for the first time, before any + // tabs appears, etc. + // + // To mitigate this, we're gonna wait for the UI thread to finish + // everything it's gotta do for the initial init, and _then_ fire + // our Initialized event. By waiting for everything else to finish + // (CoreDispatcherPriority::Low), we let all the tabs and panes + // actually get created. In the window layer, we're gonna cloak the + // window till this event is fired, so we don't actually see this + // frame until we're actually all ready to go. + // + // This will result in the window seemingly not loading as fast, but + // it will actually take exactly the same amount of time before it's + // usable. + // + // We also experimented with drawing a solid BG color before the + // initialization is finished. However, there are still a few frames + // after the frame is displayed before the XAML content first draws, + // so that didn't actually resolve any issues. + Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weak = get_weak()]() { + if (auto self{ weak.get() }) + { + self->Initialized.raise(*self, nullptr); + } + }); + } + } + + // Method Description: + // - Show a dialog with "About" information. Displays the app's Display + // Name, version, getting started link, source code link, documentation link, release + // Notes link, send feedback link and privacy policy link. + void TerminalPage::_ShowAboutDialog() + { + _ShowDialogHelper(L"AboutDialog"); + } + + winrt::hstring TerminalPage::ApplicationDisplayName() + { + return CascadiaSettings::ApplicationDisplayName(); + } + + winrt::hstring TerminalPage::ApplicationVersion() + { + return CascadiaSettings::ApplicationVersion(); + } + + // Method Description: + // - Helper to show a content dialog + // - We only open a content dialog if there isn't one open already + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowDialogHelper(const std::wstring_view& name) + { + if (auto presenter{ _dialogPresenter.get() }) + { + co_return co_await presenter.ShowDialog(FindName(name).try_as()); + } + co_return ContentDialogResult::None; + } + + // Method Description: + // - Displays a dialog to warn the user that they are about to close all open windows. + // Once the user clicks the OK button, shut down the application. + // If cancel is clicked, the dialog will close. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowQuitDialog() + { + return _ShowDialogHelper(L"QuitDialog"); + } + + // Method Description: + // - Displays a dialog for warnings found while closing the terminal app using + // key binding with multiple tabs opened. Display messages to warn user + // that more than 1 tab is opened, and once the user clicks the OK button, remove + // all the tabs and shut down and app. If cancel is clicked, the dialog will close + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseWarningDialog() + { + return _ShowDialogHelper(L"CloseAllDialog"); + } + + // Method Description: + // - Displays a dialog for warnings found while closing the terminal tab marked as read-only + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseReadOnlyDialog() + { + return _ShowDialogHelper(L"CloseReadOnlyDialog"); + } + + // Method Description: + // - Displays a dialog to warn the user about the fact that the text that + // they are trying to paste contains the "new line" character which can + // have the effect of starting commands without the user's knowledge if + // it is pasted on a shell where the "new line" character marks the end + // of a command. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowMultiLinePasteWarningDialog() + { + return _ShowDialogHelper(L"MultiLinePasteDialog"); + } + + // Method Description: + // - Displays a dialog to warn the user about the fact that the text that + // they are trying to paste is very long, in case they did not mean to + // paste it but pressed the paste shortcut by accident. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowLargePasteWarningDialog() + { + return _ShowDialogHelper(L"LargePasteDialog"); + } + + // Method Description: + // - Builds the flyout (dropdown) attached to the new tab button, and + // attaches it to the button. Populates the flyout with one entry per + // Profile, displaying the profile's name. Clicking each flyout item will + // open a new tab with that profile. + // Below the profiles are the static menu items: settings, command palette + void TerminalPage::_CreateNewTabFlyout() + { + auto newTabFlyout = WUX::Controls::MenuFlyout{}; + newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft); + + // Create profile entries from the NewTabMenu configuration using a + // recursive helper function. This returns a std::vector of FlyoutItemBases, + // that we then add to our Flyout. + auto entries = _settings.GlobalSettings().NewTabMenu(); + auto items = _CreateNewTabFlyoutItems(entries); + for (const auto& item : items) + { + newTabFlyout.Items().Append(item); + } + + // add menu separator + auto separatorItem = WUX::Controls::MenuFlyoutSeparator{}; + newTabFlyout.Items().Append(separatorItem); + + // add static items + { + // Create the settings button. + auto settingsItem = WUX::Controls::MenuFlyoutItem{}; + settingsItem.Text(RS_(L"SettingsMenuItem")); + const auto settingsToolTip = RS_(L"SettingsToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(settingsItem, box_value(settingsToolTip)); + Automation::AutomationProperties::SetHelpText(settingsItem, settingsToolTip); + + WUX::Controls::SymbolIcon ico{}; + ico.Symbol(WUX::Controls::Symbol::Setting); + settingsItem.Icon(ico); + + settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); + newTabFlyout.Items().Append(settingsItem); + + auto actionMap = _settings.ActionMap(); + const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenSettingsUI") }; + if (settingsKeyChord) + { + _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); + } + + // Create the command palette button. + auto commandPaletteFlyout = WUX::Controls::MenuFlyoutItem{}; + commandPaletteFlyout.Text(RS_(L"CommandPaletteMenuItem")); + const auto commandPaletteToolTip = RS_(L"CommandPaletteToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(commandPaletteFlyout, box_value(commandPaletteToolTip)); + Automation::AutomationProperties::SetHelpText(commandPaletteFlyout, commandPaletteToolTip); + + WUX::Controls::FontIcon commandPaletteIcon{}; + commandPaletteIcon.Glyph(L"\xE945"); + commandPaletteIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + commandPaletteFlyout.Icon(commandPaletteIcon); + + commandPaletteFlyout.Click({ this, &TerminalPage::_CommandPaletteButtonOnClick }); + newTabFlyout.Items().Append(commandPaletteFlyout); + + const auto commandPaletteKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.ToggleCommandPalette") }; + if (commandPaletteKeyChord) + { + _SetAcceleratorForMenuItem(commandPaletteFlyout, commandPaletteKeyChord); + } + + // Create the about button. + auto aboutFlyout = WUX::Controls::MenuFlyoutItem{}; + aboutFlyout.Text(RS_(L"AboutMenuItem")); + const auto aboutToolTip = RS_(L"AboutToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(aboutFlyout, box_value(aboutToolTip)); + Automation::AutomationProperties::SetHelpText(aboutFlyout, aboutToolTip); + + WUX::Controls::SymbolIcon aboutIcon{}; + aboutIcon.Symbol(WUX::Controls::Symbol::Help); + aboutFlyout.Icon(aboutIcon); + + aboutFlyout.Click({ this, &TerminalPage::_AboutButtonOnClick }); + newTabFlyout.Items().Append(aboutFlyout); + } + + // Before opening the fly-out set focus on the current tab + // so no matter how fly-out is closed later on the focus will return to some tab. + // We cannot do it on closing because if the window loses focus (alt+tab) + // the closing event is not fired. + // It is important to set the focus on the tab + // Since the previous focus location might be discarded in the background, + // e.g., the command palette will be dismissed by the menu, + // and then closing the fly-out will move the focus to wrong location. + newTabFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->_FocusCurrentTab(true); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuOpened", + TraceLoggingDescription("Event emitted when the new tab menu is opened"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }); + // Necessary for fly-out sub items to get focus on a tab before collapsing. Related to #15049 + newTabFlyout.Closing([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + if (!page->_commandPaletteIs(Visibility::Visible)) + { + page->_FocusCurrentTab(true); + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuClosed", + TraceLoggingDescription("Event emitted when the new tab menu is closed"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }); + _newTabButton.Flyout(newTabFlyout); + } + + // Method Description: + // - For a given list of tab menu entries, this method will create the corresponding + // list of flyout items. This is a recursive method that calls itself when it comes + // across a folder entry. + std::vector TerminalPage::_CreateNewTabFlyoutItems(IVector entries) + { + std::vector items; + + if (entries == nullptr || entries.Size() == 0) + { + return items; + } + + for (const auto& entry : entries) + { + if (entry == nullptr) + { + continue; + } + + switch (entry.Type()) + { + case NewTabMenuEntryType::Separator: + { + items.push_back(WUX::Controls::MenuFlyoutSeparator{}); + break; + } + // A folder has a custom name and icon, and has a number of entries that require + // us to call this method recursively. + case NewTabMenuEntryType::Folder: + { + const auto folderEntry = entry.as(); + const auto folderEntries = folderEntry.Entries(); + + // If the folder is empty, we should skip the entry if AllowEmpty is false, or + // when the folder should inline. + // The IsEmpty check includes semantics for nested (empty) folders + if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) + { + break; + } + + // Recursively generate flyout items + auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries); + + // If the folder should auto-inline and there is only one item, do so. + if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1) + { + for (auto const& folderEntryItem : folderEntryItems) + { + items.push_back(folderEntryItem); + } + + break; + } + + // Otherwise, create a flyout + auto folderItem = WUX::Controls::MenuFlyoutSubItem{}; + folderItem.Text(folderEntry.Name()); + + auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon().Resolved()); + folderItem.Icon(icon); + + for (const auto& folderEntryItem : folderEntryItems) + { + folderItem.Items().Append(folderEntryItem); + } + + // If the folder is empty, and by now we know we set AllowEmpty to true, + // create a placeholder item here + if (folderEntries.Size() == 0) + { + auto placeholder = WUX::Controls::MenuFlyoutItem{}; + placeholder.Text(RS_(L"NewTabMenuFolderEmpty")); + placeholder.IsEnabled(false); + + folderItem.Items().Append(placeholder); + } + + items.push_back(folderItem); + break; + } + // Any "collection entry" will simply make us add each profile in the collection + // separately. This collection is stored as a map , so the correct + // profile index is already known. + case NewTabMenuEntryType::RemainingProfiles: + case NewTabMenuEntryType::MatchProfiles: + { + const auto remainingProfilesEntry = entry.as(); + if (remainingProfilesEntry.Profiles() == nullptr) + { + break; + } + + for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles()) + { + items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex, {})); + } + + break; + } + // A single profile, the profile index is also given in the entry + case NewTabMenuEntryType::Profile: + { + const auto profileEntry = entry.as(); + if (profileEntry.Profile() == nullptr) + { + break; + } + + auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex(), profileEntry.Icon().Resolved()); + items.push_back(profileItem); + break; + } + case NewTabMenuEntryType::Action: + { + const auto actionEntry = entry.as(); + const auto actionId = actionEntry.ActionId(); + if (_settings.ActionMap().GetActionByID(actionId)) + { + auto actionItem = _CreateNewTabFlyoutAction(actionId, actionEntry.Icon().Resolved()); + items.push_back(actionItem); + } + + break; + } + } + } + + return items; + } + + // Method Description: + // - This method creates a flyout menu item for a given profile with the given index. + // It makes sure to set the correct icon, keybinding, and click-action. + WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex, const winrt::hstring& iconPathOverride) + { + auto profileMenuItem = WUX::Controls::MenuFlyoutItem{}; + + // Add the keyboard shortcuts based on the number of profiles defined + // Look for a keychord that is bound to the equivalent + // NewTab(ProfileIndex=N) action + NewTerminalArgs newTerminalArgs{ profileIndex }; + NewTabArgs newTabArgs{ newTerminalArgs }; + const auto id = fmt::format(FMT_COMPILE(L"Terminal.OpenNewTabProfile{}"), profileIndex); + const auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(id) }; + + // make sure we find one to display + if (profileKeyChord) + { + _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord); + } + + auto profileName = profile.Name(); + profileMenuItem.Text(profileName); + + // If a custom icon path has been specified, set it as the icon for + // this flyout item. Otherwise, if an icon is set for this profile, set that icon + // for this flyout item. + const auto& iconPath = iconPathOverride.empty() ? profile.Icon().Resolved() : iconPathOverride; + if (!iconPath.empty()) + { + const auto icon = _CreateNewTabFlyoutIcon(iconPath); + profileMenuItem.Icon(icon); + } + + if (profile.Guid() == _settings.GlobalSettings().DefaultProfile()) + { + // Contrast the default profile with others in font weight. + profileMenuItem.FontWeight(FontWeights::Bold()); + } + + auto newTabRun = WUX::Documents::Run(); + newTabRun.Text(RS_(L"NewTabRun/Text")); + auto newPaneRun = WUX::Documents::Run(); + newPaneRun.Text(RS_(L"NewPaneRun/Text")); + newPaneRun.FontStyle(FontStyle::Italic); + auto newWindowRun = WUX::Documents::Run(); + newWindowRun.Text(RS_(L"NewWindowRun/Text")); + newWindowRun.FontStyle(FontStyle::Italic); + auto elevatedRun = WUX::Documents::Run(); + elevatedRun.Text(RS_(L"ElevatedRun/Text")); + elevatedRun.FontStyle(FontStyle::Italic); + + auto textBlock = WUX::Controls::TextBlock{}; + textBlock.Inlines().Append(newTabRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(newPaneRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(newWindowRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(elevatedRun); + + auto toolTip = WUX::Controls::ToolTip{}; + toolTip.Content(textBlock); + WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip); + + profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Profile", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + NewTerminalArgs newTerminalArgs{ profileIndex }; + page->_OpenNewTerminalViaDropdown(newTerminalArgs); + } + }); + + // Using the static method on the base class seems to do what we want in terms of placement. + WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(profileMenuItem, _CreateRunAsAdminFlyout(profileIndex)); + + // Since we are not setting the ContextFlyout property of the item we have to handle the ContextRequested event + // and rely on the base class to show our menu. + profileMenuItem.ContextRequested([profileMenuItem](auto&&, auto&&) { + WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(profileMenuItem); + }); + + return profileMenuItem; + } + + // Method Description: + // - This method creates a flyout menu item for a given action + // It makes sure to set the correct icon, keybinding, and click-action. + WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride) + { + auto actionMenuItem = WUX::Controls::MenuFlyoutItem{}; + const auto action{ _settings.ActionMap().GetActionByID(actionId) }; + const auto actionKeyChord{ _settings.ActionMap().GetKeyBindingForAction(actionId) }; + + if (actionKeyChord) + { + _SetAcceleratorForMenuItem(actionMenuItem, actionKeyChord); + } + + actionMenuItem.Text(action.Name()); + + // If a custom icon path has been specified, set it as the icon for + // this flyout item. Otherwise, if an icon is set for this action, set that icon + // for this flyout item. + const auto& iconPath = iconPathOverride.empty() ? action.Icon().Resolved() : iconPathOverride; + if (!iconPath.empty()) + { + const auto icon = _CreateNewTabFlyoutIcon(iconPath); + actionMenuItem.Icon(icon); + } + + actionMenuItem.Click([action, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Action", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + page->_actionDispatch->DoAction(action.ActionAndArgs()); + } + }); + + return actionMenuItem; + } + + // Method Description: + // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and + // MenuFlyoutSubItems + IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource) + { + if (iconSource.empty()) + { + return nullptr; + } + + auto icon = UI::IconPathConverter::IconWUX(iconSource); + Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw); + + return icon; + } + + // Function Description: + // Called when the openNewTabDropdown keybinding is used. + // Shows the dropdown flyout. + void TerminalPage::_OpenNewTabDropdown() + { + _newTabButton.Flyout().ShowAt(_newTabButton); + } + + void TerminalPage::_OpenNewTerminalViaDropdown(const NewTerminalArgs newTerminalArgs) + { + // if alt is pressed, open a pane + const auto window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; + const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); + const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); + const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; + + const auto ctrlState{ window.GetKeyState(VirtualKey::Control) }; + const auto rCtrlState = window.GetKeyState(VirtualKey::RightControl); + const auto lCtrlState = window.GetKeyState(VirtualKey::LeftControl); + const auto ctrlPressed{ WI_IsFlagSet(ctrlState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rCtrlState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lCtrlState, CoreVirtualKeyStates::Down) }; + + // Check for DebugTap + auto debugTap = this->_settings.GlobalSettings().DebugFeaturesEnabled() && + WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + const auto dispatchToElevatedWindow = ctrlPressed && !IsRunningElevated(); + + auto sessionType = ""; + if ((shiftPressed || dispatchToElevatedWindow) && !debugTap) + { + // Manually fill in the evaluated profile. + if (newTerminalArgs.ProfileIndex() != nullptr) + { + // We want to promote the index to a GUID because there is no "launch to profile index" command. + const auto profile = _settings.GetProfileForArgs(newTerminalArgs); + if (profile) + { + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + newTerminalArgs.StartingDirectory(_evaluatePathForCwd(profile.EvaluatedStartingDirectory())); + } + } + + if (dispatchToElevatedWindow) + { + _OpenElevatedWT(newTerminalArgs); + sessionType = "ElevatedWindow"; + } + else + { + _OpenNewWindow(newTerminalArgs); + sessionType = "Window"; + } + } + else + { + const auto newPane = _MakePane(newTerminalArgs); + // If the newTerminalArgs caused us to open an elevated window + // instead of creating a pane, it may have returned nullptr. Just do + // nothing then. + if (!newPane) + { + return; + } + if (altPressed && !debugTap) + { + this->_SplitPane(_GetFocusedTabImpl(), + SplitDirection::Automatic, + 0.5f, + newPane); + sessionType = "Pane"; + } + else + { + _CreateNewTabFromPane(newPane); + sessionType = "Tab"; + } + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuCreatedNewTerminalSession", + TraceLoggingDescription("Event emitted when a new terminal was created via the new tab menu"), + TraceLoggingValue(NumberOfTabs(), "NewTabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue(sessionType, "SessionType", "The type of session that was created"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + std::wstring TerminalPage::_evaluatePathForCwd(const std::wstring_view path) + { + return Utils::EvaluateStartingDirectory(_WindowProperties.VirtualWorkingDirectory(), path); + } + + // Method Description: + // - Creates a new connection based on the profile settings + // Arguments: + // - the profile we want the settings from + // - the terminal settings + // Return value: + // - the desired connection + TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile, + IControlSettings settings, + const bool inheritCursor) + { + static const auto textMeasurement = [&]() -> std::wstring_view { + switch (_settings.GlobalSettings().TextMeasurement()) + { + case TextMeasurement::Graphemes: + return L"graphemes"; + case TextMeasurement::Wcswidth: + return L"wcswidth"; + case TextMeasurement::Console: + return L"console"; + default: + return {}; + } + }(); + static const auto ambiguousIsWide = [&]() -> bool { + return _settings.GlobalSettings().AmbiguousWidth() == AmbiguousWidth::Wide; + }(); + + TerminalConnection::ITerminalConnection connection{ nullptr }; + + auto connectionType = profile.ConnectionType(); + Windows::Foundation::Collections::ValueSet valueSet; + + if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && + TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) + { + connection = TerminalConnection::AzureConnection{}; + valueSet = TerminalConnection::ConptyConnection::CreateSettings(winrt::hstring{}, + L".", + L"Azure", + false, + L"", + nullptr, + settings.InitialRows(), + settings.InitialCols(), + winrt::guid(), + profile.Guid()); + } + + else + { + auto settingsInternal{ winrt::get_self(settings) }; + const auto environment = settingsInternal->EnvironmentVariables(); + + // Update the path to be relative to whatever our CWD is. + // + // Refer to the examples in + // https://en.cppreference.com/w/cpp/filesystem/path/append + // + // We need to do this here, to ensure we tell the ConptyConnection + // the correct starting path. If we're being invoked from another + // terminal instance (e.g. `wt -w 0 -d .`), then we have switched our + // CWD to the provided path. We should treat the StartingDirectory + // as relative to the current CWD. + // + // The connection must be informed of the current CWD on + // construction, because the connection might not spawn the child + // process until later, on another thread, after we've already + // restored the CWD to its original value. + auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) }; + connection = TerminalConnection::ConptyConnection{}; + valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), + newWorkingDirectory, + settings.StartingTitle(), + settingsInternal->ReloadEnvironmentVariables(), + _WindowProperties.VirtualEnvVars(), + environment, + settings.InitialRows(), + settings.InitialCols(), + winrt::guid(), + profile.Guid()); + + if (inheritCursor) + { + valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true)); + } + } + + if (!textMeasurement.empty()) + { + valueSet.Insert(L"textMeasurement", Windows::Foundation::PropertyValue::CreateString(textMeasurement)); + } + if (ambiguousIsWide) + { + valueSet.Insert(L"ambiguousIsWide", Windows::Foundation::PropertyValue::CreateBoolean(true)); + } + + if (const auto id = settings.SessionId(); id != winrt::guid{}) + { + valueSet.Insert(L"sessionId", Windows::Foundation::PropertyValue::CreateGuid(id)); + } + + connection.Initialize(valueSet); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "ConnectionCreated", + TraceLoggingDescription("Event emitted upon the creation of a connection"), + TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"), + TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"), + TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + return connection; + } + + TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent) + { + if (paneContent == nullptr) + { + return nullptr; + } + + const auto& control{ paneContent.GetTermControl() }; + if (control == nullptr) + { + return nullptr; + } + const auto& connection = control.Connection(); + auto profile{ paneContent.GetProfile() }; + + Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; + + if (profile) + { + // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. + profile = GetClosestProfileForDuplicationOfProfile(profile); + controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); + + // Replace the Starting directory with the CWD, if given + const auto workingDirectory = control.WorkingDirectory(); + const auto validWorkingDirectory = !workingDirectory.empty(); + if (validWorkingDirectory) + { + controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); + } + + // To facilitate restarting defterm connections: grab the original + // commandline out of the connection and shove that back into the + // settings. + if (const auto& conpty{ connection.try_as() }) + { + controlSettings.DefaultSettings()->Commandline(conpty.Commandline()); + } + } + + return _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), true); + } + + // Method Description: + // - Called when the settings button is clicked. Launches a background + // thread to open the settings file in the default JSON editor. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_SettingsButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + const auto window = CoreWindow::GetForCurrentThread(); + + // check alt state + const auto rAltState{ window.GetKeyState(VirtualKey::RightMenu) }; + const auto lAltState{ window.GetKeyState(VirtualKey::LeftMenu) }; + const auto altPressed{ WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down) }; + + // check shift state + const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; + const auto lShiftState{ window.GetKeyState(VirtualKey::LeftShift) }; + const auto rShiftState{ window.GetKeyState(VirtualKey::RightShift) }; + const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; + + auto target{ SettingsTarget::SettingsUI }; + if (shiftPressed) + { + target = SettingsTarget::SettingsFile; + } + else if (altPressed) + { + target = SettingsTarget::DefaultsFile; + } + + const auto targetAsString = [&target]() { + switch (target) + { + case SettingsTarget::SettingsFile: + return "SettingsFile"; + case SettingsTarget::DefaultsFile: + return "DefaultsFile"; + case SettingsTarget::SettingsUI: + default: + return "UI"; + } + }(); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Settings", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingValue(targetAsString, "SettingsTarget", "The target settings file or UI"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + _LaunchSettings(target); + } + + // Method Description: + // - Called when the command palette button is clicked. Opens the command palette. + void TerminalPage::_CommandPaletteButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + auto p = LoadCommandPalette(); + p.EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); + p.Visibility(Visibility::Visible); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("CommandPalette", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + // Method Description: + // - Called when the about button is clicked. See _ShowAboutDialog for more info. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_AboutButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + _ShowAboutDialog(); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("About", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + // Method Description: + // - Called when the users pressed keyBindings while CommandPaletteElement is open. + // - As of GH#8480, this is also bound to the TabRowControl's KeyUp event. + // That should only fire when focus is in the tab row, which is hard to + // do. Notably, that's possible: + // - When you have enough tabs to make the little scroll arrows appear, + // click one, then hit tab + // - When Narrator is in Scan mode (which is the a11y bug we're fixing here) + // - This method is effectively an extract of TermControl::_KeyHandler and TermControl::_TryHandleKeyBinding. + // Arguments: + // - e: the KeyRoutedEventArgs containing info about the keystroke. + // Return Value: + // - + void TerminalPage::_KeyDownHandler(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto keyStatus = e.KeyStatus(); + const auto vkey = gsl::narrow_cast(e.OriginalKey()); + const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); + const auto modifiers = _GetPressedModifierKeys(); + + // GH#11076: + // For some weird reason we sometimes receive a WM_KEYDOWN + // message without vkey or scanCode if a user drags a tab. + // The KeyChord constructor has a debug assertion ensuring that all KeyChord + // either have a valid vkey/scanCode. This is important, because this prevents + // accidental insertion of invalid KeyChords into classes like ActionMap. + if (!vkey && !scanCode) + { + return; + } + + // Alt-Numpad# input will send us a character once the user releases + // Alt, so we should be ignoring the individual keydowns. The character + // will be sent through the TSFInputControl. See GH#1401 for more + // details + if (modifiers.IsAltPressed() && (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) + { + return; + } + + // GH#2235: Terminal::Settings hasn't been modified to differentiate + // between AltGr and Ctrl+Alt yet. + // -> Don't check for key bindings if this is an AltGr key combination. + if (modifiers.IsAltGrPressed()) + { + return; + } + + const auto actionMap = _settings.ActionMap(); + if (!actionMap) + { + return; + } + + const auto cmd = actionMap.GetActionByKeyChord({ + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + vkey, + scanCode, + }); + if (!cmd) + { + return; + } + + if (!_actionDispatch->DoAction(cmd.ActionAndArgs())) + { + return; + } + + if (_commandPaletteIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + CommandPaletteElement().Visibility(Visibility::Collapsed); + } + if (_suggestionsControlIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + SuggestionsElement().Visibility(Visibility::Collapsed); + } + + // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". + // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. + // The following is used to manually "consume" such dead keys and clear them from the keyboard state. + _ClearKeyboardState(vkey, scanCode); + e.Handled(true); + } + + bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + { + const auto modifiers = _GetPressedModifierKeys(); + if (vkey == VK_SPACE && modifiers.IsAltPressed() && down) + { + if (const auto actionMap = _settings.ActionMap()) + { + if (const auto cmd = actionMap.GetActionByKeyChord({ + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + gsl::narrow_cast(vkey), + scanCode, + })) + { + return _actionDispatch->DoAction(cmd.ActionAndArgs()); + } + } + } + return false; + } + + // Method Description: + // - Get the modifier keys that are currently pressed. This can be used to + // find out which modifiers (ctrl, alt, shift) are pressed in events that + // don't necessarily include that state. + // - This is a copy of TermControl::_GetPressedModifierKeys. + // Return Value: + // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. + ControlKeyStates TerminalPage::_GetPressedModifierKeys() noexcept + { + const auto window = CoreWindow::GetForCurrentThread(); + // DONT USE + // != CoreVirtualKeyStates::None + // OR + // == CoreVirtualKeyStates::Down + // Sometimes with the key down, the state is Down | Locked. + // Sometimes with the key up, the state is Locked. + // IsFlagSet(Down) is the only correct solution. + + struct KeyModifier + { + VirtualKey vkey; + ControlKeyStates flags; + }; + + constexpr std::array modifiers{ { + { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, + { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, + { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, + { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, + { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, + { VirtualKey::RightWindows, ControlKeyStates::RightWinPressed }, + { VirtualKey::LeftWindows, ControlKeyStates::LeftWinPressed }, + } }; + + ControlKeyStates flags; + + for (const auto& mod : modifiers) + { + const auto state = window.GetKeyState(mod.vkey); + const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); + + if (isDown) + { + flags |= mod.flags; + } + } + + return flags; + } + + // Method Description: + // - Discards currently pressed dead keys. + // - This is a copy of TermControl::_ClearKeyboardState. + // Arguments: + // - vkey: The vkey of the key pressed. + // - scanCode: The scan code of the key pressed. + void TerminalPage::_ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept + { + std::array keyState; + if (!GetKeyboardState(keyState.data())) + { + return; + } + + // As described in "Sometimes you *want* to interfere with the keyboard's state buffer": + // http://archives.miloush.net/michkap/archive/2006/09/10/748775.html + // > "The key here is to keep trying to pass stuff to ToUnicode until -1 is not returned." + std::array buffer; + while (ToUnicodeEx(vkey, scanCode, keyState.data(), buffer.data(), gsl::narrow_cast(buffer.size()), 0b1, nullptr) < 0) + { + } + } + + // Method Description: + // - Configure the AppKeyBindings to use our ShortcutActionDispatch and the updated ActionMap + // as the object to handle dispatching ShortcutAction events. + // Arguments: + // - bindings: An IActionMapView object to wire up with our event handlers + void TerminalPage::_HookupKeyBindings(const IActionMapView& actionMap) noexcept + { + _bindings->SetDispatch(*_actionDispatch); + _bindings->SetActionMap(actionMap); + } + + // Method Description: + // - Register our event handlers with our ShortcutActionDispatch. The + // ShortcutActionDispatch is responsible for raising the appropriate + // events for an ActionAndArgs. WE'll handle each possible event in our + // own way. + // Arguments: + // - + void TerminalPage::_RegisterActionCallbacks() + { + // Hook up the ShortcutActionDispatch object's events to our handlers. + // They should all be hooked up here, regardless of whether or not + // there's an actual keychord for them. +#define ON_ALL_ACTIONS(action) HOOKUP_ACTION(action); + ALL_SHORTCUT_ACTIONS + INTERNAL_SHORTCUT_ACTIONS +#undef ON_ALL_ACTIONS + } + + // Method Description: + // - 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(const Tab& tab) + { + if (tab == _GetFocusedTab()) + { + TitleChanged.raise(*this, nullptr); + } + } + + // Method Description: + // - Connects event handlers to the TermControl for events that we want to + // handle. This includes: + // * the Copy and Paste events, for setting and retrieving clipboard data + // on the right thread + // Arguments: + // - term: The newly created TermControl to connect the events for + void TerminalPage::_RegisterTerminalEvents(TermControl term) + { + term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); + + term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); + term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler }); + + term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); + + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + + term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler }); + + term.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& e) { + if (auto page{ weakThis.get() }) + { + if (e.PropertyName() == L"BackgroundBrush") + { + page->_updateThemeColors(); + } + } + }); + + term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); + term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); + + // Don't even register for the event if the feature is compiled off. + if constexpr (Feature_ShellCompletions::IsEnabled()) + { + term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); + } + winrt::weak_ref weakTerm{ term }; + term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), false); + } + }); + term.SelectionContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); + } + }); + if constexpr (Feature_QuickFix::IsEnabled()) + { + term.QuickFixMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateQuickFixMenu(weakTerm.get(), sender.try_as()); + } + }); + } + } + + // Method Description: + // - Connects event handlers to the Tab for events that we want to + // handle. This includes: + // * the TitleChanged event, for changing the text of the tab + // * the Color{Selected,Cleared} events to change the color of a tab. + // Arguments: + // - hostingTab: The Tab that's hosting this TermControl instance + void TerminalPage::_RegisterTabEvents(Tab& hostingTab) + { + auto weakTab{ hostingTab.get_weak() }; + auto weakThis{ get_weak() }; + // PropertyChanged is the generic mechanism by which the Tab + // communicates changes to any of its observable properties, including + // the Title + hostingTab.PropertyChanged([weakTab, weakThis](auto&&, const WUX::Data::PropertyChangedEventArgs& args) { + auto page{ weakThis.get() }; + auto tab{ weakTab.get() }; + if (page && tab) + { + const auto propertyName = args.PropertyName(); + if (propertyName == L"Title") + { + page->_UpdateTitle(*tab); + } + else if (propertyName == L"Content") + { + if (*tab == page->_GetFocusedTab()) + { + const auto children = page->_tabContent.Children(); + + children.Clear(); + if (auto content = tab->Content()) + { + page->_tabContent.Children().Append(std::move(content)); + } + + tab->Focus(FocusState::Programmatic); + } + } + } + }); + + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + + hostingTab.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); + } + + // Method Description: + // - Helper to manually exit "zoom" when certain actions take place. + // Anything that modifies the state of the pane tree should probably + // un-zoom the focused pane first, so that the user can see the full pane + // tree again. These actions include: + // * Splitting a new pane + // * Closing a pane + // * Moving focus between panes + // * Resizing a pane + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_UnZoomIfNeeded() + { + if (const auto activeTab{ _GetFocusedTabImpl() }) + { + if (activeTab->IsZoomed()) + { + // Remove the content from the tab first, so Pane::UnZoom can + // re-attach the content to the tree w/in the pane + _tabContent.Children().Clear(); + // In ExitZoom, we'll change the Tab's Content(), triggering the + // content changed event, which will re-attach the tab's new content + // root to the tree. + activeTab->ExitZoom(); + } + } + } + + // Method Description: + // - Attempt to move focus between panes, as to focus the child on + // the other side of the separator. See Pane::NavigateFocus for details. + // - Moves the focus of the currently focused tab. + // Arguments: + // - direction: The direction to move the focus in. + // Return Value: + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalPage::_MoveFocus(const FocusDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->NavigateFocus(direction); + } + return false; + } + + // Method Description: + // - Attempt to swap the positions of the focused pane with another pane. + // See Pane::SwapPane for details. + // Arguments: + // - direction: The direction to move the focused pane in. + // Return Value: + // - true if panes were swapped. + bool TerminalPage::_SwapPane(const FocusDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + return tabImpl->SwapPane(direction); + } + return false; + } + + TermControl TerminalPage::_GetActiveControl() const + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->GetActiveTerminalControl(); + } + return nullptr; + } + + CommandPalette TerminalPage::LoadCommandPalette() + { + if (const auto p = CommandPaletteElement()) + { + return p; + } + + return _loadCommandPaletteSlowPath(); + } + bool TerminalPage::_commandPaletteIs(WUX::Visibility visibility) + { + const auto p = CommandPaletteElement(); + return p && p.Visibility() == visibility; + } + + CommandPalette TerminalPage::_loadCommandPaletteSlowPath() + { + const auto p = FindName(L"CommandPaletteElement").as(); + + p.SetActionMap(_settings.ActionMap()); + + // When the visibility of the command palette changes to "collapsed", + // the palette has been closed. Toss focus back to the currently active control. + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (_commandPaletteIs(Visibility::Collapsed)) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); + p.SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + + SuggestionsControl TerminalPage::LoadSuggestionsUI() + { + if (const auto p = SuggestionsElement()) + { + return p; + } + + return _loadSuggestionsElementSlowPath(); + } + bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) + { + const auto p = SuggestionsElement(); + return p && p.Visibility() == visibility; + } + + SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() + { + const auto p = FindName(L"SuggestionsElement").as(); + + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (SuggestionsElement().Visibility() == Visibility::Collapsed) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + + // Method Description: + // - Warn the user that they are about to close all open windows, then + // signal that we want to close everything. + safe_void_coroutine TerminalPage::RequestQuit() + { + if (!_displayingCloseDialog) + { + _displayingCloseDialog = true; + + const auto weak = get_weak(); + auto warningResult = co_await _ShowQuitDialog(); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + _displayingCloseDialog = false; + + if (warningResult != ContentDialogResult::Primary) + { + co_return; + } + + QuitRequested.raise(nullptr, nullptr); + } + } + + void TerminalPage::PersistState() + { + // This method may be called for a window even if it hasn't had a tab yet or lost all of them. + // We shouldn't persist such windows. + const auto tabCount = _tabs.Size(); + if (_startupState != StartupState::Initialized || tabCount == 0) + { + return; + } + + std::vector actions; + + for (auto tab : _tabs) + { + auto t = winrt::get_self(tab); + auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); + } + + // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. + if (actions.empty()) + { + return; + } + + // if the focused tab was not the last tab, restore that + auto idx = _GetFocusedTabIndex(); + if (idx && idx != tabCount - 1) + { + ActionAndArgs action; + action.Action(ShortcutAction::SwitchToTab); + SwitchToTabArgs switchToTabArgs{ idx.value() }; + action.Args(switchToTabArgs); + + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (const auto& windowName{ _WindowProperties.WindowName() }; !windowName.empty()) + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ windowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); + } + + WindowLayout layout; + layout.TabLayout(winrt::single_threaded_vector(std::move(actions))); + + auto mode = LaunchMode::DefaultMode; + WI_SetFlagIf(mode, LaunchMode::FullscreenMode, _isFullscreen); + WI_SetFlagIf(mode, LaunchMode::FocusMode, _isInFocusMode); + WI_SetFlagIf(mode, LaunchMode::MaximizedMode, _isMaximized); + + layout.LaunchMode({ mode }); + + // Only save the content size because the tab size will be added on load. + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; + + layout.InitialSize(windowSize); + + // We don't actually know our own position. So we have to ask the window + // layer for that. + const auto launchPosRequest{ winrt::make() }; + RequestLaunchPosition.raise(*this, launchPosRequest); + layout.InitialPosition(launchPosRequest.Position()); + + ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); + } + + // Method Description: + // - Close the terminal app. If there is more + // than one tab opened, show a warning dialog. + safe_void_coroutine TerminalPage::CloseWindow() + { + if (_HasMultipleTabs() && + _settings.GlobalSettings().ConfirmCloseAllTabs() && + !_displayingCloseDialog) + { + if (_newTabButton && _newTabButton.Flyout()) + { + _newTabButton.Flyout().Hide(); + } + _DismissTabContextMenus(); + _displayingCloseDialog = true; + auto warningResult = co_await _ShowCloseWarningDialog(); + _displayingCloseDialog = false; + + if (warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + + CloseWindowRequested.raise(*this, nullptr); + } + + std::vector TerminalPage::Panes() const + { + std::vector panes; + + for (const auto tab : _tabs) + { + const auto impl = _GetTabImpl(tab); + if (!impl) + { + continue; + } + + impl->GetRootPane()->WalkTree([&](auto&& pane) { + if (auto content = pane->GetContent()) + { + panes.push_back(std::move(content)); + } + }); + } + + return panes; + } + + // Method Description: + // - Move the viewport of the terminal of the currently focused tab up or + // down a number of lines. + // Arguments: + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + // - rowsToScroll: a number of lines to move the viewport. If not provided we will use a system default. + void TerminalPage::_Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + uint32_t realRowsToScroll; + if (rowsToScroll == nullptr) + { + // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page + realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? + tabImpl->GetActiveTerminalControl().ViewHeight() : + _systemRowsToScroll; + } + else + { + // use the custom value specified in the command + realRowsToScroll = rowsToScroll.Value(); + } + auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); + tabImpl->Scroll(scrollDelta); + } + } + + // Method Description: + // - Moves the currently active pane on the currently active tab to the + // specified tab. If the tab index is greater than the number of + // tabs, then a new tab will be created for the pane. Similarly, if a pane + // is the last remaining pane on a tab, that tab will be closed upon moving. + // - No move will occur if the tabIdx is the same as the current tab, or if + // the specified tab is not a host of terminals (such as the settings tab). + // - If the Window is specified, the pane will instead be detached and moved + // to the window with the given name/id. + // Return Value: + // - true if the pane was successfully moved to the new tab. + bool TerminalPage::_MovePane(MovePaneArgs args) + { + const auto tabIdx{ args.TabIndex() }; + const auto windowId{ args.Window() }; + + auto focusedTab{ _GetFocusedTabImpl() }; + + if (!focusedTab) + { + return false; + } + + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + if (!windowId.empty()) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto pane{ tabImpl->GetActivePane() }) + { + auto startupActions = pane->BuildStartupActions(0, 1, BuildStartupKind::MovePane); + _DetachPaneFromWindow(pane); + _MoveContent(std::move(startupActions.args), windowId, tabIdx); + focusedTab->DetachPane(); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewWindow"), + L"TerminalPageMovePaneToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow2", windowId), + L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); + } + } + return true; + } + } + } + + // If we are trying to move from the current tab to the current tab do nothing. + if (_GetFocusedTabIndex() == tabIdx) + { + return false; + } + + // Moving the pane from the current tab might close it, so get the next + // tab before its index changes. + if (tabIdx < _tabs.Size()) + { + auto targetTab = _GetTabImpl(_tabs.GetAt(tabIdx)); + // if the selected tab is not a host of terminals (e.g. settings) + // don't attempt to add a pane to it. + if (!targetTab) + { + return false; + } + auto pane = focusedTab->DetachPane(); + targetTab->AttachPane(pane); + _SetFocusedTab(*targetTab); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = targetTab->Title(); + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingTab", tabTitle), + L"TerminalPageMovePaneToExistingTab" /* unique name for this notification category */); + } + } + else + { + auto pane = focusedTab->DetachPane(); + _CreateNewTabFromPane(pane); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewTab"), + L"TerminalPageMovePaneToNewTab" /* unique name for this notification category */); + } + } + + return true; + } + + // Detach a tree of panes from this terminal. Helper used for moving panes + // and tabs to other windows. + void TerminalPage::_DetachPaneFromWindow(std::shared_ptr pane) + { + pane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + _manager.Detach(control); + } + }); + } + + void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr& tab) + { + // Detach the root pane, which will act like the whole tab got detached. + if (const auto rootPane = tab->GetRootPane()) + { + _DetachPaneFromWindow(rootPane); + } + } + + // Method Description: + // - Serialize these actions to json, and raise them as a RequestMoveContent + // event. Our Window will raise that to the window manager / monarch, who + // will dispatch this blob of json back to the window that should handle + // this. + // - `actions` will be emptied into a winrt IVector as a part of this method + // and should be expected to be empty after this call. + void TerminalPage::_MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint) + { + const auto winRtActions{ winrt::single_threaded_vector(std::move(actions)) }; + const auto str{ ActionAndArgs::Serialize(winRtActions) }; + const auto request = winrt::make_self(windowName, + str, + tabIndex); + if (dragPoint.has_value()) + { + request->WindowPosition(*dragPoint); + } + RequestMoveContent.raise(*this, *request); + } + + bool TerminalPage::_MoveTab(winrt::com_ptr tab, MoveTabArgs args) + { + if (!tab) + { + return false; + } + + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + const auto windowId{ args.Window() }; + if (!windowId.empty()) + { + // if the windowId is the same as our name, do nothing + if (windowId == WindowProperties().WindowName() || + windowId == winrt::to_hstring(WindowProperties().WindowId())) + { + return true; + } + + if (tab) + { + auto startupActions = tab->BuildStartupActions(BuildStartupKind::Content); + _DetachTabFromWindow(tab); + _MoveContent(std::move(startupActions), windowId, 0); + _RemoveTab(*tab); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = tab->Title(); + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_TabMovedAnnouncement_NewWindow", tabTitle), + L"TerminalPageMoveTabToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_TabMovedAnnouncement_Default", tabTitle, windowId), + L"TerminalPageMoveTabToExistingWindow" /* unique name for this notification category */); + } + } + return true; + } + } + + const auto direction = args.Direction(); + if (direction != MoveTabDirection::None) + { + // Use the requested tab, if provided. Otherwise, use the currently + // focused tab. + const auto tabIndex = til::coalesce(_GetTabIndex(*tab), + _GetFocusedTabIndex()); + if (tabIndex) + { + const auto currentTabIndex = tabIndex.value(); + const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; + _TryMoveTab(currentTabIndex, currentTabIndex + delta); + } + } + + return true; + } + + // 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. + void TerminalPage::_activePaneChanged(winrt::TerminalApp::Tab sender, + Windows::Foundation::IInspectable /*args*/) + { + if (const auto tab{ _GetTabImpl(sender) }) + { + // Possibly update the icon of the tab. + _UpdateTabIcon(*tab); + + _updateThemeColors(); + + // Update the taskbar progress as well. We'll raise our own + // SetTaskbarProgress event here, to get tell the hosting + // application to re-query this value from us. + SetTaskbarProgress.raise(*this, nullptr); + + auto profile = tab->GetFocusedProfile(); + _UpdateBackground(profile); + } + + _adjustProcessPriorityThrottled->Run(); + } + + uint32_t TerminalPage::NumberOfTabs() const + { + return _tabs.Size(); + } + + // Method Description: + // - Called when it is determined that an existing tab or pane should be + // attached to our window. content represents a blob of JSON describing + // some startup actions for rebuilding the specified panes. They will + // include `__content` properties with the GUID of the existing + // ControlInteractivity's we should use, rather than starting new ones. + // - _MakePane is already enlightened to use the ContentId property to + // reattach instead of create new content, so this method simply needs to + // parse the JSON and pump it into our action handler. Almost the same as + // doing something like `wt -w 0 nt`. + void TerminalPage::AttachContent(IVector args, uint32_t tabIndex) + { + if (args == nullptr || + args.Size() == 0) + { + return; + } + + std::vector existingTabs{}; + existingTabs.reserve(_tabs.Size()); + for (const auto& tab : _tabs) + { + existingTabs.emplace_back(tab); + } + + const auto& firstAction = args.GetAt(0); + const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; + + // `splitPane` allows the user to specify which tab to split. In that + // case, split specifically the requested pane. + // + // If there's not enough tabs, then just turn this pane into a new tab. + // + // If the first action is `newTab`, the index is always going to be 0, + // so don't do anything in that case. + if (firstIsSplitPane && tabIndex < _tabs.Size()) + { + _SelectTab(tabIndex); + } + + for (const auto& action : args) + { + _actionDispatch->DoAction(action); + } + + // After handling all the actions, then re-check the tabIndex. We might + // have been called as a part of a tab drag/drop. In that case, the + // tabIndex is actually relevant, and we need to move the tab we just + // made into position. + if (!firstIsSplitPane && tabIndex != -1) + { + const auto newTabs = _CollectNewTabs(existingTabs); + if (!newTabs.empty()) + { + _MoveTabsToIndex(newTabs, tabIndex); + _SetSelectedTabs(newTabs, newTabs.front()); + } + } + } + + // Method Description: + // - Split the focused pane of the given tab, either horizontally or vertically, and place the + // given pane accordingly + // Arguments: + // - tab: The tab that is going to be split. + // - newPane: the pane to add to our tree of panes + // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the + // new pane should be split from its parent. + // - splitSize: the size of the split + void TerminalPage::_SplitPane(const winrt::com_ptr& tab, + const SplitDirection splitDirection, + const float splitSize, + std::shared_ptr newPane) + { + auto activeTab = tab; + // Clever hack for a crash in startup, with multiple sub-commands. Say + // you have the following commandline: + // + // wtd nt -p "elevated cmd" ; sp -p "elevated cmd" ; sp -p "Command Prompt" + // + // Where "elevated cmd" is an elevated profile. + // + // In that scenario, we won't dump off the commandline immediately to an + // elevated window, because it's got the final unelevated split in it. + // However, when we get to that command, there won't be a tab yet. So + // we'd crash right about here. + // + // Instead, let's just promote this first split to be a tab instead. + // Crash avoided, and we don't need to worry about inserting a new-tab + // command in at the start. + if (!tab) + { + if (_tabs.Size() == 0) + { + _CreateNewTabFromPane(newPane); + return; + } + else + { + activeTab = _GetFocusedTabImpl(); + } + } + + // For now, prevent splitting the _settingsTab. We can always revisit this later. + if (*activeTab == _settingsTab) + { + return; + } + + // If the caller is calling us with the return value of _MakePane + // directly, it's possible that nullptr was returned, if the connections + // was supposed to be launched in an elevated window. In that case, do + // nothing here. We don't have a pane with which to create the split. + if (!newPane) + { + return; + } + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; + + const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); + if (!realSplitType) + { + return; + } + + _UnZoomIfNeeded(); + auto [original, newGuy] = activeTab->SplitPane(*realSplitType, splitSize, newPane); + + // After GH#6586, the control will no longer focus itself + // automatically when it's finished being laid out. Manually focus + // the control here instead. + if (_startupState == StartupState::Initialized) + { + if (const auto& content{ newGuy->GetContent() }) + { + content.Focus(FocusState::Programmatic); + } + } + } + + // Method Description: + // - Switches the split orientation of the currently focused pane. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ToggleSplitOrientation() + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + tabImpl->ToggleSplitOrientation(); + } + } + + // Method Description: + // - Attempt to move a separator between panes, as to resize each child on + // either size of the separator. See Pane::ResizePane for details. + // - Moves a separator on the currently focused tab. + // Arguments: + // - direction: The direction to move the separator in. + // Return Value: + // - + void TerminalPage::_ResizePane(const ResizeDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + tabImpl->ResizePane(direction); + } + } + + // Method Description: + // - Move the viewport of the terminal of the currently focused tab up or + // down a page. The page length will be dependent on the terminal view height. + // Arguments: + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + void TerminalPage::_ScrollPage(ScrollDirection scrollDirection) + { + // Do nothing if for some reason, there's no terminal tab in focus. We don't want to crash. + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto& control{ _GetActiveControl() }) + { + const auto termHeight = control.ViewHeight(); + auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); + tabImpl->Scroll(scrollDelta); + } + } + } + + void TerminalPage::_ScrollToBufferEdge(ScrollDirection scrollDirection) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + auto scrollDelta = _ComputeScrollDelta(scrollDirection, INT_MAX); + tabImpl->Scroll(scrollDelta); + } + } + + // Method Description: + // - Gets the title of the currently focused terminal control. If there + // isn't a control selected for any reason, returns "Terminal" + // Arguments: + // - + // Return Value: + // - the title of the focused control if there is one, else "Terminal" + hstring TerminalPage::Title() + { + if (_settings.GlobalSettings().ShowTitleInTitlebar()) + { + if (const auto tab{ _GetFocusedTab() }) + { + return tab.Title(); + } + } + return { L"Terminal" }; + } + + // Method Description: + // - Handles the special case of providing a text override for the UI shortcut due to VK_OEM issue. + // Looks at the flags from the KeyChord modifiers and provides a concatenated string value of all + // in the same order that XAML would put them as well. + // Return Value: + // - a string representation of the key modifiers for the shortcut + //NOTE: This needs to be localized with https://github.com/microsoft/terminal/issues/794 if XAML framework issue not resolved before then + static std::wstring _FormatOverrideShortcutText(VirtualKeyModifiers modifiers) + { + std::wstring buffer{ L"" }; + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)) + { + buffer += L"Ctrl+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)) + { + buffer += L"Shift+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)) + { + buffer += L"Alt+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)) + { + buffer += L"Win+"; + } + + return buffer; + } + + // Method Description: + // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. + // Takes into account a special case for an error condition for a comma + // Arguments: + // - MenuFlyoutItem that will be displayed, and a KeyChord to map an accelerator + void TerminalPage::_SetAcceleratorForMenuItem(WUX::Controls::MenuFlyoutItem& menuItem, + const KeyChord& keyChord) + { +#ifdef DEP_MICROSOFT_UI_XAML_708_FIXED + // work around https://github.com/microsoft/microsoft-ui-xaml/issues/708 in case of VK_OEM_COMMA + if (keyChord.Vkey() != VK_OEM_COMMA) + { + // use the XAML shortcut to give us the automatic capabilities + auto menuShortcut = Windows::UI::Xaml::Input::KeyboardAccelerator{}; + + // TODO: Modify this when https://github.com/microsoft/terminal/issues/877 is resolved + menuShortcut.Key(static_cast(keyChord.Vkey())); + + // add the modifiers to the shortcut + menuShortcut.Modifiers(keyChord.Modifiers()); + + // add to the menu + menuItem.KeyboardAccelerators().Append(menuShortcut); + } + else // we've got a comma, so need to just use the alternate method +#endif + { + // extract the modifier and key to a nice format + auto overrideString = _FormatOverrideShortcutText(keyChord.Modifiers()); + auto mappedCh = MapVirtualKeyW(keyChord.Vkey(), MAPVK_VK_TO_CHAR); + if (mappedCh != 0) + { + menuItem.KeyboardAcceleratorTextOverride(overrideString + gsl::narrow_cast(mappedCh)); + } + } + } + + // Method Description: + // - Calculates the appropriate size to snap to in the given direction, for + // the given dimension. If the global setting `snapToGridOnResize` is set + // to `false`, this will just immediately return the provided dimension, + // effectively disabling snapping. + // - See Pane::CalcSnappedDimension + float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const + { + if (_settings && _settings.GlobalSettings().SnapToGridOnResize()) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->CalcSnappedDimension(widthOrHeight, dimension); + } + } + return dimension; + } + + // Function Description: + // - This function is called when the `TermControl` requests that we send + // it the clipboard's content. + // - Retrieves the data from the Windows Clipboard and converts it to text. + // - Shows warnings if the clipboard is too big or contains multiple lines + // of text. + // - Sends the text back to the TermControl through the event's + // `HandleClipboardData` member function. + // - Does some of this in a background thread, as to not hang/crash the UI thread. + // Arguments: + // - eventArgs: the PasteFromClipboard event sent from the TermControl + safe_void_coroutine TerminalPage::_PasteFromClipboardHandler(const IInspectable sender, const PasteFromClipboardEventArgs eventArgs) + try + { + // The old Win32 clipboard API as used below is somewhere in the order of 300-1000x faster than + // the WinRT one on average, depending on CPU load. Don't use the WinRT clipboard API if you can. + const auto weakThis = get_weak(); + const auto dispatcher = Dispatcher(); + const auto globalSettings = _settings.GlobalSettings(); + const auto bracketedPaste = eventArgs.BracketedPasteEnabled(); + const auto sourceId = sender.try_as().Id(); + + // GetClipboardData might block for up to 30s for delay-rendered contents. + co_await winrt::resume_background(); + + winrt::hstring text; + if (const auto clipboard = clipboard::open(nullptr)) + { + text = clipboard::read(); + } + + if (!bracketedPaste && globalSettings.TrimPaste()) + { + text = winrt::hstring{ Utils::TrimPaste(text) }; + } + + if (text.empty()) + { + co_return; + } + + bool warnMultiLine = false; + switch (globalSettings.WarnAboutMultiLinePaste()) + { + case WarnAboutMultiLinePaste::Automatic: + // NOTE that this is unsafe, because a shell that doesn't support bracketed paste + // will allow an attacker to enable the mode, not realize that, and then accept + // the paste as if it was a series of legitimate commands. See GH#13014. + warnMultiLine = !bracketedPaste; + break; + case WarnAboutMultiLinePaste::Always: + warnMultiLine = true; + break; + default: + warnMultiLine = false; + break; + } + + if (warnMultiLine) + { + const std::wstring_view view{ text }; + warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos; + } + + constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB + const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); + + if (warnMultiLine || warnLargeText) + { + co_await wil::resume_foreground(dispatcher); + + if (const auto strongThis = weakThis.get()) + { + // We have to initialize the dialog here to be able to change the text of the text block within it + std::ignore = FindName(L"MultiLinePasteDialog"); + + // WinUI absolutely cannot deal with large amounts of text (at least O(n), possibly O(n^2), + // so we limit the string length here and add an ellipsis if necessary. + auto clipboardText = text; + if (clipboardText.size() > 1024) + { + const std::wstring_view view{ text }; + // Make sure we don't cut in the middle of a surrogate pair + const auto len = til::utf16_iterate_prev(view, 512); + clipboardText = til::hstring_format(FMT_COMPILE(L"{}\n…"), view.substr(0, len)); + } + + ClipboardText().Text(std::move(clipboardText)); + + // The vertical offset on the scrollbar does not reset automatically, so reset it manually + ClipboardContentScrollViewer().ScrollToVerticalOffset(0); + + auto warningResult = ContentDialogResult::Primary; + if (warnMultiLine) + { + warningResult = co_await _ShowMultiLinePasteWarningDialog(); + } + else if (warnLargeText) + { + warningResult = co_await _ShowLargePasteWarningDialog(); + } + + // Clear the clipboard text so it doesn't lie around in memory + ClipboardText().Text({}); + + if (warningResult != ContentDialogResult::Primary) + { + // user rejected the paste + co_return; + } + } + + co_await winrt::resume_background(); + } + + // This will end up calling ConptyConnection::WriteInput which calls WriteFile which may block for + // an indefinite amount of time. Avoid freezes and deadlocks by running this on a background thread. + assert(!dispatcher.HasThreadAccess()); + eventArgs.HandleClipboardData(text); + + // GH#18821: If broadcast input is active, paste the same text into all other + // panes on the tab. We do this here (rather than re-reading the + // clipboard per-pane) so that only one paste warning is shown. + co_await wil::resume_foreground(dispatcher); + if (const auto strongThis = weakThis.get()) + { + if (const auto& tab{ strongThis->_GetFocusedTabImpl() }) + { + if (tab->TabStatus().IsInputBroadcastActive()) + { + tab->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto control = pane->GetTerminalControl()) + { + if (control.ContentId() != sourceId && !control.ReadOnly()) + { + control.RawWriteString(text); + } + } + }); + } + } + } + } + CATCH_LOG(); + + void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs) + { + try + { + auto parsed = winrt::Windows::Foundation::Uri(eventArgs.Uri()); + if (_IsUriSupported(parsed)) + { + ShellExecute(nullptr, L"open", eventArgs.Uri().c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + else + { + _ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), eventArgs.Uri()); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _ShowCouldNotOpenDialog(RS_(L"InvalidUriText"), eventArgs.Uri()); + } + } + + // Method Description: + // - Opens up a dialog box explaining why we could not open a URI + // Arguments: + // - The reason (unsupported scheme, invalid uri, potentially more in the future) + // - The uri + void TerminalPage::_ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri) + { + if (auto presenter{ _dialogPresenter.get() }) + { + // FindName needs to be called first to actually load the xaml object + auto unopenedUriDialog = FindName(L"CouldNotOpenUriDialog").try_as(); + + // Insert the reason and the URI + CouldNotOpenUriReason().Text(reason); + UnopenedUri().Text(uri); + + // Show the dialog + presenter.ShowDialog(unopenedUriDialog); + } + } + + // Method Description: + // - Determines if the given URI is currently supported + // Arguments: + // - The parsed URI + // Return value: + // - True if we support it, false otherwise + bool TerminalPage::_IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri) + { + if (parsedUri.SchemeName() == L"http" || parsedUri.SchemeName() == L"https") + { + return true; + } + if (parsedUri.SchemeName() == L"file") + { + const auto host = parsedUri.Host(); + // If no hostname was provided or if the hostname was "localhost", Host() will return an empty string + // and we allow it + if (host == L"") + { + return true; + } + + // GH#10188: WSL paths are okay. We'll let those through. + if (host == L"wsl$" || host == L"wsl.localhost") + { + return true; + } + + // TODO: by the OSC 8 spec, if a hostname (other than localhost) is provided, we _should_ be + // comparing that value against what is returned by GetComputerNameExW and making sure they match. + // However, ShellExecute does not seem to be happy with file URIs of the form + // file://{hostname}/path/to/file.ext + // and so while we could do the hostname matching, we do not know how to actually open the URI + // if its given in that form. So for now we ignore all hostnames other than localhost + return false; + } + + // In this case, the app manually output a URI other than file:// or + // http(s)://. We'll trust the user knows what they're doing when + // clicking on those sorts of links. + // See discussion in GH#7562 for more details. + return true; + } + + // Important! Don't take this eventArgs by reference, we need to extend the + // lifetime of it to the other side of the co_await! + safe_void_coroutine TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, + const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) + { + auto weakThis = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + if (auto page = weakThis.get()) + { + auto message = eventArgs.Message(); + + winrt::hstring title; + + switch (eventArgs.Level()) + { + case NoticeLevel::Debug: + title = RS_(L"NoticeDebug"); //\xebe8 + break; + case NoticeLevel::Info: + title = RS_(L"NoticeInfo"); // \xe946 + break; + case NoticeLevel::Warning: + title = RS_(L"NoticeWarning"); //\xe7ba + break; + case NoticeLevel::Error: + title = RS_(L"NoticeError"); //\xe783 + break; + } + + page->_ShowControlNoticeDialog(title, message); + } + } + + void TerminalPage::_ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message) + { + if (auto presenter{ _dialogPresenter.get() }) + { + // FindName needs to be called first to actually load the xaml object + auto controlNoticeDialog = FindName(L"ControlNoticeDialog").try_as(); + + ControlNoticeDialog().Title(winrt::box_value(title)); + + // Insert the message + NoticeMessage().Text(message); + + // Show the dialog + presenter.ShowDialog(controlNoticeDialog); + } + } + + // Method Description: + // - Copy text from the focused terminal to the Windows Clipboard + // Arguments: + // - dismissSelection: if not enabled, copying text doesn't dismiss the selection + // - singleLine: if enabled, copy contents as a single line of text + // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection + // - formats: dictate which formats need to be copied + // Return Value: + // - true iff we we able to copy text (if a selection was active) + bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats) + { + if (const auto& control{ _GetActiveControl() }) + { + return control.CopySelectionToClipboard(dismissSelection, singleLine, withControlSequences, formats); + } + return false; + } + + // Method Description: + // - Send an event (which will be caught by AppHost) to set the progress indicator on the taskbar + // Arguments: + // - sender (not used) + // - eventArgs: the arguments specifying how to set the progress indicator + safe_void_coroutine TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) + { + const auto weak = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + if (const auto strong = weak.get()) + { + SetTaskbarProgress.raise(*this, nullptr); + } + } + + // Method Description: + // - Send an event (which will be caught by AppHost) to change the show window state of the entire hosting window + // Arguments: + // - sender (not used) + // - args: the arguments specifying how to set the display status to ShowWindow for our window handle + void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) + { + ShowWindowChanged.raise(*this, args); + } + + Windows::Foundation::IAsyncOperation> TerminalPage::_FindPackageAsync(hstring query) + { + const PackageManager packageManager = WindowsPackageManagerFactory::CreatePackageManager(); + PackageCatalogReference catalogRef{ + packageManager.GetPredefinedPackageCatalog(PredefinedPackageCatalog::OpenWindowsCatalog) + }; + catalogRef.PackageCatalogBackgroundUpdateInterval(std::chrono::hours(24)); + + ConnectResult connectResult{ nullptr }; + for (int retries = 0;;) + { + connectResult = catalogRef.Connect(); + if (connectResult.Status() == ConnectResultStatus::Ok) + { + break; + } + + if (++retries == 3) + { + co_return nullptr; + } + } + + PackageCatalog catalog = connectResult.PackageCatalog(); + PackageMatchFilter filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); + filter.Value(query); + filter.Field(PackageMatchField::Command); + filter.Option(PackageFieldMatchOption::Equals); + + FindPackagesOptions options = WindowsPackageManagerFactory::CreateFindPackagesOptions(); + options.Filters().Append(filter); + options.ResultLimit(20); + + const auto result = co_await catalog.FindPackagesAsync(options); + const IVectorView pkgList = result.Matches(); + co_return pkgList; + } + + Windows::Foundation::IAsyncAction TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args) + { + if (!Feature_QuickFix::IsEnabled()) + { + co_return; + } + + const auto weak = get_weak(); + const auto dispatcher = Dispatcher(); + + // All of the code until resume_foreground is static and + // doesn't touch `this`, so we don't need weak/strong_ref. + co_await winrt::resume_background(); + + // no packages were found, nothing to suggest + const auto pkgList = co_await _FindPackageAsync(args.MissingCommand()); + if (!pkgList || pkgList.Size() == 0) + { + co_return; + } + + std::vector suggestions; + suggestions.reserve(pkgList.Size()); + for (const auto& pkg : pkgList) + { + // --id and --source ensure we don't collide with another package catalog + suggestions.emplace_back(fmt::format(FMT_COMPILE(L"winget install --id {} -s winget"), pkg.CatalogPackage().Id())); + } + + co_await wil::resume_foreground(dispatcher); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + auto term = _GetActiveControl(); + if (!term) + { + co_return; + } + term.UpdateWinGetSuggestions(single_threaded_vector(std::move(suggestions))); + term.RefreshQuickFixMenu(); + } + + void TerminalPage::_WindowSizeChanged(const IInspectable sender, const Microsoft::Terminal::Control::WindowSizeChangedEventArgs args) + { + // Raise if: + // - Not in quake mode + // - Not in fullscreen + // - Only one tab exists + // - Only one pane exists + // else: + // - Reset conpty to its original size back + if (!WindowProperties().IsQuakeWindow() && !Fullscreen() && + NumberOfTabs() == 1 && _GetFocusedTabImpl()->GetLeafPaneCount() == 1) + { + WindowSizeChanged.raise(*this, args); + } + else if (const auto& control{ sender.try_as() }) + { + const auto& connection = control.Connection(); + + if (const auto& conpty{ connection.try_as() }) + { + conpty.ResetSize(); + } + } + } + + void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const + { + if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) + { + const auto plain = args.Plain(); + const auto html = args.Html(); + const auto rtf = args.Rtf(); + + clipboard::write( + { plain.data(), plain.size() }, + { reinterpret_cast(html.data()), html.size() }, + { reinterpret_cast(rtf.data()), rtf.size() }); + } + } + + // Method Description: + // - Paste text from the Windows Clipboard to the focused terminal + void TerminalPage::_PasteText() + { + if (const auto& control{ _GetActiveControl() }) + { + control.PasteTextFromClipboard(); + } + } + + // Function Description: + // - Called when the settings button is clicked. ShellExecutes the settings + // file, as to open it in the default editor for .json files. Does this in + // a background thread, as to not hang/crash the UI thread. + safe_void_coroutine TerminalPage::_LaunchSettings(const SettingsTarget target) + { + if (target == SettingsTarget::SettingsUI) + { + OpenSettingsUI(); + } + else + { + // This will switch the execution of the function to a background (not + // UI) thread. This is IMPORTANT, because the Windows.Storage API's + // (used for retrieving the path to the file) will crash on the UI + // thread, because the main thread is a STA. + // + // NOTE: All remaining code of this function doesn't touch `this`, so we don't need weak/strong_ref. + // NOTE NOTE: Don't touch `this` when you make changes here. + co_await winrt::resume_background(); + + auto openFile = [](const auto& filePath) { + HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); + if (static_cast(reinterpret_cast(res)) <= 32) + { + ShellExecute(nullptr, nullptr, L"notepad", filePath.c_str(), nullptr, SW_SHOW); + } + }; + + auto openFolder = [](const auto& filePath) { + HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); + if (static_cast(reinterpret_cast(res)) <= 32) + { + ShellExecute(nullptr, nullptr, L"open", filePath.c_str(), nullptr, SW_SHOW); + } + }; + + switch (target) + { + case SettingsTarget::DefaultsFile: + openFile(CascadiaSettings::DefaultSettingsPath()); + break; + case SettingsTarget::SettingsFile: + openFile(CascadiaSettings::SettingsPath()); + break; + case SettingsTarget::Directory: + openFolder(CascadiaSettings::SettingsDirectory()); + break; + case SettingsTarget::AllFiles: + openFile(CascadiaSettings::DefaultSettingsPath()); + openFile(CascadiaSettings::SettingsPath()); + break; + } + } + } + + // Method Description: + // - Responds to the TabView control's Tab Closing event by removing + // the indicated tab from the set and focusing another one. + // The event is cancelled so App maintains control over the + // items in the tabview. + // Arguments: + // - sender: the control that originated this event + // - eventArgs: the event's constituent arguments + void TerminalPage::_OnTabCloseRequested(const IInspectable& /*sender*/, const MUX::Controls::TabViewTabCloseRequestedEventArgs& eventArgs) + { + const auto tabViewItem = eventArgs.Tab(); + if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) + { + _HandleCloseTabRequested(tab); + } + } + + TermControl TerminalPage::_CreateNewControlAndContent(const Settings::TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) + { + // Do any initialization that needs to apply to _every_ TermControl we + // create here. + const auto content = _manager.CreateCore(*settings.DefaultSettings(), settings.UnfocusedSettings().try_as(), connection); + const TermControl control{ content }; + return _SetupControl(control); + } + + TermControl TerminalPage::_AttachControlToContent(const uint64_t& contentId) + { + if (const auto& content{ _manager.TryLookupCore(contentId) }) + { + // We have to pass in our current keybindings, because that's an + // object that belongs to this TerminalPage, on this thread. If we + // don't, then when we move the content to another thread, and it + // tries to handle a key, it'll callback on the original page's + // stack, inevitably resulting in a wrong_thread + return _SetupControl(TermControl::NewControlByAttachingContent(content)); + } + return nullptr; + } + + TermControl TerminalPage::_SetupControl(const TermControl& term) + { + // GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now. + if (_visible) + { + term.WindowVisibilityChanged(_visible); + } + + // Even in the case of re-attaching content from another window, this + // will correctly update the control's owning HWND + if (_hostingHwnd.has_value()) + { + term.OwningHwnd(reinterpret_cast(*_hostingHwnd)); + } + + term.KeyBindings(*_bindings); + + _RegisterTerminalEvents(term); + return term; + } + + // Method Description: + // - Creates a pane and returns a shared_ptr to it + // - The caller should handle where the pane goes after creation, + // either to split an already existing pane or to create a new tab with it + // Arguments: + // - newTerminalArgs: an object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + // - sourceTab: an optional tab reference that indicates that the created + // pane should be a duplicate of the tab's focused pane + // - existingConnection: optionally receives a connection from the outside + // world instead of attempting to create one + // Return Value: + // - If the newTerminalArgs required us to open the pane as a new elevated + // connection, then we'll return nullptr. Otherwise, we'll return a new + // Pane for this connection. + std::shared_ptr TerminalPage::_MakeTerminalPane(const NewTerminalArgs& newTerminalArgs, + const winrt::TerminalApp::Tab& sourceTab, + TerminalConnection::ITerminalConnection existingConnection) + { + // First things first - Check for making a pane from content ID. + if (newTerminalArgs && + newTerminalArgs.ContentId() != 0) + { + // Don't need to worry about duplicating or anything - we'll + // serialize the actual profile's GUID along with the content guid. + const auto& profile = _settings.GetProfileForArgs(newTerminalArgs); + const auto control = _AttachControlToContent(newTerminalArgs.ContentId()); + auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; + return std::make_shared(paneContent); + } + + Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; + Profile profile{ nullptr }; + + if (const auto& tabImpl{ _GetTabImpl(sourceTab) }) + { + profile = tabImpl->GetFocusedProfile(); + if (profile) + { + // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. + profile = GetClosestProfileForDuplicationOfProfile(profile); + controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); + const auto workingDirectory = tabImpl->GetActiveTerminalControl().WorkingDirectory(); + const auto validWorkingDirectory = !workingDirectory.empty(); + if (validWorkingDirectory) + { + controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); + } + } + } + if (!profile) + { + profile = _settings.GetProfileForArgs(newTerminalArgs); + controlSettings = Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs); + } + + // Try to handle auto-elevation + if (_maybeElevate(newTerminalArgs, controlSettings, profile)) + { + return nullptr; + } + + const auto sessionId = controlSettings.DefaultSettings()->SessionId(); + const auto hasSessionId = sessionId != winrt::guid{}; + + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), hasSessionId); + if (existingConnection) + { + connection.Resize(controlSettings.DefaultSettings()->InitialRows(), controlSettings.DefaultSettings()->InitialCols()); + } + + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + const auto window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const auto bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (bothAltsPressed) + { + std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); + } + } + + const auto control = _CreateNewControlAndContent(controlSettings, connection); + + if (hasSessionId) + { + using namespace std::string_view_literals; + + const auto settingsDir = CascadiaSettings::SettingsDirectory(); + const auto admin = IsRunningElevated(); + const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv; + const auto path = fmt::format(FMT_COMPILE(L"{}\\{}{}.txt"), settingsDir, filenamePrefix, sessionId); + control.RestoreFromPath(path); + } + + auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; + + auto resultPane = std::make_shared(paneContent); + + if (debugConnection) // this will only be set if global debugging is on and tap is active + { + auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection); + // Split (auto) with the debug tap. + auto debugContent{ winrt::make(profile, _terminalSettingsCache, newControl) }; + auto debugPane = std::make_shared(debugContent); + + // Since we're doing this split directly on the pane (instead of going through Tab, + // we need to handle the panes 'active' states + + // Set the pane we're splitting to active (otherwise Split will not do anything) + resultPane->SetActive(); + auto [original, _] = resultPane->Split(SplitDirection::Automatic, 0.5f, debugPane); + + // Set the non-debug pane as active + resultPane->ClearActive(); + original->SetActive(); + } + + return resultPane; + } + + // NOTE: callers of _MakePane should be able to accept nullptr as a return + // value gracefully. + std::shared_ptr TerminalPage::_MakePane(const INewContentArgs& contentArgs, + const winrt::TerminalApp::Tab& sourceTab, + TerminalConnection::ITerminalConnection existingConnection) + + { + const auto& newTerminalArgs{ contentArgs.try_as() }; + if (contentArgs == nullptr || newTerminalArgs != nullptr || contentArgs.Type().empty()) + { + // Terminals are of course special, and have to deal with debug taps, duplicating the tab, etc. + return _MakeTerminalPane(newTerminalArgs, sourceTab, existingConnection); + } + + IPaneContent content{ nullptr }; + + const auto& paneType{ contentArgs.Type() }; + if (paneType == L"scratchpad") + { + const auto& scratchPane{ winrt::make_self() }; + + // This is maybe a little wacky - add our key event handler to the pane + // we made. So that we can get actions for keys that the content didn't + // handle. + scratchPane->GetRoot().KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); + + content = *scratchPane; + } + else if (paneType == L"settings") + { + content = _makeSettingsContent(); + } + else if (paneType == L"snippets") + { + // Prevent the user from opening a bunch of snippets panes. + // + // Look at the focused tab, and if it already has one, then just focus it. + if (const auto& focusedTab{ _GetFocusedTabImpl() }) + { + const auto rootPane{ focusedTab->GetRootPane() }; + const bool found = rootPane == nullptr ? false : rootPane->WalkTree([](const auto& p) -> bool { + if (const auto& snippets{ p->GetContent().try_as() }) + { + snippets->Focus(FocusState::Programmatic); + return true; + } + return false; + }); + // Bail out if we already found one. + if (found) + { + return nullptr; + } + } + + const auto& tasksContent{ winrt::make_self() }; + tasksContent->UpdateSettings(_settings); + tasksContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + tasksContent->DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + if (const auto& termControl{ _GetActiveControl() }) + { + tasksContent->SetLastActiveControl(termControl); + } + + content = *tasksContent; + } + else if (paneType == L"x-markdown") + { + if (Feature_MarkdownPane::IsEnabled()) + { + const auto& markdownContent{ winrt::make_self(L"") }; + markdownContent->UpdateSettings(_settings); + markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + + // This one doesn't use DispatchCommand, because we don't create + // Command's freely at runtime like we do with just plain old actions. + markdownContent->DispatchActionRequested([weak = get_weak()](const auto& sender, const auto& actionAndArgs) { + if (const auto& page{ weak.get() }) + { + page->_actionDispatch->DoAction(sender, actionAndArgs); + } + }); + if (const auto& termControl{ _GetActiveControl() }) + { + markdownContent->SetLastActiveControl(termControl); + } + + content = *markdownContent; + } + } + + assert(content); + + return std::make_shared(content); + } + + void TerminalPage::_restartPaneConnection( + const TerminalApp::TerminalPaneContent& paneContent, + const winrt::Windows::Foundation::IInspectable&) + { + // Note: callers are likely passing in `nullptr` as the args here, as + // the TermControl.RestartTerminalRequested event doesn't actually pass + // any args upwards itself. If we ever change this, make sure you check + // for nulls + if (const auto& connection{ _duplicateConnectionForRestart(paneContent) }) + { + paneContent.GetTermControl().Connection(connection); + connection.Start(); + } + } + + // Method Description: + // - Sets background image and applies its settings (stretch, opacity and alignment) + // - Checks path validity + // Arguments: + // - newAppearance + // Return Value: + // - + void TerminalPage::_SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance) + { + if (!_settings.GlobalSettings().UseBackgroundImageForWindow()) + { + _tabContent.Background(nullptr); + return; + } + + const auto path = newAppearance.BackgroundImagePath().Resolved(); + if (path.empty()) + { + _tabContent.Background(nullptr); + return; + } + + Windows::Foundation::Uri imageUri{ nullptr }; + try + { + imageUri = Windows::Foundation::Uri{ path }; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _tabContent.Background(nullptr); + return; + } + // Check if the image brush is already pointing to the image + // in the modified settings; if it isn't (or isn't there), + // set a new image source for the brush + + auto brush = _tabContent.Background().try_as(); + Media::Imaging::BitmapImage imageSource = brush == nullptr ? nullptr : brush.ImageSource().try_as(); + + if (imageSource == nullptr || + imageSource.UriSource() == nullptr || + !imageSource.UriSource().Equals(imageUri)) + { + Media::ImageBrush b{}; + // Note that BitmapImage handles the image load asynchronously, + // which is especially important since the image + // may well be both large and somewhere out on the + // internet. + Media::Imaging::BitmapImage image(imageUri); + b.ImageSource(image); + _tabContent.Background(b); + } + + // Pull this into a separate block. If the image didn't change, but the + // properties of the image did, we should still update them. + if (const auto newBrush{ _tabContent.Background().try_as() }) + { + newBrush.Stretch(newAppearance.BackgroundImageStretchMode()); + newBrush.Opacity(newAppearance.BackgroundImageOpacity()); + } + } + + // Method Description: + // - Hook up keybindings, and refresh the UI of the terminal. + // This includes update the settings of all the tabs according + // to their profiles, update the title and icon of each tab, and + // finally create the tab flyout + void TerminalPage::_RefreshUIForSettingsReload() + { + // Re-wire the keybindings to their handlers, as we'll have created a + // new AppKeyBindings object. + _HookupKeyBindings(_settings.ActionMap()); + + // Refresh UI elements + + // Recreate the TerminalSettings cache here. We'll use that as we're + // updating terminal panes, so that we don't have to build a _new_ + // TerminalSettings for every profile we update - we can just look them + // up the previous ones we built. + _terminalSettingsCache->Reset(_settings); + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // Let the tab know that there are new settings. It's up to each content to decide what to do with them. + tabImpl->UpdateSettings(_settings); + + // Update the icon of the tab for the currently focused profile in that tab. + // Only do this for TerminalTabs. Other types of tabs won't have multiple panes + // and profiles so the Title and Icon will be set once and only once on init. + _UpdateTabIcon(*tabImpl); + + // Force the TerminalTab to re-grab its currently active control's title. + tabImpl->UpdateTitle(); + } + + auto tabImpl{ winrt::get_self(tab) }; + tabImpl->SetActionMap(_settings.ActionMap()); + } + + if (const auto focusedTab{ _GetFocusedTabImpl() }) + { + if (const auto profile{ focusedTab->GetFocusedProfile() }) + { + _SetBackgroundImage(profile.DefaultAppearance()); + } + } + + // repopulate the new tab button's flyout with entries for each + // profile, which might have changed + _UpdateTabWidthMode(); + _CreateNewTabFlyout(); + + // Reload the current value of alwaysOnTop from the settings file. This + // will let the user hot-reload this setting, but any runtime changes to + // the alwaysOnTop setting will be lost. + _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); + AlwaysOnTopChanged.raise(*this, nullptr); + + _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); + + // Settings AllowDependentAnimations will affect whether animations are + // enabled application-wide, so we don't need to check it each time we + // want to create an animation. + WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; + _tabView.Background(transparent); + + //////////////////////////////////////////////////////////////////////// + // Begin Theme handling + _updateThemeColors(); + + _updateAllTabCloseButtons(); + + // The user may have changed the "show title in titlebar" setting. + TitleChanged.raise(*this, nullptr); + } + + void TerminalPage::_updateAllTabCloseButtons() + { + // Update the state of the CloseButtonOverlayMode property of + // our TabView, to match the tab.showCloseButton property in the theme. + // + // Also update every tab's individual IsClosable to match the same property. + const auto theme = _settings.GlobalSettings().CurrentTheme(); + const auto visibility = (theme && theme.Tab()) ? + theme.Tab().ShowCloseButton() : + Settings::Model::TabCloseButtonVisibility::Always; + + _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; + + for (const auto& tab : _tabs) + { + tab.CloseButtonVisibility(visibility); + } + + switch (visibility) + { + case Settings::Model::TabCloseButtonVisibility::Never: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); + break; + case Settings::Model::TabCloseButtonVisibility::Hover: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); + break; + case Settings::Model::TabCloseButtonVisibility::ActiveOnly: + default: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); + break; + } + } + + // Method Description: + // - Sets the initial actions to process on startup. We'll make a copy of + // this list, and process these actions when we're loaded. + // - This function will have no effective result after Create() is called. + // Arguments: + // - actions: a list of Actions to process on startup. + // Return Value: + // - + void TerminalPage::SetStartupActions(std::vector actions) + { + _startupActions = std::move(actions); + } + + void TerminalPage::SetStartupConnection(ITerminalConnection connection) + { + _startupConnection = std::move(connection); + } + + winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const + { + return _dialogPresenter.get(); + } + + void TerminalPage::DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter) + { + _dialogPresenter = dialogPresenter; + } + + // Method Description: + // - Get the combined taskbar state for the page. This is the combination of + // all the states of all the tabs, which are themselves a combination of + // all their panes. Taskbar states are given a priority based on the rules + // in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a Group" + // Arguments: + // - + // Return Value: + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our tabs. + winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const + { + auto state{ winrt::make() }; + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + auto tabState{ tabImpl->GetCombinedTaskbarState() }; + // lowest priority wins + if (tabState.Priority() < state.Priority()) + { + state = tabState; + } + } + } + + return state; + } + + // Method Description: + // - This is the method that App will call when the titlebar + // has been clicked. It dismisses any open flyouts. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::TitlebarClicked() + { + if (_newTabButton && _newTabButton.Flyout()) + { + _newTabButton.Flyout().Hide(); + } + _DismissTabContextMenus(); + } + + // Method Description: + // - Notifies all attached console controls that the visibility of the + // hosting window has changed. The underlying PTYs may need to know this + // for the proper response to `::GetConsoleWindow()` from a Win32 console app. + // Arguments: + // - showOrHide: Show is true; hide is false. + // Return Value: + // - + void TerminalPage::WindowVisibilityChanged(const bool showOrHide) + { + _visible = showOrHide; + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings + // objects but only have to iterate one time. + tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { + if (auto control = pane->GetTerminalControl()) + { + control.WindowVisibilityChanged(showOrHide); + } + }); + } + } + } + + // Method Description: + // - Called when the user tries to do a search using keybindings. + // This will tell the active terminal control of the passed tab + // to create a search box and enable find process. + // Arguments: + // - tab: the tab where the search box should be created + // Return Value: + // - + void TerminalPage::_Find(const Tab& tab) + { + if (const auto& control{ tab.GetActiveTerminalControl() }) + { + control.CreateSearchBoxControl(); + } + } + + // Method Description: + // - Toggles borderless mode. Hides the tab row, and raises our + // FocusModeChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleFocusMode() + { + SetFocusMode(!_isInFocusMode); + } + + void TerminalPage::SetFocusMode(const bool inFocusMode) + { + const auto newInFocusMode = inFocusMode; + if (newInFocusMode != FocusMode()) + { + _isInFocusMode = newInFocusMode; + _UpdateTabView(); + FocusModeChanged.raise(*this, nullptr); + } + } + + // Method Description: + // - Toggles fullscreen mode. Hides the tab row, and raises our + // FullscreenChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleFullscreen() + { + SetFullscreen(!_isFullscreen); + } + + // Method Description: + // - Toggles always on top mode. Raises our AlwaysOnTopChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleAlwaysOnTop() + { + _isAlwaysOnTop = !_isAlwaysOnTop; + AlwaysOnTopChanged.raise(*this, nullptr); + } + + // Method Description: + // - Sets the tab split button color when a new tab color is selected + // Arguments: + // - color: The color of the newly selected tab, used to properly calculate + // the foreground color of the split button (to match the font + // color of the tab) + // - accentColor: the actual color we are going to use to paint the tab row and + // split button, so that there is some contrast between the tab + // and the non-client are behind it + // Return Value: + // - + void TerminalPage::_SetNewTabButtonColor(const til::color color, const til::color accentColor) + { + constexpr auto lightnessThreshold = 0.6f; + // TODO GH#3327: Look at what to do with the tab button when we have XAML theming + const auto isBrightColor = ColorFix::GetLightness(color) >= lightnessThreshold; + const auto isLightAccentColor = ColorFix::GetLightness(accentColor) >= lightnessThreshold; + const auto hoverColorAdjustment = isLightAccentColor ? -0.05f : 0.05f; + const auto pressedColorAdjustment = isLightAccentColor ? -0.1f : 0.1f; + + const auto foregroundColor = isBrightColor ? Colors::Black() : Colors::White(); + const auto hoverColor = til::color{ ColorFix::AdjustLightness(accentColor, hoverColorAdjustment) }; + const auto pressedColor = til::color{ ColorFix::AdjustLightness(accentColor, pressedColorAdjustment) }; + + Media::SolidColorBrush backgroundBrush{ accentColor }; + Media::SolidColorBrush backgroundHoverBrush{ hoverColor }; + Media::SolidColorBrush backgroundPressedBrush{ pressedColor }; + Media::SolidColorBrush foregroundBrush{ foregroundColor }; + + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackground"), backgroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPointerOver"), backgroundHoverBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPressed"), backgroundPressedBrush); + + // Load bearing: The SplitButton uses SplitButtonForegroundSecondary for + // the secondary button, but {TemplateBinding Foreground} for the + // primary button. + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForeground"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPointerOver"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPressed"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondary"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondaryPressed"), foregroundBrush); + + _newTabButton.Background(backgroundBrush); + _newTabButton.Foreground(foregroundBrush); + + // This is just like what we do in Tab::_RefreshVisualState. We need + // to manually toggle the visual state, so the setters in the visual + // state group will re-apply, and set our currently selected colors in + // the resources. + VisualStateManager::GoToState(_newTabButton, L"FlyoutOpen", true); + VisualStateManager::GoToState(_newTabButton, L"Normal", true); + } + + // Method Description: + // - Clears the tab split button color to a system color + // (or white if none is found) when the tab's color is cleared + // - Clears the tab row color to a system color + // (or white if none is found) when the tab's color is cleared + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ClearNewTabButtonColor() + { + // TODO GH#3327: Look at what to do with the tab button when we have XAML theming + winrt::hstring keys[] = { + L"SplitButtonBackground", + L"SplitButtonBackgroundPointerOver", + L"SplitButtonBackgroundPressed", + L"SplitButtonForeground", + L"SplitButtonForegroundSecondary", + L"SplitButtonForegroundPointerOver", + L"SplitButtonForegroundPressed", + L"SplitButtonForegroundSecondaryPressed" + }; + + // simply clear any of the colors in the split button's dict + for (auto keyString : keys) + { + auto key = winrt::box_value(keyString); + if (_newTabButton.Resources().HasKey(key)) + { + _newTabButton.Resources().Remove(key); + } + } + + const auto res = Application::Current().Resources(); + + const auto defaultBackgroundKey = winrt::box_value(L"TabViewItemHeaderBackground"); + const auto defaultForegroundKey = winrt::box_value(L"SystemControlForegroundBaseHighBrush"); + winrt::Windows::UI::Xaml::Media::SolidColorBrush backgroundBrush; + winrt::Windows::UI::Xaml::Media::SolidColorBrush foregroundBrush; + + // TODO: Related to GH#3917 - I think if the system is set to "Dark" + // theme, but the app is set to light theme, then this lookup still + // returns to us the dark theme brushes. There's gotta be a way to get + // the right brushes... + // See also GH#5741 + if (res.HasKey(defaultBackgroundKey)) + { + auto obj = res.Lookup(defaultBackgroundKey); + backgroundBrush = obj.try_as(); + } + else + { + backgroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Black() }; + } + + if (res.HasKey(defaultForegroundKey)) + { + auto obj = res.Lookup(defaultForegroundKey); + foregroundBrush = obj.try_as(); + } + else + { + foregroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::White() }; + } + + _newTabButton.Background(backgroundBrush); + _newTabButton.Foreground(foregroundBrush); + } + + // Function Description: + // - This is a helper method to get the commandline out of a + // ExecuteCommandline action, break it into subcommands, and attempt to + // parse it into actions. This is used by _HandleExecuteCommandline for + // processing commandlines in the current WT window. + // Arguments: + // - args: the ExecuteCommandlineArgs to synthesize a list of startup actions for. + // Return Value: + // - an empty list if we failed to parse; otherwise, a list of actions to execute. + std::vector TerminalPage::ConvertExecuteCommandlineToActions(const ExecuteCommandlineArgs& args) + { + ::TerminalApp::AppCommandlineArgs appArgs; + if (appArgs.ParseArgs(args) == 0) + { + return appArgs.GetStartupActions(); + } + + return {}; + } + + void TerminalPage::_FocusActiveControl(IInspectable /*sender*/, + IInspectable /*eventArgs*/) + { + _FocusCurrentTab(false); + } + + bool TerminalPage::FocusMode() const + { + return _isInFocusMode; + } + + bool TerminalPage::Fullscreen() const + { + return _isFullscreen; + } + + // Method Description: + // - Returns true if we're currently in "Always on top" mode. When we're in + // always on top mode, the window should be on top of all other windows. + // If multiple windows are all "always on top", they'll maintain their own + // z-order, with all the windows on top of all other non-topmost windows. + // Arguments: + // - + // Return Value: + // - true if we should be in "always on top" mode + bool TerminalPage::AlwaysOnTop() const + { + return _isAlwaysOnTop; + } + + // Method Description: + // - Returns true if the tab row should be visible when we're in full screen + // state. + // Arguments: + // - + // Return Value: + // - true if the tab row should be visible in full screen state + bool TerminalPage::ShowTabsFullscreen() const + { + return _showTabsFullscreen; + } + + // Method Description: + // - Updates the visibility of the tab row when in fullscreen state. + void TerminalPage::SetShowTabsFullscreen(bool newShowTabsFullscreen) + { + if (_showTabsFullscreen == newShowTabsFullscreen) + { + return; + } + + _showTabsFullscreen = newShowTabsFullscreen; + + // if we're currently in fullscreen, update tab view to make + // sure tabs are given the correct visibility + if (_isFullscreen) + { + _UpdateTabView(); + } + } + + void TerminalPage::SetFullscreen(bool newFullscreen) + { + if (_isFullscreen == newFullscreen) + { + return; + } + _isFullscreen = newFullscreen; + _UpdateTabView(); + FullscreenChanged.raise(*this, nullptr); + } + + // Method Description: + // - Updates the page's state for isMaximized when the window changes externally. + void TerminalPage::Maximized(bool newMaximized) + { + _isMaximized = newMaximized; + } + + // Method Description: + // - Asks the window to change its maximized state. + void TerminalPage::RequestSetMaximized(bool newMaximized) + { + if (_isMaximized == newMaximized) + { + return; + } + _isMaximized = newMaximized; + ChangeMaximizeRequested.raise(*this, nullptr); + } + + TerminalApp::IPaneContent TerminalPage::_makeSettingsContent() + { + if (auto app{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) + { + if (auto appPrivate{ winrt::get_self(app) }) + { + // Lazily load the Settings UI components so that we don't do it on startup. + appPrivate->PrepareForSettingsUI(); + } + } + + // Create the SUI pane content + auto settingsContent{ winrt::make_self(_settings) }; + auto sui = settingsContent->SettingsUI(); + + if (_hostingHwnd) + { + sui.SetHostingWindow(reinterpret_cast(*_hostingHwnd)); + } + + // GH#8767 - let unhandled keys in the SUI try to run commands too. + sui.KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); + + sui.OpenJson([weakThis{ get_weak() }](auto&& /*s*/, winrt::Microsoft::Terminal::Settings::Model::SettingsTarget e) { + if (auto page{ weakThis.get() }) + { + page->_LaunchSettings(e); + } + }); + + sui.ShowLoadWarningsDialog([weakThis{ get_weak() }](auto&& /*s*/, const Windows::Foundation::Collections::IVectorView& warnings) { + if (auto page{ weakThis.get() }) + { + page->ShowLoadWarningsDialog.raise(*page, warnings); + } + }); + + return *settingsContent; + } + + // Method Description: + // - Creates a settings UI tab and focuses it. If there's already a settings UI tab open, + // just focus the existing one. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::OpenSettingsUI() + { + // If we're holding the settings tab's switch command, don't create a new one, switch to the existing one. + if (!_settingsTab) + { + // Create the tab + auto resultPane = std::make_shared(_makeSettingsContent()); + _settingsTab = _CreateNewTabFromPane(resultPane); + } + else + { + _tabView.SelectedItem(_settingsTab.TabViewItem()); + } + } + + // Method Description: + // - Returns a com_ptr to the implementation type of the given tab if it's a Tab. + // If the tab is not a TerminalTab, returns nullptr. + // Arguments: + // - tab: the projected type of a Tab + // Return Value: + // - If the tab is a TerminalTab, a com_ptr to the implementation type. + // If the tab is not a TerminalTab, nullptr + winrt::com_ptr TerminalPage::_GetTabImpl(const TerminalApp::Tab& tab) + { + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tab)); + return tabImpl; + } + + // Method Description: + // - Computes the delta for scrolling the tab's viewport. + // Arguments: + // - scrollDirection - direction (up / down) to scroll + // - rowsToScroll - the number of rows to scroll + // Return Value: + // - delta - Signed delta, where a negative value means scrolling up. + int TerminalPage::_ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll) + { + return scrollDirection == ScrollUp ? -1 * rowsToScroll : rowsToScroll; + } + + // Method Description: + // - Reads system settings for scrolling (based on the step of the mouse scroll). + // Upon failure fallbacks to default. + // Return Value: + // - The number of rows to scroll or a magic value of WHEEL_PAGESCROLL + // indicating that we need to scroll an entire view height + uint32_t TerminalPage::_ReadSystemRowsToScroll() + { + uint32_t systemRowsToScroll; + if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) + { + LOG_LAST_ERROR(); + + // If SystemParametersInfoW fails, which it shouldn't, fall back to + // Windows' default value. + return DefaultRowsToScroll; + } + + return systemRowsToScroll; + } + + // Method Description: + // - Displays a dialog stating the "Touch Keyboard and Handwriting Panel + // Service" is disabled. + void TerminalPage::ShowKeyboardServiceWarning() const + { + if (!_IsMessageDismissed(InfoBarMessage::KeyboardServiceWarning)) + { + if (const auto keyboardServiceWarningInfoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) + { + keyboardServiceWarningInfoBar.IsOpen(true); + } + } + } + + // Function Description: + // - Helper function to get the OS-localized name for the "Touch Keyboard + // and Handwriting Panel Service". If we can't open up the service for any + // reason, then we'll just return the service's key, "TabletInputService". + // Return Value: + // - The OS-localized name for the TabletInputService + winrt::hstring _getTabletServiceName() + { + wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; + + if (LOG_LAST_ERROR_IF(!hManager.is_valid())) + { + return winrt::hstring{ TabletInputServiceKey }; + } + + DWORD cchBuffer = 0; + const auto ok = GetServiceDisplayNameW(hManager.get(), TabletInputServiceKey.data(), nullptr, &cchBuffer); + + // Windows 11 doesn't have a TabletInputService. + // (It was renamed to TextInputManagementService, because people kept thinking that a + // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) + if (ok || GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + return winrt::hstring{ TabletInputServiceKey }; + } + + std::wstring buffer; + cchBuffer += 1; // Add space for a null + buffer.resize(cchBuffer); + + if (LOG_LAST_ERROR_IF(!GetServiceDisplayNameW(hManager.get(), + TabletInputServiceKey.data(), + buffer.data(), + &cchBuffer))) + { + return winrt::hstring{ TabletInputServiceKey }; + } + return winrt::hstring{ buffer }; + } + + // Method Description: + // - Return the fully-formed warning message for the + // "KeyboardServiceDisabled" InfoBar. This InfoBar is used to warn the user + // if the keyboard service is disabled, and uses the OS localization for + // the service's actual name. It's bound to the bar in XAML. + // Return Value: + // - The warning message, including the OS-localized service name. + winrt::hstring TerminalPage::KeyboardServiceDisabledText() + { + const auto serviceName{ _getTabletServiceName() }; + const auto text{ RS_fmt(L"KeyboardServiceWarningText", serviceName) }; + return winrt::hstring{ text }; + } + + // Method Description: + // - Update the RequestedTheme of the specified FrameworkElement and all its + // Parent elements. We need to do this so that we can actually theme all + // of the elements of the TeachingTip. See GH#9717 + // Arguments: + // - element: The TeachingTip to set the theme on. + // Return Value: + // - + void TerminalPage::_UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element) + { + auto theme{ _settings.GlobalSettings().CurrentTheme() }; + auto requestedTheme{ theme.RequestedTheme() }; + while (element) + { + element.RequestedTheme(requestedTheme); + element = element.Parent().try_as(); + } + } + + // Method Description: + // - Display the name and ID of this window in a TeachingTip. If the window + // has no name, the name will be presented as "". + // - This can be invoked by either: + // * An identifyWindow action, that displays the info only for the current + // window + // * An identifyWindows action, that displays the info for all windows. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::IdentifyWindow() + { + // If we haven't ever loaded the TeachingTip, then do so now and + // create the toast for it. + if (_windowIdToast == nullptr) + { + if (auto tip{ FindName(L"WindowIdToast").try_as() }) + { + _windowIdToast = std::make_shared(tip); + // IsLightDismissEnabled == true is bugged and poorly interacts with multi-windowing. + // It causes the tip to be immediately dismissed when another tip is opened in another window. + tip.IsLightDismissEnabled(false); + // Make sure to use the weak ref when setting up this callback. + tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); + } + } + _UpdateTeachingTipTheme(WindowIdToast().try_as()); + + if (_windowIdToast != nullptr) + { + _windowIdToast->Open(); + } + } + + void TerminalPage::ShowTerminalWorkingDirectory() + { + // If we haven't ever loaded the TeachingTip, then do so now and + // create the toast for it. + if (_windowCwdToast == nullptr) + { + if (auto tip{ FindName(L"WindowCwdToast").try_as() }) + { + _windowCwdToast = std::make_shared(tip); + // Make sure to use the weak ref when setting up this + // callback. + tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); + } + } + _UpdateTeachingTipTheme(WindowCwdToast().try_as()); + + if (_windowCwdToast != nullptr) + { + _windowCwdToast->Open(); + } + } + + // Method Description: + // - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip. + // - Will raise an event that will bubble up to the monarch, asking if this + // name is acceptable. + // - we'll eventually get called back in TerminalPage::WindowName(hstring). + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_WindowRenamerActionClick(const IInspectable& /*sender*/, + const IInspectable& /*eventArgs*/) + { + auto newName = WindowRenamerTextBox().Text(); + _RequestWindowRename(newName); + } + + void TerminalPage::_RequestWindowRename(const winrt::hstring& newName) + { + auto request = winrt::make(newName); + // The WindowRenamer is _not_ a Toast - we want it to stay open until + // the user dismisses it. + if (WindowRenamer()) + { + WindowRenamer().IsOpen(false); + } + RenameWindowRequested.raise(*this, request); + // We can't just use request.Successful here, because the handler might + // (will) be handling this asynchronously, so when control returns to + // us, this hasn't actually been handled yet. We'll get called back in + // RenameFailed if this fails. + // + // Theoretically we could do a IAsyncOperation kind + // of thing with co_return winrt::make(false). + } + + // Method Description: + // - Used to track if the user pressed enter with the renamer open. If we + // immediately focus it after hitting Enter on the command palette, then + // the Enter keydown will dismiss the command palette and open the + // renamer, and then the enter keyup will go to the renamer. So we need to + // make sure both a down and up go to the renamer. + // Arguments: + // - e: the KeyRoutedEventArgs describing the key that was released + // Return Value: + // - + void TerminalPage::_WindowRenamerKeyDown(const IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + if (key == Windows::System::VirtualKey::Enter) + { + _renamerPressedEnter = true; + } + } + + // Method Description: + // - Manually handle Enter and Escape for committing and dismissing a window + // rename. This is highly similar to the TabHeaderControl's KeyUp handler. + // Arguments: + // - e: the KeyRoutedEventArgs describing the key that was released + // Return Value: + // - + void TerminalPage::_WindowRenamerKeyUp(const IInspectable& sender, + const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + if (key == Windows::System::VirtualKey::Enter && _renamerPressedEnter) + { + // User is done making changes, close the rename box + _WindowRenamerActionClick(sender, nullptr); + } + else if (key == Windows::System::VirtualKey::Escape) + { + // User wants to discard the changes they made + WindowRenamerTextBox().Text(_WindowProperties.WindowName()); + WindowRenamer().IsOpen(false); + _renamerPressedEnter = false; + } + } + + // Method Description: + // - This function stops people from duplicating the base profile, because + // it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done. + Profile TerminalPage::GetClosestProfileForDuplicationOfProfile(const Profile& profile) const noexcept + { + if (profile == _settings.ProfileDefaults()) + { + return _settings.FindProfile(_settings.GlobalSettings().DefaultProfile()); + } + return profile; + } + + // Function Description: + // - Helper to launch a new WT instance elevated. It'll do this by spawning + // a helper process, who will asking the shell to elevate the process for + // us. This might cause a UAC prompt. The elevation is performed on a + // background thread, as to not block the UI thread. + // Arguments: + // - newTerminalArgs: A NewTerminalArgs describing the terminal instance + // that should be spawned. The Profile should be filled in with the GUID + // of the profile we want to launch. + // Return Value: + // - + // Important: Don't take the param by reference, since we'll be doing work + // on another thread. + void TerminalPage::_OpenElevatedWT(NewTerminalArgs newTerminalArgs) + { + // BODGY + // + // We're going to construct the commandline we want, then toss it to a + // helper process called `elevate-shim.exe` that happens to live next to + // us. elevate-shim.exe will be the one to call ShellExecute with the + // args that we want (to elevate the given profile). + // + // We can't be the one to call ShellExecute ourselves. ShellExecute + // requires that the calling process stays alive until the child is + // spawned. However, in the case of something like `wt -p + // AlwaysElevateMe`, then the original WT will try to ShellExecute a new + // wt.exe (elevated) and immediately exit, preventing ShellExecute from + // successfully spawning the elevated WT. + + std::filesystem::path exePath = wil::GetModuleFileNameW(nullptr); + exePath.replace_filename(L"elevate-shim.exe"); + + // Build the commandline to pass to wt for this set of NewTerminalArgs + auto cmdline{ + fmt::format(FMT_COMPILE(L"new-tab {}"), newTerminalArgs.ToCommandline()) + }; + + wil::unique_process_information pi; + STARTUPINFOW si{}; + si.cb = sizeof(si); + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(exePath.c_str(), + cmdline.data(), + nullptr, + nullptr, + FALSE, + 0, + nullptr, + nullptr, + &si, + &pi)); + + // TODO: GH#8592 - It may be useful to pop a Toast here in the original + // Terminal window informing the user that the tab was opened in a new + // window. + } + + // Method Description: + // - If the requested settings want us to elevate this new terminal + // instance, and we're not currently elevated, then open the new terminal + // as an elevated instance (using _OpenElevatedWT). Does nothing if we're + // already elevated, or if the control settings don't want to be elevated. + // Arguments: + // - newTerminalArgs: The NewTerminalArgs for this terminal instance + // - controlSettings: The constructed TerminalSettingsCreateResult for this Terminal instance + // - profile: The Profile we're using to launch this Terminal instance + // Return Value: + // - true iff we tossed this request to an elevated window. Callers can use + // this result to early-return if needed. + bool TerminalPage::_maybeElevate(const NewTerminalArgs& newTerminalArgs, + const Settings::TerminalSettingsCreateResult& controlSettings, + const Profile& profile) + { + // When duplicating a tab there aren't any newTerminalArgs. + if (!newTerminalArgs) + { + return false; + } + + const auto defaultSettings = controlSettings.DefaultSettings(); + + // If we don't even want to elevate we can return early. + // If we're already elevated we can also return, because it doesn't get any more elevated than that. + if (!defaultSettings->Elevate() || IsRunningElevated()) + { + return false; + } + + // Manually set the Profile of the NewTerminalArgs to the guid we've + // resolved to. If there was a profile in the NewTerminalArgs, this + // will be that profile's GUID. If there wasn't, then we'll use + // whatever the default profile's GUID is. + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + newTerminalArgs.StartingDirectory(_evaluatePathForCwd(defaultSettings->StartingDirectory())); + _OpenElevatedWT(newTerminalArgs); + return true; + } + + // Method Description: + // - Handles the change of connection state. + // If the connection state is failure show information bar suggesting to configure termination behavior + // (unless user asked not to show this message again) + // Arguments: + // - sender: the ICoreState instance containing the connection state + // Return Value: + // - + safe_void_coroutine TerminalPage::_ConnectionStateChangedHandler(const IInspectable& sender, const IInspectable& /*args*/) + { + if (const auto coreState{ sender.try_as() }) + { + const auto newConnectionState = coreState.ConnectionState(); + const auto weak = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + _adjustProcessPriorityThrottled->Run(); + + if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) + { + if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) + { + infoBar.IsOpen(true); + } + } + } + } + + // Method Description: + // - Persists the user's choice not to show information bar guiding to configure termination behavior. + // Then hides this information buffer. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_CloseOnExitInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const + { + _DismissMessage(InfoBarMessage::CloseOnExitInfo); + if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) + { + infoBar.IsOpen(false); + } + } + + // Method Description: + // - Persists the user's choice not to show information bar warning about "Touch keyboard and Handwriting Panel Service" disabled + // Then hides this information buffer. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_KeyboardServiceWarningInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const + { + _DismissMessage(InfoBarMessage::KeyboardServiceWarning); + if (const auto infoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) + { + infoBar.IsOpen(false); + } + } + + // Method Description: + // - Checks whether information bar message was dismissed earlier (in the application state) + // Arguments: + // - message: message to look for in the state + // Return Value: + // - true, if the message was dismissed + bool TerminalPage::_IsMessageDismissed(const InfoBarMessage& message) + { + if (const auto dismissedMessages{ ApplicationState::SharedInstance().DismissedMessages() }) + { + for (const auto& dismissedMessage : dismissedMessages) + { + if (dismissedMessage == message) + { + return true; + } + } + } + return false; + } + + // Method Description: + // - Persists the user's choice to dismiss information bar message (in application state) + // Arguments: + // - message: message to dismiss + // Return Value: + // - + void TerminalPage::_DismissMessage(const InfoBarMessage& message) + { + const auto applicationState = ApplicationState::SharedInstance(); + std::vector messages; + + if (const auto values = applicationState.DismissedMessages()) + { + messages.resize(values.Size()); + values.GetMany(0, messages); + } + + if (std::none_of(messages.begin(), messages.end(), [&](const auto& m) { return m == message; })) + { + messages.emplace_back(message); + } + + applicationState.DismissedMessages(std::move(messages)); + } + + void TerminalPage::_updateThemeColors() + { + if (_settings == nullptr) + { + return; + } + + const auto theme = _settings.GlobalSettings().CurrentTheme(); + auto requestedTheme{ theme.RequestedTheme() }; + + { + _updatePaneResources(requestedTheme); + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // The root pane will propagate the theme change to all its children. + if (const auto& rootPane{ tabImpl->GetRootPane() }) + { + rootPane->UpdateResources(_paneResources); + } + } + } + } + + const auto res = Application::Current().Resources(); + + // Use our helper to lookup the theme-aware version of the resource. + const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); + const auto backgroundSolidBrush = ThemeLookup(res, requestedTheme, tabViewBackgroundKey).as(); + + til::color bgColor = backgroundSolidBrush.Color(); + + Media::Brush terminalBrush{ nullptr }; + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto& pane{ tab->GetActivePane() }) + { + if (const auto& lastContent{ pane->GetLastFocusedContent() }) + { + terminalBrush = lastContent.BackgroundBrush(); + } + } + } + + // GH#19604: Get the theme's tabRow color to use as the acrylic tint. + const auto tabRowBg{ theme.TabRow() ? (_activated ? theme.TabRow().Background() : + theme.TabRow().UnfocusedBackground()) : + ThemeColor{ nullptr } }; + + if (_settings.GlobalSettings().UseAcrylicInTabRow() && (_activated || _settings.GlobalSettings().EnableUnfocusedAcrylic())) + { + if (tabRowBg) + { + bgColor = ThemeColor::ColorFromBrush(tabRowBg.Evaluate(res, terminalBrush, true)); + } + + const auto acrylicBrush = Media::AcrylicBrush(); + acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + acrylicBrush.FallbackColor(bgColor); + acrylicBrush.TintColor(bgColor); + acrylicBrush.TintOpacity(0.5); + + TitlebarBrush(acrylicBrush); + } + else if (tabRowBg) + { + const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) }; + bgColor = ThemeColor::ColorFromBrush(themeBrush); + // If the tab content returned nullptr for the terminalBrush, we + // _don't_ want to use it as the tab row background. We want to just + // use the default tab row background. + TitlebarBrush(themeBrush ? themeBrush : backgroundSolidBrush); + } + else + { + // Nothing was set in the theme - fall back to our original `TabViewBackground` color. + TitlebarBrush(backgroundSolidBrush); + } + + if (!_settings.GlobalSettings().ShowTabsInTitlebar()) + { + _tabRow.Background(TitlebarBrush()); + } + + // Second: Update the colors of our individual TabViewItems. This + // applies tab.background to the tabs via Tab::ThemeColor. + // + // Do this second, so that we already know the bgColor of the titlebar. + { + const auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; + const auto tabUnfocusedBackground = theme.Tab() ? theme.Tab().UnfocusedBackground() : nullptr; + for (const auto& tab : _tabs) + { + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tab)); + tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor); + } + } + // Update the new tab button to have better contrast with the new color. + // In theory, it would be convenient to also change these for the + // inactive tabs as well, but we're leaving that as a follow up. + _SetNewTabButtonColor(bgColor, bgColor); + + // Third: the window frame. This is basically the same logic as the tab row background. + // We'll set our `FrameBrush` property, for the window to later use. + const auto windowTheme{ theme.Window() }; + if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() : + windowTheme.UnfocusedFrame()) : + ThemeColor{ nullptr } }) + { + const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) }; + FrameBrush(themeBrush); + } + else + { + // Nothing was set in the theme - fall back to null. The window will + // use that as an indication to use the default window frame. + FrameBrush(nullptr); + } + } + + // Function Description: + // - Attempts to load some XAML resources that Panes will need. This includes: + // * The Color they'll use for active Panes's borders - SystemAccentColor + // * The Brush they'll use for inactive Panes - TabViewBackground (to match the + // color of the titlebar) + // Arguments: + // - requestedTheme: this should be the currently active Theme for the app + // Return Value: + // - + void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) + { + const auto res = Application::Current().Resources(); + const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); + if (res.HasKey(accentColorKey)) + { + const auto colorFromResources = ThemeLookup(res, requestedTheme, 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(colorFromResources, Colors::Black()); + _paneResources.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. + _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); + if (res.HasKey(unfocusedBorderBrushKey)) + { + // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for + // the requestedTheme, not just the value from the resources (which + // might not respect the settings' requested theme) + auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); + _paneResources.unfocusedBorderBrush = obj.try_as(); + } + 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. + _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto broadcastColorKey = winrt::box_value(L"BroadcastPaneBorderColor"); + if (res.HasKey(broadcastColorKey)) + { + // MAKE SURE TO USE ThemeLookup + auto obj = ThemeLookup(res, requestedTheme, broadcastColorKey); + _paneResources.broadcastBorderBrush = obj.try_as(); + } + 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. + _paneResources.broadcastBorderBrush = SolidColorBrush{ Colors::Black() }; + } + } + + void TerminalPage::_adjustProcessPriority() const + { + // Windowing is single-threaded, so this will not cause a race condition. + static uint64_t s_lastUpdateHash{ 0 }; + static bool s_supported{ true }; + + if (!s_supported || !_hostingHwnd.has_value()) + { + return; + } + + std::array processes; + auto it = processes.begin(); + const auto end = processes.end(); + + auto&& appendFromControl = [&](auto&& control) { + if (it == end) + { + return; + } + if (control) + { + if (const auto conn{ control.Connection() }) + { + if (const auto pty{ conn.try_as() }) + { + if (const uint64_t process{ pty.RootProcessHandle() }; process != 0) + { + *it++ = reinterpret_cast(process); + } + } + } + } + }; + + auto&& appendFromTab = [&](auto&& tabImpl) { + if (const auto pane{ tabImpl->GetRootPane() }) + { + pane->WalkTree([&](auto&& child) { + if (const auto& control{ child->GetTerminalControl() }) + { + appendFromControl(control); + } + }); + } + }; + + if (!_activated) + { + // When a window is out of focus, we want to attach all of the processes + // under it to the window so they all go into the background at the same time. + for (auto&& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + appendFromTab(tabImpl); + } + } + } + else + { + // When a window is in focus, propagate our foreground boost (if we have one) + // to current all panes in the current tab. + if (auto tabImpl{ _GetFocusedTabImpl() }) + { + appendFromTab(tabImpl); + } + } + + const auto count{ gsl::narrow_cast(it - processes.begin()) }; + const auto hash = til::hash((void*)processes.data(), count * sizeof(HANDLE)); + + if (hash == s_lastUpdateHash) + { + return; + } + + s_lastUpdateHash = hash; + const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); + + if (S_FALSE == hr) + { + // Don't bother trying again or logging. The wrapper tells us it's unsupported. + s_supported = false; + return; + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "CalledNewQoSAPI", + TraceLoggingValue(reinterpret_cast(_hostingHwnd.value()), "hwnd"), + TraceLoggingValue(count), + TraceLoggingHResult(hr)); +#ifdef _DEBUG + OutputDebugStringW(fmt::format(FMT_COMPILE(L"Submitted {} processes to TerminalTrySetWindowAssociatedProcesses; return=0x{:08x}\n"), count, hr).c_str()); +#endif + } + + void TerminalPage::WindowActivated(const bool activated) + { + // Stash if we're activated. Use that when we reload + // the settings, change active panes, etc. + _activated = activated; + _updateThemeColors(); + + _adjustProcessPriorityThrottled->Run(); + + if (const auto& tab{ _GetFocusedTabImpl() }) + { + if (tab->TabStatus().IsInputBroadcastActive()) + { + tab->GetRootPane()->WalkTree([activated](const auto& p) { + if (const auto& control{ p->GetTerminalControl() }) + { + control.CursorVisibility(activated ? + Microsoft::Terminal::Control::CursorDisplayState::Shown : + Microsoft::Terminal::Control::CursorDisplayState::Default); + } + }); + } + } + } + + safe_void_coroutine TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, + const CompletionsChangedEventArgs args) + { + // This won't even get hit if the velocity flag is disabled - we gate + // registering for the event based off of + // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents + + // User must explicitly opt-in on Preview builds + if (!_settings.GlobalSettings().EnableShellCompletionMenu()) + { + co_return; + } + + // Parse the json string into a collection of actions + try + { + auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), + args.ReplacementLength()); + + auto weakThis{ get_weak() }; + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { + // On the UI thread... + if (const auto& page{ weakThis.get() }) + { + // Open the Suggestions UI with the commands from the control + page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu, L""); + } + }); + } + CATCH_LOG(); + } + + void TerminalPage::_OpenSuggestions( + const TermControl& sender, + IVector commandsCollection, + winrt::TerminalApp::SuggestionsMode mode, + winrt::hstring filterText) + + { + // ON THE UI THREAD + assert(Dispatcher().HasThreadAccess()); + + if (commandsCollection == nullptr) + { + return; + } + if (commandsCollection.Size() == 0) + { + if (const auto p = SuggestionsElement()) + { + p.Visibility(Visibility::Collapsed); + } + return; + } + + const auto& control{ sender ? sender : _GetActiveControl() }; + if (!control) + { + return; + } + + const auto& sxnUi{ LoadSuggestionsUI() }; + + const auto characterSize{ control.CharacterDimensions() }; + // This is in control-relative space. We'll need to convert it to page-relative space. + const auto cursorPos{ control.CursorPositionInDips() }; + const auto controlTransform = control.TransformToVisual(this->Root()); + const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos + const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; + + sxnUi.Open(mode, + commandsCollection, + filterText, + realCursorPos, + windowDimensions, + characterSize.Height); + } + + void TerminalPage::_PopulateContextMenu(const TermControl& control, + const MUX::Controls::CommandBarFlyout& menu, + const bool withSelection) + { + // withSelection can be used to add actions that only appear if there's + // selected text, like "search the web" + + if (!control || !menu) + { + return; + } + + // Helper lambda for dispatching an ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. + + auto weak = get_weak(); + auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { + return [weak, actionAndArgs](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + page->_actionDispatch->DoAction(actionAndArgs); + } + }; + }; + + auto makeItem = [&makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& action, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + targetMenu.SecondaryCommands().Append(button); + }; + + auto makeMenuItem = [](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& subMenu, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Flyout(subMenu); + targetMenu.SecondaryCommands().Append(button); + }; + + auto makeContextItem = [&makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const winrt::hstring& tooltip, + const auto& action, + const auto& subMenu, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + WUX::Controls::ToolTipService::SetToolTip(button, box_value(tooltip)); + button.ContextFlyout(subMenu); + targetMenu.SecondaryCommands().Append(button); + }; + + const auto focusedProfile = _GetFocusedTabImpl()->GetFocusedProfile(); + auto separatorItem = AppBarSeparator{}; + auto activeProfiles = _settings.ActiveProfiles(); + auto activeProfileCount = gsl::narrow_cast(activeProfiles.Size()); + MUX::Controls::CommandBarFlyout splitPaneMenu{}; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }, menu); + + const auto focusedProfileName = focusedProfile.Name(); + const auto focusedProfileIcon = focusedProfile.Icon().Resolved(); + const auto splitPaneDuplicateText = RS_(L"SplitPaneDuplicateText") + L" " + focusedProfileName; // SplitPaneDuplicateText + + const auto splitPaneRightText = RS_(L"SplitPaneRightText"); + const auto splitPaneDownText = RS_(L"SplitPaneDownText"); + const auto splitPaneUpText = RS_(L"SplitPaneUpText"); + const auto splitPaneLeftText = RS_(L"SplitPaneLeftText"); + const auto splitPaneToolTipText = RS_(L"SplitPaneToolTipText"); + + MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; + makeItem(splitPaneRightText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Right, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneDownText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Down, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneUpText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Up, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneLeftText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Left, .5, nullptr } }, splitPaneContextMenu); + + makeContextItem(splitPaneDuplicateText, focusedProfileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Automatic, .5, nullptr } }, splitPaneContextMenu, splitPaneMenu); + + // add menu separator + const auto separatorAutoItem = AppBarSeparator{}; + + splitPaneMenu.SecondaryCommands().Append(separatorAutoItem); + + for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) + { + const auto profile = activeProfiles.GetAt(profileIndex); + const auto profileName = profile.Name(); + const auto profileIcon = profile.Icon().Resolved(); + + NewTerminalArgs args{}; + args.Profile(profileName); + + MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; + makeItem(splitPaneRightText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Right, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneDownText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Down, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneUpText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Up, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneLeftText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Left, .5, args } }, splitPaneContextMenu); + + makeContextItem(profileName, profileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Automatic, .5, args } }, splitPaneContextMenu, splitPaneMenu); + } + + makeMenuItem(RS_(L"SplitPaneText"), L"\xF246", splitPaneMenu, menu); + + // Only wire up "Close Pane" if there's multiple panes. + if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) + { + MUX::Controls::CommandBarFlyout swapPaneMenu{}; + const auto rootPane = _GetFocusedTabImpl()->GetRootPane(); + const auto mruPanes = _GetFocusedTabImpl()->GetMruPanes(); + auto activePane = _GetFocusedTabImpl()->GetActivePane(); + rootPane->WalkTree([&](auto p) { + if (const auto& c{ p->GetTerminalControl() }) + { + if (c == control) + { + activePane = p; + } + } + }); + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Down, mruPanes)) + { + makeItem(RS_(L"SwapPaneDownText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Down } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Right, mruPanes)) + { + makeItem(RS_(L"SwapPaneRightText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Right } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Up, mruPanes)) + { + makeItem(RS_(L"SwapPaneUpText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Up } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Left, mruPanes)) + { + makeItem(RS_(L"SwapPaneLeftText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Left } }, swapPaneMenu); + } + + makeMenuItem(RS_(L"SwapPaneText"), L"\xF1CB", swapPaneMenu, menu); + + makeItem(RS_(L"TogglePaneZoomText"), L"\xE8A3", ActionAndArgs{ ShortcutAction::TogglePaneZoom, nullptr }, menu); + makeItem(RS_(L"CloseOtherPanesText"), L"\xE89F", ActionAndArgs{ ShortcutAction::CloseOtherPanes, nullptr }, menu); + makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }, menu); + } + + if (control.ConnectionState() >= ConnectionState::Closed) + { + makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }, menu); + } + + if (withSelection) + { + makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }, menu); + } + + makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }, menu); + } + + void TerminalPage::_PopulateQuickFixMenu(const TermControl& control, + const Controls::MenuFlyout& menu) + { + if (!control || !menu) + { + return; + } + + // Helper lambda for dispatching a SendInput ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. Then clear the quick fix menu. + auto weak = get_weak(); + auto makeCallback = [weak](const hstring& suggestion) { + return [weak, suggestion](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + const auto actionAndArgs = ActionAndArgs{ ShortcutAction::SendInput, SendInputArgs{ hstring{ L"\u0003" } + suggestion } }; + page->_actionDispatch->DoAction(actionAndArgs); + if (auto ctrl = page->_GetActiveControl()) + { + ctrl.ClearQuickFix(); + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "QuickFixSuggestionUsed", + TraceLoggingDescription("Event emitted when a winget suggestion from is used"), + TraceLoggingValue("QuickFixMenu", "Source"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }; + }; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + menu.Items().Clear(); + const auto quickFixes = control.CommandHistory().QuickFixes(); + for (const auto& qf : quickFixes) + { + MenuFlyoutItem item{}; + + auto iconElement = UI::IconPathConverter::IconWUX(L"\ue74c"); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + item.Text(qf); + item.Click(makeCallback(qf)); + ToolTipService::SetToolTip(item, box_value(qf)); + menu.Items().Append(item); + } + } + + // Handler for our WindowProperties's PropertyChanged event. We'll use this + // to pop the "Identify Window" toast when the user renames our window. + void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) + { + if (args.PropertyName() != L"WindowName") + { + return; + } + + // DON'T display the confirmation if this is the name we were + // given on startup! + if (_startupState == StartupState::Initialized) + { + IdentifyWindow(); + } + } + + void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, + const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) + { + _appendUiaDragLog(L"_onTabDragStarting: begin"); + const auto eventTab = e.Tab(); + const auto draggedTab = _GetTabByTabViewItem(eventTab); + if (draggedTab) + { + auto draggedTabs = _IsTabSelected(draggedTab) ? _GetSelectedTabsInDisplayOrder() : + std::vector{}; + if (draggedTabs.empty() || + !std::ranges::any_of(draggedTabs, [&](const auto& tab) { return tab == draggedTab; })) + { + draggedTabs = { draggedTab }; + _SetSelectedTabs(draggedTabs, draggedTab); + } + + _stashed.draggedTabs = std::move(draggedTabs); + _stashed.dragAnchor = draggedTab; + + // Stash the offset from where we started the drag to the + // tab's origin. We'll use that offset in the future to help + // position the dropped window. + const auto inverseScale = 1.0f / static_cast(eventTab.XamlRoot().RasterizationScale()); + POINT cursorPos; + GetCursorPos(&cursorPos); + ScreenToClient(*_hostingHwnd, &cursorPos); + _stashed.dragOffset.X = cursorPos.x * inverseScale; + _stashed.dragOffset.Y = cursorPos.y * inverseScale; + + // Into the DataPackage, let's stash our own window ID. + const auto id{ _WindowProperties.WindowId() }; + + // Get our PID + const auto pid{ GetCurrentProcessId() }; + + e.Data().Properties().Insert(L"windowId", winrt::box_value(id)); + e.Data().Properties().Insert(L"pid", winrt::box_value(pid)); + e.Data().RequestedOperation(DataPackageOperation::Move); + _appendUiaDragLog(std::wstring{ L"_onTabDragStarting: stashed " } + std::to_wstring(_stashed.draggedTabs.size()) + L" tab(s)"); + + // The next thing that will happen: + // * Another TerminalPage will get a TabStripDragOver, then get a + // TabStripDrop + // * This will be handled by the _other_ page asking the monarch + // to ask us to send our content to them. + // * We'll get a TabDroppedOutside to indicate that this tab was + // dropped _not_ on a TabView. + // * This will be handled by _onTabDroppedOutside, which will + // raise a MoveContent (to a new window) event. + } + } + + void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::DragEventArgs& e) + { + // We must mark that we can accept the drag/drop. The system will never + // call TabStripDrop on us if we don't indicate that we're willing. + const auto& props{ e.DataView().Properties() }; + if (props.HasKey(L"windowId") && + props.HasKey(L"pid") && + (winrt::unbox_value_or(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId())) + { + e.AcceptedOperation(DataPackageOperation::Move); + } + + // You may think to yourself, this is a great place to increase the + // width of the TabView artificially, to make room for the new tab item. + // However, we'll never get a message that the tab left the tab view + // (without being dropped). So there's no good way to resize back down. + } + + // Method Description: + // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage + // to find who the tab came from. We'll then ask the Monarch to ask the + // sender to move that tab to us. + void TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/, + winrt::Windows::UI::Xaml::DragEventArgs e) + { + // Get the PID and make sure it is the same as ours. + if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") }) + { + const auto pid{ winrt::unbox_value_or(pidObj, 0u) }; + if (pid != GetCurrentProcessId()) + { + // The PID doesn't match ours. We can't handle this drop. + return; + } + } + else + { + // No PID? We can't handle this drop. Bail. + return; + } + + const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") }; + if (windowIdObj == nullptr) + { + // No windowId? Bail. + return; + } + const uint64_t src{ winrt::unbox_value(windowIdObj) }; + + // Figure out where in the tab strip we're dropping this tab. Add that + // index to the request. This is largely taken from the WinUI sample + // app. + + // First we need to get the position in the List to drop to + auto index = -1; + + // Determine which items in the list our pointer is between. + for (auto i = 0u; i < _tabView.TabItems().Size(); i++) + { + if (const auto& item{ _tabView.ContainerFromIndex(i).try_as() }) + { + const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab + const auto itemWidth{ item.ActualWidth() }; // The right of the tab + // If the drag point is on the left half of the tab, then insert here. + if (posX < itemWidth / 2) + { + index = i; + break; + } + } + } + + if (index < 0) + { + index = gsl::narrow_cast(_tabView.TabItems().Size()); + } + + // `this` is safe to use + const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); + + // This will go up to the monarch, who will then dispatch the request + // back down to the source TerminalPage, who will then perform a + // RequestMoveContent to move their tab to us. + RequestReceiveContent.raise(*this, *request); + } + + // Method Description: + // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has + // requested that we send our tab to another window. We'll need to + // serialize the tab, and send it to the monarch, who will then send it to + // the destination window. + // - Fortunately, sending the tab is basically just a MoveTab action, so we + // can largely reuse that. + void TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) + { + // validate that we're the source window of the tab in this request + if (args.SourceWindow() != _WindowProperties.WindowId()) + { + return; + } + if (_stashed.draggedTabs.empty()) + { + return; + } + + _sendDraggedTabsToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); + } + + void TerminalPage::_onTabDroppedOutside(winrt::IInspectable /*sender*/, + winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs /*e*/) + { + _appendUiaDragLog(L"_onTabDroppedOutside: begin"); + // Get the current pointer point from the CoreWindow + const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() }; + + // This is called when a tab FROM OUR WINDOW was dropped outside the + // tabview. We already know which tab was being dragged. We'll just + // invoke a moveTab action with the target window being -1. That will + // force the creation of a new window. + + if (_stashed.draggedTabs.empty()) + { + return; + } + + // We need to convert the pointer point to a point that we can use + // to position the new window. We'll use the drag offset from before + // so that the tab in the new window is positioned so that it's + // basically still directly under the cursor. + + // -1 is the magic number for "new window" + // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab. + const winrt::Windows::Foundation::Point adjusted = { + pointerPoint.X - _stashed.dragOffset.X, + pointerPoint.Y - _stashed.dragOffset.Y, + }; + _appendUiaDragLog(std::wstring{ L"_onTabDroppedOutside: moving " } + std::to_wstring(_stashed.draggedTabs.size()) + L" tab(s) to a new window"); + _sendDraggedTabsToWindow(winrt::hstring{ L"-1" }, 0, adjusted); + } + + void TerminalPage::_sendDraggedTabsToWindow(const winrt::hstring& windowId, + const uint32_t tabIndex, + std::optional dragPoint) + { + _appendUiaDragLog(std::wstring{ L"_sendDraggedTabsToWindow: target=" } + windowId.c_str() + L", tabs=" + std::to_wstring(_stashed.draggedTabs.size())); + if (_stashed.draggedTabs.empty()) + { + return; + } + + auto draggedTabs = _stashed.draggedTabs; + auto startupActions = _BuildStartupActionsForTabs(draggedTabs); + if (dragPoint.has_value() && draggedTabs.size() > 1 && _stashed.dragAnchor) + { + const auto draggedAnchorIt = std::ranges::find_if(draggedTabs, [&](const auto& tab) { + return tab == _stashed.dragAnchor; + }); + if (draggedAnchorIt != draggedTabs.end()) + { + ActionAndArgs switchToTabAction{}; + switchToTabAction.Action(ShortcutAction::SwitchToTab); + switchToTabAction.Args(SwitchToTabArgs{ gsl::narrow_cast(std::distance(draggedTabs.begin(), draggedAnchorIt)) }); + startupActions.emplace_back(std::move(switchToTabAction)); + } + } + + for (const auto& tab : draggedTabs) + { + if (const auto tabImpl{ _GetTabImpl(tab) }) + { + _DetachTabFromWindow(tabImpl); + } + } + + _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); + + for (auto it = draggedTabs.rbegin(); it != draggedTabs.rend(); ++it) + { + _RemoveTab(*it); + } + + _stashed.draggedTabs.clear(); + _stashed.dragAnchor = nullptr; + } + + /// + /// Creates a sub flyout menu for profile items in the split button menu that when clicked will show a menu item for + /// Run as Administrator + /// + /// The index for the profileMenuItem + /// MenuFlyout that will show when the context is request on a profileMenuItem + WUX::Controls::MenuFlyout TerminalPage::_CreateRunAsAdminFlyout(int profileIndex) + { + // Create the MenuFlyout and set its placement + WUX::Controls::MenuFlyout profileMenuItemFlyout{}; + profileMenuItemFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight); + + // Create the menu item and an icon to use in the menu + WUX::Controls::MenuFlyoutItem runAsAdminItem{}; + WUX::Controls::FontIcon adminShieldIcon{}; + + adminShieldIcon.Glyph(L"\xEA18"); + adminShieldIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + + runAsAdminItem.Icon(adminShieldIcon); + runAsAdminItem.Text(RS_(L"RunAsAdminFlyout/Text")); + + // Click handler for the flyout item + runAsAdminItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemElevateSubmenuItemClicked", + TraceLoggingDescription("Event emitted when the elevate submenu item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + NewTerminalArgs args{ profileIndex }; + args.Elevate(true); + page->_OpenNewTerminalViaDropdown(args); + } + }); + + profileMenuItemFlyout.Items().Append(runAsAdminItem); + + return profileMenuItemFlyout; + } +} diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 55de993ebc3..44c9922e881 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -1,603 +1,604 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include - -#include "TerminalPage.g.h" -#include "Tab.h" -#include "AppKeyBindings.h" -#include "AppCommandlineArgs.h" -#include "RenameWindowRequestedArgs.g.h" -#include "RequestMoveContentArgs.g.h" -#include "LaunchPositionRequest.g.h" -#include "Toast.h" - -#include "WindowsPackageManagerFactory.h" - -#define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); - -namespace TerminalAppLocalTests -{ - class TabTests; - class SettingsTests; -} - -namespace Microsoft::Terminal::Core -{ - class ControlKeyStates; -} - -namespace winrt::Microsoft::Terminal::Settings -{ - struct TerminalSettingsCreateResult; -} - -namespace winrt::TerminalApp::implementation -{ - struct TerminalSettingsCache; - - inline constexpr uint32_t DefaultRowsToScroll{ 3 }; - inline constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; - - enum StartupState : int - { - NotInitialized = 0, - InStartup = 1, - Initialized = 2 - }; - - enum ScrollDirection : int - { - ScrollUp = 0, - ScrollDown = 1 - }; - - struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT - { - WINRT_PROPERTY(winrt::hstring, ProposedName); - - public: - RenameWindowRequestedArgs(const winrt::hstring& name) : - _ProposedName{ name } {}; - }; - - struct RequestMoveContentArgs : RequestMoveContentArgsT - { - WINRT_PROPERTY(winrt::hstring, Window); - WINRT_PROPERTY(winrt::hstring, Content); - WINRT_PROPERTY(uint32_t, TabIndex); - WINRT_PROPERTY(Windows::Foundation::IReference, WindowPosition); - - public: - RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) : - _Window{ window }, - _Content{ content }, - _TabIndex{ tabIndex } {}; - }; - - struct LaunchPositionRequest : LaunchPositionRequestT - { - LaunchPositionRequest() = default; - - til::property Position; - }; - - struct WinGetSearchParams - { - winrt::Microsoft::Management::Deployment::PackageMatchField Field; - winrt::Microsoft::Management::Deployment::PackageFieldMatchOption MatchOption; - }; - - struct TerminalPage : TerminalPageT - { - public: - TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager); - - // This implements shobjidl's IInitializeWithWindow, but due to a XAML Compiler bug we cannot - // put it in our inheritance graph. https://github.com/microsoft/microsoft-ui-xaml/issues/3331 - STDMETHODIMP Initialize(HWND hwnd); - - void SetSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings, bool needRefreshUI); - - void Create(); - Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - - bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; - void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); - - hstring Title(); - - void TitlebarClicked(); - void WindowVisibilityChanged(const bool showOrHide); - - float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; - - winrt::hstring ApplicationDisplayName(); - winrt::hstring ApplicationVersion(); - - CommandPalette LoadCommandPalette(); - SuggestionsControl LoadSuggestionsUI(); - - safe_void_coroutine RequestQuit(); - safe_void_coroutine CloseWindow(); - void PersistState(); - std::vector Panes() const; - - void ToggleFocusMode(); - void ToggleFullscreen(); - void ToggleAlwaysOnTop(); - bool FocusMode() const; - bool Fullscreen() const; - bool AlwaysOnTop() const; - bool ShowTabsFullscreen() const; - void SetShowTabsFullscreen(bool newShowTabsFullscreen); - void SetFullscreen(bool); - void SetFocusMode(const bool inFocusMode); - void Maximized(bool newMaximized); - void RequestSetMaximized(bool newMaximized); - - void SetStartupActions(std::vector actions); - void SetStartupConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); - - static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); - - winrt::TerminalApp::IDialogPresenter DialogPresenter() const; - void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); - - winrt::TerminalApp::TaskbarState TaskbarState() const; - - void ShowKeyboardServiceWarning() const; - winrt::hstring KeyboardServiceDisabledText(); - - void IdentifyWindow(); - void ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord); - void ActionSaveFailed(winrt::hstring message); - void ShowTerminalWorkingDirectory(); - - safe_void_coroutine ProcessStartupActions(std::vector actions, - const winrt::hstring cwd = winrt::hstring{}, - const winrt::hstring env = winrt::hstring{}); - safe_void_coroutine CreateTabFromConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); - - TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; - - bool CanDragDrop() const noexcept; - bool IsRunningElevated() const noexcept; - - void OpenSettingsUI(); - void WindowActivated(const bool activated); - - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - - void AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); - void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); - - uint32_t NumberOfTabs() const; - - til::property_changed_event PropertyChanged; - - // -------------------------------- WinRT Events --------------------------------- - til::typed_event TitleChanged; - til::typed_event CloseWindowRequested; - til::typed_event SetTitleBarContent; - til::typed_event FocusModeChanged; - til::typed_event FullscreenChanged; - til::typed_event ChangeMaximizeRequested; - til::typed_event AlwaysOnTopChanged; - til::typed_event RaiseVisualBell; - til::typed_event SetTaskbarProgress; - til::typed_event Initialized; - til::typed_event IdentifyWindowsRequested; - til::typed_event RenameWindowRequested; - til::typed_event SummonWindowRequested; - til::typed_event WindowSizeChanged; - - til::typed_event OpenSystemMenu; - til::typed_event QuitRequested; - til::typed_event ShowWindowChanged; - til::typed_event> ShowLoadWarningsDialog; - - til::typed_event RequestMoveContent; - til::typed_event RequestReceiveContent; - - til::typed_event RequestLaunchPosition; - - WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); - WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); - - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionName, PropertyChanged.raise, L""); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionKeyChord, PropertyChanged.raise, L""); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionCommandLine, PropertyChanged.raise, L""); - - private: - friend struct TerminalPageT; // for Xaml to bind events - std::optional _hostingHwnd; - - // If you add controls here, but forget to null them either here or in - // the ctor, you're going to have a bad time. It'll mysteriously fail to - // activate the app. - // ALSO: If you add any UIElements as roots here, make sure they're - // updated in App::_ApplyTheme. The roots currently is _tabRow - // (which is a root when the tabs are in the titlebar.) - Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr }; - TerminalApp::TabRowControl _tabRow{ nullptr }; - Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; - Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; - winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; - - Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; - - Windows::Foundation::Collections::IObservableVector _tabs; - Windows::Foundation::Collections::IObservableVector _mruTabs; - static winrt::com_ptr _GetTabImpl(const TerminalApp::Tab& tab); - - void _UpdateTabIndices(); - - TerminalApp::Tab _settingsTab{ nullptr }; - - bool _isInFocusMode{ false }; - bool _isFullscreen{ false }; - bool _isMaximized{ false }; - bool _isAlwaysOnTop{ false }; - bool _showTabsFullscreen{ false }; - - std::optional _loadFromPersistedLayoutIdx{}; - - bool _rearranging{ false }; - std::optional _rearrangeFrom{}; - std::optional _rearrangeTo{}; - bool _removing{ false }; - std::vector _selectedTabs{}; - winrt::TerminalApp::Tab _selectionAnchor{ nullptr }; - - bool _activated{ false }; - bool _visible{ true }; - - std::vector> _previouslyClosedPanesAndTabs{}; - - uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; - - // use a weak reference to prevent circular dependency with AppLogic - winrt::weak_ref _dialogPresenter; - - winrt::com_ptr _bindings{ winrt::make_self() }; - winrt::com_ptr _actionDispatch{ winrt::make_self() }; - - winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker; - StartupState _startupState{ StartupState::NotInitialized }; - - std::vector _startupActions; - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _startupConnection{ nullptr }; - - std::shared_ptr _windowIdToast{ nullptr }; - std::shared_ptr _actionSavedToast{ nullptr }; - std::shared_ptr _actionSaveFailedToast{ nullptr }; - std::shared_ptr _windowCwdToast{ nullptr }; - - winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _renamerLayoutUpdatedRevoker; - int _renamerLayoutCount{ 0 }; - bool _renamerPressedEnter{ false }; - - TerminalApp::WindowProperties _WindowProperties{ nullptr }; - PaneResources _paneResources; - - TerminalApp::ContentManager _manager{ nullptr }; - - std::shared_ptr _terminalSettingsCache{}; - - struct StashedDragData - { - std::vector draggedTabs{}; - winrt::TerminalApp::Tab dragAnchor{ nullptr }; - winrt::Windows::Foundation::Point dragOffset{ 0, 0 }; - } _stashed; - - safe_void_coroutine _NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e); - - __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); - bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); - __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); - bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); - - winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); - - void _ShowAboutDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); - - void _CreateNewTabFlyout(); - std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector entries); - winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon); - winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex, const winrt::hstring& iconPathOverride); - winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride); - - void _OpenNewTabDropdown(); - HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs); - TerminalApp::Tab _CreateNewTabFromPane(std::shared_ptr pane, uint32_t insertPosition = -1); - - std::wstring _evaluatePathForCwd(std::wstring_view path); - - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Control::IControlSettings settings, const bool inheritCursor); - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent); - void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); - - safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); - - void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); - - bool _displayingCloseDialog{ false }; - void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _AboutButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - - void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept; - static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept; - void _HookupKeyBindings(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap) noexcept; - void _RegisterActionCallbacks(); - - void _UpdateTitle(const Tab& tab); - void _UpdateTabIcon(Tab& tab); - void _UpdateTabView(); - void _UpdateTabWidthMode(); - void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); - - void _DuplicateFocusedTab(); - void _DuplicateTab(const Tab& tab); - - safe_void_coroutine _ExportTab(const Tab& tab, winrt::hstring filepath); - - winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab); - void _CloseTabAtIndex(uint32_t index); - void _RemoveTab(const winrt::TerminalApp::Tab& tab); - safe_void_coroutine _RemoveTabs(const std::vector tabs); - - void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); - void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); - void _RegisterTabEvents(Tab& hostingTab); - - void _DismissTabContextMenus(); - void _FocusCurrentTab(const bool focusAlways); - bool _HasMultipleTabs() const; - - void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); - bool _SelectTab(uint32_t tabIndex); - bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); - bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); - - std::shared_ptr> _adjustProcessPriorityThrottled; - void _adjustProcessPriority() const; - - template - bool _ApplyToActiveControls(F f) const - { - if (const auto tab{ _GetFocusedTabImpl() }) - { - if (const auto activePane = tab->GetActivePane()) - { - activePane->WalkTree([&](auto p) { - if (const auto& control{ p->GetTerminalControl() }) - { - f(control); - } - }); - - return true; - } - } - return false; - } - - winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl() const; - std::optional _GetFocusedTabIndex() const noexcept; - std::optional _GetTabIndex(const TerminalApp::Tab& tab) const noexcept; - TerminalApp::Tab _GetFocusedTab() const noexcept; - winrt::com_ptr _GetFocusedTabImpl() const noexcept; - TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept; - - void _HandleClosePaneRequested(std::shared_ptr pane); - safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab); - safe_void_coroutine _CloseFocusedPane(); - void _ClosePanes(weak_ref weakTab, std::vector paneIds); - winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); - void _AddPreviouslyClosedPaneOrTab(std::vector&& args); - - void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); - - void _SplitPane(const winrt::com_ptr& tab, - const Microsoft::Terminal::Settings::Model::SplitDirection splitType, - const float splitSize, - std::shared_ptr newPane); - void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - void _ToggleSplitOrientation(); - - void _ScrollPage(ScrollDirection scrollDirection); - void _ScrollToBufferEdge(ScrollDirection scrollDirection); - void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Control::KeyChord& keyChord); - - safe_void_coroutine _PasteFromClipboardHandler(const IInspectable sender, - const Microsoft::Terminal::Control::PasteFromClipboardEventArgs eventArgs); - - void _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs); - bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri); - - void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); - bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats); - - safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); - - void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const; - void _PasteText(); - - safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); - void _ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message); - - safe_void_coroutine _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); - - void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs); - void _TabDragCompleted(const IInspectable& sender, const IInspectable& eventArgs); - - // BODGY: WinUI's TabView has a broken close event handler: - // If the close button is disabled, middle-clicking the tab raises no close - // event. Because that's dumb, we implement our own middle-click handling. - // `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden, - // and that enables all of the rest of this machinery (and this workaround). - bool _tabItemMiddleClickHookEnabled = false; - bool _tabItemMiddleClickExited = false; - PointerEntered_revoker _tabItemMiddleClickPointerEntered; - PointerExited_revoker _tabItemMiddleClickPointerExited; - PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost; - void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); - safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender); - - void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); - void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); - void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs); - void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs); - void _UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab); - void _UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); - - void _OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command); - void _OnCommandLineExecutionRequested(const IInspectable& sender, const winrt::hstring& commandLine); - void _OnSwitchToTabRequested(const IInspectable& sender, const winrt::TerminalApp::Tab& tab); - - void _Find(const Tab& tab); - - winrt::Microsoft::Terminal::Control::TermControl _CreateNewControlAndContent(const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& settings, - const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); - winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term); - winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid); - - TerminalApp::IPaneContent _makeSettingsContent(); - std::shared_ptr _MakeTerminalPane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr, - const winrt::TerminalApp::Tab& sourceTab = nullptr, - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - std::shared_ptr _MakePane(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs = nullptr, - const winrt::TerminalApp::Tab& sourceTab = nullptr, - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - - void _RefreshUIForSettingsReload(); - - void _SetNewTabButtonColor(til::color color, til::color accentColor); - void _ClearNewTabButtonColor(); - - safe_void_coroutine _CompleteInitialization(); - - void _FocusActiveControl(IInspectable sender, IInspectable eventArgs); - - void _UnZoomIfNeeded(); - - static int _ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll); - static uint32_t _ReadSystemRowsToScroll(); - - void _UpdateMRUTab(const winrt::TerminalApp::Tab& tab); - bool _TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept; - bool _IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept; - void _SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor = nullptr); - void _RemoveSelectedTab(const winrt::TerminalApp::Tab& tab); - std::vector _GetSelectedTabsInDisplayOrder() const; - std::vector _GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const; - void _ApplyMultiSelectionVisuals(); - void _UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab); - void _MoveTabsToIndex(const std::vector& tabs, uint32_t suggestedNewTabIndex); - std::vector _CollectNewTabs(const std::vector& existingTabs) const; - std::vector _BuildStartupActionsForTabs(const std::vector& tabs) const; - - void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex); - - void _PreviewAction(const Microsoft::Terminal::Settings::Model::ActionAndArgs& args); - void _PreviewActionHandler(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& args); - void _EndPreview(); - void _RunRestorePreviews(); - void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); - void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); - void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args); - - winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; - std::vector> _restorePreviewFuncs{}; - - HRESULT _OnNewConnection(const winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection& connection); - void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); - - void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); - void _RequestWindowRename(const winrt::hstring& newName); - void _WindowRenamerKeyDown(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - void _WindowRenamerKeyUp(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - - void _UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element); - - winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept; - - bool _maybeElevate(const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, - const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& controlSettings, - const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); - void _OpenElevatedWT(winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); - - safe_void_coroutine _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; - void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; - static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); - static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); - - void _updateThemeColors(); - void _updateAllTabCloseButtons(); - void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - - safe_void_coroutine _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); - - void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode, winrt::hstring filterText); - - void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); - Windows::Foundation::IAsyncAction _SearchMissingCommandHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SearchMissingCommandEventArgs args); - static Windows::Foundation::IAsyncOperation> _FindPackageAsync(hstring query); - - void _WindowSizeChanged(const IInspectable sender, const winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs args); - void _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); - - void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); - void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); - void _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e); - void _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e); - - void _DetachPaneFromWindow(std::shared_ptr pane); - void _DetachTabFromWindow(const winrt::com_ptr& tabImpl); - void _MoveContent(std::vector&& actions, - const winrt::hstring& windowName, - const uint32_t tabIndex, - const std::optional& dragPoint = std::nullopt); - void _sendDraggedTabsToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); - - void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); - void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); - winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); - - winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); - winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); - - void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args); - safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs); - -#pragma region ActionHandlers - // These are all defined in AppActionHandlers.cpp -#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); - ALL_SHORTCUT_ACTIONS - INTERNAL_SHORTCUT_ACTIONS -#undef ON_ALL_ACTIONS -#pragma endregion - - friend class TerminalAppLocalTests::TabTests; - friend class TerminalAppLocalTests::SettingsTests; - }; -} - -namespace winrt::TerminalApp::factory_implementation -{ - BASIC_FACTORY(TerminalPage); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "TerminalPage.g.h" +#include "Tab.h" +#include "AppKeyBindings.h" +#include "AppCommandlineArgs.h" +#include "RenameWindowRequestedArgs.g.h" +#include "RequestMoveContentArgs.g.h" +#include "LaunchPositionRequest.g.h" +#include "Toast.h" + +#include "WindowsPackageManagerFactory.h" + +#define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + +namespace TerminalAppLocalTests +{ + class TabTests; + class SettingsTests; +} + +namespace Microsoft::Terminal::Core +{ + class ControlKeyStates; +} + +namespace winrt::Microsoft::Terminal::Settings +{ + struct TerminalSettingsCreateResult; +} + +namespace winrt::TerminalApp::implementation +{ + struct TerminalSettingsCache; + + inline constexpr uint32_t DefaultRowsToScroll{ 3 }; + inline constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; + + enum StartupState : int + { + NotInitialized = 0, + InStartup = 1, + Initialized = 2 + }; + + enum ScrollDirection : int + { + ScrollUp = 0, + ScrollDown = 1 + }; + + struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT + { + WINRT_PROPERTY(winrt::hstring, ProposedName); + + public: + RenameWindowRequestedArgs(const winrt::hstring& name) : + _ProposedName{ name } {}; + }; + + struct RequestMoveContentArgs : RequestMoveContentArgsT + { + WINRT_PROPERTY(winrt::hstring, Window); + WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, TabIndex); + WINRT_PROPERTY(Windows::Foundation::IReference, WindowPosition); + + public: + RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) : + _Window{ window }, + _Content{ content }, + _TabIndex{ tabIndex } {}; + }; + + struct LaunchPositionRequest : LaunchPositionRequestT + { + LaunchPositionRequest() = default; + + til::property Position; + }; + + struct WinGetSearchParams + { + winrt::Microsoft::Management::Deployment::PackageMatchField Field; + winrt::Microsoft::Management::Deployment::PackageFieldMatchOption MatchOption; + }; + + struct TerminalPage : TerminalPageT + { + public: + TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager); + + // This implements shobjidl's IInitializeWithWindow, but due to a XAML Compiler bug we cannot + // put it in our inheritance graph. https://github.com/microsoft/microsoft-ui-xaml/issues/3331 + STDMETHODIMP Initialize(HWND hwnd); + + void SetSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings, bool needRefreshUI); + + void Create(); + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); + + bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); + + hstring Title(); + + void TitlebarClicked(); + void WindowVisibilityChanged(const bool showOrHide); + + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + + winrt::hstring ApplicationDisplayName(); + winrt::hstring ApplicationVersion(); + + CommandPalette LoadCommandPalette(); + SuggestionsControl LoadSuggestionsUI(); + + safe_void_coroutine RequestQuit(); + safe_void_coroutine CloseWindow(); + void PersistState(); + std::vector Panes() const; + + void ToggleFocusMode(); + void ToggleFullscreen(); + void ToggleAlwaysOnTop(); + bool FocusMode() const; + bool Fullscreen() const; + bool AlwaysOnTop() const; + bool ShowTabsFullscreen() const; + void SetShowTabsFullscreen(bool newShowTabsFullscreen); + void SetFullscreen(bool); + void SetFocusMode(const bool inFocusMode); + void Maximized(bool newMaximized); + void RequestSetMaximized(bool newMaximized); + + void SetStartupActions(std::vector actions); + void SetStartupConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + + static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); + + winrt::TerminalApp::IDialogPresenter DialogPresenter() const; + void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); + + winrt::TerminalApp::TaskbarState TaskbarState() const; + + void ShowKeyboardServiceWarning() const; + winrt::hstring KeyboardServiceDisabledText(); + + void IdentifyWindow(); + void ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord); + void ActionSaveFailed(winrt::hstring message); + void ShowTerminalWorkingDirectory(); + + safe_void_coroutine ProcessStartupActions(std::vector actions, + const winrt::hstring cwd = winrt::hstring{}, + const winrt::hstring env = winrt::hstring{}); + safe_void_coroutine CreateTabFromConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + + TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; + + bool CanDragDrop() const noexcept; + bool IsRunningElevated() const noexcept; + + void OpenSettingsUI(); + void WindowActivated(const bool activated); + + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + + void AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); + void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); + + uint32_t NumberOfTabs() const; + bool SelectTabRangeForTesting(uint32_t startIndex, uint32_t endIndex); + + til::property_changed_event PropertyChanged; + + // -------------------------------- WinRT Events --------------------------------- + til::typed_event TitleChanged; + til::typed_event CloseWindowRequested; + til::typed_event SetTitleBarContent; + til::typed_event FocusModeChanged; + til::typed_event FullscreenChanged; + til::typed_event ChangeMaximizeRequested; + til::typed_event AlwaysOnTopChanged; + til::typed_event RaiseVisualBell; + til::typed_event SetTaskbarProgress; + til::typed_event Initialized; + til::typed_event IdentifyWindowsRequested; + til::typed_event RenameWindowRequested; + til::typed_event SummonWindowRequested; + til::typed_event WindowSizeChanged; + + til::typed_event OpenSystemMenu; + til::typed_event QuitRequested; + til::typed_event ShowWindowChanged; + til::typed_event> ShowLoadWarningsDialog; + + til::typed_event RequestMoveContent; + til::typed_event RequestReceiveContent; + + til::typed_event RequestLaunchPosition; + + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); + + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionName, PropertyChanged.raise, L""); + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionKeyChord, PropertyChanged.raise, L""); + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionCommandLine, PropertyChanged.raise, L""); + + private: + friend struct TerminalPageT; // for Xaml to bind events + std::optional _hostingHwnd; + + // If you add controls here, but forget to null them either here or in + // the ctor, you're going to have a bad time. It'll mysteriously fail to + // activate the app. + // ALSO: If you add any UIElements as roots here, make sure they're + // updated in App::_ApplyTheme. The roots currently is _tabRow + // (which is a root when the tabs are in the titlebar.) + Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr }; + TerminalApp::TabRowControl _tabRow{ nullptr }; + Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; + Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; + winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; + + Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; + + Windows::Foundation::Collections::IObservableVector _tabs; + Windows::Foundation::Collections::IObservableVector _mruTabs; + static winrt::com_ptr _GetTabImpl(const TerminalApp::Tab& tab); + + void _UpdateTabIndices(); + + TerminalApp::Tab _settingsTab{ nullptr }; + + bool _isInFocusMode{ false }; + bool _isFullscreen{ false }; + bool _isMaximized{ false }; + bool _isAlwaysOnTop{ false }; + bool _showTabsFullscreen{ false }; + + std::optional _loadFromPersistedLayoutIdx{}; + + bool _rearranging{ false }; + std::optional _rearrangeFrom{}; + std::optional _rearrangeTo{}; + bool _removing{ false }; + std::vector _selectedTabs{}; + winrt::TerminalApp::Tab _selectionAnchor{ nullptr }; + + bool _activated{ false }; + bool _visible{ true }; + + std::vector> _previouslyClosedPanesAndTabs{}; + + uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; + + // use a weak reference to prevent circular dependency with AppLogic + winrt::weak_ref _dialogPresenter; + + winrt::com_ptr _bindings{ winrt::make_self() }; + winrt::com_ptr _actionDispatch{ winrt::make_self() }; + + winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker; + StartupState _startupState{ StartupState::NotInitialized }; + + std::vector _startupActions; + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _startupConnection{ nullptr }; + + std::shared_ptr _windowIdToast{ nullptr }; + std::shared_ptr _actionSavedToast{ nullptr }; + std::shared_ptr _actionSaveFailedToast{ nullptr }; + std::shared_ptr _windowCwdToast{ nullptr }; + + winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _renamerLayoutUpdatedRevoker; + int _renamerLayoutCount{ 0 }; + bool _renamerPressedEnter{ false }; + + TerminalApp::WindowProperties _WindowProperties{ nullptr }; + PaneResources _paneResources; + + TerminalApp::ContentManager _manager{ nullptr }; + + std::shared_ptr _terminalSettingsCache{}; + + struct StashedDragData + { + std::vector draggedTabs{}; + winrt::TerminalApp::Tab dragAnchor{ nullptr }; + winrt::Windows::Foundation::Point dragOffset{ 0, 0 }; + } _stashed; + + safe_void_coroutine _NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e); + + __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); + bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); + __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); + bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); + + winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); + + void _ShowAboutDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); + + void _CreateNewTabFlyout(); + std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector entries); + winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon); + winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex, const winrt::hstring& iconPathOverride); + winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride); + + void _OpenNewTabDropdown(); + HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs); + TerminalApp::Tab _CreateNewTabFromPane(std::shared_ptr pane, uint32_t insertPosition = -1); + + std::wstring _evaluatePathForCwd(std::wstring_view path); + + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Control::IControlSettings settings, const bool inheritCursor); + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent); + void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); + + safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); + + void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + + bool _displayingCloseDialog{ false }; + void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _AboutButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + + void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept; + static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept; + void _HookupKeyBindings(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap) noexcept; + void _RegisterActionCallbacks(); + + void _UpdateTitle(const Tab& tab); + void _UpdateTabIcon(Tab& tab); + void _UpdateTabView(); + void _UpdateTabWidthMode(); + void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); + + void _DuplicateFocusedTab(); + void _DuplicateTab(const Tab& tab); + + safe_void_coroutine _ExportTab(const Tab& tab, winrt::hstring filepath); + + winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab); + void _CloseTabAtIndex(uint32_t index); + void _RemoveTab(const winrt::TerminalApp::Tab& tab); + safe_void_coroutine _RemoveTabs(const std::vector tabs); + + void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); + void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); + void _RegisterTabEvents(Tab& hostingTab); + + void _DismissTabContextMenus(); + void _FocusCurrentTab(const bool focusAlways); + bool _HasMultipleTabs() const; + + void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); + bool _SelectTab(uint32_t tabIndex); + bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); + bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); + + std::shared_ptr> _adjustProcessPriorityThrottled; + void _adjustProcessPriority() const; + + template + bool _ApplyToActiveControls(F f) const + { + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto activePane = tab->GetActivePane()) + { + activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + f(control); + } + }); + + return true; + } + } + return false; + } + + winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl() const; + std::optional _GetFocusedTabIndex() const noexcept; + std::optional _GetTabIndex(const TerminalApp::Tab& tab) const noexcept; + TerminalApp::Tab _GetFocusedTab() const noexcept; + winrt::com_ptr _GetFocusedTabImpl() const noexcept; + TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept; + + void _HandleClosePaneRequested(std::shared_ptr pane); + safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab); + safe_void_coroutine _CloseFocusedPane(); + void _ClosePanes(weak_ref weakTab, std::vector paneIds); + winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); + void _AddPreviouslyClosedPaneOrTab(std::vector&& args); + + void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); + + void _SplitPane(const winrt::com_ptr& tab, + const Microsoft::Terminal::Settings::Model::SplitDirection splitType, + const float splitSize, + std::shared_ptr newPane); + void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); + void _ToggleSplitOrientation(); + + void _ScrollPage(ScrollDirection scrollDirection); + void _ScrollToBufferEdge(ScrollDirection scrollDirection); + void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Control::KeyChord& keyChord); + + safe_void_coroutine _PasteFromClipboardHandler(const IInspectable sender, + const Microsoft::Terminal::Control::PasteFromClipboardEventArgs eventArgs); + + void _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs); + bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri); + + void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); + bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats); + + safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); + + void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const; + void _PasteText(); + + safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); + void _ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message); + + safe_void_coroutine _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); + + void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs); + void _TabDragCompleted(const IInspectable& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs); + + // BODGY: WinUI's TabView has a broken close event handler: + // If the close button is disabled, middle-clicking the tab raises no close + // event. Because that's dumb, we implement our own middle-click handling. + // `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden, + // and that enables all of the rest of this machinery (and this workaround). + bool _tabItemMiddleClickHookEnabled = false; + bool _tabItemMiddleClickExited = false; + PointerEntered_revoker _tabItemMiddleClickPointerEntered; + PointerExited_revoker _tabItemMiddleClickPointerExited; + PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost; + void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); + safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender); + + void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); + void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); + void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs); + void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs); + void _UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab); + void _UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); + + void _OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command); + void _OnCommandLineExecutionRequested(const IInspectable& sender, const winrt::hstring& commandLine); + void _OnSwitchToTabRequested(const IInspectable& sender, const winrt::TerminalApp::Tab& tab); + + void _Find(const Tab& tab); + + winrt::Microsoft::Terminal::Control::TermControl _CreateNewControlAndContent(const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& settings, + const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); + winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term); + winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid); + + TerminalApp::IPaneContent _makeSettingsContent(); + std::shared_ptr _MakeTerminalPane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr, + const winrt::TerminalApp::Tab& sourceTab = nullptr, + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + std::shared_ptr _MakePane(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs = nullptr, + const winrt::TerminalApp::Tab& sourceTab = nullptr, + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + + void _RefreshUIForSettingsReload(); + + void _SetNewTabButtonColor(til::color color, til::color accentColor); + void _ClearNewTabButtonColor(); + + safe_void_coroutine _CompleteInitialization(); + + void _FocusActiveControl(IInspectable sender, IInspectable eventArgs); + + void _UnZoomIfNeeded(); + + static int _ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll); + static uint32_t _ReadSystemRowsToScroll(); + + void _UpdateMRUTab(const winrt::TerminalApp::Tab& tab); + bool _TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept; + bool _IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept; + void _SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor = nullptr); + void _RemoveSelectedTab(const winrt::TerminalApp::Tab& tab); + std::vector _GetSelectedTabsInDisplayOrder() const; + std::vector _GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const; + void _ApplyMultiSelectionVisuals(); + void _UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab); + void _MoveTabsToIndex(const std::vector& tabs, uint32_t suggestedNewTabIndex); + std::vector _CollectNewTabs(const std::vector& existingTabs) const; + std::vector _BuildStartupActionsForTabs(const std::vector& tabs) const; + + void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex); + + void _PreviewAction(const Microsoft::Terminal::Settings::Model::ActionAndArgs& args); + void _PreviewActionHandler(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& args); + void _EndPreview(); + void _RunRestorePreviews(); + void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); + void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); + void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args); + + winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; + std::vector> _restorePreviewFuncs{}; + + HRESULT _OnNewConnection(const winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection& connection); + void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + + void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); + void _RequestWindowRename(const winrt::hstring& newName); + void _WindowRenamerKeyDown(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + void _WindowRenamerKeyUp(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + + void _UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element); + + winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept; + + bool _maybeElevate(const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, + const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& controlSettings, + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); + void _OpenElevatedWT(winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + + safe_void_coroutine _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; + void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; + static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); + static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); + + void _updateThemeColors(); + void _updateAllTabCloseButtons(); + void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + + safe_void_coroutine _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); + + void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode, winrt::hstring filterText); + + void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + Windows::Foundation::IAsyncAction _SearchMissingCommandHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SearchMissingCommandEventArgs args); + static Windows::Foundation::IAsyncOperation> _FindPackageAsync(hstring query); + + void _WindowSizeChanged(const IInspectable sender, const winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs args); + void _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + + void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); + void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + void _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e); + void _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e); + + void _DetachPaneFromWindow(std::shared_ptr pane); + void _DetachTabFromWindow(const winrt::com_ptr& tabImpl); + void _MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint = std::nullopt); + void _sendDraggedTabsToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); + + void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); + void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); + winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); + + winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); + winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); + + void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args); + safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs); + +#pragma region ActionHandlers + // These are all defined in AppActionHandlers.cpp +#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); + ALL_SHORTCUT_ACTIONS + INTERNAL_SHORTCUT_ACTIONS +#undef ON_ALL_ACTIONS +#pragma endregion + + friend class TerminalAppLocalTests::TabTests; + friend class TerminalAppLocalTests::SettingsTests; + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(TerminalPage); +} diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 266ece81db7..1f5fc836b48 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -1295,6 +1295,11 @@ namespace winrt::TerminalApp::implementation } } + bool TerminalWindow::SelectTabRangeForTesting(const uint32_t startIndex, const uint32_t endIndex) + { + return _root ? _root->SelectTabRangeForTesting(startIndex, endIndex) : false; + } + bool TerminalWindow::ShouldImmediatelyHandoffToElevated() { return _root != nullptr ? _root->ShouldImmediatelyHandoffToElevated(_settings) : false; diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index 2f1aad5a7aa..9d592fcafee 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -144,6 +144,7 @@ namespace winrt::TerminalApp::implementation void AttachContent(winrt::hstring content, uint32_t tabIndex); void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); + bool SelectTabRangeForTesting(uint32_t startIndex, uint32_t endIndex); // -------------------------------- WinRT Events --------------------------------- // PropertyChanged is surprisingly not a typed event, so we'll define that one manually. diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index b900522cbf2..c73083db04e 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -140,5 +140,6 @@ namespace TerminalApp void AttachContent(String content, UInt32 tabIndex); void SendContentToOther(RequestReceiveContentArgs args); + Boolean SelectTabRangeForTesting(UInt32 startIndex, UInt32 endIndex); } } diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 522e90c7d28..fa6058df54c 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -27,6 +27,11 @@ using namespace std::chrono_literals; static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; static constexpr auto FrameUpdateInterval = std::chrono::milliseconds(16); +static bool _uiaTestHooksEnabled() noexcept +{ + return GetEnvironmentVariableW(L"WT_UIA_ENABLE_TEST_HOOKS", nullptr, 0) != 0; +} + winrt::com_ptr getDesktopManager() { static til::shared_mutex> s_desktopManager; @@ -70,6 +75,9 @@ AppHost::AppHost(WindowEmperor* manager, const winrt::TerminalApp::AppLogic& log // Tell the window to callback to us when it's about to handle a WM_CREATE auto pfn = [this](auto&& PH1, auto&& PH2) { _HandleCreateWindow(std::forward(PH1), std::forward(PH2)); }; _window->SetCreateCallback(pfn); + _window->SetUiaSelectTabRangeCallback([this](const uint32_t startIndex, const uint32_t endIndex) { + return _SelectTabRangeForTesting(startIndex, endIndex); + }); _windowCallbacks.MouseScrolled = _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _windowCallbacks.WindowActivated = _window->WindowActivated({ this, &AppHost::_WindowActivated }); @@ -93,6 +101,13 @@ bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, cons return false; } +bool AppHost::_SelectTabRangeForTesting(const uint32_t startIndex, const uint32_t endIndex) +{ + return _windowLogic && + _uiaTestHooksEnabled() && + _windowLogic.SelectTabRangeForTesting(startIndex, endIndex); +} + // Method Description: // - Event handler to update the taskbar progress indicator // - Upon receiving the event, we ask the underlying logic for the taskbar state/progress values diff --git a/src/cascadia/WindowsTerminal/AppHost.h b/src/cascadia/WindowsTerminal/AppHost.h index 379f876b943..5934d75e321 100644 --- a/src/cascadia/WindowsTerminal/AppHost.h +++ b/src/cascadia/WindowsTerminal/AppHost.h @@ -55,6 +55,7 @@ class AppHost : public std::enable_shared_from_this winrt::Microsoft::Terminal::Settings::Model::LaunchPosition _GetWindowLaunchPosition(); void _HandleCreateWindow(HWND hwnd, const til::rect& proposedRect); + bool _SelectTabRangeForTesting(uint32_t startIndex, uint32_t endIndex); void _UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::UIElement& arg); diff --git a/src/cascadia/WindowsTerminal/BaseWindow.h b/src/cascadia/WindowsTerminal/BaseWindow.h index f7809462826..6aae009aefd 100644 --- a/src/cascadia/WindowsTerminal/BaseWindow.h +++ b/src/cascadia/WindowsTerminal/BaseWindow.h @@ -8,6 +8,7 @@ class BaseWindow { public: static constexpr UINT CM_UPDATE_TITLE = WM_USER + 0; + static constexpr UINT CM_UIA_SELECT_TAB_RANGE = WM_USER + 1; static T* GetThisFromHandle(HWND const window) noexcept { diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp index e627bd44957..5831fc3238d 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.cpp +++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp @@ -200,6 +200,11 @@ void IslandWindow::SetSnapDimensionCallback(std::function pf _pfnSnapDimensionCallback = pfn; } +void IslandWindow::SetUiaSelectTabRangeCallback(std::function pfn) noexcept +{ + _pfnUiaSelectTabRangeCallback = std::move(pfn); +} + // Method Description: // - Handles a WM_CREATE message. Calls our create callback, if one's been set. // Arguments: @@ -493,6 +498,15 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam _HandleCreateWindow(wparam, lparam); return 0; } + case CM_UIA_SELECT_TAB_RANGE: + { + if (_pfnUiaSelectTabRangeCallback) + { + return _pfnUiaSelectTabRangeCallback(gsl::narrow_cast(wparam), + gsl::narrow_cast(static_cast(lparam))) ? 1 : 0; + } + return 0; + } case WM_ENABLE: { if (_interopWindowHandle != nullptr) diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 83afeaf44f5..02f6214ca62 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -1,166 +1,168 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once -#include "BaseWindow.h" - -struct SystemMenuItemInfo -{ - winrt::hstring label; - winrt::delegate callback; -}; - -class IslandWindow : - public BaseWindow -{ -public: - static bool IsCursorHidden() noexcept; - static void HideCursor() noexcept; - static void ShowCursorMaybe(const UINT message) noexcept; - - IslandWindow() noexcept; - ~IslandWindow(); - - virtual void MakeWindow() noexcept; - virtual void Close(); - - virtual void OnSize(const UINT width, const UINT height); - HWND GetInteropHandle() const; - - [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; - - [[nodiscard]] LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept; - - void OnResize(const UINT width, const UINT height); - void OnMinimize(); - void OnRestore(); - virtual void OnAppInitialized(); - virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); - virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; - virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; - - virtual void Initialize(); - - void SetCreateCallback(std::function pfn) noexcept; - - void SetSnapDimensionCallback(std::function pfn) noexcept; - - void FocusModeChanged(const bool focusMode); - void FullscreenChanged(const bool fullscreen); - void SetAlwaysOnTop(const bool alwaysOnTop); - void ShowWindowChanged(const bool showOrHide); - virtual void SetShowTabsFullscreen(const bool newShowTabsFullscreen); - - void FlashTaskbar(); - void SetTaskbarProgress(const size_t state, const size_t progress); - - void SummonWindow(winrt::TerminalApp::SummonWindowBehavior args); - - bool IsQuakeWindow() const noexcept; - void IsQuakeWindow(bool isQuakeWindow) noexcept; - void SetAutoHideWindow(bool autoHideWindow) noexcept; - - void HideWindow(); - - void SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept; - - void OpenSystemMenu(const std::optional mouseX, const std::optional mouseY) const noexcept; - void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); - void RemoveFromSystemMenu(const winrt::hstring& itemLabel); - - void UseDarkTheme(const bool v); - virtual void UseMica(const bool newValue, const double titlebarOpacity); - - til::event> DragRegionClicked; - til::event> WindowCloseButtonClicked; - til::event> MouseScrolled; - til::event> WindowActivated; - til::event> NotifyNotificationIconPressed; - til::event> NotifyWindowHidden; - til::event> NotifyNotificationIconMenuItemSelected; - til::event> NotifyReAddNotificationIcon; - til::event> ShouldExitFullscreen; - til::event> MaximizeChanged; - - til::event> WindowMoved; - til::event> WindowVisibilityChanged; - -protected: - void ForceResize() - { - // Do a quick resize to force the island to paint - const auto size = GetPhysicalSize(); - OnSize(size.width, size.height); - } - - HWND _interopWindowHandle; - - winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor - winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor - wil::com_ptr _taskbar; - - std::function _pfnCreateCallback; - std::function _pfnSnapDimensionCallback; - - void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; - [[nodiscard]] LRESULT _OnSizing(const WPARAM wParam, const LPARAM lParam); - [[nodiscard]] LRESULT _OnMoving(const WPARAM wParam, const LPARAM lParam); - - bool _borderless{ false }; - bool _alwaysOnTop{ false }; - bool _fullscreen{ false }; - bool _showTabsFullscreen{ false }; - bool _fWasMaximizedBeforeFullscreen{ false }; - RECT _rcWindowBeforeFullscreen{}; - RECT _rcWorkBeforeFullscreen{}; - UINT _dpiBeforeFullscreen{ 96 }; - - virtual void _SetIsBorderless(const bool borderlessEnabled); - virtual void _SetIsFullscreen(const bool fullscreenEnabled); - - void _RestoreFullscreenPosition(const RECT& rcWork); - void _SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork); - - LONG _getDesiredWindowStyle() const; - - void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); - - void _globalActivateWindow(const uint32_t dropdownDuration, - const winrt::TerminalApp::MonitorBehavior toMonitor); - void _dropdownWindow(const uint32_t dropdownDuration, - const winrt::TerminalApp::MonitorBehavior toMonitor); - void _slideUpWindow(const uint32_t dropdownDuration); - void _doSlideAnimation(const uint32_t dropdownDuration, const bool down); - void _globalDismissWindow(const uint32_t dropdownDuration); - - static MONITORINFO _getMonitorForCursor(); - static MONITORINFO _getMonitorForWindow(HWND foregroundWindow); - void _moveToMonitor(HWND foregroundWindow, const winrt::TerminalApp::MonitorBehavior toMonitor); - void _moveToMonitorOfMouse(); - void _moveToMonitorOf(HWND foregroundWindow); - void _moveToMonitor(const MONITORINFO activeMonitor); - - bool _isQuakeWindow{ false }; - bool _autoHideWindow{ false }; - - void _enterQuakeMode(); - til::rect _getQuakeModeSize(HMONITOR hmon); - - bool _minimizeToNotificationArea{ false }; - - std::unordered_map _systemMenuItems; - UINT _systemMenuNextItemId = 0; - void _resetSystemMenu(); - -private: - // This minimum width allows for width the tabs fit - static constexpr float minimumWidth = 460; - - // We run with no height requirement for client area, - // though the total height will take into account the non-client area - // and the requirements of components hosted in the client area - static constexpr float minimumHeight = 0; - - inline static bool _cursorHidden; -}; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include "BaseWindow.h" + +struct SystemMenuItemInfo +{ + winrt::hstring label; + winrt::delegate callback; +}; + +class IslandWindow : + public BaseWindow +{ +public: + static bool IsCursorHidden() noexcept; + static void HideCursor() noexcept; + static void ShowCursorMaybe(const UINT message) noexcept; + + IslandWindow() noexcept; + ~IslandWindow(); + + virtual void MakeWindow() noexcept; + virtual void Close(); + + virtual void OnSize(const UINT width, const UINT height); + HWND GetInteropHandle() const; + + [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; + + [[nodiscard]] LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept; + + void OnResize(const UINT width, const UINT height); + void OnMinimize(); + void OnRestore(); + virtual void OnAppInitialized(); + virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); + virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; + virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; + + virtual void Initialize(); + + void SetCreateCallback(std::function pfn) noexcept; + + void SetSnapDimensionCallback(std::function pfn) noexcept; + void SetUiaSelectTabRangeCallback(std::function pfn) noexcept; + + void FocusModeChanged(const bool focusMode); + void FullscreenChanged(const bool fullscreen); + void SetAlwaysOnTop(const bool alwaysOnTop); + void ShowWindowChanged(const bool showOrHide); + virtual void SetShowTabsFullscreen(const bool newShowTabsFullscreen); + + void FlashTaskbar(); + void SetTaskbarProgress(const size_t state, const size_t progress); + + void SummonWindow(winrt::TerminalApp::SummonWindowBehavior args); + + bool IsQuakeWindow() const noexcept; + void IsQuakeWindow(bool isQuakeWindow) noexcept; + void SetAutoHideWindow(bool autoHideWindow) noexcept; + + void HideWindow(); + + void SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept; + + void OpenSystemMenu(const std::optional mouseX, const std::optional mouseY) const noexcept; + void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); + void RemoveFromSystemMenu(const winrt::hstring& itemLabel); + + void UseDarkTheme(const bool v); + virtual void UseMica(const bool newValue, const double titlebarOpacity); + + til::event> DragRegionClicked; + til::event> WindowCloseButtonClicked; + til::event> MouseScrolled; + til::event> WindowActivated; + til::event> NotifyNotificationIconPressed; + til::event> NotifyWindowHidden; + til::event> NotifyNotificationIconMenuItemSelected; + til::event> NotifyReAddNotificationIcon; + til::event> ShouldExitFullscreen; + til::event> MaximizeChanged; + + til::event> WindowMoved; + til::event> WindowVisibilityChanged; + +protected: + void ForceResize() + { + // Do a quick resize to force the island to paint + const auto size = GetPhysicalSize(); + OnSize(size.width, size.height); + } + + HWND _interopWindowHandle; + + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor + wil::com_ptr _taskbar; + + std::function _pfnCreateCallback; + std::function _pfnSnapDimensionCallback; + std::function _pfnUiaSelectTabRangeCallback; + + void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; + [[nodiscard]] LRESULT _OnSizing(const WPARAM wParam, const LPARAM lParam); + [[nodiscard]] LRESULT _OnMoving(const WPARAM wParam, const LPARAM lParam); + + bool _borderless{ false }; + bool _alwaysOnTop{ false }; + bool _fullscreen{ false }; + bool _showTabsFullscreen{ false }; + bool _fWasMaximizedBeforeFullscreen{ false }; + RECT _rcWindowBeforeFullscreen{}; + RECT _rcWorkBeforeFullscreen{}; + UINT _dpiBeforeFullscreen{ 96 }; + + virtual void _SetIsBorderless(const bool borderlessEnabled); + virtual void _SetIsFullscreen(const bool fullscreenEnabled); + + void _RestoreFullscreenPosition(const RECT& rcWork); + void _SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork); + + LONG _getDesiredWindowStyle() const; + + void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); + + void _globalActivateWindow(const uint32_t dropdownDuration, + const winrt::TerminalApp::MonitorBehavior toMonitor); + void _dropdownWindow(const uint32_t dropdownDuration, + const winrt::TerminalApp::MonitorBehavior toMonitor); + void _slideUpWindow(const uint32_t dropdownDuration); + void _doSlideAnimation(const uint32_t dropdownDuration, const bool down); + void _globalDismissWindow(const uint32_t dropdownDuration); + + static MONITORINFO _getMonitorForCursor(); + static MONITORINFO _getMonitorForWindow(HWND foregroundWindow); + void _moveToMonitor(HWND foregroundWindow, const winrt::TerminalApp::MonitorBehavior toMonitor); + void _moveToMonitorOfMouse(); + void _moveToMonitorOf(HWND foregroundWindow); + void _moveToMonitor(const MONITORINFO activeMonitor); + + bool _isQuakeWindow{ false }; + bool _autoHideWindow{ false }; + + void _enterQuakeMode(); + til::rect _getQuakeModeSize(HMONITOR hmon); + + bool _minimizeToNotificationArea{ false }; + + std::unordered_map _systemMenuItems; + UINT _systemMenuNextItemId = 0; + void _resetSystemMenu(); + +private: + // This minimum width allows for width the tabs fit + static constexpr float minimumWidth = 460; + + // We run with no height requirement for client area, + // though the total height will take into account the non-client area + // and the requirements of components hosted in the client area + static constexpr float minimumHeight = 0; + + inline static bool _cursorHidden; +}; diff --git a/src/cascadia/WindowsTerminal_UIATests/Common/NativeMethods.cs b/src/cascadia/WindowsTerminal_UIATests/Common/NativeMethods.cs index 0570824efb8..8c162f8c97b 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Common/NativeMethods.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Common/NativeMethods.cs @@ -398,6 +398,8 @@ public struct NT_FE_CONSOLE_PROPS public static class User32 { // http://msdn.microsoft.com/en-us/library/windows/desktop/dd162897(v=vs.85).aspx + public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + [StructLayout(LayoutKind.Sequential)] public struct RECT { @@ -416,14 +418,39 @@ public struct POINT public const int WHEEL_DELTA = 120; + [DllImport("user32.dll")] + public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll")] + public static extern bool IsWindowVisible(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + [DllImport("user32.dll")] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(int nIndex); + public const int GWL_STYLE = (-16); public const int GWL_EXSTYLE = (-20); + public const int SM_CXSCREEN = 0; + public const int SM_CYSCREEN = 1; + public const uint SWP_NOZORDER = 0x0004; + public const uint SWP_SHOWWINDOW = 0x0040; [DllImport("user32.dll", SetLastError = true)] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); @@ -442,6 +469,7 @@ public enum WindowMessages : UInt32 WM_MOUSEWHEEL = 0x020A, WM_MOUSEHWHEEL = 0x020E, WM_USER = 0x0400, + CM_UIA_SELECT_TAB_RANGE = WM_USER + 1, CM_SET_KEY_STATE = WM_USER + 18 } diff --git a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs index ac944525e98..b97e8826e73 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs @@ -6,8 +6,13 @@ //---------------------------------------------------------------------------------------------------------------------- namespace WindowsTerminal.UIA.Tests.Elements { + using System.Collections.Generic; using System; + using System.Diagnostics; + using System.Globalization; using System.IO; + using System.Linq; + using System.Threading; using WindowsTerminal.UIA.Tests.Common; using WindowsTerminal.UIA.Tests.Common.NativeMethods; @@ -24,13 +29,71 @@ namespace WindowsTerminal.UIA.Tests.Elements using System.Runtime.InteropServices; using System.Security.Principal; using OpenQA.Selenium; + using System.Windows.Automation; public class TerminalApp : IDisposable { protected const string AppDriverUrl = "http://127.0.0.1:4723"; + private const uint MouseEventLeftDown = 0x0002; + private const uint MouseEventLeftUp = 0x0004; + private const uint KeyEventKeyUp = 0x0002; + private const byte VirtualKeyControl = 0x11; + private const byte VirtualKeyShift = 0x10; + private const int InputMouse = 0; + private const int InputKeyboard = 1; private IntPtr job; + [DllImport("user32.dll", SetLastError = true)] + private static extern bool SetCursorPos(int x, int y); + + [DllImport("user32.dll", SetLastError = true)] + private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo); + + [DllImport("user32.dll", SetLastError = true)] + private static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, UIntPtr extraInfo); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure); + + [StructLayout(LayoutKind.Sequential)] + private struct INPUT + { + public int type; + public INPUTUNION U; + } + + [StructLayout(LayoutKind.Explicit)] + private struct INPUTUNION + { + [FieldOffset(0)] + public MOUSEINPUT mi; + + [FieldOffset(0)] + public KEYBDINPUT ki; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MOUSEINPUT + { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + + [StructLayout(LayoutKind.Sequential)] + private struct KEYBDINPUT + { + public ushort wVk; + public ushort wScan; + public uint dwFlags; + public uint time; + public IntPtr dwExtraInfo; + } + public IOSDriver Session { get; private set; } public Actions Actions { get; private set; } public AppiumWebElement UIRoot { get; private set; } @@ -38,6 +101,43 @@ public class TerminalApp : IDisposable private bool isDisposed = false; private TestContext context; + private string _windowTitleToFind; + private string _dragEventLogPath; + private readonly HashSet _trackedProcessIds = new HashSet(); + private readonly Dictionary> _windowSessions = new Dictionary>(); + private readonly List _scheduledTaskNames = new List(); + private readonly List _temporaryLaunchArtifacts = new List(); + private IOSDriver _desktopSession; + + public sealed class TopLevelWindow + { + public TopLevelWindow(IntPtr handle, string title) + { + Handle = handle; + Title = title; + RefreshBounds(); + } + + public IntPtr Handle { get; } + + public string Title { get; } + + public User32.RECT Bounds { get; private set; } + + internal IOSDriver Session { get; set; } + + internal AppiumWebElement Root { get; set; } + + public int Width => Bounds.right - Bounds.left; + + public int Height => Bounds.bottom - Bounds.top; + + public void RefreshBounds() + { + NativeMethods.Win32BoolHelper(User32.GetWindowRect(Handle, out var rect), $"Get bounds for top-level window '{Title}'."); + Bounds = rect; + } + } public string ContentPath { get; private set; } public string GetFullTestContentPath(string filename) @@ -45,9 +145,10 @@ public string GetFullTestContentPath(string filename) return Path.GetFullPath(Path.Combine(ContentPath, filename)); } - public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe") + public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe", string launchArgs = null, string windowTitleToFind = "WindowsTerminal.UIA.Tests") { this.context = context; + _windowTitleToFind = windowTitleToFind; // If running locally, set WTPath to where we can find a loose // deployment of Windows Terminal. That means you'll need to build @@ -76,7 +177,7 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe") } Log.Comment($"Test Content will be loaded from '{Path.GetFullPath(ContentPath)}'"); - this.CreateProcess(path, shellToLaunch); + this.CreateProcess(path, shellToLaunch, launchArgs); } ~TerminalApp() @@ -95,6 +196,813 @@ public AppiumWebElement GetRoot() return this.UIRoot; } + public TopLevelWindow FindTopLevelWindowByName(string name, TopLevelWindow excludedWindow = null) + { + var matches = new List(); + var excludedHandle = excludedWindow?.Handle ?? IntPtr.Zero; + + User32.EnumWindows((hWnd, _) => + { + if (hWnd == excludedHandle || !User32.IsWindowVisible(hWnd)) + { + return true; + } + + var title = _GetWindowText(hWnd); + if (!string.Equals(title, name, StringComparison.Ordinal)) + { + return true; + } + + matches.Add(new TopLevelWindow(hWnd, title)); + return true; + }, IntPtr.Zero); + + var windowMatch = matches.OrderByDescending(window => window.Width * window.Height).FirstOrDefault(); + if (windowMatch != null) + { + _AttachToWindow(windowMatch); + } + + return windowMatch; + } + + public TopLevelWindow WaitForTopLevelWindowByName(string name, TopLevelWindow excludedWindow = null, int timeoutMs = 15000) + { + var remaining = timeoutMs; + while (remaining >= 0) + { + var window = FindTopLevelWindowByName(name, excludedWindow); + if (window != null) + { + return window; + } + + Thread.Sleep(250); + remaining -= 250; + } + + _LogVisibleWindows(name); + throw new InvalidOperationException($"Timed out waiting for top-level window '{name}'."); + } + + public TopLevelWindow TryWaitForTopLevelWindowByName(string name, TopLevelWindow excludedWindow = null, int timeoutMs = 15000) + { + var remaining = timeoutMs; + while (remaining >= 0) + { + var window = FindTopLevelWindowByName(name, excludedWindow); + if (window != null) + { + return window; + } + + Thread.Sleep(250); + remaining -= 250; + } + + return null; + } + + public void ActivateWindow(TopLevelWindow window) + { + if (!User32.SetForegroundWindow(window.Handle)) + { + Log.Comment($"SetForegroundWindow returned false for top-level window '{window.Title}' (0x{window.Handle.ToInt64():x}); continuing with existing attachment."); + } + Thread.Sleep(250); + window.RefreshBounds(); + _AttachToWindow(window); + Session = window.Session; + UIRoot = window.Root; + } + + public void ArrangeWindowOnPrimaryMonitor(TopLevelWindow window, int slotIndex, int slotCount = 2) + { + Verify.IsNotNull(window); + Verify.IsTrue(slotCount > 0, "Expected at least one monitor slot."); + Verify.IsTrue(slotIndex >= 0 && slotIndex < slotCount, $"Expected slot index {slotIndex} to be within {slotCount} slots."); + + const int margin = 24; + const int top = 120; + var primaryWidth = User32.GetSystemMetrics(User32.SM_CXSCREEN); + var primaryHeight = User32.GetSystemMetrics(User32.SM_CYSCREEN); + + Verify.IsTrue(primaryWidth > 0, "Expected a valid primary monitor width."); + Verify.IsTrue(primaryHeight > 0, "Expected a valid primary monitor height."); + + var availableWidth = primaryWidth - ((slotCount + 1) * margin); + var targetWidth = Math.Max(720, availableWidth / slotCount); + var maxHeight = Math.Max(480, primaryHeight - top - margin); + var targetHeight = Math.Min(window.Height, maxHeight); + var targetX = margin + ((targetWidth + margin) * slotIndex); + + NativeMethods.Win32BoolHelper( + User32.SetWindowPos(window.Handle, IntPtr.Zero, targetX, top, targetWidth, targetHeight, User32.SWP_NOZORDER | User32.SWP_SHOWWINDOW), + $"Arrange top-level window '{window.Title}' in primary monitor slot {slotIndex}."); + + Thread.Sleep(400); + window.RefreshBounds(); + } + + public AppiumWebElement FindElementByName(TopLevelWindow window, string name) + { + window.RefreshBounds(); + + _AttachToWindow(window); + + var matches = window.Session.FindElementsByName(name) + .OfType() + .Where(_IsValidElement); + if (ReferenceEquals(window.Session, _desktopSession)) + { + matches = matches.Where(element => _IsElementWithinWindow(element, window)); + } + + return matches.OrderByDescending(element => element.Size.Width * element.Size.Height) + .ThenBy(element => element.Location.X) + .ThenBy(element => element.Location.Y) + .First(); + } + + public AppiumWebElement FindTabElementByName(TopLevelWindow window, string name) + { + window.RefreshBounds(); + + _AttachToWindow(window); + + var matches = window.Session.FindElementsByName(name) + .OfType() + .Where(_IsValidElement); + if (ReferenceEquals(window.Session, _desktopSession)) + { + matches = matches.Where(element => _IsElementWithinWindow(element, window)); + } + + return matches.Where(element => element.Size.Height <= 80 && + element.Size.Width < window.Width && + element.Location.Y < window.Bounds.top + 96) + .OrderBy(element => element.Location.X) + .ThenBy(element => element.Location.Y) + .First(); + } + + public bool HasElementByName(TopLevelWindow window, string name) + { + window.RefreshBounds(); + + _AttachToWindow(window); + + var matches = window.Session.FindElementsByName(name) + .OfType() + .Where(_IsValidElement); + if (ReferenceEquals(window.Session, _desktopSession)) + { + matches = matches.Where(element => _IsElementWithinWindow(element, window)); + } + + return matches.Any(); + } + + public void LogWindowDetails(string label, TopLevelWindow window) + { + window.RefreshBounds(); + Log.Comment( + $"{label}: title='{window.Title}', handle=0x{window.Handle.ToInt64():x}, bounds=({window.Bounds.left},{window.Bounds.top})-({window.Bounds.right},{window.Bounds.bottom})"); + } + + public void LogElementDetails(string label, TopLevelWindow window, AppiumWebElement element) + { + window.RefreshBounds(); + Log.Comment( + $"{label}: " + + $"element(Name='{_TryGetAttribute(element, "Name")}', Class='{_TryGetAttribute(element, "ClassName")}', Handle='{_TryGetAttribute(element, "NativeWindowHandle")}', Loc=({element.Location.X},{element.Location.Y}), Size=({element.Size.Width},{element.Size.Height}), Id='{element.Id}'); " + + $"windowHandle=0x{window.Handle.ToInt64():x}, windowBounds=({window.Bounds.left},{window.Bounds.top})-({window.Bounds.right},{window.Bounds.bottom})"); + } + + public void LogElementAncestors(string label, AppiumWebElement element, int maxDepth = 6) + { + var current = element; + for (var depth = 0; depth < maxDepth && current != null; depth++) + { + Log.Comment( + $"{label}[{depth}]: " + + $"Name='{_TryGetAttribute(current, "Name")}', Class='{_TryGetAttribute(current, "ClassName")}', ControlType='{_TryGetAttribute(current, "ControlType")}', " + + $"Loc=({current.Location.X},{current.Location.Y}), Size=({current.Size.Width},{current.Size.Height}), Id='{current.Id}'"); + + AppiumWebElement parent = null; + try + { + parent = current.FindElementByXPath(".."); + } + catch + { + break; + } + + if (parent == null || parent.Id == current.Id) + { + break; + } + + current = parent; + } + } + + private static string _TryGetAttribute(AppiumWebElement element, string attributeName) + { + try + { + return element?.GetAttribute(attributeName); + } + catch + { + return null; + } + } + + private static bool _IsValidElement(AppiumWebElement element) + { + return element != null && !string.IsNullOrEmpty(element.Id); + } + + private static bool _IsElementWithinWindow(AppiumWebElement element, TopLevelWindow window) + { + var centerX = element.Location.X + (element.Size.Width / 2); + var centerY = element.Location.Y + (element.Size.Height / 2); + return centerX >= window.Bounds.left && + centerX < window.Bounds.right && + centerY >= window.Bounds.top && + centerY < window.Bounds.bottom; + } + + private static string _GetWindowText(IntPtr hWnd) + { + var length = User32.GetWindowTextLength(hWnd); + if (length <= 0) + { + return string.Empty; + } + + var buffer = new System.Text.StringBuilder(length + 1); + User32.GetWindowText(hWnd, buffer, buffer.Capacity); + return buffer.ToString(); + } + + private void _AttachToWindow(TopLevelWindow window) + { + if (window == null) + { + return; + } + + if (window.Session != null && window.Root != null) + { + return; + } + + if (_windowSessions.TryGetValue(window.Handle, out var existingSession)) + { + try + { + window.Session = existingSession; + try + { + window.Root = existingSession.FindElementByXPath("/*"); + } + catch + { + window.Root = existingSession.FindElementByXPath("//*"); + } + + return; + } + catch (Exception ex) + { + Log.Comment($"Reused WinAppDriver session for window 0x{window.Handle.ToInt64():x} ('{window.Title}') became invalid: {ex.Message}"); + try + { + existingSession.Quit(); + } + catch + { + } + + _windowSessions.Remove(window.Handle); + window.Session = null; + window.Root = null; + } + } + + var capabilities = new DesiredCapabilities(); + capabilities.SetCapability("appTopLevelWindow", window.Handle.ToInt64().ToString("x")); + + try + { + var windowSession = new IOSDriver(new Uri(AppDriverUrl), capabilities); + Verify.IsNotNull(windowSession, $"Attach WinAppDriver session to top-level window '{window.Title}'."); + + AppiumWebElement rootElement; + try + { + rootElement = windowSession.FindElementByXPath("/*"); + } + catch + { + rootElement = windowSession.FindElementByXPath("//*"); + } + + window.Session = windowSession; + window.Root = rootElement; + _windowSessions[window.Handle] = windowSession; + } + catch + { + Log.Comment($"Falling back to desktop session for window 0x{window.Handle.ToInt64():x} ('{window.Title}') because appTopLevelWindow attach failed."); + window.Session = _desktopSession; + if (UIRoot != null) + { + window.Root = UIRoot; + } + else + { + try + { + window.Root = _desktopSession?.FindElementByXPath("/*"); + } + catch + { + try + { + window.Root = _desktopSession?.FindElementByXPath("//*"); + } + catch + { + window.Root = null; + } + } + } + } + } + + private void _LogVisibleWindows(string targetName) + { + var visibleTitles = new List(); + User32.EnumWindows((hWnd, _) => + { + if (!User32.IsWindowVisible(hWnd)) + { + return true; + } + + var title = _GetWindowText(hWnd); + if (string.IsNullOrWhiteSpace(title)) + { + return true; + } + + if (title.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0 || + title.IndexOf(_windowTitleToFind, StringComparison.OrdinalIgnoreCase) >= 0 || + title.IndexOf("DragTab", StringComparison.OrdinalIgnoreCase) >= 0 || + title.IndexOf("Terminal", StringComparison.OrdinalIgnoreCase) >= 0) + { + visibleTitles.Add($"0x{hWnd.ToInt64():x}: {title}"); + } + + return true; + }, IntPtr.Zero); + + Log.Comment($"Visible top-level windows near '{targetName}': {visibleTitles.Count}"); + foreach (var title in visibleTitles.Take(20)) + { + Log.Comment(title); + } + } + + private static AppiumWebElement _LiftToWindowRoot(AppiumWebElement element) + { + if (element == null) + { + return null; + } + + var current = element; + var processId = _TryGetAttribute(current, "ProcessId"); + for (var i = 0; i < 20; ++i) + { + AppiumWebElement parent = null; + try + { + parent = current.FindElementByXPath(".."); + } + catch + { + break; + } + + if (parent == null || parent.Id == current.Id) + { + break; + } + + if (!string.IsNullOrEmpty(processId)) + { + var parentPid = _TryGetAttribute(parent, "ProcessId"); + if (!string.Equals(parentPid, processId, StringComparison.Ordinal)) + { + break; + } + } + + current = parent; + } + + return current; + } + + public void CtrlClick(AppiumWebElement element) + { + _ModifiedClick(element, VirtualKeyControl); + } + + public void CtrlClickTab(TopLevelWindow window, string name) + { + _ModifiedNativeTabClick(window, name, VirtualKeyControl); + } + + public void ShiftClick(AppiumWebElement element) + { + _ModifiedClick(element, VirtualKeyShift); + } + + public void ShiftClickTab(TopLevelWindow window, string name) + { + _ModifiedNativeTabClick(window, name, VirtualKeyShift); + } + + public void ClickTab(TopLevelWindow window, string name) + { + _NativeTabClick(window, name); + } + + public void SelectTabRangeForTesting(TopLevelWindow window, int startIndex, int endIndex) + { + var result = User32.SendMessage(window.Handle, User32.WindowMessages.CM_UIA_SELECT_TAB_RANGE, startIndex, new IntPtr(endIndex)); + Log.Comment($"SelectTabRangeForTesting: window=0x{window.Handle.ToInt64():x}, start={startIndex}, end={endIndex}, result={result}"); + Verify.IsTrue(result != IntPtr.Zero, $"Select tab range {startIndex}-{endIndex} for '{window.Title}'."); + Thread.Sleep(200); + } + + public void LogNativeTabSelectionState(TopLevelWindow window, string name) + { + var element = _FindNativeTabElement(window, name); + var isSelected = false; + if (element.TryGetCurrentPattern(SelectionItemPattern.Pattern, out var selectionPattern)) + { + isSelected = ((SelectionItemPattern)selectionPattern).Current.IsSelected; + } + + var rect = element.Current.BoundingRectangle; + Log.Comment($"Native UIA tab '{name}' selection: Selected={isSelected}, Focused={element.Current.HasKeyboardFocus}, Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); + } + + public void DragByOffset(AppiumWebElement source, int offsetX, int offsetY) + { + var (sourceCenterX, sourceCenterY) = _GetScreenCenter(source); + var targetX = sourceCenterX + offsetX; + var targetY = sourceCenterY + offsetY; + + Log.Comment($"Native drag from ({sourceCenterX}, {sourceCenterY}) to ({targetX}, {targetY})"); + + _MoveMouseToElementOffset(source, 0, 0); + Thread.Sleep(100); + + Session.Mouse.MouseDown(null); + Thread.Sleep(250); + + _MoveCursorSmoothly(source, 0, 0, 36, 0, 8, 25); + Thread.Sleep(200); + + _MoveCursorSmoothly(source, 36, 0, 84, 0, 10, 25); + Thread.Sleep(250); + + _MoveCursorSmoothly(source, 84, 0, offsetX, offsetY, 36, 20); + + Thread.Sleep(700); + Session.Mouse.MouseUp(null); + Thread.Sleep(400); + } + + public void DragToElement(AppiumWebElement source, AppiumWebElement target, int targetOffsetX = 0, int targetOffsetY = 0) + { + var (sourceCenterX, sourceCenterY) = _GetScreenCenter(source); + var (targetCenterX, targetCenterY) = _GetScreenCenter(target); + targetCenterX += targetOffsetX; + targetCenterY += targetOffsetY; + DragByOffset(source, targetCenterX - sourceCenterX, targetCenterY - sourceCenterY); + } + + private void _ModifiedClick(AppiumWebElement element, byte modifierKey) + { + var targetX = element.Location.X + Math.Min(16, Math.Max(4, element.Size.Width / 8)); + var targetY = element.Location.Y + (element.Size.Height / 2); + _ClickScreenPoint(targetX, targetY, modifierKey, "modified click"); + } + + private static INPUT _CreateMouseInput(uint flags) + { + return new INPUT + { + type = InputMouse, + U = new INPUTUNION + { + mi = new MOUSEINPUT + { + dwFlags = flags, + }, + }, + }; + } + + private static AutomationElement _FindNativeTabElement(TopLevelWindow window, string name) + { + var windowElement = AutomationElement.FromHandle(window.Handle); + Verify.IsNotNull(windowElement, $"Find native UIA root for top-level window '{window.Title}'."); + + var conditions = new AndCondition( + new PropertyCondition(AutomationElement.NameProperty, name), + new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem)); + var matches = windowElement.FindAll(TreeScope.Descendants, conditions); + Verify.IsTrue(matches.Count > 0, $"Find native tab item '{name}' in window '{window.Title}'."); + + return matches + .OfType() + .Where(element => !element.Current.BoundingRectangle.IsEmpty) + .OrderBy(element => element.Current.BoundingRectangle.Left) + .First(); + } + + private void _ModifiedNativeTabClick(TopLevelWindow window, string name, byte modifierKey) + { + var element = _FindNativeTabElement(window, name); + var rect = element.Current.BoundingRectangle; + var targetX = (int)rect.Left + Math.Min(16, Math.Max(4, (int)rect.Width / 8)); + var targetY = (int)(rect.Top + (rect.Height / 2)); + Log.Comment($"Native UIA tab '{name}' for modified click: Class='{element.Current.ClassName}', ControlType='{element.Current.ControlType.ProgrammaticName}', Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); + _ClickScreenPoint(targetX, targetY, modifierKey, $"native tab '{name}' modified click", window.Handle); + } + + private void _NativeTabClick(TopLevelWindow window, string name) + { + var element = _FindNativeTabElement(window, name); + var rect = element.Current.BoundingRectangle; + var targetX = (int)rect.Left + Math.Min(16, Math.Max(4, (int)rect.Width / 8)); + var targetY = (int)(rect.Top + (rect.Height / 2)); + Log.Comment($"Native UIA tab '{name}' for click: Class='{element.Current.ClassName}', ControlType='{element.Current.ControlType.ProgrammaticName}', Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); + _ClickScreenPoint(targetX, targetY, null, $"native tab '{name}' click", window.Handle); + } + + private void _ClickScreenPoint(int targetX, int targetY, byte? modifierKey, string description, IntPtr targetWindow = default) + { + if (IsRunningAsAdmin()) + { + _ClickScreenPointLimited(targetX, targetY, modifierKey, description, targetWindow); + return; + } + + NativeMethods.Win32BoolHelper(SetCursorPos(targetX, targetY), $"Move cursor to {description} at ({targetX}, {targetY})."); + Thread.Sleep(100); + + var inputs = new List(); + if (modifierKey.HasValue) + { + inputs.Add(_CreateKeyboardInput(modifierKey.Value, false)); + } + + inputs.Add(_CreateMouseInput(MouseEventLeftDown)); + inputs.Add(_CreateMouseInput(MouseEventLeftUp)); + + if (modifierKey.HasValue) + { + inputs.Add(_CreateKeyboardInput(modifierKey.Value, true)); + } + + var sent = SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf()); + Verify.AreEqual((uint)inputs.Count, sent, $"Send {description} input."); + Thread.Sleep(200); + } + + private void _ClickScreenPointLimited(int targetX, int targetY, byte? modifierKey, string description, IntPtr targetWindow) + { + var taskName = $"WindowsTerminal-UIA-Input-{Process.GetCurrentProcess().Id}-{Guid.NewGuid():N}"; + var scriptPath = Path.Combine(Path.GetTempPath(), $"{taskName}.ps1"); + var launcherPath = Path.Combine(Path.GetTempPath(), $"{taskName}.cmd"); + var resultPath = Path.Combine(Path.GetTempPath(), $"{taskName}.result"); + var modifierValue = modifierKey.HasValue ? modifierKey.Value.ToString(CultureInfo.InvariantCulture) : "-1"; + var targetWindowValue = targetWindow == IntPtr.Zero ? "0" : targetWindow.ToInt64().ToString(CultureInfo.InvariantCulture); + var escapedResultPath = resultPath.Replace("'", "''"); + + var scriptContents = string.Join(Environment.NewLine, new[] + { + "$ErrorActionPreference = 'Stop'", + "try {", + "Add-Type @'", + "using System;", + "using System.Text;", + "using System.Runtime.InteropServices;", + "public static class Native {", + " public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);", + " [StructLayout(LayoutKind.Sequential)] public struct POINT { public int x; public int y; }", + " [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; }", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetCursorPos(int x, int y);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetForegroundWindow(IntPtr hWnd);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern IntPtr ChildWindowFromPointEx(IntPtr hWndParent, POINT pt, uint flags);", + " [DllImport(\"user32.dll\", CharSet=CharSet.Unicode, SetLastError=true)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);", + " [DllImport(\"user32.dll\", SetLastError=true)] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);", + " public static string GetClassNameString(IntPtr hWnd) { var sb = new StringBuilder(256); return GetClassName(hWnd, sb, sb.Capacity) > 0 ? sb.ToString() : string.Empty; }", + " private static bool Contains(RECT rect, int x, int y) { return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; }", + " public static IntPtr ResolveInputWindow(IntPtr topWindow, int screenX, int screenY) {", + " IntPtr inputSite = IntPtr.Zero;", + " IntPtr bridge = IntPtr.Zero;", + " EnumChildWindows(topWindow, (child, lParam) => {", + " RECT rect;", + " if (GetWindowRect(child, out rect) && Contains(rect, screenX, screenY)) {", + " var className = GetClassNameString(child);", + " if (string.Equals(className, \"Windows.UI.Input.InputSite.WindowClass\", StringComparison.Ordinal)) { inputSite = child; return false; }", + " if (bridge == IntPtr.Zero && string.Equals(className, \"Windows.UI.Composition.DesktopWindowContentBridge\", StringComparison.Ordinal)) { bridge = child; }", + " }", + " return true;", + " }, IntPtr.Zero);", + " if (inputSite != IntPtr.Zero) { return inputSite; }", + " if (bridge != IntPtr.Zero) { return bridge; }", + " var point = new POINT { x = screenX, y = screenY };", + " if (ScreenToClient(topWindow, ref point)) {", + " var child = ChildWindowFromPointEx(topWindow, point, 0);", + " if (child != IntPtr.Zero && child != topWindow) { return child; }", + " }", + " return topWindow;", + " }", + "}", + "'@", + $"$targetWindow = [IntPtr]::new({targetWindowValue})", + "$foreground = $true", + "if ($targetWindow -ne [IntPtr]::Zero) {", + " $foreground = [Native]::SetForegroundWindow($targetWindow)", + " Start-Sleep -Milliseconds 200", + "}", + $"$ok = [Native]::SetCursorPos({targetX}, {targetY})", + "Start-Sleep -Milliseconds 100", + $"$modifier = {modifierValue}", + "$keyDownSent = 0", + "if ($modifier -ge 0) {", + " [Native]::keybd_event([byte]$modifier, 0, 0, [UIntPtr]::Zero)", + " $keyDownSent = 1", + " Start-Sleep -Milliseconds 100", + "}", + "$client = $true", + "$messageWindow = $targetWindow", + "$messageClass = ''", + "if ($targetWindow -ne [IntPtr]::Zero) {", + " $messageWindow = [Native]::ResolveInputWindow($targetWindow, " + targetX.ToString(CultureInfo.InvariantCulture) + ", " + targetY.ToString(CultureInfo.InvariantCulture) + ")", + " $messageClass = [Native]::GetClassNameString($messageWindow)", + " $point = New-Object Native+POINT", + $" $point.x = {targetX}", + $" $point.y = {targetY}", + " $client = [Native]::ScreenToClient($messageWindow, [ref]$point)", + " $mkModifier = if ($modifier -eq 16) { 4 } elseif ($modifier -eq 17) { 8 } else { 0 }", + " $lParam = [IntPtr](($point.x -band 0xFFFF) -bor (($point.y -band 0xFFFF) -shl 16))", + " [void][Native]::SendMessage($messageWindow, 0x0200, [IntPtr]$mkModifier, $lParam)", + " [void][Native]::SendMessage($messageWindow, 0x0201, [IntPtr](1 -bor $mkModifier), $lParam)", + " Start-Sleep -Milliseconds 50", + " [void][Native]::SendMessage($messageWindow, 0x0202, [IntPtr]$mkModifier, $lParam)", + "} else {", + $" [Native]::mouse_event({MouseEventLeftDown}, 0, 0, 0, [UIntPtr]::Zero)", + " Start-Sleep -Milliseconds 50", + $" [Native]::mouse_event({MouseEventLeftUp}, 0, 0, 0, [UIntPtr]::Zero)", + "}", + "$mouseSent = 2", + "Start-Sleep -Milliseconds 100", + "$keyUpSent = 0", + "if ($modifier -ge 0) {", + $" [Native]::keybd_event([byte]$modifier, 0, {KeyEventKeyUp}, [UIntPtr]::Zero)", + " $keyUpSent = 1", + "}", + $"Set-Content -Path '{escapedResultPath}' -Value (\"ok={{0}};foreground={{1}};client={{2}};window=0x{{3:x}};class={{4}};keydown={{5}};mouse={{6}};keyup={{7}}\" -f $ok, $foreground, $client, $messageWindow.ToInt64(), $messageClass, $keyDownSent, $mouseSent, $keyUpSent)", + "} catch {", + $" Set-Content -Path '{escapedResultPath}' -Value (\"error={{0}}\" -f $_.Exception.Message)", + " exit 1", + "}", + }); + + var launcherContents = string.Join(Environment.NewLine, new[] + { + "@echo off", + $"powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{scriptPath}\"", + }); + + File.WriteAllText(scriptPath, scriptContents); + File.WriteAllText(launcherPath, launcherContents); + _scheduledTaskNames.Add(taskName); + _temporaryLaunchArtifacts.Add(scriptPath); + _temporaryLaunchArtifacts.Add(launcherPath); + _temporaryLaunchArtifacts.Add(resultPath); + + _RunProcessAndVerify("schtasks.exe", + $"/Create /TN \"{taskName}\" /SC ONCE /ST 00:00 /RL LIMITED /IT /TR \"\\\"{launcherPath}\\\"\" /F", + $"Create limited input task '{taskName}'."); + _RunProcessAndVerify("schtasks.exe", + $"/Run /TN \"{taskName}\"", + $"Start limited input task '{taskName}'."); + + var remaining = 10000; + while (!File.Exists(resultPath) && remaining > 0) + { + Thread.Sleep(100); + remaining -= 100; + } + + Verify.IsTrue(File.Exists(resultPath), $"Wait for limited input result for {description}."); + var result = File.ReadAllText(resultPath).Trim(); + Log.Comment($"{description} limited helper result: {result}"); + Verify.IsTrue(!result.StartsWith("error=", StringComparison.OrdinalIgnoreCase), $"Limited helper for {description} should succeed."); + Verify.IsTrue(result.IndexOf("mouse=2", StringComparison.OrdinalIgnoreCase) >= 0, $"Limited helper should send mouse inputs for {description}."); + if (modifierKey.HasValue) + { + Verify.IsTrue(result.IndexOf("keydown=1", StringComparison.OrdinalIgnoreCase) >= 0 && + result.IndexOf("keyup=1", StringComparison.OrdinalIgnoreCase) >= 0, + $"Limited helper should send modifier inputs for {description}."); + } + Thread.Sleep(200); + } + + private static INPUT _CreateKeyboardInput(byte virtualKey, bool keyUp) + { + return new INPUT + { + type = InputKeyboard, + U = new INPUTUNION + { + ki = new KEYBDINPUT + { + wVk = virtualKey, + dwFlags = keyUp ? KeyEventKeyUp : 0, + }, + }, + }; + } + + private static (int X, int Y) _GetScreenCenter(AppiumWebElement element) + { + return + ( + element.Location.X + (element.Size.Width / 2), + element.Location.Y + (element.Size.Height / 2) + ); + } + + private static IntPtr _TryGetWindowHandle(AppiumWebElement element) + { + var handle = _TryGetAttribute(element, "NativeWindowHandle"); + if (string.IsNullOrEmpty(handle)) + { + return IntPtr.Zero; + } + + if (long.TryParse(handle, NumberStyles.Integer, CultureInfo.InvariantCulture, out var decimalHandle)) + { + return new IntPtr(decimalHandle); + } + + handle = handle.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? handle.Substring(2) : handle; + if (long.TryParse(handle, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexHandle)) + { + return new IntPtr(hexHandle); + } + + return IntPtr.Zero; + } + + private void _MoveCursorSmoothly(AppiumWebElement source, int startOffsetX, int startOffsetY, int endOffsetX, int endOffsetY, int steps = 24, int delayMs = 15) + { + for (var step = 1; step <= steps; step++) + { + var nextX = startOffsetX + ((endOffsetX - startOffsetX) * step / steps); + var nextY = startOffsetY + ((endOffsetY - startOffsetY) * step / steps); + _MoveMouseToElementOffset(source, nextX, nextY); + Thread.Sleep(delayMs); + } + } + + private void _MoveMouseToElementOffset(AppiumWebElement element, int offsetX, int offsetY) + { + Session.Mouse.MouseMove(element.Coordinates, element.Size.Width / 2 + offsetX, element.Size.Height / 2 + offsetY); + } + protected virtual void Dispose(bool disposing) { if (!this.isDisposed) @@ -106,60 +1014,176 @@ protected virtual void Dispose(bool disposing) } } - private void CreateProcess(string path, string shellToLaunch) + private void CreateProcess(string path, string shellToLaunch, string launchArgs) { - string WindowTitleToFind = "WindowsTerminal.UIA.Tests"; - - job = WinBase.CreateJobObject(IntPtr.Zero, IntPtr.Zero); - NativeMethods.Win32NullHelper(job, "Creating job object to hold binaries under test."); - Log.Comment("Attempting to launch command-line application at '{0}'", path); string binaryToRunPath = path; - string args = $"new-tab --title \"{WindowTitleToFind}\" --suppressApplicationTitle \"{shellToLaunch}\""; + string args = launchArgs ?? $"new-tab --title \"{_windowTitleToFind}\" --suppressApplicationTitle \"{shellToLaunch}\""; + if (IsRunningAsAdmin()) + { + Log.Comment("UIA runner is elevated; launching Terminal at medium integrity through a limited scheduled task."); + _LaunchLimitedProcess(binaryToRunPath, args); + } + else + { + job = WinBase.CreateJobObject(IntPtr.Zero, IntPtr.Zero); + NativeMethods.Win32NullHelper(job, "Creating job object to hold binaries under test."); - string launchArgs = $"{binaryToRunPath} {args}"; + string processCommandLine = $"{binaryToRunPath} {args}"; - WinBase.STARTUPINFO si = new WinBase.STARTUPINFO(); - si.cb = Marshal.SizeOf(si); + WinBase.STARTUPINFO si = new WinBase.STARTUPINFO(); + si.cb = Marshal.SizeOf(si); - WinBase.PROCESS_INFORMATION pi = new WinBase.PROCESS_INFORMATION(); + WinBase.PROCESS_INFORMATION pi = new WinBase.PROCESS_INFORMATION(); - NativeMethods.Win32BoolHelper(WinBase.CreateProcess(null, - launchArgs, - IntPtr.Zero, - IntPtr.Zero, - false, - WinBase.CP_CreationFlags.CREATE_SUSPENDED, - IntPtr.Zero, - null, - ref si, - out pi), - "Attempting to create child host window process."); + NativeMethods.Win32BoolHelper(WinBase.CreateProcess(null, + processCommandLine, + IntPtr.Zero, + IntPtr.Zero, + false, + WinBase.CP_CreationFlags.CREATE_SUSPENDED, + IntPtr.Zero, + null, + ref si, + out pi), + "Attempting to create child host window process."); - Log.Comment($"Host window PID: {pi.dwProcessId}"); + Log.Comment($"Host window PID: {pi.dwProcessId}"); - NativeMethods.Win32BoolHelper(WinBase.AssignProcessToJobObject(job, pi.hProcess), "Assigning new host window (suspended) to job object."); - NativeMethods.Win32BoolHelper(-1 != WinBase.ResumeThread(pi.hThread), "Resume host window process now that it is attached and its launch of the child application will be caught in the job object."); + NativeMethods.Win32BoolHelper(WinBase.AssignProcessToJobObject(job, pi.hProcess), "Assigning new host window (suspended) to job object."); + NativeMethods.Win32BoolHelper(-1 != WinBase.ResumeThread(pi.hThread), "Resume host window process now that it is attached and its launch of the child application will be caught in the job object."); + _trackedProcessIds.Add(pi.dwProcessId); + } Globals.WaitForTimeout(); DesiredCapabilities appCapabilities = new DesiredCapabilities(); appCapabilities.SetCapability("app", @"Root"); - Session = new IOSDriver(new Uri(AppDriverUrl), appCapabilities); + _desktopSession = new IOSDriver(new Uri(AppDriverUrl), appCapabilities); + Session = _desktopSession; - Verify.IsNotNull(Session); + Verify.IsNotNull(_desktopSession); Actions = new Actions(Session); Verify.IsNotNull(Session); - Globals.WaitForLongTimeout(); + var remaining = 30000; + while (UIRoot == null && remaining >= 0) + { + var initialWindow = FindTopLevelWindowByName(_windowTitleToFind); + if (initialWindow != null) + { + ActivateWindow(initialWindow); + User32.GetWindowThreadProcessId(initialWindow.Handle, out var windowPid); + if (windowPid != 0) + { + _trackedProcessIds.Add(unchecked((int)windowPid)); + } - UIRoot = Session.FindElementByName(WindowTitleToFind); + UIRoot = initialWindow.Root; + } + + if (UIRoot == null) + { + Thread.Sleep(250); + remaining -= 250; + } + } + + if (UIRoot == null) + { + var matchingByName = Session.FindElementsByName(_windowTitleToFind) + .OfType() + .Where(_IsValidElement) + .Take(30) + .ToList(); + Log.Comment($"Elements visible to WinAppDriver with Name='{_windowTitleToFind}': {matchingByName.Count}"); + foreach (var element in matchingByName) + { + Log.Comment($"Element candidate: Name='{element.GetAttribute("Name")}', ClassName='{element.GetAttribute("ClassName")}', ProcessId='{element.GetAttribute("ProcessId")}'"); + } + + UIRoot = Session.FindElementByXPath("//*"); + Log.Comment("Falling back to desktop root element for UI automation session."); + } + + Verify.IsNotNull(UIRoot, $"Failed to find a top-level window for '{_windowTitleToFind}'."); // Set the timeout to 15 seconds after we found the initial window. Session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15); } + private void _LaunchLimitedProcess(string path, string args) + { + var taskName = $"WindowsTerminal-UIA-{Process.GetCurrentProcess().Id}-{Guid.NewGuid():N}"; + var launcherPath = Path.Combine(Path.GetTempPath(), $"{taskName}.cmd"); + _dragEventLogPath = Path.Combine(Path.GetTempPath(), $"{taskName}.drag.log"); + var launcherContents = + "@echo off" + Environment.NewLine + + $"echo launcher-started>\"{_dragEventLogPath}\"" + Environment.NewLine + + $"set \"WT_UIA_DRAG_LOG={_dragEventLogPath}\"" + Environment.NewLine + + "set \"WT_UIA_ENABLE_TEST_HOOKS=1\"" + Environment.NewLine + + $"cd /d \"{Path.GetDirectoryName(path)}\"" + Environment.NewLine + + $"\"{path}\" {args}" + Environment.NewLine; + + File.WriteAllText(launcherPath, launcherContents); + _scheduledTaskNames.Add(taskName); + _temporaryLaunchArtifacts.Add(launcherPath); + Log.Comment($"Drag event log will be written to '{_dragEventLogPath}'."); + + _RunProcessAndVerify("schtasks.exe", + $"/Create /TN \"{taskName}\" /SC ONCE /ST 00:00 /RL LIMITED /IT /TR \"\\\"{launcherPath}\\\"\" /F", + $"Create limited scheduled task '{taskName}'."); + _RunProcessAndVerify("schtasks.exe", + $"/Run /TN \"{taskName}\"", + $"Start limited scheduled task '{taskName}'."); + } + + private static void _RunProcessAndVerify(string fileName, string arguments, string description) + { + using (var process = new Process()) + { + process.StartInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + Verify.IsTrue(process.Start(), description); + var standardOutput = process.StandardOutput.ReadToEnd(); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + if (!string.IsNullOrWhiteSpace(standardOutput)) + { + Log.Comment(standardOutput.Trim()); + } + + if (!string.IsNullOrWhiteSpace(standardError)) + { + Log.Comment(standardError.Trim()); + } + + Verify.AreEqual(0, process.ExitCode, description); + } + } + + private static void _TryRunProcess(string fileName, string arguments, string description) + { + try + { + _RunProcessAndVerify(fileName, arguments, description); + } + catch (Exception ex) + { + Log.Comment($"{description} failed during cleanup: {ex.Message}"); + } + } + private bool IsRunningAsAdmin() { return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); @@ -173,12 +1197,74 @@ private void ExitProcess() WinCon.FreeConsole(); this.UIRoot = null; + this.Session = null; + + foreach (var session in _windowSessions.Values) + { + try + { + session.Quit(); + } + catch + { + } + } + _windowSessions.Clear(); + + foreach (var taskName in _scheduledTaskNames) + { + _TryRunProcess("schtasks.exe", + $"/Delete /TN \"{taskName}\" /F", + $"Delete limited scheduled task '{taskName}'."); + } + + foreach (var artifact in _temporaryLaunchArtifacts) + { + try + { + File.Delete(artifact); + } + catch (Exception ex) + { + Log.Comment($"Delete temporary launch artifact '{artifact}' failed during cleanup: {ex.Message}"); + } + } + + _scheduledTaskNames.Clear(); + _temporaryLaunchArtifacts.Clear(); + + if (_desktopSession != null) + { + try + { + _desktopSession.Quit(); + } + catch + { + } + _desktopSession = null; + } if (this.job != IntPtr.Zero) { WinBase.TerminateJobObject(this.job, 0); this.job = IntPtr.Zero; } + else + { + foreach (var pid in _trackedProcessIds.ToArray()) + { + try + { + Process.GetProcessById(pid).Kill(); + } + catch + { + } + } + + _trackedProcessIds.Clear(); + } } } } diff --git a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs index 8c643391fdb..9ba71de7c3e 100644 --- a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs +++ b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs @@ -8,6 +8,7 @@ namespace WindowsTerminal.UIA.Tests { + using WEX.TestExecution; using OpenQA.Selenium; using WEX.TestExecution.Markup; @@ -190,5 +191,89 @@ public void RunOpenSettingsUI() Globals.WaitForLongTimeout(); } } + + [TestMethod] + [TestProperty("IsPGO", "true")] + public void DragMultipleTabsAcrossWindows() + { + const string sourceATitle = "DragTabA"; + const string sourceBTitle = "DragTabB"; + const string sourceCTitle = "DragTabC"; + const string rootTitle = "WindowsTerminal.UIA.Tests"; + + var launchArgs = + $"new-tab --title \"{sourceATitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{sourceBTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{sourceCTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{rootTitle}\" --suppressApplicationTitle \"powershell.exe\""; + + using (TerminalApp app = new TerminalApp(TestContext, launchArgs: launchArgs, windowTitleToFind: rootTitle)) + { + var windowA = app.WaitForTopLevelWindowByName(rootTitle); + app.ArrangeWindowOnPrimaryMonitor(windowA, 0); + app.ActivateWindow(windowA); + var firstTab = app.FindTabElementByName(windowA, sourceATitle); + app.LogWindowDetails("initialWindowA", windowA); + app.LogElementDetails("firstTab", windowA, firstTab); + + app.DragByOffset(firstTab, windowA.Width + 80, 80); + Globals.WaitForLongTimeout(); + + var windowB = app.TryWaitForTopLevelWindowByName(sourceATitle, windowA, 5000) ?? + app.WaitForTopLevelWindowByName(rootTitle, windowA); + Verify.IsNotNull(windowB, "Expected dragging a tab outside the window to create a new top-level window."); + Verify.IsTrue(windowA.Handle != windowB.Handle, "Expected dragging a tab outside the window to create a distinct top-level window."); + Verify.IsTrue(app.HasElementByName(windowB, sourceATitle), "Expected the detached window to contain the dragged tab."); + + windowA = app.WaitForTopLevelWindowByName(rootTitle, windowB); + app.ArrangeWindowOnPrimaryMonitor(windowA, 0); + app.ArrangeWindowOnPrimaryMonitor(windowB, 1); + app.ActivateWindow(windowA); + + var secondTab = app.FindTabElementByName(windowA, sourceBTitle); + var thirdTab = app.FindTabElementByName(windowA, sourceCTitle); + + app.LogWindowDetails("windowA", windowA); + app.LogWindowDetails("windowB", windowB); + app.LogElementDetails("secondTab", windowA, secondTab); + app.LogElementDetails("thirdTab", windowA, thirdTab); + app.LogElementAncestors("secondTabAncestors", secondTab); + app.LogElementAncestors("thirdTabAncestors", thirdTab); + + app.SelectTabRangeForTesting(windowA, 0, 1); + Globals.WaitForTimeout(); + secondTab = app.FindTabElementByName(windowA, sourceBTitle); + + var existingWindowTab = app.FindTabElementByName(windowB, sourceATitle); + app.LogElementDetails("existingWindowTab", windowB, existingWindowTab); + app.LogElementAncestors("existingWindowTabAncestors", existingWindowTab); + app.ActivateWindow(windowA); + var dropOffset = System.Math.Max(24, existingWindowTab.Size.Width / 3); + app.DragToElement(secondTab, existingWindowTab, dropOffset, 0); + Globals.WaitForLongTimeout(); + + Verify.IsNull( + app.TryWaitForTopLevelWindowByName(sourceBTitle, windowB, 1500), + "Expected dropping DragTabB + DragTabC onto DragTabA to move the tabs into the existing window, not create a new top-level window."); + + Verify.IsTrue(app.HasElementByName(windowB, sourceATitle), "Expected the destination window to keep its original tab."); + Verify.IsTrue(app.HasElementByName(windowB, sourceBTitle), "Expected the destination window to receive the first dragged tab."); + Verify.IsTrue(app.HasElementByName(windowB, sourceCTitle), "Expected the destination window to receive the second dragged tab."); + + var tabAInWindowB = app.FindTabElementByName(windowB, sourceATitle); + var tabBInWindowB = app.FindTabElementByName(windowB, sourceBTitle); + var tabCInWindowB = app.FindTabElementByName(windowB, sourceCTitle); + + tabCInWindowB.Click(); + Globals.WaitForTimeout(); + + Verify.IsTrue(tabAInWindowB.Location.X < tabBInWindowB.Location.X, "Expected dragged tabs to be inserted after the drop target."); + Verify.IsTrue(tabBInWindowB.Location.X < tabCInWindowB.Location.X, "Expected dragged tabs to preserve their source order."); + + windowA = app.WaitForTopLevelWindowByName(rootTitle, windowB); + Verify.IsFalse(app.HasElementByName(windowA, sourceBTitle), "Expected the source window to no longer contain the first moved tab."); + Verify.IsFalse(app.HasElementByName(windowA, sourceCTitle), "Expected the source window to no longer contain the second moved tab."); + } + } } } From 4f27d07e4c4c7271ad7db6edfd76330043f1e44a Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Tue, 24 Mar 2026 10:13:25 +0800 Subject: [PATCH 5/6] Fix PR spelling issues Rename the UIA smoke test variables that triggered check-spelling, allow the Win32 API tokens used by the harness, and drop stale expect entries that are no longer present in the tree. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/actions/spelling/allow/apis.txt | 6 +++ .github/actions/spelling/expect/expect.txt | 2 - .../WindowsTerminal_UIATests/SmokeTests.cs | 44 +++++++++---------- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/.github/actions/spelling/allow/apis.txt b/.github/actions/spelling/allow/apis.txt index 3554798540f..7f1a2205b74 100644 --- a/.github/actions/spelling/allow/apis.txt +++ b/.github/actions/spelling/allow/apis.txt @@ -21,7 +21,9 @@ commandlinetoargv COPYFROMRESOURCE cstdint CXICON +CXSCREEN CYICON +CYSCREEN dataobject debugbreak delayimp @@ -65,11 +67,13 @@ IAppearance ICONINFO IDirect IInheritable +INPUTUNION imm iosfwd isa isspace istream +keybd KEYSELECT LCID LINEBREAK @@ -86,6 +90,7 @@ MENUDATA MENUINFO MENUITEMINFOW MINIMIZEBOX +MOUSEINPUT MOUSELEAVE mov MULTIPLEUSE @@ -135,6 +140,7 @@ RSHIFT rvrn SACL schandle +schtasks SEH semver serializer diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 6c226753b98..2a4a4a85961 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -963,7 +963,6 @@ lsconfig lstatus lstrcmp LTEXT -lto ltsc LUID luma @@ -1718,7 +1717,6 @@ titlebars TITLEISLINKNAME TLDP TLEN -Tlgg TMAE TMPF tmultiple diff --git a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs index 9ba71de7c3e..3ba11b30a24 100644 --- a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs +++ b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs @@ -196,15 +196,15 @@ public void RunOpenSettingsUI() [TestProperty("IsPGO", "true")] public void DragMultipleTabsAcrossWindows() { - const string sourceATitle = "DragTabA"; - const string sourceBTitle = "DragTabB"; - const string sourceCTitle = "DragTabC"; + const string detachedTabTitle = "DragTabA"; + const string firstAttachedTabTitle = "DragTabB"; + const string secondAttachedTabTitle = "DragTabC"; const string rootTitle = "WindowsTerminal.UIA.Tests"; var launchArgs = - $"new-tab --title \"{sourceATitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + - $"new-tab --title \"{sourceBTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + - $"new-tab --title \"{sourceCTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{detachedTabTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{firstAttachedTabTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + + $"new-tab --title \"{secondAttachedTabTitle}\" --suppressApplicationTitle \"powershell.exe\" ; " + $"new-tab --title \"{rootTitle}\" --suppressApplicationTitle \"powershell.exe\""; using (TerminalApp app = new TerminalApp(TestContext, launchArgs: launchArgs, windowTitleToFind: rootTitle)) @@ -212,26 +212,26 @@ public void DragMultipleTabsAcrossWindows() var windowA = app.WaitForTopLevelWindowByName(rootTitle); app.ArrangeWindowOnPrimaryMonitor(windowA, 0); app.ActivateWindow(windowA); - var firstTab = app.FindTabElementByName(windowA, sourceATitle); + var firstTab = app.FindTabElementByName(windowA, detachedTabTitle); app.LogWindowDetails("initialWindowA", windowA); app.LogElementDetails("firstTab", windowA, firstTab); app.DragByOffset(firstTab, windowA.Width + 80, 80); Globals.WaitForLongTimeout(); - var windowB = app.TryWaitForTopLevelWindowByName(sourceATitle, windowA, 5000) ?? + var windowB = app.TryWaitForTopLevelWindowByName(detachedTabTitle, windowA, 5000) ?? app.WaitForTopLevelWindowByName(rootTitle, windowA); Verify.IsNotNull(windowB, "Expected dragging a tab outside the window to create a new top-level window."); Verify.IsTrue(windowA.Handle != windowB.Handle, "Expected dragging a tab outside the window to create a distinct top-level window."); - Verify.IsTrue(app.HasElementByName(windowB, sourceATitle), "Expected the detached window to contain the dragged tab."); + Verify.IsTrue(app.HasElementByName(windowB, detachedTabTitle), "Expected the detached window to contain the dragged tab."); windowA = app.WaitForTopLevelWindowByName(rootTitle, windowB); app.ArrangeWindowOnPrimaryMonitor(windowA, 0); app.ArrangeWindowOnPrimaryMonitor(windowB, 1); app.ActivateWindow(windowA); - var secondTab = app.FindTabElementByName(windowA, sourceBTitle); - var thirdTab = app.FindTabElementByName(windowA, sourceCTitle); + var secondTab = app.FindTabElementByName(windowA, firstAttachedTabTitle); + var thirdTab = app.FindTabElementByName(windowA, secondAttachedTabTitle); app.LogWindowDetails("windowA", windowA); app.LogWindowDetails("windowB", windowB); @@ -242,9 +242,9 @@ public void DragMultipleTabsAcrossWindows() app.SelectTabRangeForTesting(windowA, 0, 1); Globals.WaitForTimeout(); - secondTab = app.FindTabElementByName(windowA, sourceBTitle); + secondTab = app.FindTabElementByName(windowA, firstAttachedTabTitle); - var existingWindowTab = app.FindTabElementByName(windowB, sourceATitle); + var existingWindowTab = app.FindTabElementByName(windowB, detachedTabTitle); app.LogElementDetails("existingWindowTab", windowB, existingWindowTab); app.LogElementAncestors("existingWindowTabAncestors", existingWindowTab); app.ActivateWindow(windowA); @@ -253,16 +253,16 @@ public void DragMultipleTabsAcrossWindows() Globals.WaitForLongTimeout(); Verify.IsNull( - app.TryWaitForTopLevelWindowByName(sourceBTitle, windowB, 1500), + app.TryWaitForTopLevelWindowByName(firstAttachedTabTitle, windowB, 1500), "Expected dropping DragTabB + DragTabC onto DragTabA to move the tabs into the existing window, not create a new top-level window."); - Verify.IsTrue(app.HasElementByName(windowB, sourceATitle), "Expected the destination window to keep its original tab."); - Verify.IsTrue(app.HasElementByName(windowB, sourceBTitle), "Expected the destination window to receive the first dragged tab."); - Verify.IsTrue(app.HasElementByName(windowB, sourceCTitle), "Expected the destination window to receive the second dragged tab."); + Verify.IsTrue(app.HasElementByName(windowB, detachedTabTitle), "Expected the destination window to keep its original tab."); + Verify.IsTrue(app.HasElementByName(windowB, firstAttachedTabTitle), "Expected the destination window to receive the first dragged tab."); + Verify.IsTrue(app.HasElementByName(windowB, secondAttachedTabTitle), "Expected the destination window to receive the second dragged tab."); - var tabAInWindowB = app.FindTabElementByName(windowB, sourceATitle); - var tabBInWindowB = app.FindTabElementByName(windowB, sourceBTitle); - var tabCInWindowB = app.FindTabElementByName(windowB, sourceCTitle); + var tabAInWindowB = app.FindTabElementByName(windowB, detachedTabTitle); + var tabBInWindowB = app.FindTabElementByName(windowB, firstAttachedTabTitle); + var tabCInWindowB = app.FindTabElementByName(windowB, secondAttachedTabTitle); tabCInWindowB.Click(); Globals.WaitForTimeout(); @@ -271,8 +271,8 @@ public void DragMultipleTabsAcrossWindows() Verify.IsTrue(tabBInWindowB.Location.X < tabCInWindowB.Location.X, "Expected dragged tabs to preserve their source order."); windowA = app.WaitForTopLevelWindowByName(rootTitle, windowB); - Verify.IsFalse(app.HasElementByName(windowA, sourceBTitle), "Expected the source window to no longer contain the first moved tab."); - Verify.IsFalse(app.HasElementByName(windowA, sourceCTitle), "Expected the source window to no longer contain the second moved tab."); + Verify.IsFalse(app.HasElementByName(windowA, firstAttachedTabTitle), "Expected the source window to no longer contain the first moved tab."); + Verify.IsFalse(app.HasElementByName(windowA, secondAttachedTabTitle), "Expected the source window to no longer contain the second moved tab."); } } } From 6bd43e0a06bcd5e58e3888341564bc31e2fd90e9 Mon Sep 17 00:00:00 2001 From: DevDengChao <2325690622@qq.com> Date: Wed, 25 Mar 2026 01:06:21 +0800 Subject: [PATCH 6/6] Trim UIA harness for review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cascadia/TerminalApp/TabManagement.cpp | 99 +- src/cascadia/TerminalApp/TerminalPage.cpp | 11700 ++++++++-------- src/cascadia/TerminalApp/TerminalPage.h | 1208 +- src/cascadia/WindowsTerminal/IslandWindow.h | 336 +- .../Elements/TerminalApp.cs | 968 +- .../WindowsTerminal_UIATests/SmokeTests.cs | 12 - 6 files changed, 6716 insertions(+), 7607 deletions(-) diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index 71f5d7223f5..98129a8dc67 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -48,71 +48,6 @@ namespace winrt using IInspectable = Windows::Foundation::IInspectable; } -namespace -{ - void _appendUiaDragLog(const wchar_t* message) noexcept - { - std::wstring buffer(MAX_PATH, L'\0'); - const auto length = GetEnvironmentVariableW(L"WT_UIA_DRAG_LOG", buffer.data(), gsl::narrow_cast(buffer.size())); - if (length == 0 || length >= buffer.size()) - { - return; - } - - buffer.resize(length); - - const wil::unique_hfile file{ CreateFileW(buffer.c_str(), - FILE_APPEND_DATA, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - nullptr) }; - if (!file) - { - return; - } - - const auto line = winrt::to_string(std::wstring{ message } + L"\r\n"); - DWORD bytesWritten = 0; - WriteFile(file.get(), line.data(), gsl::narrow_cast(line.size()), &bytesWritten, nullptr); - } - - std::wstring _tabTitleForUiaLog(const winrt::TerminalApp::Tab& tab) - { - if (!tab) - { - return L""; - } - - const auto title = tab.Title(); - return title.empty() ? L"" : std::wstring{ title }; - } - - void _appendUiaSelectionLog(const std::vector& tabs, const winrt::TerminalApp::Tab& anchor) noexcept - { - std::wstring message{ L"_SetSelectedTabs: count=" }; - message += std::to_wstring(tabs.size()); - message += L", anchor="; - message += _tabTitleForUiaLog(anchor); - message += L", tabs=["; - - bool first = true; - for (const auto& tab : tabs) - { - if (!first) - { - message += L", "; - } - first = false; - message += _tabTitleForUiaLog(tab); - } - - message += L"]"; - _appendUiaDragLog(message.c_str()); - } -} - namespace winrt::TerminalApp::implementation { // Method Description: @@ -1216,7 +1151,6 @@ namespace winrt::TerminalApp::implementation _selectionAnchor = _selectedTabs.front(); } - _appendUiaSelectionLog(_selectedTabs, _selectionAnchor); _ApplyMultiSelectionVisuals(); } @@ -1285,11 +1219,6 @@ namespace winrt::TerminalApp::implementation const auto tabCount = _tabs.Size(); if (tabCount == 0 || startIndex >= tabCount || endIndex >= tabCount) { - const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: invalid range {}-{} count={}"), - startIndex, - endIndex, - tabCount); - _appendUiaDragLog(message.c_str()); return false; } @@ -1297,10 +1226,6 @@ namespace winrt::TerminalApp::implementation const auto endTab = _tabs.GetAt(endIndex); if (!_TabSupportsMultiSelection(startTab) || !_TabSupportsMultiSelection(endTab)) { - const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: unsupported tabs start={} end={}"), - _tabTitleForUiaLog(startTab), - _tabTitleForUiaLog(endTab)); - _appendUiaDragLog(message.c_str()); return false; } @@ -1308,17 +1233,9 @@ namespace winrt::TerminalApp::implementation auto range = _GetTabRange(startTab, endTab); if (range.empty()) { - const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: empty range start={} end={}"), - _tabTitleForUiaLog(startTab), - _tabTitleForUiaLog(endTab)); - _appendUiaDragLog(message.c_str()); return false; } - const auto message = til::hstring_format(FMT_COMPILE(L"SelectTabRangeForTesting: start={} end={}"), - _tabTitleForUiaLog(startTab), - _tabTitleForUiaLog(endTab)); - _appendUiaDragLog(message.c_str()); _SetSelectedTabs(std::move(range), startTab); return true; } @@ -1327,12 +1244,6 @@ namespace winrt::TerminalApp::implementation { const bool ctrlPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_CONTROL)), 0x8000); const bool shiftPressed = WI_IsFlagSet(static_cast(GetKeyState(VK_SHIFT)), 0x8000); - const auto clickedTitle = _tabTitleForUiaLog(tab); - const auto pointerMessage = til::hstring_format(FMT_COMPILE(L"_UpdateSelectionFromPointer: tab={}, ctrl={}, shift={}"), - clickedTitle, - ctrlPressed ? 1 : 0, - shiftPressed ? 1 : 0); - _appendUiaDragLog(pointerMessage.c_str()); if (!_TabSupportsMultiSelection(tab)) { @@ -1504,17 +1415,14 @@ namespace winrt::TerminalApp::implementation void TerminalPage::_TabDragStarted(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) { - _appendUiaDragLog(L"_TabDragStarted"); _rearranging = true; _rearrangeFrom = std::nullopt; _rearrangeTo = std::nullopt; } void TerminalPage::_TabDragCompleted(const IInspectable& /*sender*/, - const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs) + const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& /*eventArgs*/) { - const auto dropResultLog = std::wstring{ L"_TabDragCompleted: dropResult=" } + std::to_wstring(static_cast(eventArgs.DropResult())); - _appendUiaDragLog(dropResultLog.c_str()); auto& from{ _rearrangeFrom }; auto& to{ _rearrangeTo }; @@ -1534,14 +1442,9 @@ namespace winrt::TerminalApp::implementation _rearranging = false; if (from.has_value() || to.has_value()) { - _appendUiaDragLog(L"_TabDragCompleted: clearing stashed drag data after in-window reorder"); _stashed.draggedTabs.clear(); _stashed.dragAnchor = nullptr; } - else - { - _appendUiaDragLog(L"_TabDragCompleted: preserving stashed drag data for post-complete drop handlers"); - } if (to.has_value() && *to < gsl::narrow_cast(TabRow().TabView().TabItems().Size())) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 42739842ea9..ac121a6a8af 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1,5884 +1,5816 @@ - -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "TerminalPage.h" - -#include -#include -#include -#include -#include - -#include "../../types/inc/ColorFix.hpp" -#include "../../types/inc/utils.hpp" -#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" -#include "App.h" -#include "DebugTapConnection.h" -#include "MarkdownPaneContent.h" -#include "Remoting.h" -#include "ScratchpadContent.h" -#include "SettingsPaneContent.h" -#include "SnippetsPaneContent.h" -#include "TabRowControl.h" -#include "TerminalSettingsCache.h" - -#include "LaunchPositionRequest.g.cpp" -#include "RenameWindowRequestedArgs.g.cpp" -#include "RequestMoveContentArgs.g.cpp" -#include "TerminalPage.g.cpp" - -using namespace winrt; -using namespace winrt::Microsoft::Management::Deployment; -using namespace winrt::Microsoft::Terminal::Control; -using namespace winrt::Microsoft::Terminal::Settings::Model; -using namespace winrt::Microsoft::Terminal::TerminalConnection; -using namespace winrt::Microsoft::Terminal; -using namespace winrt::Windows::ApplicationModel::DataTransfer; -using namespace winrt::Windows::Foundation::Collections; -using namespace winrt::Windows::System; -using namespace winrt::Windows::UI; -using namespace winrt::Windows::UI::Core; -using namespace winrt::Windows::UI::Text; -using namespace winrt::Windows::UI::Xaml::Controls; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Xaml::Media; -using namespace ::TerminalApp; -using namespace ::Microsoft::Console; -using namespace ::Microsoft::Terminal::Core; -using namespace std::chrono_literals; - -#define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action }); - -namespace winrt -{ - namespace MUX = Microsoft::UI::Xaml; - namespace WUX = Windows::UI::Xaml; - using IInspectable = Windows::Foundation::IInspectable; - using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers; -} - -namespace clipboard -{ - static SRWLOCK lock = SRWLOCK_INIT; - - struct ClipboardHandle - { - explicit ClipboardHandle(bool open) : - _open{ open } - { - } - - ~ClipboardHandle() - { - if (_open) - { - ReleaseSRWLockExclusive(&lock); - CloseClipboard(); - } - } - - explicit operator bool() const noexcept - { - return _open; - } - - private: - bool _open = false; - }; - - ClipboardHandle open(HWND hwnd) - { - // Turns out, OpenClipboard/CloseClipboard are not thread-safe whatsoever, - // and on CloseClipboard, the GetClipboardData handle may get freed. - // The problem is that WinUI also uses OpenClipboard (through WinRT which uses OLE), - // and so even with this mutex we can still crash randomly if you copy something via WinUI. - // Makes you wonder how many Windows apps are subtly broken, huh. - AcquireSRWLockExclusive(&lock); - - bool success = false; - - // OpenClipboard may fail to acquire the internal lock --> retry. - for (DWORD sleep = 10;; sleep *= 2) - { - if (OpenClipboard(hwnd)) - { - success = true; - break; - } - // 10 iterations - if (sleep > 10000) - { - break; - } - Sleep(sleep); - } - - if (!success) - { - ReleaseSRWLockExclusive(&lock); - } - - return ClipboardHandle{ success }; - } - - void write(wil::zwstring_view text, std::string_view html, std::string_view rtf) - { - static const auto regular = [](const UINT format, const void* src, const size_t bytes) { - wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) }; - - const auto locked = GlobalLock(handle.get()); - memcpy(locked, src, bytes); - GlobalUnlock(handle.get()); - - THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get())); - handle.release(); - }; - static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) { - const auto id = RegisterClipboardFormatW(format); - if (!id) - { - LOG_LAST_ERROR(); - return; - } - regular(id, src, bytes); - }; - - EmptyClipboard(); - - if (!text.empty()) - { - // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats - // CF_UNICODETEXT: [...] A null character signals the end of the data. - // --> We add +1 to the length. This works because .c_str() is null-terminated. - regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); - } - - if (!html.empty()) - { - registered(L"HTML Format", html.data(), html.size()); - } - - if (!rtf.empty()) - { - registered(L"Rich Text Format", rtf.data(), rtf.size()); - } - } - - winrt::hstring read() - { - // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically. - if (const auto handle = GetClipboardData(CF_UNICODETEXT)) - { - const wil::unique_hglobal_locked lock{ handle }; - const auto str = static_cast(lock.get()); - if (!str) - { - return {}; - } - - const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); - const auto len = wcsnlen(str, maxLen); - return winrt::hstring{ str, gsl::narrow_cast(len) }; - } - - // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others). - if (const auto handle = GetClipboardData(CF_HDROP)) - { - const wil::unique_hglobal_locked lock{ handle }; - const auto drop = static_cast(lock.get()); - if (!drop) - { - return {}; - } - - const auto cap = DragQueryFileW(drop, 0, nullptr, 0); - if (cap == 0) - { - return {}; - } - - auto buffer = winrt::impl::hstring_builder{ cap }; - const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); - if (len == 0) - { - return {}; - } - - return buffer.to_hstring(); - } - - return {}; - } -} // namespace clipboard - -namespace -{ - std::wstring_view _uiaDragLogPath() noexcept - { - static const auto path = []() { - std::wstring buffer(MAX_PATH, L'\0'); - const auto length = GetEnvironmentVariableW(L"WT_UIA_DRAG_LOG", buffer.data(), gsl::narrow_cast(buffer.size())); - if (length == 0 || length >= buffer.size()) - { - return std::wstring{}; - } - - buffer.resize(length); - return buffer; - }(); - - return path; - } - - void _appendUiaDragLog(const std::wstring& message) noexcept - { - const auto path = _uiaDragLogPath(); - if (path.empty()) - { - return; - } - - const wil::unique_hfile file{ CreateFileW(path.data(), - FILE_APPEND_DATA, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - nullptr, - OPEN_ALWAYS, - FILE_ATTRIBUTE_NORMAL, - nullptr) }; - if (!file) - { - return; - } - - SYSTEMTIME timestamp{}; - GetLocalTime(×tamp); - - wchar_t buffer[256]; - const auto written = swprintf_s(buffer, - L"%02u:%02u:%02u.%03u %s\r\n", - timestamp.wHour, - timestamp.wMinute, - timestamp.wSecond, - timestamp.wMilliseconds, - message.c_str()); - if (written <= 0) - { - return; - } - - const auto utf8 = winrt::to_string(std::wstring_view{ buffer, gsl::narrow_cast(written) }); - DWORD bytesWritten = 0; - WriteFile(file.get(), utf8.data(), gsl::narrow_cast(utf8.size()), &bytesWritten, nullptr); - } -} - -namespace winrt::TerminalApp::implementation -{ - TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : - _tabs{ winrt::single_threaded_observable_vector() }, - _mruTabs{ winrt::single_threaded_observable_vector() }, - _manager{ manager }, - _hostingHwnd{}, - _WindowProperties{ std::move(properties) } - { - InitializeComponent(); - _WindowProperties.PropertyChanged({ get_weak(), &TerminalPage::_windowPropertyChanged }); - } - - // Method Description: - // - implements the IInitializeWithWindow interface from shobjidl_core. - // - We're going to use this HWND as the owner for the ConPTY windows, via - // ConptyConnection::ReparentWindow. We need this for applications that - // call GetConsoleWindow, and attempt to open a MessageBox for the - // console. By marking the conpty windows as owned by the Terminal HWND, - // the message box will be owned by the Terminal window as well. - // - see GH#2988 - HRESULT TerminalPage::Initialize(HWND hwnd) - { - if (!_hostingHwnd.has_value()) - { - // GH#13211 - if we haven't yet set the owning hwnd, reparent all the controls now. - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { - if (const auto& term{ pane->GetTerminalControl() }) - { - term.OwningHwnd(reinterpret_cast(hwnd)); - } - }); - } - // We don't need to worry about resetting the owning hwnd for the - // SUI here. GH#13211 only repros for a defterm connection, where - // the tab is spawned before the window is created. It's not - // possible to make a SUI tab like that, before the window is - // created. The SUI could be spawned as a part of a window restore, - // but that would still work fine. The window would be created - // before restoring previous tabs in that scenario. - } - } - - _hostingHwnd = hwnd; - return S_OK; - } - - // INVARIANT: This needs to be called on OUR UI thread! - void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) - { - assert(Dispatcher().HasThreadAccess()); - if (_settings == nullptr) - { - // Create this only on the first time we load the settings. - _terminalSettingsCache = std::make_shared(settings); - } - _settings = settings; - - // Make sure to call SetCommands before _RefreshUIForSettingsReload. - // SetCommands will make sure the KeyChordText of Commands is updated, which needs - // to happen before the Settings UI is reloaded and tries to re-read those values. - if (const auto p = CommandPaletteElement()) - { - p.SetActionMap(_settings.ActionMap()); - } - - if (needRefreshUI) - { - _RefreshUIForSettingsReload(); - } - - // Upon settings update we reload the system settings for scrolling as well. - // TODO: consider reloading this value periodically. - _systemRowsToScroll = _ReadSystemRowsToScroll(); - } - - bool TerminalPage::IsRunningElevated() const noexcept - { - // GH#2455 - Make sure to try/catch calls to Application::Current, - // because that _won't_ be an instance of TerminalApp::App in the - // LocalTests - try - { - return Application::Current().as().Logic().IsRunningElevated(); - } - CATCH_LOG(); - return false; - } - bool TerminalPage::CanDragDrop() const noexcept - { - try - { - return Application::Current().as().Logic().CanDragDrop(); - } - CATCH_LOG(); - return true; - } - - void TerminalPage::Create() - { - // Hookup the key bindings - _HookupKeyBindings(_settings.ActionMap()); - - _tabContent = this->TabContent(); - _tabRow = this->TabRow(); - _tabView = _tabRow.TabView(); - _rearranging = false; - - const auto canDragDrop = CanDragDrop(); - _appendUiaDragLog(std::wstring{ L"Create: canDragDrop=" } + (canDragDrop ? L"true" : L"false")); - - _tabView.CanReorderTabs(canDragDrop); - _tabView.CanDragTabs(canDragDrop); - _tabView.TabDragStarting({ get_weak(), &TerminalPage::_TabDragStarted }); - _tabView.TabDragCompleted({ get_weak(), &TerminalPage::_TabDragCompleted }); - - auto tabRowImpl = winrt::get_self(_tabRow); - _newTabButton = tabRowImpl->NewTabButton(); - - if (_settings.GlobalSettings().ShowTabsInTitlebar()) - { - // Remove the TabView from the page. We'll hang on to it, we need to - // put it in the titlebar. - uint32_t index = 0; - if (this->Root().Children().IndexOf(_tabRow, index)) - { - this->Root().Children().RemoveAt(index); - } - - // Inform the host that our titlebar content has changed. - SetTitleBarContent.raise(*this, _tabRow); - - // GH#13143 Manually set the tab row's background to transparent here. - // - // We're doing it this way because ThemeResources are tricky. We - // default in XAML to using the appropriate ThemeResource background - // color for our TabRow. When tabs in the titlebar are _disabled_, - // this will ensure that the tab row has the correct theme-dependent - // value. When tabs in the titlebar are _enabled_ (the default), - // we'll switch the BG to Transparent, to let the Titlebar Control's - // background be used as the BG for the tab row. - // - // We can't do it the other way around (default to Transparent, only - // switch to a color when disabling tabs in the titlebar), because - // looking up the correct ThemeResource from and App dictionary is a - // capital-H Hard problem. - const auto transparent = Media::SolidColorBrush(); - transparent.Color(Windows::UI::Colors::Transparent()); - _tabRow.Background(transparent); - } - _updateThemeColors(); - - // Initialize the state of the CloseButtonOverlayMode property of - // our TabView, to match the tab.showCloseButton property in the theme. - if (const auto theme = _settings.GlobalSettings().CurrentTheme()) - { - const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always; - - _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; - - switch (visibility) - { - case Settings::Model::TabCloseButtonVisibility::Never: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); - break; - case Settings::Model::TabCloseButtonVisibility::Hover: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); - break; - default: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); - break; - } - } - - // Hookup our event handlers to the ShortcutActionDispatch - _RegisterActionCallbacks(); - - //Event Bindings (Early) - _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuDefaultButtonClicked", - TraceLoggingDescription("Event emitted when the default button from the new tab split button is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); - } - }); - _newTabButton.Drop({ get_weak(), &TerminalPage::_NewTerminalByDrop }); - _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); - _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); - _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); - - _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); - _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); - _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop }); - _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside }); - - _CreateNewTabFlyout(); - - _UpdateTabWidthMode(); - - // Settings AllowDependentAnimations will affect whether animations are - // enabled application-wide, so we don't need to check it each time we - // want to create an animation. - WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); - - // Once the page is actually laid out on the screen, trigger all our - // startup actions. Things like Panes need to know at least how big the - // window will be, so they can subdivide that space. - // - // _OnFirstLayout will remove this handler so it doesn't get called more than once. - _layoutUpdatedRevoker = _tabContent.LayoutUpdated(winrt::auto_revoke, { this, &TerminalPage::_OnFirstLayout }); - - _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); - _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); - - // DON'T set up Toasts/TeachingTips here. They should be loaded and - // initialized the first time they're opened, in whatever method opens - // them. - - _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); - - _adjustProcessPriorityThrottled = std::make_shared>( - DispatcherQueue::GetForCurrentThread(), - til::throttled_func_options{ - .delay = std::chrono::milliseconds{ 100 }, - .debounce = true, - .trailing = true, - }, - [=]() { - _adjustProcessPriority(); - }); - } - - Windows::UI::Xaml::Automation::Peers::AutomationPeer TerminalPage::OnCreateAutomationPeer() - { - return Automation::Peers::FrameworkElementAutomationPeer(*this); - } - - // Method Description: - // - This is a bit of trickiness: If we're running unelevated, and the user - // passed in only --elevate actions, the we don't _actually_ want to - // restore the layouts here. We're not _actually_ about to create the - // window. We're simply going to toss the commandlines - // Arguments: - // - - // Return Value: - // - true if we're not elevated but all relevant pane-spawning actions are elevated - bool TerminalPage::ShouldImmediatelyHandoffToElevated(const CascadiaSettings& settings) const - { - if (_startupActions.empty() || _startupConnection || IsRunningElevated()) - { - // No point in handing off if we got no startup actions, or we're already elevated. - // Also, we shouldn't need to elevate handoff ConPTY connections. - assert(!_startupConnection); - return false; - } - - // Check that there's at least one action that's not just an elevated newTab action. - for (const auto& action : _startupActions) - { - // Only new terminal panes will be requesting elevation. - NewTerminalArgs newTerminalArgs{ nullptr }; - - if (action.Action() == ShortcutAction::NewTab) - { - const auto& args{ action.Args().try_as() }; - if (args) - { - newTerminalArgs = args.ContentArgs().try_as(); - } - else - { - // This was a nt action that didn't have any args. The default - // profile may want to be elevated, so don't just early return. - } - } - else if (action.Action() == ShortcutAction::SplitPane) - { - const auto& args{ action.Args().try_as() }; - if (args) - { - newTerminalArgs = args.ContentArgs().try_as(); - } - else - { - // This was a nt action that didn't have any args. The default - // profile may want to be elevated, so don't just early return. - } - } - else - { - // This was not a new tab or split pane action. - // This doesn't affect the outcome - continue; - } - - // It's possible that newTerminalArgs is null here. - // GetProfileForArgs should be resilient to that. - const auto profile{ settings.GetProfileForArgs(newTerminalArgs) }; - if (profile.Elevate()) - { - continue; - } - - // The profile didn't want to be elevated, and we aren't elevated. - // We're going to open at least one tab, so return false. - return false; - } - return true; - } - - // Method Description: - // - Escape hatch for immediately dispatching requests to elevated windows - // when first launched. At this point in startup, the window doesn't exist - // yet, XAML hasn't been started, but we need to dispatch these actions. - // We can't just go through ProcessStartupActions, because that processes - // the actions async using the XAML dispatcher (which doesn't exist yet) - // - DON'T CALL THIS if you haven't already checked - // ShouldImmediatelyHandoffToElevated. If you're thinking about calling - // this outside of the one place it's used, that's probably the wrong - // solution. - // Arguments: - // - settings: the settings we should use for dispatching these actions. At - // this point in startup, we hadn't otherwise been initialized with these, - // so use them now. - // Return Value: - // - - void TerminalPage::HandoffToElevated(const CascadiaSettings& settings) - { - if (_startupActions.empty()) - { - return; - } - - // Hookup our event handlers to the ShortcutActionDispatch - _settings = settings; - _HookupKeyBindings(_settings.ActionMap()); - _RegisterActionCallbacks(); - - for (const auto& action : _startupActions) - { - // only process new tabs and split panes. They're all going to the elevated window anyways. - if (action.Action() == ShortcutAction::NewTab || action.Action() == ShortcutAction::SplitPane) - { - _actionDispatch->DoAction(action); - } - } - } - - safe_void_coroutine TerminalPage::_NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e) - try - { - const auto data = e.DataView(); - if (!data.Contains(StandardDataFormats::StorageItems())) - { - co_return; - } - - const auto weakThis = get_weak(); - const auto items = co_await data.GetStorageItemsAsync(); - const auto strongThis = weakThis.get(); - if (!strongThis) - { - co_return; - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabByDragDrop", - TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - for (const auto& item : items) - { - auto directory = item.Path(); - - std::filesystem::path path(std::wstring_view{ directory }); - if (!std::filesystem::is_directory(path)) - { - directory = winrt::hstring{ path.parent_path().native() }; - } - - NewTerminalArgs args; - args.StartingDirectory(directory); - _OpenNewTerminalViaDropdown(args); - } - } - CATCH_LOG() - - // Method Description: - // - This method is called once command palette action was chosen for dispatching - // We'll use this event to dispatch this command. - // Arguments: - // - command - command to dispatch - // Return Value: - // - - void TerminalPage::_OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command) - { - const auto& actionAndArgs = command.ActionAndArgs(); - _actionDispatch->DoAction(sender, actionAndArgs); - } - - // Method Description: - // - This method is called once command palette command line was chosen for execution - // We'll use this event to create a command line execution command and dispatch it. - // Arguments: - // - command - command to dispatch - // Return Value: - // - - void TerminalPage::_OnCommandLineExecutionRequested(const IInspectable& /*sender*/, const winrt::hstring& commandLine) - { - ExecuteCommandlineArgs args{ commandLine }; - ActionAndArgs actionAndArgs{ ShortcutAction::ExecuteCommandline, args }; - _actionDispatch->DoAction(actionAndArgs); - } - - // Method Description: - // - This method is called once on startup, on the first LayoutUpdated event. - // We'll use this event to know that we have an ActualWidth and - // ActualHeight, so we can now attempt to process our list of startup - // actions. - // - We'll remove this event handler when the event is first handled. - // - If there are no startup actions, we'll open a single tab with the - // default profile. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_OnFirstLayout(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) - { - // Only let this succeed once. - _layoutUpdatedRevoker.revoke(); - - // This event fires every time the layout changes, but it is always the - // last one to fire in any layout change chain. That gives us great - // flexibility in finding the right point at which to initialize our - // renderer (and our terminal). Any earlier than the last layout update - // and we may not know the terminal's starting size. - if (_startupState == StartupState::NotInitialized) - { - _startupState = StartupState::InStartup; - - if (_startupConnection) - { - CreateTabFromConnection(std::move(_startupConnection)); - } - else if (!_startupActions.empty()) - { - ProcessStartupActions(std::move(_startupActions)); - } - - _CompleteInitialization(); - } - } - - // Method Description: - // - Process all the startup actions in the provided list of startup - // actions. We'll do this all at once here. - // Arguments: - // - actions: a winrt vector of actions to process. Note that this must NOT - // be an IVector&, because we need the collection to be accessible on the - // other side of the co_await. - // - initial: if true, we're parsing these args during startup, and we - // should fire an Initialized event. - // - cwd: If not empty, we should try switching to this provided directory - // while processing these actions. This will allow something like `wt -w 0 - // nt -d .` from inside another directory to work as expected. - // Return Value: - // - - safe_void_coroutine TerminalPage::ProcessStartupActions(std::vector actions, const winrt::hstring cwd, const winrt::hstring env) - { - const auto strong = get_strong(); - - // If the caller provided a CWD, "switch" to that directory, then switch - // back once we're done. - auto originalVirtualCwd{ _WindowProperties.VirtualWorkingDirectory() }; - auto originalVirtualEnv{ _WindowProperties.VirtualEnvVars() }; - auto restoreCwd = wil::scope_exit([&]() { - if (!cwd.empty()) - { - // ignore errors, we'll just power on through. We'd rather do - // something rather than fail silently if the directory doesn't - // actually exist. - _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd); - _WindowProperties.VirtualEnvVars(originalVirtualEnv); - } - }); - if (!cwd.empty()) - { - _WindowProperties.VirtualWorkingDirectory(cwd); - _WindowProperties.VirtualEnvVars(env); - } - - // The current TerminalWindow & TerminalPage architecture is rather instable - // and fails to start up if the first tab isn't created synchronously. - // - // While that's a fair assumption in on itself, simultaneously WinUI will - // not assign tab contents a size if they're not shown at least once, - // which we need however in order to initialize ControlCore with a size. - // - // So, we do two things here: - // * DO NOT suspend if this is the first tab. - // * DO suspend between the creation of panes (or tabs) in order to allow - // WinUI to layout the new controls and for ControlCore to get a size. - // - // This same logic is also applied to CreateTabFromConnection. - // - // See GH#13136. - auto suspend = _tabs.Size() > 0; - - for (size_t i = 0; i < actions.size(); ++i) - { - if (suspend) - { - co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); - } - - _actionDispatch->DoAction(actions[i]); - suspend = true; - } - - // GH#6586: now that we're done processing all startup commands, - // focus the active control. This will work as expected for both - // commandline invocations and for `wt` action invocations. - if (const auto& tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto& content{ tabImpl->GetActiveContent() }) - { - content.Focus(FocusState::Programmatic); - } - } - } - - safe_void_coroutine TerminalPage::CreateTabFromConnection(ITerminalConnection connection) - { - const auto strong = get_strong(); - - // This is the exact same logic as in ProcessStartupActions. - if (_tabs.Size() > 0) - { - co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); - } - - NewTerminalArgs newTerminalArgs; - - if (const auto conpty = connection.try_as()) - { - newTerminalArgs.Commandline(conpty.Commandline()); - newTerminalArgs.TabTitle(conpty.StartingTitle()); - } - - // GH #12370: We absolutely cannot allow a defterm connection to - // auto-elevate. Defterm doesn't work for elevated scenarios in the - // first place. If we try accepting the connection, the spawning an - // elevated version of the Terminal with that profile... that's a - // recipe for disaster. We won't ever open up a tab in this window. - newTerminalArgs.Elevate(false); - - const auto newPane = _MakePane(newTerminalArgs, nullptr, std::move(connection)); - newPane->WalkTree([](const auto& pane) { - pane->FinalizeConfigurationGivenDefault(); - }); - _CreateNewTabFromPane(newPane); - } - - // Method Description: - // - Perform and steps that need to be done once our initial state is all - // set up. This includes entering fullscreen mode and firing our - // Initialized event. - // Arguments: - // - - // Return Value: - // - - safe_void_coroutine TerminalPage::_CompleteInitialization() - { - _startupState = StartupState::Initialized; - - // GH#632 - It's possible that the user tried to create the terminal - // with only one tab, with only an elevated profile. If that happens, - // we'll create _another_ process to host the elevated version of that - // profile. This can happen from the jumplist, or if the default profile - // is `elevate:true`, or from the commandline. - // - // However, we need to make sure to close this window in that scenario. - // Since there aren't any _tabs_ in this window, we won't ever get a - // closed event. So do it manually. - // - // GH#12267: Make sure that we don't instantly close ourselves when - // we're readying to accept a defterm connection. In that case, we don't - // have a tab yet, but will once we're initialized. - if (_tabs.Size() == 0) - { - CloseWindowRequested.raise(*this, nullptr); - co_return; - } - else - { - // GH#11561: When we start up, our window is initially just a frame - // with a transparent content area. We're gonna do all this startup - // init on the UI thread, so the UI won't actually paint till it's - // all done. This results in a few frames where the frame is - // visible, before the page paints for the first time, before any - // tabs appears, etc. - // - // To mitigate this, we're gonna wait for the UI thread to finish - // everything it's gotta do for the initial init, and _then_ fire - // our Initialized event. By waiting for everything else to finish - // (CoreDispatcherPriority::Low), we let all the tabs and panes - // actually get created. In the window layer, we're gonna cloak the - // window till this event is fired, so we don't actually see this - // frame until we're actually all ready to go. - // - // This will result in the window seemingly not loading as fast, but - // it will actually take exactly the same amount of time before it's - // usable. - // - // We also experimented with drawing a solid BG color before the - // initialization is finished. However, there are still a few frames - // after the frame is displayed before the XAML content first draws, - // so that didn't actually resolve any issues. - Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weak = get_weak()]() { - if (auto self{ weak.get() }) - { - self->Initialized.raise(*self, nullptr); - } - }); - } - } - - // Method Description: - // - Show a dialog with "About" information. Displays the app's Display - // Name, version, getting started link, source code link, documentation link, release - // Notes link, send feedback link and privacy policy link. - void TerminalPage::_ShowAboutDialog() - { - _ShowDialogHelper(L"AboutDialog"); - } - - winrt::hstring TerminalPage::ApplicationDisplayName() - { - return CascadiaSettings::ApplicationDisplayName(); - } - - winrt::hstring TerminalPage::ApplicationVersion() - { - return CascadiaSettings::ApplicationVersion(); - } - - // Method Description: - // - Helper to show a content dialog - // - We only open a content dialog if there isn't one open already - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowDialogHelper(const std::wstring_view& name) - { - if (auto presenter{ _dialogPresenter.get() }) - { - co_return co_await presenter.ShowDialog(FindName(name).try_as()); - } - co_return ContentDialogResult::None; - } - - // Method Description: - // - Displays a dialog to warn the user that they are about to close all open windows. - // Once the user clicks the OK button, shut down the application. - // If cancel is clicked, the dialog will close. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowQuitDialog() - { - return _ShowDialogHelper(L"QuitDialog"); - } - - // Method Description: - // - Displays a dialog for warnings found while closing the terminal app using - // key binding with multiple tabs opened. Display messages to warn user - // that more than 1 tab is opened, and once the user clicks the OK button, remove - // all the tabs and shut down and app. If cancel is clicked, the dialog will close - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseWarningDialog() - { - return _ShowDialogHelper(L"CloseAllDialog"); - } - - // Method Description: - // - Displays a dialog for warnings found while closing the terminal tab marked as read-only - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseReadOnlyDialog() - { - return _ShowDialogHelper(L"CloseReadOnlyDialog"); - } - - // Method Description: - // - Displays a dialog to warn the user about the fact that the text that - // they are trying to paste contains the "new line" character which can - // have the effect of starting commands without the user's knowledge if - // it is pasted on a shell where the "new line" character marks the end - // of a command. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowMultiLinePasteWarningDialog() - { - return _ShowDialogHelper(L"MultiLinePasteDialog"); - } - - // Method Description: - // - Displays a dialog to warn the user about the fact that the text that - // they are trying to paste is very long, in case they did not mean to - // paste it but pressed the paste shortcut by accident. - // - Only one dialog can be visible at a time. If another dialog is visible - // when this is called, nothing happens. See _ShowDialog for details - winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowLargePasteWarningDialog() - { - return _ShowDialogHelper(L"LargePasteDialog"); - } - - // Method Description: - // - Builds the flyout (dropdown) attached to the new tab button, and - // attaches it to the button. Populates the flyout with one entry per - // Profile, displaying the profile's name. Clicking each flyout item will - // open a new tab with that profile. - // Below the profiles are the static menu items: settings, command palette - void TerminalPage::_CreateNewTabFlyout() - { - auto newTabFlyout = WUX::Controls::MenuFlyout{}; - newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft); - - // Create profile entries from the NewTabMenu configuration using a - // recursive helper function. This returns a std::vector of FlyoutItemBases, - // that we then add to our Flyout. - auto entries = _settings.GlobalSettings().NewTabMenu(); - auto items = _CreateNewTabFlyoutItems(entries); - for (const auto& item : items) - { - newTabFlyout.Items().Append(item); - } - - // add menu separator - auto separatorItem = WUX::Controls::MenuFlyoutSeparator{}; - newTabFlyout.Items().Append(separatorItem); - - // add static items - { - // Create the settings button. - auto settingsItem = WUX::Controls::MenuFlyoutItem{}; - settingsItem.Text(RS_(L"SettingsMenuItem")); - const auto settingsToolTip = RS_(L"SettingsToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(settingsItem, box_value(settingsToolTip)); - Automation::AutomationProperties::SetHelpText(settingsItem, settingsToolTip); - - WUX::Controls::SymbolIcon ico{}; - ico.Symbol(WUX::Controls::Symbol::Setting); - settingsItem.Icon(ico); - - settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); - newTabFlyout.Items().Append(settingsItem); - - auto actionMap = _settings.ActionMap(); - const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenSettingsUI") }; - if (settingsKeyChord) - { - _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); - } - - // Create the command palette button. - auto commandPaletteFlyout = WUX::Controls::MenuFlyoutItem{}; - commandPaletteFlyout.Text(RS_(L"CommandPaletteMenuItem")); - const auto commandPaletteToolTip = RS_(L"CommandPaletteToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(commandPaletteFlyout, box_value(commandPaletteToolTip)); - Automation::AutomationProperties::SetHelpText(commandPaletteFlyout, commandPaletteToolTip); - - WUX::Controls::FontIcon commandPaletteIcon{}; - commandPaletteIcon.Glyph(L"\xE945"); - commandPaletteIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); - commandPaletteFlyout.Icon(commandPaletteIcon); - - commandPaletteFlyout.Click({ this, &TerminalPage::_CommandPaletteButtonOnClick }); - newTabFlyout.Items().Append(commandPaletteFlyout); - - const auto commandPaletteKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.ToggleCommandPalette") }; - if (commandPaletteKeyChord) - { - _SetAcceleratorForMenuItem(commandPaletteFlyout, commandPaletteKeyChord); - } - - // Create the about button. - auto aboutFlyout = WUX::Controls::MenuFlyoutItem{}; - aboutFlyout.Text(RS_(L"AboutMenuItem")); - const auto aboutToolTip = RS_(L"AboutToolTip"); - - WUX::Controls::ToolTipService::SetToolTip(aboutFlyout, box_value(aboutToolTip)); - Automation::AutomationProperties::SetHelpText(aboutFlyout, aboutToolTip); - - WUX::Controls::SymbolIcon aboutIcon{}; - aboutIcon.Symbol(WUX::Controls::Symbol::Help); - aboutFlyout.Icon(aboutIcon); - - aboutFlyout.Click({ this, &TerminalPage::_AboutButtonOnClick }); - newTabFlyout.Items().Append(aboutFlyout); - } - - // Before opening the fly-out set focus on the current tab - // so no matter how fly-out is closed later on the focus will return to some tab. - // We cannot do it on closing because if the window loses focus (alt+tab) - // the closing event is not fired. - // It is important to set the focus on the tab - // Since the previous focus location might be discarded in the background, - // e.g., the command palette will be dismissed by the menu, - // and then closing the fly-out will move the focus to wrong location. - newTabFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - page->_FocusCurrentTab(true); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuOpened", - TraceLoggingDescription("Event emitted when the new tab menu is opened"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }); - // Necessary for fly-out sub items to get focus on a tab before collapsing. Related to #15049 - newTabFlyout.Closing([weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - if (!page->_commandPaletteIs(Visibility::Visible)) - { - page->_FocusCurrentTab(true); - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuClosed", - TraceLoggingDescription("Event emitted when the new tab menu is closed"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }); - _newTabButton.Flyout(newTabFlyout); - } - - // Method Description: - // - For a given list of tab menu entries, this method will create the corresponding - // list of flyout items. This is a recursive method that calls itself when it comes - // across a folder entry. - std::vector TerminalPage::_CreateNewTabFlyoutItems(IVector entries) - { - std::vector items; - - if (entries == nullptr || entries.Size() == 0) - { - return items; - } - - for (const auto& entry : entries) - { - if (entry == nullptr) - { - continue; - } - - switch (entry.Type()) - { - case NewTabMenuEntryType::Separator: - { - items.push_back(WUX::Controls::MenuFlyoutSeparator{}); - break; - } - // A folder has a custom name and icon, and has a number of entries that require - // us to call this method recursively. - case NewTabMenuEntryType::Folder: - { - const auto folderEntry = entry.as(); - const auto folderEntries = folderEntry.Entries(); - - // If the folder is empty, we should skip the entry if AllowEmpty is false, or - // when the folder should inline. - // The IsEmpty check includes semantics for nested (empty) folders - if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) - { - break; - } - - // Recursively generate flyout items - auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries); - - // If the folder should auto-inline and there is only one item, do so. - if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1) - { - for (auto const& folderEntryItem : folderEntryItems) - { - items.push_back(folderEntryItem); - } - - break; - } - - // Otherwise, create a flyout - auto folderItem = WUX::Controls::MenuFlyoutSubItem{}; - folderItem.Text(folderEntry.Name()); - - auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon().Resolved()); - folderItem.Icon(icon); - - for (const auto& folderEntryItem : folderEntryItems) - { - folderItem.Items().Append(folderEntryItem); - } - - // If the folder is empty, and by now we know we set AllowEmpty to true, - // create a placeholder item here - if (folderEntries.Size() == 0) - { - auto placeholder = WUX::Controls::MenuFlyoutItem{}; - placeholder.Text(RS_(L"NewTabMenuFolderEmpty")); - placeholder.IsEnabled(false); - - folderItem.Items().Append(placeholder); - } - - items.push_back(folderItem); - break; - } - // Any "collection entry" will simply make us add each profile in the collection - // separately. This collection is stored as a map , so the correct - // profile index is already known. - case NewTabMenuEntryType::RemainingProfiles: - case NewTabMenuEntryType::MatchProfiles: - { - const auto remainingProfilesEntry = entry.as(); - if (remainingProfilesEntry.Profiles() == nullptr) - { - break; - } - - for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles()) - { - items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex, {})); - } - - break; - } - // A single profile, the profile index is also given in the entry - case NewTabMenuEntryType::Profile: - { - const auto profileEntry = entry.as(); - if (profileEntry.Profile() == nullptr) - { - break; - } - - auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex(), profileEntry.Icon().Resolved()); - items.push_back(profileItem); - break; - } - case NewTabMenuEntryType::Action: - { - const auto actionEntry = entry.as(); - const auto actionId = actionEntry.ActionId(); - if (_settings.ActionMap().GetActionByID(actionId)) - { - auto actionItem = _CreateNewTabFlyoutAction(actionId, actionEntry.Icon().Resolved()); - items.push_back(actionItem); - } - - break; - } - } - } - - return items; - } - - // Method Description: - // - This method creates a flyout menu item for a given profile with the given index. - // It makes sure to set the correct icon, keybinding, and click-action. - WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex, const winrt::hstring& iconPathOverride) - { - auto profileMenuItem = WUX::Controls::MenuFlyoutItem{}; - - // Add the keyboard shortcuts based on the number of profiles defined - // Look for a keychord that is bound to the equivalent - // NewTab(ProfileIndex=N) action - NewTerminalArgs newTerminalArgs{ profileIndex }; - NewTabArgs newTabArgs{ newTerminalArgs }; - const auto id = fmt::format(FMT_COMPILE(L"Terminal.OpenNewTabProfile{}"), profileIndex); - const auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(id) }; - - // make sure we find one to display - if (profileKeyChord) - { - _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord); - } - - auto profileName = profile.Name(); - profileMenuItem.Text(profileName); - - // If a custom icon path has been specified, set it as the icon for - // this flyout item. Otherwise, if an icon is set for this profile, set that icon - // for this flyout item. - const auto& iconPath = iconPathOverride.empty() ? profile.Icon().Resolved() : iconPathOverride; - if (!iconPath.empty()) - { - const auto icon = _CreateNewTabFlyoutIcon(iconPath); - profileMenuItem.Icon(icon); - } - - if (profile.Guid() == _settings.GlobalSettings().DefaultProfile()) - { - // Contrast the default profile with others in font weight. - profileMenuItem.FontWeight(FontWeights::Bold()); - } - - auto newTabRun = WUX::Documents::Run(); - newTabRun.Text(RS_(L"NewTabRun/Text")); - auto newPaneRun = WUX::Documents::Run(); - newPaneRun.Text(RS_(L"NewPaneRun/Text")); - newPaneRun.FontStyle(FontStyle::Italic); - auto newWindowRun = WUX::Documents::Run(); - newWindowRun.Text(RS_(L"NewWindowRun/Text")); - newWindowRun.FontStyle(FontStyle::Italic); - auto elevatedRun = WUX::Documents::Run(); - elevatedRun.Text(RS_(L"ElevatedRun/Text")); - elevatedRun.FontStyle(FontStyle::Italic); - - auto textBlock = WUX::Controls::TextBlock{}; - textBlock.Inlines().Append(newTabRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(newPaneRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(newWindowRun); - textBlock.Inlines().Append(WUX::Documents::LineBreak{}); - textBlock.Inlines().Append(elevatedRun); - - auto toolTip = WUX::Controls::ToolTip{}; - toolTip.Content(textBlock); - WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip); - - profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Profile", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - NewTerminalArgs newTerminalArgs{ profileIndex }; - page->_OpenNewTerminalViaDropdown(newTerminalArgs); - } - }); - - // Using the static method on the base class seems to do what we want in terms of placement. - WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(profileMenuItem, _CreateRunAsAdminFlyout(profileIndex)); - - // Since we are not setting the ContextFlyout property of the item we have to handle the ContextRequested event - // and rely on the base class to show our menu. - profileMenuItem.ContextRequested([profileMenuItem](auto&&, auto&&) { - WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(profileMenuItem); - }); - - return profileMenuItem; - } - - // Method Description: - // - This method creates a flyout menu item for a given action - // It makes sure to set the correct icon, keybinding, and click-action. - WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride) - { - auto actionMenuItem = WUX::Controls::MenuFlyoutItem{}; - const auto action{ _settings.ActionMap().GetActionByID(actionId) }; - const auto actionKeyChord{ _settings.ActionMap().GetKeyBindingForAction(actionId) }; - - if (actionKeyChord) - { - _SetAcceleratorForMenuItem(actionMenuItem, actionKeyChord); - } - - actionMenuItem.Text(action.Name()); - - // If a custom icon path has been specified, set it as the icon for - // this flyout item. Otherwise, if an icon is set for this action, set that icon - // for this flyout item. - const auto& iconPath = iconPathOverride.empty() ? action.Icon().Resolved() : iconPathOverride; - if (!iconPath.empty()) - { - const auto icon = _CreateNewTabFlyoutIcon(iconPath); - actionMenuItem.Icon(icon); - } - - actionMenuItem.Click([action, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Action", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - page->_actionDispatch->DoAction(action.ActionAndArgs()); - } - }); - - return actionMenuItem; - } - - // Method Description: - // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and - // MenuFlyoutSubItems - IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource) - { - if (iconSource.empty()) - { - return nullptr; - } - - auto icon = UI::IconPathConverter::IconWUX(iconSource); - Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw); - - return icon; - } - - // Function Description: - // Called when the openNewTabDropdown keybinding is used. - // Shows the dropdown flyout. - void TerminalPage::_OpenNewTabDropdown() - { - _newTabButton.Flyout().ShowAt(_newTabButton); - } - - void TerminalPage::_OpenNewTerminalViaDropdown(const NewTerminalArgs newTerminalArgs) - { - // if alt is pressed, open a pane - const auto window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); - const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - const auto ctrlState{ window.GetKeyState(VirtualKey::Control) }; - const auto rCtrlState = window.GetKeyState(VirtualKey::RightControl); - const auto lCtrlState = window.GetKeyState(VirtualKey::LeftControl); - const auto ctrlPressed{ WI_IsFlagSet(ctrlState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rCtrlState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lCtrlState, CoreVirtualKeyStates::Down) }; - - // Check for DebugTap - auto debugTap = this->_settings.GlobalSettings().DebugFeaturesEnabled() && - WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - - const auto dispatchToElevatedWindow = ctrlPressed && !IsRunningElevated(); - - auto sessionType = ""; - if ((shiftPressed || dispatchToElevatedWindow) && !debugTap) - { - // Manually fill in the evaluated profile. - if (newTerminalArgs.ProfileIndex() != nullptr) - { - // We want to promote the index to a GUID because there is no "launch to profile index" command. - const auto profile = _settings.GetProfileForArgs(newTerminalArgs); - if (profile) - { - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); - newTerminalArgs.StartingDirectory(_evaluatePathForCwd(profile.EvaluatedStartingDirectory())); - } - } - - if (dispatchToElevatedWindow) - { - _OpenElevatedWT(newTerminalArgs); - sessionType = "ElevatedWindow"; - } - else - { - _OpenNewWindow(newTerminalArgs); - sessionType = "Window"; - } - } - else - { - const auto newPane = _MakePane(newTerminalArgs); - // If the newTerminalArgs caused us to open an elevated window - // instead of creating a pane, it may have returned nullptr. Just do - // nothing then. - if (!newPane) - { - return; - } - if (altPressed && !debugTap) - { - this->_SplitPane(_GetFocusedTabImpl(), - SplitDirection::Automatic, - 0.5f, - newPane); - sessionType = "Pane"; - } - else - { - _CreateNewTabFromPane(newPane); - sessionType = "Tab"; - } - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuCreatedNewTerminalSession", - TraceLoggingDescription("Event emitted when a new terminal was created via the new tab menu"), - TraceLoggingValue(NumberOfTabs(), "NewTabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue(sessionType, "SessionType", "The type of session that was created"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - std::wstring TerminalPage::_evaluatePathForCwd(const std::wstring_view path) - { - return Utils::EvaluateStartingDirectory(_WindowProperties.VirtualWorkingDirectory(), path); - } - - // Method Description: - // - Creates a new connection based on the profile settings - // Arguments: - // - the profile we want the settings from - // - the terminal settings - // Return value: - // - the desired connection - TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile, - IControlSettings settings, - const bool inheritCursor) - { - static const auto textMeasurement = [&]() -> std::wstring_view { - switch (_settings.GlobalSettings().TextMeasurement()) - { - case TextMeasurement::Graphemes: - return L"graphemes"; - case TextMeasurement::Wcswidth: - return L"wcswidth"; - case TextMeasurement::Console: - return L"console"; - default: - return {}; - } - }(); - static const auto ambiguousIsWide = [&]() -> bool { - return _settings.GlobalSettings().AmbiguousWidth() == AmbiguousWidth::Wide; - }(); - - TerminalConnection::ITerminalConnection connection{ nullptr }; - - auto connectionType = profile.ConnectionType(); - Windows::Foundation::Collections::ValueSet valueSet; - - if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && - TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) - { - connection = TerminalConnection::AzureConnection{}; - valueSet = TerminalConnection::ConptyConnection::CreateSettings(winrt::hstring{}, - L".", - L"Azure", - false, - L"", - nullptr, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid(), - profile.Guid()); - } - - else - { - auto settingsInternal{ winrt::get_self(settings) }; - const auto environment = settingsInternal->EnvironmentVariables(); - - // Update the path to be relative to whatever our CWD is. - // - // Refer to the examples in - // https://en.cppreference.com/w/cpp/filesystem/path/append - // - // We need to do this here, to ensure we tell the ConptyConnection - // the correct starting path. If we're being invoked from another - // terminal instance (e.g. `wt -w 0 -d .`), then we have switched our - // CWD to the provided path. We should treat the StartingDirectory - // as relative to the current CWD. - // - // The connection must be informed of the current CWD on - // construction, because the connection might not spawn the child - // process until later, on another thread, after we've already - // restored the CWD to its original value. - auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) }; - connection = TerminalConnection::ConptyConnection{}; - valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), - newWorkingDirectory, - settings.StartingTitle(), - settingsInternal->ReloadEnvironmentVariables(), - _WindowProperties.VirtualEnvVars(), - environment, - settings.InitialRows(), - settings.InitialCols(), - winrt::guid(), - profile.Guid()); - - if (inheritCursor) - { - valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true)); - } - } - - if (!textMeasurement.empty()) - { - valueSet.Insert(L"textMeasurement", Windows::Foundation::PropertyValue::CreateString(textMeasurement)); - } - if (ambiguousIsWide) - { - valueSet.Insert(L"ambiguousIsWide", Windows::Foundation::PropertyValue::CreateBoolean(true)); - } - - if (const auto id = settings.SessionId(); id != winrt::guid{}) - { - valueSet.Insert(L"sessionId", Windows::Foundation::PropertyValue::CreateGuid(id)); - } - - connection.Initialize(valueSet); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "ConnectionCreated", - TraceLoggingDescription("Event emitted upon the creation of a connection"), - TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"), - TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"), - TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - return connection; - } - - TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent) - { - if (paneContent == nullptr) - { - return nullptr; - } - - const auto& control{ paneContent.GetTermControl() }; - if (control == nullptr) - { - return nullptr; - } - const auto& connection = control.Connection(); - auto profile{ paneContent.GetProfile() }; - - Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; - - if (profile) - { - // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. - profile = GetClosestProfileForDuplicationOfProfile(profile); - controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); - - // Replace the Starting directory with the CWD, if given - const auto workingDirectory = control.WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) - { - controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); - } - - // To facilitate restarting defterm connections: grab the original - // commandline out of the connection and shove that back into the - // settings. - if (const auto& conpty{ connection.try_as() }) - { - controlSettings.DefaultSettings()->Commandline(conpty.Commandline()); - } - } - - return _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), true); - } - - // Method Description: - // - Called when the settings button is clicked. Launches a background - // thread to open the settings file in the default JSON editor. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_SettingsButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - const auto window = CoreWindow::GetForCurrentThread(); - - // check alt state - const auto rAltState{ window.GetKeyState(VirtualKey::RightMenu) }; - const auto lAltState{ window.GetKeyState(VirtualKey::LeftMenu) }; - const auto altPressed{ WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down) }; - - // check shift state - const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; - const auto lShiftState{ window.GetKeyState(VirtualKey::LeftShift) }; - const auto rShiftState{ window.GetKeyState(VirtualKey::RightShift) }; - const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || - WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; - - auto target{ SettingsTarget::SettingsUI }; - if (shiftPressed) - { - target = SettingsTarget::SettingsFile; - } - else if (altPressed) - { - target = SettingsTarget::DefaultsFile; - } - - const auto targetAsString = [&target]() { - switch (target) - { - case SettingsTarget::SettingsFile: - return "SettingsFile"; - case SettingsTarget::DefaultsFile: - return "DefaultsFile"; - case SettingsTarget::SettingsUI: - default: - return "UI"; - } - }(); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("Settings", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingValue(targetAsString, "SettingsTarget", "The target settings file or UI"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - _LaunchSettings(target); - } - - // Method Description: - // - Called when the command palette button is clicked. Opens the command palette. - void TerminalPage::_CommandPaletteButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - auto p = LoadCommandPalette(); - p.EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); - p.Visibility(Visibility::Visible); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("CommandPalette", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - // Method Description: - // - Called when the about button is clicked. See _ShowAboutDialog for more info. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_AboutButtonOnClick(const IInspectable&, - const RoutedEventArgs&) - { - _ShowAboutDialog(); - - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemClicked", - TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), - TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingValue("About", "ItemType", "The type of item that was clicked in the new tab menu"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - - // Method Description: - // - Called when the users pressed keyBindings while CommandPaletteElement is open. - // - As of GH#8480, this is also bound to the TabRowControl's KeyUp event. - // That should only fire when focus is in the tab row, which is hard to - // do. Notably, that's possible: - // - When you have enough tabs to make the little scroll arrows appear, - // click one, then hit tab - // - When Narrator is in Scan mode (which is the a11y bug we're fixing here) - // - This method is effectively an extract of TermControl::_KeyHandler and TermControl::_TryHandleKeyBinding. - // Arguments: - // - e: the KeyRoutedEventArgs containing info about the keystroke. - // Return Value: - // - - void TerminalPage::_KeyDownHandler(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto keyStatus = e.KeyStatus(); - const auto vkey = gsl::narrow_cast(e.OriginalKey()); - const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); - const auto modifiers = _GetPressedModifierKeys(); - - // GH#11076: - // For some weird reason we sometimes receive a WM_KEYDOWN - // message without vkey or scanCode if a user drags a tab. - // The KeyChord constructor has a debug assertion ensuring that all KeyChord - // either have a valid vkey/scanCode. This is important, because this prevents - // accidental insertion of invalid KeyChords into classes like ActionMap. - if (!vkey && !scanCode) - { - return; - } - - // Alt-Numpad# input will send us a character once the user releases - // Alt, so we should be ignoring the individual keydowns. The character - // will be sent through the TSFInputControl. See GH#1401 for more - // details - if (modifiers.IsAltPressed() && (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) - { - return; - } - - // GH#2235: Terminal::Settings hasn't been modified to differentiate - // between AltGr and Ctrl+Alt yet. - // -> Don't check for key bindings if this is an AltGr key combination. - if (modifiers.IsAltGrPressed()) - { - return; - } - - const auto actionMap = _settings.ActionMap(); - if (!actionMap) - { - return; - } - - const auto cmd = actionMap.GetActionByKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - modifiers.IsWinPressed(), - vkey, - scanCode, - }); - if (!cmd) - { - return; - } - - if (!_actionDispatch->DoAction(cmd.ActionAndArgs())) - { - return; - } - - if (_commandPaletteIs(Visibility::Visible) && - cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) - { - CommandPaletteElement().Visibility(Visibility::Collapsed); - } - if (_suggestionsControlIs(Visibility::Visible) && - cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) - { - SuggestionsElement().Visibility(Visibility::Collapsed); - } - - // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". - // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. - // The following is used to manually "consume" such dead keys and clear them from the keyboard state. - _ClearKeyboardState(vkey, scanCode); - e.Handled(true); - } - - bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) - { - const auto modifiers = _GetPressedModifierKeys(); - if (vkey == VK_SPACE && modifiers.IsAltPressed() && down) - { - if (const auto actionMap = _settings.ActionMap()) - { - if (const auto cmd = actionMap.GetActionByKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - modifiers.IsWinPressed(), - gsl::narrow_cast(vkey), - scanCode, - })) - { - return _actionDispatch->DoAction(cmd.ActionAndArgs()); - } - } - } - return false; - } - - // Method Description: - // - Get the modifier keys that are currently pressed. This can be used to - // find out which modifiers (ctrl, alt, shift) are pressed in events that - // don't necessarily include that state. - // - This is a copy of TermControl::_GetPressedModifierKeys. - // Return Value: - // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. - ControlKeyStates TerminalPage::_GetPressedModifierKeys() noexcept - { - const auto window = CoreWindow::GetForCurrentThread(); - // DONT USE - // != CoreVirtualKeyStates::None - // OR - // == CoreVirtualKeyStates::Down - // Sometimes with the key down, the state is Down | Locked. - // Sometimes with the key up, the state is Locked. - // IsFlagSet(Down) is the only correct solution. - - struct KeyModifier - { - VirtualKey vkey; - ControlKeyStates flags; - }; - - constexpr std::array modifiers{ { - { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, - { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, - { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, - { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, - { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, - { VirtualKey::RightWindows, ControlKeyStates::RightWinPressed }, - { VirtualKey::LeftWindows, ControlKeyStates::LeftWinPressed }, - } }; - - ControlKeyStates flags; - - for (const auto& mod : modifiers) - { - const auto state = window.GetKeyState(mod.vkey); - const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); - - if (isDown) - { - flags |= mod.flags; - } - } - - return flags; - } - - // Method Description: - // - Discards currently pressed dead keys. - // - This is a copy of TermControl::_ClearKeyboardState. - // Arguments: - // - vkey: The vkey of the key pressed. - // - scanCode: The scan code of the key pressed. - void TerminalPage::_ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept - { - std::array keyState; - if (!GetKeyboardState(keyState.data())) - { - return; - } - - // As described in "Sometimes you *want* to interfere with the keyboard's state buffer": - // http://archives.miloush.net/michkap/archive/2006/09/10/748775.html - // > "The key here is to keep trying to pass stuff to ToUnicode until -1 is not returned." - std::array buffer; - while (ToUnicodeEx(vkey, scanCode, keyState.data(), buffer.data(), gsl::narrow_cast(buffer.size()), 0b1, nullptr) < 0) - { - } - } - - // Method Description: - // - Configure the AppKeyBindings to use our ShortcutActionDispatch and the updated ActionMap - // as the object to handle dispatching ShortcutAction events. - // Arguments: - // - bindings: An IActionMapView object to wire up with our event handlers - void TerminalPage::_HookupKeyBindings(const IActionMapView& actionMap) noexcept - { - _bindings->SetDispatch(*_actionDispatch); - _bindings->SetActionMap(actionMap); - } - - // Method Description: - // - Register our event handlers with our ShortcutActionDispatch. The - // ShortcutActionDispatch is responsible for raising the appropriate - // events for an ActionAndArgs. WE'll handle each possible event in our - // own way. - // Arguments: - // - - void TerminalPage::_RegisterActionCallbacks() - { - // Hook up the ShortcutActionDispatch object's events to our handlers. - // They should all be hooked up here, regardless of whether or not - // there's an actual keychord for them. -#define ON_ALL_ACTIONS(action) HOOKUP_ACTION(action); - ALL_SHORTCUT_ACTIONS - INTERNAL_SHORTCUT_ACTIONS -#undef ON_ALL_ACTIONS - } - - // Method Description: - // - 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(const Tab& tab) - { - if (tab == _GetFocusedTab()) - { - TitleChanged.raise(*this, nullptr); - } - } - - // Method Description: - // - Connects event handlers to the TermControl for events that we want to - // handle. This includes: - // * the Copy and Paste events, for setting and retrieving clipboard data - // on the right thread - // Arguments: - // - term: The newly created TermControl to connect the events for - void TerminalPage::_RegisterTerminalEvents(TermControl term) - { - term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); - - term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); - term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler }); - - term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); - - // Add an event handler for when the terminal or tab wants to set a - // progress indicator on the taskbar - term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - - term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler }); - - term.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& e) { - if (auto page{ weakThis.get() }) - { - if (e.PropertyName() == L"BackgroundBrush") - { - page->_updateThemeColors(); - } - } - }); - - term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); - term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); - term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); - - // Don't even register for the event if the feature is compiled off. - if constexpr (Feature_ShellCompletions::IsEnabled()) - { - term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); - } - winrt::weak_ref weakTerm{ term }; - term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), false); - } - }); - term.SelectionContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); - } - }); - if constexpr (Feature_QuickFix::IsEnabled()) - { - term.QuickFixMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { - if (const auto& page{ weak.get() }) - { - page->_PopulateQuickFixMenu(weakTerm.get(), sender.try_as()); - } - }); - } - } - - // Method Description: - // - Connects event handlers to the Tab for events that we want to - // handle. This includes: - // * the TitleChanged event, for changing the text of the tab - // * the Color{Selected,Cleared} events to change the color of a tab. - // Arguments: - // - hostingTab: The Tab that's hosting this TermControl instance - void TerminalPage::_RegisterTabEvents(Tab& hostingTab) - { - auto weakTab{ hostingTab.get_weak() }; - auto weakThis{ get_weak() }; - // PropertyChanged is the generic mechanism by which the Tab - // communicates changes to any of its observable properties, including - // the Title - hostingTab.PropertyChanged([weakTab, weakThis](auto&&, const WUX::Data::PropertyChangedEventArgs& args) { - auto page{ weakThis.get() }; - auto tab{ weakTab.get() }; - if (page && tab) - { - const auto propertyName = args.PropertyName(); - if (propertyName == L"Title") - { - page->_UpdateTitle(*tab); - } - else if (propertyName == L"Content") - { - if (*tab == page->_GetFocusedTab()) - { - const auto children = page->_tabContent.Children(); - - children.Clear(); - if (auto content = tab->Content()) - { - page->_tabContent.Children().Append(std::move(content)); - } - - tab->Focus(FocusState::Programmatic); - } - } - } - }); - - // Add an event handler for when the terminal or tab wants to set a - // progress indicator on the taskbar - hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); - - hostingTab.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); - } - - // Method Description: - // - Helper to manually exit "zoom" when certain actions take place. - // Anything that modifies the state of the pane tree should probably - // un-zoom the focused pane first, so that the user can see the full pane - // tree again. These actions include: - // * Splitting a new pane - // * Closing a pane - // * Moving focus between panes - // * Resizing a pane - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_UnZoomIfNeeded() - { - if (const auto activeTab{ _GetFocusedTabImpl() }) - { - if (activeTab->IsZoomed()) - { - // Remove the content from the tab first, so Pane::UnZoom can - // re-attach the content to the tree w/in the pane - _tabContent.Children().Clear(); - // In ExitZoom, we'll change the Tab's Content(), triggering the - // content changed event, which will re-attach the tab's new content - // root to the tree. - activeTab->ExitZoom(); - } - } - } - - // Method Description: - // - Attempt to move focus between panes, as to focus the child on - // the other side of the separator. See Pane::NavigateFocus for details. - // - Moves the focus of the currently focused tab. - // Arguments: - // - direction: The direction to move the focus in. - // Return Value: - // - Whether changing the focus succeeded. This allows a keychord to propagate - // to the terminal when no other panes are present (GH#6219) - bool TerminalPage::_MoveFocus(const FocusDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->NavigateFocus(direction); - } - return false; - } - - // Method Description: - // - Attempt to swap the positions of the focused pane with another pane. - // See Pane::SwapPane for details. - // Arguments: - // - direction: The direction to move the focused pane in. - // Return Value: - // - true if panes were swapped. - bool TerminalPage::_SwapPane(const FocusDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - return tabImpl->SwapPane(direction); - } - return false; - } - - TermControl TerminalPage::_GetActiveControl() const - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->GetActiveTerminalControl(); - } - return nullptr; - } - - CommandPalette TerminalPage::LoadCommandPalette() - { - if (const auto p = CommandPaletteElement()) - { - return p; - } - - return _loadCommandPaletteSlowPath(); - } - bool TerminalPage::_commandPaletteIs(WUX::Visibility visibility) - { - const auto p = CommandPaletteElement(); - return p && p.Visibility() == visibility; - } - - CommandPalette TerminalPage::_loadCommandPaletteSlowPath() - { - const auto p = FindName(L"CommandPaletteElement").as(); - - p.SetActionMap(_settings.ActionMap()); - - // When the visibility of the command palette changes to "collapsed", - // the palette has been closed. Toss focus back to the currently active control. - p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - if (_commandPaletteIs(Visibility::Collapsed)) - { - _FocusActiveControl(nullptr, nullptr); - } - }); - p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - p.CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); - p.SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); - p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); - - return p; - } - - SuggestionsControl TerminalPage::LoadSuggestionsUI() - { - if (const auto p = SuggestionsElement()) - { - return p; - } - - return _loadSuggestionsElementSlowPath(); - } - bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) - { - const auto p = SuggestionsElement(); - return p && p.Visibility() == visibility; - } - - SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() - { - const auto p = FindName(L"SuggestionsElement").as(); - - p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { - if (SuggestionsElement().Visibility() == Visibility::Collapsed) - { - _FocusActiveControl(nullptr, nullptr); - } - }); - p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); - - return p; - } - - // Method Description: - // - Warn the user that they are about to close all open windows, then - // signal that we want to close everything. - safe_void_coroutine TerminalPage::RequestQuit() - { - if (!_displayingCloseDialog) - { - _displayingCloseDialog = true; - - const auto weak = get_weak(); - auto warningResult = co_await _ShowQuitDialog(); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - _displayingCloseDialog = false; - - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - - QuitRequested.raise(nullptr, nullptr); - } - } - - void TerminalPage::PersistState() - { - // This method may be called for a window even if it hasn't had a tab yet or lost all of them. - // We shouldn't persist such windows. - const auto tabCount = _tabs.Size(); - if (_startupState != StartupState::Initialized || tabCount == 0) - { - return; - } - - std::vector actions; - - for (auto tab : _tabs) - { - auto t = winrt::get_self(tab); - auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist); - actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); - } - - // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. - if (actions.empty()) - { - return; - } - - // if the focused tab was not the last tab, restore that - auto idx = _GetFocusedTabIndex(); - if (idx && idx != tabCount - 1) - { - ActionAndArgs action; - action.Action(ShortcutAction::SwitchToTab); - SwitchToTabArgs switchToTabArgs{ idx.value() }; - action.Args(switchToTabArgs); - - actions.emplace_back(std::move(action)); - } - - // If the user set a custom name, save it - if (const auto& windowName{ _WindowProperties.WindowName() }; !windowName.empty()) - { - ActionAndArgs action; - action.Action(ShortcutAction::RenameWindow); - RenameWindowArgs args{ windowName }; - action.Args(args); - - actions.emplace_back(std::move(action)); - } - - WindowLayout layout; - layout.TabLayout(winrt::single_threaded_vector(std::move(actions))); - - auto mode = LaunchMode::DefaultMode; - WI_SetFlagIf(mode, LaunchMode::FullscreenMode, _isFullscreen); - WI_SetFlagIf(mode, LaunchMode::FocusMode, _isInFocusMode); - WI_SetFlagIf(mode, LaunchMode::MaximizedMode, _isMaximized); - - layout.LaunchMode({ mode }); - - // Only save the content size because the tab size will be added on load. - const auto contentWidth = static_cast(_tabContent.ActualWidth()); - const auto contentHeight = static_cast(_tabContent.ActualHeight()); - const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; - - layout.InitialSize(windowSize); - - // We don't actually know our own position. So we have to ask the window - // layer for that. - const auto launchPosRequest{ winrt::make() }; - RequestLaunchPosition.raise(*this, launchPosRequest); - layout.InitialPosition(launchPosRequest.Position()); - - ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); - } - - // Method Description: - // - Close the terminal app. If there is more - // than one tab opened, show a warning dialog. - safe_void_coroutine TerminalPage::CloseWindow() - { - if (_HasMultipleTabs() && - _settings.GlobalSettings().ConfirmCloseAllTabs() && - !_displayingCloseDialog) - { - if (_newTabButton && _newTabButton.Flyout()) - { - _newTabButton.Flyout().Hide(); - } - _DismissTabContextMenus(); - _displayingCloseDialog = true; - auto warningResult = co_await _ShowCloseWarningDialog(); - _displayingCloseDialog = false; - - if (warningResult != ContentDialogResult::Primary) - { - co_return; - } - } - - CloseWindowRequested.raise(*this, nullptr); - } - - std::vector TerminalPage::Panes() const - { - std::vector panes; - - for (const auto tab : _tabs) - { - const auto impl = _GetTabImpl(tab); - if (!impl) - { - continue; - } - - impl->GetRootPane()->WalkTree([&](auto&& pane) { - if (auto content = pane->GetContent()) - { - panes.push_back(std::move(content)); - } - }); - } - - return panes; - } - - // Method Description: - // - Move the viewport of the terminal of the currently focused tab up or - // down a number of lines. - // Arguments: - // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down - // - rowsToScroll: a number of lines to move the viewport. If not provided we will use a system default. - void TerminalPage::_Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - uint32_t realRowsToScroll; - if (rowsToScroll == nullptr) - { - // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page - realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? - tabImpl->GetActiveTerminalControl().ViewHeight() : - _systemRowsToScroll; - } - else - { - // use the custom value specified in the command - realRowsToScroll = rowsToScroll.Value(); - } - auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); - tabImpl->Scroll(scrollDelta); - } - } - - // Method Description: - // - Moves the currently active pane on the currently active tab to the - // specified tab. If the tab index is greater than the number of - // tabs, then a new tab will be created for the pane. Similarly, if a pane - // is the last remaining pane on a tab, that tab will be closed upon moving. - // - No move will occur if the tabIdx is the same as the current tab, or if - // the specified tab is not a host of terminals (such as the settings tab). - // - If the Window is specified, the pane will instead be detached and moved - // to the window with the given name/id. - // Return Value: - // - true if the pane was successfully moved to the new tab. - bool TerminalPage::_MovePane(MovePaneArgs args) - { - const auto tabIdx{ args.TabIndex() }; - const auto windowId{ args.Window() }; - - auto focusedTab{ _GetFocusedTabImpl() }; - - if (!focusedTab) - { - return false; - } - - // If there was a windowId in the action, try to move it to the - // specified window instead of moving it in our tab row. - if (!windowId.empty()) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto pane{ tabImpl->GetActivePane() }) - { - auto startupActions = pane->BuildStartupActions(0, 1, BuildStartupKind::MovePane); - _DetachPaneFromWindow(pane); - _MoveContent(std::move(startupActions.args), windowId, tabIdx); - focusedTab->DetachPane(); - - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - if (windowId == L"new") - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_(L"TerminalPage_PaneMovedAnnouncement_NewWindow"), - L"TerminalPageMovePaneToNewWindow" /* unique name for this notification category */); - } - else - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow2", windowId), - L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); - } - } - return true; - } - } - } - - // If we are trying to move from the current tab to the current tab do nothing. - if (_GetFocusedTabIndex() == tabIdx) - { - return false; - } - - // Moving the pane from the current tab might close it, so get the next - // tab before its index changes. - if (tabIdx < _tabs.Size()) - { - auto targetTab = _GetTabImpl(_tabs.GetAt(tabIdx)); - // if the selected tab is not a host of terminals (e.g. settings) - // don't attempt to add a pane to it. - if (!targetTab) - { - return false; - } - auto pane = focusedTab->DetachPane(); - targetTab->AttachPane(pane); - _SetFocusedTab(*targetTab); - - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - const auto tabTitle = targetTab->Title(); - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingTab", tabTitle), - L"TerminalPageMovePaneToExistingTab" /* unique name for this notification category */); - } - } - else - { - auto pane = focusedTab->DetachPane(); - _CreateNewTabFromPane(pane); - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_(L"TerminalPage_PaneMovedAnnouncement_NewTab"), - L"TerminalPageMovePaneToNewTab" /* unique name for this notification category */); - } - } - - return true; - } - - // Detach a tree of panes from this terminal. Helper used for moving panes - // and tabs to other windows. - void TerminalPage::_DetachPaneFromWindow(std::shared_ptr pane) - { - pane->WalkTree([&](auto p) { - if (const auto& control{ p->GetTerminalControl() }) - { - _manager.Detach(control); - } - }); - } - - void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr& tab) - { - // Detach the root pane, which will act like the whole tab got detached. - if (const auto rootPane = tab->GetRootPane()) - { - _DetachPaneFromWindow(rootPane); - } - } - - // Method Description: - // - Serialize these actions to json, and raise them as a RequestMoveContent - // event. Our Window will raise that to the window manager / monarch, who - // will dispatch this blob of json back to the window that should handle - // this. - // - `actions` will be emptied into a winrt IVector as a part of this method - // and should be expected to be empty after this call. - void TerminalPage::_MoveContent(std::vector&& actions, - const winrt::hstring& windowName, - const uint32_t tabIndex, - const std::optional& dragPoint) - { - const auto winRtActions{ winrt::single_threaded_vector(std::move(actions)) }; - const auto str{ ActionAndArgs::Serialize(winRtActions) }; - const auto request = winrt::make_self(windowName, - str, - tabIndex); - if (dragPoint.has_value()) - { - request->WindowPosition(*dragPoint); - } - RequestMoveContent.raise(*this, *request); - } - - bool TerminalPage::_MoveTab(winrt::com_ptr tab, MoveTabArgs args) - { - if (!tab) - { - return false; - } - - // If there was a windowId in the action, try to move it to the - // specified window instead of moving it in our tab row. - const auto windowId{ args.Window() }; - if (!windowId.empty()) - { - // if the windowId is the same as our name, do nothing - if (windowId == WindowProperties().WindowName() || - windowId == winrt::to_hstring(WindowProperties().WindowId())) - { - return true; - } - - if (tab) - { - auto startupActions = tab->BuildStartupActions(BuildStartupKind::Content); - _DetachTabFromWindow(tab); - _MoveContent(std::move(startupActions), windowId, 0); - _RemoveTab(*tab); - if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) - { - const auto tabTitle = tab->Title(); - if (windowId == L"new") - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_TabMovedAnnouncement_NewWindow", tabTitle), - L"TerminalPageMoveTabToNewWindow" /* unique name for this notification category */); - } - else - { - autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, - Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, - RS_fmt(L"TerminalPage_TabMovedAnnouncement_Default", tabTitle, windowId), - L"TerminalPageMoveTabToExistingWindow" /* unique name for this notification category */); - } - } - return true; - } - } - - const auto direction = args.Direction(); - if (direction != MoveTabDirection::None) - { - // Use the requested tab, if provided. Otherwise, use the currently - // focused tab. - const auto tabIndex = til::coalesce(_GetTabIndex(*tab), - _GetFocusedTabIndex()); - if (tabIndex) - { - const auto currentTabIndex = tabIndex.value(); - const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; - _TryMoveTab(currentTabIndex, currentTabIndex + delta); - } - } - - return true; - } - - // 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. - void TerminalPage::_activePaneChanged(winrt::TerminalApp::Tab sender, - Windows::Foundation::IInspectable /*args*/) - { - if (const auto tab{ _GetTabImpl(sender) }) - { - // Possibly update the icon of the tab. - _UpdateTabIcon(*tab); - - _updateThemeColors(); - - // Update the taskbar progress as well. We'll raise our own - // SetTaskbarProgress event here, to get tell the hosting - // application to re-query this value from us. - SetTaskbarProgress.raise(*this, nullptr); - - auto profile = tab->GetFocusedProfile(); - _UpdateBackground(profile); - } - - _adjustProcessPriorityThrottled->Run(); - } - - uint32_t TerminalPage::NumberOfTabs() const - { - return _tabs.Size(); - } - - // Method Description: - // - Called when it is determined that an existing tab or pane should be - // attached to our window. content represents a blob of JSON describing - // some startup actions for rebuilding the specified panes. They will - // include `__content` properties with the GUID of the existing - // ControlInteractivity's we should use, rather than starting new ones. - // - _MakePane is already enlightened to use the ContentId property to - // reattach instead of create new content, so this method simply needs to - // parse the JSON and pump it into our action handler. Almost the same as - // doing something like `wt -w 0 nt`. - void TerminalPage::AttachContent(IVector args, uint32_t tabIndex) - { - if (args == nullptr || - args.Size() == 0) - { - return; - } - - std::vector existingTabs{}; - existingTabs.reserve(_tabs.Size()); - for (const auto& tab : _tabs) - { - existingTabs.emplace_back(tab); - } - - const auto& firstAction = args.GetAt(0); - const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; - - // `splitPane` allows the user to specify which tab to split. In that - // case, split specifically the requested pane. - // - // If there's not enough tabs, then just turn this pane into a new tab. - // - // If the first action is `newTab`, the index is always going to be 0, - // so don't do anything in that case. - if (firstIsSplitPane && tabIndex < _tabs.Size()) - { - _SelectTab(tabIndex); - } - - for (const auto& action : args) - { - _actionDispatch->DoAction(action); - } - - // After handling all the actions, then re-check the tabIndex. We might - // have been called as a part of a tab drag/drop. In that case, the - // tabIndex is actually relevant, and we need to move the tab we just - // made into position. - if (!firstIsSplitPane && tabIndex != -1) - { - const auto newTabs = _CollectNewTabs(existingTabs); - if (!newTabs.empty()) - { - _MoveTabsToIndex(newTabs, tabIndex); - _SetSelectedTabs(newTabs, newTabs.front()); - } - } - } - - // Method Description: - // - Split the focused pane of the given tab, either horizontally or vertically, and place the - // given pane accordingly - // Arguments: - // - tab: The tab that is going to be split. - // - newPane: the pane to add to our tree of panes - // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the - // new pane should be split from its parent. - // - splitSize: the size of the split - void TerminalPage::_SplitPane(const winrt::com_ptr& tab, - const SplitDirection splitDirection, - const float splitSize, - std::shared_ptr newPane) - { - auto activeTab = tab; - // Clever hack for a crash in startup, with multiple sub-commands. Say - // you have the following commandline: - // - // wtd nt -p "elevated cmd" ; sp -p "elevated cmd" ; sp -p "Command Prompt" - // - // Where "elevated cmd" is an elevated profile. - // - // In that scenario, we won't dump off the commandline immediately to an - // elevated window, because it's got the final unelevated split in it. - // However, when we get to that command, there won't be a tab yet. So - // we'd crash right about here. - // - // Instead, let's just promote this first split to be a tab instead. - // Crash avoided, and we don't need to worry about inserting a new-tab - // command in at the start. - if (!tab) - { - if (_tabs.Size() == 0) - { - _CreateNewTabFromPane(newPane); - return; - } - else - { - activeTab = _GetFocusedTabImpl(); - } - } - - // For now, prevent splitting the _settingsTab. We can always revisit this later. - if (*activeTab == _settingsTab) - { - return; - } - - // If the caller is calling us with the return value of _MakePane - // directly, it's possible that nullptr was returned, if the connections - // was supposed to be launched in an elevated window. In that case, do - // nothing here. We don't have a pane with which to create the split. - if (!newPane) - { - return; - } - const auto contentWidth = static_cast(_tabContent.ActualWidth()); - const auto contentHeight = static_cast(_tabContent.ActualHeight()); - const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; - - const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); - if (!realSplitType) - { - return; - } - - _UnZoomIfNeeded(); - auto [original, newGuy] = activeTab->SplitPane(*realSplitType, splitSize, newPane); - - // After GH#6586, the control will no longer focus itself - // automatically when it's finished being laid out. Manually focus - // the control here instead. - if (_startupState == StartupState::Initialized) - { - if (const auto& content{ newGuy->GetContent() }) - { - content.Focus(FocusState::Programmatic); - } - } - } - - // Method Description: - // - Switches the split orientation of the currently focused pane. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_ToggleSplitOrientation() - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - tabImpl->ToggleSplitOrientation(); - } - } - - // Method Description: - // - Attempt to move a separator between panes, as to resize each child on - // either size of the separator. See Pane::ResizePane for details. - // - Moves a separator on the currently focused tab. - // Arguments: - // - direction: The direction to move the separator in. - // Return Value: - // - - void TerminalPage::_ResizePane(const ResizeDirection& direction) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - _UnZoomIfNeeded(); - tabImpl->ResizePane(direction); - } - } - - // Method Description: - // - Move the viewport of the terminal of the currently focused tab up or - // down a page. The page length will be dependent on the terminal view height. - // Arguments: - // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down - void TerminalPage::_ScrollPage(ScrollDirection scrollDirection) - { - // Do nothing if for some reason, there's no terminal tab in focus. We don't want to crash. - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - if (const auto& control{ _GetActiveControl() }) - { - const auto termHeight = control.ViewHeight(); - auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); - tabImpl->Scroll(scrollDelta); - } - } - } - - void TerminalPage::_ScrollToBufferEdge(ScrollDirection scrollDirection) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - auto scrollDelta = _ComputeScrollDelta(scrollDirection, INT_MAX); - tabImpl->Scroll(scrollDelta); - } - } - - // Method Description: - // - Gets the title of the currently focused terminal control. If there - // isn't a control selected for any reason, returns "Terminal" - // Arguments: - // - - // Return Value: - // - the title of the focused control if there is one, else "Terminal" - hstring TerminalPage::Title() - { - if (_settings.GlobalSettings().ShowTitleInTitlebar()) - { - if (const auto tab{ _GetFocusedTab() }) - { - return tab.Title(); - } - } - return { L"Terminal" }; - } - - // Method Description: - // - Handles the special case of providing a text override for the UI shortcut due to VK_OEM issue. - // Looks at the flags from the KeyChord modifiers and provides a concatenated string value of all - // in the same order that XAML would put them as well. - // Return Value: - // - a string representation of the key modifiers for the shortcut - //NOTE: This needs to be localized with https://github.com/microsoft/terminal/issues/794 if XAML framework issue not resolved before then - static std::wstring _FormatOverrideShortcutText(VirtualKeyModifiers modifiers) - { - std::wstring buffer{ L"" }; - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)) - { - buffer += L"Ctrl+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)) - { - buffer += L"Shift+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)) - { - buffer += L"Alt+"; - } - - if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)) - { - buffer += L"Win+"; - } - - return buffer; - } - - // Method Description: - // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. - // Takes into account a special case for an error condition for a comma - // Arguments: - // - MenuFlyoutItem that will be displayed, and a KeyChord to map an accelerator - void TerminalPage::_SetAcceleratorForMenuItem(WUX::Controls::MenuFlyoutItem& menuItem, - const KeyChord& keyChord) - { -#ifdef DEP_MICROSOFT_UI_XAML_708_FIXED - // work around https://github.com/microsoft/microsoft-ui-xaml/issues/708 in case of VK_OEM_COMMA - if (keyChord.Vkey() != VK_OEM_COMMA) - { - // use the XAML shortcut to give us the automatic capabilities - auto menuShortcut = Windows::UI::Xaml::Input::KeyboardAccelerator{}; - - // TODO: Modify this when https://github.com/microsoft/terminal/issues/877 is resolved - menuShortcut.Key(static_cast(keyChord.Vkey())); - - // add the modifiers to the shortcut - menuShortcut.Modifiers(keyChord.Modifiers()); - - // add to the menu - menuItem.KeyboardAccelerators().Append(menuShortcut); - } - else // we've got a comma, so need to just use the alternate method -#endif - { - // extract the modifier and key to a nice format - auto overrideString = _FormatOverrideShortcutText(keyChord.Modifiers()); - auto mappedCh = MapVirtualKeyW(keyChord.Vkey(), MAPVK_VK_TO_CHAR); - if (mappedCh != 0) - { - menuItem.KeyboardAcceleratorTextOverride(overrideString + gsl::narrow_cast(mappedCh)); - } - } - } - - // Method Description: - // - Calculates the appropriate size to snap to in the given direction, for - // the given dimension. If the global setting `snapToGridOnResize` is set - // to `false`, this will just immediately return the provided dimension, - // effectively disabling snapping. - // - See Pane::CalcSnappedDimension - float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const - { - if (_settings && _settings.GlobalSettings().SnapToGridOnResize()) - { - if (const auto tabImpl{ _GetFocusedTabImpl() }) - { - return tabImpl->CalcSnappedDimension(widthOrHeight, dimension); - } - } - return dimension; - } - - // Function Description: - // - This function is called when the `TermControl` requests that we send - // it the clipboard's content. - // - Retrieves the data from the Windows Clipboard and converts it to text. - // - Shows warnings if the clipboard is too big or contains multiple lines - // of text. - // - Sends the text back to the TermControl through the event's - // `HandleClipboardData` member function. - // - Does some of this in a background thread, as to not hang/crash the UI thread. - // Arguments: - // - eventArgs: the PasteFromClipboard event sent from the TermControl - safe_void_coroutine TerminalPage::_PasteFromClipboardHandler(const IInspectable sender, const PasteFromClipboardEventArgs eventArgs) - try - { - // The old Win32 clipboard API as used below is somewhere in the order of 300-1000x faster than - // the WinRT one on average, depending on CPU load. Don't use the WinRT clipboard API if you can. - const auto weakThis = get_weak(); - const auto dispatcher = Dispatcher(); - const auto globalSettings = _settings.GlobalSettings(); - const auto bracketedPaste = eventArgs.BracketedPasteEnabled(); - const auto sourceId = sender.try_as().Id(); - - // GetClipboardData might block for up to 30s for delay-rendered contents. - co_await winrt::resume_background(); - - winrt::hstring text; - if (const auto clipboard = clipboard::open(nullptr)) - { - text = clipboard::read(); - } - - if (!bracketedPaste && globalSettings.TrimPaste()) - { - text = winrt::hstring{ Utils::TrimPaste(text) }; - } - - if (text.empty()) - { - co_return; - } - - bool warnMultiLine = false; - switch (globalSettings.WarnAboutMultiLinePaste()) - { - case WarnAboutMultiLinePaste::Automatic: - // NOTE that this is unsafe, because a shell that doesn't support bracketed paste - // will allow an attacker to enable the mode, not realize that, and then accept - // the paste as if it was a series of legitimate commands. See GH#13014. - warnMultiLine = !bracketedPaste; - break; - case WarnAboutMultiLinePaste::Always: - warnMultiLine = true; - break; - default: - warnMultiLine = false; - break; - } - - if (warnMultiLine) - { - const std::wstring_view view{ text }; - warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos; - } - - constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB - const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); - - if (warnMultiLine || warnLargeText) - { - co_await wil::resume_foreground(dispatcher); - - if (const auto strongThis = weakThis.get()) - { - // We have to initialize the dialog here to be able to change the text of the text block within it - std::ignore = FindName(L"MultiLinePasteDialog"); - - // WinUI absolutely cannot deal with large amounts of text (at least O(n), possibly O(n^2), - // so we limit the string length here and add an ellipsis if necessary. - auto clipboardText = text; - if (clipboardText.size() > 1024) - { - const std::wstring_view view{ text }; - // Make sure we don't cut in the middle of a surrogate pair - const auto len = til::utf16_iterate_prev(view, 512); - clipboardText = til::hstring_format(FMT_COMPILE(L"{}\n…"), view.substr(0, len)); - } - - ClipboardText().Text(std::move(clipboardText)); - - // The vertical offset on the scrollbar does not reset automatically, so reset it manually - ClipboardContentScrollViewer().ScrollToVerticalOffset(0); - - auto warningResult = ContentDialogResult::Primary; - if (warnMultiLine) - { - warningResult = co_await _ShowMultiLinePasteWarningDialog(); - } - else if (warnLargeText) - { - warningResult = co_await _ShowLargePasteWarningDialog(); - } - - // Clear the clipboard text so it doesn't lie around in memory - ClipboardText().Text({}); - - if (warningResult != ContentDialogResult::Primary) - { - // user rejected the paste - co_return; - } - } - - co_await winrt::resume_background(); - } - - // This will end up calling ConptyConnection::WriteInput which calls WriteFile which may block for - // an indefinite amount of time. Avoid freezes and deadlocks by running this on a background thread. - assert(!dispatcher.HasThreadAccess()); - eventArgs.HandleClipboardData(text); - - // GH#18821: If broadcast input is active, paste the same text into all other - // panes on the tab. We do this here (rather than re-reading the - // clipboard per-pane) so that only one paste warning is shown. - co_await wil::resume_foreground(dispatcher); - if (const auto strongThis = weakThis.get()) - { - if (const auto& tab{ strongThis->_GetFocusedTabImpl() }) - { - if (tab->TabStatus().IsInputBroadcastActive()) - { - tab->GetRootPane()->WalkTree([&](auto&& pane) { - if (const auto control = pane->GetTerminalControl()) - { - if (control.ContentId() != sourceId && !control.ReadOnly()) - { - control.RawWriteString(text); - } - } - }); - } - } - } - } - CATCH_LOG(); - - void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs) - { - try - { - auto parsed = winrt::Windows::Foundation::Uri(eventArgs.Uri()); - if (_IsUriSupported(parsed)) - { - ShellExecute(nullptr, L"open", eventArgs.Uri().c_str(), nullptr, nullptr, SW_SHOWNORMAL); - } - else - { - _ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), eventArgs.Uri()); - } - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - _ShowCouldNotOpenDialog(RS_(L"InvalidUriText"), eventArgs.Uri()); - } - } - - // Method Description: - // - Opens up a dialog box explaining why we could not open a URI - // Arguments: - // - The reason (unsupported scheme, invalid uri, potentially more in the future) - // - The uri - void TerminalPage::_ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri) - { - if (auto presenter{ _dialogPresenter.get() }) - { - // FindName needs to be called first to actually load the xaml object - auto unopenedUriDialog = FindName(L"CouldNotOpenUriDialog").try_as(); - - // Insert the reason and the URI - CouldNotOpenUriReason().Text(reason); - UnopenedUri().Text(uri); - - // Show the dialog - presenter.ShowDialog(unopenedUriDialog); - } - } - - // Method Description: - // - Determines if the given URI is currently supported - // Arguments: - // - The parsed URI - // Return value: - // - True if we support it, false otherwise - bool TerminalPage::_IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri) - { - if (parsedUri.SchemeName() == L"http" || parsedUri.SchemeName() == L"https") - { - return true; - } - if (parsedUri.SchemeName() == L"file") - { - const auto host = parsedUri.Host(); - // If no hostname was provided or if the hostname was "localhost", Host() will return an empty string - // and we allow it - if (host == L"") - { - return true; - } - - // GH#10188: WSL paths are okay. We'll let those through. - if (host == L"wsl$" || host == L"wsl.localhost") - { - return true; - } - - // TODO: by the OSC 8 spec, if a hostname (other than localhost) is provided, we _should_ be - // comparing that value against what is returned by GetComputerNameExW and making sure they match. - // However, ShellExecute does not seem to be happy with file URIs of the form - // file://{hostname}/path/to/file.ext - // and so while we could do the hostname matching, we do not know how to actually open the URI - // if its given in that form. So for now we ignore all hostnames other than localhost - return false; - } - - // In this case, the app manually output a URI other than file:// or - // http(s)://. We'll trust the user knows what they're doing when - // clicking on those sorts of links. - // See discussion in GH#7562 for more details. - return true; - } - - // Important! Don't take this eventArgs by reference, we need to extend the - // lifetime of it to the other side of the co_await! - safe_void_coroutine TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, - const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) - { - auto weakThis = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - if (auto page = weakThis.get()) - { - auto message = eventArgs.Message(); - - winrt::hstring title; - - switch (eventArgs.Level()) - { - case NoticeLevel::Debug: - title = RS_(L"NoticeDebug"); //\xebe8 - break; - case NoticeLevel::Info: - title = RS_(L"NoticeInfo"); // \xe946 - break; - case NoticeLevel::Warning: - title = RS_(L"NoticeWarning"); //\xe7ba - break; - case NoticeLevel::Error: - title = RS_(L"NoticeError"); //\xe783 - break; - } - - page->_ShowControlNoticeDialog(title, message); - } - } - - void TerminalPage::_ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message) - { - if (auto presenter{ _dialogPresenter.get() }) - { - // FindName needs to be called first to actually load the xaml object - auto controlNoticeDialog = FindName(L"ControlNoticeDialog").try_as(); - - ControlNoticeDialog().Title(winrt::box_value(title)); - - // Insert the message - NoticeMessage().Text(message); - - // Show the dialog - presenter.ShowDialog(controlNoticeDialog); - } - } - - // Method Description: - // - Copy text from the focused terminal to the Windows Clipboard - // Arguments: - // - dismissSelection: if not enabled, copying text doesn't dismiss the selection - // - singleLine: if enabled, copy contents as a single line of text - // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection - // - formats: dictate which formats need to be copied - // Return Value: - // - true iff we we able to copy text (if a selection was active) - bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats) - { - if (const auto& control{ _GetActiveControl() }) - { - return control.CopySelectionToClipboard(dismissSelection, singleLine, withControlSequences, formats); - } - return false; - } - - // Method Description: - // - Send an event (which will be caught by AppHost) to set the progress indicator on the taskbar - // Arguments: - // - sender (not used) - // - eventArgs: the arguments specifying how to set the progress indicator - safe_void_coroutine TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) - { - const auto weak = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - if (const auto strong = weak.get()) - { - SetTaskbarProgress.raise(*this, nullptr); - } - } - - // Method Description: - // - Send an event (which will be caught by AppHost) to change the show window state of the entire hosting window - // Arguments: - // - sender (not used) - // - args: the arguments specifying how to set the display status to ShowWindow for our window handle - void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) - { - ShowWindowChanged.raise(*this, args); - } - - Windows::Foundation::IAsyncOperation> TerminalPage::_FindPackageAsync(hstring query) - { - const PackageManager packageManager = WindowsPackageManagerFactory::CreatePackageManager(); - PackageCatalogReference catalogRef{ - packageManager.GetPredefinedPackageCatalog(PredefinedPackageCatalog::OpenWindowsCatalog) - }; - catalogRef.PackageCatalogBackgroundUpdateInterval(std::chrono::hours(24)); - - ConnectResult connectResult{ nullptr }; - for (int retries = 0;;) - { - connectResult = catalogRef.Connect(); - if (connectResult.Status() == ConnectResultStatus::Ok) - { - break; - } - - if (++retries == 3) - { - co_return nullptr; - } - } - - PackageCatalog catalog = connectResult.PackageCatalog(); - PackageMatchFilter filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); - filter.Value(query); - filter.Field(PackageMatchField::Command); - filter.Option(PackageFieldMatchOption::Equals); - - FindPackagesOptions options = WindowsPackageManagerFactory::CreateFindPackagesOptions(); - options.Filters().Append(filter); - options.ResultLimit(20); - - const auto result = co_await catalog.FindPackagesAsync(options); - const IVectorView pkgList = result.Matches(); - co_return pkgList; - } - - Windows::Foundation::IAsyncAction TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args) - { - if (!Feature_QuickFix::IsEnabled()) - { - co_return; - } - - const auto weak = get_weak(); - const auto dispatcher = Dispatcher(); - - // All of the code until resume_foreground is static and - // doesn't touch `this`, so we don't need weak/strong_ref. - co_await winrt::resume_background(); - - // no packages were found, nothing to suggest - const auto pkgList = co_await _FindPackageAsync(args.MissingCommand()); - if (!pkgList || pkgList.Size() == 0) - { - co_return; - } - - std::vector suggestions; - suggestions.reserve(pkgList.Size()); - for (const auto& pkg : pkgList) - { - // --id and --source ensure we don't collide with another package catalog - suggestions.emplace_back(fmt::format(FMT_COMPILE(L"winget install --id {} -s winget"), pkg.CatalogPackage().Id())); - } - - co_await wil::resume_foreground(dispatcher); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - auto term = _GetActiveControl(); - if (!term) - { - co_return; - } - term.UpdateWinGetSuggestions(single_threaded_vector(std::move(suggestions))); - term.RefreshQuickFixMenu(); - } - - void TerminalPage::_WindowSizeChanged(const IInspectable sender, const Microsoft::Terminal::Control::WindowSizeChangedEventArgs args) - { - // Raise if: - // - Not in quake mode - // - Not in fullscreen - // - Only one tab exists - // - Only one pane exists - // else: - // - Reset conpty to its original size back - if (!WindowProperties().IsQuakeWindow() && !Fullscreen() && - NumberOfTabs() == 1 && _GetFocusedTabImpl()->GetLeafPaneCount() == 1) - { - WindowSizeChanged.raise(*this, args); - } - else if (const auto& control{ sender.try_as() }) - { - const auto& connection = control.Connection(); - - if (const auto& conpty{ connection.try_as() }) - { - conpty.ResetSize(); - } - } - } - - void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const - { - if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) - { - const auto plain = args.Plain(); - const auto html = args.Html(); - const auto rtf = args.Rtf(); - - clipboard::write( - { plain.data(), plain.size() }, - { reinterpret_cast(html.data()), html.size() }, - { reinterpret_cast(rtf.data()), rtf.size() }); - } - } - - // Method Description: - // - Paste text from the Windows Clipboard to the focused terminal - void TerminalPage::_PasteText() - { - if (const auto& control{ _GetActiveControl() }) - { - control.PasteTextFromClipboard(); - } - } - - // Function Description: - // - Called when the settings button is clicked. ShellExecutes the settings - // file, as to open it in the default editor for .json files. Does this in - // a background thread, as to not hang/crash the UI thread. - safe_void_coroutine TerminalPage::_LaunchSettings(const SettingsTarget target) - { - if (target == SettingsTarget::SettingsUI) - { - OpenSettingsUI(); - } - else - { - // This will switch the execution of the function to a background (not - // UI) thread. This is IMPORTANT, because the Windows.Storage API's - // (used for retrieving the path to the file) will crash on the UI - // thread, because the main thread is a STA. - // - // NOTE: All remaining code of this function doesn't touch `this`, so we don't need weak/strong_ref. - // NOTE NOTE: Don't touch `this` when you make changes here. - co_await winrt::resume_background(); - - auto openFile = [](const auto& filePath) { - HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); - if (static_cast(reinterpret_cast(res)) <= 32) - { - ShellExecute(nullptr, nullptr, L"notepad", filePath.c_str(), nullptr, SW_SHOW); - } - }; - - auto openFolder = [](const auto& filePath) { - HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); - if (static_cast(reinterpret_cast(res)) <= 32) - { - ShellExecute(nullptr, nullptr, L"open", filePath.c_str(), nullptr, SW_SHOW); - } - }; - - switch (target) - { - case SettingsTarget::DefaultsFile: - openFile(CascadiaSettings::DefaultSettingsPath()); - break; - case SettingsTarget::SettingsFile: - openFile(CascadiaSettings::SettingsPath()); - break; - case SettingsTarget::Directory: - openFolder(CascadiaSettings::SettingsDirectory()); - break; - case SettingsTarget::AllFiles: - openFile(CascadiaSettings::DefaultSettingsPath()); - openFile(CascadiaSettings::SettingsPath()); - break; - } - } - } - - // Method Description: - // - Responds to the TabView control's Tab Closing event by removing - // the indicated tab from the set and focusing another one. - // The event is cancelled so App maintains control over the - // items in the tabview. - // Arguments: - // - sender: the control that originated this event - // - eventArgs: the event's constituent arguments - void TerminalPage::_OnTabCloseRequested(const IInspectable& /*sender*/, const MUX::Controls::TabViewTabCloseRequestedEventArgs& eventArgs) - { - const auto tabViewItem = eventArgs.Tab(); - if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) - { - _HandleCloseTabRequested(tab); - } - } - - TermControl TerminalPage::_CreateNewControlAndContent(const Settings::TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) - { - // Do any initialization that needs to apply to _every_ TermControl we - // create here. - const auto content = _manager.CreateCore(*settings.DefaultSettings(), settings.UnfocusedSettings().try_as(), connection); - const TermControl control{ content }; - return _SetupControl(control); - } - - TermControl TerminalPage::_AttachControlToContent(const uint64_t& contentId) - { - if (const auto& content{ _manager.TryLookupCore(contentId) }) - { - // We have to pass in our current keybindings, because that's an - // object that belongs to this TerminalPage, on this thread. If we - // don't, then when we move the content to another thread, and it - // tries to handle a key, it'll callback on the original page's - // stack, inevitably resulting in a wrong_thread - return _SetupControl(TermControl::NewControlByAttachingContent(content)); - } - return nullptr; - } - - TermControl TerminalPage::_SetupControl(const TermControl& term) - { - // GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now. - if (_visible) - { - term.WindowVisibilityChanged(_visible); - } - - // Even in the case of re-attaching content from another window, this - // will correctly update the control's owning HWND - if (_hostingHwnd.has_value()) - { - term.OwningHwnd(reinterpret_cast(*_hostingHwnd)); - } - - term.KeyBindings(*_bindings); - - _RegisterTerminalEvents(term); - return term; - } - - // Method Description: - // - Creates a pane and returns a shared_ptr to it - // - The caller should handle where the pane goes after creation, - // either to split an already existing pane or to create a new tab with it - // Arguments: - // - newTerminalArgs: an object that may contain a blob of parameters to - // control which profile is created and with possible other - // configurations. See CascadiaSettings::BuildSettings for more details. - // - sourceTab: an optional tab reference that indicates that the created - // pane should be a duplicate of the tab's focused pane - // - existingConnection: optionally receives a connection from the outside - // world instead of attempting to create one - // Return Value: - // - If the newTerminalArgs required us to open the pane as a new elevated - // connection, then we'll return nullptr. Otherwise, we'll return a new - // Pane for this connection. - std::shared_ptr TerminalPage::_MakeTerminalPane(const NewTerminalArgs& newTerminalArgs, - const winrt::TerminalApp::Tab& sourceTab, - TerminalConnection::ITerminalConnection existingConnection) - { - // First things first - Check for making a pane from content ID. - if (newTerminalArgs && - newTerminalArgs.ContentId() != 0) - { - // Don't need to worry about duplicating or anything - we'll - // serialize the actual profile's GUID along with the content guid. - const auto& profile = _settings.GetProfileForArgs(newTerminalArgs); - const auto control = _AttachControlToContent(newTerminalArgs.ContentId()); - auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; - return std::make_shared(paneContent); - } - - Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; - Profile profile{ nullptr }; - - if (const auto& tabImpl{ _GetTabImpl(sourceTab) }) - { - profile = tabImpl->GetFocusedProfile(); - if (profile) - { - // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. - profile = GetClosestProfileForDuplicationOfProfile(profile); - controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); - const auto workingDirectory = tabImpl->GetActiveTerminalControl().WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) - { - controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); - } - } - } - if (!profile) - { - profile = _settings.GetProfileForArgs(newTerminalArgs); - controlSettings = Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs); - } - - // Try to handle auto-elevation - if (_maybeElevate(newTerminalArgs, controlSettings, profile)) - { - return nullptr; - } - - const auto sessionId = controlSettings.DefaultSettings()->SessionId(); - const auto hasSessionId = sessionId != winrt::guid{}; - - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), hasSessionId); - if (existingConnection) - { - connection.Resize(controlSettings.DefaultSettings()->InitialRows(), controlSettings.DefaultSettings()->InitialCols()); - } - - TerminalConnection::ITerminalConnection debugConnection{ nullptr }; - if (_settings.GlobalSettings().DebugFeaturesEnabled()) - { - const auto window = CoreWindow::GetForCurrentThread(); - const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); - const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); - const auto bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && - WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); - if (bothAltsPressed) - { - std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); - } - } - - const auto control = _CreateNewControlAndContent(controlSettings, connection); - - if (hasSessionId) - { - using namespace std::string_view_literals; - - const auto settingsDir = CascadiaSettings::SettingsDirectory(); - const auto admin = IsRunningElevated(); - const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv; - const auto path = fmt::format(FMT_COMPILE(L"{}\\{}{}.txt"), settingsDir, filenamePrefix, sessionId); - control.RestoreFromPath(path); - } - - auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; - - auto resultPane = std::make_shared(paneContent); - - if (debugConnection) // this will only be set if global debugging is on and tap is active - { - auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection); - // Split (auto) with the debug tap. - auto debugContent{ winrt::make(profile, _terminalSettingsCache, newControl) }; - auto debugPane = std::make_shared(debugContent); - - // Since we're doing this split directly on the pane (instead of going through Tab, - // we need to handle the panes 'active' states - - // Set the pane we're splitting to active (otherwise Split will not do anything) - resultPane->SetActive(); - auto [original, _] = resultPane->Split(SplitDirection::Automatic, 0.5f, debugPane); - - // Set the non-debug pane as active - resultPane->ClearActive(); - original->SetActive(); - } - - return resultPane; - } - - // NOTE: callers of _MakePane should be able to accept nullptr as a return - // value gracefully. - std::shared_ptr TerminalPage::_MakePane(const INewContentArgs& contentArgs, - const winrt::TerminalApp::Tab& sourceTab, - TerminalConnection::ITerminalConnection existingConnection) - - { - const auto& newTerminalArgs{ contentArgs.try_as() }; - if (contentArgs == nullptr || newTerminalArgs != nullptr || contentArgs.Type().empty()) - { - // Terminals are of course special, and have to deal with debug taps, duplicating the tab, etc. - return _MakeTerminalPane(newTerminalArgs, sourceTab, existingConnection); - } - - IPaneContent content{ nullptr }; - - const auto& paneType{ contentArgs.Type() }; - if (paneType == L"scratchpad") - { - const auto& scratchPane{ winrt::make_self() }; - - // This is maybe a little wacky - add our key event handler to the pane - // we made. So that we can get actions for keys that the content didn't - // handle. - scratchPane->GetRoot().KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); - - content = *scratchPane; - } - else if (paneType == L"settings") - { - content = _makeSettingsContent(); - } - else if (paneType == L"snippets") - { - // Prevent the user from opening a bunch of snippets panes. - // - // Look at the focused tab, and if it already has one, then just focus it. - if (const auto& focusedTab{ _GetFocusedTabImpl() }) - { - const auto rootPane{ focusedTab->GetRootPane() }; - const bool found = rootPane == nullptr ? false : rootPane->WalkTree([](const auto& p) -> bool { - if (const auto& snippets{ p->GetContent().try_as() }) - { - snippets->Focus(FocusState::Programmatic); - return true; - } - return false; - }); - // Bail out if we already found one. - if (found) - { - return nullptr; - } - } - - const auto& tasksContent{ winrt::make_self() }; - tasksContent->UpdateSettings(_settings); - tasksContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); - tasksContent->DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); - if (const auto& termControl{ _GetActiveControl() }) - { - tasksContent->SetLastActiveControl(termControl); - } - - content = *tasksContent; - } - else if (paneType == L"x-markdown") - { - if (Feature_MarkdownPane::IsEnabled()) - { - const auto& markdownContent{ winrt::make_self(L"") }; - markdownContent->UpdateSettings(_settings); - markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); - - // This one doesn't use DispatchCommand, because we don't create - // Command's freely at runtime like we do with just plain old actions. - markdownContent->DispatchActionRequested([weak = get_weak()](const auto& sender, const auto& actionAndArgs) { - if (const auto& page{ weak.get() }) - { - page->_actionDispatch->DoAction(sender, actionAndArgs); - } - }); - if (const auto& termControl{ _GetActiveControl() }) - { - markdownContent->SetLastActiveControl(termControl); - } - - content = *markdownContent; - } - } - - assert(content); - - return std::make_shared(content); - } - - void TerminalPage::_restartPaneConnection( - const TerminalApp::TerminalPaneContent& paneContent, - const winrt::Windows::Foundation::IInspectable&) - { - // Note: callers are likely passing in `nullptr` as the args here, as - // the TermControl.RestartTerminalRequested event doesn't actually pass - // any args upwards itself. If we ever change this, make sure you check - // for nulls - if (const auto& connection{ _duplicateConnectionForRestart(paneContent) }) - { - paneContent.GetTermControl().Connection(connection); - connection.Start(); - } - } - - // Method Description: - // - Sets background image and applies its settings (stretch, opacity and alignment) - // - Checks path validity - // Arguments: - // - newAppearance - // Return Value: - // - - void TerminalPage::_SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance) - { - if (!_settings.GlobalSettings().UseBackgroundImageForWindow()) - { - _tabContent.Background(nullptr); - return; - } - - const auto path = newAppearance.BackgroundImagePath().Resolved(); - if (path.empty()) - { - _tabContent.Background(nullptr); - return; - } - - Windows::Foundation::Uri imageUri{ nullptr }; - try - { - imageUri = Windows::Foundation::Uri{ path }; - } - catch (...) - { - LOG_CAUGHT_EXCEPTION(); - _tabContent.Background(nullptr); - return; - } - // Check if the image brush is already pointing to the image - // in the modified settings; if it isn't (or isn't there), - // set a new image source for the brush - - auto brush = _tabContent.Background().try_as(); - Media::Imaging::BitmapImage imageSource = brush == nullptr ? nullptr : brush.ImageSource().try_as(); - - if (imageSource == nullptr || - imageSource.UriSource() == nullptr || - !imageSource.UriSource().Equals(imageUri)) - { - Media::ImageBrush b{}; - // Note that BitmapImage handles the image load asynchronously, - // which is especially important since the image - // may well be both large and somewhere out on the - // internet. - Media::Imaging::BitmapImage image(imageUri); - b.ImageSource(image); - _tabContent.Background(b); - } - - // Pull this into a separate block. If the image didn't change, but the - // properties of the image did, we should still update them. - if (const auto newBrush{ _tabContent.Background().try_as() }) - { - newBrush.Stretch(newAppearance.BackgroundImageStretchMode()); - newBrush.Opacity(newAppearance.BackgroundImageOpacity()); - } - } - - // Method Description: - // - Hook up keybindings, and refresh the UI of the terminal. - // This includes update the settings of all the tabs according - // to their profiles, update the title and icon of each tab, and - // finally create the tab flyout - void TerminalPage::_RefreshUIForSettingsReload() - { - // Re-wire the keybindings to their handlers, as we'll have created a - // new AppKeyBindings object. - _HookupKeyBindings(_settings.ActionMap()); - - // Refresh UI elements - - // Recreate the TerminalSettings cache here. We'll use that as we're - // updating terminal panes, so that we don't have to build a _new_ - // TerminalSettings for every profile we update - we can just look them - // up the previous ones we built. - _terminalSettingsCache->Reset(_settings); - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // Let the tab know that there are new settings. It's up to each content to decide what to do with them. - tabImpl->UpdateSettings(_settings); - - // Update the icon of the tab for the currently focused profile in that tab. - // Only do this for TerminalTabs. Other types of tabs won't have multiple panes - // and profiles so the Title and Icon will be set once and only once on init. - _UpdateTabIcon(*tabImpl); - - // Force the TerminalTab to re-grab its currently active control's title. - tabImpl->UpdateTitle(); - } - - auto tabImpl{ winrt::get_self(tab) }; - tabImpl->SetActionMap(_settings.ActionMap()); - } - - if (const auto focusedTab{ _GetFocusedTabImpl() }) - { - if (const auto profile{ focusedTab->GetFocusedProfile() }) - { - _SetBackgroundImage(profile.DefaultAppearance()); - } - } - - // repopulate the new tab button's flyout with entries for each - // profile, which might have changed - _UpdateTabWidthMode(); - _CreateNewTabFlyout(); - - // Reload the current value of alwaysOnTop from the settings file. This - // will let the user hot-reload this setting, but any runtime changes to - // the alwaysOnTop setting will be lost. - _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); - AlwaysOnTopChanged.raise(*this, nullptr); - - _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); - - // Settings AllowDependentAnimations will affect whether animations are - // enabled application-wide, so we don't need to check it each time we - // want to create an animation. - WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); - - _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); - - Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; - _tabView.Background(transparent); - - //////////////////////////////////////////////////////////////////////// - // Begin Theme handling - _updateThemeColors(); - - _updateAllTabCloseButtons(); - - // The user may have changed the "show title in titlebar" setting. - TitleChanged.raise(*this, nullptr); - } - - void TerminalPage::_updateAllTabCloseButtons() - { - // Update the state of the CloseButtonOverlayMode property of - // our TabView, to match the tab.showCloseButton property in the theme. - // - // Also update every tab's individual IsClosable to match the same property. - const auto theme = _settings.GlobalSettings().CurrentTheme(); - const auto visibility = (theme && theme.Tab()) ? - theme.Tab().ShowCloseButton() : - Settings::Model::TabCloseButtonVisibility::Always; - - _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; - - for (const auto& tab : _tabs) - { - tab.CloseButtonVisibility(visibility); - } - - switch (visibility) - { - case Settings::Model::TabCloseButtonVisibility::Never: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); - break; - case Settings::Model::TabCloseButtonVisibility::Hover: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); - break; - case Settings::Model::TabCloseButtonVisibility::ActiveOnly: - default: - _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); - break; - } - } - - // Method Description: - // - Sets the initial actions to process on startup. We'll make a copy of - // this list, and process these actions when we're loaded. - // - This function will have no effective result after Create() is called. - // Arguments: - // - actions: a list of Actions to process on startup. - // Return Value: - // - - void TerminalPage::SetStartupActions(std::vector actions) - { - _startupActions = std::move(actions); - } - - void TerminalPage::SetStartupConnection(ITerminalConnection connection) - { - _startupConnection = std::move(connection); - } - - winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const - { - return _dialogPresenter.get(); - } - - void TerminalPage::DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter) - { - _dialogPresenter = dialogPresenter; - } - - // Method Description: - // - Get the combined taskbar state for the page. This is the combination of - // all the states of all the tabs, which are themselves a combination of - // all their panes. Taskbar states are given a priority based on the rules - // in: - // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate - // under "How the Taskbar Button Chooses the Progress Indicator for a Group" - // Arguments: - // - - // Return Value: - // - A TaskbarState object representing the combined taskbar state and - // progress percentage of all our tabs. - winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const - { - auto state{ winrt::make() }; - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - auto tabState{ tabImpl->GetCombinedTaskbarState() }; - // lowest priority wins - if (tabState.Priority() < state.Priority()) - { - state = tabState; - } - } - } - - return state; - } - - // Method Description: - // - This is the method that App will call when the titlebar - // has been clicked. It dismisses any open flyouts. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::TitlebarClicked() - { - if (_newTabButton && _newTabButton.Flyout()) - { - _newTabButton.Flyout().Hide(); - } - _DismissTabContextMenus(); - } - - // Method Description: - // - Notifies all attached console controls that the visibility of the - // hosting window has changed. The underlying PTYs may need to know this - // for the proper response to `::GetConsoleWindow()` from a Win32 console app. - // Arguments: - // - showOrHide: Show is true; hide is false. - // Return Value: - // - - void TerminalPage::WindowVisibilityChanged(const bool showOrHide) - { - _visible = showOrHide; - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings - // objects but only have to iterate one time. - tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { - if (auto control = pane->GetTerminalControl()) - { - control.WindowVisibilityChanged(showOrHide); - } - }); - } - } - } - - // Method Description: - // - Called when the user tries to do a search using keybindings. - // This will tell the active terminal control of the passed tab - // to create a search box and enable find process. - // Arguments: - // - tab: the tab where the search box should be created - // Return Value: - // - - void TerminalPage::_Find(const Tab& tab) - { - if (const auto& control{ tab.GetActiveTerminalControl() }) - { - control.CreateSearchBoxControl(); - } - } - - // Method Description: - // - Toggles borderless mode. Hides the tab row, and raises our - // FocusModeChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleFocusMode() - { - SetFocusMode(!_isInFocusMode); - } - - void TerminalPage::SetFocusMode(const bool inFocusMode) - { - const auto newInFocusMode = inFocusMode; - if (newInFocusMode != FocusMode()) - { - _isInFocusMode = newInFocusMode; - _UpdateTabView(); - FocusModeChanged.raise(*this, nullptr); - } - } - - // Method Description: - // - Toggles fullscreen mode. Hides the tab row, and raises our - // FullscreenChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleFullscreen() - { - SetFullscreen(!_isFullscreen); - } - - // Method Description: - // - Toggles always on top mode. Raises our AlwaysOnTopChanged event. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::ToggleAlwaysOnTop() - { - _isAlwaysOnTop = !_isAlwaysOnTop; - AlwaysOnTopChanged.raise(*this, nullptr); - } - - // Method Description: - // - Sets the tab split button color when a new tab color is selected - // Arguments: - // - color: The color of the newly selected tab, used to properly calculate - // the foreground color of the split button (to match the font - // color of the tab) - // - accentColor: the actual color we are going to use to paint the tab row and - // split button, so that there is some contrast between the tab - // and the non-client are behind it - // Return Value: - // - - void TerminalPage::_SetNewTabButtonColor(const til::color color, const til::color accentColor) - { - constexpr auto lightnessThreshold = 0.6f; - // TODO GH#3327: Look at what to do with the tab button when we have XAML theming - const auto isBrightColor = ColorFix::GetLightness(color) >= lightnessThreshold; - const auto isLightAccentColor = ColorFix::GetLightness(accentColor) >= lightnessThreshold; - const auto hoverColorAdjustment = isLightAccentColor ? -0.05f : 0.05f; - const auto pressedColorAdjustment = isLightAccentColor ? -0.1f : 0.1f; - - const auto foregroundColor = isBrightColor ? Colors::Black() : Colors::White(); - const auto hoverColor = til::color{ ColorFix::AdjustLightness(accentColor, hoverColorAdjustment) }; - const auto pressedColor = til::color{ ColorFix::AdjustLightness(accentColor, pressedColorAdjustment) }; - - Media::SolidColorBrush backgroundBrush{ accentColor }; - Media::SolidColorBrush backgroundHoverBrush{ hoverColor }; - Media::SolidColorBrush backgroundPressedBrush{ pressedColor }; - Media::SolidColorBrush foregroundBrush{ foregroundColor }; - - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackground"), backgroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPointerOver"), backgroundHoverBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPressed"), backgroundPressedBrush); - - // Load bearing: The SplitButton uses SplitButtonForegroundSecondary for - // the secondary button, but {TemplateBinding Foreground} for the - // primary button. - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForeground"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPointerOver"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPressed"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondary"), foregroundBrush); - _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondaryPressed"), foregroundBrush); - - _newTabButton.Background(backgroundBrush); - _newTabButton.Foreground(foregroundBrush); - - // This is just like what we do in Tab::_RefreshVisualState. We need - // to manually toggle the visual state, so the setters in the visual - // state group will re-apply, and set our currently selected colors in - // the resources. - VisualStateManager::GoToState(_newTabButton, L"FlyoutOpen", true); - VisualStateManager::GoToState(_newTabButton, L"Normal", true); - } - - // Method Description: - // - Clears the tab split button color to a system color - // (or white if none is found) when the tab's color is cleared - // - Clears the tab row color to a system color - // (or white if none is found) when the tab's color is cleared - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_ClearNewTabButtonColor() - { - // TODO GH#3327: Look at what to do with the tab button when we have XAML theming - winrt::hstring keys[] = { - L"SplitButtonBackground", - L"SplitButtonBackgroundPointerOver", - L"SplitButtonBackgroundPressed", - L"SplitButtonForeground", - L"SplitButtonForegroundSecondary", - L"SplitButtonForegroundPointerOver", - L"SplitButtonForegroundPressed", - L"SplitButtonForegroundSecondaryPressed" - }; - - // simply clear any of the colors in the split button's dict - for (auto keyString : keys) - { - auto key = winrt::box_value(keyString); - if (_newTabButton.Resources().HasKey(key)) - { - _newTabButton.Resources().Remove(key); - } - } - - const auto res = Application::Current().Resources(); - - const auto defaultBackgroundKey = winrt::box_value(L"TabViewItemHeaderBackground"); - const auto defaultForegroundKey = winrt::box_value(L"SystemControlForegroundBaseHighBrush"); - winrt::Windows::UI::Xaml::Media::SolidColorBrush backgroundBrush; - winrt::Windows::UI::Xaml::Media::SolidColorBrush foregroundBrush; - - // TODO: Related to GH#3917 - I think if the system is set to "Dark" - // theme, but the app is set to light theme, then this lookup still - // returns to us the dark theme brushes. There's gotta be a way to get - // the right brushes... - // See also GH#5741 - if (res.HasKey(defaultBackgroundKey)) - { - auto obj = res.Lookup(defaultBackgroundKey); - backgroundBrush = obj.try_as(); - } - else - { - backgroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Black() }; - } - - if (res.HasKey(defaultForegroundKey)) - { - auto obj = res.Lookup(defaultForegroundKey); - foregroundBrush = obj.try_as(); - } - else - { - foregroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::White() }; - } - - _newTabButton.Background(backgroundBrush); - _newTabButton.Foreground(foregroundBrush); - } - - // Function Description: - // - This is a helper method to get the commandline out of a - // ExecuteCommandline action, break it into subcommands, and attempt to - // parse it into actions. This is used by _HandleExecuteCommandline for - // processing commandlines in the current WT window. - // Arguments: - // - args: the ExecuteCommandlineArgs to synthesize a list of startup actions for. - // Return Value: - // - an empty list if we failed to parse; otherwise, a list of actions to execute. - std::vector TerminalPage::ConvertExecuteCommandlineToActions(const ExecuteCommandlineArgs& args) - { - ::TerminalApp::AppCommandlineArgs appArgs; - if (appArgs.ParseArgs(args) == 0) - { - return appArgs.GetStartupActions(); - } - - return {}; - } - - void TerminalPage::_FocusActiveControl(IInspectable /*sender*/, - IInspectable /*eventArgs*/) - { - _FocusCurrentTab(false); - } - - bool TerminalPage::FocusMode() const - { - return _isInFocusMode; - } - - bool TerminalPage::Fullscreen() const - { - return _isFullscreen; - } - - // Method Description: - // - Returns true if we're currently in "Always on top" mode. When we're in - // always on top mode, the window should be on top of all other windows. - // If multiple windows are all "always on top", they'll maintain their own - // z-order, with all the windows on top of all other non-topmost windows. - // Arguments: - // - - // Return Value: - // - true if we should be in "always on top" mode - bool TerminalPage::AlwaysOnTop() const - { - return _isAlwaysOnTop; - } - - // Method Description: - // - Returns true if the tab row should be visible when we're in full screen - // state. - // Arguments: - // - - // Return Value: - // - true if the tab row should be visible in full screen state - bool TerminalPage::ShowTabsFullscreen() const - { - return _showTabsFullscreen; - } - - // Method Description: - // - Updates the visibility of the tab row when in fullscreen state. - void TerminalPage::SetShowTabsFullscreen(bool newShowTabsFullscreen) - { - if (_showTabsFullscreen == newShowTabsFullscreen) - { - return; - } - - _showTabsFullscreen = newShowTabsFullscreen; - - // if we're currently in fullscreen, update tab view to make - // sure tabs are given the correct visibility - if (_isFullscreen) - { - _UpdateTabView(); - } - } - - void TerminalPage::SetFullscreen(bool newFullscreen) - { - if (_isFullscreen == newFullscreen) - { - return; - } - _isFullscreen = newFullscreen; - _UpdateTabView(); - FullscreenChanged.raise(*this, nullptr); - } - - // Method Description: - // - Updates the page's state for isMaximized when the window changes externally. - void TerminalPage::Maximized(bool newMaximized) - { - _isMaximized = newMaximized; - } - - // Method Description: - // - Asks the window to change its maximized state. - void TerminalPage::RequestSetMaximized(bool newMaximized) - { - if (_isMaximized == newMaximized) - { - return; - } - _isMaximized = newMaximized; - ChangeMaximizeRequested.raise(*this, nullptr); - } - - TerminalApp::IPaneContent TerminalPage::_makeSettingsContent() - { - if (auto app{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) - { - if (auto appPrivate{ winrt::get_self(app) }) - { - // Lazily load the Settings UI components so that we don't do it on startup. - appPrivate->PrepareForSettingsUI(); - } - } - - // Create the SUI pane content - auto settingsContent{ winrt::make_self(_settings) }; - auto sui = settingsContent->SettingsUI(); - - if (_hostingHwnd) - { - sui.SetHostingWindow(reinterpret_cast(*_hostingHwnd)); - } - - // GH#8767 - let unhandled keys in the SUI try to run commands too. - sui.KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); - - sui.OpenJson([weakThis{ get_weak() }](auto&& /*s*/, winrt::Microsoft::Terminal::Settings::Model::SettingsTarget e) { - if (auto page{ weakThis.get() }) - { - page->_LaunchSettings(e); - } - }); - - sui.ShowLoadWarningsDialog([weakThis{ get_weak() }](auto&& /*s*/, const Windows::Foundation::Collections::IVectorView& warnings) { - if (auto page{ weakThis.get() }) - { - page->ShowLoadWarningsDialog.raise(*page, warnings); - } - }); - - return *settingsContent; - } - - // Method Description: - // - Creates a settings UI tab and focuses it. If there's already a settings UI tab open, - // just focus the existing one. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::OpenSettingsUI() - { - // If we're holding the settings tab's switch command, don't create a new one, switch to the existing one. - if (!_settingsTab) - { - // Create the tab - auto resultPane = std::make_shared(_makeSettingsContent()); - _settingsTab = _CreateNewTabFromPane(resultPane); - } - else - { - _tabView.SelectedItem(_settingsTab.TabViewItem()); - } - } - - // Method Description: - // - Returns a com_ptr to the implementation type of the given tab if it's a Tab. - // If the tab is not a TerminalTab, returns nullptr. - // Arguments: - // - tab: the projected type of a Tab - // Return Value: - // - If the tab is a TerminalTab, a com_ptr to the implementation type. - // If the tab is not a TerminalTab, nullptr - winrt::com_ptr TerminalPage::_GetTabImpl(const TerminalApp::Tab& tab) - { - winrt::com_ptr tabImpl; - tabImpl.copy_from(winrt::get_self(tab)); - return tabImpl; - } - - // Method Description: - // - Computes the delta for scrolling the tab's viewport. - // Arguments: - // - scrollDirection - direction (up / down) to scroll - // - rowsToScroll - the number of rows to scroll - // Return Value: - // - delta - Signed delta, where a negative value means scrolling up. - int TerminalPage::_ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll) - { - return scrollDirection == ScrollUp ? -1 * rowsToScroll : rowsToScroll; - } - - // Method Description: - // - Reads system settings for scrolling (based on the step of the mouse scroll). - // Upon failure fallbacks to default. - // Return Value: - // - The number of rows to scroll or a magic value of WHEEL_PAGESCROLL - // indicating that we need to scroll an entire view height - uint32_t TerminalPage::_ReadSystemRowsToScroll() - { - uint32_t systemRowsToScroll; - if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) - { - LOG_LAST_ERROR(); - - // If SystemParametersInfoW fails, which it shouldn't, fall back to - // Windows' default value. - return DefaultRowsToScroll; - } - - return systemRowsToScroll; - } - - // Method Description: - // - Displays a dialog stating the "Touch Keyboard and Handwriting Panel - // Service" is disabled. - void TerminalPage::ShowKeyboardServiceWarning() const - { - if (!_IsMessageDismissed(InfoBarMessage::KeyboardServiceWarning)) - { - if (const auto keyboardServiceWarningInfoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) - { - keyboardServiceWarningInfoBar.IsOpen(true); - } - } - } - - // Function Description: - // - Helper function to get the OS-localized name for the "Touch Keyboard - // and Handwriting Panel Service". If we can't open up the service for any - // reason, then we'll just return the service's key, "TabletInputService". - // Return Value: - // - The OS-localized name for the TabletInputService - winrt::hstring _getTabletServiceName() - { - wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; - - if (LOG_LAST_ERROR_IF(!hManager.is_valid())) - { - return winrt::hstring{ TabletInputServiceKey }; - } - - DWORD cchBuffer = 0; - const auto ok = GetServiceDisplayNameW(hManager.get(), TabletInputServiceKey.data(), nullptr, &cchBuffer); - - // Windows 11 doesn't have a TabletInputService. - // (It was renamed to TextInputManagementService, because people kept thinking that a - // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) - if (ok || GetLastError() != ERROR_INSUFFICIENT_BUFFER) - { - return winrt::hstring{ TabletInputServiceKey }; - } - - std::wstring buffer; - cchBuffer += 1; // Add space for a null - buffer.resize(cchBuffer); - - if (LOG_LAST_ERROR_IF(!GetServiceDisplayNameW(hManager.get(), - TabletInputServiceKey.data(), - buffer.data(), - &cchBuffer))) - { - return winrt::hstring{ TabletInputServiceKey }; - } - return winrt::hstring{ buffer }; - } - - // Method Description: - // - Return the fully-formed warning message for the - // "KeyboardServiceDisabled" InfoBar. This InfoBar is used to warn the user - // if the keyboard service is disabled, and uses the OS localization for - // the service's actual name. It's bound to the bar in XAML. - // Return Value: - // - The warning message, including the OS-localized service name. - winrt::hstring TerminalPage::KeyboardServiceDisabledText() - { - const auto serviceName{ _getTabletServiceName() }; - const auto text{ RS_fmt(L"KeyboardServiceWarningText", serviceName) }; - return winrt::hstring{ text }; - } - - // Method Description: - // - Update the RequestedTheme of the specified FrameworkElement and all its - // Parent elements. We need to do this so that we can actually theme all - // of the elements of the TeachingTip. See GH#9717 - // Arguments: - // - element: The TeachingTip to set the theme on. - // Return Value: - // - - void TerminalPage::_UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element) - { - auto theme{ _settings.GlobalSettings().CurrentTheme() }; - auto requestedTheme{ theme.RequestedTheme() }; - while (element) - { - element.RequestedTheme(requestedTheme); - element = element.Parent().try_as(); - } - } - - // Method Description: - // - Display the name and ID of this window in a TeachingTip. If the window - // has no name, the name will be presented as "". - // - This can be invoked by either: - // * An identifyWindow action, that displays the info only for the current - // window - // * An identifyWindows action, that displays the info for all windows. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::IdentifyWindow() - { - // If we haven't ever loaded the TeachingTip, then do so now and - // create the toast for it. - if (_windowIdToast == nullptr) - { - if (auto tip{ FindName(L"WindowIdToast").try_as() }) - { - _windowIdToast = std::make_shared(tip); - // IsLightDismissEnabled == true is bugged and poorly interacts with multi-windowing. - // It causes the tip to be immediately dismissed when another tip is opened in another window. - tip.IsLightDismissEnabled(false); - // Make sure to use the weak ref when setting up this callback. - tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); - } - } - _UpdateTeachingTipTheme(WindowIdToast().try_as()); - - if (_windowIdToast != nullptr) - { - _windowIdToast->Open(); - } - } - - void TerminalPage::ShowTerminalWorkingDirectory() - { - // If we haven't ever loaded the TeachingTip, then do so now and - // create the toast for it. - if (_windowCwdToast == nullptr) - { - if (auto tip{ FindName(L"WindowCwdToast").try_as() }) - { - _windowCwdToast = std::make_shared(tip); - // Make sure to use the weak ref when setting up this - // callback. - tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); - } - } - _UpdateTeachingTipTheme(WindowCwdToast().try_as()); - - if (_windowCwdToast != nullptr) - { - _windowCwdToast->Open(); - } - } - - // Method Description: - // - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip. - // - Will raise an event that will bubble up to the monarch, asking if this - // name is acceptable. - // - we'll eventually get called back in TerminalPage::WindowName(hstring). - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_WindowRenamerActionClick(const IInspectable& /*sender*/, - const IInspectable& /*eventArgs*/) - { - auto newName = WindowRenamerTextBox().Text(); - _RequestWindowRename(newName); - } - - void TerminalPage::_RequestWindowRename(const winrt::hstring& newName) - { - auto request = winrt::make(newName); - // The WindowRenamer is _not_ a Toast - we want it to stay open until - // the user dismisses it. - if (WindowRenamer()) - { - WindowRenamer().IsOpen(false); - } - RenameWindowRequested.raise(*this, request); - // We can't just use request.Successful here, because the handler might - // (will) be handling this asynchronously, so when control returns to - // us, this hasn't actually been handled yet. We'll get called back in - // RenameFailed if this fails. - // - // Theoretically we could do a IAsyncOperation kind - // of thing with co_return winrt::make(false). - } - - // Method Description: - // - Used to track if the user pressed enter with the renamer open. If we - // immediately focus it after hitting Enter on the command palette, then - // the Enter keydown will dismiss the command palette and open the - // renamer, and then the enter keyup will go to the renamer. So we need to - // make sure both a down and up go to the renamer. - // Arguments: - // - e: the KeyRoutedEventArgs describing the key that was released - // Return Value: - // - - void TerminalPage::_WindowRenamerKeyDown(const IInspectable& /*sender*/, - const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto key = e.OriginalKey(); - if (key == Windows::System::VirtualKey::Enter) - { - _renamerPressedEnter = true; - } - } - - // Method Description: - // - Manually handle Enter and Escape for committing and dismissing a window - // rename. This is highly similar to the TabHeaderControl's KeyUp handler. - // Arguments: - // - e: the KeyRoutedEventArgs describing the key that was released - // Return Value: - // - - void TerminalPage::_WindowRenamerKeyUp(const IInspectable& sender, - const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) - { - const auto key = e.OriginalKey(); - if (key == Windows::System::VirtualKey::Enter && _renamerPressedEnter) - { - // User is done making changes, close the rename box - _WindowRenamerActionClick(sender, nullptr); - } - else if (key == Windows::System::VirtualKey::Escape) - { - // User wants to discard the changes they made - WindowRenamerTextBox().Text(_WindowProperties.WindowName()); - WindowRenamer().IsOpen(false); - _renamerPressedEnter = false; - } - } - - // Method Description: - // - This function stops people from duplicating the base profile, because - // it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done. - Profile TerminalPage::GetClosestProfileForDuplicationOfProfile(const Profile& profile) const noexcept - { - if (profile == _settings.ProfileDefaults()) - { - return _settings.FindProfile(_settings.GlobalSettings().DefaultProfile()); - } - return profile; - } - - // Function Description: - // - Helper to launch a new WT instance elevated. It'll do this by spawning - // a helper process, who will asking the shell to elevate the process for - // us. This might cause a UAC prompt. The elevation is performed on a - // background thread, as to not block the UI thread. - // Arguments: - // - newTerminalArgs: A NewTerminalArgs describing the terminal instance - // that should be spawned. The Profile should be filled in with the GUID - // of the profile we want to launch. - // Return Value: - // - - // Important: Don't take the param by reference, since we'll be doing work - // on another thread. - void TerminalPage::_OpenElevatedWT(NewTerminalArgs newTerminalArgs) - { - // BODGY - // - // We're going to construct the commandline we want, then toss it to a - // helper process called `elevate-shim.exe` that happens to live next to - // us. elevate-shim.exe will be the one to call ShellExecute with the - // args that we want (to elevate the given profile). - // - // We can't be the one to call ShellExecute ourselves. ShellExecute - // requires that the calling process stays alive until the child is - // spawned. However, in the case of something like `wt -p - // AlwaysElevateMe`, then the original WT will try to ShellExecute a new - // wt.exe (elevated) and immediately exit, preventing ShellExecute from - // successfully spawning the elevated WT. - - std::filesystem::path exePath = wil::GetModuleFileNameW(nullptr); - exePath.replace_filename(L"elevate-shim.exe"); - - // Build the commandline to pass to wt for this set of NewTerminalArgs - auto cmdline{ - fmt::format(FMT_COMPILE(L"new-tab {}"), newTerminalArgs.ToCommandline()) - }; - - wil::unique_process_information pi; - STARTUPINFOW si{}; - si.cb = sizeof(si); - - LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(exePath.c_str(), - cmdline.data(), - nullptr, - nullptr, - FALSE, - 0, - nullptr, - nullptr, - &si, - &pi)); - - // TODO: GH#8592 - It may be useful to pop a Toast here in the original - // Terminal window informing the user that the tab was opened in a new - // window. - } - - // Method Description: - // - If the requested settings want us to elevate this new terminal - // instance, and we're not currently elevated, then open the new terminal - // as an elevated instance (using _OpenElevatedWT). Does nothing if we're - // already elevated, or if the control settings don't want to be elevated. - // Arguments: - // - newTerminalArgs: The NewTerminalArgs for this terminal instance - // - controlSettings: The constructed TerminalSettingsCreateResult for this Terminal instance - // - profile: The Profile we're using to launch this Terminal instance - // Return Value: - // - true iff we tossed this request to an elevated window. Callers can use - // this result to early-return if needed. - bool TerminalPage::_maybeElevate(const NewTerminalArgs& newTerminalArgs, - const Settings::TerminalSettingsCreateResult& controlSettings, - const Profile& profile) - { - // When duplicating a tab there aren't any newTerminalArgs. - if (!newTerminalArgs) - { - return false; - } - - const auto defaultSettings = controlSettings.DefaultSettings(); - - // If we don't even want to elevate we can return early. - // If we're already elevated we can also return, because it doesn't get any more elevated than that. - if (!defaultSettings->Elevate() || IsRunningElevated()) - { - return false; - } - - // Manually set the Profile of the NewTerminalArgs to the guid we've - // resolved to. If there was a profile in the NewTerminalArgs, this - // will be that profile's GUID. If there wasn't, then we'll use - // whatever the default profile's GUID is. - newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); - newTerminalArgs.StartingDirectory(_evaluatePathForCwd(defaultSettings->StartingDirectory())); - _OpenElevatedWT(newTerminalArgs); - return true; - } - - // Method Description: - // - Handles the change of connection state. - // If the connection state is failure show information bar suggesting to configure termination behavior - // (unless user asked not to show this message again) - // Arguments: - // - sender: the ICoreState instance containing the connection state - // Return Value: - // - - safe_void_coroutine TerminalPage::_ConnectionStateChangedHandler(const IInspectable& sender, const IInspectable& /*args*/) - { - if (const auto coreState{ sender.try_as() }) - { - const auto newConnectionState = coreState.ConnectionState(); - const auto weak = get_weak(); - co_await wil::resume_foreground(Dispatcher()); - const auto strong = weak.get(); - if (!strong) - { - co_return; - } - - _adjustProcessPriorityThrottled->Run(); - - if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) - { - if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) - { - infoBar.IsOpen(true); - } - } - } - } - - // Method Description: - // - Persists the user's choice not to show information bar guiding to configure termination behavior. - // Then hides this information buffer. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_CloseOnExitInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const - { - _DismissMessage(InfoBarMessage::CloseOnExitInfo); - if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) - { - infoBar.IsOpen(false); - } - } - - // Method Description: - // - Persists the user's choice not to show information bar warning about "Touch keyboard and Handwriting Panel Service" disabled - // Then hides this information buffer. - // Arguments: - // - - // Return Value: - // - - void TerminalPage::_KeyboardServiceWarningInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const - { - _DismissMessage(InfoBarMessage::KeyboardServiceWarning); - if (const auto infoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) - { - infoBar.IsOpen(false); - } - } - - // Method Description: - // - Checks whether information bar message was dismissed earlier (in the application state) - // Arguments: - // - message: message to look for in the state - // Return Value: - // - true, if the message was dismissed - bool TerminalPage::_IsMessageDismissed(const InfoBarMessage& message) - { - if (const auto dismissedMessages{ ApplicationState::SharedInstance().DismissedMessages() }) - { - for (const auto& dismissedMessage : dismissedMessages) - { - if (dismissedMessage == message) - { - return true; - } - } - } - return false; - } - - // Method Description: - // - Persists the user's choice to dismiss information bar message (in application state) - // Arguments: - // - message: message to dismiss - // Return Value: - // - - void TerminalPage::_DismissMessage(const InfoBarMessage& message) - { - const auto applicationState = ApplicationState::SharedInstance(); - std::vector messages; - - if (const auto values = applicationState.DismissedMessages()) - { - messages.resize(values.Size()); - values.GetMany(0, messages); - } - - if (std::none_of(messages.begin(), messages.end(), [&](const auto& m) { return m == message; })) - { - messages.emplace_back(message); - } - - applicationState.DismissedMessages(std::move(messages)); - } - - void TerminalPage::_updateThemeColors() - { - if (_settings == nullptr) - { - return; - } - - const auto theme = _settings.GlobalSettings().CurrentTheme(); - auto requestedTheme{ theme.RequestedTheme() }; - - { - _updatePaneResources(requestedTheme); - - for (const auto& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - // The root pane will propagate the theme change to all its children. - if (const auto& rootPane{ tabImpl->GetRootPane() }) - { - rootPane->UpdateResources(_paneResources); - } - } - } - } - - const auto res = Application::Current().Resources(); - - // Use our helper to lookup the theme-aware version of the resource. - const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); - const auto backgroundSolidBrush = ThemeLookup(res, requestedTheme, tabViewBackgroundKey).as(); - - til::color bgColor = backgroundSolidBrush.Color(); - - Media::Brush terminalBrush{ nullptr }; - if (const auto tab{ _GetFocusedTabImpl() }) - { - if (const auto& pane{ tab->GetActivePane() }) - { - if (const auto& lastContent{ pane->GetLastFocusedContent() }) - { - terminalBrush = lastContent.BackgroundBrush(); - } - } - } - - // GH#19604: Get the theme's tabRow color to use as the acrylic tint. - const auto tabRowBg{ theme.TabRow() ? (_activated ? theme.TabRow().Background() : - theme.TabRow().UnfocusedBackground()) : - ThemeColor{ nullptr } }; - - if (_settings.GlobalSettings().UseAcrylicInTabRow() && (_activated || _settings.GlobalSettings().EnableUnfocusedAcrylic())) - { - if (tabRowBg) - { - bgColor = ThemeColor::ColorFromBrush(tabRowBg.Evaluate(res, terminalBrush, true)); - } - - const auto acrylicBrush = Media::AcrylicBrush(); - acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); - acrylicBrush.FallbackColor(bgColor); - acrylicBrush.TintColor(bgColor); - acrylicBrush.TintOpacity(0.5); - - TitlebarBrush(acrylicBrush); - } - else if (tabRowBg) - { - const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) }; - bgColor = ThemeColor::ColorFromBrush(themeBrush); - // If the tab content returned nullptr for the terminalBrush, we - // _don't_ want to use it as the tab row background. We want to just - // use the default tab row background. - TitlebarBrush(themeBrush ? themeBrush : backgroundSolidBrush); - } - else - { - // Nothing was set in the theme - fall back to our original `TabViewBackground` color. - TitlebarBrush(backgroundSolidBrush); - } - - if (!_settings.GlobalSettings().ShowTabsInTitlebar()) - { - _tabRow.Background(TitlebarBrush()); - } - - // Second: Update the colors of our individual TabViewItems. This - // applies tab.background to the tabs via Tab::ThemeColor. - // - // Do this second, so that we already know the bgColor of the titlebar. - { - const auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; - const auto tabUnfocusedBackground = theme.Tab() ? theme.Tab().UnfocusedBackground() : nullptr; - for (const auto& tab : _tabs) - { - winrt::com_ptr tabImpl; - tabImpl.copy_from(winrt::get_self(tab)); - tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor); - } - } - // Update the new tab button to have better contrast with the new color. - // In theory, it would be convenient to also change these for the - // inactive tabs as well, but we're leaving that as a follow up. - _SetNewTabButtonColor(bgColor, bgColor); - - // Third: the window frame. This is basically the same logic as the tab row background. - // We'll set our `FrameBrush` property, for the window to later use. - const auto windowTheme{ theme.Window() }; - if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() : - windowTheme.UnfocusedFrame()) : - ThemeColor{ nullptr } }) - { - const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) }; - FrameBrush(themeBrush); - } - else - { - // Nothing was set in the theme - fall back to null. The window will - // use that as an indication to use the default window frame. - FrameBrush(nullptr); - } - } - - // Function Description: - // - Attempts to load some XAML resources that Panes will need. This includes: - // * The Color they'll use for active Panes's borders - SystemAccentColor - // * The Brush they'll use for inactive Panes - TabViewBackground (to match the - // color of the titlebar) - // Arguments: - // - requestedTheme: this should be the currently active Theme for the app - // Return Value: - // - - void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) - { - const auto res = Application::Current().Resources(); - const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); - if (res.HasKey(accentColorKey)) - { - const auto colorFromResources = ThemeLookup(res, requestedTheme, 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(colorFromResources, Colors::Black()); - _paneResources.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. - _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; - } - - const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); - if (res.HasKey(unfocusedBorderBrushKey)) - { - // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for - // the requestedTheme, not just the value from the resources (which - // might not respect the settings' requested theme) - auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); - _paneResources.unfocusedBorderBrush = obj.try_as(); - } - 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. - _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; - } - - const auto broadcastColorKey = winrt::box_value(L"BroadcastPaneBorderColor"); - if (res.HasKey(broadcastColorKey)) - { - // MAKE SURE TO USE ThemeLookup - auto obj = ThemeLookup(res, requestedTheme, broadcastColorKey); - _paneResources.broadcastBorderBrush = obj.try_as(); - } - 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. - _paneResources.broadcastBorderBrush = SolidColorBrush{ Colors::Black() }; - } - } - - void TerminalPage::_adjustProcessPriority() const - { - // Windowing is single-threaded, so this will not cause a race condition. - static uint64_t s_lastUpdateHash{ 0 }; - static bool s_supported{ true }; - - if (!s_supported || !_hostingHwnd.has_value()) - { - return; - } - - std::array processes; - auto it = processes.begin(); - const auto end = processes.end(); - - auto&& appendFromControl = [&](auto&& control) { - if (it == end) - { - return; - } - if (control) - { - if (const auto conn{ control.Connection() }) - { - if (const auto pty{ conn.try_as() }) - { - if (const uint64_t process{ pty.RootProcessHandle() }; process != 0) - { - *it++ = reinterpret_cast(process); - } - } - } - } - }; - - auto&& appendFromTab = [&](auto&& tabImpl) { - if (const auto pane{ tabImpl->GetRootPane() }) - { - pane->WalkTree([&](auto&& child) { - if (const auto& control{ child->GetTerminalControl() }) - { - appendFromControl(control); - } - }); - } - }; - - if (!_activated) - { - // When a window is out of focus, we want to attach all of the processes - // under it to the window so they all go into the background at the same time. - for (auto&& tab : _tabs) - { - if (auto tabImpl{ _GetTabImpl(tab) }) - { - appendFromTab(tabImpl); - } - } - } - else - { - // When a window is in focus, propagate our foreground boost (if we have one) - // to current all panes in the current tab. - if (auto tabImpl{ _GetFocusedTabImpl() }) - { - appendFromTab(tabImpl); - } - } - - const auto count{ gsl::narrow_cast(it - processes.begin()) }; - const auto hash = til::hash((void*)processes.data(), count * sizeof(HANDLE)); - - if (hash == s_lastUpdateHash) - { - return; - } - - s_lastUpdateHash = hash; - const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); - - if (S_FALSE == hr) - { - // Don't bother trying again or logging. The wrapper tells us it's unsupported. - s_supported = false; - return; - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "CalledNewQoSAPI", - TraceLoggingValue(reinterpret_cast(_hostingHwnd.value()), "hwnd"), - TraceLoggingValue(count), - TraceLoggingHResult(hr)); -#ifdef _DEBUG - OutputDebugStringW(fmt::format(FMT_COMPILE(L"Submitted {} processes to TerminalTrySetWindowAssociatedProcesses; return=0x{:08x}\n"), count, hr).c_str()); -#endif - } - - void TerminalPage::WindowActivated(const bool activated) - { - // Stash if we're activated. Use that when we reload - // the settings, change active panes, etc. - _activated = activated; - _updateThemeColors(); - - _adjustProcessPriorityThrottled->Run(); - - if (const auto& tab{ _GetFocusedTabImpl() }) - { - if (tab->TabStatus().IsInputBroadcastActive()) - { - tab->GetRootPane()->WalkTree([activated](const auto& p) { - if (const auto& control{ p->GetTerminalControl() }) - { - control.CursorVisibility(activated ? - Microsoft::Terminal::Control::CursorDisplayState::Shown : - Microsoft::Terminal::Control::CursorDisplayState::Default); - } - }); - } - } - } - - safe_void_coroutine TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, - const CompletionsChangedEventArgs args) - { - // This won't even get hit if the velocity flag is disabled - we gate - // registering for the event based off of - // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents - - // User must explicitly opt-in on Preview builds - if (!_settings.GlobalSettings().EnableShellCompletionMenu()) - { - co_return; - } - - // Parse the json string into a collection of actions - try - { - auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), - args.ReplacementLength()); - - auto weakThis{ get_weak() }; - Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { - // On the UI thread... - if (const auto& page{ weakThis.get() }) - { - // Open the Suggestions UI with the commands from the control - page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu, L""); - } - }); - } - CATCH_LOG(); - } - - void TerminalPage::_OpenSuggestions( - const TermControl& sender, - IVector commandsCollection, - winrt::TerminalApp::SuggestionsMode mode, - winrt::hstring filterText) - - { - // ON THE UI THREAD - assert(Dispatcher().HasThreadAccess()); - - if (commandsCollection == nullptr) - { - return; - } - if (commandsCollection.Size() == 0) - { - if (const auto p = SuggestionsElement()) - { - p.Visibility(Visibility::Collapsed); - } - return; - } - - const auto& control{ sender ? sender : _GetActiveControl() }; - if (!control) - { - return; - } - - const auto& sxnUi{ LoadSuggestionsUI() }; - - const auto characterSize{ control.CharacterDimensions() }; - // This is in control-relative space. We'll need to convert it to page-relative space. - const auto cursorPos{ control.CursorPositionInDips() }; - const auto controlTransform = control.TransformToVisual(this->Root()); - const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos - const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; - - sxnUi.Open(mode, - commandsCollection, - filterText, - realCursorPos, - windowDimensions, - characterSize.Height); - } - - void TerminalPage::_PopulateContextMenu(const TermControl& control, - const MUX::Controls::CommandBarFlyout& menu, - const bool withSelection) - { - // withSelection can be used to add actions that only appear if there's - // selected text, like "search the web" - - if (!control || !menu) - { - return; - } - - // Helper lambda for dispatching an ActionAndArgs onto the - // ShortcutActionDispatch. Used below to wire up each menu entry to the - // respective action. - - auto weak = get_weak(); - auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { - return [weak, actionAndArgs](auto&&, auto&&) { - if (auto page{ weak.get() }) - { - page->_actionDispatch->DoAction(actionAndArgs); - } - }; - }; - - auto makeItem = [&makeCallback](const winrt::hstring& label, - const winrt::hstring& icon, - const auto& action, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Click(makeCallback(action)); - targetMenu.SecondaryCommands().Append(button); - }; - - auto makeMenuItem = [](const winrt::hstring& label, - const winrt::hstring& icon, - const auto& subMenu, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Flyout(subMenu); - targetMenu.SecondaryCommands().Append(button); - }; - - auto makeContextItem = [&makeCallback](const winrt::hstring& label, - const winrt::hstring& icon, - const winrt::hstring& tooltip, - const auto& action, - const auto& subMenu, - auto& targetMenu) { - AppBarButton button{}; - - if (!icon.empty()) - { - auto iconElement = UI::IconPathConverter::IconWUX(icon); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - button.Icon(iconElement); - } - - button.Label(label); - button.Click(makeCallback(action)); - WUX::Controls::ToolTipService::SetToolTip(button, box_value(tooltip)); - button.ContextFlyout(subMenu); - targetMenu.SecondaryCommands().Append(button); - }; - - const auto focusedProfile = _GetFocusedTabImpl()->GetFocusedProfile(); - auto separatorItem = AppBarSeparator{}; - auto activeProfiles = _settings.ActiveProfiles(); - auto activeProfileCount = gsl::narrow_cast(activeProfiles.Size()); - MUX::Controls::CommandBarFlyout splitPaneMenu{}; - - // Wire up each item to the action that should be performed. By actually - // connecting these to actions, we ensure the implementation is - // consistent. This also leaves room for customizing this menu with - // actions in the future. - - makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }, menu); - - const auto focusedProfileName = focusedProfile.Name(); - const auto focusedProfileIcon = focusedProfile.Icon().Resolved(); - const auto splitPaneDuplicateText = RS_(L"SplitPaneDuplicateText") + L" " + focusedProfileName; // SplitPaneDuplicateText - - const auto splitPaneRightText = RS_(L"SplitPaneRightText"); - const auto splitPaneDownText = RS_(L"SplitPaneDownText"); - const auto splitPaneUpText = RS_(L"SplitPaneUpText"); - const auto splitPaneLeftText = RS_(L"SplitPaneLeftText"); - const auto splitPaneToolTipText = RS_(L"SplitPaneToolTipText"); - - MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; - makeItem(splitPaneRightText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Right, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneDownText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Down, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneUpText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Up, .5, nullptr } }, splitPaneContextMenu); - makeItem(splitPaneLeftText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Left, .5, nullptr } }, splitPaneContextMenu); - - makeContextItem(splitPaneDuplicateText, focusedProfileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Automatic, .5, nullptr } }, splitPaneContextMenu, splitPaneMenu); - - // add menu separator - const auto separatorAutoItem = AppBarSeparator{}; - - splitPaneMenu.SecondaryCommands().Append(separatorAutoItem); - - for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) - { - const auto profile = activeProfiles.GetAt(profileIndex); - const auto profileName = profile.Name(); - const auto profileIcon = profile.Icon().Resolved(); - - NewTerminalArgs args{}; - args.Profile(profileName); - - MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; - makeItem(splitPaneRightText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Right, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneDownText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Down, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneUpText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Up, .5, args } }, splitPaneContextMenu); - makeItem(splitPaneLeftText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Left, .5, args } }, splitPaneContextMenu); - - makeContextItem(profileName, profileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Automatic, .5, args } }, splitPaneContextMenu, splitPaneMenu); - } - - makeMenuItem(RS_(L"SplitPaneText"), L"\xF246", splitPaneMenu, menu); - - // Only wire up "Close Pane" if there's multiple panes. - if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) - { - MUX::Controls::CommandBarFlyout swapPaneMenu{}; - const auto rootPane = _GetFocusedTabImpl()->GetRootPane(); - const auto mruPanes = _GetFocusedTabImpl()->GetMruPanes(); - auto activePane = _GetFocusedTabImpl()->GetActivePane(); - rootPane->WalkTree([&](auto p) { - if (const auto& c{ p->GetTerminalControl() }) - { - if (c == control) - { - activePane = p; - } - } - }); - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Down, mruPanes)) - { - makeItem(RS_(L"SwapPaneDownText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Down } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Right, mruPanes)) - { - makeItem(RS_(L"SwapPaneRightText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Right } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Up, mruPanes)) - { - makeItem(RS_(L"SwapPaneUpText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Up } }, swapPaneMenu); - } - - if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Left, mruPanes)) - { - makeItem(RS_(L"SwapPaneLeftText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Left } }, swapPaneMenu); - } - - makeMenuItem(RS_(L"SwapPaneText"), L"\xF1CB", swapPaneMenu, menu); - - makeItem(RS_(L"TogglePaneZoomText"), L"\xE8A3", ActionAndArgs{ ShortcutAction::TogglePaneZoom, nullptr }, menu); - makeItem(RS_(L"CloseOtherPanesText"), L"\xE89F", ActionAndArgs{ ShortcutAction::CloseOtherPanes, nullptr }, menu); - makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }, menu); - } - - if (control.ConnectionState() >= ConnectionState::Closed) - { - makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }, menu); - } - - if (withSelection) - { - makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }, menu); - } - - makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }, menu); - } - - void TerminalPage::_PopulateQuickFixMenu(const TermControl& control, - const Controls::MenuFlyout& menu) - { - if (!control || !menu) - { - return; - } - - // Helper lambda for dispatching a SendInput ActionAndArgs onto the - // ShortcutActionDispatch. Used below to wire up each menu entry to the - // respective action. Then clear the quick fix menu. - auto weak = get_weak(); - auto makeCallback = [weak](const hstring& suggestion) { - return [weak, suggestion](auto&&, auto&&) { - if (auto page{ weak.get() }) - { - const auto actionAndArgs = ActionAndArgs{ ShortcutAction::SendInput, SendInputArgs{ hstring{ L"\u0003" } + suggestion } }; - page->_actionDispatch->DoAction(actionAndArgs); - if (auto ctrl = page->_GetActiveControl()) - { - ctrl.ClearQuickFix(); - } - - TraceLoggingWrite( - g_hTerminalAppProvider, - "QuickFixSuggestionUsed", - TraceLoggingDescription("Event emitted when a winget suggestion from is used"), - TraceLoggingValue("QuickFixMenu", "Source"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - } - }; - }; - - // Wire up each item to the action that should be performed. By actually - // connecting these to actions, we ensure the implementation is - // consistent. This also leaves room for customizing this menu with - // actions in the future. - - menu.Items().Clear(); - const auto quickFixes = control.CommandHistory().QuickFixes(); - for (const auto& qf : quickFixes) - { - MenuFlyoutItem item{}; - - auto iconElement = UI::IconPathConverter::IconWUX(L"\ue74c"); - Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); - item.Icon(iconElement); - - item.Text(qf); - item.Click(makeCallback(qf)); - ToolTipService::SetToolTip(item, box_value(qf)); - menu.Items().Append(item); - } - } - - // Handler for our WindowProperties's PropertyChanged event. We'll use this - // to pop the "Identify Window" toast when the user renames our window. - void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) - { - if (args.PropertyName() != L"WindowName") - { - return; - } - - // DON'T display the confirmation if this is the name we were - // given on startup! - if (_startupState == StartupState::Initialized) - { - IdentifyWindow(); - } - } - - void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, - const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) - { - _appendUiaDragLog(L"_onTabDragStarting: begin"); - const auto eventTab = e.Tab(); - const auto draggedTab = _GetTabByTabViewItem(eventTab); - if (draggedTab) - { - auto draggedTabs = _IsTabSelected(draggedTab) ? _GetSelectedTabsInDisplayOrder() : - std::vector{}; - if (draggedTabs.empty() || - !std::ranges::any_of(draggedTabs, [&](const auto& tab) { return tab == draggedTab; })) - { - draggedTabs = { draggedTab }; - _SetSelectedTabs(draggedTabs, draggedTab); - } - - _stashed.draggedTabs = std::move(draggedTabs); - _stashed.dragAnchor = draggedTab; - - // Stash the offset from where we started the drag to the - // tab's origin. We'll use that offset in the future to help - // position the dropped window. - const auto inverseScale = 1.0f / static_cast(eventTab.XamlRoot().RasterizationScale()); - POINT cursorPos; - GetCursorPos(&cursorPos); - ScreenToClient(*_hostingHwnd, &cursorPos); - _stashed.dragOffset.X = cursorPos.x * inverseScale; - _stashed.dragOffset.Y = cursorPos.y * inverseScale; - - // Into the DataPackage, let's stash our own window ID. - const auto id{ _WindowProperties.WindowId() }; - - // Get our PID - const auto pid{ GetCurrentProcessId() }; - - e.Data().Properties().Insert(L"windowId", winrt::box_value(id)); - e.Data().Properties().Insert(L"pid", winrt::box_value(pid)); - e.Data().RequestedOperation(DataPackageOperation::Move); - _appendUiaDragLog(std::wstring{ L"_onTabDragStarting: stashed " } + std::to_wstring(_stashed.draggedTabs.size()) + L" tab(s)"); - - // The next thing that will happen: - // * Another TerminalPage will get a TabStripDragOver, then get a - // TabStripDrop - // * This will be handled by the _other_ page asking the monarch - // to ask us to send our content to them. - // * We'll get a TabDroppedOutside to indicate that this tab was - // dropped _not_ on a TabView. - // * This will be handled by _onTabDroppedOutside, which will - // raise a MoveContent (to a new window) event. - } - } - - void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/, - const winrt::Windows::UI::Xaml::DragEventArgs& e) - { - // We must mark that we can accept the drag/drop. The system will never - // call TabStripDrop on us if we don't indicate that we're willing. - const auto& props{ e.DataView().Properties() }; - if (props.HasKey(L"windowId") && - props.HasKey(L"pid") && - (winrt::unbox_value_or(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId())) - { - e.AcceptedOperation(DataPackageOperation::Move); - } - - // You may think to yourself, this is a great place to increase the - // width of the TabView artificially, to make room for the new tab item. - // However, we'll never get a message that the tab left the tab view - // (without being dropped). So there's no good way to resize back down. - } - - // Method Description: - // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage - // to find who the tab came from. We'll then ask the Monarch to ask the - // sender to move that tab to us. - void TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/, - winrt::Windows::UI::Xaml::DragEventArgs e) - { - // Get the PID and make sure it is the same as ours. - if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") }) - { - const auto pid{ winrt::unbox_value_or(pidObj, 0u) }; - if (pid != GetCurrentProcessId()) - { - // The PID doesn't match ours. We can't handle this drop. - return; - } - } - else - { - // No PID? We can't handle this drop. Bail. - return; - } - - const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") }; - if (windowIdObj == nullptr) - { - // No windowId? Bail. - return; - } - const uint64_t src{ winrt::unbox_value(windowIdObj) }; - - // Figure out where in the tab strip we're dropping this tab. Add that - // index to the request. This is largely taken from the WinUI sample - // app. - - // First we need to get the position in the List to drop to - auto index = -1; - - // Determine which items in the list our pointer is between. - for (auto i = 0u; i < _tabView.TabItems().Size(); i++) - { - if (const auto& item{ _tabView.ContainerFromIndex(i).try_as() }) - { - const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab - const auto itemWidth{ item.ActualWidth() }; // The right of the tab - // If the drag point is on the left half of the tab, then insert here. - if (posX < itemWidth / 2) - { - index = i; - break; - } - } - } - - if (index < 0) - { - index = gsl::narrow_cast(_tabView.TabItems().Size()); - } - - // `this` is safe to use - const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); - - // This will go up to the monarch, who will then dispatch the request - // back down to the source TerminalPage, who will then perform a - // RequestMoveContent to move their tab to us. - RequestReceiveContent.raise(*this, *request); - } - - // Method Description: - // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has - // requested that we send our tab to another window. We'll need to - // serialize the tab, and send it to the monarch, who will then send it to - // the destination window. - // - Fortunately, sending the tab is basically just a MoveTab action, so we - // can largely reuse that. - void TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) - { - // validate that we're the source window of the tab in this request - if (args.SourceWindow() != _WindowProperties.WindowId()) - { - return; - } - if (_stashed.draggedTabs.empty()) - { - return; - } - - _sendDraggedTabsToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); - } - - void TerminalPage::_onTabDroppedOutside(winrt::IInspectable /*sender*/, - winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs /*e*/) - { - _appendUiaDragLog(L"_onTabDroppedOutside: begin"); - // Get the current pointer point from the CoreWindow - const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() }; - - // This is called when a tab FROM OUR WINDOW was dropped outside the - // tabview. We already know which tab was being dragged. We'll just - // invoke a moveTab action with the target window being -1. That will - // force the creation of a new window. - - if (_stashed.draggedTabs.empty()) - { - return; - } - - // We need to convert the pointer point to a point that we can use - // to position the new window. We'll use the drag offset from before - // so that the tab in the new window is positioned so that it's - // basically still directly under the cursor. - - // -1 is the magic number for "new window" - // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab. - const winrt::Windows::Foundation::Point adjusted = { - pointerPoint.X - _stashed.dragOffset.X, - pointerPoint.Y - _stashed.dragOffset.Y, - }; - _appendUiaDragLog(std::wstring{ L"_onTabDroppedOutside: moving " } + std::to_wstring(_stashed.draggedTabs.size()) + L" tab(s) to a new window"); - _sendDraggedTabsToWindow(winrt::hstring{ L"-1" }, 0, adjusted); - } - - void TerminalPage::_sendDraggedTabsToWindow(const winrt::hstring& windowId, - const uint32_t tabIndex, - std::optional dragPoint) - { - _appendUiaDragLog(std::wstring{ L"_sendDraggedTabsToWindow: target=" } + windowId.c_str() + L", tabs=" + std::to_wstring(_stashed.draggedTabs.size())); - if (_stashed.draggedTabs.empty()) - { - return; - } - - auto draggedTabs = _stashed.draggedTabs; - auto startupActions = _BuildStartupActionsForTabs(draggedTabs); - if (dragPoint.has_value() && draggedTabs.size() > 1 && _stashed.dragAnchor) - { - const auto draggedAnchorIt = std::ranges::find_if(draggedTabs, [&](const auto& tab) { - return tab == _stashed.dragAnchor; - }); - if (draggedAnchorIt != draggedTabs.end()) - { - ActionAndArgs switchToTabAction{}; - switchToTabAction.Action(ShortcutAction::SwitchToTab); - switchToTabAction.Args(SwitchToTabArgs{ gsl::narrow_cast(std::distance(draggedTabs.begin(), draggedAnchorIt)) }); - startupActions.emplace_back(std::move(switchToTabAction)); - } - } - - for (const auto& tab : draggedTabs) - { - if (const auto tabImpl{ _GetTabImpl(tab) }) - { - _DetachTabFromWindow(tabImpl); - } - } - - _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); - - for (auto it = draggedTabs.rbegin(); it != draggedTabs.rend(); ++it) - { - _RemoveTab(*it); - } - - _stashed.draggedTabs.clear(); - _stashed.dragAnchor = nullptr; - } - - /// - /// Creates a sub flyout menu for profile items in the split button menu that when clicked will show a menu item for - /// Run as Administrator - /// - /// The index for the profileMenuItem - /// MenuFlyout that will show when the context is request on a profileMenuItem - WUX::Controls::MenuFlyout TerminalPage::_CreateRunAsAdminFlyout(int profileIndex) - { - // Create the MenuFlyout and set its placement - WUX::Controls::MenuFlyout profileMenuItemFlyout{}; - profileMenuItemFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight); - - // Create the menu item and an icon to use in the menu - WUX::Controls::MenuFlyoutItem runAsAdminItem{}; - WUX::Controls::FontIcon adminShieldIcon{}; - - adminShieldIcon.Glyph(L"\xEA18"); - adminShieldIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); - - runAsAdminItem.Icon(adminShieldIcon); - runAsAdminItem.Text(RS_(L"RunAsAdminFlyout/Text")); - - // Click handler for the flyout item - runAsAdminItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { - if (auto page{ weakThis.get() }) - { - TraceLoggingWrite( - g_hTerminalAppProvider, - "NewTabMenuItemElevateSubmenuItemClicked", - TraceLoggingDescription("Event emitted when the elevate submenu item from the new tab menu is invoked"), - TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); - - NewTerminalArgs args{ profileIndex }; - args.Elevate(true); - page->_OpenNewTerminalViaDropdown(args); - } - }); - - profileMenuItemFlyout.Items().Append(runAsAdminItem); - - return profileMenuItemFlyout; - } -} + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TerminalPage.h" + +#include +#include +#include +#include +#include + +#include "../../types/inc/ColorFix.hpp" +#include "../../types/inc/utils.hpp" +#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h" +#include "App.h" +#include "DebugTapConnection.h" +#include "MarkdownPaneContent.h" +#include "Remoting.h" +#include "ScratchpadContent.h" +#include "SettingsPaneContent.h" +#include "SnippetsPaneContent.h" +#include "TabRowControl.h" +#include "TerminalSettingsCache.h" + +#include "LaunchPositionRequest.g.cpp" +#include "RenameWindowRequestedArgs.g.cpp" +#include "RequestMoveContentArgs.g.cpp" +#include "TerminalPage.g.cpp" + +using namespace winrt; +using namespace winrt::Microsoft::Management::Deployment; +using namespace winrt::Microsoft::Terminal::Control; +using namespace winrt::Microsoft::Terminal::Settings::Model; +using namespace winrt::Microsoft::Terminal::TerminalConnection; +using namespace winrt::Microsoft::Terminal; +using namespace winrt::Windows::ApplicationModel::DataTransfer; +using namespace winrt::Windows::Foundation::Collections; +using namespace winrt::Windows::System; +using namespace winrt::Windows::UI; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::UI::Text; +using namespace winrt::Windows::UI::Xaml::Controls; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Xaml::Media; +using namespace ::TerminalApp; +using namespace ::Microsoft::Console; +using namespace ::Microsoft::Terminal::Core; +using namespace std::chrono_literals; + +#define HOOKUP_ACTION(action) _actionDispatch->action({ this, &TerminalPage::_Handle##action }); + +namespace winrt +{ + namespace MUX = Microsoft::UI::Xaml; + namespace WUX = Windows::UI::Xaml; + using IInspectable = Windows::Foundation::IInspectable; + using VirtualKeyModifiers = Windows::System::VirtualKeyModifiers; +} + +namespace clipboard +{ + static SRWLOCK lock = SRWLOCK_INIT; + + struct ClipboardHandle + { + explicit ClipboardHandle(bool open) : + _open{ open } + { + } + + ~ClipboardHandle() + { + if (_open) + { + ReleaseSRWLockExclusive(&lock); + CloseClipboard(); + } + } + + explicit operator bool() const noexcept + { + return _open; + } + + private: + bool _open = false; + }; + + ClipboardHandle open(HWND hwnd) + { + // Turns out, OpenClipboard/CloseClipboard are not thread-safe whatsoever, + // and on CloseClipboard, the GetClipboardData handle may get freed. + // The problem is that WinUI also uses OpenClipboard (through WinRT which uses OLE), + // and so even with this mutex we can still crash randomly if you copy something via WinUI. + // Makes you wonder how many Windows apps are subtly broken, huh. + AcquireSRWLockExclusive(&lock); + + bool success = false; + + // OpenClipboard may fail to acquire the internal lock --> retry. + for (DWORD sleep = 10;; sleep *= 2) + { + if (OpenClipboard(hwnd)) + { + success = true; + break; + } + // 10 iterations + if (sleep > 10000) + { + break; + } + Sleep(sleep); + } + + if (!success) + { + ReleaseSRWLockExclusive(&lock); + } + + return ClipboardHandle{ success }; + } + + void write(wil::zwstring_view text, std::string_view html, std::string_view rtf) + { + static const auto regular = [](const UINT format, const void* src, const size_t bytes) { + wil::unique_hglobal handle{ THROW_LAST_ERROR_IF_NULL(GlobalAlloc(GMEM_MOVEABLE, bytes)) }; + + const auto locked = GlobalLock(handle.get()); + memcpy(locked, src, bytes); + GlobalUnlock(handle.get()); + + THROW_LAST_ERROR_IF_NULL(SetClipboardData(format, handle.get())); + handle.release(); + }; + static const auto registered = [](const wchar_t* format, const void* src, size_t bytes) { + const auto id = RegisterClipboardFormatW(format); + if (!id) + { + LOG_LAST_ERROR(); + return; + } + regular(id, src, bytes); + }; + + EmptyClipboard(); + + if (!text.empty()) + { + // As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats + // CF_UNICODETEXT: [...] A null character signals the end of the data. + // --> We add +1 to the length. This works because .c_str() is null-terminated. + regular(CF_UNICODETEXT, text.c_str(), (text.size() + 1) * sizeof(wchar_t)); + } + + if (!html.empty()) + { + registered(L"HTML Format", html.data(), html.size()); + } + + if (!rtf.empty()) + { + registered(L"Rich Text Format", rtf.data(), rtf.size()); + } + } + + winrt::hstring read() + { + // This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically. + if (const auto handle = GetClipboardData(CF_UNICODETEXT)) + { + const wil::unique_hglobal_locked lock{ handle }; + const auto str = static_cast(lock.get()); + if (!str) + { + return {}; + } + + const auto maxLen = GlobalSize(handle) / sizeof(wchar_t); + const auto len = wcsnlen(str, maxLen); + return winrt::hstring{ str, gsl::narrow_cast(len) }; + } + + // We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others). + if (const auto handle = GetClipboardData(CF_HDROP)) + { + const wil::unique_hglobal_locked lock{ handle }; + const auto drop = static_cast(lock.get()); + if (!drop) + { + return {}; + } + + const auto cap = DragQueryFileW(drop, 0, nullptr, 0); + if (cap == 0) + { + return {}; + } + + auto buffer = winrt::impl::hstring_builder{ cap }; + const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1); + if (len == 0) + { + return {}; + } + + return buffer.to_hstring(); + } + + return {}; + } +} // namespace clipboard + +namespace winrt::TerminalApp::implementation +{ + TerminalPage::TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager) : + _tabs{ winrt::single_threaded_observable_vector() }, + _mruTabs{ winrt::single_threaded_observable_vector() }, + _manager{ manager }, + _hostingHwnd{}, + _WindowProperties{ std::move(properties) } + { + InitializeComponent(); + _WindowProperties.PropertyChanged({ get_weak(), &TerminalPage::_windowPropertyChanged }); + } + + // Method Description: + // - implements the IInitializeWithWindow interface from shobjidl_core. + // - We're going to use this HWND as the owner for the ConPTY windows, via + // ConptyConnection::ReparentWindow. We need this for applications that + // call GetConsoleWindow, and attempt to open a MessageBox for the + // console. By marking the conpty windows as owned by the Terminal HWND, + // the message box will be owned by the Terminal window as well. + // - see GH#2988 + HRESULT TerminalPage::Initialize(HWND hwnd) + { + if (!_hostingHwnd.has_value()) + { + // GH#13211 - if we haven't yet set the owning hwnd, reparent all the controls now. + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto& term{ pane->GetTerminalControl() }) + { + term.OwningHwnd(reinterpret_cast(hwnd)); + } + }); + } + // We don't need to worry about resetting the owning hwnd for the + // SUI here. GH#13211 only repros for a defterm connection, where + // the tab is spawned before the window is created. It's not + // possible to make a SUI tab like that, before the window is + // created. The SUI could be spawned as a part of a window restore, + // but that would still work fine. The window would be created + // before restoring previous tabs in that scenario. + } + } + + _hostingHwnd = hwnd; + return S_OK; + } + + // INVARIANT: This needs to be called on OUR UI thread! + void TerminalPage::SetSettings(CascadiaSettings settings, bool needRefreshUI) + { + assert(Dispatcher().HasThreadAccess()); + if (_settings == nullptr) + { + // Create this only on the first time we load the settings. + _terminalSettingsCache = std::make_shared(settings); + } + _settings = settings; + + // Make sure to call SetCommands before _RefreshUIForSettingsReload. + // SetCommands will make sure the KeyChordText of Commands is updated, which needs + // to happen before the Settings UI is reloaded and tries to re-read those values. + if (const auto p = CommandPaletteElement()) + { + p.SetActionMap(_settings.ActionMap()); + } + + if (needRefreshUI) + { + _RefreshUIForSettingsReload(); + } + + // Upon settings update we reload the system settings for scrolling as well. + // TODO: consider reloading this value periodically. + _systemRowsToScroll = _ReadSystemRowsToScroll(); + } + + bool TerminalPage::IsRunningElevated() const noexcept + { + // GH#2455 - Make sure to try/catch calls to Application::Current, + // because that _won't_ be an instance of TerminalApp::App in the + // LocalTests + try + { + return Application::Current().as().Logic().IsRunningElevated(); + } + CATCH_LOG(); + return false; + } + bool TerminalPage::CanDragDrop() const noexcept + { + try + { + return Application::Current().as().Logic().CanDragDrop(); + } + CATCH_LOG(); + return true; + } + + void TerminalPage::Create() + { + // Hookup the key bindings + _HookupKeyBindings(_settings.ActionMap()); + + _tabContent = this->TabContent(); + _tabRow = this->TabRow(); + _tabView = _tabRow.TabView(); + _rearranging = false; + + const auto canDragDrop = CanDragDrop(); + _tabView.CanReorderTabs(canDragDrop); + _tabView.CanDragTabs(canDragDrop); + _tabView.TabDragStarting({ get_weak(), &TerminalPage::_TabDragStarted }); + _tabView.TabDragCompleted({ get_weak(), &TerminalPage::_TabDragCompleted }); + + auto tabRowImpl = winrt::get_self(_tabRow); + _newTabButton = tabRowImpl->NewTabButton(); + + if (_settings.GlobalSettings().ShowTabsInTitlebar()) + { + // Remove the TabView from the page. We'll hang on to it, we need to + // put it in the titlebar. + uint32_t index = 0; + if (this->Root().Children().IndexOf(_tabRow, index)) + { + this->Root().Children().RemoveAt(index); + } + + // Inform the host that our titlebar content has changed. + SetTitleBarContent.raise(*this, _tabRow); + + // GH#13143 Manually set the tab row's background to transparent here. + // + // We're doing it this way because ThemeResources are tricky. We + // default in XAML to using the appropriate ThemeResource background + // color for our TabRow. When tabs in the titlebar are _disabled_, + // this will ensure that the tab row has the correct theme-dependent + // value. When tabs in the titlebar are _enabled_ (the default), + // we'll switch the BG to Transparent, to let the Titlebar Control's + // background be used as the BG for the tab row. + // + // We can't do it the other way around (default to Transparent, only + // switch to a color when disabling tabs in the titlebar), because + // looking up the correct ThemeResource from and App dictionary is a + // capital-H Hard problem. + const auto transparent = Media::SolidColorBrush(); + transparent.Color(Windows::UI::Colors::Transparent()); + _tabRow.Background(transparent); + } + _updateThemeColors(); + + // Initialize the state of the CloseButtonOverlayMode property of + // our TabView, to match the tab.showCloseButton property in the theme. + if (const auto theme = _settings.GlobalSettings().CurrentTheme()) + { + const auto visibility = theme.Tab() ? theme.Tab().ShowCloseButton() : Settings::Model::TabCloseButtonVisibility::Always; + + _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; + + switch (visibility) + { + case Settings::Model::TabCloseButtonVisibility::Never: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); + break; + case Settings::Model::TabCloseButtonVisibility::Hover: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); + break; + default: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); + break; + } + } + + // Hookup our event handlers to the ShortcutActionDispatch + _RegisterActionCallbacks(); + + //Event Bindings (Early) + _newTabButton.Click([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuDefaultButtonClicked", + TraceLoggingDescription("Event emitted when the default button from the new tab split button is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + page->_OpenNewTerminalViaDropdown(NewTerminalArgs()); + } + }); + _newTabButton.Drop({ get_weak(), &TerminalPage::_NewTerminalByDrop }); + _tabView.SelectionChanged({ this, &TerminalPage::_OnTabSelectionChanged }); + _tabView.TabCloseRequested({ this, &TerminalPage::_OnTabCloseRequested }); + _tabView.TabItemsChanged({ this, &TerminalPage::_OnTabItemsChanged }); + + _tabView.TabDragStarting({ this, &TerminalPage::_onTabDragStarting }); + _tabView.TabStripDragOver({ this, &TerminalPage::_onTabStripDragOver }); + _tabView.TabStripDrop({ this, &TerminalPage::_onTabStripDrop }); + _tabView.TabDroppedOutside({ this, &TerminalPage::_onTabDroppedOutside }); + + _CreateNewTabFlyout(); + + _UpdateTabWidthMode(); + + // Settings AllowDependentAnimations will affect whether animations are + // enabled application-wide, so we don't need to check it each time we + // want to create an animation. + WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + // Once the page is actually laid out on the screen, trigger all our + // startup actions. Things like Panes need to know at least how big the + // window will be, so they can subdivide that space. + // + // _OnFirstLayout will remove this handler so it doesn't get called more than once. + _layoutUpdatedRevoker = _tabContent.LayoutUpdated(winrt::auto_revoke, { this, &TerminalPage::_OnFirstLayout }); + + _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); + _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); + + // DON'T set up Toasts/TeachingTips here. They should be loaded and + // initialized the first time they're opened, in whatever method opens + // them. + + _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + + _adjustProcessPriorityThrottled = std::make_shared>( + DispatcherQueue::GetForCurrentThread(), + til::throttled_func_options{ + .delay = std::chrono::milliseconds{ 100 }, + .debounce = true, + .trailing = true, + }, + [=]() { + _adjustProcessPriority(); + }); + } + + Windows::UI::Xaml::Automation::Peers::AutomationPeer TerminalPage::OnCreateAutomationPeer() + { + return Automation::Peers::FrameworkElementAutomationPeer(*this); + } + + // Method Description: + // - This is a bit of trickiness: If we're running unelevated, and the user + // passed in only --elevate actions, the we don't _actually_ want to + // restore the layouts here. We're not _actually_ about to create the + // window. We're simply going to toss the commandlines + // Arguments: + // - + // Return Value: + // - true if we're not elevated but all relevant pane-spawning actions are elevated + bool TerminalPage::ShouldImmediatelyHandoffToElevated(const CascadiaSettings& settings) const + { + if (_startupActions.empty() || _startupConnection || IsRunningElevated()) + { + // No point in handing off if we got no startup actions, or we're already elevated. + // Also, we shouldn't need to elevate handoff ConPTY connections. + assert(!_startupConnection); + return false; + } + + // Check that there's at least one action that's not just an elevated newTab action. + for (const auto& action : _startupActions) + { + // Only new terminal panes will be requesting elevation. + NewTerminalArgs newTerminalArgs{ nullptr }; + + if (action.Action() == ShortcutAction::NewTab) + { + const auto& args{ action.Args().try_as() }; + if (args) + { + newTerminalArgs = args.ContentArgs().try_as(); + } + else + { + // This was a nt action that didn't have any args. The default + // profile may want to be elevated, so don't just early return. + } + } + else if (action.Action() == ShortcutAction::SplitPane) + { + const auto& args{ action.Args().try_as() }; + if (args) + { + newTerminalArgs = args.ContentArgs().try_as(); + } + else + { + // This was a nt action that didn't have any args. The default + // profile may want to be elevated, so don't just early return. + } + } + else + { + // This was not a new tab or split pane action. + // This doesn't affect the outcome + continue; + } + + // It's possible that newTerminalArgs is null here. + // GetProfileForArgs should be resilient to that. + const auto profile{ settings.GetProfileForArgs(newTerminalArgs) }; + if (profile.Elevate()) + { + continue; + } + + // The profile didn't want to be elevated, and we aren't elevated. + // We're going to open at least one tab, so return false. + return false; + } + return true; + } + + // Method Description: + // - Escape hatch for immediately dispatching requests to elevated windows + // when first launched. At this point in startup, the window doesn't exist + // yet, XAML hasn't been started, but we need to dispatch these actions. + // We can't just go through ProcessStartupActions, because that processes + // the actions async using the XAML dispatcher (which doesn't exist yet) + // - DON'T CALL THIS if you haven't already checked + // ShouldImmediatelyHandoffToElevated. If you're thinking about calling + // this outside of the one place it's used, that's probably the wrong + // solution. + // Arguments: + // - settings: the settings we should use for dispatching these actions. At + // this point in startup, we hadn't otherwise been initialized with these, + // so use them now. + // Return Value: + // - + void TerminalPage::HandoffToElevated(const CascadiaSettings& settings) + { + if (_startupActions.empty()) + { + return; + } + + // Hookup our event handlers to the ShortcutActionDispatch + _settings = settings; + _HookupKeyBindings(_settings.ActionMap()); + _RegisterActionCallbacks(); + + for (const auto& action : _startupActions) + { + // only process new tabs and split panes. They're all going to the elevated window anyways. + if (action.Action() == ShortcutAction::NewTab || action.Action() == ShortcutAction::SplitPane) + { + _actionDispatch->DoAction(action); + } + } + } + + safe_void_coroutine TerminalPage::_NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e) + try + { + const auto data = e.DataView(); + if (!data.Contains(StandardDataFormats::StorageItems())) + { + co_return; + } + + const auto weakThis = get_weak(); + const auto items = co_await data.GetStorageItemsAsync(); + const auto strongThis = weakThis.get(); + if (!strongThis) + { + co_return; + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabByDragDrop", + TraceLoggingDescription("Event emitted when the user drag&drops onto the new tab button"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + for (const auto& item : items) + { + auto directory = item.Path(); + + std::filesystem::path path(std::wstring_view{ directory }); + if (!std::filesystem::is_directory(path)) + { + directory = winrt::hstring{ path.parent_path().native() }; + } + + NewTerminalArgs args; + args.StartingDirectory(directory); + _OpenNewTerminalViaDropdown(args); + } + } + CATCH_LOG() + + // Method Description: + // - This method is called once command palette action was chosen for dispatching + // We'll use this event to dispatch this command. + // Arguments: + // - command - command to dispatch + // Return Value: + // - + void TerminalPage::_OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command) + { + const auto& actionAndArgs = command.ActionAndArgs(); + _actionDispatch->DoAction(sender, actionAndArgs); + } + + // Method Description: + // - This method is called once command palette command line was chosen for execution + // We'll use this event to create a command line execution command and dispatch it. + // Arguments: + // - command - command to dispatch + // Return Value: + // - + void TerminalPage::_OnCommandLineExecutionRequested(const IInspectable& /*sender*/, const winrt::hstring& commandLine) + { + ExecuteCommandlineArgs args{ commandLine }; + ActionAndArgs actionAndArgs{ ShortcutAction::ExecuteCommandline, args }; + _actionDispatch->DoAction(actionAndArgs); + } + + // Method Description: + // - This method is called once on startup, on the first LayoutUpdated event. + // We'll use this event to know that we have an ActualWidth and + // ActualHeight, so we can now attempt to process our list of startup + // actions. + // - We'll remove this event handler when the event is first handled. + // - If there are no startup actions, we'll open a single tab with the + // default profile. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_OnFirstLayout(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/) + { + // Only let this succeed once. + _layoutUpdatedRevoker.revoke(); + + // This event fires every time the layout changes, but it is always the + // last one to fire in any layout change chain. That gives us great + // flexibility in finding the right point at which to initialize our + // renderer (and our terminal). Any earlier than the last layout update + // and we may not know the terminal's starting size. + if (_startupState == StartupState::NotInitialized) + { + _startupState = StartupState::InStartup; + + if (_startupConnection) + { + CreateTabFromConnection(std::move(_startupConnection)); + } + else if (!_startupActions.empty()) + { + ProcessStartupActions(std::move(_startupActions)); + } + + _CompleteInitialization(); + } + } + + // Method Description: + // - Process all the startup actions in the provided list of startup + // actions. We'll do this all at once here. + // Arguments: + // - actions: a winrt vector of actions to process. Note that this must NOT + // be an IVector&, because we need the collection to be accessible on the + // other side of the co_await. + // - initial: if true, we're parsing these args during startup, and we + // should fire an Initialized event. + // - cwd: If not empty, we should try switching to this provided directory + // while processing these actions. This will allow something like `wt -w 0 + // nt -d .` from inside another directory to work as expected. + // Return Value: + // - + safe_void_coroutine TerminalPage::ProcessStartupActions(std::vector actions, const winrt::hstring cwd, const winrt::hstring env) + { + const auto strong = get_strong(); + + // If the caller provided a CWD, "switch" to that directory, then switch + // back once we're done. + auto originalVirtualCwd{ _WindowProperties.VirtualWorkingDirectory() }; + auto originalVirtualEnv{ _WindowProperties.VirtualEnvVars() }; + auto restoreCwd = wil::scope_exit([&]() { + if (!cwd.empty()) + { + // ignore errors, we'll just power on through. We'd rather do + // something rather than fail silently if the directory doesn't + // actually exist. + _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd); + _WindowProperties.VirtualEnvVars(originalVirtualEnv); + } + }); + if (!cwd.empty()) + { + _WindowProperties.VirtualWorkingDirectory(cwd); + _WindowProperties.VirtualEnvVars(env); + } + + // The current TerminalWindow & TerminalPage architecture is rather instable + // and fails to start up if the first tab isn't created synchronously. + // + // While that's a fair assumption in on itself, simultaneously WinUI will + // not assign tab contents a size if they're not shown at least once, + // which we need however in order to initialize ControlCore with a size. + // + // So, we do two things here: + // * DO NOT suspend if this is the first tab. + // * DO suspend between the creation of panes (or tabs) in order to allow + // WinUI to layout the new controls and for ControlCore to get a size. + // + // This same logic is also applied to CreateTabFromConnection. + // + // See GH#13136. + auto suspend = _tabs.Size() > 0; + + for (size_t i = 0; i < actions.size(); ++i) + { + if (suspend) + { + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); + } + + _actionDispatch->DoAction(actions[i]); + suspend = true; + } + + // GH#6586: now that we're done processing all startup commands, + // focus the active control. This will work as expected for both + // commandline invocations and for `wt` action invocations. + if (const auto& tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto& content{ tabImpl->GetActiveContent() }) + { + content.Focus(FocusState::Programmatic); + } + } + } + + safe_void_coroutine TerminalPage::CreateTabFromConnection(ITerminalConnection connection) + { + const auto strong = get_strong(); + + // This is the exact same logic as in ProcessStartupActions. + if (_tabs.Size() > 0) + { + co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); + } + + NewTerminalArgs newTerminalArgs; + + if (const auto conpty = connection.try_as()) + { + newTerminalArgs.Commandline(conpty.Commandline()); + newTerminalArgs.TabTitle(conpty.StartingTitle()); + } + + // GH #12370: We absolutely cannot allow a defterm connection to + // auto-elevate. Defterm doesn't work for elevated scenarios in the + // first place. If we try accepting the connection, the spawning an + // elevated version of the Terminal with that profile... that's a + // recipe for disaster. We won't ever open up a tab in this window. + newTerminalArgs.Elevate(false); + + const auto newPane = _MakePane(newTerminalArgs, nullptr, std::move(connection)); + newPane->WalkTree([](const auto& pane) { + pane->FinalizeConfigurationGivenDefault(); + }); + _CreateNewTabFromPane(newPane); + } + + // Method Description: + // - Perform and steps that need to be done once our initial state is all + // set up. This includes entering fullscreen mode and firing our + // Initialized event. + // Arguments: + // - + // Return Value: + // - + safe_void_coroutine TerminalPage::_CompleteInitialization() + { + _startupState = StartupState::Initialized; + + // GH#632 - It's possible that the user tried to create the terminal + // with only one tab, with only an elevated profile. If that happens, + // we'll create _another_ process to host the elevated version of that + // profile. This can happen from the jumplist, or if the default profile + // is `elevate:true`, or from the commandline. + // + // However, we need to make sure to close this window in that scenario. + // Since there aren't any _tabs_ in this window, we won't ever get a + // closed event. So do it manually. + // + // GH#12267: Make sure that we don't instantly close ourselves when + // we're readying to accept a defterm connection. In that case, we don't + // have a tab yet, but will once we're initialized. + if (_tabs.Size() == 0) + { + CloseWindowRequested.raise(*this, nullptr); + co_return; + } + else + { + // GH#11561: When we start up, our window is initially just a frame + // with a transparent content area. We're gonna do all this startup + // init on the UI thread, so the UI won't actually paint till it's + // all done. This results in a few frames where the frame is + // visible, before the page paints for the first time, before any + // tabs appears, etc. + // + // To mitigate this, we're gonna wait for the UI thread to finish + // everything it's gotta do for the initial init, and _then_ fire + // our Initialized event. By waiting for everything else to finish + // (CoreDispatcherPriority::Low), we let all the tabs and panes + // actually get created. In the window layer, we're gonna cloak the + // window till this event is fired, so we don't actually see this + // frame until we're actually all ready to go. + // + // This will result in the window seemingly not loading as fast, but + // it will actually take exactly the same amount of time before it's + // usable. + // + // We also experimented with drawing a solid BG color before the + // initialization is finished. However, there are still a few frames + // after the frame is displayed before the XAML content first draws, + // so that didn't actually resolve any issues. + Dispatcher().RunAsync(CoreDispatcherPriority::Low, [weak = get_weak()]() { + if (auto self{ weak.get() }) + { + self->Initialized.raise(*self, nullptr); + } + }); + } + } + + // Method Description: + // - Show a dialog with "About" information. Displays the app's Display + // Name, version, getting started link, source code link, documentation link, release + // Notes link, send feedback link and privacy policy link. + void TerminalPage::_ShowAboutDialog() + { + _ShowDialogHelper(L"AboutDialog"); + } + + winrt::hstring TerminalPage::ApplicationDisplayName() + { + return CascadiaSettings::ApplicationDisplayName(); + } + + winrt::hstring TerminalPage::ApplicationVersion() + { + return CascadiaSettings::ApplicationVersion(); + } + + // Method Description: + // - Helper to show a content dialog + // - We only open a content dialog if there isn't one open already + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowDialogHelper(const std::wstring_view& name) + { + if (auto presenter{ _dialogPresenter.get() }) + { + co_return co_await presenter.ShowDialog(FindName(name).try_as()); + } + co_return ContentDialogResult::None; + } + + // Method Description: + // - Displays a dialog to warn the user that they are about to close all open windows. + // Once the user clicks the OK button, shut down the application. + // If cancel is clicked, the dialog will close. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowQuitDialog() + { + return _ShowDialogHelper(L"QuitDialog"); + } + + // Method Description: + // - Displays a dialog for warnings found while closing the terminal app using + // key binding with multiple tabs opened. Display messages to warn user + // that more than 1 tab is opened, and once the user clicks the OK button, remove + // all the tabs and shut down and app. If cancel is clicked, the dialog will close + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseWarningDialog() + { + return _ShowDialogHelper(L"CloseAllDialog"); + } + + // Method Description: + // - Displays a dialog for warnings found while closing the terminal tab marked as read-only + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowCloseReadOnlyDialog() + { + return _ShowDialogHelper(L"CloseReadOnlyDialog"); + } + + // Method Description: + // - Displays a dialog to warn the user about the fact that the text that + // they are trying to paste contains the "new line" character which can + // have the effect of starting commands without the user's knowledge if + // it is pasted on a shell where the "new line" character marks the end + // of a command. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowMultiLinePasteWarningDialog() + { + return _ShowDialogHelper(L"MultiLinePasteDialog"); + } + + // Method Description: + // - Displays a dialog to warn the user about the fact that the text that + // they are trying to paste is very long, in case they did not mean to + // paste it but pressed the paste shortcut by accident. + // - Only one dialog can be visible at a time. If another dialog is visible + // when this is called, nothing happens. See _ShowDialog for details + winrt::Windows::Foundation::IAsyncOperation TerminalPage::_ShowLargePasteWarningDialog() + { + return _ShowDialogHelper(L"LargePasteDialog"); + } + + // Method Description: + // - Builds the flyout (dropdown) attached to the new tab button, and + // attaches it to the button. Populates the flyout with one entry per + // Profile, displaying the profile's name. Clicking each flyout item will + // open a new tab with that profile. + // Below the profiles are the static menu items: settings, command palette + void TerminalPage::_CreateNewTabFlyout() + { + auto newTabFlyout = WUX::Controls::MenuFlyout{}; + newTabFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedLeft); + + // Create profile entries from the NewTabMenu configuration using a + // recursive helper function. This returns a std::vector of FlyoutItemBases, + // that we then add to our Flyout. + auto entries = _settings.GlobalSettings().NewTabMenu(); + auto items = _CreateNewTabFlyoutItems(entries); + for (const auto& item : items) + { + newTabFlyout.Items().Append(item); + } + + // add menu separator + auto separatorItem = WUX::Controls::MenuFlyoutSeparator{}; + newTabFlyout.Items().Append(separatorItem); + + // add static items + { + // Create the settings button. + auto settingsItem = WUX::Controls::MenuFlyoutItem{}; + settingsItem.Text(RS_(L"SettingsMenuItem")); + const auto settingsToolTip = RS_(L"SettingsToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(settingsItem, box_value(settingsToolTip)); + Automation::AutomationProperties::SetHelpText(settingsItem, settingsToolTip); + + WUX::Controls::SymbolIcon ico{}; + ico.Symbol(WUX::Controls::Symbol::Setting); + settingsItem.Icon(ico); + + settingsItem.Click({ this, &TerminalPage::_SettingsButtonOnClick }); + newTabFlyout.Items().Append(settingsItem); + + auto actionMap = _settings.ActionMap(); + const auto settingsKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.OpenSettingsUI") }; + if (settingsKeyChord) + { + _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); + } + + // Create the command palette button. + auto commandPaletteFlyout = WUX::Controls::MenuFlyoutItem{}; + commandPaletteFlyout.Text(RS_(L"CommandPaletteMenuItem")); + const auto commandPaletteToolTip = RS_(L"CommandPaletteToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(commandPaletteFlyout, box_value(commandPaletteToolTip)); + Automation::AutomationProperties::SetHelpText(commandPaletteFlyout, commandPaletteToolTip); + + WUX::Controls::FontIcon commandPaletteIcon{}; + commandPaletteIcon.Glyph(L"\xE945"); + commandPaletteIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + commandPaletteFlyout.Icon(commandPaletteIcon); + + commandPaletteFlyout.Click({ this, &TerminalPage::_CommandPaletteButtonOnClick }); + newTabFlyout.Items().Append(commandPaletteFlyout); + + const auto commandPaletteKeyChord{ actionMap.GetKeyBindingForAction(L"Terminal.ToggleCommandPalette") }; + if (commandPaletteKeyChord) + { + _SetAcceleratorForMenuItem(commandPaletteFlyout, commandPaletteKeyChord); + } + + // Create the about button. + auto aboutFlyout = WUX::Controls::MenuFlyoutItem{}; + aboutFlyout.Text(RS_(L"AboutMenuItem")); + const auto aboutToolTip = RS_(L"AboutToolTip"); + + WUX::Controls::ToolTipService::SetToolTip(aboutFlyout, box_value(aboutToolTip)); + Automation::AutomationProperties::SetHelpText(aboutFlyout, aboutToolTip); + + WUX::Controls::SymbolIcon aboutIcon{}; + aboutIcon.Symbol(WUX::Controls::Symbol::Help); + aboutFlyout.Icon(aboutIcon); + + aboutFlyout.Click({ this, &TerminalPage::_AboutButtonOnClick }); + newTabFlyout.Items().Append(aboutFlyout); + } + + // Before opening the fly-out set focus on the current tab + // so no matter how fly-out is closed later on the focus will return to some tab. + // We cannot do it on closing because if the window loses focus (alt+tab) + // the closing event is not fired. + // It is important to set the focus on the tab + // Since the previous focus location might be discarded in the background, + // e.g., the command palette will be dismissed by the menu, + // and then closing the fly-out will move the focus to wrong location. + newTabFlyout.Opening([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + page->_FocusCurrentTab(true); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuOpened", + TraceLoggingDescription("Event emitted when the new tab menu is opened"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }); + // Necessary for fly-out sub items to get focus on a tab before collapsing. Related to #15049 + newTabFlyout.Closing([weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + if (!page->_commandPaletteIs(Visibility::Visible)) + { + page->_FocusCurrentTab(true); + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuClosed", + TraceLoggingDescription("Event emitted when the new tab menu is closed"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The Count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }); + _newTabButton.Flyout(newTabFlyout); + } + + // Method Description: + // - For a given list of tab menu entries, this method will create the corresponding + // list of flyout items. This is a recursive method that calls itself when it comes + // across a folder entry. + std::vector TerminalPage::_CreateNewTabFlyoutItems(IVector entries) + { + std::vector items; + + if (entries == nullptr || entries.Size() == 0) + { + return items; + } + + for (const auto& entry : entries) + { + if (entry == nullptr) + { + continue; + } + + switch (entry.Type()) + { + case NewTabMenuEntryType::Separator: + { + items.push_back(WUX::Controls::MenuFlyoutSeparator{}); + break; + } + // A folder has a custom name and icon, and has a number of entries that require + // us to call this method recursively. + case NewTabMenuEntryType::Folder: + { + const auto folderEntry = entry.as(); + const auto folderEntries = folderEntry.Entries(); + + // If the folder is empty, we should skip the entry if AllowEmpty is false, or + // when the folder should inline. + // The IsEmpty check includes semantics for nested (empty) folders + if (folderEntries.Size() == 0 && (!folderEntry.AllowEmpty() || folderEntry.Inlining() == FolderEntryInlining::Auto)) + { + break; + } + + // Recursively generate flyout items + auto folderEntryItems = _CreateNewTabFlyoutItems(folderEntries); + + // If the folder should auto-inline and there is only one item, do so. + if (folderEntry.Inlining() == FolderEntryInlining::Auto && folderEntryItems.size() == 1) + { + for (auto const& folderEntryItem : folderEntryItems) + { + items.push_back(folderEntryItem); + } + + break; + } + + // Otherwise, create a flyout + auto folderItem = WUX::Controls::MenuFlyoutSubItem{}; + folderItem.Text(folderEntry.Name()); + + auto icon = _CreateNewTabFlyoutIcon(folderEntry.Icon().Resolved()); + folderItem.Icon(icon); + + for (const auto& folderEntryItem : folderEntryItems) + { + folderItem.Items().Append(folderEntryItem); + } + + // If the folder is empty, and by now we know we set AllowEmpty to true, + // create a placeholder item here + if (folderEntries.Size() == 0) + { + auto placeholder = WUX::Controls::MenuFlyoutItem{}; + placeholder.Text(RS_(L"NewTabMenuFolderEmpty")); + placeholder.IsEnabled(false); + + folderItem.Items().Append(placeholder); + } + + items.push_back(folderItem); + break; + } + // Any "collection entry" will simply make us add each profile in the collection + // separately. This collection is stored as a map , so the correct + // profile index is already known. + case NewTabMenuEntryType::RemainingProfiles: + case NewTabMenuEntryType::MatchProfiles: + { + const auto remainingProfilesEntry = entry.as(); + if (remainingProfilesEntry.Profiles() == nullptr) + { + break; + } + + for (auto&& [profileIndex, remainingProfile] : remainingProfilesEntry.Profiles()) + { + items.push_back(_CreateNewTabFlyoutProfile(remainingProfile, profileIndex, {})); + } + + break; + } + // A single profile, the profile index is also given in the entry + case NewTabMenuEntryType::Profile: + { + const auto profileEntry = entry.as(); + if (profileEntry.Profile() == nullptr) + { + break; + } + + auto profileItem = _CreateNewTabFlyoutProfile(profileEntry.Profile(), profileEntry.ProfileIndex(), profileEntry.Icon().Resolved()); + items.push_back(profileItem); + break; + } + case NewTabMenuEntryType::Action: + { + const auto actionEntry = entry.as(); + const auto actionId = actionEntry.ActionId(); + if (_settings.ActionMap().GetActionByID(actionId)) + { + auto actionItem = _CreateNewTabFlyoutAction(actionId, actionEntry.Icon().Resolved()); + items.push_back(actionItem); + } + + break; + } + } + } + + return items; + } + + // Method Description: + // - This method creates a flyout menu item for a given profile with the given index. + // It makes sure to set the correct icon, keybinding, and click-action. + WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutProfile(const Profile profile, int profileIndex, const winrt::hstring& iconPathOverride) + { + auto profileMenuItem = WUX::Controls::MenuFlyoutItem{}; + + // Add the keyboard shortcuts based on the number of profiles defined + // Look for a keychord that is bound to the equivalent + // NewTab(ProfileIndex=N) action + NewTerminalArgs newTerminalArgs{ profileIndex }; + NewTabArgs newTabArgs{ newTerminalArgs }; + const auto id = fmt::format(FMT_COMPILE(L"Terminal.OpenNewTabProfile{}"), profileIndex); + const auto profileKeyChord{ _settings.ActionMap().GetKeyBindingForAction(id) }; + + // make sure we find one to display + if (profileKeyChord) + { + _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord); + } + + auto profileName = profile.Name(); + profileMenuItem.Text(profileName); + + // If a custom icon path has been specified, set it as the icon for + // this flyout item. Otherwise, if an icon is set for this profile, set that icon + // for this flyout item. + const auto& iconPath = iconPathOverride.empty() ? profile.Icon().Resolved() : iconPathOverride; + if (!iconPath.empty()) + { + const auto icon = _CreateNewTabFlyoutIcon(iconPath); + profileMenuItem.Icon(icon); + } + + if (profile.Guid() == _settings.GlobalSettings().DefaultProfile()) + { + // Contrast the default profile with others in font weight. + profileMenuItem.FontWeight(FontWeights::Bold()); + } + + auto newTabRun = WUX::Documents::Run(); + newTabRun.Text(RS_(L"NewTabRun/Text")); + auto newPaneRun = WUX::Documents::Run(); + newPaneRun.Text(RS_(L"NewPaneRun/Text")); + newPaneRun.FontStyle(FontStyle::Italic); + auto newWindowRun = WUX::Documents::Run(); + newWindowRun.Text(RS_(L"NewWindowRun/Text")); + newWindowRun.FontStyle(FontStyle::Italic); + auto elevatedRun = WUX::Documents::Run(); + elevatedRun.Text(RS_(L"ElevatedRun/Text")); + elevatedRun.FontStyle(FontStyle::Italic); + + auto textBlock = WUX::Controls::TextBlock{}; + textBlock.Inlines().Append(newTabRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(newPaneRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(newWindowRun); + textBlock.Inlines().Append(WUX::Documents::LineBreak{}); + textBlock.Inlines().Append(elevatedRun); + + auto toolTip = WUX::Controls::ToolTip{}; + toolTip.Content(textBlock); + WUX::Controls::ToolTipService::SetToolTip(profileMenuItem, toolTip); + + profileMenuItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Profile", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + NewTerminalArgs newTerminalArgs{ profileIndex }; + page->_OpenNewTerminalViaDropdown(newTerminalArgs); + } + }); + + // Using the static method on the base class seems to do what we want in terms of placement. + WUX::Controls::Primitives::FlyoutBase::SetAttachedFlyout(profileMenuItem, _CreateRunAsAdminFlyout(profileIndex)); + + // Since we are not setting the ContextFlyout property of the item we have to handle the ContextRequested event + // and rely on the base class to show our menu. + profileMenuItem.ContextRequested([profileMenuItem](auto&&, auto&&) { + WUX::Controls::Primitives::FlyoutBase::ShowAttachedFlyout(profileMenuItem); + }); + + return profileMenuItem; + } + + // Method Description: + // - This method creates a flyout menu item for a given action + // It makes sure to set the correct icon, keybinding, and click-action. + WUX::Controls::MenuFlyoutItem TerminalPage::_CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride) + { + auto actionMenuItem = WUX::Controls::MenuFlyoutItem{}; + const auto action{ _settings.ActionMap().GetActionByID(actionId) }; + const auto actionKeyChord{ _settings.ActionMap().GetKeyBindingForAction(actionId) }; + + if (actionKeyChord) + { + _SetAcceleratorForMenuItem(actionMenuItem, actionKeyChord); + } + + actionMenuItem.Text(action.Name()); + + // If a custom icon path has been specified, set it as the icon for + // this flyout item. Otherwise, if an icon is set for this action, set that icon + // for this flyout item. + const auto& iconPath = iconPathOverride.empty() ? action.Icon().Resolved() : iconPathOverride; + if (!iconPath.empty()) + { + const auto icon = _CreateNewTabFlyoutIcon(iconPath); + actionMenuItem.Icon(icon); + } + + actionMenuItem.Click([action, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Action", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + page->_actionDispatch->DoAction(action.ActionAndArgs()); + } + }); + + return actionMenuItem; + } + + // Method Description: + // - Helper method to create an IconElement that can be passed to MenuFlyoutItems and + // MenuFlyoutSubItems + IconElement TerminalPage::_CreateNewTabFlyoutIcon(const winrt::hstring& iconSource) + { + if (iconSource.empty()) + { + return nullptr; + } + + auto icon = UI::IconPathConverter::IconWUX(iconSource); + Automation::AutomationProperties::SetAccessibilityView(icon, Automation::Peers::AccessibilityView::Raw); + + return icon; + } + + // Function Description: + // Called when the openNewTabDropdown keybinding is used. + // Shows the dropdown flyout. + void TerminalPage::_OpenNewTabDropdown() + { + _newTabButton.Flyout().ShowAt(_newTabButton); + } + + void TerminalPage::_OpenNewTerminalViaDropdown(const NewTerminalArgs newTerminalArgs) + { + // if alt is pressed, open a pane + const auto window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const auto altPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; + const auto rShiftState = window.GetKeyState(VirtualKey::RightShift); + const auto lShiftState = window.GetKeyState(VirtualKey::LeftShift); + const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; + + const auto ctrlState{ window.GetKeyState(VirtualKey::Control) }; + const auto rCtrlState = window.GetKeyState(VirtualKey::RightControl); + const auto lCtrlState = window.GetKeyState(VirtualKey::LeftControl); + const auto ctrlPressed{ WI_IsFlagSet(ctrlState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rCtrlState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lCtrlState, CoreVirtualKeyStates::Down) }; + + // Check for DebugTap + auto debugTap = this->_settings.GlobalSettings().DebugFeaturesEnabled() && + WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + + const auto dispatchToElevatedWindow = ctrlPressed && !IsRunningElevated(); + + auto sessionType = ""; + if ((shiftPressed || dispatchToElevatedWindow) && !debugTap) + { + // Manually fill in the evaluated profile. + if (newTerminalArgs.ProfileIndex() != nullptr) + { + // We want to promote the index to a GUID because there is no "launch to profile index" command. + const auto profile = _settings.GetProfileForArgs(newTerminalArgs); + if (profile) + { + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + newTerminalArgs.StartingDirectory(_evaluatePathForCwd(profile.EvaluatedStartingDirectory())); + } + } + + if (dispatchToElevatedWindow) + { + _OpenElevatedWT(newTerminalArgs); + sessionType = "ElevatedWindow"; + } + else + { + _OpenNewWindow(newTerminalArgs); + sessionType = "Window"; + } + } + else + { + const auto newPane = _MakePane(newTerminalArgs); + // If the newTerminalArgs caused us to open an elevated window + // instead of creating a pane, it may have returned nullptr. Just do + // nothing then. + if (!newPane) + { + return; + } + if (altPressed && !debugTap) + { + this->_SplitPane(_GetFocusedTabImpl(), + SplitDirection::Automatic, + 0.5f, + newPane); + sessionType = "Pane"; + } + else + { + _CreateNewTabFromPane(newPane); + sessionType = "Tab"; + } + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuCreatedNewTerminalSession", + TraceLoggingDescription("Event emitted when a new terminal was created via the new tab menu"), + TraceLoggingValue(NumberOfTabs(), "NewTabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue(sessionType, "SessionType", "The type of session that was created"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + std::wstring TerminalPage::_evaluatePathForCwd(const std::wstring_view path) + { + return Utils::EvaluateStartingDirectory(_WindowProperties.VirtualWorkingDirectory(), path); + } + + // Method Description: + // - Creates a new connection based on the profile settings + // Arguments: + // - the profile we want the settings from + // - the terminal settings + // Return value: + // - the desired connection + TerminalConnection::ITerminalConnection TerminalPage::_CreateConnectionFromSettings(Profile profile, + IControlSettings settings, + const bool inheritCursor) + { + static const auto textMeasurement = [&]() -> std::wstring_view { + switch (_settings.GlobalSettings().TextMeasurement()) + { + case TextMeasurement::Graphemes: + return L"graphemes"; + case TextMeasurement::Wcswidth: + return L"wcswidth"; + case TextMeasurement::Console: + return L"console"; + default: + return {}; + } + }(); + static const auto ambiguousIsWide = [&]() -> bool { + return _settings.GlobalSettings().AmbiguousWidth() == AmbiguousWidth::Wide; + }(); + + TerminalConnection::ITerminalConnection connection{ nullptr }; + + auto connectionType = profile.ConnectionType(); + Windows::Foundation::Collections::ValueSet valueSet; + + if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && + TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) + { + connection = TerminalConnection::AzureConnection{}; + valueSet = TerminalConnection::ConptyConnection::CreateSettings(winrt::hstring{}, + L".", + L"Azure", + false, + L"", + nullptr, + settings.InitialRows(), + settings.InitialCols(), + winrt::guid(), + profile.Guid()); + } + + else + { + auto settingsInternal{ winrt::get_self(settings) }; + const auto environment = settingsInternal->EnvironmentVariables(); + + // Update the path to be relative to whatever our CWD is. + // + // Refer to the examples in + // https://en.cppreference.com/w/cpp/filesystem/path/append + // + // We need to do this here, to ensure we tell the ConptyConnection + // the correct starting path. If we're being invoked from another + // terminal instance (e.g. `wt -w 0 -d .`), then we have switched our + // CWD to the provided path. We should treat the StartingDirectory + // as relative to the current CWD. + // + // The connection must be informed of the current CWD on + // construction, because the connection might not spawn the child + // process until later, on another thread, after we've already + // restored the CWD to its original value. + auto newWorkingDirectory{ _evaluatePathForCwd(settings.StartingDirectory()) }; + connection = TerminalConnection::ConptyConnection{}; + valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(), + newWorkingDirectory, + settings.StartingTitle(), + settingsInternal->ReloadEnvironmentVariables(), + _WindowProperties.VirtualEnvVars(), + environment, + settings.InitialRows(), + settings.InitialCols(), + winrt::guid(), + profile.Guid()); + + if (inheritCursor) + { + valueSet.Insert(L"inheritCursor", Windows::Foundation::PropertyValue::CreateBoolean(true)); + } + } + + if (!textMeasurement.empty()) + { + valueSet.Insert(L"textMeasurement", Windows::Foundation::PropertyValue::CreateString(textMeasurement)); + } + if (ambiguousIsWide) + { + valueSet.Insert(L"ambiguousIsWide", Windows::Foundation::PropertyValue::CreateBoolean(true)); + } + + if (const auto id = settings.SessionId(); id != winrt::guid{}) + { + valueSet.Insert(L"sessionId", Windows::Foundation::PropertyValue::CreateGuid(id)); + } + + connection.Initialize(valueSet); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "ConnectionCreated", + TraceLoggingDescription("Event emitted upon the creation of a connection"), + TraceLoggingGuid(connectionType, "ConnectionTypeGuid", "The type of the connection"), + TraceLoggingGuid(profile.Guid(), "ProfileGuid", "The profile's GUID"), + TraceLoggingGuid(connection.SessionId(), "SessionGuid", "The WT_SESSION's GUID"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + return connection; + } + + TerminalConnection::ITerminalConnection TerminalPage::_duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent) + { + if (paneContent == nullptr) + { + return nullptr; + } + + const auto& control{ paneContent.GetTermControl() }; + if (control == nullptr) + { + return nullptr; + } + const auto& connection = control.Connection(); + auto profile{ paneContent.GetProfile() }; + + Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; + + if (profile) + { + // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. + profile = GetClosestProfileForDuplicationOfProfile(profile); + controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); + + // Replace the Starting directory with the CWD, if given + const auto workingDirectory = control.WorkingDirectory(); + const auto validWorkingDirectory = !workingDirectory.empty(); + if (validWorkingDirectory) + { + controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); + } + + // To facilitate restarting defterm connections: grab the original + // commandline out of the connection and shove that back into the + // settings. + if (const auto& conpty{ connection.try_as() }) + { + controlSettings.DefaultSettings()->Commandline(conpty.Commandline()); + } + } + + return _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), true); + } + + // Method Description: + // - Called when the settings button is clicked. Launches a background + // thread to open the settings file in the default JSON editor. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_SettingsButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + const auto window = CoreWindow::GetForCurrentThread(); + + // check alt state + const auto rAltState{ window.GetKeyState(VirtualKey::RightMenu) }; + const auto lAltState{ window.GetKeyState(VirtualKey::LeftMenu) }; + const auto altPressed{ WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down) }; + + // check shift state + const auto shiftState{ window.GetKeyState(VirtualKey::Shift) }; + const auto lShiftState{ window.GetKeyState(VirtualKey::LeftShift) }; + const auto rShiftState{ window.GetKeyState(VirtualKey::RightShift) }; + const auto shiftPressed{ WI_IsFlagSet(shiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(lShiftState, CoreVirtualKeyStates::Down) || + WI_IsFlagSet(rShiftState, CoreVirtualKeyStates::Down) }; + + auto target{ SettingsTarget::SettingsUI }; + if (shiftPressed) + { + target = SettingsTarget::SettingsFile; + } + else if (altPressed) + { + target = SettingsTarget::DefaultsFile; + } + + const auto targetAsString = [&target]() { + switch (target) + { + case SettingsTarget::SettingsFile: + return "SettingsFile"; + case SettingsTarget::DefaultsFile: + return "DefaultsFile"; + case SettingsTarget::SettingsUI: + default: + return "UI"; + } + }(); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("Settings", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingValue(targetAsString, "SettingsTarget", "The target settings file or UI"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + _LaunchSettings(target); + } + + // Method Description: + // - Called when the command palette button is clicked. Opens the command palette. + void TerminalPage::_CommandPaletteButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + auto p = LoadCommandPalette(); + p.EnableCommandPaletteMode(CommandPaletteLaunchMode::Action); + p.Visibility(Visibility::Visible); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("CommandPalette", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + // Method Description: + // - Called when the about button is clicked. See _ShowAboutDialog for more info. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_AboutButtonOnClick(const IInspectable&, + const RoutedEventArgs&) + { + _ShowAboutDialog(); + + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemClicked", + TraceLoggingDescription("Event emitted when an item from the new tab menu is invoked"), + TraceLoggingValue(NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingValue("About", "ItemType", "The type of item that was clicked in the new tab menu"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + + // Method Description: + // - Called when the users pressed keyBindings while CommandPaletteElement is open. + // - As of GH#8480, this is also bound to the TabRowControl's KeyUp event. + // That should only fire when focus is in the tab row, which is hard to + // do. Notably, that's possible: + // - When you have enough tabs to make the little scroll arrows appear, + // click one, then hit tab + // - When Narrator is in Scan mode (which is the a11y bug we're fixing here) + // - This method is effectively an extract of TermControl::_KeyHandler and TermControl::_TryHandleKeyBinding. + // Arguments: + // - e: the KeyRoutedEventArgs containing info about the keystroke. + // Return Value: + // - + void TerminalPage::_KeyDownHandler(const Windows::Foundation::IInspectable& /*sender*/, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto keyStatus = e.KeyStatus(); + const auto vkey = gsl::narrow_cast(e.OriginalKey()); + const auto scanCode = gsl::narrow_cast(keyStatus.ScanCode); + const auto modifiers = _GetPressedModifierKeys(); + + // GH#11076: + // For some weird reason we sometimes receive a WM_KEYDOWN + // message without vkey or scanCode if a user drags a tab. + // The KeyChord constructor has a debug assertion ensuring that all KeyChord + // either have a valid vkey/scanCode. This is important, because this prevents + // accidental insertion of invalid KeyChords into classes like ActionMap. + if (!vkey && !scanCode) + { + return; + } + + // Alt-Numpad# input will send us a character once the user releases + // Alt, so we should be ignoring the individual keydowns. The character + // will be sent through the TSFInputControl. See GH#1401 for more + // details + if (modifiers.IsAltPressed() && (vkey >= VK_NUMPAD0 && vkey <= VK_NUMPAD9)) + { + return; + } + + // GH#2235: Terminal::Settings hasn't been modified to differentiate + // between AltGr and Ctrl+Alt yet. + // -> Don't check for key bindings if this is an AltGr key combination. + if (modifiers.IsAltGrPressed()) + { + return; + } + + const auto actionMap = _settings.ActionMap(); + if (!actionMap) + { + return; + } + + const auto cmd = actionMap.GetActionByKeyChord({ + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + vkey, + scanCode, + }); + if (!cmd) + { + return; + } + + if (!_actionDispatch->DoAction(cmd.ActionAndArgs())) + { + return; + } + + if (_commandPaletteIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + CommandPaletteElement().Visibility(Visibility::Collapsed); + } + if (_suggestionsControlIs(Visibility::Visible) && + cmd.ActionAndArgs().Action() != ShortcutAction::ToggleCommandPalette) + { + SuggestionsElement().Visibility(Visibility::Collapsed); + } + + // Let's assume the user has bound the dead key "^" to a sendInput command that sends "b". + // If the user presses the two keys "^a" it'll produce "bâ", despite us marking the key event as handled. + // The following is used to manually "consume" such dead keys and clear them from the keyboard state. + _ClearKeyboardState(vkey, scanCode); + e.Handled(true); + } + + bool TerminalPage::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) + { + const auto modifiers = _GetPressedModifierKeys(); + if (vkey == VK_SPACE && modifiers.IsAltPressed() && down) + { + if (const auto actionMap = _settings.ActionMap()) + { + if (const auto cmd = actionMap.GetActionByKeyChord({ + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + modifiers.IsWinPressed(), + gsl::narrow_cast(vkey), + scanCode, + })) + { + return _actionDispatch->DoAction(cmd.ActionAndArgs()); + } + } + } + return false; + } + + // Method Description: + // - Get the modifier keys that are currently pressed. This can be used to + // find out which modifiers (ctrl, alt, shift) are pressed in events that + // don't necessarily include that state. + // - This is a copy of TermControl::_GetPressedModifierKeys. + // Return Value: + // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. + ControlKeyStates TerminalPage::_GetPressedModifierKeys() noexcept + { + const auto window = CoreWindow::GetForCurrentThread(); + // DONT USE + // != CoreVirtualKeyStates::None + // OR + // == CoreVirtualKeyStates::Down + // Sometimes with the key down, the state is Down | Locked. + // Sometimes with the key up, the state is Locked. + // IsFlagSet(Down) is the only correct solution. + + struct KeyModifier + { + VirtualKey vkey; + ControlKeyStates flags; + }; + + constexpr std::array modifiers{ { + { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, + { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, + { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, + { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, + { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, + { VirtualKey::RightWindows, ControlKeyStates::RightWinPressed }, + { VirtualKey::LeftWindows, ControlKeyStates::LeftWinPressed }, + } }; + + ControlKeyStates flags; + + for (const auto& mod : modifiers) + { + const auto state = window.GetKeyState(mod.vkey); + const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); + + if (isDown) + { + flags |= mod.flags; + } + } + + return flags; + } + + // Method Description: + // - Discards currently pressed dead keys. + // - This is a copy of TermControl::_ClearKeyboardState. + // Arguments: + // - vkey: The vkey of the key pressed. + // - scanCode: The scan code of the key pressed. + void TerminalPage::_ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept + { + std::array keyState; + if (!GetKeyboardState(keyState.data())) + { + return; + } + + // As described in "Sometimes you *want* to interfere with the keyboard's state buffer": + // http://archives.miloush.net/michkap/archive/2006/09/10/748775.html + // > "The key here is to keep trying to pass stuff to ToUnicode until -1 is not returned." + std::array buffer; + while (ToUnicodeEx(vkey, scanCode, keyState.data(), buffer.data(), gsl::narrow_cast(buffer.size()), 0b1, nullptr) < 0) + { + } + } + + // Method Description: + // - Configure the AppKeyBindings to use our ShortcutActionDispatch and the updated ActionMap + // as the object to handle dispatching ShortcutAction events. + // Arguments: + // - bindings: An IActionMapView object to wire up with our event handlers + void TerminalPage::_HookupKeyBindings(const IActionMapView& actionMap) noexcept + { + _bindings->SetDispatch(*_actionDispatch); + _bindings->SetActionMap(actionMap); + } + + // Method Description: + // - Register our event handlers with our ShortcutActionDispatch. The + // ShortcutActionDispatch is responsible for raising the appropriate + // events for an ActionAndArgs. WE'll handle each possible event in our + // own way. + // Arguments: + // - + void TerminalPage::_RegisterActionCallbacks() + { + // Hook up the ShortcutActionDispatch object's events to our handlers. + // They should all be hooked up here, regardless of whether or not + // there's an actual keychord for them. +#define ON_ALL_ACTIONS(action) HOOKUP_ACTION(action); + ALL_SHORTCUT_ACTIONS + INTERNAL_SHORTCUT_ACTIONS +#undef ON_ALL_ACTIONS + } + + // Method Description: + // - 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(const Tab& tab) + { + if (tab == _GetFocusedTab()) + { + TitleChanged.raise(*this, nullptr); + } + } + + // Method Description: + // - Connects event handlers to the TermControl for events that we want to + // handle. This includes: + // * the Copy and Paste events, for setting and retrieving clipboard data + // on the right thread + // Arguments: + // - term: The newly created TermControl to connect the events for + void TerminalPage::_RegisterTerminalEvents(TermControl term) + { + term.RaiseNotice({ this, &TerminalPage::_ControlNoticeRaisedHandler }); + + term.WriteToClipboard({ get_weak(), &TerminalPage::_copyToClipboard }); + term.PasteFromClipboard({ this, &TerminalPage::_PasteFromClipboardHandler }); + + term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); + + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + + term.ConnectionStateChanged({ get_weak(), &TerminalPage::_ConnectionStateChangedHandler }); + + term.PropertyChanged([weakThis = get_weak()](auto& /*sender*/, auto& e) { + if (auto page{ weakThis.get() }) + { + if (e.PropertyName() == L"BackgroundBrush") + { + page->_updateThemeColors(); + } + } + }); + + term.ShowWindowChanged({ get_weak(), &TerminalPage::_ShowWindowChangedHandler }); + term.SearchMissingCommand({ get_weak(), &TerminalPage::_SearchMissingCommandHandler }); + term.WindowSizeChanged({ get_weak(), &TerminalPage::_WindowSizeChanged }); + + // Don't even register for the event if the feature is compiled off. + if constexpr (Feature_ShellCompletions::IsEnabled()) + { + term.CompletionsChanged({ get_weak(), &TerminalPage::_ControlCompletionsChangedHandler }); + } + winrt::weak_ref weakTerm{ term }; + term.ContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), false); + } + }); + term.SelectionContextMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateContextMenu(weakTerm.get(), sender.try_as(), true); + } + }); + if constexpr (Feature_QuickFix::IsEnabled()) + { + term.QuickFixMenu().Opening([weak = get_weak(), weakTerm](auto&& sender, auto&& /*args*/) { + if (const auto& page{ weak.get() }) + { + page->_PopulateQuickFixMenu(weakTerm.get(), sender.try_as()); + } + }); + } + } + + // Method Description: + // - Connects event handlers to the Tab for events that we want to + // handle. This includes: + // * the TitleChanged event, for changing the text of the tab + // * the Color{Selected,Cleared} events to change the color of a tab. + // Arguments: + // - hostingTab: The Tab that's hosting this TermControl instance + void TerminalPage::_RegisterTabEvents(Tab& hostingTab) + { + auto weakTab{ hostingTab.get_weak() }; + auto weakThis{ get_weak() }; + // PropertyChanged is the generic mechanism by which the Tab + // communicates changes to any of its observable properties, including + // the Title + hostingTab.PropertyChanged([weakTab, weakThis](auto&&, const WUX::Data::PropertyChangedEventArgs& args) { + auto page{ weakThis.get() }; + auto tab{ weakTab.get() }; + if (page && tab) + { + const auto propertyName = args.PropertyName(); + if (propertyName == L"Title") + { + page->_UpdateTitle(*tab); + } + else if (propertyName == L"Content") + { + if (*tab == page->_GetFocusedTab()) + { + const auto children = page->_tabContent.Children(); + + children.Clear(); + if (auto content = tab->Content()) + { + page->_tabContent.Children().Append(std::move(content)); + } + + tab->Focus(FocusState::Programmatic); + } + } + } + }); + + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + + hostingTab.RestartTerminalRequested({ get_weak(), &TerminalPage::_restartPaneConnection }); + } + + // Method Description: + // - Helper to manually exit "zoom" when certain actions take place. + // Anything that modifies the state of the pane tree should probably + // un-zoom the focused pane first, so that the user can see the full pane + // tree again. These actions include: + // * Splitting a new pane + // * Closing a pane + // * Moving focus between panes + // * Resizing a pane + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_UnZoomIfNeeded() + { + if (const auto activeTab{ _GetFocusedTabImpl() }) + { + if (activeTab->IsZoomed()) + { + // Remove the content from the tab first, so Pane::UnZoom can + // re-attach the content to the tree w/in the pane + _tabContent.Children().Clear(); + // In ExitZoom, we'll change the Tab's Content(), triggering the + // content changed event, which will re-attach the tab's new content + // root to the tree. + activeTab->ExitZoom(); + } + } + } + + // Method Description: + // - Attempt to move focus between panes, as to focus the child on + // the other side of the separator. See Pane::NavigateFocus for details. + // - Moves the focus of the currently focused tab. + // Arguments: + // - direction: The direction to move the focus in. + // Return Value: + // - Whether changing the focus succeeded. This allows a keychord to propagate + // to the terminal when no other panes are present (GH#6219) + bool TerminalPage::_MoveFocus(const FocusDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->NavigateFocus(direction); + } + return false; + } + + // Method Description: + // - Attempt to swap the positions of the focused pane with another pane. + // See Pane::SwapPane for details. + // Arguments: + // - direction: The direction to move the focused pane in. + // Return Value: + // - true if panes were swapped. + bool TerminalPage::_SwapPane(const FocusDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + return tabImpl->SwapPane(direction); + } + return false; + } + + TermControl TerminalPage::_GetActiveControl() const + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->GetActiveTerminalControl(); + } + return nullptr; + } + + CommandPalette TerminalPage::LoadCommandPalette() + { + if (const auto p = CommandPaletteElement()) + { + return p; + } + + return _loadCommandPaletteSlowPath(); + } + bool TerminalPage::_commandPaletteIs(WUX::Visibility visibility) + { + const auto p = CommandPaletteElement(); + return p && p.Visibility() == visibility; + } + + CommandPalette TerminalPage::_loadCommandPaletteSlowPath() + { + const auto p = FindName(L"CommandPaletteElement").as(); + + p.SetActionMap(_settings.ActionMap()); + + // When the visibility of the command palette changes to "collapsed", + // the palette has been closed. Toss focus back to the currently active control. + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (_commandPaletteIs(Visibility::Collapsed)) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.CommandLineExecutionRequested({ this, &TerminalPage::_OnCommandLineExecutionRequested }); + p.SwitchToTabRequested({ this, &TerminalPage::_OnSwitchToTabRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + + SuggestionsControl TerminalPage::LoadSuggestionsUI() + { + if (const auto p = SuggestionsElement()) + { + return p; + } + + return _loadSuggestionsElementSlowPath(); + } + bool TerminalPage::_suggestionsControlIs(WUX::Visibility visibility) + { + const auto p = SuggestionsElement(); + return p && p.Visibility() == visibility; + } + + SuggestionsControl TerminalPage::_loadSuggestionsElementSlowPath() + { + const auto p = FindName(L"SuggestionsElement").as(); + + p.RegisterPropertyChangedCallback(UIElement::VisibilityProperty(), [this](auto&&, auto&&) { + if (SuggestionsElement().Visibility() == Visibility::Collapsed) + { + _FocusActiveControl(nullptr, nullptr); + } + }); + p.DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + p.PreviewAction({ this, &TerminalPage::_PreviewActionHandler }); + + return p; + } + + // Method Description: + // - Warn the user that they are about to close all open windows, then + // signal that we want to close everything. + safe_void_coroutine TerminalPage::RequestQuit() + { + if (!_displayingCloseDialog) + { + _displayingCloseDialog = true; + + const auto weak = get_weak(); + auto warningResult = co_await _ShowQuitDialog(); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + _displayingCloseDialog = false; + + if (warningResult != ContentDialogResult::Primary) + { + co_return; + } + + QuitRequested.raise(nullptr, nullptr); + } + } + + void TerminalPage::PersistState() + { + // This method may be called for a window even if it hasn't had a tab yet or lost all of them. + // We shouldn't persist such windows. + const auto tabCount = _tabs.Size(); + if (_startupState != StartupState::Initialized || tabCount == 0) + { + return; + } + + std::vector actions; + + for (auto tab : _tabs) + { + auto t = winrt::get_self(tab); + auto tabActions = t->BuildStartupActions(BuildStartupKind::Persist); + actions.insert(actions.end(), std::make_move_iterator(tabActions.begin()), std::make_move_iterator(tabActions.end())); + } + + // Avoid persisting a window with zero tabs, because `BuildStartupActions` happened to return an empty vector. + if (actions.empty()) + { + return; + } + + // if the focused tab was not the last tab, restore that + auto idx = _GetFocusedTabIndex(); + if (idx && idx != tabCount - 1) + { + ActionAndArgs action; + action.Action(ShortcutAction::SwitchToTab); + SwitchToTabArgs switchToTabArgs{ idx.value() }; + action.Args(switchToTabArgs); + + actions.emplace_back(std::move(action)); + } + + // If the user set a custom name, save it + if (const auto& windowName{ _WindowProperties.WindowName() }; !windowName.empty()) + { + ActionAndArgs action; + action.Action(ShortcutAction::RenameWindow); + RenameWindowArgs args{ windowName }; + action.Args(args); + + actions.emplace_back(std::move(action)); + } + + WindowLayout layout; + layout.TabLayout(winrt::single_threaded_vector(std::move(actions))); + + auto mode = LaunchMode::DefaultMode; + WI_SetFlagIf(mode, LaunchMode::FullscreenMode, _isFullscreen); + WI_SetFlagIf(mode, LaunchMode::FocusMode, _isInFocusMode); + WI_SetFlagIf(mode, LaunchMode::MaximizedMode, _isMaximized); + + layout.LaunchMode({ mode }); + + // Only save the content size because the tab size will be added on load. + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size windowSize{ contentWidth, contentHeight }; + + layout.InitialSize(windowSize); + + // We don't actually know our own position. So we have to ask the window + // layer for that. + const auto launchPosRequest{ winrt::make() }; + RequestLaunchPosition.raise(*this, launchPosRequest); + layout.InitialPosition(launchPosRequest.Position()); + + ApplicationState::SharedInstance().AppendPersistedWindowLayout(layout); + } + + // Method Description: + // - Close the terminal app. If there is more + // than one tab opened, show a warning dialog. + safe_void_coroutine TerminalPage::CloseWindow() + { + if (_HasMultipleTabs() && + _settings.GlobalSettings().ConfirmCloseAllTabs() && + !_displayingCloseDialog) + { + if (_newTabButton && _newTabButton.Flyout()) + { + _newTabButton.Flyout().Hide(); + } + _DismissTabContextMenus(); + _displayingCloseDialog = true; + auto warningResult = co_await _ShowCloseWarningDialog(); + _displayingCloseDialog = false; + + if (warningResult != ContentDialogResult::Primary) + { + co_return; + } + } + + CloseWindowRequested.raise(*this, nullptr); + } + + std::vector TerminalPage::Panes() const + { + std::vector panes; + + for (const auto tab : _tabs) + { + const auto impl = _GetTabImpl(tab); + if (!impl) + { + continue; + } + + impl->GetRootPane()->WalkTree([&](auto&& pane) { + if (auto content = pane->GetContent()) + { + panes.push_back(std::move(content)); + } + }); + } + + return panes; + } + + // Method Description: + // - Move the viewport of the terminal of the currently focused tab up or + // down a number of lines. + // Arguments: + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + // - rowsToScroll: a number of lines to move the viewport. If not provided we will use a system default. + void TerminalPage::_Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + uint32_t realRowsToScroll; + if (rowsToScroll == nullptr) + { + // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page + realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? + tabImpl->GetActiveTerminalControl().ViewHeight() : + _systemRowsToScroll; + } + else + { + // use the custom value specified in the command + realRowsToScroll = rowsToScroll.Value(); + } + auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); + tabImpl->Scroll(scrollDelta); + } + } + + // Method Description: + // - Moves the currently active pane on the currently active tab to the + // specified tab. If the tab index is greater than the number of + // tabs, then a new tab will be created for the pane. Similarly, if a pane + // is the last remaining pane on a tab, that tab will be closed upon moving. + // - No move will occur if the tabIdx is the same as the current tab, or if + // the specified tab is not a host of terminals (such as the settings tab). + // - If the Window is specified, the pane will instead be detached and moved + // to the window with the given name/id. + // Return Value: + // - true if the pane was successfully moved to the new tab. + bool TerminalPage::_MovePane(MovePaneArgs args) + { + const auto tabIdx{ args.TabIndex() }; + const auto windowId{ args.Window() }; + + auto focusedTab{ _GetFocusedTabImpl() }; + + if (!focusedTab) + { + return false; + } + + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + if (!windowId.empty()) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto pane{ tabImpl->GetActivePane() }) + { + auto startupActions = pane->BuildStartupActions(0, 1, BuildStartupKind::MovePane); + _DetachPaneFromWindow(pane); + _MoveContent(std::move(startupActions.args), windowId, tabIdx); + focusedTab->DetachPane(); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewWindow"), + L"TerminalPageMovePaneToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingWindow2", windowId), + L"TerminalPageMovePaneToExistingWindow" /* unique name for this notification category */); + } + } + return true; + } + } + } + + // If we are trying to move from the current tab to the current tab do nothing. + if (_GetFocusedTabIndex() == tabIdx) + { + return false; + } + + // Moving the pane from the current tab might close it, so get the next + // tab before its index changes. + if (tabIdx < _tabs.Size()) + { + auto targetTab = _GetTabImpl(_tabs.GetAt(tabIdx)); + // if the selected tab is not a host of terminals (e.g. settings) + // don't attempt to add a pane to it. + if (!targetTab) + { + return false; + } + auto pane = focusedTab->DetachPane(); + targetTab->AttachPane(pane); + _SetFocusedTab(*targetTab); + + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = targetTab->Title(); + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_PaneMovedAnnouncement_ExistingTab", tabTitle), + L"TerminalPageMovePaneToExistingTab" /* unique name for this notification category */); + } + } + else + { + auto pane = focusedTab->DetachPane(); + _CreateNewTabFromPane(pane); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_(L"TerminalPage_PaneMovedAnnouncement_NewTab"), + L"TerminalPageMovePaneToNewTab" /* unique name for this notification category */); + } + } + + return true; + } + + // Detach a tree of panes from this terminal. Helper used for moving panes + // and tabs to other windows. + void TerminalPage::_DetachPaneFromWindow(std::shared_ptr pane) + { + pane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + _manager.Detach(control); + } + }); + } + + void TerminalPage::_DetachTabFromWindow(const winrt::com_ptr& tab) + { + // Detach the root pane, which will act like the whole tab got detached. + if (const auto rootPane = tab->GetRootPane()) + { + _DetachPaneFromWindow(rootPane); + } + } + + // Method Description: + // - Serialize these actions to json, and raise them as a RequestMoveContent + // event. Our Window will raise that to the window manager / monarch, who + // will dispatch this blob of json back to the window that should handle + // this. + // - `actions` will be emptied into a winrt IVector as a part of this method + // and should be expected to be empty after this call. + void TerminalPage::_MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint) + { + const auto winRtActions{ winrt::single_threaded_vector(std::move(actions)) }; + const auto str{ ActionAndArgs::Serialize(winRtActions) }; + const auto request = winrt::make_self(windowName, + str, + tabIndex); + if (dragPoint.has_value()) + { + request->WindowPosition(*dragPoint); + } + RequestMoveContent.raise(*this, *request); + } + + bool TerminalPage::_MoveTab(winrt::com_ptr tab, MoveTabArgs args) + { + if (!tab) + { + return false; + } + + // If there was a windowId in the action, try to move it to the + // specified window instead of moving it in our tab row. + const auto windowId{ args.Window() }; + if (!windowId.empty()) + { + // if the windowId is the same as our name, do nothing + if (windowId == WindowProperties().WindowName() || + windowId == winrt::to_hstring(WindowProperties().WindowId())) + { + return true; + } + + if (tab) + { + auto startupActions = tab->BuildStartupActions(BuildStartupKind::Content); + _DetachTabFromWindow(tab); + _MoveContent(std::move(startupActions), windowId, 0); + _RemoveTab(*tab); + if (auto autoPeer = Automation::Peers::FrameworkElementAutomationPeer::FromElement(*this)) + { + const auto tabTitle = tab->Title(); + if (windowId == L"new") + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_TabMovedAnnouncement_NewWindow", tabTitle), + L"TerminalPageMoveTabToNewWindow" /* unique name for this notification category */); + } + else + { + autoPeer.RaiseNotificationEvent(Automation::Peers::AutomationNotificationKind::ActionCompleted, + Automation::Peers::AutomationNotificationProcessing::ImportantMostRecent, + RS_fmt(L"TerminalPage_TabMovedAnnouncement_Default", tabTitle, windowId), + L"TerminalPageMoveTabToExistingWindow" /* unique name for this notification category */); + } + } + return true; + } + } + + const auto direction = args.Direction(); + if (direction != MoveTabDirection::None) + { + // Use the requested tab, if provided. Otherwise, use the currently + // focused tab. + const auto tabIndex = til::coalesce(_GetTabIndex(*tab), + _GetFocusedTabIndex()); + if (tabIndex) + { + const auto currentTabIndex = tabIndex.value(); + const auto delta = direction == MoveTabDirection::Forward ? 1 : -1; + _TryMoveTab(currentTabIndex, currentTabIndex + delta); + } + } + + return true; + } + + // 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. + void TerminalPage::_activePaneChanged(winrt::TerminalApp::Tab sender, + Windows::Foundation::IInspectable /*args*/) + { + if (const auto tab{ _GetTabImpl(sender) }) + { + // Possibly update the icon of the tab. + _UpdateTabIcon(*tab); + + _updateThemeColors(); + + // Update the taskbar progress as well. We'll raise our own + // SetTaskbarProgress event here, to get tell the hosting + // application to re-query this value from us. + SetTaskbarProgress.raise(*this, nullptr); + + auto profile = tab->GetFocusedProfile(); + _UpdateBackground(profile); + } + + _adjustProcessPriorityThrottled->Run(); + } + + uint32_t TerminalPage::NumberOfTabs() const + { + return _tabs.Size(); + } + + // Method Description: + // - Called when it is determined that an existing tab or pane should be + // attached to our window. content represents a blob of JSON describing + // some startup actions for rebuilding the specified panes. They will + // include `__content` properties with the GUID of the existing + // ControlInteractivity's we should use, rather than starting new ones. + // - _MakePane is already enlightened to use the ContentId property to + // reattach instead of create new content, so this method simply needs to + // parse the JSON and pump it into our action handler. Almost the same as + // doing something like `wt -w 0 nt`. + void TerminalPage::AttachContent(IVector args, uint32_t tabIndex) + { + if (args == nullptr || + args.Size() == 0) + { + return; + } + + std::vector existingTabs{}; + existingTabs.reserve(_tabs.Size()); + for (const auto& tab : _tabs) + { + existingTabs.emplace_back(tab); + } + + const auto& firstAction = args.GetAt(0); + const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane }; + + // `splitPane` allows the user to specify which tab to split. In that + // case, split specifically the requested pane. + // + // If there's not enough tabs, then just turn this pane into a new tab. + // + // If the first action is `newTab`, the index is always going to be 0, + // so don't do anything in that case. + if (firstIsSplitPane && tabIndex < _tabs.Size()) + { + _SelectTab(tabIndex); + } + + for (const auto& action : args) + { + _actionDispatch->DoAction(action); + } + + // After handling all the actions, then re-check the tabIndex. We might + // have been called as a part of a tab drag/drop. In that case, the + // tabIndex is actually relevant, and we need to move the tab we just + // made into position. + if (!firstIsSplitPane && tabIndex != -1) + { + const auto newTabs = _CollectNewTabs(existingTabs); + if (!newTabs.empty()) + { + _MoveTabsToIndex(newTabs, tabIndex); + _SetSelectedTabs(newTabs, newTabs.front()); + } + } + } + + // Method Description: + // - Split the focused pane of the given tab, either horizontally or vertically, and place the + // given pane accordingly + // Arguments: + // - tab: The tab that is going to be split. + // - newPane: the pane to add to our tree of panes + // - splitDirection: one value from the TerminalApp::SplitDirection enum, indicating how the + // new pane should be split from its parent. + // - splitSize: the size of the split + void TerminalPage::_SplitPane(const winrt::com_ptr& tab, + const SplitDirection splitDirection, + const float splitSize, + std::shared_ptr newPane) + { + auto activeTab = tab; + // Clever hack for a crash in startup, with multiple sub-commands. Say + // you have the following commandline: + // + // wtd nt -p "elevated cmd" ; sp -p "elevated cmd" ; sp -p "Command Prompt" + // + // Where "elevated cmd" is an elevated profile. + // + // In that scenario, we won't dump off the commandline immediately to an + // elevated window, because it's got the final unelevated split in it. + // However, when we get to that command, there won't be a tab yet. So + // we'd crash right about here. + // + // Instead, let's just promote this first split to be a tab instead. + // Crash avoided, and we don't need to worry about inserting a new-tab + // command in at the start. + if (!tab) + { + if (_tabs.Size() == 0) + { + _CreateNewTabFromPane(newPane); + return; + } + else + { + activeTab = _GetFocusedTabImpl(); + } + } + + // For now, prevent splitting the _settingsTab. We can always revisit this later. + if (*activeTab == _settingsTab) + { + return; + } + + // If the caller is calling us with the return value of _MakePane + // directly, it's possible that nullptr was returned, if the connections + // was supposed to be launched in an elevated window. In that case, do + // nothing here. We don't have a pane with which to create the split. + if (!newPane) + { + return; + } + const auto contentWidth = static_cast(_tabContent.ActualWidth()); + const auto contentHeight = static_cast(_tabContent.ActualHeight()); + const winrt::Windows::Foundation::Size availableSpace{ contentWidth, contentHeight }; + + const auto realSplitType = activeTab->PreCalculateCanSplit(splitDirection, splitSize, availableSpace); + if (!realSplitType) + { + return; + } + + _UnZoomIfNeeded(); + auto [original, newGuy] = activeTab->SplitPane(*realSplitType, splitSize, newPane); + + // After GH#6586, the control will no longer focus itself + // automatically when it's finished being laid out. Manually focus + // the control here instead. + if (_startupState == StartupState::Initialized) + { + if (const auto& content{ newGuy->GetContent() }) + { + content.Focus(FocusState::Programmatic); + } + } + } + + // Method Description: + // - Switches the split orientation of the currently focused pane. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ToggleSplitOrientation() + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + tabImpl->ToggleSplitOrientation(); + } + } + + // Method Description: + // - Attempt to move a separator between panes, as to resize each child on + // either size of the separator. See Pane::ResizePane for details. + // - Moves a separator on the currently focused tab. + // Arguments: + // - direction: The direction to move the separator in. + // Return Value: + // - + void TerminalPage::_ResizePane(const ResizeDirection& direction) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + _UnZoomIfNeeded(); + tabImpl->ResizePane(direction); + } + } + + // Method Description: + // - Move the viewport of the terminal of the currently focused tab up or + // down a page. The page length will be dependent on the terminal view height. + // Arguments: + // - scrollDirection: ScrollUp will move the viewport up, ScrollDown will move the viewport down + void TerminalPage::_ScrollPage(ScrollDirection scrollDirection) + { + // Do nothing if for some reason, there's no terminal tab in focus. We don't want to crash. + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + if (const auto& control{ _GetActiveControl() }) + { + const auto termHeight = control.ViewHeight(); + auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); + tabImpl->Scroll(scrollDelta); + } + } + } + + void TerminalPage::_ScrollToBufferEdge(ScrollDirection scrollDirection) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + auto scrollDelta = _ComputeScrollDelta(scrollDirection, INT_MAX); + tabImpl->Scroll(scrollDelta); + } + } + + // Method Description: + // - Gets the title of the currently focused terminal control. If there + // isn't a control selected for any reason, returns "Terminal" + // Arguments: + // - + // Return Value: + // - the title of the focused control if there is one, else "Terminal" + hstring TerminalPage::Title() + { + if (_settings.GlobalSettings().ShowTitleInTitlebar()) + { + if (const auto tab{ _GetFocusedTab() }) + { + return tab.Title(); + } + } + return { L"Terminal" }; + } + + // Method Description: + // - Handles the special case of providing a text override for the UI shortcut due to VK_OEM issue. + // Looks at the flags from the KeyChord modifiers and provides a concatenated string value of all + // in the same order that XAML would put them as well. + // Return Value: + // - a string representation of the key modifiers for the shortcut + //NOTE: This needs to be localized with https://github.com/microsoft/terminal/issues/794 if XAML framework issue not resolved before then + static std::wstring _FormatOverrideShortcutText(VirtualKeyModifiers modifiers) + { + std::wstring buffer{ L"" }; + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Control)) + { + buffer += L"Ctrl+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Shift)) + { + buffer += L"Shift+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Menu)) + { + buffer += L"Alt+"; + } + + if (WI_IsFlagSet(modifiers, VirtualKeyModifiers::Windows)) + { + buffer += L"Win+"; + } + + return buffer; + } + + // Method Description: + // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. + // Takes into account a special case for an error condition for a comma + // Arguments: + // - MenuFlyoutItem that will be displayed, and a KeyChord to map an accelerator + void TerminalPage::_SetAcceleratorForMenuItem(WUX::Controls::MenuFlyoutItem& menuItem, + const KeyChord& keyChord) + { +#ifdef DEP_MICROSOFT_UI_XAML_708_FIXED + // work around https://github.com/microsoft/microsoft-ui-xaml/issues/708 in case of VK_OEM_COMMA + if (keyChord.Vkey() != VK_OEM_COMMA) + { + // use the XAML shortcut to give us the automatic capabilities + auto menuShortcut = Windows::UI::Xaml::Input::KeyboardAccelerator{}; + + // TODO: Modify this when https://github.com/microsoft/terminal/issues/877 is resolved + menuShortcut.Key(static_cast(keyChord.Vkey())); + + // add the modifiers to the shortcut + menuShortcut.Modifiers(keyChord.Modifiers()); + + // add to the menu + menuItem.KeyboardAccelerators().Append(menuShortcut); + } + else // we've got a comma, so need to just use the alternate method +#endif + { + // extract the modifier and key to a nice format + auto overrideString = _FormatOverrideShortcutText(keyChord.Modifiers()); + auto mappedCh = MapVirtualKeyW(keyChord.Vkey(), MAPVK_VK_TO_CHAR); + if (mappedCh != 0) + { + menuItem.KeyboardAcceleratorTextOverride(overrideString + gsl::narrow_cast(mappedCh)); + } + } + } + + // Method Description: + // - Calculates the appropriate size to snap to in the given direction, for + // the given dimension. If the global setting `snapToGridOnResize` is set + // to `false`, this will just immediately return the provided dimension, + // effectively disabling snapping. + // - See Pane::CalcSnappedDimension + float TerminalPage::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const + { + if (_settings && _settings.GlobalSettings().SnapToGridOnResize()) + { + if (const auto tabImpl{ _GetFocusedTabImpl() }) + { + return tabImpl->CalcSnappedDimension(widthOrHeight, dimension); + } + } + return dimension; + } + + // Function Description: + // - This function is called when the `TermControl` requests that we send + // it the clipboard's content. + // - Retrieves the data from the Windows Clipboard and converts it to text. + // - Shows warnings if the clipboard is too big or contains multiple lines + // of text. + // - Sends the text back to the TermControl through the event's + // `HandleClipboardData` member function. + // - Does some of this in a background thread, as to not hang/crash the UI thread. + // Arguments: + // - eventArgs: the PasteFromClipboard event sent from the TermControl + safe_void_coroutine TerminalPage::_PasteFromClipboardHandler(const IInspectable sender, const PasteFromClipboardEventArgs eventArgs) + try + { + // The old Win32 clipboard API as used below is somewhere in the order of 300-1000x faster than + // the WinRT one on average, depending on CPU load. Don't use the WinRT clipboard API if you can. + const auto weakThis = get_weak(); + const auto dispatcher = Dispatcher(); + const auto globalSettings = _settings.GlobalSettings(); + const auto bracketedPaste = eventArgs.BracketedPasteEnabled(); + const auto sourceId = sender.try_as().Id(); + + // GetClipboardData might block for up to 30s for delay-rendered contents. + co_await winrt::resume_background(); + + winrt::hstring text; + if (const auto clipboard = clipboard::open(nullptr)) + { + text = clipboard::read(); + } + + if (!bracketedPaste && globalSettings.TrimPaste()) + { + text = winrt::hstring{ Utils::TrimPaste(text) }; + } + + if (text.empty()) + { + co_return; + } + + bool warnMultiLine = false; + switch (globalSettings.WarnAboutMultiLinePaste()) + { + case WarnAboutMultiLinePaste::Automatic: + // NOTE that this is unsafe, because a shell that doesn't support bracketed paste + // will allow an attacker to enable the mode, not realize that, and then accept + // the paste as if it was a series of legitimate commands. See GH#13014. + warnMultiLine = !bracketedPaste; + break; + case WarnAboutMultiLinePaste::Always: + warnMultiLine = true; + break; + default: + warnMultiLine = false; + break; + } + + if (warnMultiLine) + { + const std::wstring_view view{ text }; + warnMultiLine = view.find_first_of(L"\r\n") != std::wstring_view::npos; + } + + constexpr std::size_t minimumSizeForWarning = 1024 * 5; // 5 KiB + const auto warnLargeText = text.size() > minimumSizeForWarning && globalSettings.WarnAboutLargePaste(); + + if (warnMultiLine || warnLargeText) + { + co_await wil::resume_foreground(dispatcher); + + if (const auto strongThis = weakThis.get()) + { + // We have to initialize the dialog here to be able to change the text of the text block within it + std::ignore = FindName(L"MultiLinePasteDialog"); + + // WinUI absolutely cannot deal with large amounts of text (at least O(n), possibly O(n^2), + // so we limit the string length here and add an ellipsis if necessary. + auto clipboardText = text; + if (clipboardText.size() > 1024) + { + const std::wstring_view view{ text }; + // Make sure we don't cut in the middle of a surrogate pair + const auto len = til::utf16_iterate_prev(view, 512); + clipboardText = til::hstring_format(FMT_COMPILE(L"{}\n…"), view.substr(0, len)); + } + + ClipboardText().Text(std::move(clipboardText)); + + // The vertical offset on the scrollbar does not reset automatically, so reset it manually + ClipboardContentScrollViewer().ScrollToVerticalOffset(0); + + auto warningResult = ContentDialogResult::Primary; + if (warnMultiLine) + { + warningResult = co_await _ShowMultiLinePasteWarningDialog(); + } + else if (warnLargeText) + { + warningResult = co_await _ShowLargePasteWarningDialog(); + } + + // Clear the clipboard text so it doesn't lie around in memory + ClipboardText().Text({}); + + if (warningResult != ContentDialogResult::Primary) + { + // user rejected the paste + co_return; + } + } + + co_await winrt::resume_background(); + } + + // This will end up calling ConptyConnection::WriteInput which calls WriteFile which may block for + // an indefinite amount of time. Avoid freezes and deadlocks by running this on a background thread. + assert(!dispatcher.HasThreadAccess()); + eventArgs.HandleClipboardData(text); + + // GH#18821: If broadcast input is active, paste the same text into all other + // panes on the tab. We do this here (rather than re-reading the + // clipboard per-pane) so that only one paste warning is shown. + co_await wil::resume_foreground(dispatcher); + if (const auto strongThis = weakThis.get()) + { + if (const auto& tab{ strongThis->_GetFocusedTabImpl() }) + { + if (tab->TabStatus().IsInputBroadcastActive()) + { + tab->GetRootPane()->WalkTree([&](auto&& pane) { + if (const auto control = pane->GetTerminalControl()) + { + if (control.ContentId() != sourceId && !control.ReadOnly()) + { + control.RawWriteString(text); + } + } + }); + } + } + } + } + CATCH_LOG(); + + void TerminalPage::_OpenHyperlinkHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs) + { + try + { + auto parsed = winrt::Windows::Foundation::Uri(eventArgs.Uri()); + if (_IsUriSupported(parsed)) + { + ShellExecute(nullptr, L"open", eventArgs.Uri().c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + else + { + _ShowCouldNotOpenDialog(RS_(L"UnsupportedSchemeText"), eventArgs.Uri()); + } + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _ShowCouldNotOpenDialog(RS_(L"InvalidUriText"), eventArgs.Uri()); + } + } + + // Method Description: + // - Opens up a dialog box explaining why we could not open a URI + // Arguments: + // - The reason (unsupported scheme, invalid uri, potentially more in the future) + // - The uri + void TerminalPage::_ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri) + { + if (auto presenter{ _dialogPresenter.get() }) + { + // FindName needs to be called first to actually load the xaml object + auto unopenedUriDialog = FindName(L"CouldNotOpenUriDialog").try_as(); + + // Insert the reason and the URI + CouldNotOpenUriReason().Text(reason); + UnopenedUri().Text(uri); + + // Show the dialog + presenter.ShowDialog(unopenedUriDialog); + } + } + + // Method Description: + // - Determines if the given URI is currently supported + // Arguments: + // - The parsed URI + // Return value: + // - True if we support it, false otherwise + bool TerminalPage::_IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri) + { + if (parsedUri.SchemeName() == L"http" || parsedUri.SchemeName() == L"https") + { + return true; + } + if (parsedUri.SchemeName() == L"file") + { + const auto host = parsedUri.Host(); + // If no hostname was provided or if the hostname was "localhost", Host() will return an empty string + // and we allow it + if (host == L"") + { + return true; + } + + // GH#10188: WSL paths are okay. We'll let those through. + if (host == L"wsl$" || host == L"wsl.localhost") + { + return true; + } + + // TODO: by the OSC 8 spec, if a hostname (other than localhost) is provided, we _should_ be + // comparing that value against what is returned by GetComputerNameExW and making sure they match. + // However, ShellExecute does not seem to be happy with file URIs of the form + // file://{hostname}/path/to/file.ext + // and so while we could do the hostname matching, we do not know how to actually open the URI + // if its given in that form. So for now we ignore all hostnames other than localhost + return false; + } + + // In this case, the app manually output a URI other than file:// or + // http(s)://. We'll trust the user knows what they're doing when + // clicking on those sorts of links. + // See discussion in GH#7562 for more details. + return true; + } + + // Important! Don't take this eventArgs by reference, we need to extend the + // lifetime of it to the other side of the co_await! + safe_void_coroutine TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, + const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) + { + auto weakThis = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + if (auto page = weakThis.get()) + { + auto message = eventArgs.Message(); + + winrt::hstring title; + + switch (eventArgs.Level()) + { + case NoticeLevel::Debug: + title = RS_(L"NoticeDebug"); //\xebe8 + break; + case NoticeLevel::Info: + title = RS_(L"NoticeInfo"); // \xe946 + break; + case NoticeLevel::Warning: + title = RS_(L"NoticeWarning"); //\xe7ba + break; + case NoticeLevel::Error: + title = RS_(L"NoticeError"); //\xe783 + break; + } + + page->_ShowControlNoticeDialog(title, message); + } + } + + void TerminalPage::_ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message) + { + if (auto presenter{ _dialogPresenter.get() }) + { + // FindName needs to be called first to actually load the xaml object + auto controlNoticeDialog = FindName(L"ControlNoticeDialog").try_as(); + + ControlNoticeDialog().Title(winrt::box_value(title)); + + // Insert the message + NoticeMessage().Text(message); + + // Show the dialog + presenter.ShowDialog(controlNoticeDialog); + } + } + + // Method Description: + // - Copy text from the focused terminal to the Windows Clipboard + // Arguments: + // - dismissSelection: if not enabled, copying text doesn't dismiss the selection + // - singleLine: if enabled, copy contents as a single line of text + // - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection + // - formats: dictate which formats need to be copied + // Return Value: + // - true iff we we able to copy text (if a selection was active) + bool TerminalPage::_CopyText(const bool dismissSelection, const bool singleLine, const bool withControlSequences, const CopyFormat formats) + { + if (const auto& control{ _GetActiveControl() }) + { + return control.CopySelectionToClipboard(dismissSelection, singleLine, withControlSequences, formats); + } + return false; + } + + // Method Description: + // - Send an event (which will be caught by AppHost) to set the progress indicator on the taskbar + // Arguments: + // - sender (not used) + // - eventArgs: the arguments specifying how to set the progress indicator + safe_void_coroutine TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) + { + const auto weak = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + if (const auto strong = weak.get()) + { + SetTaskbarProgress.raise(*this, nullptr); + } + } + + // Method Description: + // - Send an event (which will be caught by AppHost) to change the show window state of the entire hosting window + // Arguments: + // - sender (not used) + // - args: the arguments specifying how to set the display status to ShowWindow for our window handle + void TerminalPage::_ShowWindowChangedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::ShowWindowArgs args) + { + ShowWindowChanged.raise(*this, args); + } + + Windows::Foundation::IAsyncOperation> TerminalPage::_FindPackageAsync(hstring query) + { + const PackageManager packageManager = WindowsPackageManagerFactory::CreatePackageManager(); + PackageCatalogReference catalogRef{ + packageManager.GetPredefinedPackageCatalog(PredefinedPackageCatalog::OpenWindowsCatalog) + }; + catalogRef.PackageCatalogBackgroundUpdateInterval(std::chrono::hours(24)); + + ConnectResult connectResult{ nullptr }; + for (int retries = 0;;) + { + connectResult = catalogRef.Connect(); + if (connectResult.Status() == ConnectResultStatus::Ok) + { + break; + } + + if (++retries == 3) + { + co_return nullptr; + } + } + + PackageCatalog catalog = connectResult.PackageCatalog(); + PackageMatchFilter filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); + filter.Value(query); + filter.Field(PackageMatchField::Command); + filter.Option(PackageFieldMatchOption::Equals); + + FindPackagesOptions options = WindowsPackageManagerFactory::CreateFindPackagesOptions(); + options.Filters().Append(filter); + options.ResultLimit(20); + + const auto result = co_await catalog.FindPackagesAsync(options); + const IVectorView pkgList = result.Matches(); + co_return pkgList; + } + + Windows::Foundation::IAsyncAction TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args) + { + if (!Feature_QuickFix::IsEnabled()) + { + co_return; + } + + const auto weak = get_weak(); + const auto dispatcher = Dispatcher(); + + // All of the code until resume_foreground is static and + // doesn't touch `this`, so we don't need weak/strong_ref. + co_await winrt::resume_background(); + + // no packages were found, nothing to suggest + const auto pkgList = co_await _FindPackageAsync(args.MissingCommand()); + if (!pkgList || pkgList.Size() == 0) + { + co_return; + } + + std::vector suggestions; + suggestions.reserve(pkgList.Size()); + for (const auto& pkg : pkgList) + { + // --id and --source ensure we don't collide with another package catalog + suggestions.emplace_back(fmt::format(FMT_COMPILE(L"winget install --id {} -s winget"), pkg.CatalogPackage().Id())); + } + + co_await wil::resume_foreground(dispatcher); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + auto term = _GetActiveControl(); + if (!term) + { + co_return; + } + term.UpdateWinGetSuggestions(single_threaded_vector(std::move(suggestions))); + term.RefreshQuickFixMenu(); + } + + void TerminalPage::_WindowSizeChanged(const IInspectable sender, const Microsoft::Terminal::Control::WindowSizeChangedEventArgs args) + { + // Raise if: + // - Not in quake mode + // - Not in fullscreen + // - Only one tab exists + // - Only one pane exists + // else: + // - Reset conpty to its original size back + if (!WindowProperties().IsQuakeWindow() && !Fullscreen() && + NumberOfTabs() == 1 && _GetFocusedTabImpl()->GetLeafPaneCount() == 1) + { + WindowSizeChanged.raise(*this, args); + } + else if (const auto& control{ sender.try_as() }) + { + const auto& connection = control.Connection(); + + if (const auto& conpty{ connection.try_as() }) + { + conpty.ResetSize(); + } + } + } + + void TerminalPage::_copyToClipboard(const IInspectable, const WriteToClipboardEventArgs args) const + { + if (const auto clipboard = clipboard::open(_hostingHwnd.value_or(nullptr))) + { + const auto plain = args.Plain(); + const auto html = args.Html(); + const auto rtf = args.Rtf(); + + clipboard::write( + { plain.data(), plain.size() }, + { reinterpret_cast(html.data()), html.size() }, + { reinterpret_cast(rtf.data()), rtf.size() }); + } + } + + // Method Description: + // - Paste text from the Windows Clipboard to the focused terminal + void TerminalPage::_PasteText() + { + if (const auto& control{ _GetActiveControl() }) + { + control.PasteTextFromClipboard(); + } + } + + // Function Description: + // - Called when the settings button is clicked. ShellExecutes the settings + // file, as to open it in the default editor for .json files. Does this in + // a background thread, as to not hang/crash the UI thread. + safe_void_coroutine TerminalPage::_LaunchSettings(const SettingsTarget target) + { + if (target == SettingsTarget::SettingsUI) + { + OpenSettingsUI(); + } + else + { + // This will switch the execution of the function to a background (not + // UI) thread. This is IMPORTANT, because the Windows.Storage API's + // (used for retrieving the path to the file) will crash on the UI + // thread, because the main thread is a STA. + // + // NOTE: All remaining code of this function doesn't touch `this`, so we don't need weak/strong_ref. + // NOTE NOTE: Don't touch `this` when you make changes here. + co_await winrt::resume_background(); + + auto openFile = [](const auto& filePath) { + HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); + if (static_cast(reinterpret_cast(res)) <= 32) + { + ShellExecute(nullptr, nullptr, L"notepad", filePath.c_str(), nullptr, SW_SHOW); + } + }; + + auto openFolder = [](const auto& filePath) { + HINSTANCE res = ShellExecute(nullptr, nullptr, filePath.c_str(), nullptr, nullptr, SW_SHOW); + if (static_cast(reinterpret_cast(res)) <= 32) + { + ShellExecute(nullptr, nullptr, L"open", filePath.c_str(), nullptr, SW_SHOW); + } + }; + + switch (target) + { + case SettingsTarget::DefaultsFile: + openFile(CascadiaSettings::DefaultSettingsPath()); + break; + case SettingsTarget::SettingsFile: + openFile(CascadiaSettings::SettingsPath()); + break; + case SettingsTarget::Directory: + openFolder(CascadiaSettings::SettingsDirectory()); + break; + case SettingsTarget::AllFiles: + openFile(CascadiaSettings::DefaultSettingsPath()); + openFile(CascadiaSettings::SettingsPath()); + break; + } + } + } + + // Method Description: + // - Responds to the TabView control's Tab Closing event by removing + // the indicated tab from the set and focusing another one. + // The event is cancelled so App maintains control over the + // items in the tabview. + // Arguments: + // - sender: the control that originated this event + // - eventArgs: the event's constituent arguments + void TerminalPage::_OnTabCloseRequested(const IInspectable& /*sender*/, const MUX::Controls::TabViewTabCloseRequestedEventArgs& eventArgs) + { + const auto tabViewItem = eventArgs.Tab(); + if (auto tab{ _GetTabByTabViewItem(tabViewItem) }) + { + _HandleCloseTabRequested(tab); + } + } + + TermControl TerminalPage::_CreateNewControlAndContent(const Settings::TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) + { + // Do any initialization that needs to apply to _every_ TermControl we + // create here. + const auto content = _manager.CreateCore(*settings.DefaultSettings(), settings.UnfocusedSettings().try_as(), connection); + const TermControl control{ content }; + return _SetupControl(control); + } + + TermControl TerminalPage::_AttachControlToContent(const uint64_t& contentId) + { + if (const auto& content{ _manager.TryLookupCore(contentId) }) + { + // We have to pass in our current keybindings, because that's an + // object that belongs to this TerminalPage, on this thread. If we + // don't, then when we move the content to another thread, and it + // tries to handle a key, it'll callback on the original page's + // stack, inevitably resulting in a wrong_thread + return _SetupControl(TermControl::NewControlByAttachingContent(content)); + } + return nullptr; + } + + TermControl TerminalPage::_SetupControl(const TermControl& term) + { + // GH#12515: ConPTY assumes it's hidden at the start. If we're not, let it know now. + if (_visible) + { + term.WindowVisibilityChanged(_visible); + } + + // Even in the case of re-attaching content from another window, this + // will correctly update the control's owning HWND + if (_hostingHwnd.has_value()) + { + term.OwningHwnd(reinterpret_cast(*_hostingHwnd)); + } + + term.KeyBindings(*_bindings); + + _RegisterTerminalEvents(term); + return term; + } + + // Method Description: + // - Creates a pane and returns a shared_ptr to it + // - The caller should handle where the pane goes after creation, + // either to split an already existing pane or to create a new tab with it + // Arguments: + // - newTerminalArgs: an object that may contain a blob of parameters to + // control which profile is created and with possible other + // configurations. See CascadiaSettings::BuildSettings for more details. + // - sourceTab: an optional tab reference that indicates that the created + // pane should be a duplicate of the tab's focused pane + // - existingConnection: optionally receives a connection from the outside + // world instead of attempting to create one + // Return Value: + // - If the newTerminalArgs required us to open the pane as a new elevated + // connection, then we'll return nullptr. Otherwise, we'll return a new + // Pane for this connection. + std::shared_ptr TerminalPage::_MakeTerminalPane(const NewTerminalArgs& newTerminalArgs, + const winrt::TerminalApp::Tab& sourceTab, + TerminalConnection::ITerminalConnection existingConnection) + { + // First things first - Check for making a pane from content ID. + if (newTerminalArgs && + newTerminalArgs.ContentId() != 0) + { + // Don't need to worry about duplicating or anything - we'll + // serialize the actual profile's GUID along with the content guid. + const auto& profile = _settings.GetProfileForArgs(newTerminalArgs); + const auto control = _AttachControlToContent(newTerminalArgs.ContentId()); + auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; + return std::make_shared(paneContent); + } + + Settings::TerminalSettingsCreateResult controlSettings{ nullptr }; + Profile profile{ nullptr }; + + if (const auto& tabImpl{ _GetTabImpl(sourceTab) }) + { + profile = tabImpl->GetFocusedProfile(); + if (profile) + { + // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. + profile = GetClosestProfileForDuplicationOfProfile(profile); + controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); + const auto workingDirectory = tabImpl->GetActiveTerminalControl().WorkingDirectory(); + const auto validWorkingDirectory = !workingDirectory.empty(); + if (validWorkingDirectory) + { + controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); + } + } + } + if (!profile) + { + profile = _settings.GetProfileForArgs(newTerminalArgs); + controlSettings = Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs); + } + + // Try to handle auto-elevation + if (_maybeElevate(newTerminalArgs, controlSettings, profile)) + { + return nullptr; + } + + const auto sessionId = controlSettings.DefaultSettings()->SessionId(); + const auto hasSessionId = sessionId != winrt::guid{}; + + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, *controlSettings.DefaultSettings(), hasSessionId); + if (existingConnection) + { + connection.Resize(controlSettings.DefaultSettings()->InitialRows(), controlSettings.DefaultSettings()->InitialCols()); + } + + TerminalConnection::ITerminalConnection debugConnection{ nullptr }; + if (_settings.GlobalSettings().DebugFeaturesEnabled()) + { + const auto window = CoreWindow::GetForCurrentThread(); + const auto rAltState = window.GetKeyState(VirtualKey::RightMenu); + const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu); + const auto bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) && + WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down); + if (bothAltsPressed) + { + std::tie(connection, debugConnection) = OpenDebugTapConnection(connection); + } + } + + const auto control = _CreateNewControlAndContent(controlSettings, connection); + + if (hasSessionId) + { + using namespace std::string_view_literals; + + const auto settingsDir = CascadiaSettings::SettingsDirectory(); + const auto admin = IsRunningElevated(); + const auto filenamePrefix = admin ? L"elevated_"sv : L"buffer_"sv; + const auto path = fmt::format(FMT_COMPILE(L"{}\\{}{}.txt"), settingsDir, filenamePrefix, sessionId); + control.RestoreFromPath(path); + } + + auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; + + auto resultPane = std::make_shared(paneContent); + + if (debugConnection) // this will only be set if global debugging is on and tap is active + { + auto newControl = _CreateNewControlAndContent(controlSettings, debugConnection); + // Split (auto) with the debug tap. + auto debugContent{ winrt::make(profile, _terminalSettingsCache, newControl) }; + auto debugPane = std::make_shared(debugContent); + + // Since we're doing this split directly on the pane (instead of going through Tab, + // we need to handle the panes 'active' states + + // Set the pane we're splitting to active (otherwise Split will not do anything) + resultPane->SetActive(); + auto [original, _] = resultPane->Split(SplitDirection::Automatic, 0.5f, debugPane); + + // Set the non-debug pane as active + resultPane->ClearActive(); + original->SetActive(); + } + + return resultPane; + } + + // NOTE: callers of _MakePane should be able to accept nullptr as a return + // value gracefully. + std::shared_ptr TerminalPage::_MakePane(const INewContentArgs& contentArgs, + const winrt::TerminalApp::Tab& sourceTab, + TerminalConnection::ITerminalConnection existingConnection) + + { + const auto& newTerminalArgs{ contentArgs.try_as() }; + if (contentArgs == nullptr || newTerminalArgs != nullptr || contentArgs.Type().empty()) + { + // Terminals are of course special, and have to deal with debug taps, duplicating the tab, etc. + return _MakeTerminalPane(newTerminalArgs, sourceTab, existingConnection); + } + + IPaneContent content{ nullptr }; + + const auto& paneType{ contentArgs.Type() }; + if (paneType == L"scratchpad") + { + const auto& scratchPane{ winrt::make_self() }; + + // This is maybe a little wacky - add our key event handler to the pane + // we made. So that we can get actions for keys that the content didn't + // handle. + scratchPane->GetRoot().KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); + + content = *scratchPane; + } + else if (paneType == L"settings") + { + content = _makeSettingsContent(); + } + else if (paneType == L"snippets") + { + // Prevent the user from opening a bunch of snippets panes. + // + // Look at the focused tab, and if it already has one, then just focus it. + if (const auto& focusedTab{ _GetFocusedTabImpl() }) + { + const auto rootPane{ focusedTab->GetRootPane() }; + const bool found = rootPane == nullptr ? false : rootPane->WalkTree([](const auto& p) -> bool { + if (const auto& snippets{ p->GetContent().try_as() }) + { + snippets->Focus(FocusState::Programmatic); + return true; + } + return false; + }); + // Bail out if we already found one. + if (found) + { + return nullptr; + } + } + + const auto& tasksContent{ winrt::make_self() }; + tasksContent->UpdateSettings(_settings); + tasksContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + tasksContent->DispatchCommandRequested({ this, &TerminalPage::_OnDispatchCommandRequested }); + if (const auto& termControl{ _GetActiveControl() }) + { + tasksContent->SetLastActiveControl(termControl); + } + + content = *tasksContent; + } + else if (paneType == L"x-markdown") + { + if (Feature_MarkdownPane::IsEnabled()) + { + const auto& markdownContent{ winrt::make_self(L"") }; + markdownContent->UpdateSettings(_settings); + markdownContent->GetRoot().KeyDown({ this, &TerminalPage::_KeyDownHandler }); + + // This one doesn't use DispatchCommand, because we don't create + // Command's freely at runtime like we do with just plain old actions. + markdownContent->DispatchActionRequested([weak = get_weak()](const auto& sender, const auto& actionAndArgs) { + if (const auto& page{ weak.get() }) + { + page->_actionDispatch->DoAction(sender, actionAndArgs); + } + }); + if (const auto& termControl{ _GetActiveControl() }) + { + markdownContent->SetLastActiveControl(termControl); + } + + content = *markdownContent; + } + } + + assert(content); + + return std::make_shared(content); + } + + void TerminalPage::_restartPaneConnection( + const TerminalApp::TerminalPaneContent& paneContent, + const winrt::Windows::Foundation::IInspectable&) + { + // Note: callers are likely passing in `nullptr` as the args here, as + // the TermControl.RestartTerminalRequested event doesn't actually pass + // any args upwards itself. If we ever change this, make sure you check + // for nulls + if (const auto& connection{ _duplicateConnectionForRestart(paneContent) }) + { + paneContent.GetTermControl().Connection(connection); + connection.Start(); + } + } + + // Method Description: + // - Sets background image and applies its settings (stretch, opacity and alignment) + // - Checks path validity + // Arguments: + // - newAppearance + // Return Value: + // - + void TerminalPage::_SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance) + { + if (!_settings.GlobalSettings().UseBackgroundImageForWindow()) + { + _tabContent.Background(nullptr); + return; + } + + const auto path = newAppearance.BackgroundImagePath().Resolved(); + if (path.empty()) + { + _tabContent.Background(nullptr); + return; + } + + Windows::Foundation::Uri imageUri{ nullptr }; + try + { + imageUri = Windows::Foundation::Uri{ path }; + } + catch (...) + { + LOG_CAUGHT_EXCEPTION(); + _tabContent.Background(nullptr); + return; + } + // Check if the image brush is already pointing to the image + // in the modified settings; if it isn't (or isn't there), + // set a new image source for the brush + + auto brush = _tabContent.Background().try_as(); + Media::Imaging::BitmapImage imageSource = brush == nullptr ? nullptr : brush.ImageSource().try_as(); + + if (imageSource == nullptr || + imageSource.UriSource() == nullptr || + !imageSource.UriSource().Equals(imageUri)) + { + Media::ImageBrush b{}; + // Note that BitmapImage handles the image load asynchronously, + // which is especially important since the image + // may well be both large and somewhere out on the + // internet. + Media::Imaging::BitmapImage image(imageUri); + b.ImageSource(image); + _tabContent.Background(b); + } + + // Pull this into a separate block. If the image didn't change, but the + // properties of the image did, we should still update them. + if (const auto newBrush{ _tabContent.Background().try_as() }) + { + newBrush.Stretch(newAppearance.BackgroundImageStretchMode()); + newBrush.Opacity(newAppearance.BackgroundImageOpacity()); + } + } + + // Method Description: + // - Hook up keybindings, and refresh the UI of the terminal. + // This includes update the settings of all the tabs according + // to their profiles, update the title and icon of each tab, and + // finally create the tab flyout + void TerminalPage::_RefreshUIForSettingsReload() + { + // Re-wire the keybindings to their handlers, as we'll have created a + // new AppKeyBindings object. + _HookupKeyBindings(_settings.ActionMap()); + + // Refresh UI elements + + // Recreate the TerminalSettings cache here. We'll use that as we're + // updating terminal panes, so that we don't have to build a _new_ + // TerminalSettings for every profile we update - we can just look them + // up the previous ones we built. + _terminalSettingsCache->Reset(_settings); + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // Let the tab know that there are new settings. It's up to each content to decide what to do with them. + tabImpl->UpdateSettings(_settings); + + // Update the icon of the tab for the currently focused profile in that tab. + // Only do this for TerminalTabs. Other types of tabs won't have multiple panes + // and profiles so the Title and Icon will be set once and only once on init. + _UpdateTabIcon(*tabImpl); + + // Force the TerminalTab to re-grab its currently active control's title. + tabImpl->UpdateTitle(); + } + + auto tabImpl{ winrt::get_self(tab) }; + tabImpl->SetActionMap(_settings.ActionMap()); + } + + if (const auto focusedTab{ _GetFocusedTabImpl() }) + { + if (const auto profile{ focusedTab->GetFocusedProfile() }) + { + _SetBackgroundImage(profile.DefaultAppearance()); + } + } + + // repopulate the new tab button's flyout with entries for each + // profile, which might have changed + _UpdateTabWidthMode(); + _CreateNewTabFlyout(); + + // Reload the current value of alwaysOnTop from the settings file. This + // will let the user hot-reload this setting, but any runtime changes to + // the alwaysOnTop setting will be lost. + _isAlwaysOnTop = _settings.GlobalSettings().AlwaysOnTop(); + AlwaysOnTopChanged.raise(*this, nullptr); + + _showTabsFullscreen = _settings.GlobalSettings().ShowTabsFullscreen(); + + // Settings AllowDependentAnimations will affect whether animations are + // enabled application-wide, so we don't need to check it each time we + // want to create an animation. + WUX::Media::Animation::Timeline::AllowDependentAnimations(!_settings.GlobalSettings().DisableAnimations()); + + _tabRow.ShowElevationShield(IsRunningElevated() && _settings.GlobalSettings().ShowAdminShield()); + + Media::SolidColorBrush transparent{ Windows::UI::Colors::Transparent() }; + _tabView.Background(transparent); + + //////////////////////////////////////////////////////////////////////// + // Begin Theme handling + _updateThemeColors(); + + _updateAllTabCloseButtons(); + + // The user may have changed the "show title in titlebar" setting. + TitleChanged.raise(*this, nullptr); + } + + void TerminalPage::_updateAllTabCloseButtons() + { + // Update the state of the CloseButtonOverlayMode property of + // our TabView, to match the tab.showCloseButton property in the theme. + // + // Also update every tab's individual IsClosable to match the same property. + const auto theme = _settings.GlobalSettings().CurrentTheme(); + const auto visibility = (theme && theme.Tab()) ? + theme.Tab().ShowCloseButton() : + Settings::Model::TabCloseButtonVisibility::Always; + + _tabItemMiddleClickHookEnabled = visibility == Settings::Model::TabCloseButtonVisibility::Never; + + for (const auto& tab : _tabs) + { + tab.CloseButtonVisibility(visibility); + } + + switch (visibility) + { + case Settings::Model::TabCloseButtonVisibility::Never: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Auto); + break; + case Settings::Model::TabCloseButtonVisibility::Hover: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::OnPointerOver); + break; + case Settings::Model::TabCloseButtonVisibility::ActiveOnly: + default: + _tabView.CloseButtonOverlayMode(MUX::Controls::TabViewCloseButtonOverlayMode::Always); + break; + } + } + + // Method Description: + // - Sets the initial actions to process on startup. We'll make a copy of + // this list, and process these actions when we're loaded. + // - This function will have no effective result after Create() is called. + // Arguments: + // - actions: a list of Actions to process on startup. + // Return Value: + // - + void TerminalPage::SetStartupActions(std::vector actions) + { + _startupActions = std::move(actions); + } + + void TerminalPage::SetStartupConnection(ITerminalConnection connection) + { + _startupConnection = std::move(connection); + } + + winrt::TerminalApp::IDialogPresenter TerminalPage::DialogPresenter() const + { + return _dialogPresenter.get(); + } + + void TerminalPage::DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter) + { + _dialogPresenter = dialogPresenter; + } + + // Method Description: + // - Get the combined taskbar state for the page. This is the combination of + // all the states of all the tabs, which are themselves a combination of + // all their panes. Taskbar states are given a priority based on the rules + // in: + // https://docs.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist3-setprogressstate + // under "How the Taskbar Button Chooses the Progress Indicator for a Group" + // Arguments: + // - + // Return Value: + // - A TaskbarState object representing the combined taskbar state and + // progress percentage of all our tabs. + winrt::TerminalApp::TaskbarState TerminalPage::TaskbarState() const + { + auto state{ winrt::make() }; + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + auto tabState{ tabImpl->GetCombinedTaskbarState() }; + // lowest priority wins + if (tabState.Priority() < state.Priority()) + { + state = tabState; + } + } + } + + return state; + } + + // Method Description: + // - This is the method that App will call when the titlebar + // has been clicked. It dismisses any open flyouts. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::TitlebarClicked() + { + if (_newTabButton && _newTabButton.Flyout()) + { + _newTabButton.Flyout().Hide(); + } + _DismissTabContextMenus(); + } + + // Method Description: + // - Notifies all attached console controls that the visibility of the + // hosting window has changed. The underlying PTYs may need to know this + // for the proper response to `::GetConsoleWindow()` from a Win32 console app. + // Arguments: + // - showOrHide: Show is true; hide is false. + // Return Value: + // - + void TerminalPage::WindowVisibilityChanged(const bool showOrHide) + { + _visible = showOrHide; + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings + // objects but only have to iterate one time. + tabImpl->GetRootPane()->WalkTree([&](auto&& pane) { + if (auto control = pane->GetTerminalControl()) + { + control.WindowVisibilityChanged(showOrHide); + } + }); + } + } + } + + // Method Description: + // - Called when the user tries to do a search using keybindings. + // This will tell the active terminal control of the passed tab + // to create a search box and enable find process. + // Arguments: + // - tab: the tab where the search box should be created + // Return Value: + // - + void TerminalPage::_Find(const Tab& tab) + { + if (const auto& control{ tab.GetActiveTerminalControl() }) + { + control.CreateSearchBoxControl(); + } + } + + // Method Description: + // - Toggles borderless mode. Hides the tab row, and raises our + // FocusModeChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleFocusMode() + { + SetFocusMode(!_isInFocusMode); + } + + void TerminalPage::SetFocusMode(const bool inFocusMode) + { + const auto newInFocusMode = inFocusMode; + if (newInFocusMode != FocusMode()) + { + _isInFocusMode = newInFocusMode; + _UpdateTabView(); + FocusModeChanged.raise(*this, nullptr); + } + } + + // Method Description: + // - Toggles fullscreen mode. Hides the tab row, and raises our + // FullscreenChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleFullscreen() + { + SetFullscreen(!_isFullscreen); + } + + // Method Description: + // - Toggles always on top mode. Raises our AlwaysOnTopChanged event. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::ToggleAlwaysOnTop() + { + _isAlwaysOnTop = !_isAlwaysOnTop; + AlwaysOnTopChanged.raise(*this, nullptr); + } + + // Method Description: + // - Sets the tab split button color when a new tab color is selected + // Arguments: + // - color: The color of the newly selected tab, used to properly calculate + // the foreground color of the split button (to match the font + // color of the tab) + // - accentColor: the actual color we are going to use to paint the tab row and + // split button, so that there is some contrast between the tab + // and the non-client are behind it + // Return Value: + // - + void TerminalPage::_SetNewTabButtonColor(const til::color color, const til::color accentColor) + { + constexpr auto lightnessThreshold = 0.6f; + // TODO GH#3327: Look at what to do with the tab button when we have XAML theming + const auto isBrightColor = ColorFix::GetLightness(color) >= lightnessThreshold; + const auto isLightAccentColor = ColorFix::GetLightness(accentColor) >= lightnessThreshold; + const auto hoverColorAdjustment = isLightAccentColor ? -0.05f : 0.05f; + const auto pressedColorAdjustment = isLightAccentColor ? -0.1f : 0.1f; + + const auto foregroundColor = isBrightColor ? Colors::Black() : Colors::White(); + const auto hoverColor = til::color{ ColorFix::AdjustLightness(accentColor, hoverColorAdjustment) }; + const auto pressedColor = til::color{ ColorFix::AdjustLightness(accentColor, pressedColorAdjustment) }; + + Media::SolidColorBrush backgroundBrush{ accentColor }; + Media::SolidColorBrush backgroundHoverBrush{ hoverColor }; + Media::SolidColorBrush backgroundPressedBrush{ pressedColor }; + Media::SolidColorBrush foregroundBrush{ foregroundColor }; + + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackground"), backgroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPointerOver"), backgroundHoverBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonBackgroundPressed"), backgroundPressedBrush); + + // Load bearing: The SplitButton uses SplitButtonForegroundSecondary for + // the secondary button, but {TemplateBinding Foreground} for the + // primary button. + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForeground"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPointerOver"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundPressed"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondary"), foregroundBrush); + _newTabButton.Resources().Insert(winrt::box_value(L"SplitButtonForegroundSecondaryPressed"), foregroundBrush); + + _newTabButton.Background(backgroundBrush); + _newTabButton.Foreground(foregroundBrush); + + // This is just like what we do in Tab::_RefreshVisualState. We need + // to manually toggle the visual state, so the setters in the visual + // state group will re-apply, and set our currently selected colors in + // the resources. + VisualStateManager::GoToState(_newTabButton, L"FlyoutOpen", true); + VisualStateManager::GoToState(_newTabButton, L"Normal", true); + } + + // Method Description: + // - Clears the tab split button color to a system color + // (or white if none is found) when the tab's color is cleared + // - Clears the tab row color to a system color + // (or white if none is found) when the tab's color is cleared + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_ClearNewTabButtonColor() + { + // TODO GH#3327: Look at what to do with the tab button when we have XAML theming + winrt::hstring keys[] = { + L"SplitButtonBackground", + L"SplitButtonBackgroundPointerOver", + L"SplitButtonBackgroundPressed", + L"SplitButtonForeground", + L"SplitButtonForegroundSecondary", + L"SplitButtonForegroundPointerOver", + L"SplitButtonForegroundPressed", + L"SplitButtonForegroundSecondaryPressed" + }; + + // simply clear any of the colors in the split button's dict + for (auto keyString : keys) + { + auto key = winrt::box_value(keyString); + if (_newTabButton.Resources().HasKey(key)) + { + _newTabButton.Resources().Remove(key); + } + } + + const auto res = Application::Current().Resources(); + + const auto defaultBackgroundKey = winrt::box_value(L"TabViewItemHeaderBackground"); + const auto defaultForegroundKey = winrt::box_value(L"SystemControlForegroundBaseHighBrush"); + winrt::Windows::UI::Xaml::Media::SolidColorBrush backgroundBrush; + winrt::Windows::UI::Xaml::Media::SolidColorBrush foregroundBrush; + + // TODO: Related to GH#3917 - I think if the system is set to "Dark" + // theme, but the app is set to light theme, then this lookup still + // returns to us the dark theme brushes. There's gotta be a way to get + // the right brushes... + // See also GH#5741 + if (res.HasKey(defaultBackgroundKey)) + { + auto obj = res.Lookup(defaultBackgroundKey); + backgroundBrush = obj.try_as(); + } + else + { + backgroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::Black() }; + } + + if (res.HasKey(defaultForegroundKey)) + { + auto obj = res.Lookup(defaultForegroundKey); + foregroundBrush = obj.try_as(); + } + else + { + foregroundBrush = winrt::Windows::UI::Xaml::Media::SolidColorBrush{ winrt::Windows::UI::Colors::White() }; + } + + _newTabButton.Background(backgroundBrush); + _newTabButton.Foreground(foregroundBrush); + } + + // Function Description: + // - This is a helper method to get the commandline out of a + // ExecuteCommandline action, break it into subcommands, and attempt to + // parse it into actions. This is used by _HandleExecuteCommandline for + // processing commandlines in the current WT window. + // Arguments: + // - args: the ExecuteCommandlineArgs to synthesize a list of startup actions for. + // Return Value: + // - an empty list if we failed to parse; otherwise, a list of actions to execute. + std::vector TerminalPage::ConvertExecuteCommandlineToActions(const ExecuteCommandlineArgs& args) + { + ::TerminalApp::AppCommandlineArgs appArgs; + if (appArgs.ParseArgs(args) == 0) + { + return appArgs.GetStartupActions(); + } + + return {}; + } + + void TerminalPage::_FocusActiveControl(IInspectable /*sender*/, + IInspectable /*eventArgs*/) + { + _FocusCurrentTab(false); + } + + bool TerminalPage::FocusMode() const + { + return _isInFocusMode; + } + + bool TerminalPage::Fullscreen() const + { + return _isFullscreen; + } + + // Method Description: + // - Returns true if we're currently in "Always on top" mode. When we're in + // always on top mode, the window should be on top of all other windows. + // If multiple windows are all "always on top", they'll maintain their own + // z-order, with all the windows on top of all other non-topmost windows. + // Arguments: + // - + // Return Value: + // - true if we should be in "always on top" mode + bool TerminalPage::AlwaysOnTop() const + { + return _isAlwaysOnTop; + } + + // Method Description: + // - Returns true if the tab row should be visible when we're in full screen + // state. + // Arguments: + // - + // Return Value: + // - true if the tab row should be visible in full screen state + bool TerminalPage::ShowTabsFullscreen() const + { + return _showTabsFullscreen; + } + + // Method Description: + // - Updates the visibility of the tab row when in fullscreen state. + void TerminalPage::SetShowTabsFullscreen(bool newShowTabsFullscreen) + { + if (_showTabsFullscreen == newShowTabsFullscreen) + { + return; + } + + _showTabsFullscreen = newShowTabsFullscreen; + + // if we're currently in fullscreen, update tab view to make + // sure tabs are given the correct visibility + if (_isFullscreen) + { + _UpdateTabView(); + } + } + + void TerminalPage::SetFullscreen(bool newFullscreen) + { + if (_isFullscreen == newFullscreen) + { + return; + } + _isFullscreen = newFullscreen; + _UpdateTabView(); + FullscreenChanged.raise(*this, nullptr); + } + + // Method Description: + // - Updates the page's state for isMaximized when the window changes externally. + void TerminalPage::Maximized(bool newMaximized) + { + _isMaximized = newMaximized; + } + + // Method Description: + // - Asks the window to change its maximized state. + void TerminalPage::RequestSetMaximized(bool newMaximized) + { + if (_isMaximized == newMaximized) + { + return; + } + _isMaximized = newMaximized; + ChangeMaximizeRequested.raise(*this, nullptr); + } + + TerminalApp::IPaneContent TerminalPage::_makeSettingsContent() + { + if (auto app{ winrt::Windows::UI::Xaml::Application::Current().try_as() }) + { + if (auto appPrivate{ winrt::get_self(app) }) + { + // Lazily load the Settings UI components so that we don't do it on startup. + appPrivate->PrepareForSettingsUI(); + } + } + + // Create the SUI pane content + auto settingsContent{ winrt::make_self(_settings) }; + auto sui = settingsContent->SettingsUI(); + + if (_hostingHwnd) + { + sui.SetHostingWindow(reinterpret_cast(*_hostingHwnd)); + } + + // GH#8767 - let unhandled keys in the SUI try to run commands too. + sui.KeyDown({ get_weak(), &TerminalPage::_KeyDownHandler }); + + sui.OpenJson([weakThis{ get_weak() }](auto&& /*s*/, winrt::Microsoft::Terminal::Settings::Model::SettingsTarget e) { + if (auto page{ weakThis.get() }) + { + page->_LaunchSettings(e); + } + }); + + sui.ShowLoadWarningsDialog([weakThis{ get_weak() }](auto&& /*s*/, const Windows::Foundation::Collections::IVectorView& warnings) { + if (auto page{ weakThis.get() }) + { + page->ShowLoadWarningsDialog.raise(*page, warnings); + } + }); + + return *settingsContent; + } + + // Method Description: + // - Creates a settings UI tab and focuses it. If there's already a settings UI tab open, + // just focus the existing one. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::OpenSettingsUI() + { + // If we're holding the settings tab's switch command, don't create a new one, switch to the existing one. + if (!_settingsTab) + { + // Create the tab + auto resultPane = std::make_shared(_makeSettingsContent()); + _settingsTab = _CreateNewTabFromPane(resultPane); + } + else + { + _tabView.SelectedItem(_settingsTab.TabViewItem()); + } + } + + // Method Description: + // - Returns a com_ptr to the implementation type of the given tab if it's a Tab. + // If the tab is not a TerminalTab, returns nullptr. + // Arguments: + // - tab: the projected type of a Tab + // Return Value: + // - If the tab is a TerminalTab, a com_ptr to the implementation type. + // If the tab is not a TerminalTab, nullptr + winrt::com_ptr TerminalPage::_GetTabImpl(const TerminalApp::Tab& tab) + { + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tab)); + return tabImpl; + } + + // Method Description: + // - Computes the delta for scrolling the tab's viewport. + // Arguments: + // - scrollDirection - direction (up / down) to scroll + // - rowsToScroll - the number of rows to scroll + // Return Value: + // - delta - Signed delta, where a negative value means scrolling up. + int TerminalPage::_ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll) + { + return scrollDirection == ScrollUp ? -1 * rowsToScroll : rowsToScroll; + } + + // Method Description: + // - Reads system settings for scrolling (based on the step of the mouse scroll). + // Upon failure fallbacks to default. + // Return Value: + // - The number of rows to scroll or a magic value of WHEEL_PAGESCROLL + // indicating that we need to scroll an entire view height + uint32_t TerminalPage::_ReadSystemRowsToScroll() + { + uint32_t systemRowsToScroll; + if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) + { + LOG_LAST_ERROR(); + + // If SystemParametersInfoW fails, which it shouldn't, fall back to + // Windows' default value. + return DefaultRowsToScroll; + } + + return systemRowsToScroll; + } + + // Method Description: + // - Displays a dialog stating the "Touch Keyboard and Handwriting Panel + // Service" is disabled. + void TerminalPage::ShowKeyboardServiceWarning() const + { + if (!_IsMessageDismissed(InfoBarMessage::KeyboardServiceWarning)) + { + if (const auto keyboardServiceWarningInfoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) + { + keyboardServiceWarningInfoBar.IsOpen(true); + } + } + } + + // Function Description: + // - Helper function to get the OS-localized name for the "Touch Keyboard + // and Handwriting Panel Service". If we can't open up the service for any + // reason, then we'll just return the service's key, "TabletInputService". + // Return Value: + // - The OS-localized name for the TabletInputService + winrt::hstring _getTabletServiceName() + { + wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) }; + + if (LOG_LAST_ERROR_IF(!hManager.is_valid())) + { + return winrt::hstring{ TabletInputServiceKey }; + } + + DWORD cchBuffer = 0; + const auto ok = GetServiceDisplayNameW(hManager.get(), TabletInputServiceKey.data(), nullptr, &cchBuffer); + + // Windows 11 doesn't have a TabletInputService. + // (It was renamed to TextInputManagementService, because people kept thinking that a + // service called "tablet-something" is system-irrelevant on PCs and can be disabled.) + if (ok || GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + return winrt::hstring{ TabletInputServiceKey }; + } + + std::wstring buffer; + cchBuffer += 1; // Add space for a null + buffer.resize(cchBuffer); + + if (LOG_LAST_ERROR_IF(!GetServiceDisplayNameW(hManager.get(), + TabletInputServiceKey.data(), + buffer.data(), + &cchBuffer))) + { + return winrt::hstring{ TabletInputServiceKey }; + } + return winrt::hstring{ buffer }; + } + + // Method Description: + // - Return the fully-formed warning message for the + // "KeyboardServiceDisabled" InfoBar. This InfoBar is used to warn the user + // if the keyboard service is disabled, and uses the OS localization for + // the service's actual name. It's bound to the bar in XAML. + // Return Value: + // - The warning message, including the OS-localized service name. + winrt::hstring TerminalPage::KeyboardServiceDisabledText() + { + const auto serviceName{ _getTabletServiceName() }; + const auto text{ RS_fmt(L"KeyboardServiceWarningText", serviceName) }; + return winrt::hstring{ text }; + } + + // Method Description: + // - Update the RequestedTheme of the specified FrameworkElement and all its + // Parent elements. We need to do this so that we can actually theme all + // of the elements of the TeachingTip. See GH#9717 + // Arguments: + // - element: The TeachingTip to set the theme on. + // Return Value: + // - + void TerminalPage::_UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element) + { + auto theme{ _settings.GlobalSettings().CurrentTheme() }; + auto requestedTheme{ theme.RequestedTheme() }; + while (element) + { + element.RequestedTheme(requestedTheme); + element = element.Parent().try_as(); + } + } + + // Method Description: + // - Display the name and ID of this window in a TeachingTip. If the window + // has no name, the name will be presented as "". + // - This can be invoked by either: + // * An identifyWindow action, that displays the info only for the current + // window + // * An identifyWindows action, that displays the info for all windows. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::IdentifyWindow() + { + // If we haven't ever loaded the TeachingTip, then do so now and + // create the toast for it. + if (_windowIdToast == nullptr) + { + if (auto tip{ FindName(L"WindowIdToast").try_as() }) + { + _windowIdToast = std::make_shared(tip); + // IsLightDismissEnabled == true is bugged and poorly interacts with multi-windowing. + // It causes the tip to be immediately dismissed when another tip is opened in another window. + tip.IsLightDismissEnabled(false); + // Make sure to use the weak ref when setting up this callback. + tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); + } + } + _UpdateTeachingTipTheme(WindowIdToast().try_as()); + + if (_windowIdToast != nullptr) + { + _windowIdToast->Open(); + } + } + + void TerminalPage::ShowTerminalWorkingDirectory() + { + // If we haven't ever loaded the TeachingTip, then do so now and + // create the toast for it. + if (_windowCwdToast == nullptr) + { + if (auto tip{ FindName(L"WindowCwdToast").try_as() }) + { + _windowCwdToast = std::make_shared(tip); + // Make sure to use the weak ref when setting up this + // callback. + tip.Closed({ get_weak(), &TerminalPage::_FocusActiveControl }); + } + } + _UpdateTeachingTipTheme(WindowCwdToast().try_as()); + + if (_windowCwdToast != nullptr) + { + _windowCwdToast->Open(); + } + } + + // Method Description: + // - Called when the user hits the "Ok" button on the WindowRenamer TeachingTip. + // - Will raise an event that will bubble up to the monarch, asking if this + // name is acceptable. + // - we'll eventually get called back in TerminalPage::WindowName(hstring). + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_WindowRenamerActionClick(const IInspectable& /*sender*/, + const IInspectable& /*eventArgs*/) + { + auto newName = WindowRenamerTextBox().Text(); + _RequestWindowRename(newName); + } + + void TerminalPage::_RequestWindowRename(const winrt::hstring& newName) + { + auto request = winrt::make(newName); + // The WindowRenamer is _not_ a Toast - we want it to stay open until + // the user dismisses it. + if (WindowRenamer()) + { + WindowRenamer().IsOpen(false); + } + RenameWindowRequested.raise(*this, request); + // We can't just use request.Successful here, because the handler might + // (will) be handling this asynchronously, so when control returns to + // us, this hasn't actually been handled yet. We'll get called back in + // RenameFailed if this fails. + // + // Theoretically we could do a IAsyncOperation kind + // of thing with co_return winrt::make(false). + } + + // Method Description: + // - Used to track if the user pressed enter with the renamer open. If we + // immediately focus it after hitting Enter on the command palette, then + // the Enter keydown will dismiss the command palette and open the + // renamer, and then the enter keyup will go to the renamer. So we need to + // make sure both a down and up go to the renamer. + // Arguments: + // - e: the KeyRoutedEventArgs describing the key that was released + // Return Value: + // - + void TerminalPage::_WindowRenamerKeyDown(const IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + if (key == Windows::System::VirtualKey::Enter) + { + _renamerPressedEnter = true; + } + } + + // Method Description: + // - Manually handle Enter and Escape for committing and dismissing a window + // rename. This is highly similar to the TabHeaderControl's KeyUp handler. + // Arguments: + // - e: the KeyRoutedEventArgs describing the key that was released + // Return Value: + // - + void TerminalPage::_WindowRenamerKeyUp(const IInspectable& sender, + const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e) + { + const auto key = e.OriginalKey(); + if (key == Windows::System::VirtualKey::Enter && _renamerPressedEnter) + { + // User is done making changes, close the rename box + _WindowRenamerActionClick(sender, nullptr); + } + else if (key == Windows::System::VirtualKey::Escape) + { + // User wants to discard the changes they made + WindowRenamerTextBox().Text(_WindowProperties.WindowName()); + WindowRenamer().IsOpen(false); + _renamerPressedEnter = false; + } + } + + // Method Description: + // - This function stops people from duplicating the base profile, because + // it gets ~ ~ weird ~ ~ when they do. Remove when TODO GH#5047 is done. + Profile TerminalPage::GetClosestProfileForDuplicationOfProfile(const Profile& profile) const noexcept + { + if (profile == _settings.ProfileDefaults()) + { + return _settings.FindProfile(_settings.GlobalSettings().DefaultProfile()); + } + return profile; + } + + // Function Description: + // - Helper to launch a new WT instance elevated. It'll do this by spawning + // a helper process, who will asking the shell to elevate the process for + // us. This might cause a UAC prompt. The elevation is performed on a + // background thread, as to not block the UI thread. + // Arguments: + // - newTerminalArgs: A NewTerminalArgs describing the terminal instance + // that should be spawned. The Profile should be filled in with the GUID + // of the profile we want to launch. + // Return Value: + // - + // Important: Don't take the param by reference, since we'll be doing work + // on another thread. + void TerminalPage::_OpenElevatedWT(NewTerminalArgs newTerminalArgs) + { + // BODGY + // + // We're going to construct the commandline we want, then toss it to a + // helper process called `elevate-shim.exe` that happens to live next to + // us. elevate-shim.exe will be the one to call ShellExecute with the + // args that we want (to elevate the given profile). + // + // We can't be the one to call ShellExecute ourselves. ShellExecute + // requires that the calling process stays alive until the child is + // spawned. However, in the case of something like `wt -p + // AlwaysElevateMe`, then the original WT will try to ShellExecute a new + // wt.exe (elevated) and immediately exit, preventing ShellExecute from + // successfully spawning the elevated WT. + + std::filesystem::path exePath = wil::GetModuleFileNameW(nullptr); + exePath.replace_filename(L"elevate-shim.exe"); + + // Build the commandline to pass to wt for this set of NewTerminalArgs + auto cmdline{ + fmt::format(FMT_COMPILE(L"new-tab {}"), newTerminalArgs.ToCommandline()) + }; + + wil::unique_process_information pi; + STARTUPINFOW si{}; + si.cb = sizeof(si); + + LOG_IF_WIN32_BOOL_FALSE(CreateProcessW(exePath.c_str(), + cmdline.data(), + nullptr, + nullptr, + FALSE, + 0, + nullptr, + nullptr, + &si, + &pi)); + + // TODO: GH#8592 - It may be useful to pop a Toast here in the original + // Terminal window informing the user that the tab was opened in a new + // window. + } + + // Method Description: + // - If the requested settings want us to elevate this new terminal + // instance, and we're not currently elevated, then open the new terminal + // as an elevated instance (using _OpenElevatedWT). Does nothing if we're + // already elevated, or if the control settings don't want to be elevated. + // Arguments: + // - newTerminalArgs: The NewTerminalArgs for this terminal instance + // - controlSettings: The constructed TerminalSettingsCreateResult for this Terminal instance + // - profile: The Profile we're using to launch this Terminal instance + // Return Value: + // - true iff we tossed this request to an elevated window. Callers can use + // this result to early-return if needed. + bool TerminalPage::_maybeElevate(const NewTerminalArgs& newTerminalArgs, + const Settings::TerminalSettingsCreateResult& controlSettings, + const Profile& profile) + { + // When duplicating a tab there aren't any newTerminalArgs. + if (!newTerminalArgs) + { + return false; + } + + const auto defaultSettings = controlSettings.DefaultSettings(); + + // If we don't even want to elevate we can return early. + // If we're already elevated we can also return, because it doesn't get any more elevated than that. + if (!defaultSettings->Elevate() || IsRunningElevated()) + { + return false; + } + + // Manually set the Profile of the NewTerminalArgs to the guid we've + // resolved to. If there was a profile in the NewTerminalArgs, this + // will be that profile's GUID. If there wasn't, then we'll use + // whatever the default profile's GUID is. + newTerminalArgs.Profile(::Microsoft::Console::Utils::GuidToString(profile.Guid())); + newTerminalArgs.StartingDirectory(_evaluatePathForCwd(defaultSettings->StartingDirectory())); + _OpenElevatedWT(newTerminalArgs); + return true; + } + + // Method Description: + // - Handles the change of connection state. + // If the connection state is failure show information bar suggesting to configure termination behavior + // (unless user asked not to show this message again) + // Arguments: + // - sender: the ICoreState instance containing the connection state + // Return Value: + // - + safe_void_coroutine TerminalPage::_ConnectionStateChangedHandler(const IInspectable& sender, const IInspectable& /*args*/) + { + if (const auto coreState{ sender.try_as() }) + { + const auto newConnectionState = coreState.ConnectionState(); + const auto weak = get_weak(); + co_await wil::resume_foreground(Dispatcher()); + const auto strong = weak.get(); + if (!strong) + { + co_return; + } + + _adjustProcessPriorityThrottled->Run(); + + if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) + { + if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) + { + infoBar.IsOpen(true); + } + } + } + } + + // Method Description: + // - Persists the user's choice not to show information bar guiding to configure termination behavior. + // Then hides this information buffer. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_CloseOnExitInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const + { + _DismissMessage(InfoBarMessage::CloseOnExitInfo); + if (const auto infoBar = FindName(L"CloseOnExitInfoBar").try_as()) + { + infoBar.IsOpen(false); + } + } + + // Method Description: + // - Persists the user's choice not to show information bar warning about "Touch keyboard and Handwriting Panel Service" disabled + // Then hides this information buffer. + // Arguments: + // - + // Return Value: + // - + void TerminalPage::_KeyboardServiceWarningInfoDismissHandler(const IInspectable& /*sender*/, const IInspectable& /*args*/) const + { + _DismissMessage(InfoBarMessage::KeyboardServiceWarning); + if (const auto infoBar = FindName(L"KeyboardServiceWarningInfoBar").try_as()) + { + infoBar.IsOpen(false); + } + } + + // Method Description: + // - Checks whether information bar message was dismissed earlier (in the application state) + // Arguments: + // - message: message to look for in the state + // Return Value: + // - true, if the message was dismissed + bool TerminalPage::_IsMessageDismissed(const InfoBarMessage& message) + { + if (const auto dismissedMessages{ ApplicationState::SharedInstance().DismissedMessages() }) + { + for (const auto& dismissedMessage : dismissedMessages) + { + if (dismissedMessage == message) + { + return true; + } + } + } + return false; + } + + // Method Description: + // - Persists the user's choice to dismiss information bar message (in application state) + // Arguments: + // - message: message to dismiss + // Return Value: + // - + void TerminalPage::_DismissMessage(const InfoBarMessage& message) + { + const auto applicationState = ApplicationState::SharedInstance(); + std::vector messages; + + if (const auto values = applicationState.DismissedMessages()) + { + messages.resize(values.Size()); + values.GetMany(0, messages); + } + + if (std::none_of(messages.begin(), messages.end(), [&](const auto& m) { return m == message; })) + { + messages.emplace_back(message); + } + + applicationState.DismissedMessages(std::move(messages)); + } + + void TerminalPage::_updateThemeColors() + { + if (_settings == nullptr) + { + return; + } + + const auto theme = _settings.GlobalSettings().CurrentTheme(); + auto requestedTheme{ theme.RequestedTheme() }; + + { + _updatePaneResources(requestedTheme); + + for (const auto& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + // The root pane will propagate the theme change to all its children. + if (const auto& rootPane{ tabImpl->GetRootPane() }) + { + rootPane->UpdateResources(_paneResources); + } + } + } + } + + const auto res = Application::Current().Resources(); + + // Use our helper to lookup the theme-aware version of the resource. + const auto tabViewBackgroundKey = winrt::box_value(L"TabViewBackground"); + const auto backgroundSolidBrush = ThemeLookup(res, requestedTheme, tabViewBackgroundKey).as(); + + til::color bgColor = backgroundSolidBrush.Color(); + + Media::Brush terminalBrush{ nullptr }; + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto& pane{ tab->GetActivePane() }) + { + if (const auto& lastContent{ pane->GetLastFocusedContent() }) + { + terminalBrush = lastContent.BackgroundBrush(); + } + } + } + + // GH#19604: Get the theme's tabRow color to use as the acrylic tint. + const auto tabRowBg{ theme.TabRow() ? (_activated ? theme.TabRow().Background() : + theme.TabRow().UnfocusedBackground()) : + ThemeColor{ nullptr } }; + + if (_settings.GlobalSettings().UseAcrylicInTabRow() && (_activated || _settings.GlobalSettings().EnableUnfocusedAcrylic())) + { + if (tabRowBg) + { + bgColor = ThemeColor::ColorFromBrush(tabRowBg.Evaluate(res, terminalBrush, true)); + } + + const auto acrylicBrush = Media::AcrylicBrush(); + acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + acrylicBrush.FallbackColor(bgColor); + acrylicBrush.TintColor(bgColor); + acrylicBrush.TintOpacity(0.5); + + TitlebarBrush(acrylicBrush); + } + else if (tabRowBg) + { + const auto themeBrush{ tabRowBg.Evaluate(res, terminalBrush, true) }; + bgColor = ThemeColor::ColorFromBrush(themeBrush); + // If the tab content returned nullptr for the terminalBrush, we + // _don't_ want to use it as the tab row background. We want to just + // use the default tab row background. + TitlebarBrush(themeBrush ? themeBrush : backgroundSolidBrush); + } + else + { + // Nothing was set in the theme - fall back to our original `TabViewBackground` color. + TitlebarBrush(backgroundSolidBrush); + } + + if (!_settings.GlobalSettings().ShowTabsInTitlebar()) + { + _tabRow.Background(TitlebarBrush()); + } + + // Second: Update the colors of our individual TabViewItems. This + // applies tab.background to the tabs via Tab::ThemeColor. + // + // Do this second, so that we already know the bgColor of the titlebar. + { + const auto tabBackground = theme.Tab() ? theme.Tab().Background() : nullptr; + const auto tabUnfocusedBackground = theme.Tab() ? theme.Tab().UnfocusedBackground() : nullptr; + for (const auto& tab : _tabs) + { + winrt::com_ptr tabImpl; + tabImpl.copy_from(winrt::get_self(tab)); + tabImpl->ThemeColor(tabBackground, tabUnfocusedBackground, bgColor); + } + } + // Update the new tab button to have better contrast with the new color. + // In theory, it would be convenient to also change these for the + // inactive tabs as well, but we're leaving that as a follow up. + _SetNewTabButtonColor(bgColor, bgColor); + + // Third: the window frame. This is basically the same logic as the tab row background. + // We'll set our `FrameBrush` property, for the window to later use. + const auto windowTheme{ theme.Window() }; + if (auto windowFrame{ windowTheme ? (_activated ? windowTheme.Frame() : + windowTheme.UnfocusedFrame()) : + ThemeColor{ nullptr } }) + { + const auto themeBrush{ windowFrame.Evaluate(res, terminalBrush, true) }; + FrameBrush(themeBrush); + } + else + { + // Nothing was set in the theme - fall back to null. The window will + // use that as an indication to use the default window frame. + FrameBrush(nullptr); + } + } + + // Function Description: + // - Attempts to load some XAML resources that Panes will need. This includes: + // * The Color they'll use for active Panes's borders - SystemAccentColor + // * The Brush they'll use for inactive Panes - TabViewBackground (to match the + // color of the titlebar) + // Arguments: + // - requestedTheme: this should be the currently active Theme for the app + // Return Value: + // - + void TerminalPage::_updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme) + { + const auto res = Application::Current().Resources(); + const auto accentColorKey = winrt::box_value(L"SystemAccentColor"); + if (res.HasKey(accentColorKey)) + { + const auto colorFromResources = ThemeLookup(res, requestedTheme, 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(colorFromResources, Colors::Black()); + _paneResources.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. + _paneResources.focusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto unfocusedBorderBrushKey = winrt::box_value(L"UnfocusedBorderBrush"); + if (res.HasKey(unfocusedBorderBrushKey)) + { + // MAKE SURE TO USE ThemeLookup, so that we get the correct resource for + // the requestedTheme, not just the value from the resources (which + // might not respect the settings' requested theme) + auto obj = ThemeLookup(res, requestedTheme, unfocusedBorderBrushKey); + _paneResources.unfocusedBorderBrush = obj.try_as(); + } + 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. + _paneResources.unfocusedBorderBrush = SolidColorBrush{ Colors::Black() }; + } + + const auto broadcastColorKey = winrt::box_value(L"BroadcastPaneBorderColor"); + if (res.HasKey(broadcastColorKey)) + { + // MAKE SURE TO USE ThemeLookup + auto obj = ThemeLookup(res, requestedTheme, broadcastColorKey); + _paneResources.broadcastBorderBrush = obj.try_as(); + } + 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. + _paneResources.broadcastBorderBrush = SolidColorBrush{ Colors::Black() }; + } + } + + void TerminalPage::_adjustProcessPriority() const + { + // Windowing is single-threaded, so this will not cause a race condition. + static uint64_t s_lastUpdateHash{ 0 }; + static bool s_supported{ true }; + + if (!s_supported || !_hostingHwnd.has_value()) + { + return; + } + + std::array processes; + auto it = processes.begin(); + const auto end = processes.end(); + + auto&& appendFromControl = [&](auto&& control) { + if (it == end) + { + return; + } + if (control) + { + if (const auto conn{ control.Connection() }) + { + if (const auto pty{ conn.try_as() }) + { + if (const uint64_t process{ pty.RootProcessHandle() }; process != 0) + { + *it++ = reinterpret_cast(process); + } + } + } + } + }; + + auto&& appendFromTab = [&](auto&& tabImpl) { + if (const auto pane{ tabImpl->GetRootPane() }) + { + pane->WalkTree([&](auto&& child) { + if (const auto& control{ child->GetTerminalControl() }) + { + appendFromControl(control); + } + }); + } + }; + + if (!_activated) + { + // When a window is out of focus, we want to attach all of the processes + // under it to the window so they all go into the background at the same time. + for (auto&& tab : _tabs) + { + if (auto tabImpl{ _GetTabImpl(tab) }) + { + appendFromTab(tabImpl); + } + } + } + else + { + // When a window is in focus, propagate our foreground boost (if we have one) + // to current all panes in the current tab. + if (auto tabImpl{ _GetFocusedTabImpl() }) + { + appendFromTab(tabImpl); + } + } + + const auto count{ gsl::narrow_cast(it - processes.begin()) }; + const auto hash = til::hash((void*)processes.data(), count * sizeof(HANDLE)); + + if (hash == s_lastUpdateHash) + { + return; + } + + s_lastUpdateHash = hash; + const auto hr = TerminalTrySetWindowAssociatedProcesses(_hostingHwnd.value(), count, count ? processes.data() : nullptr); + + if (S_FALSE == hr) + { + // Don't bother trying again or logging. The wrapper tells us it's unsupported. + s_supported = false; + return; + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "CalledNewQoSAPI", + TraceLoggingValue(reinterpret_cast(_hostingHwnd.value()), "hwnd"), + TraceLoggingValue(count), + TraceLoggingHResult(hr)); +#ifdef _DEBUG + OutputDebugStringW(fmt::format(FMT_COMPILE(L"Submitted {} processes to TerminalTrySetWindowAssociatedProcesses; return=0x{:08x}\n"), count, hr).c_str()); +#endif + } + + void TerminalPage::WindowActivated(const bool activated) + { + // Stash if we're activated. Use that when we reload + // the settings, change active panes, etc. + _activated = activated; + _updateThemeColors(); + + _adjustProcessPriorityThrottled->Run(); + + if (const auto& tab{ _GetFocusedTabImpl() }) + { + if (tab->TabStatus().IsInputBroadcastActive()) + { + tab->GetRootPane()->WalkTree([activated](const auto& p) { + if (const auto& control{ p->GetTerminalControl() }) + { + control.CursorVisibility(activated ? + Microsoft::Terminal::Control::CursorDisplayState::Shown : + Microsoft::Terminal::Control::CursorDisplayState::Default); + } + }); + } + } + } + + safe_void_coroutine TerminalPage::_ControlCompletionsChangedHandler(const IInspectable sender, + const CompletionsChangedEventArgs args) + { + // This won't even get hit if the velocity flag is disabled - we gate + // registering for the event based off of + // Feature_ShellCompletions::IsEnabled back in _RegisterTerminalEvents + + // User must explicitly opt-in on Preview builds + if (!_settings.GlobalSettings().EnableShellCompletionMenu()) + { + co_return; + } + + // Parse the json string into a collection of actions + try + { + auto commandsCollection = Command::ParsePowerShellMenuComplete(args.MenuJson(), + args.ReplacementLength()); + + auto weakThis{ get_weak() }; + Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [weakThis, commandsCollection, sender]() { + // On the UI thread... + if (const auto& page{ weakThis.get() }) + { + // Open the Suggestions UI with the commands from the control + page->_OpenSuggestions(sender.try_as(), commandsCollection, SuggestionsMode::Menu, L""); + } + }); + } + CATCH_LOG(); + } + + void TerminalPage::_OpenSuggestions( + const TermControl& sender, + IVector commandsCollection, + winrt::TerminalApp::SuggestionsMode mode, + winrt::hstring filterText) + + { + // ON THE UI THREAD + assert(Dispatcher().HasThreadAccess()); + + if (commandsCollection == nullptr) + { + return; + } + if (commandsCollection.Size() == 0) + { + if (const auto p = SuggestionsElement()) + { + p.Visibility(Visibility::Collapsed); + } + return; + } + + const auto& control{ sender ? sender : _GetActiveControl() }; + if (!control) + { + return; + } + + const auto& sxnUi{ LoadSuggestionsUI() }; + + const auto characterSize{ control.CharacterDimensions() }; + // This is in control-relative space. We'll need to convert it to page-relative space. + const auto cursorPos{ control.CursorPositionInDips() }; + const auto controlTransform = control.TransformToVisual(this->Root()); + const auto realCursorPos{ controlTransform.TransformPoint({ cursorPos.X, cursorPos.Y }) }; // == controlTransform + cursorPos + const Windows::Foundation::Size windowDimensions{ gsl::narrow_cast(ActualWidth()), gsl::narrow_cast(ActualHeight()) }; + + sxnUi.Open(mode, + commandsCollection, + filterText, + realCursorPos, + windowDimensions, + characterSize.Height); + } + + void TerminalPage::_PopulateContextMenu(const TermControl& control, + const MUX::Controls::CommandBarFlyout& menu, + const bool withSelection) + { + // withSelection can be used to add actions that only appear if there's + // selected text, like "search the web" + + if (!control || !menu) + { + return; + } + + // Helper lambda for dispatching an ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. + + auto weak = get_weak(); + auto makeCallback = [weak](const ActionAndArgs& actionAndArgs) { + return [weak, actionAndArgs](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + page->_actionDispatch->DoAction(actionAndArgs); + } + }; + }; + + auto makeItem = [&makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& action, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + targetMenu.SecondaryCommands().Append(button); + }; + + auto makeMenuItem = [](const winrt::hstring& label, + const winrt::hstring& icon, + const auto& subMenu, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Flyout(subMenu); + targetMenu.SecondaryCommands().Append(button); + }; + + auto makeContextItem = [&makeCallback](const winrt::hstring& label, + const winrt::hstring& icon, + const winrt::hstring& tooltip, + const auto& action, + const auto& subMenu, + auto& targetMenu) { + AppBarButton button{}; + + if (!icon.empty()) + { + auto iconElement = UI::IconPathConverter::IconWUX(icon); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + button.Icon(iconElement); + } + + button.Label(label); + button.Click(makeCallback(action)); + WUX::Controls::ToolTipService::SetToolTip(button, box_value(tooltip)); + button.ContextFlyout(subMenu); + targetMenu.SecondaryCommands().Append(button); + }; + + const auto focusedProfile = _GetFocusedTabImpl()->GetFocusedProfile(); + auto separatorItem = AppBarSeparator{}; + auto activeProfiles = _settings.ActiveProfiles(); + auto activeProfileCount = gsl::narrow_cast(activeProfiles.Size()); + MUX::Controls::CommandBarFlyout splitPaneMenu{}; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }, menu); + + const auto focusedProfileName = focusedProfile.Name(); + const auto focusedProfileIcon = focusedProfile.Icon().Resolved(); + const auto splitPaneDuplicateText = RS_(L"SplitPaneDuplicateText") + L" " + focusedProfileName; // SplitPaneDuplicateText + + const auto splitPaneRightText = RS_(L"SplitPaneRightText"); + const auto splitPaneDownText = RS_(L"SplitPaneDownText"); + const auto splitPaneUpText = RS_(L"SplitPaneUpText"); + const auto splitPaneLeftText = RS_(L"SplitPaneLeftText"); + const auto splitPaneToolTipText = RS_(L"SplitPaneToolTipText"); + + MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; + makeItem(splitPaneRightText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Right, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneDownText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Down, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneUpText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Up, .5, nullptr } }, splitPaneContextMenu); + makeItem(splitPaneLeftText, focusedProfileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Left, .5, nullptr } }, splitPaneContextMenu); + + makeContextItem(splitPaneDuplicateText, focusedProfileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate, SplitDirection::Automatic, .5, nullptr } }, splitPaneContextMenu, splitPaneMenu); + + // add menu separator + const auto separatorAutoItem = AppBarSeparator{}; + + splitPaneMenu.SecondaryCommands().Append(separatorAutoItem); + + for (auto profileIndex = 0; profileIndex < activeProfileCount; profileIndex++) + { + const auto profile = activeProfiles.GetAt(profileIndex); + const auto profileName = profile.Name(); + const auto profileIcon = profile.Icon().Resolved(); + + NewTerminalArgs args{}; + args.Profile(profileName); + + MUX::Controls::CommandBarFlyout splitPaneContextMenu{}; + makeItem(splitPaneRightText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Right, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneDownText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Down, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneUpText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Up, .5, args } }, splitPaneContextMenu); + makeItem(splitPaneLeftText, profileIcon, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Left, .5, args } }, splitPaneContextMenu); + + makeContextItem(profileName, profileIcon, splitPaneToolTipText, ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Manual, SplitDirection::Automatic, .5, args } }, splitPaneContextMenu, splitPaneMenu); + } + + makeMenuItem(RS_(L"SplitPaneText"), L"\xF246", splitPaneMenu, menu); + + // Only wire up "Close Pane" if there's multiple panes. + if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) + { + MUX::Controls::CommandBarFlyout swapPaneMenu{}; + const auto rootPane = _GetFocusedTabImpl()->GetRootPane(); + const auto mruPanes = _GetFocusedTabImpl()->GetMruPanes(); + auto activePane = _GetFocusedTabImpl()->GetActivePane(); + rootPane->WalkTree([&](auto p) { + if (const auto& c{ p->GetTerminalControl() }) + { + if (c == control) + { + activePane = p; + } + } + }); + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Down, mruPanes)) + { + makeItem(RS_(L"SwapPaneDownText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Down } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Right, mruPanes)) + { + makeItem(RS_(L"SwapPaneRightText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Right } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Up, mruPanes)) + { + makeItem(RS_(L"SwapPaneUpText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Up } }, swapPaneMenu); + } + + if (auto neighbor = rootPane->NavigateDirection(activePane, FocusDirection::Left, mruPanes)) + { + makeItem(RS_(L"SwapPaneLeftText"), neighbor->GetProfile().Icon().Resolved(), ActionAndArgs{ ShortcutAction::SwapPane, SwapPaneArgs{ FocusDirection::Left } }, swapPaneMenu); + } + + makeMenuItem(RS_(L"SwapPaneText"), L"\xF1CB", swapPaneMenu, menu); + + makeItem(RS_(L"TogglePaneZoomText"), L"\xE8A3", ActionAndArgs{ ShortcutAction::TogglePaneZoom, nullptr }, menu); + makeItem(RS_(L"CloseOtherPanesText"), L"\xE89F", ActionAndArgs{ ShortcutAction::CloseOtherPanes, nullptr }, menu); + makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }, menu); + } + + if (control.ConnectionState() >= ConnectionState::Closed) + { + makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }, menu); + } + + if (withSelection) + { + makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }, menu); + } + + makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }, menu); + } + + void TerminalPage::_PopulateQuickFixMenu(const TermControl& control, + const Controls::MenuFlyout& menu) + { + if (!control || !menu) + { + return; + } + + // Helper lambda for dispatching a SendInput ActionAndArgs onto the + // ShortcutActionDispatch. Used below to wire up each menu entry to the + // respective action. Then clear the quick fix menu. + auto weak = get_weak(); + auto makeCallback = [weak](const hstring& suggestion) { + return [weak, suggestion](auto&&, auto&&) { + if (auto page{ weak.get() }) + { + const auto actionAndArgs = ActionAndArgs{ ShortcutAction::SendInput, SendInputArgs{ hstring{ L"\u0003" } + suggestion } }; + page->_actionDispatch->DoAction(actionAndArgs); + if (auto ctrl = page->_GetActiveControl()) + { + ctrl.ClearQuickFix(); + } + + TraceLoggingWrite( + g_hTerminalAppProvider, + "QuickFixSuggestionUsed", + TraceLoggingDescription("Event emitted when a winget suggestion from is used"), + TraceLoggingValue("QuickFixMenu", "Source"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + } + }; + }; + + // Wire up each item to the action that should be performed. By actually + // connecting these to actions, we ensure the implementation is + // consistent. This also leaves room for customizing this menu with + // actions in the future. + + menu.Items().Clear(); + const auto quickFixes = control.CommandHistory().QuickFixes(); + for (const auto& qf : quickFixes) + { + MenuFlyoutItem item{}; + + auto iconElement = UI::IconPathConverter::IconWUX(L"\ue74c"); + Automation::AutomationProperties::SetAccessibilityView(iconElement, Automation::Peers::AccessibilityView::Raw); + item.Icon(iconElement); + + item.Text(qf); + item.Click(makeCallback(qf)); + ToolTipService::SetToolTip(item, box_value(qf)); + menu.Items().Append(item); + } + } + + // Handler for our WindowProperties's PropertyChanged event. We'll use this + // to pop the "Identify Window" toast when the user renames our window. + void TerminalPage::_windowPropertyChanged(const IInspectable& /*sender*/, const WUX::Data::PropertyChangedEventArgs& args) + { + if (args.PropertyName() != L"WindowName") + { + return; + } + + // DON'T display the confirmation if this is the name we were + // given on startup! + if (_startupState == StartupState::Initialized) + { + IdentifyWindow(); + } + } + + void TerminalPage::_onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView&, + const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e) + { + const auto eventTab = e.Tab(); + const auto draggedTab = _GetTabByTabViewItem(eventTab); + if (draggedTab) + { + auto draggedTabs = _IsTabSelected(draggedTab) ? _GetSelectedTabsInDisplayOrder() : + std::vector{}; + if (draggedTabs.empty() || + !std::ranges::any_of(draggedTabs, [&](const auto& tab) { return tab == draggedTab; })) + { + draggedTabs = { draggedTab }; + _SetSelectedTabs(draggedTabs, draggedTab); + } + + _stashed.draggedTabs = std::move(draggedTabs); + _stashed.dragAnchor = draggedTab; + + // Stash the offset from where we started the drag to the + // tab's origin. We'll use that offset in the future to help + // position the dropped window. + const auto inverseScale = 1.0f / static_cast(eventTab.XamlRoot().RasterizationScale()); + POINT cursorPos; + GetCursorPos(&cursorPos); + ScreenToClient(*_hostingHwnd, &cursorPos); + _stashed.dragOffset.X = cursorPos.x * inverseScale; + _stashed.dragOffset.Y = cursorPos.y * inverseScale; + + // Into the DataPackage, let's stash our own window ID. + const auto id{ _WindowProperties.WindowId() }; + + // Get our PID + const auto pid{ GetCurrentProcessId() }; + + e.Data().Properties().Insert(L"windowId", winrt::box_value(id)); + e.Data().Properties().Insert(L"pid", winrt::box_value(pid)); + e.Data().RequestedOperation(DataPackageOperation::Move); + + // The next thing that will happen: + // * Another TerminalPage will get a TabStripDragOver, then get a + // TabStripDrop + // * This will be handled by the _other_ page asking the monarch + // to ask us to send our content to them. + // * We'll get a TabDroppedOutside to indicate that this tab was + // dropped _not_ on a TabView. + // * This will be handled by _onTabDroppedOutside, which will + // raise a MoveContent (to a new window) event. + } + } + + void TerminalPage::_onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& /*sender*/, + const winrt::Windows::UI::Xaml::DragEventArgs& e) + { + // We must mark that we can accept the drag/drop. The system will never + // call TabStripDrop on us if we don't indicate that we're willing. + const auto& props{ e.DataView().Properties() }; + if (props.HasKey(L"windowId") && + props.HasKey(L"pid") && + (winrt::unbox_value_or(props.TryLookup(L"pid"), 0u) == GetCurrentProcessId())) + { + e.AcceptedOperation(DataPackageOperation::Move); + } + + // You may think to yourself, this is a great place to increase the + // width of the TabView artificially, to make room for the new tab item. + // However, we'll never get a message that the tab left the tab view + // (without being dropped). So there's no good way to resize back down. + } + + // Method Description: + // - Called on the TARGET of a tab drag/drop. We'll unpack the DataPackage + // to find who the tab came from. We'll then ask the Monarch to ask the + // sender to move that tab to us. + void TerminalPage::_onTabStripDrop(winrt::Windows::Foundation::IInspectable /*sender*/, + winrt::Windows::UI::Xaml::DragEventArgs e) + { + // Get the PID and make sure it is the same as ours. + if (const auto& pidObj{ e.DataView().Properties().TryLookup(L"pid") }) + { + const auto pid{ winrt::unbox_value_or(pidObj, 0u) }; + if (pid != GetCurrentProcessId()) + { + // The PID doesn't match ours. We can't handle this drop. + return; + } + } + else + { + // No PID? We can't handle this drop. Bail. + return; + } + + const auto& windowIdObj{ e.DataView().Properties().TryLookup(L"windowId") }; + if (windowIdObj == nullptr) + { + // No windowId? Bail. + return; + } + const uint64_t src{ winrt::unbox_value(windowIdObj) }; + + // Figure out where in the tab strip we're dropping this tab. Add that + // index to the request. This is largely taken from the WinUI sample + // app. + + // First we need to get the position in the List to drop to + auto index = -1; + + // Determine which items in the list our pointer is between. + for (auto i = 0u; i < _tabView.TabItems().Size(); i++) + { + if (const auto& item{ _tabView.ContainerFromIndex(i).try_as() }) + { + const auto posX{ e.GetPosition(item).X }; // The point of the drop, relative to the tab + const auto itemWidth{ item.ActualWidth() }; // The right of the tab + // If the drag point is on the left half of the tab, then insert here. + if (posX < itemWidth / 2) + { + index = i; + break; + } + } + } + + if (index < 0) + { + index = gsl::narrow_cast(_tabView.TabItems().Size()); + } + + // `this` is safe to use + const auto request = winrt::make_self(src, _WindowProperties.WindowId(), index); + + // This will go up to the monarch, who will then dispatch the request + // back down to the source TerminalPage, who will then perform a + // RequestMoveContent to move their tab to us. + RequestReceiveContent.raise(*this, *request); + } + + // Method Description: + // - This is called on the drag/drop SOURCE TerminalPage, when the monarch has + // requested that we send our tab to another window. We'll need to + // serialize the tab, and send it to the monarch, who will then send it to + // the destination window. + // - Fortunately, sending the tab is basically just a MoveTab action, so we + // can largely reuse that. + void TerminalPage::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args) + { + // validate that we're the source window of the tab in this request + if (args.SourceWindow() != _WindowProperties.WindowId()) + { + return; + } + if (_stashed.draggedTabs.empty()) + { + return; + } + + _sendDraggedTabsToWindow(winrt::to_hstring(args.TargetWindow()), args.TabIndex(), std::nullopt); + } + + void TerminalPage::_onTabDroppedOutside(winrt::IInspectable /*sender*/, + winrt::MUX::Controls::TabViewTabDroppedOutsideEventArgs /*e*/) + { + // Get the current pointer point from the CoreWindow + const auto& pointerPoint{ CoreWindow::GetForCurrentThread().PointerPosition() }; + + // This is called when a tab FROM OUR WINDOW was dropped outside the + // tabview. We already know which tab was being dragged. We'll just + // invoke a moveTab action with the target window being -1. That will + // force the creation of a new window. + + if (_stashed.draggedTabs.empty()) + { + return; + } + + // We need to convert the pointer point to a point that we can use + // to position the new window. We'll use the drag offset from before + // so that the tab in the new window is positioned so that it's + // basically still directly under the cursor. + + // -1 is the magic number for "new window" + // 0 as the tab index, because we don't care. It's making a new window. It'll be the only tab. + const winrt::Windows::Foundation::Point adjusted = { + pointerPoint.X - _stashed.dragOffset.X, + pointerPoint.Y - _stashed.dragOffset.Y, + }; + _sendDraggedTabsToWindow(winrt::hstring{ L"-1" }, 0, adjusted); + } + + void TerminalPage::_sendDraggedTabsToWindow(const winrt::hstring& windowId, + const uint32_t tabIndex, + std::optional dragPoint) + { + if (_stashed.draggedTabs.empty()) + { + return; + } + + auto draggedTabs = _stashed.draggedTabs; + auto startupActions = _BuildStartupActionsForTabs(draggedTabs); + if (dragPoint.has_value() && draggedTabs.size() > 1 && _stashed.dragAnchor) + { + const auto draggedAnchorIt = std::ranges::find_if(draggedTabs, [&](const auto& tab) { + return tab == _stashed.dragAnchor; + }); + if (draggedAnchorIt != draggedTabs.end()) + { + ActionAndArgs switchToTabAction{}; + switchToTabAction.Action(ShortcutAction::SwitchToTab); + switchToTabAction.Args(SwitchToTabArgs{ gsl::narrow_cast(std::distance(draggedTabs.begin(), draggedAnchorIt)) }); + startupActions.emplace_back(std::move(switchToTabAction)); + } + } + + for (const auto& tab : draggedTabs) + { + if (const auto tabImpl{ _GetTabImpl(tab) }) + { + _DetachTabFromWindow(tabImpl); + } + } + + _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); + + for (auto it = draggedTabs.rbegin(); it != draggedTabs.rend(); ++it) + { + _RemoveTab(*it); + } + + _stashed.draggedTabs.clear(); + _stashed.dragAnchor = nullptr; + } + + /// + /// Creates a sub flyout menu for profile items in the split button menu that when clicked will show a menu item for + /// Run as Administrator + /// + /// The index for the profileMenuItem + /// MenuFlyout that will show when the context is request on a profileMenuItem + WUX::Controls::MenuFlyout TerminalPage::_CreateRunAsAdminFlyout(int profileIndex) + { + // Create the MenuFlyout and set its placement + WUX::Controls::MenuFlyout profileMenuItemFlyout{}; + profileMenuItemFlyout.Placement(WUX::Controls::Primitives::FlyoutPlacementMode::BottomEdgeAlignedRight); + + // Create the menu item and an icon to use in the menu + WUX::Controls::MenuFlyoutItem runAsAdminItem{}; + WUX::Controls::FontIcon adminShieldIcon{}; + + adminShieldIcon.Glyph(L"\xEA18"); + adminShieldIcon.FontFamily(Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" }); + + runAsAdminItem.Icon(adminShieldIcon); + runAsAdminItem.Text(RS_(L"RunAsAdminFlyout/Text")); + + // Click handler for the flyout item + runAsAdminItem.Click([profileIndex, weakThis{ get_weak() }](auto&&, auto&&) { + if (auto page{ weakThis.get() }) + { + TraceLoggingWrite( + g_hTerminalAppProvider, + "NewTabMenuItemElevateSubmenuItemClicked", + TraceLoggingDescription("Event emitted when the elevate submenu item from the new tab menu is invoked"), + TraceLoggingValue(page->NumberOfTabs(), "TabCount", "The count of tabs currently opened in this window"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage)); + + NewTerminalArgs args{ profileIndex }; + args.Elevate(true); + page->_OpenNewTerminalViaDropdown(args); + } + }); + + profileMenuItemFlyout.Items().Append(runAsAdminItem); + + return profileMenuItemFlyout; + } +} diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 44c9922e881..1c8f1996d4c 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -1,604 +1,604 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once - -#include - -#include "TerminalPage.g.h" -#include "Tab.h" -#include "AppKeyBindings.h" -#include "AppCommandlineArgs.h" -#include "RenameWindowRequestedArgs.g.h" -#include "RequestMoveContentArgs.g.h" -#include "LaunchPositionRequest.g.h" -#include "Toast.h" - -#include "WindowsPackageManagerFactory.h" - -#define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); - -namespace TerminalAppLocalTests -{ - class TabTests; - class SettingsTests; -} - -namespace Microsoft::Terminal::Core -{ - class ControlKeyStates; -} - -namespace winrt::Microsoft::Terminal::Settings -{ - struct TerminalSettingsCreateResult; -} - -namespace winrt::TerminalApp::implementation -{ - struct TerminalSettingsCache; - - inline constexpr uint32_t DefaultRowsToScroll{ 3 }; - inline constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; - - enum StartupState : int - { - NotInitialized = 0, - InStartup = 1, - Initialized = 2 - }; - - enum ScrollDirection : int - { - ScrollUp = 0, - ScrollDown = 1 - }; - - struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT - { - WINRT_PROPERTY(winrt::hstring, ProposedName); - - public: - RenameWindowRequestedArgs(const winrt::hstring& name) : - _ProposedName{ name } {}; - }; - - struct RequestMoveContentArgs : RequestMoveContentArgsT - { - WINRT_PROPERTY(winrt::hstring, Window); - WINRT_PROPERTY(winrt::hstring, Content); - WINRT_PROPERTY(uint32_t, TabIndex); - WINRT_PROPERTY(Windows::Foundation::IReference, WindowPosition); - - public: - RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) : - _Window{ window }, - _Content{ content }, - _TabIndex{ tabIndex } {}; - }; - - struct LaunchPositionRequest : LaunchPositionRequestT - { - LaunchPositionRequest() = default; - - til::property Position; - }; - - struct WinGetSearchParams - { - winrt::Microsoft::Management::Deployment::PackageMatchField Field; - winrt::Microsoft::Management::Deployment::PackageFieldMatchOption MatchOption; - }; - - struct TerminalPage : TerminalPageT - { - public: - TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager); - - // This implements shobjidl's IInitializeWithWindow, but due to a XAML Compiler bug we cannot - // put it in our inheritance graph. https://github.com/microsoft/microsoft-ui-xaml/issues/3331 - STDMETHODIMP Initialize(HWND hwnd); - - void SetSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings, bool needRefreshUI); - - void Create(); - Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); - - bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; - void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); - - hstring Title(); - - void TitlebarClicked(); - void WindowVisibilityChanged(const bool showOrHide); - - float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; - - winrt::hstring ApplicationDisplayName(); - winrt::hstring ApplicationVersion(); - - CommandPalette LoadCommandPalette(); - SuggestionsControl LoadSuggestionsUI(); - - safe_void_coroutine RequestQuit(); - safe_void_coroutine CloseWindow(); - void PersistState(); - std::vector Panes() const; - - void ToggleFocusMode(); - void ToggleFullscreen(); - void ToggleAlwaysOnTop(); - bool FocusMode() const; - bool Fullscreen() const; - bool AlwaysOnTop() const; - bool ShowTabsFullscreen() const; - void SetShowTabsFullscreen(bool newShowTabsFullscreen); - void SetFullscreen(bool); - void SetFocusMode(const bool inFocusMode); - void Maximized(bool newMaximized); - void RequestSetMaximized(bool newMaximized); - - void SetStartupActions(std::vector actions); - void SetStartupConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); - - static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); - - winrt::TerminalApp::IDialogPresenter DialogPresenter() const; - void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); - - winrt::TerminalApp::TaskbarState TaskbarState() const; - - void ShowKeyboardServiceWarning() const; - winrt::hstring KeyboardServiceDisabledText(); - - void IdentifyWindow(); - void ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord); - void ActionSaveFailed(winrt::hstring message); - void ShowTerminalWorkingDirectory(); - - safe_void_coroutine ProcessStartupActions(std::vector actions, - const winrt::hstring cwd = winrt::hstring{}, - const winrt::hstring env = winrt::hstring{}); - safe_void_coroutine CreateTabFromConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); - - TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; - - bool CanDragDrop() const noexcept; - bool IsRunningElevated() const noexcept; - - void OpenSettingsUI(); - void WindowActivated(const bool activated); - - bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); - - void AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); - void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); - - uint32_t NumberOfTabs() const; - bool SelectTabRangeForTesting(uint32_t startIndex, uint32_t endIndex); - - til::property_changed_event PropertyChanged; - - // -------------------------------- WinRT Events --------------------------------- - til::typed_event TitleChanged; - til::typed_event CloseWindowRequested; - til::typed_event SetTitleBarContent; - til::typed_event FocusModeChanged; - til::typed_event FullscreenChanged; - til::typed_event ChangeMaximizeRequested; - til::typed_event AlwaysOnTopChanged; - til::typed_event RaiseVisualBell; - til::typed_event SetTaskbarProgress; - til::typed_event Initialized; - til::typed_event IdentifyWindowsRequested; - til::typed_event RenameWindowRequested; - til::typed_event SummonWindowRequested; - til::typed_event WindowSizeChanged; - - til::typed_event OpenSystemMenu; - til::typed_event QuitRequested; - til::typed_event ShowWindowChanged; - til::typed_event> ShowLoadWarningsDialog; - - til::typed_event RequestMoveContent; - til::typed_event RequestReceiveContent; - - til::typed_event RequestLaunchPosition; - - WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); - WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); - - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionName, PropertyChanged.raise, L""); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionKeyChord, PropertyChanged.raise, L""); - WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionCommandLine, PropertyChanged.raise, L""); - - private: - friend struct TerminalPageT; // for Xaml to bind events - std::optional _hostingHwnd; - - // If you add controls here, but forget to null them either here or in - // the ctor, you're going to have a bad time. It'll mysteriously fail to - // activate the app. - // ALSO: If you add any UIElements as roots here, make sure they're - // updated in App::_ApplyTheme. The roots currently is _tabRow - // (which is a root when the tabs are in the titlebar.) - Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr }; - TerminalApp::TabRowControl _tabRow{ nullptr }; - Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; - Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; - winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; - - Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; - - Windows::Foundation::Collections::IObservableVector _tabs; - Windows::Foundation::Collections::IObservableVector _mruTabs; - static winrt::com_ptr _GetTabImpl(const TerminalApp::Tab& tab); - - void _UpdateTabIndices(); - - TerminalApp::Tab _settingsTab{ nullptr }; - - bool _isInFocusMode{ false }; - bool _isFullscreen{ false }; - bool _isMaximized{ false }; - bool _isAlwaysOnTop{ false }; - bool _showTabsFullscreen{ false }; - - std::optional _loadFromPersistedLayoutIdx{}; - - bool _rearranging{ false }; - std::optional _rearrangeFrom{}; - std::optional _rearrangeTo{}; - bool _removing{ false }; - std::vector _selectedTabs{}; - winrt::TerminalApp::Tab _selectionAnchor{ nullptr }; - - bool _activated{ false }; - bool _visible{ true }; - - std::vector> _previouslyClosedPanesAndTabs{}; - - uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; - - // use a weak reference to prevent circular dependency with AppLogic - winrt::weak_ref _dialogPresenter; - - winrt::com_ptr _bindings{ winrt::make_self() }; - winrt::com_ptr _actionDispatch{ winrt::make_self() }; - - winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker; - StartupState _startupState{ StartupState::NotInitialized }; - - std::vector _startupActions; - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _startupConnection{ nullptr }; - - std::shared_ptr _windowIdToast{ nullptr }; - std::shared_ptr _actionSavedToast{ nullptr }; - std::shared_ptr _actionSaveFailedToast{ nullptr }; - std::shared_ptr _windowCwdToast{ nullptr }; - - winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _renamerLayoutUpdatedRevoker; - int _renamerLayoutCount{ 0 }; - bool _renamerPressedEnter{ false }; - - TerminalApp::WindowProperties _WindowProperties{ nullptr }; - PaneResources _paneResources; - - TerminalApp::ContentManager _manager{ nullptr }; - - std::shared_ptr _terminalSettingsCache{}; - - struct StashedDragData - { - std::vector draggedTabs{}; - winrt::TerminalApp::Tab dragAnchor{ nullptr }; - winrt::Windows::Foundation::Point dragOffset{ 0, 0 }; - } _stashed; - - safe_void_coroutine _NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e); - - __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); - bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); - __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); - bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); - - winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); - - void _ShowAboutDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); - winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); - - void _CreateNewTabFlyout(); - std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector entries); - winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon); - winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex, const winrt::hstring& iconPathOverride); - winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride); - - void _OpenNewTabDropdown(); - HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs); - TerminalApp::Tab _CreateNewTabFromPane(std::shared_ptr pane, uint32_t insertPosition = -1); - - std::wstring _evaluatePathForCwd(std::wstring_view path); - - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Control::IControlSettings settings, const bool inheritCursor); - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent); - void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); - - safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); - - void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); - - bool _displayingCloseDialog{ false }; - void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - void _AboutButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); - - void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept; - static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept; - void _HookupKeyBindings(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap) noexcept; - void _RegisterActionCallbacks(); - - void _UpdateTitle(const Tab& tab); - void _UpdateTabIcon(Tab& tab); - void _UpdateTabView(); - void _UpdateTabWidthMode(); - void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); - - void _DuplicateFocusedTab(); - void _DuplicateTab(const Tab& tab); - - safe_void_coroutine _ExportTab(const Tab& tab, winrt::hstring filepath); - - winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab); - void _CloseTabAtIndex(uint32_t index); - void _RemoveTab(const winrt::TerminalApp::Tab& tab); - safe_void_coroutine _RemoveTabs(const std::vector tabs); - - void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); - void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); - void _RegisterTabEvents(Tab& hostingTab); - - void _DismissTabContextMenus(); - void _FocusCurrentTab(const bool focusAlways); - bool _HasMultipleTabs() const; - - void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); - bool _SelectTab(uint32_t tabIndex); - bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); - bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); - bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); - - std::shared_ptr> _adjustProcessPriorityThrottled; - void _adjustProcessPriority() const; - - template - bool _ApplyToActiveControls(F f) const - { - if (const auto tab{ _GetFocusedTabImpl() }) - { - if (const auto activePane = tab->GetActivePane()) - { - activePane->WalkTree([&](auto p) { - if (const auto& control{ p->GetTerminalControl() }) - { - f(control); - } - }); - - return true; - } - } - return false; - } - - winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl() const; - std::optional _GetFocusedTabIndex() const noexcept; - std::optional _GetTabIndex(const TerminalApp::Tab& tab) const noexcept; - TerminalApp::Tab _GetFocusedTab() const noexcept; - winrt::com_ptr _GetFocusedTabImpl() const noexcept; - TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept; - - void _HandleClosePaneRequested(std::shared_ptr pane); - safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab); - safe_void_coroutine _CloseFocusedPane(); - void _ClosePanes(weak_ref weakTab, std::vector paneIds); - winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); - void _AddPreviouslyClosedPaneOrTab(std::vector&& args); - - void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); - - void _SplitPane(const winrt::com_ptr& tab, - const Microsoft::Terminal::Settings::Model::SplitDirection splitType, - const float splitSize, - std::shared_ptr newPane); - void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); - void _ToggleSplitOrientation(); - - void _ScrollPage(ScrollDirection scrollDirection); - void _ScrollToBufferEdge(ScrollDirection scrollDirection); - void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Control::KeyChord& keyChord); - - safe_void_coroutine _PasteFromClipboardHandler(const IInspectable sender, - const Microsoft::Terminal::Control::PasteFromClipboardEventArgs eventArgs); - - void _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs); - bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri); - - void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); - bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats); - - safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); - - void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const; - void _PasteText(); - - safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); - void _ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message); - - safe_void_coroutine _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); - - void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs); - void _TabDragCompleted(const IInspectable& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs); - - // BODGY: WinUI's TabView has a broken close event handler: - // If the close button is disabled, middle-clicking the tab raises no close - // event. Because that's dumb, we implement our own middle-click handling. - // `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden, - // and that enables all of the rest of this machinery (and this workaround). - bool _tabItemMiddleClickHookEnabled = false; - bool _tabItemMiddleClickExited = false; - PointerEntered_revoker _tabItemMiddleClickPointerEntered; - PointerExited_revoker _tabItemMiddleClickPointerExited; - PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost; - void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); - safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender); - - void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); - void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); - void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs); - void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs); - void _UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab); - void _UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); - - void _OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command); - void _OnCommandLineExecutionRequested(const IInspectable& sender, const winrt::hstring& commandLine); - void _OnSwitchToTabRequested(const IInspectable& sender, const winrt::TerminalApp::Tab& tab); - - void _Find(const Tab& tab); - - winrt::Microsoft::Terminal::Control::TermControl _CreateNewControlAndContent(const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& settings, - const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); - winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term); - winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid); - - TerminalApp::IPaneContent _makeSettingsContent(); - std::shared_ptr _MakeTerminalPane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr, - const winrt::TerminalApp::Tab& sourceTab = nullptr, - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - std::shared_ptr _MakePane(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs = nullptr, - const winrt::TerminalApp::Tab& sourceTab = nullptr, - winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); - - void _RefreshUIForSettingsReload(); - - void _SetNewTabButtonColor(til::color color, til::color accentColor); - void _ClearNewTabButtonColor(); - - safe_void_coroutine _CompleteInitialization(); - - void _FocusActiveControl(IInspectable sender, IInspectable eventArgs); - - void _UnZoomIfNeeded(); - - static int _ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll); - static uint32_t _ReadSystemRowsToScroll(); - - void _UpdateMRUTab(const winrt::TerminalApp::Tab& tab); - bool _TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept; - bool _IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept; - void _SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor = nullptr); - void _RemoveSelectedTab(const winrt::TerminalApp::Tab& tab); - std::vector _GetSelectedTabsInDisplayOrder() const; - std::vector _GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const; - void _ApplyMultiSelectionVisuals(); - void _UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab); - void _MoveTabsToIndex(const std::vector& tabs, uint32_t suggestedNewTabIndex); - std::vector _CollectNewTabs(const std::vector& existingTabs) const; - std::vector _BuildStartupActionsForTabs(const std::vector& tabs) const; - - void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex); - - void _PreviewAction(const Microsoft::Terminal::Settings::Model::ActionAndArgs& args); - void _PreviewActionHandler(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& args); - void _EndPreview(); - void _RunRestorePreviews(); - void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); - void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); - void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args); - - winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; - std::vector> _restorePreviewFuncs{}; - - HRESULT _OnNewConnection(const winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection& connection); - void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); - - void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); - void _RequestWindowRename(const winrt::hstring& newName); - void _WindowRenamerKeyDown(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - void _WindowRenamerKeyUp(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); - - void _UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element); - - winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept; - - bool _maybeElevate(const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, - const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& controlSettings, - const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); - void _OpenElevatedWT(winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); - - safe_void_coroutine _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); - void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; - void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; - static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); - static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); - - void _updateThemeColors(); - void _updateAllTabCloseButtons(); - void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - - safe_void_coroutine _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); - - void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode, winrt::hstring filterText); - - void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); - Windows::Foundation::IAsyncAction _SearchMissingCommandHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SearchMissingCommandEventArgs args); - static Windows::Foundation::IAsyncOperation> _FindPackageAsync(hstring query); - - void _WindowSizeChanged(const IInspectable sender, const winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs args); - void _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); - - void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); - void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); - void _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e); - void _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e); - - void _DetachPaneFromWindow(std::shared_ptr pane); - void _DetachTabFromWindow(const winrt::com_ptr& tabImpl); - void _MoveContent(std::vector&& actions, - const winrt::hstring& windowName, - const uint32_t tabIndex, - const std::optional& dragPoint = std::nullopt); - void _sendDraggedTabsToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); - - void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); - void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); - winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); - - winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); - winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); - - void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args); - safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs); - -#pragma region ActionHandlers - // These are all defined in AppActionHandlers.cpp -#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); - ALL_SHORTCUT_ACTIONS - INTERNAL_SHORTCUT_ACTIONS -#undef ON_ALL_ACTIONS -#pragma endregion - - friend class TerminalAppLocalTests::TabTests; - friend class TerminalAppLocalTests::SettingsTests; - }; -} - -namespace winrt::TerminalApp::factory_implementation -{ - BASIC_FACTORY(TerminalPage); -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include + +#include "TerminalPage.g.h" +#include "Tab.h" +#include "AppKeyBindings.h" +#include "AppCommandlineArgs.h" +#include "RenameWindowRequestedArgs.g.h" +#include "RequestMoveContentArgs.g.h" +#include "LaunchPositionRequest.g.h" +#include "Toast.h" + +#include "WindowsPackageManagerFactory.h" + +#define DECLARE_ACTION_HANDLER(action) void _Handle##action(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + +namespace TerminalAppLocalTests +{ + class TabTests; + class SettingsTests; +} + +namespace Microsoft::Terminal::Core +{ + class ControlKeyStates; +} + +namespace winrt::Microsoft::Terminal::Settings +{ + struct TerminalSettingsCreateResult; +} + +namespace winrt::TerminalApp::implementation +{ + struct TerminalSettingsCache; + + inline constexpr uint32_t DefaultRowsToScroll{ 3 }; + inline constexpr std::wstring_view TabletInputServiceKey{ L"TabletInputService" }; + + enum StartupState : int + { + NotInitialized = 0, + InStartup = 1, + Initialized = 2 + }; + + enum ScrollDirection : int + { + ScrollUp = 0, + ScrollDown = 1 + }; + + struct RenameWindowRequestedArgs : RenameWindowRequestedArgsT + { + WINRT_PROPERTY(winrt::hstring, ProposedName); + + public: + RenameWindowRequestedArgs(const winrt::hstring& name) : + _ProposedName{ name } {}; + }; + + struct RequestMoveContentArgs : RequestMoveContentArgsT + { + WINRT_PROPERTY(winrt::hstring, Window); + WINRT_PROPERTY(winrt::hstring, Content); + WINRT_PROPERTY(uint32_t, TabIndex); + WINRT_PROPERTY(Windows::Foundation::IReference, WindowPosition); + + public: + RequestMoveContentArgs(const winrt::hstring window, const winrt::hstring content, uint32_t tabIndex) : + _Window{ window }, + _Content{ content }, + _TabIndex{ tabIndex } {}; + }; + + struct LaunchPositionRequest : LaunchPositionRequestT + { + LaunchPositionRequest() = default; + + til::property Position; + }; + + struct WinGetSearchParams + { + winrt::Microsoft::Management::Deployment::PackageMatchField Field; + winrt::Microsoft::Management::Deployment::PackageFieldMatchOption MatchOption; + }; + + struct TerminalPage : TerminalPageT + { + public: + TerminalPage(TerminalApp::WindowProperties properties, const TerminalApp::ContentManager& manager); + + // This implements shobjidl's IInitializeWithWindow, but due to a XAML Compiler bug we cannot + // put it in our inheritance graph. https://github.com/microsoft/microsoft-ui-xaml/issues/3331 + STDMETHODIMP Initialize(HWND hwnd); + + void SetSettings(Microsoft::Terminal::Settings::Model::CascadiaSettings settings, bool needRefreshUI); + + void Create(); + Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); + + bool ShouldImmediatelyHandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings) const; + void HandoffToElevated(const Microsoft::Terminal::Settings::Model::CascadiaSettings& settings); + + hstring Title(); + + void TitlebarClicked(); + void WindowVisibilityChanged(const bool showOrHide); + + float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const; + + winrt::hstring ApplicationDisplayName(); + winrt::hstring ApplicationVersion(); + + CommandPalette LoadCommandPalette(); + SuggestionsControl LoadSuggestionsUI(); + + safe_void_coroutine RequestQuit(); + safe_void_coroutine CloseWindow(); + void PersistState(); + std::vector Panes() const; + + void ToggleFocusMode(); + void ToggleFullscreen(); + void ToggleAlwaysOnTop(); + bool FocusMode() const; + bool Fullscreen() const; + bool AlwaysOnTop() const; + bool ShowTabsFullscreen() const; + void SetShowTabsFullscreen(bool newShowTabsFullscreen); + void SetFullscreen(bool); + void SetFocusMode(const bool inFocusMode); + void Maximized(bool newMaximized); + void RequestSetMaximized(bool newMaximized); + + void SetStartupActions(std::vector actions); + void SetStartupConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + + static std::vector ConvertExecuteCommandlineToActions(const Microsoft::Terminal::Settings::Model::ExecuteCommandlineArgs& args); + + winrt::TerminalApp::IDialogPresenter DialogPresenter() const; + void DialogPresenter(winrt::TerminalApp::IDialogPresenter dialogPresenter); + + winrt::TerminalApp::TaskbarState TaskbarState() const; + + void ShowKeyboardServiceWarning() const; + winrt::hstring KeyboardServiceDisabledText(); + + void IdentifyWindow(); + void ActionSaved(winrt::hstring input, winrt::hstring name, winrt::hstring keyChord); + void ActionSaveFailed(winrt::hstring message); + void ShowTerminalWorkingDirectory(); + + safe_void_coroutine ProcessStartupActions(std::vector actions, + const winrt::hstring cwd = winrt::hstring{}, + const winrt::hstring env = winrt::hstring{}); + safe_void_coroutine CreateTabFromConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection connection); + + TerminalApp::WindowProperties WindowProperties() const noexcept { return _WindowProperties; }; + + bool CanDragDrop() const noexcept; + bool IsRunningElevated() const noexcept; + + void OpenSettingsUI(); + void WindowActivated(const bool activated); + + bool OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down); + + void AttachContent(Windows::Foundation::Collections::IVector args, uint32_t tabIndex); + void SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args); + + uint32_t NumberOfTabs() const; + bool SelectTabRangeForTesting(uint32_t startIndex, uint32_t endIndex); + + til::property_changed_event PropertyChanged; + + // -------------------------------- WinRT Events --------------------------------- + til::typed_event TitleChanged; + til::typed_event CloseWindowRequested; + til::typed_event SetTitleBarContent; + til::typed_event FocusModeChanged; + til::typed_event FullscreenChanged; + til::typed_event ChangeMaximizeRequested; + til::typed_event AlwaysOnTopChanged; + til::typed_event RaiseVisualBell; + til::typed_event SetTaskbarProgress; + til::typed_event Initialized; + til::typed_event IdentifyWindowsRequested; + til::typed_event RenameWindowRequested; + til::typed_event SummonWindowRequested; + til::typed_event WindowSizeChanged; + + til::typed_event OpenSystemMenu; + til::typed_event QuitRequested; + til::typed_event ShowWindowChanged; + til::typed_event> ShowLoadWarningsDialog; + + til::typed_event RequestMoveContent; + til::typed_event RequestReceiveContent; + + til::typed_event RequestLaunchPosition; + + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, TitlebarBrush, PropertyChanged.raise, nullptr); + WINRT_OBSERVABLE_PROPERTY(winrt::Windows::UI::Xaml::Media::Brush, FrameBrush, PropertyChanged.raise, nullptr); + + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionName, PropertyChanged.raise, L""); + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionKeyChord, PropertyChanged.raise, L""); + WINRT_OBSERVABLE_PROPERTY(winrt::hstring, SavedActionCommandLine, PropertyChanged.raise, L""); + + private: + friend struct TerminalPageT; // for Xaml to bind events + std::optional _hostingHwnd; + + // If you add controls here, but forget to null them either here or in + // the ctor, you're going to have a bad time. It'll mysteriously fail to + // activate the app. + // ALSO: If you add any UIElements as roots here, make sure they're + // updated in App::_ApplyTheme. The roots currently is _tabRow + // (which is a root when the tabs are in the titlebar.) + Microsoft::UI::Xaml::Controls::TabView _tabView{ nullptr }; + TerminalApp::TabRowControl _tabRow{ nullptr }; + Windows::UI::Xaml::Controls::Grid _tabContent{ nullptr }; + Microsoft::UI::Xaml::Controls::SplitButton _newTabButton{ nullptr }; + winrt::TerminalApp::ColorPickupFlyout _tabColorPicker{ nullptr }; + + Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr }; + + Windows::Foundation::Collections::IObservableVector _tabs; + Windows::Foundation::Collections::IObservableVector _mruTabs; + static winrt::com_ptr _GetTabImpl(const TerminalApp::Tab& tab); + + void _UpdateTabIndices(); + + TerminalApp::Tab _settingsTab{ nullptr }; + + bool _isInFocusMode{ false }; + bool _isFullscreen{ false }; + bool _isMaximized{ false }; + bool _isAlwaysOnTop{ false }; + bool _showTabsFullscreen{ false }; + + std::optional _loadFromPersistedLayoutIdx{}; + + bool _rearranging{ false }; + std::optional _rearrangeFrom{}; + std::optional _rearrangeTo{}; + bool _removing{ false }; + std::vector _selectedTabs{}; + winrt::TerminalApp::Tab _selectionAnchor{ nullptr }; + + bool _activated{ false }; + bool _visible{ true }; + + std::vector> _previouslyClosedPanesAndTabs{}; + + uint32_t _systemRowsToScroll{ DefaultRowsToScroll }; + + // use a weak reference to prevent circular dependency with AppLogic + winrt::weak_ref _dialogPresenter; + + winrt::com_ptr _bindings{ winrt::make_self() }; + winrt::com_ptr _actionDispatch{ winrt::make_self() }; + + winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker; + StartupState _startupState{ StartupState::NotInitialized }; + + std::vector _startupActions; + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _startupConnection{ nullptr }; + + std::shared_ptr _windowIdToast{ nullptr }; + std::shared_ptr _actionSavedToast{ nullptr }; + std::shared_ptr _actionSaveFailedToast{ nullptr }; + std::shared_ptr _windowCwdToast{ nullptr }; + + winrt::Windows::UI::Xaml::Controls::TextBox::LayoutUpdated_revoker _renamerLayoutUpdatedRevoker; + int _renamerLayoutCount{ 0 }; + bool _renamerPressedEnter{ false }; + + TerminalApp::WindowProperties _WindowProperties{ nullptr }; + PaneResources _paneResources; + + TerminalApp::ContentManager _manager{ nullptr }; + + std::shared_ptr _terminalSettingsCache{}; + + struct StashedDragData + { + std::vector draggedTabs{}; + winrt::TerminalApp::Tab dragAnchor{ nullptr }; + winrt::Windows::Foundation::Point dragOffset{ 0, 0 }; + } _stashed; + + safe_void_coroutine _NewTerminalByDrop(const Windows::Foundation::IInspectable&, winrt::Windows::UI::Xaml::DragEventArgs e); + + __declspec(noinline) CommandPalette _loadCommandPaletteSlowPath(); + bool _commandPaletteIs(winrt::Windows::UI::Xaml::Visibility visibility); + __declspec(noinline) SuggestionsControl _loadSuggestionsElementSlowPath(); + bool _suggestionsControlIs(winrt::Windows::UI::Xaml::Visibility visibility); + + winrt::Windows::Foundation::IAsyncOperation _ShowDialogHelper(const std::wstring_view& name); + + void _ShowAboutDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowQuitDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowCloseWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowCloseReadOnlyDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowMultiLinePasteWarningDialog(); + winrt::Windows::Foundation::IAsyncOperation _ShowLargePasteWarningDialog(); + + void _CreateNewTabFlyout(); + std::vector _CreateNewTabFlyoutItems(winrt::Windows::Foundation::Collections::IVector entries); + winrt::Windows::UI::Xaml::Controls::IconElement _CreateNewTabFlyoutIcon(const winrt::hstring& icon); + winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutProfile(const Microsoft::Terminal::Settings::Model::Profile profile, int profileIndex, const winrt::hstring& iconPathOverride); + winrt::Windows::UI::Xaml::Controls::MenuFlyoutItem _CreateNewTabFlyoutAction(const winrt::hstring& actionId, const winrt::hstring& iconPathOverride); + + void _OpenNewTabDropdown(); + HRESULT _OpenNewTab(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs); + TerminalApp::Tab _CreateNewTabFromPane(std::shared_ptr pane, uint32_t insertPosition = -1); + + std::wstring _evaluatePathForCwd(std::wstring_view path); + + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _CreateConnectionFromSettings(Microsoft::Terminal::Settings::Model::Profile profile, Microsoft::Terminal::Control::IControlSettings settings, const bool inheritCursor); + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection _duplicateConnectionForRestart(const TerminalApp::TerminalPaneContent& paneContent); + void _restartPaneConnection(const TerminalApp::TerminalPaneContent&, const winrt::Windows::Foundation::IInspectable&); + + safe_void_coroutine _OpenNewWindow(const Microsoft::Terminal::Settings::Model::INewContentArgs newContentArgs); + + void _OpenNewTerminalViaDropdown(const Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + + bool _displayingCloseDialog{ false }; + void _SettingsButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _CommandPaletteButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + void _AboutButtonOnClick(const IInspectable& sender, const Windows::UI::Xaml::RoutedEventArgs& eventArgs); + + void _KeyDownHandler(const Windows::Foundation::IInspectable& sender, const Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + static ::Microsoft::Terminal::Core::ControlKeyStates _GetPressedModifierKeys() noexcept; + static void _ClearKeyboardState(const WORD vkey, const WORD scanCode) noexcept; + void _HookupKeyBindings(const Microsoft::Terminal::Settings::Model::IActionMapView& actionMap) noexcept; + void _RegisterActionCallbacks(); + + void _UpdateTitle(const Tab& tab); + void _UpdateTabIcon(Tab& tab); + void _UpdateTabView(); + void _UpdateTabWidthMode(); + void _SetBackgroundImage(const winrt::Microsoft::Terminal::Settings::Model::IAppearanceConfig& newAppearance); + + void _DuplicateFocusedTab(); + void _DuplicateTab(const Tab& tab); + + safe_void_coroutine _ExportTab(const Tab& tab, winrt::hstring filepath); + + winrt::Windows::Foundation::IAsyncAction _HandleCloseTabRequested(winrt::TerminalApp::Tab tab); + void _CloseTabAtIndex(uint32_t index); + void _RemoveTab(const winrt::TerminalApp::Tab& tab); + safe_void_coroutine _RemoveTabs(const std::vector tabs); + + void _InitializeTab(winrt::com_ptr newTabImpl, uint32_t insertPosition = -1); + void _RegisterTerminalEvents(Microsoft::Terminal::Control::TermControl term); + void _RegisterTabEvents(Tab& hostingTab); + + void _DismissTabContextMenus(); + void _FocusCurrentTab(const bool focusAlways); + bool _HasMultipleTabs() const; + + void _SelectNextTab(const bool bMoveRight, const Windows::Foundation::IReference& customTabSwitcherMode); + bool _SelectTab(uint32_t tabIndex); + bool _MoveFocus(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _SwapPane(const Microsoft::Terminal::Settings::Model::FocusDirection& direction); + bool _MovePane(const Microsoft::Terminal::Settings::Model::MovePaneArgs args); + bool _MoveTab(winrt::com_ptr tab, const Microsoft::Terminal::Settings::Model::MoveTabArgs args); + + std::shared_ptr> _adjustProcessPriorityThrottled; + void _adjustProcessPriority() const; + + template + bool _ApplyToActiveControls(F f) const + { + if (const auto tab{ _GetFocusedTabImpl() }) + { + if (const auto activePane = tab->GetActivePane()) + { + activePane->WalkTree([&](auto p) { + if (const auto& control{ p->GetTerminalControl() }) + { + f(control); + } + }); + + return true; + } + } + return false; + } + + winrt::Microsoft::Terminal::Control::TermControl _GetActiveControl() const; + std::optional _GetFocusedTabIndex() const noexcept; + std::optional _GetTabIndex(const TerminalApp::Tab& tab) const noexcept; + TerminalApp::Tab _GetFocusedTab() const noexcept; + winrt::com_ptr _GetFocusedTabImpl() const noexcept; + TerminalApp::Tab _GetTabByTabViewItem(const IInspectable& tabViewItem) const noexcept; + + void _HandleClosePaneRequested(std::shared_ptr pane); + safe_void_coroutine _SetFocusedTab(const winrt::TerminalApp::Tab tab); + safe_void_coroutine _CloseFocusedPane(); + void _ClosePanes(weak_ref weakTab, std::vector paneIds); + winrt::Windows::Foundation::IAsyncOperation _PaneConfirmCloseReadOnly(std::shared_ptr pane); + void _AddPreviouslyClosedPaneOrTab(std::vector&& args); + + void _Scroll(ScrollDirection scrollDirection, const Windows::Foundation::IReference& rowsToScroll); + + void _SplitPane(const winrt::com_ptr& tab, + const Microsoft::Terminal::Settings::Model::SplitDirection splitType, + const float splitSize, + std::shared_ptr newPane); + void _ResizePane(const Microsoft::Terminal::Settings::Model::ResizeDirection& direction); + void _ToggleSplitOrientation(); + + void _ScrollPage(ScrollDirection scrollDirection); + void _ScrollToBufferEdge(ScrollDirection scrollDirection); + void _SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Control::KeyChord& keyChord); + + safe_void_coroutine _PasteFromClipboardHandler(const IInspectable sender, + const Microsoft::Terminal::Control::PasteFromClipboardEventArgs eventArgs); + + void _OpenHyperlinkHandler(const IInspectable sender, const Microsoft::Terminal::Control::OpenHyperlinkEventArgs eventArgs); + bool _IsUriSupported(const winrt::Windows::Foundation::Uri& parsedUri); + + void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); + bool _CopyText(bool dismissSelection, bool singleLine, bool withControlSequences, Microsoft::Terminal::Control::CopyFormat formats); + + safe_void_coroutine _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); + + void _copyToClipboard(IInspectable, Microsoft::Terminal::Control::WriteToClipboardEventArgs args) const; + void _PasteText(); + + safe_void_coroutine _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); + void _ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message); + + safe_void_coroutine _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); + + void _TabDragStarted(const IInspectable& sender, const IInspectable& eventArgs); + void _TabDragCompleted(const IInspectable& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragCompletedEventArgs& eventArgs); + + // BODGY: WinUI's TabView has a broken close event handler: + // If the close button is disabled, middle-clicking the tab raises no close + // event. Because that's dumb, we implement our own middle-click handling. + // `_tabItemMiddleClickHookEnabled` is true whenever the close button is hidden, + // and that enables all of the rest of this machinery (and this workaround). + bool _tabItemMiddleClickHookEnabled = false; + bool _tabItemMiddleClickExited = false; + PointerEntered_revoker _tabItemMiddleClickPointerEntered; + PointerExited_revoker _tabItemMiddleClickPointerExited; + PointerCaptureLost_revoker _tabItemMiddleClickPointerCaptureLost; + void _OnTabPointerPressed(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs); + safe_void_coroutine _OnTabPointerReleasedCloseTab(IInspectable sender); + + void _OnTabSelectionChanged(const IInspectable& sender, const Windows::UI::Xaml::Controls::SelectionChangedEventArgs& eventArgs); + void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs); + void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs); + void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs); + void _UpdatedSelectedTab(const winrt::TerminalApp::Tab& tab); + void _UpdateBackground(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); + + void _OnDispatchCommandRequested(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& command); + void _OnCommandLineExecutionRequested(const IInspectable& sender, const winrt::hstring& commandLine); + void _OnSwitchToTabRequested(const IInspectable& sender, const winrt::TerminalApp::Tab& tab); + + void _Find(const Tab& tab); + + winrt::Microsoft::Terminal::Control::TermControl _CreateNewControlAndContent(const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& settings, + const winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection& connection); + winrt::Microsoft::Terminal::Control::TermControl _SetupControl(const winrt::Microsoft::Terminal::Control::TermControl& term); + winrt::Microsoft::Terminal::Control::TermControl _AttachControlToContent(const uint64_t& contentGuid); + + TerminalApp::IPaneContent _makeSettingsContent(); + std::shared_ptr _MakeTerminalPane(const Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs = nullptr, + const winrt::TerminalApp::Tab& sourceTab = nullptr, + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + std::shared_ptr _MakePane(const Microsoft::Terminal::Settings::Model::INewContentArgs& newContentArgs = nullptr, + const winrt::TerminalApp::Tab& sourceTab = nullptr, + winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection existingConnection = nullptr); + + void _RefreshUIForSettingsReload(); + + void _SetNewTabButtonColor(til::color color, til::color accentColor); + void _ClearNewTabButtonColor(); + + safe_void_coroutine _CompleteInitialization(); + + void _FocusActiveControl(IInspectable sender, IInspectable eventArgs); + + void _UnZoomIfNeeded(); + + static int _ComputeScrollDelta(ScrollDirection scrollDirection, const uint32_t rowsToScroll); + static uint32_t _ReadSystemRowsToScroll(); + + void _UpdateMRUTab(const winrt::TerminalApp::Tab& tab); + bool _TabSupportsMultiSelection(const winrt::TerminalApp::Tab& tab) const noexcept; + bool _IsTabSelected(const winrt::TerminalApp::Tab& tab) const noexcept; + void _SetSelectedTabs(std::vector tabs, const winrt::TerminalApp::Tab& anchor = nullptr); + void _RemoveSelectedTab(const winrt::TerminalApp::Tab& tab); + std::vector _GetSelectedTabsInDisplayOrder() const; + std::vector _GetTabRange(const winrt::TerminalApp::Tab& start, const winrt::TerminalApp::Tab& end) const; + void _ApplyMultiSelectionVisuals(); + void _UpdateSelectionFromPointer(const winrt::TerminalApp::Tab& tab); + void _MoveTabsToIndex(const std::vector& tabs, uint32_t suggestedNewTabIndex); + std::vector _CollectNewTabs(const std::vector& existingTabs) const; + std::vector _BuildStartupActionsForTabs(const std::vector& tabs) const; + + void _TryMoveTab(const uint32_t currentTabIndex, const int32_t suggestedNewTabIndex); + + void _PreviewAction(const Microsoft::Terminal::Settings::Model::ActionAndArgs& args); + void _PreviewActionHandler(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::Command& args); + void _EndPreview(); + void _RunRestorePreviews(); + void _PreviewColorScheme(const Microsoft::Terminal::Settings::Model::SetColorSchemeArgs& args); + void _PreviewAdjustOpacity(const Microsoft::Terminal::Settings::Model::AdjustOpacityArgs& args); + void _PreviewSendInput(const Microsoft::Terminal::Settings::Model::SendInputArgs& args); + + winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs _lastPreviewedAction{ nullptr }; + std::vector> _restorePreviewFuncs{}; + + HRESULT _OnNewConnection(const winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection& connection); + void _HandleToggleInboundPty(const IInspectable& sender, const Microsoft::Terminal::Settings::Model::ActionEventArgs& args); + + void _WindowRenamerActionClick(const IInspectable& sender, const IInspectable& eventArgs); + void _RequestWindowRename(const winrt::hstring& newName); + void _WindowRenamerKeyDown(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + void _WindowRenamerKeyUp(const IInspectable& sender, const winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs& e); + + void _UpdateTeachingTipTheme(winrt::Windows::UI::Xaml::FrameworkElement element); + + winrt::Microsoft::Terminal::Settings::Model::Profile GetClosestProfileForDuplicationOfProfile(const winrt::Microsoft::Terminal::Settings::Model::Profile& profile) const noexcept; + + bool _maybeElevate(const winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs& newTerminalArgs, + const winrt::Microsoft::Terminal::Settings::TerminalSettingsCreateResult& controlSettings, + const winrt::Microsoft::Terminal::Settings::Model::Profile& profile); + void _OpenElevatedWT(winrt::Microsoft::Terminal::Settings::Model::NewTerminalArgs newTerminalArgs); + + safe_void_coroutine _ConnectionStateChangedHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args); + void _CloseOnExitInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; + void _KeyboardServiceWarningInfoDismissHandler(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::Foundation::IInspectable& args) const; + static bool _IsMessageDismissed(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); + static void _DismissMessage(const winrt::Microsoft::Terminal::Settings::Model::InfoBarMessage& message); + + void _updateThemeColors(); + void _updateAllTabCloseButtons(); + void _updatePaneResources(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + + safe_void_coroutine _ControlCompletionsChangedHandler(const winrt::Windows::Foundation::IInspectable sender, const winrt::Microsoft::Terminal::Control::CompletionsChangedEventArgs args); + + void _OpenSuggestions(const Microsoft::Terminal::Control::TermControl& sender, Windows::Foundation::Collections::IVector commandsCollection, winrt::TerminalApp::SuggestionsMode mode, winrt::hstring filterText); + + void _ShowWindowChangedHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::ShowWindowArgs args); + Windows::Foundation::IAsyncAction _SearchMissingCommandHandler(const IInspectable sender, const winrt::Microsoft::Terminal::Control::SearchMissingCommandEventArgs args); + static Windows::Foundation::IAsyncOperation> _FindPackageAsync(hstring query); + + void _WindowSizeChanged(const IInspectable sender, const winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs args); + void _windowPropertyChanged(const IInspectable& sender, const winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs& args); + + void _onTabDragStarting(const winrt::Microsoft::UI::Xaml::Controls::TabView& sender, const winrt::Microsoft::UI::Xaml::Controls::TabViewTabDragStartingEventArgs& e); + void _onTabStripDragOver(const winrt::Windows::Foundation::IInspectable& sender, const winrt::Windows::UI::Xaml::DragEventArgs& e); + void _onTabStripDrop(winrt::Windows::Foundation::IInspectable sender, winrt::Windows::UI::Xaml::DragEventArgs e); + void _onTabDroppedOutside(winrt::Windows::Foundation::IInspectable sender, winrt::Microsoft::UI::Xaml::Controls::TabViewTabDroppedOutsideEventArgs e); + + void _DetachPaneFromWindow(std::shared_ptr pane); + void _DetachTabFromWindow(const winrt::com_ptr& tabImpl); + void _MoveContent(std::vector&& actions, + const winrt::hstring& windowName, + const uint32_t tabIndex, + const std::optional& dragPoint = std::nullopt); + void _sendDraggedTabsToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint); + + void _PopulateContextMenu(const Microsoft::Terminal::Control::TermControl& control, const Microsoft::UI::Xaml::Controls::CommandBarFlyout& sender, const bool withSelection); + void _PopulateQuickFixMenu(const Microsoft::Terminal::Control::TermControl& control, const Windows::UI::Xaml::Controls::MenuFlyout& sender); + winrt::Windows::UI::Xaml::Controls::MenuFlyout _CreateRunAsAdminFlyout(int profileIndex); + + winrt::Microsoft::Terminal::Control::TermControl _senderOrActiveControl(const winrt::Windows::Foundation::IInspectable& sender); + winrt::com_ptr _senderOrFocusedTab(const IInspectable& sender); + + void _activePaneChanged(winrt::TerminalApp::Tab tab, Windows::Foundation::IInspectable args); + safe_void_coroutine _doHandleSuggestions(Microsoft::Terminal::Settings::Model::SuggestionsArgs realArgs); + +#pragma region ActionHandlers + // These are all defined in AppActionHandlers.cpp +#define ON_ALL_ACTIONS(action) DECLARE_ACTION_HANDLER(action); + ALL_SHORTCUT_ACTIONS + INTERNAL_SHORTCUT_ACTIONS +#undef ON_ALL_ACTIONS +#pragma endregion + + friend class TerminalAppLocalTests::TabTests; + friend class TerminalAppLocalTests::SettingsTests; + }; +} + +namespace winrt::TerminalApp::factory_implementation +{ + BASIC_FACTORY(TerminalPage); +} diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h index 02f6214ca62..749f47dcf39 100644 --- a/src/cascadia/WindowsTerminal/IslandWindow.h +++ b/src/cascadia/WindowsTerminal/IslandWindow.h @@ -1,168 +1,168 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#pragma once -#include "BaseWindow.h" - -struct SystemMenuItemInfo -{ - winrt::hstring label; - winrt::delegate callback; -}; - -class IslandWindow : - public BaseWindow -{ -public: - static bool IsCursorHidden() noexcept; - static void HideCursor() noexcept; - static void ShowCursorMaybe(const UINT message) noexcept; - - IslandWindow() noexcept; - ~IslandWindow(); - - virtual void MakeWindow() noexcept; - virtual void Close(); - - virtual void OnSize(const UINT width, const UINT height); - HWND GetInteropHandle() const; - - [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; - - [[nodiscard]] LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept; - - void OnResize(const UINT width, const UINT height); - void OnMinimize(); - void OnRestore(); - virtual void OnAppInitialized(); - virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); - virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); - virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; - virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; - - virtual void Initialize(); - - void SetCreateCallback(std::function pfn) noexcept; - - void SetSnapDimensionCallback(std::function pfn) noexcept; - void SetUiaSelectTabRangeCallback(std::function pfn) noexcept; - - void FocusModeChanged(const bool focusMode); - void FullscreenChanged(const bool fullscreen); - void SetAlwaysOnTop(const bool alwaysOnTop); - void ShowWindowChanged(const bool showOrHide); - virtual void SetShowTabsFullscreen(const bool newShowTabsFullscreen); - - void FlashTaskbar(); - void SetTaskbarProgress(const size_t state, const size_t progress); - - void SummonWindow(winrt::TerminalApp::SummonWindowBehavior args); - - bool IsQuakeWindow() const noexcept; - void IsQuakeWindow(bool isQuakeWindow) noexcept; - void SetAutoHideWindow(bool autoHideWindow) noexcept; - - void HideWindow(); - - void SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept; - - void OpenSystemMenu(const std::optional mouseX, const std::optional mouseY) const noexcept; - void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); - void RemoveFromSystemMenu(const winrt::hstring& itemLabel); - - void UseDarkTheme(const bool v); - virtual void UseMica(const bool newValue, const double titlebarOpacity); - - til::event> DragRegionClicked; - til::event> WindowCloseButtonClicked; - til::event> MouseScrolled; - til::event> WindowActivated; - til::event> NotifyNotificationIconPressed; - til::event> NotifyWindowHidden; - til::event> NotifyNotificationIconMenuItemSelected; - til::event> NotifyReAddNotificationIcon; - til::event> ShouldExitFullscreen; - til::event> MaximizeChanged; - - til::event> WindowMoved; - til::event> WindowVisibilityChanged; - -protected: - void ForceResize() - { - // Do a quick resize to force the island to paint - const auto size = GetPhysicalSize(); - OnSize(size.width, size.height); - } - - HWND _interopWindowHandle; - - winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor - winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor - wil::com_ptr _taskbar; - - std::function _pfnCreateCallback; - std::function _pfnSnapDimensionCallback; - std::function _pfnUiaSelectTabRangeCallback; - - void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; - [[nodiscard]] LRESULT _OnSizing(const WPARAM wParam, const LPARAM lParam); - [[nodiscard]] LRESULT _OnMoving(const WPARAM wParam, const LPARAM lParam); - - bool _borderless{ false }; - bool _alwaysOnTop{ false }; - bool _fullscreen{ false }; - bool _showTabsFullscreen{ false }; - bool _fWasMaximizedBeforeFullscreen{ false }; - RECT _rcWindowBeforeFullscreen{}; - RECT _rcWorkBeforeFullscreen{}; - UINT _dpiBeforeFullscreen{ 96 }; - - virtual void _SetIsBorderless(const bool borderlessEnabled); - virtual void _SetIsFullscreen(const bool fullscreenEnabled); - - void _RestoreFullscreenPosition(const RECT& rcWork); - void _SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork); - - LONG _getDesiredWindowStyle() const; - - void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); - - void _globalActivateWindow(const uint32_t dropdownDuration, - const winrt::TerminalApp::MonitorBehavior toMonitor); - void _dropdownWindow(const uint32_t dropdownDuration, - const winrt::TerminalApp::MonitorBehavior toMonitor); - void _slideUpWindow(const uint32_t dropdownDuration); - void _doSlideAnimation(const uint32_t dropdownDuration, const bool down); - void _globalDismissWindow(const uint32_t dropdownDuration); - - static MONITORINFO _getMonitorForCursor(); - static MONITORINFO _getMonitorForWindow(HWND foregroundWindow); - void _moveToMonitor(HWND foregroundWindow, const winrt::TerminalApp::MonitorBehavior toMonitor); - void _moveToMonitorOfMouse(); - void _moveToMonitorOf(HWND foregroundWindow); - void _moveToMonitor(const MONITORINFO activeMonitor); - - bool _isQuakeWindow{ false }; - bool _autoHideWindow{ false }; - - void _enterQuakeMode(); - til::rect _getQuakeModeSize(HMONITOR hmon); - - bool _minimizeToNotificationArea{ false }; - - std::unordered_map _systemMenuItems; - UINT _systemMenuNextItemId = 0; - void _resetSystemMenu(); - -private: - // This minimum width allows for width the tabs fit - static constexpr float minimumWidth = 460; - - // We run with no height requirement for client area, - // though the total height will take into account the non-client area - // and the requirements of components hosted in the client area - static constexpr float minimumHeight = 0; - - inline static bool _cursorHidden; -}; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once +#include "BaseWindow.h" + +struct SystemMenuItemInfo +{ + winrt::hstring label; + winrt::delegate callback; +}; + +class IslandWindow : + public BaseWindow +{ +public: + static bool IsCursorHidden() noexcept; + static void HideCursor() noexcept; + static void ShowCursorMaybe(const UINT message) noexcept; + + IslandWindow() noexcept; + ~IslandWindow(); + + virtual void MakeWindow() noexcept; + virtual void Close(); + + virtual void OnSize(const UINT width, const UINT height); + HWND GetInteropHandle() const; + + [[nodiscard]] virtual LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept; + + [[nodiscard]] LRESULT OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept; + + void OnResize(const UINT width, const UINT height); + void OnMinimize(); + void OnRestore(); + virtual void OnAppInitialized(); + virtual void SetContent(winrt::Windows::UI::Xaml::UIElement content); + virtual void OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme); + virtual til::rect GetNonClientFrame(const UINT dpi) const noexcept; + virtual til::size GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept; + + virtual void Initialize(); + + void SetCreateCallback(std::function pfn) noexcept; + + void SetSnapDimensionCallback(std::function pfn) noexcept; + void SetUiaSelectTabRangeCallback(std::function pfn) noexcept; + + void FocusModeChanged(const bool focusMode); + void FullscreenChanged(const bool fullscreen); + void SetAlwaysOnTop(const bool alwaysOnTop); + void ShowWindowChanged(const bool showOrHide); + virtual void SetShowTabsFullscreen(const bool newShowTabsFullscreen); + + void FlashTaskbar(); + void SetTaskbarProgress(const size_t state, const size_t progress); + + void SummonWindow(winrt::TerminalApp::SummonWindowBehavior args); + + bool IsQuakeWindow() const noexcept; + void IsQuakeWindow(bool isQuakeWindow) noexcept; + void SetAutoHideWindow(bool autoHideWindow) noexcept; + + void HideWindow(); + + void SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept; + + void OpenSystemMenu(const std::optional mouseX, const std::optional mouseY) const noexcept; + void AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate callback); + void RemoveFromSystemMenu(const winrt::hstring& itemLabel); + + void UseDarkTheme(const bool v); + virtual void UseMica(const bool newValue, const double titlebarOpacity); + + til::event> DragRegionClicked; + til::event> WindowCloseButtonClicked; + til::event> MouseScrolled; + til::event> WindowActivated; + til::event> NotifyNotificationIconPressed; + til::event> NotifyWindowHidden; + til::event> NotifyNotificationIconMenuItemSelected; + til::event> NotifyReAddNotificationIcon; + til::event> ShouldExitFullscreen; + til::event> MaximizeChanged; + + til::event> WindowMoved; + til::event> WindowVisibilityChanged; + +protected: + void ForceResize() + { + // Do a quick resize to force the island to paint + const auto size = GetPhysicalSize(); + OnSize(size.width, size.height); + } + + HWND _interopWindowHandle; + + winrt::Windows::UI::Xaml::Hosting::DesktopWindowXamlSource _source; // nulled in ctor + winrt::Windows::UI::Xaml::Controls::Grid _rootGrid; // nulled in ctor + wil::com_ptr _taskbar; + + std::function _pfnCreateCallback; + std::function _pfnSnapDimensionCallback; + std::function _pfnUiaSelectTabRangeCallback; + + void _HandleCreateWindow(const WPARAM wParam, const LPARAM lParam) noexcept; + [[nodiscard]] LRESULT _OnSizing(const WPARAM wParam, const LPARAM lParam); + [[nodiscard]] LRESULT _OnMoving(const WPARAM wParam, const LPARAM lParam); + + bool _borderless{ false }; + bool _alwaysOnTop{ false }; + bool _fullscreen{ false }; + bool _showTabsFullscreen{ false }; + bool _fWasMaximizedBeforeFullscreen{ false }; + RECT _rcWindowBeforeFullscreen{}; + RECT _rcWorkBeforeFullscreen{}; + UINT _dpiBeforeFullscreen{ 96 }; + + virtual void _SetIsBorderless(const bool borderlessEnabled); + virtual void _SetIsFullscreen(const bool fullscreenEnabled); + + void _RestoreFullscreenPosition(const RECT& rcWork); + void _SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork); + + LONG _getDesiredWindowStyle() const; + + void _OnGetMinMaxInfo(const WPARAM wParam, const LPARAM lParam); + + void _globalActivateWindow(const uint32_t dropdownDuration, + const winrt::TerminalApp::MonitorBehavior toMonitor); + void _dropdownWindow(const uint32_t dropdownDuration, + const winrt::TerminalApp::MonitorBehavior toMonitor); + void _slideUpWindow(const uint32_t dropdownDuration); + void _doSlideAnimation(const uint32_t dropdownDuration, const bool down); + void _globalDismissWindow(const uint32_t dropdownDuration); + + static MONITORINFO _getMonitorForCursor(); + static MONITORINFO _getMonitorForWindow(HWND foregroundWindow); + void _moveToMonitor(HWND foregroundWindow, const winrt::TerminalApp::MonitorBehavior toMonitor); + void _moveToMonitorOfMouse(); + void _moveToMonitorOf(HWND foregroundWindow); + void _moveToMonitor(const MONITORINFO activeMonitor); + + bool _isQuakeWindow{ false }; + bool _autoHideWindow{ false }; + + void _enterQuakeMode(); + til::rect _getQuakeModeSize(HMONITOR hmon); + + bool _minimizeToNotificationArea{ false }; + + std::unordered_map _systemMenuItems; + UINT _systemMenuNextItemId = 0; + void _resetSystemMenu(); + +private: + // This minimum width allows for width the tabs fit + static constexpr float minimumWidth = 460; + + // We run with no height requirement for client area, + // though the total height will take into account the non-client area + // and the requirements of components hosted in the client area + static constexpr float minimumHeight = 0; + + inline static bool _cursorHidden; +}; diff --git a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs index b97e8826e73..b7bb911aaab 100644 --- a/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs +++ b/src/cascadia/WindowsTerminal_UIATests/Elements/TerminalApp.cs @@ -6,107 +6,35 @@ //---------------------------------------------------------------------------------------------------------------------- namespace WindowsTerminal.UIA.Tests.Elements { - using System.Collections.Generic; using System; - using System.Diagnostics; - using System.Globalization; + using System.Collections.Generic; using System.IO; using System.Linq; + using System.Runtime.InteropServices; using System.Threading; using WindowsTerminal.UIA.Tests.Common; using WindowsTerminal.UIA.Tests.Common.NativeMethods; - using OpenQA.Selenium.Remote; + using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.iOS; using OpenQA.Selenium.Interactions; + using OpenQA.Selenium.Remote; using WEX.Logging.Interop; using WEX.TestExecution; using WEX.TestExecution.Markup; - using System.Runtime.InteropServices; - using System.Security.Principal; - using OpenQA.Selenium; - using System.Windows.Automation; - public class TerminalApp : IDisposable { protected const string AppDriverUrl = "http://127.0.0.1:4723"; - private const uint MouseEventLeftDown = 0x0002; - private const uint MouseEventLeftUp = 0x0004; - private const uint KeyEventKeyUp = 0x0002; - private const byte VirtualKeyControl = 0x11; - private const byte VirtualKeyShift = 0x10; - private const int InputMouse = 0; - private const int InputKeyboard = 1; private IntPtr job; - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool SetCursorPos(int x, int y); - - [DllImport("user32.dll", SetLastError = true)] - private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo); - - [DllImport("user32.dll", SetLastError = true)] - private static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, UIntPtr extraInfo); - - [DllImport("user32.dll", SetLastError = true)] - private static extern uint SendInput(uint numberOfInputs, INPUT[] inputs, int sizeOfInputStructure); - - [StructLayout(LayoutKind.Sequential)] - private struct INPUT - { - public int type; - public INPUTUNION U; - } - - [StructLayout(LayoutKind.Explicit)] - private struct INPUTUNION - { - [FieldOffset(0)] - public MOUSEINPUT mi; - - [FieldOffset(0)] - public KEYBDINPUT ki; - } - - [StructLayout(LayoutKind.Sequential)] - private struct MOUSEINPUT - { - public int dx; - public int dy; - public uint mouseData; - public uint dwFlags; - public uint time; - public IntPtr dwExtraInfo; - } - - [StructLayout(LayoutKind.Sequential)] - private struct KEYBDINPUT - { - public ushort wVk; - public ushort wScan; - public uint dwFlags; - public uint time; - public IntPtr dwExtraInfo; - } - - public IOSDriver Session { get; private set; } - public Actions Actions { get; private set; } - public AppiumWebElement UIRoot { get; private set; } - private bool isDisposed = false; - - private TestContext context; - private string _windowTitleToFind; - private string _dragEventLogPath; - private readonly HashSet _trackedProcessIds = new HashSet(); + private readonly TestContext context; + private readonly string _windowTitleToFind; private readonly Dictionary> _windowSessions = new Dictionary>(); - private readonly List _scheduledTaskNames = new List(); - private readonly List _temporaryLaunchArtifacts = new List(); private IOSDriver _desktopSession; public sealed class TopLevelWindow @@ -139,7 +67,14 @@ public void RefreshBounds() } } + public IOSDriver Session { get; private set; } + + public Actions Actions { get; private set; } + + public AppiumWebElement UIRoot { get; private set; } + public string ContentPath { get; private set; } + public string GetFullTestContentPath(string filename) { return Path.GetFullPath(Path.Combine(ContentPath, filename)); @@ -150,16 +85,6 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe", this.context = context; _windowTitleToFind = windowTitleToFind; - // If running locally, set WTPath to where we can find a loose - // deployment of Windows Terminal. That means you'll need to build - // the Terminal appx, then use - // New-UnpackagedTerminalDistribution.ps1 to build an unpackaged - // layout that can successfully launch. Then, point the tests at - // that WindowsTerminal.exe like so: - // - // te.exe WindowsTerminal.UIA.Tests.dll /p:WTPath=C:\the\path\to\the\unpackaged\layout\WindowsTerminal.exe - // - // On the build machines, the scripts lay it out at the terminal-0.0.1.0\ subfolder of the test deployment directory string path = Path.GetFullPath(Path.Combine(context.TestDeploymentDir, @"terminal-0.0.1.0\WindowsTerminal.exe")); if (context.Properties.Contains("WTPath")) { @@ -167,9 +92,6 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe", } Log.Comment($"Windows Terminal will be launched from '{path}'"); - // Same goes for the content directory. Set WTTestContent for where the content files are - // for running tests. - // On the build machines, the scripts lay it out at the content\ subfolder. ContentPath = @"content"; if (context.Properties.Contains("WTTestContent")) { @@ -177,23 +99,23 @@ public TerminalApp(TestContext context, string shellToLaunch = "powershell.exe", } Log.Comment($"Test Content will be loaded from '{Path.GetFullPath(ContentPath)}'"); - this.CreateProcess(path, shellToLaunch, launchArgs); + CreateProcess(path, shellToLaunch, launchArgs); } ~TerminalApp() { - this.Dispose(false); + Dispose(false); } public void Dispose() { - this.Dispose(true); + Dispose(true); GC.SuppressFinalize(this); } public AppiumWebElement GetRoot() { - return this.UIRoot; + return UIRoot; } public TopLevelWindow FindTopLevelWindowByName(string name, TopLevelWindow excludedWindow = null) @@ -242,7 +164,6 @@ public TopLevelWindow WaitForTopLevelWindowByName(string name, TopLevelWindow ex remaining -= 250; } - _LogVisibleWindows(name); throw new InvalidOperationException($"Timed out waiting for top-level window '{name}'."); } @@ -270,10 +191,12 @@ public void ActivateWindow(TopLevelWindow window) { Log.Comment($"SetForegroundWindow returned false for top-level window '{window.Title}' (0x{window.Handle.ToInt64():x}); continuing with existing attachment."); } + Thread.Sleep(250); window.RefreshBounds(); _AttachToWindow(window); Session = window.Session; + Actions = new Actions(Session); UIRoot = window.Root; } @@ -305,120 +228,82 @@ public void ArrangeWindowOnPrimaryMonitor(TopLevelWindow window, int slotIndex, window.RefreshBounds(); } - public AppiumWebElement FindElementByName(TopLevelWindow window, string name) + public AppiumWebElement FindTabElementByName(TopLevelWindow window, string name) { - window.RefreshBounds(); - - _AttachToWindow(window); - - var matches = window.Session.FindElementsByName(name) - .OfType() - .Where(_IsValidElement); - if (ReferenceEquals(window.Session, _desktopSession)) - { - matches = matches.Where(element => _IsElementWithinWindow(element, window)); - } - - return matches.OrderByDescending(element => element.Size.Width * element.Size.Height) - .ThenBy(element => element.Location.X) - .ThenBy(element => element.Location.Y) - .First(); + return _FindElementsByName(window, name) + .Where(element => element.Size.Height <= 80 && + element.Size.Width < window.Width && + element.Location.Y < window.Bounds.top + 96) + .OrderBy(element => element.Location.X) + .ThenBy(element => element.Location.Y) + .First(); } - public AppiumWebElement FindTabElementByName(TopLevelWindow window, string name) + public bool HasElementByName(TopLevelWindow window, string name) { - window.RefreshBounds(); - - _AttachToWindow(window); - - var matches = window.Session.FindElementsByName(name) - .OfType() - .Where(_IsValidElement); - if (ReferenceEquals(window.Session, _desktopSession)) - { - matches = matches.Where(element => _IsElementWithinWindow(element, window)); - } + return _FindElementsByName(window, name).Any(); + } - return matches.Where(element => element.Size.Height <= 80 && - element.Size.Width < window.Width && - element.Location.Y < window.Bounds.top + 96) - .OrderBy(element => element.Location.X) - .ThenBy(element => element.Location.Y) - .First(); + public void SelectTabRangeForTesting(TopLevelWindow window, int startIndex, int endIndex) + { + var result = User32.SendMessage(window.Handle, User32.WindowMessages.CM_UIA_SELECT_TAB_RANGE, startIndex, new IntPtr(endIndex)); + Verify.IsTrue(result != IntPtr.Zero, $"Select tab range {startIndex}-{endIndex} for '{window.Title}'."); + Thread.Sleep(200); } - public bool HasElementByName(TopLevelWindow window, string name) + public void DragByOffset(AppiumWebElement source, int offsetX, int offsetY) { - window.RefreshBounds(); + _MoveMouseToElementOffset(source, 0, 0); + Thread.Sleep(100); - _AttachToWindow(window); + Session.Mouse.MouseDown(null); + Thread.Sleep(250); - var matches = window.Session.FindElementsByName(name) - .OfType() - .Where(_IsValidElement); - if (ReferenceEquals(window.Session, _desktopSession)) - { - matches = matches.Where(element => _IsElementWithinWindow(element, window)); - } + _MoveCursorSmoothly(source, 0, 0, 36, 0, 8, 25); + Thread.Sleep(200); - return matches.Any(); - } + _MoveCursorSmoothly(source, 36, 0, 84, 0, 10, 25); + Thread.Sleep(250); - public void LogWindowDetails(string label, TopLevelWindow window) - { - window.RefreshBounds(); - Log.Comment( - $"{label}: title='{window.Title}', handle=0x{window.Handle.ToInt64():x}, bounds=({window.Bounds.left},{window.Bounds.top})-({window.Bounds.right},{window.Bounds.bottom})"); + _MoveCursorSmoothly(source, 84, 0, offsetX, offsetY, 36, 20); + + Thread.Sleep(700); + Session.Mouse.MouseUp(null); + Thread.Sleep(400); } - public void LogElementDetails(string label, TopLevelWindow window, AppiumWebElement element) + public void DragToElement(AppiumWebElement source, AppiumWebElement target, int targetOffsetX = 0, int targetOffsetY = 0) { - window.RefreshBounds(); - Log.Comment( - $"{label}: " + - $"element(Name='{_TryGetAttribute(element, "Name")}', Class='{_TryGetAttribute(element, "ClassName")}', Handle='{_TryGetAttribute(element, "NativeWindowHandle")}', Loc=({element.Location.X},{element.Location.Y}), Size=({element.Size.Width},{element.Size.Height}), Id='{element.Id}'); " + - $"windowHandle=0x{window.Handle.ToInt64():x}, windowBounds=({window.Bounds.left},{window.Bounds.top})-({window.Bounds.right},{window.Bounds.bottom})"); + var (sourceCenterX, sourceCenterY) = _GetScreenCenter(source); + var (targetCenterX, targetCenterY) = _GetScreenCenter(target); + targetCenterX += targetOffsetX; + targetCenterY += targetOffsetY; + DragByOffset(source, targetCenterX - sourceCenterX, targetCenterY - sourceCenterY); } - public void LogElementAncestors(string label, AppiumWebElement element, int maxDepth = 6) + protected virtual void Dispose(bool disposing) { - var current = element; - for (var depth = 0; depth < maxDepth && current != null; depth++) + if (!isDisposed) { - Log.Comment( - $"{label}[{depth}]: " + - $"Name='{_TryGetAttribute(current, "Name")}', Class='{_TryGetAttribute(current, "ClassName")}', ControlType='{_TryGetAttribute(current, "ControlType")}', " + - $"Loc=({current.Location.X},{current.Location.Y}), Size=({current.Size.Width},{current.Size.Height}), Id='{current.Id}'"); - - AppiumWebElement parent = null; - try - { - parent = current.FindElementByXPath(".."); - } - catch - { - break; - } - - if (parent == null || parent.Id == current.Id) - { - break; - } - - current = parent; + ExitProcess(); + isDisposed = true; } } - private static string _TryGetAttribute(AppiumWebElement element, string attributeName) + private IReadOnlyList _FindElementsByName(TopLevelWindow window, string name) { - try - { - return element?.GetAttribute(attributeName); - } - catch + window.RefreshBounds(); + _AttachToWindow(window); + + var matches = window.Session.FindElementsByName(name) + .OfType() + .Where(_IsValidElement); + if (ReferenceEquals(window.Session, _desktopSession)) { - return null; + matches = matches.Where(element => _IsElementWithinWindow(element, window)); } + + return matches.ToList(); } private static bool _IsValidElement(AppiumWebElement element) @@ -463,35 +348,23 @@ private void _AttachToWindow(TopLevelWindow window) if (_windowSessions.TryGetValue(window.Handle, out var existingSession)) { - try + var existingRoot = _GetRootElement(existingSession); + if (existingRoot != null) { window.Session = existingSession; - try - { - window.Root = existingSession.FindElementByXPath("/*"); - } - catch - { - window.Root = existingSession.FindElementByXPath("//*"); - } - + window.Root = existingRoot; return; } - catch (Exception ex) + + try + { + existingSession.Quit(); + } + catch { - Log.Comment($"Reused WinAppDriver session for window 0x{window.Handle.ToInt64():x} ('{window.Title}') became invalid: {ex.Message}"); - try - { - existingSession.Quit(); - } - catch - { - } - - _windowSessions.Remove(window.Handle); - window.Session = null; - window.Root = null; } + + _windowSessions.Remove(window.Handle); } var capabilities = new DesiredCapabilities(); @@ -501,459 +374,56 @@ private void _AttachToWindow(TopLevelWindow window) { var windowSession = new IOSDriver(new Uri(AppDriverUrl), capabilities); Verify.IsNotNull(windowSession, $"Attach WinAppDriver session to top-level window '{window.Title}'."); - - AppiumWebElement rootElement; - try - { - rootElement = windowSession.FindElementByXPath("/*"); - } - catch - { - rootElement = windowSession.FindElementByXPath("//*"); - } + windowSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15); window.Session = windowSession; - window.Root = rootElement; + window.Root = _GetRootElement(windowSession); _windowSessions[window.Handle] = windowSession; } catch { - Log.Comment($"Falling back to desktop session for window 0x{window.Handle.ToInt64():x} ('{window.Title}') because appTopLevelWindow attach failed."); window.Session = _desktopSession; - if (UIRoot != null) - { - window.Root = UIRoot; - } - else - { - try - { - window.Root = _desktopSession?.FindElementByXPath("/*"); - } - catch - { - try - { - window.Root = _desktopSession?.FindElementByXPath("//*"); - } - catch - { - window.Root = null; - } - } - } + window.Root = _FindWindowRootFromDesktopSession(window) ?? _GetRootElement(_desktopSession); } } - private void _LogVisibleWindows(string targetName) + private AppiumWebElement _FindWindowRootFromDesktopSession(TopLevelWindow window) { - var visibleTitles = new List(); - User32.EnumWindows((hWnd, _) => - { - if (!User32.IsWindowVisible(hWnd)) - { - return true; - } - - var title = _GetWindowText(hWnd); - if (string.IsNullOrWhiteSpace(title)) - { - return true; - } - - if (title.IndexOf(targetName, StringComparison.OrdinalIgnoreCase) >= 0 || - title.IndexOf(_windowTitleToFind, StringComparison.OrdinalIgnoreCase) >= 0 || - title.IndexOf("DragTab", StringComparison.OrdinalIgnoreCase) >= 0 || - title.IndexOf("Terminal", StringComparison.OrdinalIgnoreCase) >= 0) - { - visibleTitles.Add($"0x{hWnd.ToInt64():x}: {title}"); - } - - return true; - }, IntPtr.Zero); - - Log.Comment($"Visible top-level windows near '{targetName}': {visibleTitles.Count}"); - foreach (var title in visibleTitles.Take(20)) + if (_desktopSession == null) { - Log.Comment(title); + return null; } + + return _desktopSession.FindElementsByName(window.Title) + .OfType() + .Where(_IsValidElement) + .Where(element => _IsElementWithinWindow(element, window)) + .OrderByDescending(element => element.Size.Width * element.Size.Height) + .FirstOrDefault(); } - private static AppiumWebElement _LiftToWindowRoot(AppiumWebElement element) + private static AppiumWebElement _GetRootElement(IOSDriver session) { - if (element == null) + if (session == null) { return null; } - var current = element; - var processId = _TryGetAttribute(current, "ProcessId"); - for (var i = 0; i < 20; ++i) + try + { + return session.FindElementByXPath("/*"); + } + catch { - AppiumWebElement parent = null; try { - parent = current.FindElementByXPath(".."); + return session.FindElementByXPath("//*"); } catch { - break; - } - - if (parent == null || parent.Id == current.Id) - { - break; - } - - if (!string.IsNullOrEmpty(processId)) - { - var parentPid = _TryGetAttribute(parent, "ProcessId"); - if (!string.Equals(parentPid, processId, StringComparison.Ordinal)) - { - break; - } + return null; } - - current = parent; - } - - return current; - } - - public void CtrlClick(AppiumWebElement element) - { - _ModifiedClick(element, VirtualKeyControl); - } - - public void CtrlClickTab(TopLevelWindow window, string name) - { - _ModifiedNativeTabClick(window, name, VirtualKeyControl); - } - - public void ShiftClick(AppiumWebElement element) - { - _ModifiedClick(element, VirtualKeyShift); - } - - public void ShiftClickTab(TopLevelWindow window, string name) - { - _ModifiedNativeTabClick(window, name, VirtualKeyShift); - } - - public void ClickTab(TopLevelWindow window, string name) - { - _NativeTabClick(window, name); - } - - public void SelectTabRangeForTesting(TopLevelWindow window, int startIndex, int endIndex) - { - var result = User32.SendMessage(window.Handle, User32.WindowMessages.CM_UIA_SELECT_TAB_RANGE, startIndex, new IntPtr(endIndex)); - Log.Comment($"SelectTabRangeForTesting: window=0x{window.Handle.ToInt64():x}, start={startIndex}, end={endIndex}, result={result}"); - Verify.IsTrue(result != IntPtr.Zero, $"Select tab range {startIndex}-{endIndex} for '{window.Title}'."); - Thread.Sleep(200); - } - - public void LogNativeTabSelectionState(TopLevelWindow window, string name) - { - var element = _FindNativeTabElement(window, name); - var isSelected = false; - if (element.TryGetCurrentPattern(SelectionItemPattern.Pattern, out var selectionPattern)) - { - isSelected = ((SelectionItemPattern)selectionPattern).Current.IsSelected; - } - - var rect = element.Current.BoundingRectangle; - Log.Comment($"Native UIA tab '{name}' selection: Selected={isSelected}, Focused={element.Current.HasKeyboardFocus}, Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); - } - - public void DragByOffset(AppiumWebElement source, int offsetX, int offsetY) - { - var (sourceCenterX, sourceCenterY) = _GetScreenCenter(source); - var targetX = sourceCenterX + offsetX; - var targetY = sourceCenterY + offsetY; - - Log.Comment($"Native drag from ({sourceCenterX}, {sourceCenterY}) to ({targetX}, {targetY})"); - - _MoveMouseToElementOffset(source, 0, 0); - Thread.Sleep(100); - - Session.Mouse.MouseDown(null); - Thread.Sleep(250); - - _MoveCursorSmoothly(source, 0, 0, 36, 0, 8, 25); - Thread.Sleep(200); - - _MoveCursorSmoothly(source, 36, 0, 84, 0, 10, 25); - Thread.Sleep(250); - - _MoveCursorSmoothly(source, 84, 0, offsetX, offsetY, 36, 20); - - Thread.Sleep(700); - Session.Mouse.MouseUp(null); - Thread.Sleep(400); - } - - public void DragToElement(AppiumWebElement source, AppiumWebElement target, int targetOffsetX = 0, int targetOffsetY = 0) - { - var (sourceCenterX, sourceCenterY) = _GetScreenCenter(source); - var (targetCenterX, targetCenterY) = _GetScreenCenter(target); - targetCenterX += targetOffsetX; - targetCenterY += targetOffsetY; - DragByOffset(source, targetCenterX - sourceCenterX, targetCenterY - sourceCenterY); - } - - private void _ModifiedClick(AppiumWebElement element, byte modifierKey) - { - var targetX = element.Location.X + Math.Min(16, Math.Max(4, element.Size.Width / 8)); - var targetY = element.Location.Y + (element.Size.Height / 2); - _ClickScreenPoint(targetX, targetY, modifierKey, "modified click"); - } - - private static INPUT _CreateMouseInput(uint flags) - { - return new INPUT - { - type = InputMouse, - U = new INPUTUNION - { - mi = new MOUSEINPUT - { - dwFlags = flags, - }, - }, - }; - } - - private static AutomationElement _FindNativeTabElement(TopLevelWindow window, string name) - { - var windowElement = AutomationElement.FromHandle(window.Handle); - Verify.IsNotNull(windowElement, $"Find native UIA root for top-level window '{window.Title}'."); - - var conditions = new AndCondition( - new PropertyCondition(AutomationElement.NameProperty, name), - new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TabItem)); - var matches = windowElement.FindAll(TreeScope.Descendants, conditions); - Verify.IsTrue(matches.Count > 0, $"Find native tab item '{name}' in window '{window.Title}'."); - - return matches - .OfType() - .Where(element => !element.Current.BoundingRectangle.IsEmpty) - .OrderBy(element => element.Current.BoundingRectangle.Left) - .First(); - } - - private void _ModifiedNativeTabClick(TopLevelWindow window, string name, byte modifierKey) - { - var element = _FindNativeTabElement(window, name); - var rect = element.Current.BoundingRectangle; - var targetX = (int)rect.Left + Math.Min(16, Math.Max(4, (int)rect.Width / 8)); - var targetY = (int)(rect.Top + (rect.Height / 2)); - Log.Comment($"Native UIA tab '{name}' for modified click: Class='{element.Current.ClassName}', ControlType='{element.Current.ControlType.ProgrammaticName}', Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); - _ClickScreenPoint(targetX, targetY, modifierKey, $"native tab '{name}' modified click", window.Handle); - } - - private void _NativeTabClick(TopLevelWindow window, string name) - { - var element = _FindNativeTabElement(window, name); - var rect = element.Current.BoundingRectangle; - var targetX = (int)rect.Left + Math.Min(16, Math.Max(4, (int)rect.Width / 8)); - var targetY = (int)(rect.Top + (rect.Height / 2)); - Log.Comment($"Native UIA tab '{name}' for click: Class='{element.Current.ClassName}', ControlType='{element.Current.ControlType.ProgrammaticName}', Bounds=({rect.Left},{rect.Top})-({rect.Right},{rect.Bottom})"); - _ClickScreenPoint(targetX, targetY, null, $"native tab '{name}' click", window.Handle); - } - - private void _ClickScreenPoint(int targetX, int targetY, byte? modifierKey, string description, IntPtr targetWindow = default) - { - if (IsRunningAsAdmin()) - { - _ClickScreenPointLimited(targetX, targetY, modifierKey, description, targetWindow); - return; - } - - NativeMethods.Win32BoolHelper(SetCursorPos(targetX, targetY), $"Move cursor to {description} at ({targetX}, {targetY})."); - Thread.Sleep(100); - - var inputs = new List(); - if (modifierKey.HasValue) - { - inputs.Add(_CreateKeyboardInput(modifierKey.Value, false)); - } - - inputs.Add(_CreateMouseInput(MouseEventLeftDown)); - inputs.Add(_CreateMouseInput(MouseEventLeftUp)); - - if (modifierKey.HasValue) - { - inputs.Add(_CreateKeyboardInput(modifierKey.Value, true)); - } - - var sent = SendInput((uint)inputs.Count, inputs.ToArray(), Marshal.SizeOf()); - Verify.AreEqual((uint)inputs.Count, sent, $"Send {description} input."); - Thread.Sleep(200); - } - - private void _ClickScreenPointLimited(int targetX, int targetY, byte? modifierKey, string description, IntPtr targetWindow) - { - var taskName = $"WindowsTerminal-UIA-Input-{Process.GetCurrentProcess().Id}-{Guid.NewGuid():N}"; - var scriptPath = Path.Combine(Path.GetTempPath(), $"{taskName}.ps1"); - var launcherPath = Path.Combine(Path.GetTempPath(), $"{taskName}.cmd"); - var resultPath = Path.Combine(Path.GetTempPath(), $"{taskName}.result"); - var modifierValue = modifierKey.HasValue ? modifierKey.Value.ToString(CultureInfo.InvariantCulture) : "-1"; - var targetWindowValue = targetWindow == IntPtr.Zero ? "0" : targetWindow.ToInt64().ToString(CultureInfo.InvariantCulture); - var escapedResultPath = resultPath.Replace("'", "''"); - - var scriptContents = string.Join(Environment.NewLine, new[] - { - "$ErrorActionPreference = 'Stop'", - "try {", - "Add-Type @'", - "using System;", - "using System.Text;", - "using System.Runtime.InteropServices;", - "public static class Native {", - " public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);", - " [StructLayout(LayoutKind.Sequential)] public struct POINT { public int x; public int y; }", - " [StructLayout(LayoutKind.Sequential)] public struct RECT { public int left; public int top; public int right; public int bottom; }", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetCursorPos(int x, int y);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetForegroundWindow(IntPtr hWnd);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern IntPtr ChildWindowFromPointEx(IntPtr hWndParent, POINT pt, uint flags);", - " [DllImport(\"user32.dll\", CharSet=CharSet.Unicode, SetLastError=true)] public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, UIntPtr dwExtraInfo);", - " [DllImport(\"user32.dll\", SetLastError=true)] public static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint dwData, UIntPtr dwExtraInfo);", - " public static string GetClassNameString(IntPtr hWnd) { var sb = new StringBuilder(256); return GetClassName(hWnd, sb, sb.Capacity) > 0 ? sb.ToString() : string.Empty; }", - " private static bool Contains(RECT rect, int x, int y) { return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; }", - " public static IntPtr ResolveInputWindow(IntPtr topWindow, int screenX, int screenY) {", - " IntPtr inputSite = IntPtr.Zero;", - " IntPtr bridge = IntPtr.Zero;", - " EnumChildWindows(topWindow, (child, lParam) => {", - " RECT rect;", - " if (GetWindowRect(child, out rect) && Contains(rect, screenX, screenY)) {", - " var className = GetClassNameString(child);", - " if (string.Equals(className, \"Windows.UI.Input.InputSite.WindowClass\", StringComparison.Ordinal)) { inputSite = child; return false; }", - " if (bridge == IntPtr.Zero && string.Equals(className, \"Windows.UI.Composition.DesktopWindowContentBridge\", StringComparison.Ordinal)) { bridge = child; }", - " }", - " return true;", - " }, IntPtr.Zero);", - " if (inputSite != IntPtr.Zero) { return inputSite; }", - " if (bridge != IntPtr.Zero) { return bridge; }", - " var point = new POINT { x = screenX, y = screenY };", - " if (ScreenToClient(topWindow, ref point)) {", - " var child = ChildWindowFromPointEx(topWindow, point, 0);", - " if (child != IntPtr.Zero && child != topWindow) { return child; }", - " }", - " return topWindow;", - " }", - "}", - "'@", - $"$targetWindow = [IntPtr]::new({targetWindowValue})", - "$foreground = $true", - "if ($targetWindow -ne [IntPtr]::Zero) {", - " $foreground = [Native]::SetForegroundWindow($targetWindow)", - " Start-Sleep -Milliseconds 200", - "}", - $"$ok = [Native]::SetCursorPos({targetX}, {targetY})", - "Start-Sleep -Milliseconds 100", - $"$modifier = {modifierValue}", - "$keyDownSent = 0", - "if ($modifier -ge 0) {", - " [Native]::keybd_event([byte]$modifier, 0, 0, [UIntPtr]::Zero)", - " $keyDownSent = 1", - " Start-Sleep -Milliseconds 100", - "}", - "$client = $true", - "$messageWindow = $targetWindow", - "$messageClass = ''", - "if ($targetWindow -ne [IntPtr]::Zero) {", - " $messageWindow = [Native]::ResolveInputWindow($targetWindow, " + targetX.ToString(CultureInfo.InvariantCulture) + ", " + targetY.ToString(CultureInfo.InvariantCulture) + ")", - " $messageClass = [Native]::GetClassNameString($messageWindow)", - " $point = New-Object Native+POINT", - $" $point.x = {targetX}", - $" $point.y = {targetY}", - " $client = [Native]::ScreenToClient($messageWindow, [ref]$point)", - " $mkModifier = if ($modifier -eq 16) { 4 } elseif ($modifier -eq 17) { 8 } else { 0 }", - " $lParam = [IntPtr](($point.x -band 0xFFFF) -bor (($point.y -band 0xFFFF) -shl 16))", - " [void][Native]::SendMessage($messageWindow, 0x0200, [IntPtr]$mkModifier, $lParam)", - " [void][Native]::SendMessage($messageWindow, 0x0201, [IntPtr](1 -bor $mkModifier), $lParam)", - " Start-Sleep -Milliseconds 50", - " [void][Native]::SendMessage($messageWindow, 0x0202, [IntPtr]$mkModifier, $lParam)", - "} else {", - $" [Native]::mouse_event({MouseEventLeftDown}, 0, 0, 0, [UIntPtr]::Zero)", - " Start-Sleep -Milliseconds 50", - $" [Native]::mouse_event({MouseEventLeftUp}, 0, 0, 0, [UIntPtr]::Zero)", - "}", - "$mouseSent = 2", - "Start-Sleep -Milliseconds 100", - "$keyUpSent = 0", - "if ($modifier -ge 0) {", - $" [Native]::keybd_event([byte]$modifier, 0, {KeyEventKeyUp}, [UIntPtr]::Zero)", - " $keyUpSent = 1", - "}", - $"Set-Content -Path '{escapedResultPath}' -Value (\"ok={{0}};foreground={{1}};client={{2}};window=0x{{3:x}};class={{4}};keydown={{5}};mouse={{6}};keyup={{7}}\" -f $ok, $foreground, $client, $messageWindow.ToInt64(), $messageClass, $keyDownSent, $mouseSent, $keyUpSent)", - "} catch {", - $" Set-Content -Path '{escapedResultPath}' -Value (\"error={{0}}\" -f $_.Exception.Message)", - " exit 1", - "}", - }); - - var launcherContents = string.Join(Environment.NewLine, new[] - { - "@echo off", - $"powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"{scriptPath}\"", - }); - - File.WriteAllText(scriptPath, scriptContents); - File.WriteAllText(launcherPath, launcherContents); - _scheduledTaskNames.Add(taskName); - _temporaryLaunchArtifacts.Add(scriptPath); - _temporaryLaunchArtifacts.Add(launcherPath); - _temporaryLaunchArtifacts.Add(resultPath); - - _RunProcessAndVerify("schtasks.exe", - $"/Create /TN \"{taskName}\" /SC ONCE /ST 00:00 /RL LIMITED /IT /TR \"\\\"{launcherPath}\\\"\" /F", - $"Create limited input task '{taskName}'."); - _RunProcessAndVerify("schtasks.exe", - $"/Run /TN \"{taskName}\"", - $"Start limited input task '{taskName}'."); - - var remaining = 10000; - while (!File.Exists(resultPath) && remaining > 0) - { - Thread.Sleep(100); - remaining -= 100; - } - - Verify.IsTrue(File.Exists(resultPath), $"Wait for limited input result for {description}."); - var result = File.ReadAllText(resultPath).Trim(); - Log.Comment($"{description} limited helper result: {result}"); - Verify.IsTrue(!result.StartsWith("error=", StringComparison.OrdinalIgnoreCase), $"Limited helper for {description} should succeed."); - Verify.IsTrue(result.IndexOf("mouse=2", StringComparison.OrdinalIgnoreCase) >= 0, $"Limited helper should send mouse inputs for {description}."); - if (modifierKey.HasValue) - { - Verify.IsTrue(result.IndexOf("keydown=1", StringComparison.OrdinalIgnoreCase) >= 0 && - result.IndexOf("keyup=1", StringComparison.OrdinalIgnoreCase) >= 0, - $"Limited helper should send modifier inputs for {description}."); } - Thread.Sleep(200); - } - - private static INPUT _CreateKeyboardInput(byte virtualKey, bool keyUp) - { - return new INPUT - { - type = InputKeyboard, - U = new INPUTUNION - { - ki = new KEYBDINPUT - { - wVk = virtualKey, - dwFlags = keyUp ? KeyEventKeyUp : 0, - }, - }, - }; } private static (int X, int Y) _GetScreenCenter(AppiumWebElement element) @@ -965,28 +435,6 @@ private static (int X, int Y) _GetScreenCenter(AppiumWebElement element) ); } - private static IntPtr _TryGetWindowHandle(AppiumWebElement element) - { - var handle = _TryGetAttribute(element, "NativeWindowHandle"); - if (string.IsNullOrEmpty(handle)) - { - return IntPtr.Zero; - } - - if (long.TryParse(handle, NumberStyles.Integer, CultureInfo.InvariantCulture, out var decimalHandle)) - { - return new IntPtr(decimalHandle); - } - - handle = handle.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? handle.Substring(2) : handle; - if (long.TryParse(handle, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexHandle)) - { - return new IntPtr(hexHandle); - } - - return IntPtr.Zero; - } - private void _MoveCursorSmoothly(AppiumWebElement source, int startOffsetX, int startOffsetY, int endOffsetX, int endOffsetY, int steps = 24, int delayMs = 15) { for (var step = 1; step <= steps; step++) @@ -1003,34 +451,21 @@ private void _MoveMouseToElementOffset(AppiumWebElement element, int offsetX, in Session.Mouse.MouseMove(element.Coordinates, element.Size.Width / 2 + offsetX, element.Size.Height / 2 + offsetY); } - protected virtual void Dispose(bool disposing) - { - if (!this.isDisposed) - { - // ensure we're exited when this is destroyed or disposed of explicitly - this.ExitProcess(); - - this.isDisposed = true; - } - } - private void CreateProcess(string path, string shellToLaunch, string launchArgs) { Log.Comment("Attempting to launch command-line application at '{0}'", path); + job = WinBase.CreateJobObject(IntPtr.Zero, IntPtr.Zero); + NativeMethods.Win32NullHelper(job, "Creating job object to hold binaries under test."); + string binaryToRunPath = path; string args = launchArgs ?? $"new-tab --title \"{_windowTitleToFind}\" --suppressApplicationTitle \"{shellToLaunch}\""; - if (IsRunningAsAdmin()) - { - Log.Comment("UIA runner is elevated; launching Terminal at medium integrity through a limited scheduled task."); - _LaunchLimitedProcess(binaryToRunPath, args); - } - else - { - job = WinBase.CreateJobObject(IntPtr.Zero, IntPtr.Zero); - NativeMethods.Win32NullHelper(job, "Creating job object to hold binaries under test."); + string processCommandLine = $"{binaryToRunPath} {args}"; - string processCommandLine = $"{binaryToRunPath} {args}"; + var priorHookValue = Environment.GetEnvironmentVariable("WT_UIA_ENABLE_TEST_HOOKS"); + try + { + Environment.SetEnvironmentVariable("WT_UIA_ENABLE_TEST_HOOKS", "1"); WinBase.STARTUPINFO si = new WinBase.STARTUPINFO(); si.cb = Marshal.SizeOf(si); @@ -1053,7 +488,10 @@ private void CreateProcess(string path, string shellToLaunch, string launchArgs) NativeMethods.Win32BoolHelper(WinBase.AssignProcessToJobObject(job, pi.hProcess), "Assigning new host window (suspended) to job object."); NativeMethods.Win32BoolHelper(-1 != WinBase.ResumeThread(pi.hThread), "Resume host window process now that it is attached and its launch of the child application will be caught in the job object."); - _trackedProcessIds.Add(pi.dwProcessId); + } + finally + { + Environment.SetEnvironmentVariable("WT_UIA_ENABLE_TEST_HOOKS", priorHookValue); } Globals.WaitForTimeout(); @@ -1061,143 +499,27 @@ private void CreateProcess(string path, string shellToLaunch, string launchArgs) DesiredCapabilities appCapabilities = new DesiredCapabilities(); appCapabilities.SetCapability("app", @"Root"); _desktopSession = new IOSDriver(new Uri(AppDriverUrl), appCapabilities); + _desktopSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15); Session = _desktopSession; + Actions = new Actions(Session); Verify.IsNotNull(_desktopSession); - Actions = new Actions(Session); Verify.IsNotNull(Session); - var remaining = 30000; - while (UIRoot == null && remaining >= 0) - { - var initialWindow = FindTopLevelWindowByName(_windowTitleToFind); - if (initialWindow != null) - { - ActivateWindow(initialWindow); - User32.GetWindowThreadProcessId(initialWindow.Handle, out var windowPid); - if (windowPid != 0) - { - _trackedProcessIds.Add(unchecked((int)windowPid)); - } - - UIRoot = initialWindow.Root; - } - - if (UIRoot == null) - { - Thread.Sleep(250); - remaining -= 250; - } - } - - if (UIRoot == null) - { - var matchingByName = Session.FindElementsByName(_windowTitleToFind) - .OfType() - .Where(_IsValidElement) - .Take(30) - .ToList(); - Log.Comment($"Elements visible to WinAppDriver with Name='{_windowTitleToFind}': {matchingByName.Count}"); - foreach (var element in matchingByName) - { - Log.Comment($"Element candidate: Name='{element.GetAttribute("Name")}', ClassName='{element.GetAttribute("ClassName")}', ProcessId='{element.GetAttribute("ProcessId")}'"); - } - - UIRoot = Session.FindElementByXPath("//*"); - Log.Comment("Falling back to desktop root element for UI automation session."); - } - + var initialWindow = WaitForTopLevelWindowByName(_windowTitleToFind, null, 30000); + ActivateWindow(initialWindow); Verify.IsNotNull(UIRoot, $"Failed to find a top-level window for '{_windowTitleToFind}'."); - - // Set the timeout to 15 seconds after we found the initial window. - Session.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(15); - } - - private void _LaunchLimitedProcess(string path, string args) - { - var taskName = $"WindowsTerminal-UIA-{Process.GetCurrentProcess().Id}-{Guid.NewGuid():N}"; - var launcherPath = Path.Combine(Path.GetTempPath(), $"{taskName}.cmd"); - _dragEventLogPath = Path.Combine(Path.GetTempPath(), $"{taskName}.drag.log"); - var launcherContents = - "@echo off" + Environment.NewLine + - $"echo launcher-started>\"{_dragEventLogPath}\"" + Environment.NewLine + - $"set \"WT_UIA_DRAG_LOG={_dragEventLogPath}\"" + Environment.NewLine + - "set \"WT_UIA_ENABLE_TEST_HOOKS=1\"" + Environment.NewLine + - $"cd /d \"{Path.GetDirectoryName(path)}\"" + Environment.NewLine + - $"\"{path}\" {args}" + Environment.NewLine; - - File.WriteAllText(launcherPath, launcherContents); - _scheduledTaskNames.Add(taskName); - _temporaryLaunchArtifacts.Add(launcherPath); - Log.Comment($"Drag event log will be written to '{_dragEventLogPath}'."); - - _RunProcessAndVerify("schtasks.exe", - $"/Create /TN \"{taskName}\" /SC ONCE /ST 00:00 /RL LIMITED /IT /TR \"\\\"{launcherPath}\\\"\" /F", - $"Create limited scheduled task '{taskName}'."); - _RunProcessAndVerify("schtasks.exe", - $"/Run /TN \"{taskName}\"", - $"Start limited scheduled task '{taskName}'."); - } - - private static void _RunProcessAndVerify(string fileName, string arguments, string description) - { - using (var process = new Process()) - { - process.StartInfo = new ProcessStartInfo - { - FileName = fileName, - Arguments = arguments, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - Verify.IsTrue(process.Start(), description); - var standardOutput = process.StandardOutput.ReadToEnd(); - var standardError = process.StandardError.ReadToEnd(); - process.WaitForExit(); - - if (!string.IsNullOrWhiteSpace(standardOutput)) - { - Log.Comment(standardOutput.Trim()); - } - - if (!string.IsNullOrWhiteSpace(standardError)) - { - Log.Comment(standardError.Trim()); - } - - Verify.AreEqual(0, process.ExitCode, description); - } - } - - private static void _TryRunProcess(string fileName, string arguments, string description) - { - try - { - _RunProcessAndVerify(fileName, arguments, description); - } - catch (Exception ex) - { - Log.Comment($"{description} failed during cleanup: {ex.Message}"); - } - } - - private bool IsRunningAsAdmin() - { - return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); } private void ExitProcess() { - Globals.SweepAllModules(this.context); + Globals.SweepAllModules(context); - // Release attachment to the child process console. WinCon.FreeConsole(); - this.UIRoot = null; - this.Session = null; + UIRoot = null; + Session = null; + Actions = null; foreach (var session in _windowSessions.Values) { @@ -1211,28 +533,6 @@ private void ExitProcess() } _windowSessions.Clear(); - foreach (var taskName in _scheduledTaskNames) - { - _TryRunProcess("schtasks.exe", - $"/Delete /TN \"{taskName}\" /F", - $"Delete limited scheduled task '{taskName}'."); - } - - foreach (var artifact in _temporaryLaunchArtifacts) - { - try - { - File.Delete(artifact); - } - catch (Exception ex) - { - Log.Comment($"Delete temporary launch artifact '{artifact}' failed during cleanup: {ex.Message}"); - } - } - - _scheduledTaskNames.Clear(); - _temporaryLaunchArtifacts.Clear(); - if (_desktopSession != null) { try @@ -1242,28 +542,14 @@ private void ExitProcess() catch { } + _desktopSession = null; } - if (this.job != IntPtr.Zero) - { - WinBase.TerminateJobObject(this.job, 0); - this.job = IntPtr.Zero; - } - else + if (job != IntPtr.Zero) { - foreach (var pid in _trackedProcessIds.ToArray()) - { - try - { - Process.GetProcessById(pid).Kill(); - } - catch - { - } - } - - _trackedProcessIds.Clear(); + WinBase.TerminateJobObject(job, 0); + job = IntPtr.Zero; } } } diff --git a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs index 3ba11b30a24..db418dad041 100644 --- a/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs +++ b/src/cascadia/WindowsTerminal_UIATests/SmokeTests.cs @@ -213,8 +213,6 @@ public void DragMultipleTabsAcrossWindows() app.ArrangeWindowOnPrimaryMonitor(windowA, 0); app.ActivateWindow(windowA); var firstTab = app.FindTabElementByName(windowA, detachedTabTitle); - app.LogWindowDetails("initialWindowA", windowA); - app.LogElementDetails("firstTab", windowA, firstTab); app.DragByOffset(firstTab, windowA.Width + 80, 80); Globals.WaitForLongTimeout(); @@ -231,22 +229,12 @@ public void DragMultipleTabsAcrossWindows() app.ActivateWindow(windowA); var secondTab = app.FindTabElementByName(windowA, firstAttachedTabTitle); - var thirdTab = app.FindTabElementByName(windowA, secondAttachedTabTitle); - - app.LogWindowDetails("windowA", windowA); - app.LogWindowDetails("windowB", windowB); - app.LogElementDetails("secondTab", windowA, secondTab); - app.LogElementDetails("thirdTab", windowA, thirdTab); - app.LogElementAncestors("secondTabAncestors", secondTab); - app.LogElementAncestors("thirdTabAncestors", thirdTab); app.SelectTabRangeForTesting(windowA, 0, 1); Globals.WaitForTimeout(); secondTab = app.FindTabElementByName(windowA, firstAttachedTabTitle); var existingWindowTab = app.FindTabElementByName(windowB, detachedTabTitle); - app.LogElementDetails("existingWindowTab", windowB, existingWindowTab); - app.LogElementAncestors("existingWindowTabAncestors", existingWindowTab); app.ActivateWindow(windowA); var dropOffset = System.Math.Max(24, existingWindowTab.Size.Width / 3); app.DragToElement(secondTab, existingWindowTab, dropOffset, 0);