terminal/src/cascadia/TerminalApp/TerminalWindow.cpp
Mike Griese b9248fa903
One process to rule them all (#14843)
## Summary

_In the days of old, the windows were sundered, each with its own
process, like the scattered stars in the sky. But a new age hath dawned,
for all windows now reside within a single process, like the bright gems
of a single crown._
_And lo, there came the `WindowEmperor`, a new lord to rule over the
global state, wielding the power of hotkeys and the beacon of the
notification icon. The `WindowManager` was cast aside, no longer needed
to seek out other processes or determine the `Monarch`._
_Should the `WindowEmperor` determine that a new window shall be raised,
it shall set forth a new thread, born from the ether, to govern this new
realm. On the main thread shall reside a message loop, charged with the
weighty task of preserving the global state, guarded by hotkeys and the
beacon of the notification icon._
_Each window doth live on its own thread, each guarded by the new
`WindowThread`, a knightly champion to hold the `TerminalWindow`,
`AppHost`, and `IslandWindow` in its grasp. And so the windows shall run
free, no longer burdened by their former ways._


All windows are now in a single process, rather than in one process per
window. We'll add a new `WindowEmperor` class to manage global state
such as hotkeys and the notification icon. The `WindowManager` has been
streamlined and no longer needs to connect to other processes or
determine a new `Monarch`. Each window will run on its own thread, using
the new `WindowThread` class to encapsulate the thread and manage the
`TerminalWindow`, `AppHost`, and `IslandWindow`.


* Related to #5000
* Related to #1256

## Windows Terminal Process Model 3.0

Everything is now one process. All the windows for a single Terminal
instance live in a singular Terminal process. When a new terminal
process launches, it will still attempt to communicate with an existing
one. If it finds one, it'll pass the commandline to that process and
exit. Otherwise, it'll become the "monarch" and create a new window.

We'll introduce a new abstraction here, called the `WindowEmperor`.
`Monarch` & `Peasant` will still remain, for facilitating cross-window
communication. The Emperor will allow us to have a single dedicated
class for all global state, and will now always represent the "monarch"
(rather than our previously established non-deterministic monarchy to
elevate a random peasant to the role of monarch). We still need to do a
very minimal amount of x-proc calls. Namely, one right on startup, to
see if another `Terminal.exe` was already running. If we find one, then
we toss our commandline at it and bail. If we don't, then we need to
`CoRegister` the Monarch still, to prepare for subsequent launches to
send commands to us.

`WindowManager` takes the most changes here. It had a ton of logic to
redundantly attempt to connect to other monarchs of other processes, or
elect a new one. It doesn't need to do any of that anymore, which is a
pretty dramatic change to that class.

This creates the opportunity to move some lifetime management around.
We've played silly games in the past trying to have individual windows
determine if they're the singular monarch for global state.
`IslandWindow`s no longer need to track things like global hotkeys or
the notification icon. The emperor can do that - there's only ever one
emperor. It can also own a singular copy of the settings model, and hand
out references to each other thread.

Each window lives on separate threads. We'll need to separately
initialize XAML islands for each thread. This is totally fine, and
actually supported these days. We'll use a new class called
`WindowThread` to encapsulate one of these threads. It'll be responsible
for owning the `TerminalWindow`, `AppHost` and `IslandWindow` for a
single thread.

This introduces new classes of bugs we'll need to worry about. It's now
easier than ever to have "wrong thread" bugs when interacting with any
XAML object from another thread. A good case in point - we used to stash
a `static` `Brush` in `Pane`, for the color of the borders. We can't do
that anymore! The first window will end up stashing a brush from its
thread. So now when a second window starts, the app explodes, because
the panes of that window try to draw their borders using a brush from
the wrong thread.

_Another fun change_: The keybinding labels of the command palette.
`TerminalPage` is the thing that ends up expanding iterable `Command`s.
It does this largely with copies - it makes a new `map`, a new `vector`,
copies the `Command`s over, and does the work there before setting up
the cmdpal.
Except, it's not making a copy of the `Command`s, it's making a copy of
the `vector`, with winrt objects all pointing at the `Command` objects
that are ultimately owned by `CascadiaSettings`.
This doesn't matter if there's only one `TerminalPage` - we'll only ever
do that once. However, now there are many Pages, on different threads.
That causes one `TerminalPage` to end up expanding the subcommands of a
`Command` while another `TerminalPage` is ALSO iterating on those
subcommands.

_Emperor message window_: The Emperor will have its own HWND, that's
entirely unrelated to any terminal window. This window is a
`HWND_MESSAGE` window, which specifically cannot be visible, but is
useful for getting messages. We'll use that to handle the notification
icon and global hotkeys. This alleviates the need for the IslandWindow
to raise events for the tray icon up to the AppHost to handle them. Less
plumbing=more good.

### Class ownership diagram

_pretend that I know UML for a second_:

```mermaid
classDiagram
    direction LR
    class Monarch
    class Peasant
    class Emperor
    class WindowThread
    class AppHost

    Monarch "1" --o "*" Peasant: Tracks
    Emperor --* "1" AppLogic: 
    Monarch <..> "1" Emperor
    Peasant "1" .. "1" WindowThread
    Emperor "1" --o "*" WindowThread: Tracks
    WindowThread --* AppHost
    AppHost --* IslandWindow
    AppHost --* TerminalWindow
    TerminalWindow --* TerminalPage
```

* There's still only one `Monarch`. One for the Terminal process.
* There's still many `Peasant`s, one per window.
* The `Monarch` is no longer associated with a window. It's associated
with the `Emperor`, who maintains _all_ the Terminal windows (but is not
associated with any particular window)
* It may be relevant to note: As far as the `Remoting` dll is concerned,
it doesn't care if monarchs and peasants are associated with windows or
not. Prior to this PR, _yes_, the Monarch was in fact associated with a
specific window (which was also associated with a window). Now, the
monarch is associated with the Emperor, who isn't technically any of the
windows.
* The `Emperor` owns the `App` (and by extension, the single `AppLogic`
instance).
* Each Terminal window lives on its own thread, owed by a `WindowThread`
object.
* There's still one `AppHost`, one `IslandWindow`, one `TerminalWindow`
& `TerminalPage` per window.
* `AppLogic` hands out references to its settings to each
`TerminalWindow` as they're created.

### Isolated Mode

This was a bit of a tiny brainstorm Dustin and I discussed. This is a
new setting introduced as an escape watch from the "one process to rule
them all" model. Technically, the Terminal already did something like
this if it couldn't find a `Monarch`, though, we were never really sure
if that hit. This just adds a setting to manually enable this mode.

In isolated mode, we always instantiate a Monarch instance locally,
without attempting to use the `CoRegister`-ed one, and we _never_
register one. This prevents the Terminal from talking with other
windows.
* Global hotkeys won't work right
* Trying to run commandlines in other windows (`wt -w foo`) won't work
* Every window will be its own process again
* Tray icon behavior is left undefined for now.
* Tab tearout straight-up won't work.

### A diagram about settings

This helps explain how settings changes get propagated

```mermaid
sequenceDiagram
    participant Emperor
    participant AppLogic
    
    participant AppHost
    participant TerminalWindow
    participant TerminalPage

    Note Right of AppLogic: AL::ReloadSettings
    AppLogic ->> Emperor: raise SettingsChanged
    Note left of Emperor: E::...GlobalHotkeys
    Note left of Emperor: E::...NotificationIcon
    AppLogic ->> TerminalWindow: raise SettingsChanged<br>(to each window)
    AppLogic ->> TerminalWindow: 
    AppLogic ->> TerminalWindow: 
    Note right of TerminalWindow: TW::UpdateSettingsHandler
    Note right of TerminalWindow: TW::UpdateSettings
    TerminalWindow ->> TerminalPage: SetSettings
    TerminalWindow ->> AppHost: raise SettingsChanged
    Note right of AppHost: AH::_HandleSettingsChanged
```
2023-03-17 17:59:35 -05:00

1257 lines
49 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "TerminalWindow.h"
#include "../inc/WindowingBehavior.h"
#include "TerminalWindow.g.cpp"
#include "SettingsLoadEventArgs.g.cpp"
#include "WindowProperties.g.cpp"
#include <LibraryResources.h>
#include <WtExeUtils.h>
#include <wil/token_helpers.h>
#include "../../types/inc/utils.hpp"
using namespace winrt::Windows::ApplicationModel;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
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 ::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"InvalidBackgroundImage"),
USES_RESOURCE(L"InvalidIcon"),
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"FailedToParseSubCommands"),
USES_RESOURCE(L"UnknownTheme"),
USES_RESOURCE(L"DuplicateRemainingProfilesEntry"),
};
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) :
_settings{ settingsLoadedResult.NewSettings() },
_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);
_dialog = ContentDialog{};
// Pass commandline args into the TerminalPage. If we were supposed to
// load from a persisted layout, do that instead.
// 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
if (const auto& layout = LoadPersistedLayout())
{
std::vector<Settings::Model::ActionAndArgs> actions;
for (const auto& a : layout.TabLayout())
{
actions.emplace_back(a);
}
_root->SetStartupActions(actions);
}
else
{
_root->SetStartupActions(_appArgs.GetStartupActions());
}
// Check if we were started as a COM server for inbound connections of console sessions
// coming out of the operating system default application feature. If so,
// tell TerminalPage to start the listener as we have to make sure it has the chance
// to register a handler to hear about the requests first and is all ready to receive
// them before the COM server registers itself. Otherwise, the request might come
// in and be routed to an event with no handlers or a non-ready Page.
if (_appArgs.IsHandoffListener())
{
_root->SetInboundListener(true);
}
return _root->Initialize(hwnd);
}
// Method Description:
// - Called around the codebase to discover if this is a UWP where we need to turn off specific settings.
// Arguments:
// - <none> - reports internal state
// Return Value:
// - True if UWP, false otherwise.
bool TerminalWindow::IsUwp() const noexcept
{
// use C++11 magic statics to make sure we only do this once.
// This won't change over the lifetime of the application
static const auto isUwp = []() {
// *** THIS IS A SINGLETON ***
auto result = false;
// 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
{
result = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp();
}
CATCH_LOG();
return result;
}();
return isUwp;
}
// 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.
if (!_hasCommandLineArguments && _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([this](auto&&, auto&&) {
// 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);
}
});
_root->Create();
_RefreshThemeRoutine();
auto args = winrt::make_self<SystemMenuChangeArgs>(RS_(L"SettingsMenuItem"),
SystemMenuChangeAction::Add,
SystemMenuItemHandler(this, &TerminalWindow::_OpenSettingsUI));
_SystemMenuChangeRequestedHandlers(*this, *args);
TraceLoggingWrite(
g_hTerminalAppProvider,
"WindowCreated",
TraceLoggingDescription("Event emitted when the window is started"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}
void TerminalWindow::Quit()
{
if (_root)
{
_root->CloseWindow(true);
}
}
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::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();
}
// 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 an 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)
{
// DON'T release this lock in a wil::scope_exit. The scope_exit will get
// called when we await, which is not what we want.
std::unique_lock lock{ _dialogLock, std::try_to_lock };
if (!lock)
{
// Another dialog is visible.
co_return ContentDialogResult::None;
}
_dialog = 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>();
}
} };
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
// Display the dialog.
co_return co_await dialog.ShowAsync(Controls::ContentDialogPlacement::Popup);
// After the dialog is dismissed, the dialog lock (held by `lock`) will
// be released so another can be shown
}
// Method Description:
// - Dismiss the (only) visible ContentDialog
void TerminalWindow::DismissDialog()
{
if (auto localDialog = std::exchange(_dialog, nullptr))
{
localDialog.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;
// 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 Windows::Foundation::Collections::IVector<SettingsLoadWarnings>& warnings)
{
auto title = RS_(L"SettingsValidateErrorTitle");
auto buttonText = RS_(L"Ok");
Controls::TextBlock warningsTextBlock;
// 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(_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 (IsUwp())
{
return true;
}
// 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{};
const auto scale = static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
if (const auto layout = LoadPersistedLayout())
{
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.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{ TerminalSettings::CreateWithNewTerminalArgs(_settings, nullptr, nullptr) };
const til::size emptySize{};
const auto commandlineSize = _appArgs.GetSize().value_or(emptySize);
proposedSize = TermControl::GetProposedDimensions(settings.DefaultSettings(),
dpi,
commandlineSize.width,
commandlineSize.height);
}
// 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())
{
// 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())
{
// 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()
{
// 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.GetLaunchMode();
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 else
if (_appArgs.GetPosition().has_value())
{
initialPosition = _appArgs.GetPosition().value();
}
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, don't center on launch
return _settings.GlobalSettings().CenterOnLaunch() && !_appArgs.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
_RequestedThemeChangedHandlers(*this, Theme());
}
// This may be called on a background thread, or the main thread, but almost
// definitely not on OUR UI thread.
winrt::fire_and_forget TerminalWindow::UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args)
{
_settings = args.NewSettings();
const auto weakThis{ get_weak() };
co_await wil::resume_foreground(_root->Dispatcher());
// Back on our UI thread...
if (auto logic{ weakThis.get() })
{
// 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.
_SettingsChangedHandlers(*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());
co_return;
}
else if (args.Result() == S_FALSE)
{
_ShowLoadWarningsDialog(args.Warnings());
}
_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<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);
}
}
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(LaunchPosition pos, const bool isLastWindow)
{
if (_root)
{
// If persisted layout is enabled and we are the last window closing
// we should save our state.
if (_settings.GlobalSettings().ShouldUsePersistedLayout() && isLastWindow)
{
if (const auto layout = _root->GetWindowLayout())
{
layout.InitialPosition(pos);
const auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(winrt::single_threaded_vector<WindowLayout>({ layout }));
}
}
_root->CloseWindow(false);
}
}
void TerminalWindow::ClearPersistedWindowState()
{
if (_settings.GlobalSettings().ShouldUsePersistedLayout())
{
auto state = ApplicationState::SharedInstance();
state.PersistedWindowLayouts(nullptr);
}
}
winrt::TerminalApp::TaskbarState TerminalWindow::TaskbarState()
{
if (_root)
{
return _root->TaskbarState();
}
return {};
}
winrt::Windows::UI::Xaml::Media::Brush TerminalWindow::TitlebarBrush()
{
if (_root)
{
return _root->TitlebarBrush();
}
return { nullptr };
}
void TerminalWindow::WindowActivated(const bool activated)
{
if (_root)
{
_root->WindowActivated(activated);
}
}
// Method Description:
// - Returns true if we should exit the application before even starting the
// window. We might want to do this if we're displaying an error message or
// the version string, or if we want to open the settings file.
// Arguments:
// - <none>
// Return Value:
// - true iff we should exit the application before even starting the window
bool TerminalWindow::ShouldExitEarly()
{
return _appArgs.ShouldExitEarly();
}
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;
}
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
// 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(array_view<const winrt::hstring> args)
{
// This is called in AppHost::ctor(), before we've created the window
// (or called TerminalWindow::Initialize)
const auto result = _appArgs.ParseArgs(args);
if (result == 0)
{
// If the size of the arguments list is 1,
// then it contains only the executable name and no other arguments.
_hasCommandLineArguments = args.size() > 1;
_appArgs.ValidateStartupCommands();
// 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 = _appArgs.GetPersistedLayoutIdx())
{
SetPersistedLayoutIdx(idx.value());
}
return result;
}
// 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(array_view<const winrt::hstring> args,
const winrt::hstring& cwd)
{
::TerminalApp::AppCommandlineArgs appArgs;
auto result = appArgs.ParseArgs(args);
if (result == 0)
{
auto actions = winrt::single_threaded_vector<ActionAndArgs>(std::move(appArgs.GetStartupActions()));
_root->ProcessStartupActions(actions, false, cwd);
if (appArgs.IsHandoffListener())
{
_root->SetInboundListener(true);
}
}
// Return the result of parsing with commandline, though it may or may not be used.
return result;
}
// Method Description:
// - If there were any errors parsing the commandline that was used to
// initialize the terminal, this will return a string containing that
// message. If there were no errors, this message will be blank.
// - If the user requested help on any command (using --help), this will
// contain the help message.
// - If the user requested the version number (using --version), this will
// contain the version string.
// Arguments:
// - <none>
// Return Value:
// - the help text or error message for the provided commandline, if one
// exists, otherwise the empty string.
winrt::hstring TerminalWindow::ParseCommandlineMessage()
{
return winrt::to_hstring(_appArgs.GetExitMessage());
}
hstring TerminalWindow::GetWindowLayoutJson(LaunchPosition position)
{
if (_root != nullptr)
{
if (const auto layout = _root->GetWindowLayout())
{
layout.InitialPosition(position);
return WindowLayout::ToJson(layout);
}
}
return L"";
}
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::RenameFailed()
{
if (_root)
{
_root->RenameFailed();
}
}
void TerminalWindow::WindowName(const winrt::hstring& name)
{
const auto oldIsQuakeMode = _WindowProperties->IsQuakeWindow();
_WindowProperties->WindowName(name);
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);
_IsQuakeWindowChangedHandlers(*this, nullptr);
}
}
void TerminalWindow::WindowId(const uint64_t& id)
{
_WindowProperties->WindowId(id);
}
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;
}
}
winrt::hstring WindowProperties::WindowName() const noexcept
{
return _WindowName;
}
void WindowProperties::WindowName(const winrt::hstring& value)
{
if (_WindowName != value)
{
_WindowName = value;
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowName" });
_PropertyChangedHandlers(*this, Windows::UI::Xaml::Data::PropertyChangedEventArgs{ L"WindowNameForDisplay" });
}
}
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(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(L"<{}>", RS_(L"UnnamedWindowName")) } :
_WindowName;
}
bool WindowProperties::IsQuakeWindow() const noexcept
{
return _WindowName == QuakeWindowName;
}
};