8ba8f35dc5
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
614 lines
24 KiB
C++
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);
|
|
}
|