// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "TerminalPage.h" #include #include #include #include #include "../../types/inc/utils.hpp" #include "App.h" #include "ColorHelper.h" #include "DebugTapConnection.h" #include "SettingsPaneContent.h" #include "ScratchpadContent.h" #include "SnippetsPaneContent.h" #include "MarkdownPaneContent.h" #include "TabRowControl.h" #include "Remoting.h" #include "TerminalPage.g.cpp" #include "RenameWindowRequestedArgs.g.cpp" #include "RequestMoveContentArgs.g.cpp" #include "LaunchPositionRequest.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::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 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 terminalTab{ _GetTerminalTabImpl(tab) }) { terminalTab->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 = TerminalApp::TerminalSettingsCache{ settings, *_bindings }; } _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; 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() }) { 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()); } 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 { // GH#12267: Don't forget about defterm handoff here. If we're being // created for embedding, then _yea_, we don't need to handoff to an // elevated window. if (_startupActions.empty() || IsRunningElevated() || _shouldStartInboundListener) { // there aren't startup actions, or we're elevated. In that case, go for it. 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; ProcessStartupActions(std::move(_startupActions), true); // If we were told that the COM server needs to be started to listen for incoming // default application connections, start it now. // This MUST be done after we've registered the event listener for the new connections // or the COM server might start receiving requests on another thread and dispatch // them to nowhere. _StartInboundListener(); } } // Routine Description: // - Will start the listener for inbound console handoffs if we have already determined // that we should do so. // NOTE: Must be after TerminalPage::_OnNewConnection has been connected up. // Arguments: // - - Looks at _shouldStartInboundListener // Return Value: // - - May fail fast if setup fails as that would leave us in a weird state. void TerminalPage::_StartInboundListener() { if (_shouldStartInboundListener) { _shouldStartInboundListener = false; // Hook up inbound connection event handler _newConnectionRevoker = ConptyConnection::NewConnection(winrt::auto_revoke, { this, &TerminalPage::_OnNewConnection }); try { winrt::Microsoft::Terminal::TerminalConnection::ConptyConnection::StartInboundListener(); } // If we failed to start the listener, it will throw. // We don't want to fail fast here because if a peasant has some trouble with // starting the listener, we don't want it to crash and take all its tabs down // with it. catch (...) { LOG_CAUGHT_EXCEPTION(); } } } // 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 bool initial, 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); } for (size_t i = 0; i < actions.size(); ++i) { if (i != 0) { // Each action may rely on the XAML layout of a preceding action. // Most importantly, this is the case for the combination of NewTab + SplitPane, // as the former appears to only have a layout size after at least 1 resume_foreground, // while the latter relies on that information. This is also why it uses Low priority. // // Curiously, this does not seem to be required when using startupActions, but only when // tearing out a tab (this currently creates a new window with injected startup actions). // This indicates that this is really more of an architectural issue and not a fundamental one. co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low); } _actionDispatch->DoAction(actions[i]); } // 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& terminalTab{ _GetFocusedTabImpl() }) { if (const auto& content{ terminalTab->GetActiveContent() }) { content.Focus(FocusState::Programmatic); } } if (initial) { _CompleteInitialization(); } } // 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 && !_shouldStartInboundListener && !_isEmbeddingInboundListener) { 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([this](auto&&, auto&&) { _FocusCurrentTab(true); }); // Necessary for fly-out sub items to get focus on a tab before collapsing. Related to #15049 newTabFlyout.Closing([this](auto&&, auto&&) { if (!_commandPaletteIs(Visibility::Visible)) { _FocusCurrentTab(true); } }); _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 && folderEntries.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()); 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()); 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()); 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.EvaluatedIcon() : 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() }) { 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.IconPath() : 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() }) { 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(); 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); } else { _OpenNewWindow(newTerminalArgs); } } 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); } else { _CreateNewTabFromPane(newPane); } } } 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, TerminalSettings 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 {}; } }(); TerminalConnection::ITerminalConnection connection{ nullptr }; auto connectionType = profile.ConnectionType(); Windows::Foundation::Collections::ValueSet valueSet; if (connectionType == TerminalConnection::AzureConnection::ConnectionType() && TerminalConnection::AzureConnection::IsAzureConnectionAvailable()) { std::filesystem::path azBridgePath{ wil::GetModuleFileNameW(nullptr) }; azBridgePath.replace_filename(L"TerminalAzBridge.exe"); if constexpr (Feature_AzureConnectionInProc::IsEnabled()) { connection = TerminalConnection::AzureConnection{}; } else { connection = TerminalConnection::ConptyConnection{}; } valueSet = TerminalConnection::ConptyConnection::CreateSettings(azBridgePath.native(), L".", L"Azure", false, L"", nullptr, settings.InitialRows(), settings.InitialCols(), winrt::guid(), profile.Guid()); } else { const auto environment = settings.EnvironmentVariables() != nullptr ? settings.EnvironmentVariables().GetView() : nullptr; // 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(), settings.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 (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() }; TerminalSettingsCreateResult controlSettings{ nullptr }; if (profile) { // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. profile = GetClosestProfileForDuplicationOfProfile(profile); controlSettings = TerminalSettings::CreateWithProfile(_settings, profile, *_bindings); // 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; } _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); } // 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(); } // 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 TerminalTab& tab) { auto newTabTitle = tab.Title(); if (tab == _GetFocusedTab()) { TitleChanged.raise(*this, newTabTitle); } } // 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 }); // Add an event handler when the terminal wants to paste data from the Clipboard. 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 TerminalTab 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(TerminalTab& 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 terminalTab{ _GetFocusedTabImpl() }) { return terminalTab->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 terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); return terminalTab->SwapPane(direction); } return false; } TermControl TerminalPage::_GetActiveControl() { if (const auto terminalTab{ _GetFocusedTabImpl() }) { return terminalTab->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; auto warningResult = co_await _ShowQuitDialog(); _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); } // 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 terminalTab{ _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 ? terminalTab->GetActiveTerminalControl().ViewHeight() : _systemRowsToScroll; } else { // use the custom value specified in the command realRowsToScroll = rowsToScroll.Value(); } auto scrollDelta = _ComputeScrollDelta(scrollDirection, realRowsToScroll); terminalTab->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 terminalTab{ _GetFocusedTabImpl() }) { if (const auto pane{ terminalTab->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 = _GetTerminalTabImpl(_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) { if (const auto terminalTab = tab.try_as()) { // Detach the root pane, which will act like the whole tab got detached. if (const auto rootPane = terminalTab->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::TerminalTab sender, Windows::Foundation::IInspectable args) { if (const auto tab{ _GetTerminalTabImpl(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); } } 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; } 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) { // 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 source = *focusedTabIndex; _TryMoveTab(source, tabIndex); } // else: This shouldn't really be possible, because the tab we _just_ opened should be active. } } // 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 terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); terminalTab->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 terminalTab{ _GetFocusedTabImpl() }) { _UnZoomIfNeeded(); terminalTab->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 terminalTab{ _GetFocusedTabImpl() }) { if (const auto& control{ _GetActiveControl() }) { const auto termHeight = control.ViewHeight(); auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); terminalTab->Scroll(scrollDelta); } } } void TerminalPage::_ScrollToBufferEdge(ScrollDirection scrollDirection) { if (const auto terminalTab{ _GetFocusedTabImpl() }) { auto scrollDelta = _ComputeScrollDelta(scrollDirection, INT_MAX); terminalTab->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()) { auto selectedIndex = _tabView.SelectedIndex(); if (selectedIndex >= 0) { try { if (auto focusedControl{ _GetActiveControl() }) { return focusedControl.Title(); } } CATCH_LOG(); } } 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 terminalTab{ _GetFocusedTabImpl() }) { return terminalTab->CalcSnappedDimension(widthOrHeight, dimension); } } return dimension; } static wil::unique_close_clipboard_call _openClipboard(HWND hwnd) { 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); } return wil::unique_close_clipboard_call{ success }; } static winrt::hstring _extractClipboard() { // 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 {}; } // 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(); // GetClipboardData might block for up to 30s for delay-rendered contents. co_await winrt::resume_background(); winrt::hstring text; if (const auto clipboard = _openClipboard(nullptr)) { text = _extractClipboard(); } if (globalSettings.TrimPaste()) { text = { Utils::TrimPaste(text) }; if (text.empty()) { // Text is all white space, nothing to paste co_return; } } // If the requesting terminal is in bracketed paste mode, then we don't need to warn about a multi-line paste. auto warnMultiLine = globalSettings.WarnAboutMultiLinePaste() && !eventArgs.BracketedPasteEnabled(); if (warnMultiLine) { const auto isNewLineLambda = [](auto c) { return c == L'\n' || c == L'\r'; }; const auto hasNewLine = std::find_if(text.cbegin(), text.cend(), isNewLineLambda) != text.cend(); warnMultiLine = hasNewLine; } constexpr const 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 FindName(L"MultiLinePasteDialog").try_as(); ClipboardText().Text(text); // 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(L""); 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(std::move(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 Windows::Foundation::IReference& 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*/) { co_await wil::resume_foreground(Dispatcher()); 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(); // clang-format off static constexpr std::array searches{ { { .Field = PackageMatchField::Command, .MatchOption = PackageFieldMatchOption::StartsWithCaseInsensitive }, { .Field = PackageMatchField::Name, .MatchOption = PackageFieldMatchOption::ContainsCaseInsensitive }, { .Field = PackageMatchField::Moniker, .MatchOption = PackageFieldMatchOption::ContainsCaseInsensitive } } }; // clang-format on PackageMatchFilter filter = WindowsPackageManagerFactory::CreatePackageMatchFilter(); filter.Value(query); FindPackagesOptions options = WindowsPackageManagerFactory::CreateFindPackagesOptions(); options.Filters().Append(filter); options.ResultLimit(20); IVectorView pkgList; for (const auto& search : searches) { filter.Field(search.Field); filter.Option(search.MatchOption); const auto result = co_await catalog.FindPackagesAsync(options); pkgList = result.Matches(); if (pkgList.Size() > 0) { break; } } co_return pkgList; } Windows::Foundation::IAsyncAction TerminalPage::_SearchMissingCommandHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::SearchMissingCommandEventArgs args) { if (!Feature_QuickFix::IsEnabled()) { co_return; } 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()); 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(); } } } // Method Description: // - Paste text from the Windows Clipboard to the focused terminal void TerminalPage::_PasteText() { // First, check if we're in broadcast input mode. If so, let's tell all // the controls to paste. if (const auto& tab{ _GetFocusedTabImpl() }) { if (tab->TabStatus().IsInputBroadcastActive()) { tab->GetRootPane()->WalkTree([](auto&& pane) { if (auto control = pane->GetTerminalControl()) { control.PasteTextFromClipboard(); } }); return; } } // The focused tab wasn't in broadcast mode. No matter. Just ask the // current one to paste. 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. 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 TerminalSettingsCreateResult& settings, const ITerminalConnection& connection) { // Do any initialization that needs to apply to _every_ TermControl we // create here. // TermControl will copy the settings out of the settings passed to it. const auto content = _manager.CreateCore(settings.DefaultSettings(), settings.UnfocusedSettings(), 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, *_bindings)); } 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)); } _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::TabBase& 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); } TerminalSettingsCreateResult controlSettings{ nullptr }; Profile profile{ nullptr }; if (const auto& terminalTab{ _GetTerminalTabImpl(sourceTab) }) { profile = terminalTab->GetFocusedProfile(); if (profile) { // TODO GH#5047 If we cache the NewTerminalArgs, we no longer need to do this. profile = GetClosestProfileForDuplicationOfProfile(profile); controlSettings = TerminalSettings::CreateWithProfile(_settings, profile, *_bindings); const auto workingDirectory = terminalTab->GetActiveTerminalControl().WorkingDirectory(); const auto validWorkingDirectory = !workingDirectory.empty(); if (validWorkingDirectory) { controlSettings.DefaultSettings().StartingDirectory(workingDirectory); } } } if (!profile) { profile = _settings.GetProfileForArgs(newTerminalArgs); controlSettings = TerminalSettings::CreateWithNewTerminalArgs(_settings, newTerminalArgs, *_bindings); } // 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) { const auto settingsDir = CascadiaSettings::SettingsDirectory(); const auto idStr = Utils::GuidToPlainString(sessionId); const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr); 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 TerminalTab, // 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::TabBase& 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{ _GetFocusedTab() }) { const auto rootPane{ focusedTab.try_as()->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.ExpandedBackgroundImagePath(); 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, *_bindings); for (const auto& tab : _tabs) { if (auto terminalTab{ _GetTerminalTabImpl(tab) }) { // Let the tab know that there are new settings. It's up to each content to decide what to do with them. terminalTab->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(*terminalTab); // Force the TerminalTab to re-grab its currently active control's title. terminalTab->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(); } 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; 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); } // Routine Description: // - Notifies this Terminal Page that it should start the incoming connection // listener for command-line tools attempting to join this Terminal // through the default application channel. // Arguments: // - isEmbedding - True if COM started us to be a server. False if we're doing it of our own accord. // Return Value: // - void TerminalPage::SetInboundListener(bool isEmbedding) { _shouldStartInboundListener = true; _isEmbeddingInboundListener = isEmbedding; // If the page has already passed the NotInitialized state, // then it is ready-enough for us to just start this immediately. if (_startupState != StartupState::NotInitialized) { _StartInboundListener(); } } 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{ _GetTerminalTabImpl(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 terminalTab{ _GetTerminalTabImpl(tab) }) { // Manually enumerate the panes in each tab; this will let us recycle TerminalSettings // objects but only have to iterate one time. terminalTab->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 TerminalTab& 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 Windows::UI::Color& color, const Windows::UI::Color& accentColor) { // TODO GH#3327: Look at what to do with the tab button when we have XAML theming auto IsBrightColor = ColorHelper::IsBrightColor(color); auto isLightAccentColor = ColorHelper::IsBrightColor(accentColor); winrt::Windows::UI::Color pressedColor{}; winrt::Windows::UI::Color hoverColor{}; winrt::Windows::UI::Color foregroundColor{}; const auto hoverColorAdjustment = 5.f; const auto pressedColorAdjustment = 7.f; if (IsBrightColor) { foregroundColor = winrt::Windows::UI::Colors::Black(); } else { foregroundColor = winrt::Windows::UI::Colors::White(); } if (isLightAccentColor) { hoverColor = ColorHelper::Darken(accentColor, hoverColorAdjustment); pressedColor = ColorHelper::Darken(accentColor, pressedColorAdjustment); } else { hoverColor = ColorHelper::Lighten(accentColor, hoverColorAdjustment); pressedColor = ColorHelper::Lighten(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 TabBase::_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); } HRESULT TerminalPage::_OnNewConnection(const ConptyConnection& connection) { _newConnectionRevoker.revoke(); // We need to be on the UI thread in order for _OpenNewTab to run successfully. // HasThreadAccess will return true if we're currently on a UI thread and false otherwise. // When we're on a COM thread, we'll need to dispatch the calls to the UI thread // and wait on it hence the locking mechanism. if (!Dispatcher().HasThreadAccess()) { til::latch latch{ 1 }; auto finalVal = S_OK; Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [&]() { finalVal = _OnNewConnection(connection); latch.count_down(); }); latch.wait(); return finalVal; } try { NewTerminalArgs newTerminalArgs; newTerminalArgs.Commandline(connection.Commandline()); newTerminalArgs.TabTitle(connection.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, connection); newPane->WalkTree([](const auto& pane) { pane->FinalizeConfigurationGivenDefault(); }); _CreateNewTabFromPane(newPane); // Request a summon of this window to the foreground SummonWindowRequested.raise(*this, nullptr); // TEMPORARY SOLUTION // If the connection has requested for the window to be maximized, // manually maximize it here. Ideally, we should be _initializing_ // the session maximized, instead of manually maximizing it after initialization. // However, because of the current way our defterm handoff works, // we are unable to get the connection info before the terminal session // has already started. // Make sure that there were no other tabs already existing (in // the case that we are in glomming mode), because we don't want // to be maximizing other existing sessions that did not ask for it. if (_tabs.Size() == 1 && connection.ShowWindow() == SW_SHOWMAXIMIZED) { RequestSetMaximized(true); } return S_OK; } CATCH_RETURN() } 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); } }); 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 TerminalTab. // 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::_GetTerminalTabImpl(const TerminalApp::TabBase& tab) { if (auto terminalTab = tab.try_as()) { winrt::com_ptr tabImpl; tabImpl.copy_from(winrt::get_self(terminalTab)); return tabImpl; } else { return nullptr; } } // 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 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*/) const { if (const auto coreState{ sender.try_as() }) { const auto newConnectionState = coreState.ConnectionState(); if (newConnectionState == ConnectionState::Failed && !_IsMessageDismissed(InfoBarMessage::CloseOnExitInfo)) { co_await wil::resume_foreground(Dispatcher()); 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 terminalTab{ _GetTerminalTabImpl(tab) }) { // The root pane will propagate the theme change to all its children. if (const auto& rootPane{ terminalTab->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(); } } } if (_settings.GlobalSettings().UseAcrylicInTabRow()) { const auto acrylicBrush = Media::AcrylicBrush(); acrylicBrush.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); acrylicBrush.FallbackColor(bgColor); acrylicBrush.TintColor(bgColor); acrylicBrush.TintOpacity(0.5); TitlebarBrush(acrylicBrush); } else if (auto tabRowBg{ theme.TabRow() ? (_activated ? theme.TabRow().Background() : theme.TabRow().UnfocusedBackground()) : ThemeColor{ nullptr } }) { 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 TerminalTab::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::WindowActivated(const bool activated) { // Stash if we're activated. Use that when we reload // the settings, change active panes, etc. _activated = activated; _updateThemeColors(); 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 will come in on a background (not-UI, not output) thread. // 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 = [&menu, &makeCallback](const winrt::hstring& label, const winrt::hstring& icon, const auto& action) { 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)); menu.SecondaryCommands().Append(button); }; // 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"SplitPaneText"), L"\xF246", ActionAndArgs{ ShortcutAction::SplitPane, SplitPaneArgs{ SplitType::Duplicate } }); makeItem(RS_(L"DuplicateTabText"), L"\xF5ED", ActionAndArgs{ ShortcutAction::DuplicateTab, nullptr }); // Only wire up "Close Pane" if there's multiple panes. if (_GetFocusedTabImpl()->GetLeafPaneCount() > 1) { makeItem(RS_(L"PaneClose"), L"\xE89F", ActionAndArgs{ ShortcutAction::ClosePane, nullptr }); } if (control.ConnectionState() >= ConnectionState::Closed) { makeItem(RS_(L"RestartConnectionText"), L"\xE72C", ActionAndArgs{ ShortcutAction::RestartConnection, nullptr }); } if (withSelection) { makeItem(RS_(L"SearchWebText"), L"\xF6FA", ActionAndArgs{ ShortcutAction::SearchForText, nullptr }); } makeItem(RS_(L"TabClose"), L"\xE711", ActionAndArgs{ ShortcutAction::CloseTab, CloseTabArgs{ _GetFocusedTabIndex().value() } }); } 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) { // 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) { // First: stash the tab we started dragging. // We're going to be asked for this. _stashed.draggedTab = tabImpl; // 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; } } } // `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.draggedTab) { return; } _sendDraggedTabToWindow(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.draggedTab) { 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, }; _sendDraggedTabToWindow(winrt::hstring{ L"-1" }, 0, adjusted); } void TerminalPage::_sendDraggedTabToWindow(const winrt::hstring& windowId, const uint32_t tabIndex, std::optional dragPoint) { auto startupActions = _stashed.draggedTab->BuildStartupActions(BuildStartupKind::Content); _DetachTabFromWindow(_stashed.draggedTab); _MoveContent(std::move(startupActions), windowId, tabIndex, dragPoint); // _RemoveTab will make sure to null out the _stashed.draggedTab _RemoveTab(*_stashed.draggedTab); } /// /// 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() }) { NewTerminalArgs args{ profileIndex }; args.Elevate(true); page->_OpenNewTerminalViaDropdown(args); } }); profileMenuItemFlyout.Items().Append(runAsAdminItem); return profileMenuItemFlyout; } }