Files
terminal/src/cascadia/TerminalApp/TerminalWindow.cpp
Leonard Hecker 0aee174e68 Fix behavior of split-pane for existing windows (#19347)
Closes #18815

## Validation Steps Performed
* `wt -w 0 sp` splits the current tab 
2025-09-16 13:09:56 -05:00

1416 lines
57 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TerminalWindow.h"
#include "AppLogic.h"
#include <LibraryResources.h>
#include <til/env.h>
#include "TerminalWindow.g.cpp"
#include "SettingsLoadEventArgs.g.cpp"
#include "WindowProperties.g.cpp"
#include "../TerminalSettingsAppAdapterLib/TerminalSettings.h"
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::Graphics::Display;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Controls;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Windows::Foundation::Collections;
using namespace ::TerminalApp;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
namespace WUX = Windows::UI::Xaml;
using IInspectable = Windows::Foundation::IInspectable;
}
// !!! IMPORTANT !!!
// Make sure that these keys are in the same order as the
// SettingsLoadWarnings/Errors enum is!
static const std::array settingsLoadWarningsLabels{
USES_RESOURCE(L"MissingDefaultProfileText"),
USES_RESOURCE(L"DuplicateProfileText"),
USES_RESOURCE(L"UnknownColorSchemeText"),
USES_RESOURCE(L"InvalidMediaResource"),
USES_RESOURCE(L"AtLeastOneKeybindingWarning"),
USES_RESOURCE(L"TooManyKeysForChord"),
USES_RESOURCE(L"MissingRequiredParameter"),
USES_RESOURCE(L"FailedToParseCommandJson"),
USES_RESOURCE(L"FailedToWriteToSettings"),
USES_RESOURCE(L"InvalidColorSchemeInCmd"),
USES_RESOURCE(L"InvalidSplitSize"),
USES_RESOURCE(L"FailedToParseStartupActions"),
USES_RESOURCE(L"InvalidProfileEnvironmentVariables"),
USES_RESOURCE(L"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
USES_RESOURCE(L"InvalidUseOfContent"),
USES_RESOURCE(L"InvalidRegex"),
};
static_assert(settingsLoadWarningsLabels.size() == static_cast<size_t>(SettingsLoadWarnings::WARNINGS_SIZE));
// Errors are defined in AppLogic.cpp
// Function Description:
// - General-purpose helper for looking up a localized string for a
// warning/error. First will look for the given key in the provided map of
// keys->strings, where the values in the map are ResourceKeys. If it finds
// one, it will lookup the localized string from that ResourceKey.
// - If it does not find a key, it'll return an empty string
// Arguments:
// - key: the value to use to look for a resource key in the given map
// - map: A map of keys->Resource keys.
// Return Value:
// - the localized string for the given type, if it exists.
template<typename T>
winrt::hstring _GetMessageText(uint32_t index, const T& keys)
{
if (index < keys.size())
{
return GetLibraryResourceString(til::at(keys, index));
}
return {};
}
// Function Description:
// - Gets the text from our ResourceDictionary for the given
// SettingsLoadWarning. If there is no such text, we'll return nullptr.
// - The warning should have an entry in settingsLoadWarningsLabels.
// Arguments:
// - warning: the SettingsLoadWarnings value to get the localized text for.
// Return Value:
// - localized text for the given warning
static winrt::hstring _GetWarningText(SettingsLoadWarnings warning)
{
return _GetMessageText(static_cast<uint32_t>(warning), settingsLoadWarningsLabels);
}
// Function Description:
// - Creates a Run of text to display an error message. The text is yellow or
// red for dark/light theme, respectively.
// Arguments:
// - text: The text of the error message.
// - resources: The application's resource loader.
// Return Value:
// - The fully styled text run.
static Documents::Run _BuildErrorRun(const winrt::hstring& text, const ResourceDictionary& resources)
{
Documents::Run textRun;
textRun.Text(text);
// Color the text red (light theme) or yellow (dark theme) based on the system theme
auto key = winrt::box_value(L"ErrorTextBrush");
if (resources.HasKey(key))
{
auto g = resources.Lookup(key);
auto brush = g.try_as<winrt::Windows::UI::Xaml::Media::Brush>();
textRun.Foreground(brush);
}
return textRun;
}
namespace winrt::TerminalApp::implementation
{
TerminalWindow::TerminalWindow(const TerminalApp::SettingsLoadEventArgs& settingsLoadedResult,
const TerminalApp::ContentManager& manager) :
_settings{ settingsLoadedResult.NewSettings() },
_manager{ manager },
_initialLoadResult{ settingsLoadedResult },
_WindowProperties{ winrt::make_self<TerminalApp::implementation::WindowProperties>() }
{
// The TerminalPage has to ABSOLUTELY NOT BE constructed during our
// construction. We can't do ANY xaml till Initialize() is called.
// For your own sanity, it's better to do setup outside the ctor.
// If you do any setup in the ctor that ends up throwing an exception,
// then it might look like App just failed to activate, which will
// cause you to chase down the rabbit hole of "why is App not
// registered?" when it definitely is.
}
// Method Description:
// - Implements the IInitializeWithWindow interface from shobjidl_core.
HRESULT TerminalWindow::Initialize(HWND hwnd)
{
// Now that we know we can do XAML, build our page.
_root = winrt::make_self<TerminalPage>(*_WindowProperties, _manager);
// Pass in information about the initial state of the window.
// * If we were supposed to start from serialized "content", do that,
// * If we were supposed to load from a persisted layout, do that
// instead.
// * if we have commandline arguments, Pass commandline args into the
// TerminalPage.
if (_startupConnection)
{
_root->SetStartupConnection(std::move(_startupConnection));
}
else if (!_initialContentArgs.empty())
{
_root->SetStartupActions(std::move(_initialContentArgs));
}
else if (const auto& layout = LoadPersistedLayout())
{
// layout will only ever be non-null if there were >0 tabs persisted in
// .TabLayout(). We can re-evaluate that as a part of TODO: GH#12633
_root->SetStartupActions(wil::to_vector(layout.TabLayout()));
}
else if (_appArgs)
{
_root->SetStartupActions(_appArgs->ParsedArgs().GetStartupActions());
}
return _root->Initialize(hwnd);
}
// Method Description:
// - Build the UI for the terminal app. Before this method is called, it
// should not be assumed that the TerminalApp is usable. The Settings
// should be loaded before this is called, either with LoadSettings or
// GetLaunchDimensions (which will call LoadSettings)
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalWindow::Create()
{
_root->DialogPresenter(*this);
// Pay attention, that even if some command line arguments were parsed (like launch mode),
// we will not use the startup actions from settings.
// While this simplifies the logic, we might want to reconsider this behavior in the future.
//
// Obviously, don't use the `startupActions` from the settings in the
// case of a tear-out / reattach. GH#16050
if (!_hasCommandLineArguments &&
_initialContentArgs.empty() &&
_gotSettingsStartupActions)
{
_root->SetStartupActions(_settingsStartupArgs);
}
_root->SetSettings(_settings, false); // We're on our UI thread right now, so this is safe
_root->Loaded({ get_weak(), &TerminalWindow::_OnLoaded });
_root->Initialized({ get_weak(), &TerminalWindow::_pageInitialized });
_root->WindowSizeChanged({ get_weak(), &TerminalWindow::_WindowSizeChanged });
_root->RenameWindowRequested({ get_weak(), &TerminalWindow::_RenameWindowRequested });
_root->ShowLoadWarningsDialog({ get_weak(), &TerminalWindow::_ShowLoadWarningsDialog });
_root->Create();
AppLogic::Current()->SettingsChanged({ get_weak(), &TerminalWindow::UpdateSettingsHandler });
_RefreshThemeRoutine();
auto args = winrt::make_self<SystemMenuChangeArgs>(RS_(L"SettingsMenuItem"),
SystemMenuChangeAction::Add,
SystemMenuItemHandler(this, &TerminalWindow::_OpenSettingsUI));
SystemMenuChangeRequested.raise(*this, *args);
TraceLoggingWrite(
g_hTerminalAppProvider,
"WindowCreated",
TraceLoggingDescription("Event emitted when the window is started"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
void TerminalWindow::_pageInitialized(const IInspectable&, const IInspectable&)
{
// GH#288 - When we finish initialization, if the user wanted us
// launched _fullscreen_, toggle fullscreen mode. This will make sure
// that the window size is _first_ set up as something sensible, so
// leaving fullscreen returns to a reasonable size.
const auto launchMode = this->GetLaunchMode();
if (_WindowProperties->IsQuakeWindow() || WI_IsFlagSet(launchMode, LaunchMode::FocusMode))
{
_root->SetFocusMode(true);
}
// The IslandWindow handles (creating) the maximized state
// we just want to record it here on the page as well.
if (WI_IsFlagSet(launchMode, LaunchMode::MaximizedMode))
{
_root->Maximized(true);
}
if (WI_IsFlagSet(launchMode, LaunchMode::FullscreenMode) && !_WindowProperties->IsQuakeWindow())
{
_root->SetFullscreen(true);
}
AppLogic::Current()->NotifyRootInitialized();
}
void TerminalWindow::PersistState(bool serializeBuffer)
{
if (_root)
{
_root->PersistState(serializeBuffer);
}
}
winrt::Windows::UI::Xaml::ElementTheme TerminalWindow::GetRequestedTheme()
{
return Theme().RequestedTheme();
}
bool TerminalWindow::GetShowTabsInTitlebar()
{
return _settings.GlobalSettings().ShowTabsInTitlebar();
}
bool TerminalWindow::GetInitialAlwaysOnTop()
{
return _settings.GlobalSettings().AlwaysOnTop();
}
bool TerminalWindow::GetInitialShowTabsFullscreen()
{
return _settings.GlobalSettings().ShowTabsFullscreen();
}
bool TerminalWindow::GetMinimizeToNotificationArea()
{
return _settings.GlobalSettings().MinimizeToNotificationArea();
}
bool TerminalWindow::GetAlwaysShowNotificationIcon()
{
return _settings.GlobalSettings().AlwaysShowNotificationIcon();
}
bool TerminalWindow::GetShowTitleInTitlebar()
{
return _settings.GlobalSettings().ShowTitleInTitlebar();
}
Microsoft::Terminal::Settings::Model::Theme TerminalWindow::Theme()
{
return _settings.GlobalSettings().CurrentTheme();
}
// WinUI can't show 2 dialogs simultaneously. Yes, really. If you do, you get an exception.
// As such, we must dismiss whatever dialog is currently being shown.
//
// This limit is of course per-thread and not per-window. Yes... really. See:
// https://github.com/microsoft/microsoft-ui-xaml/issues/794
// The consequence is that we use a static variable to keep track of the shown dialog.
static ContentDialog s_activeDialog{ nullptr };
// Method Description:
// - Show a ContentDialog with buttons to take further action. Uses the
// FrameworkElements provided as the title and content of this dialog, and
// displays buttons (or a single button). Two buttons (primary and secondary)
// will be displayed if this is a warning dialog for closing the terminal,
// this allows the users to abandon the closing action. Otherwise, a single
// close button will be displayed.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens.
// Arguments:
// - dialog: the dialog object that is going to show up
// Return value:
// - an IAsyncOperation with the dialog result
winrt::Windows::Foundation::IAsyncOperation<ContentDialogResult> TerminalWindow::ShowDialog(winrt::WUX::Controls::ContentDialog dialog)
{
// As mentioned on s_activeDialog, dismissing the active dialog is necessary.
// We repeat it a few times in case the resume_foreground failed to work,
// but I found that one iteration will always be enough in practice.
for (int i = 0; i < 3; ++i)
{
if (!s_activeDialog)
{
break;
}
s_activeDialog.Hide();
// Wait for the current dialog to be hidden.
co_await wil::resume_foreground(_root->Dispatcher(), CoreDispatcherPriority::Low);
}
// If two sources call ShowDialog() simultaneously, it may happen that both enter the above loop,
// but it's crucial that only one of them continues below as only 1 dialog can be shown at a time.
// Thankfully, everything runs on the UI thread, so only 1 caller will exit the above loop at a time.
// So, if s_activeDialog is still set at this point, we must have lost the race.
if (s_activeDialog)
{
co_return ContentDialogResult::None;
}
s_activeDialog = dialog;
// IMPORTANT: This is necessary as documented in the ContentDialog MSDN docs.
// Since we're hosting the dialog in a Xaml island, we need to connect it to the
// xaml tree somehow.
dialog.XamlRoot(_root->XamlRoot());
// IMPORTANT: Set the requested theme of the dialog, because the
// PopupRoot isn't directly in the Xaml tree of our root. So the dialog
// won't inherit our RequestedTheme automagically.
// GH#5195, GH#3654 Because we cannot set RequestedTheme at the application level,
// we occasionally run into issues where parts of our UI end up themed incorrectly.
// Dialogs, for example, live under a different Xaml root element than the rest of
// our application. This makes our popup menus and buttons "disappear" when the
// user wants Terminal to be in a different theme than the rest of the system.
// This hack---and it _is_ a hack--walks up a dialog's ancestry and forces the
// theme on each element up to the root. We're relying a bit on Xaml's implementation
// details here, but it does have the desired effect.
// It's not enough to set the theme on the dialog alone.
auto themingLambda{ [this](const Windows::Foundation::IInspectable& sender, const RoutedEventArgs&) {
auto theme{ _settings.GlobalSettings().CurrentTheme() };
auto requestedTheme{ theme.RequestedTheme() };
auto element{ sender.try_as<winrt::Windows::UI::Xaml::FrameworkElement>() };
while (element)
{
element.RequestedTheme(requestedTheme);
element = element.Parent().try_as<winrt::Windows::UI::Xaml::FrameworkElement>();
}
} };
auto result = ContentDialogResult::None;
// Extra scope to drop the revoker before resetting the s_activeDialog to null.
{
themingLambda(dialog, nullptr); // if it's already in the tree
auto loadedRevoker{ dialog.Loaded(winrt::auto_revoke, themingLambda) }; // if it's not yet in the tree
result = co_await dialog.ShowAsync(Controls::ContentDialogPlacement::Popup);
}
s_activeDialog = nullptr;
co_return result;
}
// Method Description:
// - Dismiss the (only) visible ContentDialog
void TerminalWindow::DismissDialog()
{
if (s_activeDialog)
{
s_activeDialog.Hide();
}
}
// Method Description:
// - Displays a dialog for errors found while loading or validating the
// settings. Uses the resources under the provided title and content keys
// as the title and first content of the dialog, then also displays a
// message for whatever exception was found while validating the settings.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens. See ShowDialog for details
// Arguments:
// - titleKey: The key to use to lookup the title text from our resources.
// - contentKey: The key to use to lookup the content text from our resources.
void TerminalWindow::_ShowLoadErrorsDialog(const winrt::hstring& titleKey,
const winrt::hstring& contentKey,
HRESULT settingsLoadedResult,
const winrt::hstring& exceptionText)
{
auto title = GetLibraryResourceString(titleKey);
auto buttonText = RS_(L"Ok");
Controls::TextBlock warningsTextBlock;
warningsTextBlock.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
// Make sure you can copy-paste
warningsTextBlock.IsTextSelectionEnabled(true);
// Make sure the lines of text wrap
warningsTextBlock.TextWrapping(TextWrapping::Wrap);
winrt::Windows::UI::Xaml::Documents::Run errorRun;
const auto errorLabel = GetLibraryResourceString(contentKey);
errorRun.Text(errorLabel);
warningsTextBlock.Inlines().Append(errorRun);
warningsTextBlock.Inlines().Append(Documents::LineBreak{});
if (FAILED(settingsLoadedResult))
{
if (!exceptionText.empty())
{
warningsTextBlock.Inlines().Append(_BuildErrorRun(exceptionText,
winrt::WUX::Application::Current().as<::winrt::TerminalApp::App>().Resources()));
warningsTextBlock.Inlines().Append(Documents::LineBreak{});
}
}
// Add a note that we're using the default settings in this case.
winrt::Windows::UI::Xaml::Documents::Run usingDefaultsRun;
const auto usingDefaultsText = RS_(L"UsingDefaultSettingsText");
usingDefaultsRun.Text(usingDefaultsText);
warningsTextBlock.Inlines().Append(Documents::LineBreak{});
warningsTextBlock.Inlines().Append(usingDefaultsRun);
Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(winrt::box_value(warningsTextBlock));
dialog.CloseButtonText(buttonText);
dialog.DefaultButton(Controls::ContentDialogButton::Close);
ShowDialog(dialog);
}
// Method Description:
// - Displays a dialog for warnings found while loading or validating the
// settings. Displays messages for whatever warnings were found while
// validating the settings.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens. See ShowDialog for details
void TerminalWindow::_ShowLoadWarningsDialog(const IInspectable&, const Windows::Foundation::Collections::IVectorView<SettingsLoadWarnings>& warnings)
{
auto title = RS_(L"SettingsValidateErrorTitle");
auto buttonText = RS_(L"Ok");
Controls::TextBlock warningsTextBlock;
warningsTextBlock.ContextFlyout(winrt::Microsoft::Terminal::UI::TextMenuFlyout{});
// Make sure you can copy-paste
warningsTextBlock.IsTextSelectionEnabled(true);
// Make sure the lines of text wrap
warningsTextBlock.TextWrapping(TextWrapping::Wrap);
for (const auto& warning : warnings)
{
// Try looking up the warning message key for each warning.
const auto warningText = _GetWarningText(warning);
if (!warningText.empty())
{
warningsTextBlock.Inlines().Append(_BuildErrorRun(warningText, winrt::WUX::Application::Current().as<::winrt::TerminalApp::App>().Resources()));
warningsTextBlock.Inlines().Append(Documents::LineBreak{});
}
}
Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(winrt::box_value(warningsTextBlock));
dialog.CloseButtonText(buttonText);
dialog.DefaultButton(Controls::ContentDialogButton::Close);
ShowDialog(dialog);
}
// Method Description:
// - Triggered when the application is finished loading. If we failed to load
// the settings, then this will display the error dialog. This is done
// here instead of when loading the settings, because we need our UI to be
// visible to display the dialog, and when we're loading the settings,
// the UI might not be visible yet.
// Arguments:
// - <unused>
void TerminalWindow::_OnLoaded(const IInspectable& /*sender*/,
const RoutedEventArgs& /*eventArgs*/)
{
if (_settings.GlobalSettings().InputServiceWarning())
{
const auto keyboardServiceIsDisabled = !_IsKeyboardServiceEnabled();
if (keyboardServiceIsDisabled)
{
_root->ShowKeyboardServiceWarning();
}
}
const auto& settingsLoadedResult = gsl::narrow_cast<HRESULT>(_initialLoadResult.Result());
if (FAILED(settingsLoadedResult))
{
const winrt::hstring titleKey = USES_RESOURCE(L"InitialJsonParseErrorTitle");
const winrt::hstring textKey = USES_RESOURCE(L"InitialJsonParseErrorText");
_ShowLoadErrorsDialog(titleKey, textKey, settingsLoadedResult, _initialLoadResult.ExceptionText());
}
else if (settingsLoadedResult == S_FALSE)
{
_ShowLoadWarningsDialog(nullptr, _initialLoadResult.Warnings());
}
}
// Method Description:
// - Helper for determining if the "Touch Keyboard and Handwriting Panel
// Service" is enabled. If it isn't, we want to be able to display a
// warning to the user, because they won't be able to type in the
// Terminal.
// Return Value:
// - true if the service is enabled, or if we fail to query the service. We
// return true in that case, to be less noisy (though, that is unexpected)
bool TerminalWindow::_IsKeyboardServiceEnabled()
{
// If at any point we fail to open the service manager, the service,
// etc, then just quick return true to disable the dialog. We'd rather
// not be noisy with this dialog if we failed for some reason.
// Open the service manager. This will return 0 if it failed.
wil::unique_schandle hManager{ OpenSCManagerW(nullptr, nullptr, 0) };
if (LOG_LAST_ERROR_IF(!hManager.is_valid()))
{
return true;
}
// Get a handle to the keyboard service
wil::unique_schandle hService{ OpenServiceW(hManager.get(), TabletInputServiceKey.data(), SERVICE_QUERY_STATUS) };
// 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 (!hService.is_valid())
{
return true;
}
// Get the current state of the service
SERVICE_STATUS status{ 0 };
if (!LOG_IF_WIN32_BOOL_FALSE(QueryServiceStatus(hService.get(), &status)))
{
return true;
}
const auto state = status.dwCurrentState;
return (state == SERVICE_RUNNING || state == SERVICE_START_PENDING);
}
// Method Description:
// - Get the size in pixels of the client area we'll need to launch this
// terminal app. This method will use the default profile's settings to do
// this calculation, as well as the _system_ dpi scaling. See also
// TermControl::GetProposedDimensions.
// Arguments:
// - <none>
// Return Value:
// - a point containing the requested dimensions in pixels.
winrt::Windows::Foundation::Size TerminalWindow::GetLaunchDimensions(uint32_t dpi)
{
winrt::Windows::Foundation::Size proposedSize{};
// In focus mode, we don't want to include our own tab row in the size
// of the window that we hand back. So we account for passing
// --focusMode on the commandline here, and the mode in the settings.
// Below, we'll also account for if focus mode was persisted into the
// session for restoration.
bool focusMode = _appArgs && _appArgs->ParsedArgs().GetLaunchMode().value_or(_settings.GlobalSettings().LaunchMode()) == LaunchMode::FocusMode;
const auto scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
if (const auto layout = LoadPersistedLayout())
{
if (layout.LaunchMode())
{
focusMode = layout.LaunchMode().Value() == LaunchMode::FocusMode;
}
if (layout.InitialSize())
{
proposedSize = layout.InitialSize().Value();
// The size is saved as a non-scaled real pixel size,
// so we need to scale it appropriately.
proposedSize.Height = proposedSize.Height * scale;
proposedSize.Width = proposedSize.Width * scale;
}
}
if ((_appArgs && _appArgs->ParsedArgs().GetSize().has_value()) || (proposedSize.Width == 0 && proposedSize.Height == 0))
{
// Use the default profile to determine how big of a window we need.
const auto settings{ Settings::TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr) };
const til::size emptySize{};
const auto commandlineSize = _appArgs ? _appArgs->ParsedArgs().GetSize().value_or(emptySize) : til::size{};
proposedSize = TermControl::GetProposedDimensions(*settings.DefaultSettings(),
dpi,
commandlineSize.width,
commandlineSize.height);
}
if (_contentBounds)
{
// If we've been created as a torn-out window, then we'll need to
// use that size instead. _contentBounds is in DIPs. Scale
// accordingly to the new pixel size.
return {
_contentBounds.Value().Width * scale,
_contentBounds.Value().Height * scale
};
}
// GH#2061 - If the global setting "Always show tab bar" is
// set or if "Show tabs in title bar" is set, then we'll need to add
// the height of the tab bar here.
if (_settings.GlobalSettings().ShowTabsInTitlebar() && !focusMode)
{
// In the past, we used to actually instantiate a TitlebarControl
// and use Measure() to determine the DesiredSize of the control, to
// reserve exactly what we'd need.
//
// We can't do that anymore, because this is now called _before_
// we've initialized XAML for this thread. We can't start XAML till
// we have an HWND, and we can't finish creating the window till we
// know how big it should be.
//
// Instead, we'll just hardcode how big the titlebar should be. If
// the titlebar / tab row ever change size, these numbers will have
// to change accordingly.
static constexpr auto titlebarHeight = 40;
proposedSize.Height += (titlebarHeight)*scale;
}
else if (_settings.GlobalSettings().AlwaysShowTabs() && !focusMode)
{
// Same comment as above, but with a TabRowControl.
//
// A note from before: For whatever reason, there's about 10px of
// unaccounted-for space in the application. I couldn't tell you
// where these 10px are coming from, but they need to be included in
// this math.
static constexpr auto tabRowHeight = 32;
proposedSize.Height += (tabRowHeight + 10) * scale;
}
return proposedSize;
}
// Method Description:
// - Get the launch mode in json settings file. Now there
// two launch mode: default, maximized. Default means the window
// will launch according to the launch dimensions provided. Maximized
// means the window will launch as a maximized window
// Arguments:
// - <none>
// Return Value:
// - LaunchMode enum that indicates the launch mode
LaunchMode TerminalWindow::GetLaunchMode()
{
if (_contentBounds)
{
return LaunchMode::DefaultMode;
}
// GH#4620/#5801 - If the user passed --maximized or --fullscreen on the
// commandline, then use that to override the value from the settings.
const auto valueFromSettings = _settings.GlobalSettings().LaunchMode();
const auto valueFromCommandlineArgs = _appArgs ? _appArgs->ParsedArgs().GetLaunchMode() : std::nullopt;
if (const auto layout = LoadPersistedLayout())
{
if (layout.LaunchMode())
{
return layout.LaunchMode().Value();
}
}
return valueFromCommandlineArgs.has_value() ?
valueFromCommandlineArgs.value() :
valueFromSettings;
}
// Method Description:
// - Get the user defined initial position from Json settings file.
// This position represents the top left corner of the Terminal window.
// This setting is optional, if not provided, we will use the system
// default size, which is provided in IslandWindow::MakeWindow.
// Arguments:
// - defaultInitialX: the system default x coordinate value
// - defaultInitialY: the system default y coordinate value
// Return Value:
// - a point containing the requested initial position in pixels.
TerminalApp::InitialPosition TerminalWindow::GetInitialPosition(int64_t defaultInitialX, int64_t defaultInitialY)
{
auto initialPosition{ _settings.GlobalSettings().InitialPosition() };
if (const auto layout = LoadPersistedLayout())
{
if (layout.InitialPosition())
{
initialPosition = layout.InitialPosition().Value();
}
}
// Commandline args trump everything except for content bounds (tear-out)
if (_appArgs && _appArgs->ParsedArgs().GetPosition().has_value())
{
initialPosition = _appArgs->ParsedArgs().GetPosition().value();
}
if (_contentBounds)
{
// If the user has specified a contentBounds, then we should use
// that to determine the initial position of the window. This is
// used when the user is dragging a tab out of the window, to create
// a new window.
// BODY: Technically we aren't guaranteed to be within the XAML stack right now to have a "current view".
const auto scale = static_cast<float>(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel());
const auto bounds = _contentBounds.Value();
initialPosition = { lroundf(bounds.X * scale), lroundf(bounds.Y * scale) };
}
return {
initialPosition.X ? initialPosition.X.Value() : defaultInitialX,
initialPosition.Y ? initialPosition.Y.Value() : defaultInitialY
};
}
bool TerminalWindow::CenterOnLaunch()
{
// If
// * the position has been specified on the commandline,
// * we're re-opening from a persisted layout,
// * We're opening the window as a part of tear out (and _contentBounds were set)
// then don't center on launch
bool hadPersistedPosition = false;
if (const auto layout = LoadPersistedLayout())
{
hadPersistedPosition = (bool)layout.InitialPosition();
}
return !_contentBounds &&
!hadPersistedPosition &&
_settings.GlobalSettings().CenterOnLaunch() &&
(_appArgs && !_appArgs->ParsedArgs().GetPosition().has_value());
}
// Method Description:
// - See Pane::CalcSnappedDimension
float TerminalWindow::CalcSnappedDimension(const bool widthOrHeight, const float dimension) const
{
return _root->CalcSnappedDimension(widthOrHeight, dimension);
}
// Method Description:
// - Update the current theme of the application. This will trigger our
// RequestedThemeChanged event, to have our host change the theme of the
// root of the application.
// Arguments:
// - newTheme: The ElementTheme to apply to our elements.
void TerminalWindow::_RefreshThemeRoutine()
{
// Propagate the event to the host layer, so it can update its own UI
RequestedThemeChanged.raise(*this, Theme());
}
// This may be called on a background thread, or the main thread, but almost
// definitely not on OUR UI thread.
void TerminalWindow::UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args)
{
_settings = args.NewSettings();
// Update the settings in TerminalPage
// We're on our UI thread right now, so this is safe
_root->SetSettings(_settings, true);
// Bubble the notification up to the AppHost, now that we've updated our _settings.
SettingsChanged.raise(*this, args);
if (FAILED(args.Result()))
{
const winrt::hstring titleKey = USES_RESOURCE(L"ReloadJsonParseErrorTitle");
const winrt::hstring textKey = USES_RESOURCE(L"ReloadJsonParseErrorText");
_ShowLoadErrorsDialog(titleKey,
textKey,
gsl::narrow_cast<HRESULT>(args.Result()),
args.ExceptionText());
return;
}
else if (args.Result() == S_FALSE)
{
_ShowLoadWarningsDialog(nullptr, args.Warnings());
}
else if (args.Result() == S_OK)
{
DismissDialog();
}
_RefreshThemeRoutine();
}
void TerminalWindow::_OpenSettingsUI()
{
_root->OpenSettingsUI();
}
UIElement TerminalWindow::GetRoot() noexcept
{
return _root.as<winrt::Windows::UI::Xaml::Controls::Control>();
}
// Method Description:
// - Gets the title of the currently focused terminal control. If there
// isn't a control selected for any reason, returns "Terminal"
// Arguments:
// - <none>
// Return Value:
// - the title of the focused control if there is one, else "Terminal"
hstring TerminalWindow::Title()
{
if (_root)
{
return _root->Title();
}
return { L"Terminal" };
}
// Method Description:
// - Used to tell the app that the titlebar has been clicked. The App won't
// actually receive any clicks in the titlebar area, so this is a helper
// to clue the app in that a click has happened. The App will use this as
// a indicator that it needs to dismiss any open flyouts.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalWindow::TitlebarClicked()
{
if (_root)
{
_root->TitlebarClicked();
}
}
// Method Description:
// - Used to tell the PTY connection that the window visibility has changed.
// The underlying PTY might need to expose window visibility status to the
// client application for the `::GetConsoleWindow()` API.
// Arguments:
// - showOrHide - True is show; false is hide.
// Return Value:
// - <none>
void TerminalWindow::WindowVisibilityChanged(const bool showOrHide)
{
if (_root)
{
_root->WindowVisibilityChanged(showOrHide);
}
}
// Method Description:
// - Implements the F7 handler (per GH#638)
// - Implements the Alt handler (per GH#6421)
// Return value:
// - whether the key was handled
bool TerminalWindow::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down)
{
if (_root)
{
// Manually bubble the OnDirectKeyEvent event up through the focus tree.
auto xamlRoot{ _root->XamlRoot() };
auto focusedObject{ Windows::UI::Xaml::Input::FocusManager::GetFocusedElement(xamlRoot) };
do
{
if (auto keyListener{ focusedObject.try_as<UI::IDirectKeyListener>() })
{
if (keyListener.OnDirectKeyEvent(vkey, scanCode, down))
{
return true;
}
// otherwise, keep walking. bubble the event manually.
}
if (auto focusedElement{ focusedObject.try_as<Windows::UI::Xaml::FrameworkElement>() })
{
focusedObject = focusedElement.Parent();
// Parent() seems to return null when the focusedElement is created from an ItemTemplate.
// Use the VisualTreeHelper's GetParent as a fallback.
if (!focusedObject)
{
focusedObject = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::GetParent(focusedElement);
// We were unable to find a focused object. Give the
// TerminalPage one last chance to let the alt+space
// menu still work.
//
// We return always, because the TerminalPage handler
// will return false for just a bare `alt` press, and
// don't want to go around the loop again.
if (!focusedObject)
{
if (auto keyListener{ _root.try_as<UI::IDirectKeyListener>() })
{
return keyListener.OnDirectKeyEvent(vkey, scanCode, down);
}
}
}
}
else
{
break; // we hit a non-FE object, stop bubbling.
}
} while (focusedObject);
}
return false;
}
// Method Description:
// - Used to tell the app that the 'X' button has been clicked and
// the user wants to close the app. We kick off the close warning
// experience.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TerminalWindow::CloseWindow()
{
if (_root)
{
_root->CloseWindow();
}
}
winrt::TerminalApp::TaskbarState TerminalWindow::TaskbarState()
{
if (_root)
{
return _root->TaskbarState();
}
return {};
}
winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::TitlebarBrush()
{
return _root ? _root->TitlebarBrush() : nullptr;
}
winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::FrameBrush()
{
return _root ? _root->FrameBrush() : nullptr;
}
void TerminalWindow::WindowActivated(const bool activated)
{
if (_root)
{
_root->WindowActivated(activated);
}
}
bool TerminalWindow::FocusMode() const
{
return _root ? _root->FocusMode() : false;
}
bool TerminalWindow::Fullscreen() const
{
return _root ? _root->Fullscreen() : false;
}
void TerminalWindow::Maximized(bool newMaximized)
{
if (_root)
{
_root->Maximized(newMaximized);
}
}
bool TerminalWindow::AlwaysOnTop() const
{
return _root ? _root->AlwaysOnTop() : false;
}
bool TerminalWindow::ShowTabsFullscreen() const
{
return _root ? _root->ShowTabsFullscreen() : false;
}
void TerminalWindow::SetSettingsStartupArgs(const std::vector<ActionAndArgs>& actions)
{
for (const auto& action : actions)
{
_settingsStartupArgs.push_back(action);
}
_gotSettingsStartupActions = true;
}
bool TerminalWindow::HasCommandlineArguments() const noexcept
{
return _hasCommandLineArguments;
}
// Method Description:
// - Sets the initial commandline to process on startup, and attempts to
// parse it. Commands will be parsed into a list of ShortcutActions that
// will be processed on TerminalPage::Create().
// - This function will have no effective result after Create() is called.
// - This function returns 0, unless a there was a non-zero result from
// trying to parse one of the commands provided. In that case, no commands
// after the failing command will be parsed, and the non-zero code
// returned.
// Arguments:
// - args: an array of strings to process as a commandline. These args can contain spaces
// - cwd: The CWD that this window should treat as its own "virtual" CWD
// Return Value:
// - the result of the first command who's parsing returned a non-zero code,
// or 0. (see TerminalWindow::_ParseArgs)
int32_t TerminalWindow::SetStartupCommandline(TerminalApp::CommandlineArgs args)
{
_appArgs = winrt::get_self<CommandlineArgs>(args);
_startupConnection = args.Connection();
auto& parsedArgs = _appArgs->ParsedArgs();
_WindowProperties->SetInitialCwd(_appArgs->CurrentDirectory());
_WindowProperties->VirtualEnvVars(_appArgs->CurrentEnvironment());
// This is called in AppHost::ctor(), before we've created the window
// (or called TerminalWindow::Initialize)
if (_appArgs->ExitCode() == 0)
{
// The existing logic (before this commit) strictly relied on
// ValidateStartupCommands() only to be called for new windows.
// It modifies the actions it stores.
parsedArgs.ValidateStartupCommands();
// If the size of the arguments list is 1,
// then it contains only the executable name and no other arguments.
_hasCommandLineArguments = _appArgs->CommandlineRef().size() > 1;
// DON'T pass the args into the page yet. It doesn't exist yet.
// Instead, we'll handle that in Initialize, when we first instantiate the page.
}
// If we have a -s param passed to us to load a saved layout, cache that now.
if (const auto idx = parsedArgs.GetPersistedLayoutIdx())
{
SetPersistedLayoutIdx(idx.value());
}
return _appArgs->ExitCode();
}
void TerminalWindow::SetStartupContent(const winrt::hstring& content,
const Windows::Foundation::IReference<Windows::Foundation::Rect>& bounds)
{
_contentBounds = bounds;
const auto args = _contentStringToActions(content, true);
_initialContentArgs = wil::to_vector(args);
}
// Method Description:
// - Parse the provided commandline arguments into actions, and try to
// perform them immediately.
// - This function returns 0, unless a there was a non-zero result from
// trying to parse one of the commands provided. In that case, no commands
// after the failing command will be parsed, and the non-zero code
// returned.
// - If a non-empty cwd is provided, the entire terminal exe will switch to
// that CWD while we handle these actions, then return to the original
// CWD.
// Arguments:
// - args: an array of strings to process as a commandline. These args can contain spaces
// - cwd: The directory to use as the CWD while performing these actions.
// Return Value:
// - the result of the first command who's parsing returned a non-zero code,
// or 0. (see TerminalWindow::_ParseArgs)
int32_t TerminalWindow::ExecuteCommandline(TerminalApp::CommandlineArgs args)
{
_appArgs = winrt::get_self<CommandlineArgs>(args);
if (_appArgs->ExitCode() == 0)
{
auto& parsedArgs = _appArgs->ParsedArgs();
auto& actions = parsedArgs.GetStartupActions();
if (auto conn = args.Connection())
{
_root->CreateTabFromConnection(std::move(conn));
}
else if (!actions.empty())
{
_root->ProcessStartupActions(actions, _appArgs->CurrentDirectory(), _appArgs->CurrentEnvironment());
}
}
// Return the result of parsing with commandline, though it may or may not be used.
return _appArgs->ExitCode();
}
void TerminalWindow::SetPersistedLayoutIdx(const uint32_t idx)
{
_loadFromPersistedLayoutIdx = idx;
_cachedLayout = std::nullopt;
}
// Method Description;
// - Checks if the current window is configured to load a particular layout
// Arguments:
// - settings: The settings to use as this may be called before the page is
// fully initialized.
// Return Value:
// - non-null if there is a particular saved layout to use
std::optional<uint32_t> TerminalWindow::LoadPersistedLayoutIdx() const
{
return _settings.GlobalSettings().ShouldUsePersistedLayout() ? _loadFromPersistedLayoutIdx : std::nullopt;
}
WindowLayout TerminalWindow::LoadPersistedLayout()
{
if (_cachedLayout.has_value())
{
return *_cachedLayout;
}
if (const auto idx = LoadPersistedLayoutIdx())
{
const auto i = idx.value();
const auto layouts = ApplicationState::SharedInstance().PersistedWindowLayouts();
if (layouts && layouts.Size() > i)
{
auto layout = layouts.GetAt(i);
// TODO: GH#12633: Right now, we're manually making sure that we
// have at least one tab to restore. If we ever want to come
// back and make it so that you can persist position and size,
// but not the tabs themselves, we can revisit this assumption.
_cachedLayout = (layout.TabLayout() && layout.TabLayout().Size() > 0) ? layout : nullptr;
return *_cachedLayout;
}
}
_cachedLayout = nullptr;
return *_cachedLayout;
}
void TerminalWindow::RequestExitFullscreen()
{
_root->SetFullscreen(false);
}
bool TerminalWindow::AutoHideWindow()
{
return _settings.GlobalSettings().AutoHideWindow();
}
void TerminalWindow::UpdateSettingsHandler(const winrt::IInspectable& /*sender*/,
const winrt::TerminalApp::SettingsLoadEventArgs& args)
{
UpdateSettings(args);
}
void TerminalWindow::IdentifyWindow()
{
if (_root)
{
_root->IdentifyWindow();
}
}
void TerminalWindow::WindowName(const winrt::hstring& name)
{
const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow();
_WindowProperties->WindowName(name);
if (!_root)
{
return;
}
const auto newIsQuakeMode = _WindowProperties->IsQuakeWindow();
if (newIsQuakeMode != oldIsQuakeMode)
{
// If we're entering Quake Mode from ~Focus Mode, then this will enter Focus Mode
// If we're entering Quake Mode from Focus Mode, then this will do nothing
// If we're leaving Quake Mode (we're already in Focus Mode), then this will do nothing
_root->SetFocusMode(true);
IsQuakeWindowChanged.raise(*this, nullptr);
}
}
void TerminalWindow::WindowId(const uint64_t& id)
{
_WindowProperties->WindowId(id);
}
// Method Description:
// - Deserialize this string of content into a list of actions to perform.
// If replaceFirstWithNewTab is true and the first serialized action is a
// `splitPane` action, we'll attempt to replace that action with the
// equivalent `newTab` action.
IVector<Settings::Model::ActionAndArgs> TerminalWindow::_contentStringToActions(const winrt::hstring& content,
const bool replaceFirstWithNewTab)
{
try
{
const auto args = ActionAndArgs::Deserialize(content);
if (args == nullptr ||
args.Size() == 0)
{
return args;
}
const auto& firstAction = args.GetAt(0);
const bool firstIsSplitPane{ firstAction.Action() == ShortcutAction::SplitPane };
if (replaceFirstWithNewTab &&
firstIsSplitPane)
{
// Create the equivalent NewTab action.
const auto newAction = Settings::Model::ActionAndArgs{ Settings::Model::ShortcutAction::NewTab,
Settings::Model::NewTabArgs(firstAction.Args() ?
firstAction.Args().try_as<Settings::Model::SplitPaneArgs>().ContentArgs() :
nullptr) };
args.SetAt(0, newAction);
}
return args;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
}
return nullptr;
}
void TerminalWindow::AttachContent(winrt::hstring content, uint32_t tabIndex)
{
if (_root)
{
// `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.
const bool replaceFirstWithNewTab = tabIndex >= _root->NumberOfTabs();
auto args = _contentStringToActions(content, replaceFirstWithNewTab);
_root->AttachContent(std::move(args), tabIndex);
}
}
void TerminalWindow::SendContentToOther(winrt::TerminalApp::RequestReceiveContentArgs args)
{
if (_root)
{
_root->SendContentToOther(args);
}
}
bool TerminalWindow::ShouldImmediatelyHandoffToElevated()
{
return _root != nullptr ? _root->ShouldImmediatelyHandoffToElevated(_settings) : false;
}
// 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:
// - <none>
void TerminalWindow::HandoffToElevated()
{
if (_root)
{
_root->HandoffToElevated(_settings);
return;
}
}
void TerminalWindow::_WindowSizeChanged(const IInspectable&, winrt::Microsoft::Terminal::Control::WindowSizeChangedEventArgs args)
{
winrt::Windows::Foundation::Size pixelSize = { static_cast<float>(args.Width()), static_cast<float>(args.Height()) };
const auto scale = static_cast<float>(DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel());
if (!FocusMode())
{
if (!_settings.GlobalSettings().AlwaysShowTabs())
{
// Hide the title bar = off, Always show tabs = off.
static constexpr auto titlebarHeight = 10;
pixelSize.Height += (titlebarHeight)*scale;
}
else if (!_settings.GlobalSettings().ShowTabsInTitlebar())
{
// Hide the title bar = off, Always show tabs = on.
static constexpr auto titlebarAndTabBarHeight = 40;
pixelSize.Height += (titlebarAndTabBarHeight)*scale;
}
// Hide the title bar = on, Always show tabs = on.
// In this case, we don't add any height because
// NonClientIslandWindow::GetTotalNonClientExclusiveSize() gets
// called in AppHost::_resizeWindow and it already takes title bar
// height into account. In other cases above
// IslandWindow::GetTotalNonClientExclusiveSize() is called, and it
// doesn't take the title bar height into account, so we have to do
// the calculation manually.
}
args.Width(static_cast<int32_t>(pixelSize.Width));
args.Height(static_cast<int32_t>(pixelSize.Height));
WindowSizeChanged.raise(*this, args);
}
void TerminalWindow::_RenameWindowRequested(const IInspectable&, const winrt::TerminalApp::RenameWindowRequestedArgs args)
{
WindowName(args.ProposedName());
}
winrt::hstring WindowProperties::WindowName() const noexcept
{
return _WindowName;
}
void WindowProperties::WindowName(const winrt::hstring& value)
{
if (_WindowName != value)
{
_WindowName = value;
// If we get initialized with a window name, this will be called
// before XAML is stood up, and constructing a
// PropertyChangedEventArgs will throw.
try
{
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowName" });
PropertyChanged.raise(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" });
}
CATCH_LOG();
}
}
uint64_t WindowProperties::WindowId() const noexcept
{
return _WindowId;
}
void WindowProperties::WindowId(const uint64_t& value)
{
_WindowId = value;
}
// Method Description:
// - Returns a label like "Window: 1234" for the ID of this window
// Arguments:
// - <none>
// Return Value:
// - a string for displaying the name of the window.
winrt::hstring WindowProperties::WindowIdForDisplay() const noexcept
{
return winrt::hstring{ fmt::format(FMT_COMPILE(L"{}: {}"),
std::wstring_view(RS_(L"WindowIdLabel")),
_WindowId) };
}
// Method Description:
// - Returns a label like "<unnamed window>" when the window has no name, or the name of the window.
// Arguments:
// - <none>
// Return Value:
// - a string for displaying the name of the window.
winrt::hstring WindowProperties::WindowNameForDisplay() const noexcept
{
return _WindowName.empty() ?
winrt::hstring{ fmt::format(FMT_COMPILE(L"<{}>"), RS_(L"UnnamedWindowName")) } :
_WindowName;
}
bool WindowProperties::IsQuakeWindow() const noexcept
{
return _WindowName == L"_quake";
}
};