terminal/src/cascadia/TerminalApp/App.cpp
Mike Griese 8ba8f35dc5
Add Cascading User + Default Settings (#2515)
This PR represents the start of the work on Cascading User + default settings, #754.

Cascading settings will be done in two parts: 
* [ ] Layered Default+User settings (this PR)
* [ ] Dynamic Profile Generation (#2603).

Until _both_ are done, _neither are going in. The dynamic profiles PR will target this PR when it's ready, but will go in as a separate commit into master.

This PR covers adding one primary feature: the settings are now in two separate files:
* a static `defaults.json` that ships with the package (the "default settings")
* a `profiles.json` with the user's customizations (the "user settings)

User settings are _layered_ upon the settings in the defaults settings.

## References

Other things that might be related here:
* #1378 - This seems like it's definitely fixed. The default keybindings are _much_ cleaner, and without the save-on-load behavior, the user's keybindings will be left in a good state 
* #1398 - This might have honestly been solved by #2475 

## PR Checklist
* [x] Closes #754
* [x] Closes #1378 
* [x] Closes #2566
* [x] I work here
* [x] Tests added/passed
* [x] Requires documentation to be updated - it **ABSOLUTELY DOES**


## Detailed Description of the Pull Request / Additional comments

1. We start by taking all of the `FromJson` functions in Profile, ColorScheme, Globals, etc, and converting them to `LayerJson` methods. These are effectively the same, with the change that instead of building a new object, they are simply layering the values on top of `this` object. 
2. Next, we add tests for layering properties like that.
3. Now, we add a `defaults.json` to the package. This is the file the users can refer to as our default settings.
4. We then take that `defaults.json` and stamp it into an auto generated `.h` file, so we can use it's data without having to worry about reading it from disk.
5. We then change the `LoadAll` function in `CascadiaSettings`. Now, the function does two loads - one from the defaults, and then a second load from the `profiles.json` file, layering the settings from each source upon the previous values.
6. If the `profiles.json` file doesn't exist, we'll create it from a hardcoded `userDefaults.json`, which is stamped in similar to how `defaults.json` is.
7. We also add support for _unbinding_ keybindings that might exist in the `defaults.json`, but the user doesn't want to be bound to anything.
8. We add support for _hiding_ a profile, which is useful if a user doesn't want one of the default profiles to appear in the list of profiles.

## TODO:
* [x] Still need to make Alt+Click work on the settings button
* [x] Need to write some user documentation on how the new settings model works
* [x] Fix the pair of tests I broke (re: Duplicate profiles)


<hr>

* Create profiles by layering them

* Update test to layer multiple times on the same profile

* Add support for layering an array of profiles, but break a couple tests

* Add a defaults.json to the package

* Layer colorschemes

  * Moves tests into individual classes
  * adds support for layering a colorscheme on top of another

* Layer an array of color schemes

* oh no, this was missed with #2481

  must have committed without staging this change, uh oh. Not like those tests actually work so nbd

* Layer keybindings

* Read settings from defaults.json + profiles.json, layer appropriately

  This is like 80% of #754. Needs tests.

* Add tests for keybindings

  * add support to unbind a key with `null` or `"unbound"` or `"garbage"`

* Layer or clear optional properties

* Add a helper to get an optional variable for a bunch of different types

  In the end, I think we need to ask _was this worth it_

* Do this with the stretch mode too

* Add back in the GUID check for profiles

* Add some tests for global settings layering

* M A D  W I T H  P O W E R

  Add a MsBuild target to auto-generate a header with the defaults.json as a
  string in the file. That way, we can _always_ load the defaults. Literally impossible to not.

* When the user's profile.json doesn't exist, create it from a template

* Re-order profiles to match the order set in the user's profiles.json

* Add tests for re-ordering profiles to match user ordering

* Add support for hiding profiles using `"hidden": true`

* Use the hardcoded defaults.json for the exception->"use defaults" case

* Somehow I messed up the git submodules?

* woo documentation

* Fix a Terminal.App.Unit.Tests failure

* signed/unsigned is hard

* Use Alt+Settings button to open the default settings

* Missed a signed/unsigned

* Some very preliminary PR feedback

* More PR feedback

  Use the wil helper for the exe path
  Move jsonutils into their own file
  kill some dead code

* Add templates to these bois

* remove some code for generating defaults, reorder defaults.json a tad

* Make guid a std::optional

* Large block of PR feedback

  * Remove some dead code
  * add some comments
  * tag some todos

* stl is love, stl is life

* add `-noprofile`

* Fix the crash that dustin found

* -Encoding ASCII

* Set a profile's default scheme to Campbell

* Fix the tests I regressed

* Update UsingJsonSetting.md to reflect that changes from these PRs

* Change how GenerateGuidForProfile works

* Make AppKeyBindings do its own serialization

* Remove leftover dead code from the previous commit

* Fix up an enormous number of PR nits

* Fix a typo; Update the defaults to match #2378

* Tiny nits

* Some typos, PR nits

* Fix this broken defaults case
2019-09-16 12:57:10 -07:00

614 lines
24 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "App.h"
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>
#include "App.g.cpp"
using namespace winrt::Windows::ApplicationModel::DataTransfer;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Core;
using namespace winrt::Windows::System;
using namespace winrt::Microsoft::Terminal;
using namespace winrt::Microsoft::Terminal::Settings;
using namespace winrt::Microsoft::Terminal::TerminalControl;
using namespace ::TerminalApp;
namespace winrt
{
namespace MUX = Microsoft::UI::Xaml;
using IInspectable = Windows::Foundation::IInspectable;
}
// clang-format off
// !!! IMPORTANT !!!
// Make sure that these keys are in the same order as the
// SettingsLoadWarnings/Errors enum is!
static const std::array<std::wstring_view, 2> settingsLoadWarningsLabels {
L"MissingDefaultProfileText",
L"DuplicateProfileText"
};
static const std::array<std::wstring_view, 1> settingsLoadErrorsLabels {
L"NoProfilesText"
};
// clang-format on
// 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.
// - loader: the ScopedResourceLoader to use to look up the localized string.
// Return Value:
// - the localized string for the given type, if it exists.
template<std::size_t N>
static winrt::hstring _GetMessageText(uint32_t index, std::array<std::wstring_view, N> keys, ScopedResourceLoader& loader)
{
if (index < keys.size())
{
return loader.GetLocalizedString(keys.at(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.
// - loader: the ScopedResourceLoader to use to look up the localized string.
// Return Value:
// - localized text for the given warning
static winrt::hstring _GetWarningText(::TerminalApp::SettingsLoadWarnings warning, ScopedResourceLoader& loader)
{
return _GetMessageText(static_cast<uint32_t>(warning), settingsLoadWarningsLabels, loader);
}
// Function Description:
// - Gets the text from our ResourceDictionary for the given
// SettingsLoadError. If there is no such text, we'll return nullptr.
// - The warning should have an entry in settingsLoadErrorsLabels.
// Arguments:
// - error: the SettingsLoadErrors value to get the localized text for.
// - loader: the ScopedResourceLoader to use to look up the localized string.
// Return Value:
// - localized text for the given error
static winrt::hstring _GetErrorText(::TerminalApp::SettingsLoadErrors error, ScopedResourceLoader& loader)
{
return _GetMessageText(static_cast<uint32_t>(error), settingsLoadErrorsLabels, loader);
}
// 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
winrt::IInspectable key = winrt::box_value(L"ErrorTextBrush");
if (resources.HasKey(key))
{
winrt::IInspectable g = resources.Lookup(key);
auto brush = g.try_as<winrt::Windows::UI::Xaml::Media::Brush>();
textRun.Foreground(brush);
}
return textRun;
}
namespace winrt::TerminalApp::implementation
{
App::App() :
_dialogLock{},
_loadedInitialSettings{ false },
_settingsLoadedResult{ S_OK }
{
// 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.
// Initialize will become protected or be deleted when GH#1339 (workaround for MSFT:22116519) are fixed.
Initialize();
_resourceLoader = std::make_shared<ScopedResourceLoader>(L"TerminalApp/Resources");
// The TerminalPage has to be constructed during our construction, to
// make sure that there's a terminal page for callers of
// SetTitleBarContent
_root = winrt::make_self<TerminalPage>(_resourceLoader);
}
// 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 App::Create()
{
// Assert that we've already loaded our settings. We have to do
// this as a MTA, before the app is Create()'d
WINRT_ASSERT(_loadedInitialSettings);
_root->ShowDialog({ this, &App::_ShowDialog });
_root->SetSettings(_settings, false);
_root->Loaded({ this, &App::_OnLoaded });
_root->Create();
_ApplyTheme(_settings->GlobalSettings().GetRequestedTheme());
TraceLoggingWrite(
g_hTerminalAppProvider,
"AppCreated",
TraceLoggingDescription("Event emitted when the application is started"),
TraceLoggingBool(_settings->GlobalSettings().GetShowTabsInTitlebar(), "TabsInTitlebar"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
}
// Method Description:
// - Show a ContentDialog with a single button to dismiss. Uses the
// FrameworkElements provided as the title and content of this dialog, and
// displays a single button to dismiss.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens.
// Arguments:
// sender: unused
// dialog: the dialog object that is going to show up
fire_and_forget App::_ShowDialog(const winrt::Windows::Foundation::IInspectable& sender, winrt::Windows::UI::Xaml::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.
return;
}
// 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.
dialog.RequestedTheme(_settings->GlobalSettings().GetRequestedTheme());
// Display the dialog.
Controls::ContentDialogResult result = 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:
// - 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 App::_ShowLoadErrorsDialog(const winrt::hstring& titleKey,
const winrt::hstring& contentKey,
HRESULT settingsLoadedResult)
{
auto title = _resourceLoader->GetLocalizedString(titleKey);
auto buttonText = _resourceLoader->GetLocalizedString(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 = _resourceLoader->GetLocalizedString(contentKey);
errorRun.Text(errorLabel);
warningsTextBlock.Inlines().Append(errorRun);
if (FAILED(settingsLoadedResult))
{
if (!_settingsLoadExceptionText.empty())
{
warningsTextBlock.Inlines().Append(_BuildErrorRun(_settingsLoadExceptionText, Resources()));
}
}
// Add a note that we're using the default settings in this case.
winrt::Windows::UI::Xaml::Documents::Run usingDefaultsRun;
const auto usingDefaultsText = _resourceLoader->GetLocalizedString(L"UsingDefaultSettingsText");
usingDefaultsRun.Text(usingDefaultsText);
warningsTextBlock.Inlines().Append(usingDefaultsRun);
Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(winrt::box_value(warningsTextBlock));
dialog.CloseButtonText(buttonText);
_ShowDialog(nullptr, 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 App::_ShowLoadWarningsDialog()
{
auto title = _resourceLoader->GetLocalizedString(L"SettingsValidateErrorTitle");
auto buttonText = _resourceLoader->GetLocalizedString(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);
const auto& warnings = _settings->GetWarnings();
for (const auto& warning : warnings)
{
// Try looking up the warning message key for each warning.
const auto warningText = _GetWarningText(warning, *_resourceLoader);
if (!warningText.empty())
{
warningsTextBlock.Inlines().Append(_BuildErrorRun(warningText, Resources()));
}
}
Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(winrt::box_value(warningsTextBlock));
dialog.CloseButtonText(buttonText);
_ShowDialog(nullptr, dialog);
}
// Method Description:
// - Triggered when the application is fiished 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 App::_OnLoaded(const IInspectable& /*sender*/,
const RoutedEventArgs& /*eventArgs*/)
{
if (FAILED(_settingsLoadedResult))
{
const winrt::hstring titleKey = L"InitialJsonParseErrorTitle";
const winrt::hstring textKey = L"InitialJsonParseErrorText";
_ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult);
}
else if (_settingsLoadedResult == S_FALSE)
{
_ShowLoadWarningsDialog();
}
}
// 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::Point App::GetLaunchDimensions(uint32_t dpi)
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}
// Use the default profile to determine how big of a window we need.
TerminalSettings settings = _settings->MakeSettings(std::nullopt);
// TODO MSFT:21150597 - If the global setting "Always show tab bar" is
// set, then we'll need to add the height of the tab bar here.
return TermControl::GetProposedDimensions(settings, dpi);
}
bool App::GetShowTabsInTitlebar()
{
if (!_loadedInitialSettings)
{
// Load settings if we haven't already
LoadSettings();
}
return _settings->GlobalSettings().GetShowTabsInTitlebar();
}
// Method Description:
// - Attempt to load the settings. If we fail for any reason, returns an error.
// Return Value:
// - S_OK if we successfully parsed the settings, otherwise an appropriate HRESULT.
[[nodiscard]] HRESULT App::_TryLoadSettings() noexcept
{
HRESULT hr = E_FAIL;
try
{
auto newSettings = CascadiaSettings::LoadAll();
_settings = std::move(newSettings);
const auto& warnings = _settings->GetWarnings();
hr = warnings.size() == 0 ? S_OK : S_FALSE;
}
catch (const winrt::hresult_error& e)
{
hr = e.code();
_settingsLoadExceptionText = e.message();
LOG_HR(hr);
}
catch (const ::TerminalApp::SettingsException& ex)
{
hr = E_INVALIDARG;
_settingsLoadExceptionText = _GetErrorText(ex.Error(), *_resourceLoader);
}
catch (...)
{
hr = wil::ResultFromCaughtException();
LOG_HR(hr);
}
return hr;
}
// Method Description:
// - Initialized our settings. See CascadiaSettings for more details.
// Additionally hooks up our callbacks for keybinding events to the
// keybindings object.
// NOTE: This must be called from a MTA if we're running as a packaged
// application. The Windows.Storage APIs require a MTA. If this isn't
// happening during startup, it'll need to happen on a background thread.
void App::LoadSettings()
{
auto start = std::chrono::high_resolution_clock::now();
TraceLoggingWrite(
g_hTerminalAppProvider,
"SettingsLoadStarted",
TraceLoggingDescription("Event emitted before loading the settings"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
// Attempt to load the settings.
// If it fails,
// - use Default settings,
// - don't persist them (LoadAll won't save them in this case).
// - _settingsLoadedResult will be set to an error, indicating that
// we should display the loading error.
// * We can't display the error now, because we might not have a
// UI yet. We'll display the error in _OnLoaded.
_settingsLoadedResult = _TryLoadSettings();
if (FAILED(_settingsLoadedResult))
{
_settings = CascadiaSettings::LoadDefaults();
}
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> delta = end - start;
TraceLoggingWrite(
g_hTerminalAppProvider,
"SettingsLoadComplete",
TraceLoggingDescription("Event emitted when loading the settings is finished"),
TraceLoggingFloat64(delta.count(), "Duration"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
_loadedInitialSettings = true;
// Register for directory change notification.
_RegisterSettingsChange();
}
// Method Description:
// - Registers for changes to the settings folder and upon a updated settings
// profile calls _ReloadSettings().
// Arguments:
// - <none>
// Return Value:
// - <none>
void App::_RegisterSettingsChange()
{
// Get the containing folder.
std::filesystem::path settingsPath{ CascadiaSettings::GetSettingsPath() };
const auto folder = settingsPath.parent_path();
_reader.create(folder.c_str(),
false,
wil::FolderChangeEvents::All,
[this, settingsPath](wil::FolderChangeEvent event, PCWSTR fileModified) {
// We want file modifications, AND when files are renamed to be
// profiles.json. This second case will oftentimes happen with text
// editors, who will write a temp file, then rename it to be the
// actual file you wrote. So listen for that too.
if (!(event == wil::FolderChangeEvent::Modified ||
event == wil::FolderChangeEvent::RenameNewName))
{
return;
}
std::filesystem::path modifiedFilePath = fileModified;
// Getting basename (filename.ext)
const auto settingsBasename = settingsPath.filename();
const auto modifiedBasename = modifiedFilePath.filename();
if (settingsBasename == modifiedBasename)
{
this->_DispatchReloadSettings();
}
});
}
// Method Description:
// - Dispatches a settings reload with debounce.
// Text editors implement Save in a bunch of different ways, so
// this stops us from reloading too many times or too quickly.
fire_and_forget App::_DispatchReloadSettings()
{
static constexpr auto FileActivityQuiesceTime{ std::chrono::milliseconds(50) };
if (!_settingsReloadQueued.exchange(true))
{
co_await winrt::resume_after(FileActivityQuiesceTime);
_ReloadSettings();
_settingsReloadQueued.store(false);
}
}
// Method Description:
// - Reloads the settings from the profile.json.
void App::_ReloadSettings()
{
// Attempt to load our settings.
// If it fails,
// - don't change the settings (and don't actually apply the new settings)
// - don't persist them.
// - display a loading error
_settingsLoadedResult = _TryLoadSettings();
if (FAILED(_settingsLoadedResult))
{
_root->Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() {
const winrt::hstring titleKey = L"ReloadJsonParseErrorTitle";
const winrt::hstring textKey = L"ReloadJsonParseErrorText";
_ShowLoadErrorsDialog(titleKey, textKey, _settingsLoadedResult);
});
return;
}
else if (_settingsLoadedResult == S_FALSE)
{
_root->Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() {
_ShowLoadWarningsDialog();
});
}
// Here, we successfully reloaded the settings, and created a new
// TerminalSettings object.
// Update the settings in TerminalPage
_root->SetSettings(_settings, true);
_root->Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() {
// Refresh the UI theme
_ApplyTheme(_settings->GlobalSettings().GetRequestedTheme());
});
}
// 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 App::_ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme)
{
// Propagate the event to the host layer, so it can update its own UI
_requestedThemeChangedHandlers(*this, newTheme);
}
UIElement App::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 "Windows Terminal"
// Arguments:
// - <none>
// Return Value:
// - the title of the focused control if there is one, else "Windows Terminal"
hstring App::Title()
{
if (_root)
{
return _root->Title();
}
return { L"Windows Terminal" };
}
// Method Description:
// - Used to tell the app that the titlebar has been clicked. The App won't
// actually recieve 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 App::TitlebarClicked()
{
if (_root)
{
_root->TitlebarClicked();
}
}
// Methods that proxy typed event handlers through TerminalPage
winrt::event_token App::SetTitleBarContent(Windows::Foundation::TypedEventHandler<winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::UIElement> const& handler)
{
return _root->SetTitleBarContent(handler);
}
void App::SetTitleBarContent(winrt::event_token const& token) noexcept
{
return _root->SetTitleBarContent(token);
}
winrt::event_token App::TitleChanged(Windows::Foundation::TypedEventHandler<winrt::Windows::Foundation::IInspectable, winrt::hstring> const& handler)
{
return _root->TitleChanged(handler);
}
void App::TitleChanged(winrt::event_token const& token) noexcept
{
return _root->TitleChanged(token);
}
winrt::event_token App::LastTabClosed(Windows::Foundation::TypedEventHandler<winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::LastTabClosedEventArgs> const& handler)
{
return _root->LastTabClosed(handler);
}
void App::LastTabClosed(winrt::event_token const& token) noexcept
{
return _root->LastTabClosed(token);
}
// -------------------------------- WinRT Events ---------------------------------
// Winrt events need a method for adding a callback to the event and removing the callback.
// These macros will define them both for you.
DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(App, RequestedThemeChanged, _requestedThemeChangedHandlers, TerminalApp::App, winrt::Windows::UI::Xaml::ElementTheme);
}