// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "App.h" #include #include #include #include #include "App.g.cpp" using namespace winrt::Windows::ApplicationModel::DataTransfer; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Text; 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; // Note: Generate GUID using TlgGuid.exe tool TRACELOGGING_DEFINE_PROVIDER( g_hTerminalAppProvider, "Microsoft.Windows.Terminal.App", // {24a1622f-7da7-5c77-3303-d850bd1ab2ed} (0x24a1622f, 0x7da7, 0x5c77, 0x33, 0x03, 0xd8, 0x50, 0xbd, 0x1a, 0xb2, 0xed), TraceLoggingOptionMicrosoftTelemetry()); namespace winrt { namespace MUX = Microsoft::UI::Xaml; using IInspectable = Windows::Foundation::IInspectable; } namespace winrt::TerminalApp::implementation { App::App() : App(winrt::TerminalApp::XamlMetaDataProvider()) { } App::App(Windows::UI::Xaml::Markup::IXamlMetadataProvider const& parentProvider) : base_type(parentProvider), _settings{ }, _tabs{ }, _loadedInitialSettings{ false }, _settingsLoadedResult{ S_OK }, _dialogLock{} { // 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: // - 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: // - // Return Value: // - 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); TraceLoggingRegister(g_hTerminalAppProvider); _Create(); } App::~App() { TraceLoggingUnregister(g_hTerminalAppProvider); } // Method Description: // - Create all of the initial UI elements of the Terminal app. // * Creates the tab bar, initially hidden. // * Creates the tab content area, which is where we'll display the tabs/panes. // * Initializes the first terminal control, using the default profile, // and adds it to our list of tabs. void App::_Create() { _tabView = MUX::Controls::TabView{}; _tabView.SelectionChanged({ this, &App::_OnTabSelectionChanged }); _tabView.TabClosing({ this, &App::_OnTabClosing }); _tabView.Items().VectorChanged({ this, &App::_OnTabItemsChanged }); _root = Controls::Grid{}; _tabRow = Controls::Grid{}; _tabRow.Name(L"Tab Row"); _tabContent = Controls::Grid{}; _tabContent.Name(L"Tab Content"); // Set up two columns in the tabs row - one for the tabs themselves, and // another for the settings button. auto tabsColDef = Controls::ColumnDefinition(); auto newTabBtnColDef = Controls::ColumnDefinition(); newTabBtnColDef.Width(GridLengthHelper::Auto()); _tabRow.ColumnDefinitions().Append(tabsColDef); _tabRow.ColumnDefinitions().Append(newTabBtnColDef); // Set up two rows - one for the tabs, the other for the tab content, // the terminal panes. auto tabBarRowDef = Controls::RowDefinition(); tabBarRowDef.Height(GridLengthHelper::Auto()); _root.RowDefinitions().Append(tabBarRowDef); _root.RowDefinitions().Append(Controls::RowDefinition{}); if (_settings->GlobalSettings().GetShowTabsInTitlebar() == false) { _root.Children().Append(_tabRow); Controls::Grid::SetRow(_tabRow, 0); } _root.Children().Append(_tabContent); Controls::Grid::SetRow(_tabContent, 1); Controls::Grid::SetColumn(_tabView, 0); // Create the new tab button. _newTabButton = Controls::SplitButton{}; Controls::SymbolIcon newTabIco{}; newTabIco.Symbol(Controls::Symbol::Add); _newTabButton.Content(newTabIco); Controls::Grid::SetRow(_newTabButton, 0); Controls::Grid::SetColumn(_newTabButton, 1); _newTabButton.VerticalAlignment(VerticalAlignment::Stretch); _newTabButton.HorizontalAlignment(HorizontalAlignment::Left); // When the new tab button is clicked, open the default profile _newTabButton.Click([this](auto&&, auto&&){ this->_OpenNewTab(std::nullopt); }); // Populate the new tab button's flyout with entries for each profile _CreateNewTabFlyout(); _tabRow.Children().Append(_tabView); _tabRow.Children().Append(_newTabButton); _tabContent.VerticalAlignment(VerticalAlignment::Stretch); _tabContent.HorizontalAlignment(HorizontalAlignment::Stretch); // Here, we're doing the equivalent of defining the _tabRow as the // following: We need to set the Background // to that ThemeResource, so it'll be colored appropriately regardless // of what theme the user has selected. // We're looking up the Style we've defined in App.xaml, and applying it // here. A ResourceDictionary is a Map, so // you'll need to try_as to get the type we actually want. auto res = Resources(); IInspectable key = winrt::box_value(L"BackgroundGridThemeStyle"); if (res.HasKey(key)) { IInspectable g = res.Lookup(key); winrt::Windows::UI::Xaml::Style style = g.try_as(); _root.Style(style); _tabRow.Style(style); } // Apply the UI theme from our settings to our UI elements _ApplyTheme(_settings->GlobalSettings().GetRequestedTheme()); _OpenNewTab(std::nullopt); _root.Loaded({ this, &App::_OnLoaded }); } // Method Description: // - Show a ContentDialog with a single "Ok" button to dismiss. Looks up the // the title and text from our Resources using the provided keys. // - Only one dialog can be visible at a time. If another dialog is visible // when this is called, nothing happens. // 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. fire_and_forget App::_ShowOkDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey) { // 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; } auto resourceLoader = Windows::ApplicationModel::Resources::ResourceLoader::GetForCurrentView(); auto title = resourceLoader.GetString(titleKey); auto message = resourceLoader.GetString(contentKey); auto buttonText = resourceLoader.GetString(L"Ok"); Controls::ContentDialog dialog; dialog.Title(winrt::box_value(title)); dialog.Content(winrt::box_value(message)); dialog.CloseButtonText(buttonText); // IMPORTANT: Add the dialog to the _root UIElement before you show it, // so it knows how to attach to the XAML content. _root.Children().Append(dialog); // 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: // - 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: // - void App::_OnLoaded(const IInspectable& /*sender*/, const RoutedEventArgs& /*eventArgs*/) { if (FAILED(_settingsLoadedResult)) { const winrt::hstring titleKey = L"InitialJsonParseErrorTitle"; const winrt::hstring textKey = L"InitialJsonParseErrorText"; _ShowOkDialog(titleKey, textKey); } } // 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: // - // 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: // - Builds the flyout (dropdown) attached to the new tab button, and // attaches it to the button. Populates the flyout with one entry per // Profile, displaying the profile's name. Clicking each flyout item will // open a new tab with that profile. // Below the profiles are the static menu items: settings, feedback void App::_CreateNewTabFlyout() { auto newTabFlyout = Controls::MenuFlyout{}; auto keyBindings = _settings->GetKeybindings(); const GUID defaultProfileGuid = _settings->GlobalSettings().GetDefaultProfile(); for (int profileIndex = 0; profileIndex < _settings->GetProfiles().size(); profileIndex++) { const auto& profile = _settings->GetProfiles()[profileIndex]; auto profileMenuItem = Controls::MenuFlyoutItem{}; // add the keyboard shortcuts for the first 9 profiles if (profileIndex < 9) { // enum value for ShortcutAction::NewTabProfileX; 0==NewTabProfile0 auto profileKeyChord = keyBindings.GetKeyBinding(static_cast(profileIndex + static_cast(ShortcutAction::NewTabProfile0))); // make sure we find one to display if (profileKeyChord) { _SetAcceleratorForMenuItem(profileMenuItem, profileKeyChord); } } auto profileName = profile.GetName(); winrt::hstring hName{ profileName }; profileMenuItem.Text(hName); // If there's an icon set for this profile, set it as the icon for // this flyout item. if (profile.HasIcon()) { profileMenuItem.Icon(_GetIconFromProfile(profile)); } if (profile.GetGuid() == defaultProfileGuid) { // Contrast the default profile with others in font weight. profileMenuItem.FontWeight(FontWeights::Bold()); } profileMenuItem.Click([this, profileIndex](auto&&, auto&&){ this->_OpenNewTab({ profileIndex }); }); newTabFlyout.Items().Append(profileMenuItem); } // add menu separator auto separatorItem = Controls::MenuFlyoutSeparator{}; newTabFlyout.Items().Append(separatorItem); // add static items { // Create the settings button. auto settingsItem = Controls::MenuFlyoutItem{}; settingsItem.Text(L"Settings"); Controls::SymbolIcon ico{}; ico.Symbol(Controls::Symbol::Setting); settingsItem.Icon(ico); settingsItem.Click({ this, &App::_SettingsButtonOnClick }); newTabFlyout.Items().Append(settingsItem); auto settingsKeyChord = keyBindings.GetKeyBinding(ShortcutAction::OpenSettings); if (settingsKeyChord) { _SetAcceleratorForMenuItem(settingsItem, settingsKeyChord); } // Create the feedback button. auto feedbackFlyout = Controls::MenuFlyoutItem{}; feedbackFlyout.Text(L"Feedback"); Controls::FontIcon feedbackIco{}; feedbackIco.Glyph(L"\xE939"); feedbackIco.FontFamily(Media::FontFamily{ L"Segoe MDL2 Assets" }); feedbackFlyout.Icon(feedbackIco); feedbackFlyout.Click({ this, &App::_FeedbackButtonOnClick }); newTabFlyout.Items().Append(feedbackFlyout); } _newTabButton.Flyout(newTabFlyout); } // Function Description: // - Called when the settings button is clicked. ShellExecutes the settings // file, as to open it in the default editor for .json files. Does this in // a background thread, as to not hang/crash the UI thread. fire_and_forget LaunchSettings() { // This will switch the execution of the function to a background (not // UI) thread. This is IMPORTANT, because the Windows.Storage API's // (used for retrieving the path to the file) will crash on the UI // thread, because the main thread is a STA. co_await winrt::resume_background(); const auto settingsPath = CascadiaSettings::GetSettingsPath(); ShellExecute(nullptr, L"open", settingsPath.c_str(), nullptr, nullptr, SW_SHOW); } // Method Description: // - Called when the settings button is clicked. Launches a background // thread to open the settings file in the default JSON editor. // Arguments: // - // Return Value: // - void App::_SettingsButtonOnClick(const IInspectable&, const RoutedEventArgs&) { LaunchSettings(); } // Method Description: // - Called when the feedback button is clicked. Launches github in your // default browser, navigated to the "issues" page of the Terminal repo. void App::_FeedbackButtonOnClick(const IInspectable&, const RoutedEventArgs&) { winrt::Windows::System::Launcher::LaunchUriAsync({ L"https://github.com/microsoft/Terminal/issues" }); } // Method Description: // - Register our event handlers with the given keybindings object. This // should be done regardless of what the events are actually bound to - // this simply ensures the AppKeyBindings object will call us correctly // for each event. // Arguments: // - bindings: A AppKeyBindings object to wire up with our event handlers void App::_HookupKeyBindings(TerminalApp::AppKeyBindings bindings) noexcept { // Hook up the KeyBinding object's events to our handlers. // They should all be hooked up here, regardless of whether or not // there's an actual keychord for them. bindings.NewTab([this]() { _OpenNewTab(std::nullopt); }); bindings.CloseTab([this]() { _CloseFocusedTab(); }); bindings.NewTabWithProfile([this](const auto index) { _OpenNewTab({ index }); }); bindings.ScrollUp([this]() { _Scroll(-1); }); bindings.ScrollDown([this]() { _Scroll(1); }); bindings.NextTab([this]() { _SelectNextTab(true); }); bindings.PrevTab([this]() { _SelectNextTab(false); }); bindings.ScrollUpPage([this]() { _ScrollPage(-1); }); bindings.ScrollDownPage([this]() { _ScrollPage(1); }); bindings.SwitchToTab([this](const auto index) { _SelectTab({ index }); }); bindings.OpenSettings([this]() { _OpenSettings(); }); } // Method Description: // - Attempt to load the settings. If we fail for any reason, returns an error. // Arguments: // - saveOnLoad: If true, after loading the settings, we should re-write // them to the file, to make sure the schema is updated. See // `CascadiaSettings::LoadAll` for details. // Return Value: // - S_OK if we successfully parsed the settings, otherwise an appropriate HRESULT. [[nodiscard]] HRESULT App::_TryLoadSettings(const bool saveOnLoad) noexcept { HRESULT hr = E_FAIL; try { auto newSettings = CascadiaSettings::LoadAll(saveOnLoad); _settings = std::move(newSettings); hr = S_OK; } catch (const winrt::hresult_error& e) { hr = e.code(); LOG_HR(hr); } 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() { // 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(true); if (FAILED(_settingsLoadedResult)) { _settings = std::make_unique(); _settings->CreateDefaults(); } _HookupKeyBindings(_settings->GetKeybindings()); _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: // - // Return Value: // - void App::_RegisterSettingsChange() { // Make sure this hstring has a stack-local reference. If we don't it // might get cleaned up before we parse the path. const auto localPathCopy = CascadiaSettings::GetSettingsPath(); // Getting the containing folder. std::filesystem::path fileParser = localPathCopy.c_str(); const auto folder = fileParser.parent_path(); _reader.create(folder.c_str(), false, wil::FolderChangeEvents::All, [this](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; } const auto localPathCopy = CascadiaSettings::GetSettingsPath(); std::filesystem::path settingsParser = localPathCopy.c_str(); std::filesystem::path modifiedParser = fileModified; // Getting basename (filename.ext) const auto settingsBasename = settingsParser.filename(); const auto modifiedBasename = modifiedParser.filename(); if (settingsBasename == modifiedBasename) { this->_ReloadSettings(); } }); } // 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(false); if (FAILED(_settingsLoadedResult)) { _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { const winrt::hstring titleKey = L"ReloadJsonParseErrorTitle"; const winrt::hstring textKey = L"ReloadJsonParseErrorText"; _ShowOkDialog(titleKey, textKey); }); return; } // Here, we successfully reloaded the settings, and created a new // TerminalSettings object. // Re-wire the keybindings to their handlers, as we'll have created a // new AppKeyBindings object. _HookupKeyBindings(_settings->GetKeybindings()); // Refresh UI elements auto profiles = _settings->GetProfiles(); for (auto &profile : profiles) { const GUID profileGuid = profile.GetGuid(); TerminalSettings settings = _settings->MakeSettings(profileGuid); for (auto &tab : _tabs) { const auto term = tab->GetTerminalControl(); const GUID tabProfile = tab->GetProfile(); if (profileGuid == tabProfile) { term.UpdateSettings(settings); // Update the icons of the tabs with this profile open. auto tabViewItem = tab->GetTabViewItem(); tabViewItem.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [profile, tabViewItem]() { // _GetIconFromProfile has to run on the main thread tabViewItem.Icon(App::_GetIconFromProfile(profile)); }); } } } _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { // Refresh the UI theme _ApplyTheme(_settings->GlobalSettings().GetRequestedTheme()); // repopulate the new tab button's flyout with entries for each // profile, which might have changed _CreateNewTabFlyout(); }); } // Method Description: // - Update the current theme of the application. This will manually update // all of the elements in our UI to match the given theme. // Arguments: // - newTheme: The ElementTheme to apply to our elements. void App::_ApplyTheme(const Windows::UI::Xaml::ElementTheme& newTheme) { _root.RequestedTheme(newTheme); _tabRow.RequestedTheme(newTheme); } UIElement App::GetRoot() noexcept { return _root; } UIElement App::GetTabs() noexcept { return _tabRow; } void App::_SetFocusedTabIndex(int tabIndex) { auto tab = _tabs.at(tabIndex); _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [tab, this](){ auto tabViewItem = tab->GetTabViewItem(); _tabView.SelectedItem(tabViewItem); }); } // Method Description: // - Handle changes in tab layout. void App::_UpdateTabView() { // Show tabs when there's more than 1, or the user has chosen to always // show the tab bar. const bool isVisible = _settings->GlobalSettings().GetShowTabsInTitlebar() || (_tabs.size() > 1) || _settings->GlobalSettings().GetAlwaysShowTabs(); // collapse/show the tabs themselves _tabView.Visibility(isVisible ? Visibility::Visible : Visibility::Collapsed); // collapse/show the row that the tabs are in. // NaN is the special value XAML uses for "Auto" sizing. _tabRow.Height(isVisible ? NAN : 0); } // Method Description: // - Open a new tab. This will create the TerminalControl hosting the // terminal, and add a new Tab to our list of tabs. The method can // optionally be provided a profile index, which will be used to create // a tab using the profile in that index. // If no index is provided, the default profile will be used. // Arguments: // - profileIndex: an optional index into the list of profiles to use to // initialize this tab up with. void App::_OpenNewTab(std::optional profileIndex) { GUID profileGuid; if (profileIndex) { const auto realIndex = profileIndex.value(); const auto profiles = _settings->GetProfiles(); // If we don't have that many profiles, then do nothing. if (realIndex >= profiles.size()) { return; } const auto& selectedProfile = profiles[realIndex]; profileGuid = selectedProfile.GetGuid(); } else { // Getting Guid for default profile const auto globalSettings = _settings->GlobalSettings(); profileGuid = globalSettings.GetDefaultProfile(); } TerminalSettings settings = _settings->MakeSettings(profileGuid); _CreateNewTabFromSettings(profileGuid, settings); const int tabCount = static_cast(_tabs.size()); TraceLoggingWrite( g_hTerminalAppProvider, // handle to TerminalApp tracelogging provider "TabInformation", TraceLoggingDescription("Event emitted upon new tab creation in TerminalApp"), TraceLoggingInt32(tabCount, "TabCount", "Count of tabs curently opened in TerminalApp"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } // Function Description: // - Copies and processes the text data from the Windows Clipboard. // Does some of this in a background thread, as to not hang/crash the UI thread. // Arguments: // - eventArgs: the PasteFromClipboard event sent from the TermControl fire_and_forget PasteFromClipboard(PasteFromClipboardEventArgs eventArgs) { const DataPackageView data = Clipboard::GetContent(); // This will switch the execution of the function to a background (not // UI) thread. This is IMPORTANT, because the getting the clipboard data // will crash on the UI thread, because the main thread is a STA. co_await winrt::resume_background(); hstring text = L""; if (data.Contains(StandardDataFormats::Text())) { text = co_await data.GetTextAsync(); } eventArgs.HandleClipboardData(text); } // Method Description: // - Creates a new tab with the given settings. If the tab bar is not being // currently displayed, it will be shown. // Arguments: // - settings: the TerminalSettings object to use to create the TerminalControl with. void App::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings) { // Initialize the new tab TermControl term{ settings }; // Add an event handler when the terminal's selection wants to be copied. // When the text buffer data is retrieved, we'll copy the data into the Clipboard term.CopyToClipboard([=](auto copiedData) { _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [copiedData]() { DataPackage dataPack = DataPackage(); dataPack.RequestedOperation(DataPackageOperation::Copy); dataPack.SetText(copiedData); Clipboard::SetContent(dataPack); // TODO: MSFT 20642290 and 20642291 // rtf copy and html copy }); }); // Add an event handler when the terminal wants to paste data from the Clipboard. term.PasteFromClipboard([=](auto /*sender*/, auto eventArgs) { _root.Dispatcher().RunAsync(CoreDispatcherPriority::High, [eventArgs]() { PasteFromClipboard(eventArgs); }); }); // Add the new tab to the list of our tabs. auto newTab = _tabs.emplace_back(std::make_shared(profileGuid, term)); // Add an event handler when the terminal's title changes. When the // title changes, we'll bubble it up to listeners of our own title // changed event, so they can handle it. newTab->GetTerminalControl().TitleChanged([=](auto newTitle){ // Only bubble the change if this tab is the focused tab. if (_settings->GlobalSettings().GetShowTitleInTitlebar() && newTab->IsFocused()) { _titleChangeHandlers(newTitle); } }); auto tabViewItem = newTab->GetTabViewItem(); _tabView.Items().Append(tabViewItem); const auto* const profile = _settings->FindProfile(profileGuid); // Set this profile's tab to the icon the user specified if (profile != nullptr && profile->HasIcon()) { tabViewItem.Icon(_GetIconFromProfile(*profile)); } // Add an event handler when the terminal's connection is closed. newTab->GetTerminalControl().ConnectionClosed([=]() { _tabView.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [newTab, tabViewItem, this]() { const GUID tabProfile = newTab->GetProfile(); // Don't just capture this pointer, because the profile might // get destroyed before this is called (case in point - // reloading settings) const auto* const p = _settings->FindProfile(tabProfile); if (p != nullptr && p->GetCloseOnExit()) { _RemoveTabViewItem(tabViewItem); } }); }); tabViewItem.PointerPressed({ this, &App::_OnTabClick }); // This is one way to set the tab's selected background color. // tabViewItem.Resources().Insert(winrt::box_value(L"TabViewItemHeaderBackgroundSelected"), a Brush?); // This kicks off TabView::SelectionChanged, in response to which we'll attach the terminal's // Xaml control to the Xaml root. _tabView.SelectedItem(tabViewItem); } // Method Description: // - Returns the index in our list of tabs of the currently focused tab. If // no tab is currently selected, returns -1. // Return Value: // - the index of the currently focused tab if there is one, else -1 int App::_GetFocusedTabIndex() const { return _tabView.SelectedIndex(); } void App::_OpenSettings() { LaunchSettings(); } // Method Description: // - Close the currently focused tab. Focus will move to the left, if possible. void App::_CloseFocusedTab() { int focusedTabIndex = _GetFocusedTabIndex(); std::shared_ptr focusedTab{ _tabs[focusedTabIndex] }; _RemoveTabViewItem(focusedTab->GetTabViewItem()); } // Method Description: // - Move the viewport of the terminal of the currently focused tab up or // down a number of lines. Negative values of `delta` will move the // view up, and positive values will move the viewport down. // Arguments: // - delta: a number of lines to move the viewport relative to the current viewport. void App::_Scroll(int delta) { int focusedTabIndex = _GetFocusedTabIndex(); _tabs[focusedTabIndex]->Scroll(delta); } // Method Description: // - Move the viewport of the terminal of the currently focused tab up or // down a page. The page length will be dependent on the terminal view height. // Negative values of `delta` will move the view up by one page, and positive values // will move the viewport down by one page. // Arguments: // - delta: The direction to move the view relative to the current viewport(it // is clamped between -1 and 1) void App::_ScrollPage(int delta) { delta = std::clamp(delta, -1, 1); const auto focusedTabIndex = _GetFocusedTabIndex(); const auto control = _tabs[focusedTabIndex]->GetTerminalControl(); const auto termHeight = control.GetViewHeight(); _tabs[focusedTabIndex]->Scroll(termHeight * delta); } // Method Description: // - Copy text from the focused terminal to the Windows Clipboard // Arguments: // - trimTrailingWhitespace: enable removing any whitespace from copied selection // and get text to appear on separate lines. void App::_CopyText(const bool trimTrailingWhitespace) { const int focusedTabIndex = _GetFocusedTabIndex(); std::shared_ptr focusedTab{ _tabs[focusedTabIndex] }; const auto control = focusedTab->GetTerminalControl(); control.CopySelectionToClipboard(trimTrailingWhitespace); } // Method Description: // - Sets focus to the tab to the right or left the currently selected tab. void App::_SelectNextTab(const bool bMoveRight) { int focusedTabIndex = _GetFocusedTabIndex(); auto tabCount = _tabs.size(); // Wraparound math. By adding tabCount and then calculating modulo tabCount, // we clamp the values to the range [0, tabCount) while still supporting moving // leftward from 0 to tabCount - 1. _SetFocusedTabIndex( static_cast((tabCount + focusedTabIndex + (bMoveRight ? 1 : -1)) % tabCount) ); } // Method Description: // - Sets focus to the desired tab. void App::_SelectTab(const int tabIndex) { if (tabIndex >= 0 && tabIndex < _tabs.size()) { _SetFocusedTabIndex(tabIndex); } } // Method Description: // - Responds to the TabView control's Selection Changed event (to move a // new terminal control into focus.) // Arguments: // - sender: the control that originated this event // - eventArgs: the event's constituent arguments void App::_OnTabSelectionChanged(const IInspectable& sender, const Controls::SelectionChangedEventArgs& eventArgs) { auto tabView = sender.as(); auto selectedIndex = tabView.SelectedIndex(); // Unfocus all the tabs. for (auto tab : _tabs) { tab->SetFocused(false); } if (selectedIndex >= 0) { try { auto tab = _tabs.at(selectedIndex); auto control = tab->GetTerminalControl().GetControl(); _tabContent.Children().Clear(); _tabContent.Children().Append(control); tab->SetFocused(true); _titleChangeHandlers(GetTitle()); } CATCH_LOG(); } } // Method Description: // - Responds to the TabView control's Tab Closing event by removing // the indicated tab from the set and focusing another one. // The event is cancelled so App maintains control over the // items in the tabview. // Arguments: // - sender: the control that originated this event // - eventArgs: the event's constituent arguments void App::_OnTabClosing(const IInspectable& sender, const MUX::Controls::TabViewTabClosingEventArgs& eventArgs) { const auto tabViewItem = eventArgs.Item(); _RemoveTabViewItem(tabViewItem); // If we don't cancel the event, the TabView will remove the item itself. eventArgs.Cancel(true); } // Method Description: // - Responds to changes in the TabView's item list by changing the tabview's // visibility. // Arguments: // - sender: the control that originated this event // - eventArgs: the event's constituent arguments void App::_OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs) { _UpdateTabView(); } // 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: // - // Return Value: // - the title of the focused control if there is one, else "Windows Terminal" hstring App::GetTitle() { if (_settings->GlobalSettings().GetShowTitleInTitlebar()) { auto selectedIndex = _tabView.SelectedIndex(); if (selectedIndex >= 0) { try { auto tab = _tabs.at(selectedIndex); return tab->GetTerminalControl().Title(); } CATCH_LOG(); } } return { L"Windows Terminal" }; } // Method Description: // - Additional responses to clicking on a TabView's item. Currently, just remove tab with middle click // Arguments: // - sender: the control that originated this event (TabViewItem) // - eventArgs: the event's constituent arguments void App::_OnTabClick(const IInspectable& sender, const Windows::UI::Xaml::Input::PointerRoutedEventArgs& eventArgs) { if (eventArgs.GetCurrentPoint(_root).Properties().IsMiddleButtonPressed()) { _RemoveTabViewItem(sender); eventArgs.Handled(true); } } // Method Description: // - Removes the tab (both TerminalControl and XAML) // Arguments: // - tabViewItem: the TabViewItem in the TabView that is being removed. void App::_RemoveTabViewItem(const IInspectable& tabViewItem) { // To close the window here, we need to close the hosting window. if (_tabs.size() == 1) { _lastTabClosedHandlers(); } uint32_t tabIndexFromControl = 0; _tabView.Items().IndexOf(tabViewItem, tabIndexFromControl); auto focusedTabIndex = _GetFocusedTabIndex(); // Removing the tab from the collection will destroy its control and disconnect its connection. _tabs.erase(_tabs.begin() + tabIndexFromControl); _tabView.Items().RemoveAt(tabIndexFromControl); if (tabIndexFromControl == focusedTabIndex) { if (focusedTabIndex >= _tabs.size()) { focusedTabIndex = _tabs.size() - 1; } if (focusedTabIndex < 0) { focusedTabIndex = 0; } _SelectTab(focusedTabIndex); } } // Method Description: // - Gets a colored IconElement for the profile in question. If the profile // has an `icon` set in the settings, this will return an icon with that // image in it. Otherwise it will return a nullptr-initialized // IconElement. // Arguments: // - profile: the profile to get the icon from // Return Value: // - an IconElement for the profile's icon, if it has one. Controls::IconElement App::_GetIconFromProfile(const Profile& profile) { if (profile.HasIcon()) { auto path = profile.GetIconPath(); winrt::hstring iconPath{ path }; winrt::Windows::Foundation::Uri iconUri{ iconPath }; Controls::BitmapIconSource iconSource; // Make sure to set this to false, so we keep the RGB data of the // image. Otherwise, the icon will be white for all the // non-transparent pixels in the image. iconSource.ShowAsMonochrome(false); iconSource.UriSource(iconUri); Controls::IconSourceElement elem; elem.IconSource(iconSource); return elem; } else { return { nullptr }; } } // Method Description: // - Takes a MenuFlyoutItem and a corresponding KeyChord value and creates the accelerator for UI display. // Takes into account a special case for an error condition for a comma // Arguments: // - MenuFlyoutItem that will be displayed, and a KeyChord to map an accelerator void App::_SetAcceleratorForMenuItem(Windows::UI::Xaml::Controls::MenuFlyoutItem& menuItem, const winrt::Microsoft::Terminal::Settings::KeyChord& keyChord) { // work around https://github.com/microsoft/microsoft-ui-xaml/issues/708 in case of VK_OEM_COMMA if (keyChord.Vkey() != VK_OEM_COMMA) { // use the XAML shortcut to give us the automatic capabilities auto menuShortcut = Windows::UI::Xaml::Input::KeyboardAccelerator{}; // TODO: Modify this when https://github.com/microsoft/terminal/issues/877 is resolved menuShortcut.Key(static_cast(keyChord.Vkey())); // inspect the modifiers from the KeyChord and set the flags int he XAML value auto modifiers = AppKeyBindings::ConvertVKModifiers(keyChord.Modifiers()); // add the modifiers to the shortcut menuShortcut.Modifiers(modifiers); // add to the menu menuItem.KeyboardAccelerators().Append(menuShortcut); } else // we've got a comma, so need to just use the alternate method { // extract the modifier and key to a nice format auto overrideString = AppKeyBindings::FormatOverrideShortcutText(keyChord.Modifiers()); menuItem.KeyboardAcceleratorTextOverride(overrideString + L" ,"); } } // -------------------------------- 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(App, TitleChanged, _titleChangeHandlers, TerminalControl::TitleChangedEventArgs); DEFINE_EVENT(App, LastTabClosed, _lastTabClosedHandlers, winrt::TerminalApp::LastTabClosedEventArgs); }