terminal/src/cascadia/TerminalApp/App.cpp
Hao f74a9d3e0b add shortcut alt-* for select tab (#623)
* add shortcut alt-* for select tab

* all right, 0 for 10th
2019-05-10 09:48:36 -07:00

890 lines
35 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "App.h"
#include <shellapi.h>
#include <filesystem>
#include <winrt/Microsoft.UI.Xaml.XamlTypeInfo.h>
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;
// 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 }
{
// 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:
// - <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);
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: <Grid Background="{ThemeResource
// ApplicationPageBackgroundThemeBrush}"> 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<IInspectable, IInspectable>, 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<winrt::Windows::UI::Xaml::Style>();
_tabRow.Style(style);
}
// Apply the UI theme from our settings to our UI elements
_ApplyTheme(_settings->GlobalSettings().GetRequestedTheme());
_OpenNewTab(std::nullopt);
}
// 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:
// - 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{};
for (int profileIndex = 0; profileIndex < _settings->GetProfiles().size(); profileIndex++)
{
const auto& profile = _settings->GetProfiles()[profileIndex];
auto profileMenuItem = Controls::MenuFlyoutItem{};
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));
}
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);
// 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:
// - <none>
// Return Value:
// - <none>
void App::_SettingsButtonOnClick(const IInspectable&,
const RoutedEventArgs&)
{
LaunchSettings();
}
// Method Description:
// - Called when the feedback button is clicked. Launches the feedback hub
// to the list of all feedback for the Terminal app.
void App::_FeedbackButtonOnClick(const IInspectable&,
const RoutedEventArgs&)
{
// If you want this to go to the new feedback page automatically, use &newFeedback=true
winrt::Windows::System::Launcher::LaunchUriAsync({ L"feedback-hub://?tabid=2&appid=Microsoft.WindowsTerminal_8wekyb3d8bbwe!App" });
}
// 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]() { _DoScroll(-1); });
bindings.ScrollDown([this]() { _DoScroll(1); });
bindings.NextTab([this]() { _SelectNextTab(true); });
bindings.PrevTab([this]() { _SelectNextTab(false); });
bindings.SwitchToTab([this](const auto index) { _SelectTab({ index }); });
}
// 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()
{
_settings = CascadiaSettings::LoadAll();
_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:
// - <none>
// Return Value:
// - <none>
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 ofentimes 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.
// Arguments:
// - <none>
// Return Value:
// - <none>
void App::_ReloadSettings()
{
_settings = CascadiaSettings::LoadAll();
// Re-wire the keybindings to their handlers, as we'll have created a
// new AppKeyBindings object.
_HookupKeyBindings(_settings->GetKeybindings());
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<int> 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<int>(_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<Tab>(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);
// TODO: MSFT:21268795: Need a better story for what should happen when the last tab is closed.
if (p != nullptr && p->GetCloseOnExit() && _tabs.size() > 1)
{
_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();
}
// Method Description:
// - Close the currently focused tab. Focus will move to the left, if possible.
void App::_CloseFocusedTab()
{
if (_tabs.size() > 1)
{
int focusedTabIndex = _GetFocusedTabIndex();
std::shared_ptr<Tab> focusedTab{ _tabs[focusedTabIndex] };
// We're not calling _FocusTab here because it makes an async dispatch
// that is practically guaranteed to not happen before we delete the tab.
_tabView.SelectedIndex((focusedTabIndex > 0) ? focusedTabIndex - 1 : 1);
_tabView.Items().RemoveAt(focusedTabIndex);
_tabs.erase(_tabs.begin() + focusedTabIndex);
_UpdateTabView();
}
}
// 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::_DoScroll(int delta)
{
int focusedTabIndex = _GetFocusedTabIndex();
_tabs[focusedTabIndex]->Scroll(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<Tab> 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<int>((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<MUX::Controls::TabView>();
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)
{
// Don't allow the user to close the last tab ..
// .. yet.
if (_tabs.size() > 1)
{
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:
// - <none>
// 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)
{
uint32_t tabIndexFromControl = 0;
_tabView.Items().IndexOf(tabViewItem, tabIndexFromControl);
if (tabIndexFromControl == _GetFocusedTabIndex())
{
_tabView.SelectedIndex((tabIndexFromControl > 0) ? tabIndexFromControl - 1 : 1);
}
// Removing the tab from the collection will destroy its control and disconnect its connection.
_tabs.erase(_tabs.begin() + tabIndexFromControl);
_tabView.Items().RemoveAt(tabIndexFromControl);
}
// 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 };
}
}
// -------------------------------- 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);
}