// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "AppHost.h" #include "../types/inc/Viewport.hpp" #include "../types/inc/utils.hpp" #include "../types/inc/User32Utils.hpp" #include "resource.h" using namespace winrt::Windows::UI; using namespace winrt::Windows::UI::Composition; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Hosting; using namespace winrt::Windows::Foundation::Numerics; using namespace winrt::Microsoft::Terminal; using namespace winrt::Microsoft::Terminal::Settings::Model; using namespace ::Microsoft::Console; using namespace ::Microsoft::Console::Types; // This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx // "If the high-order bit is 1, the key is down; otherwise, it is up." static constexpr short KeyPressed{ gsl::narrow_cast(0x8000) }; AppHost::AppHost() noexcept : _app{}, _windowManager{}, _logic{ nullptr }, // don't make one, we're going to take a ref on app's _window{ nullptr } { _logic = _app.Logic(); // get a ref to app's logic // If there were commandline args to our process, try and process them here. // Do this before AppLogic::Create, otherwise this will have no effect. // // This will send our commandline to the Monarch, to ask if we should make a // new window or not. If not, exit immediately. _HandleCommandlineArgs(); if (!_shouldCreateWindow) { return; } _useNonClientArea = _logic.GetShowTabsInTitlebar(); if (_useNonClientArea) { _window = std::make_unique(_logic.GetRequestedTheme()); } else { _window = std::make_unique(); } // Tell the window to callback to us when it's about to handle a WM_CREATE auto pfn = std::bind(&AppHost::_HandleCreateWindow, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); _window->SetCreateCallback(pfn); _window->SetSnapDimensionCallback(std::bind(&winrt::TerminalApp::AppLogic::CalcSnappedDimension, _logic, std::placeholders::_1, std::placeholders::_2)); _window->MouseScrolled({ this, &AppHost::_WindowMouseWheeled }); _window->SetAlwaysOnTop(_logic.GetInitialAlwaysOnTop()); _window->MakeWindow(); } AppHost::~AppHost() { // destruction order is important for proper teardown here _window = nullptr; _app.Close(); _app = nullptr; } bool AppHost::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { if (_logic) { return _logic.OnDirectKeyEvent(vkey, scanCode, down); } return false; } // Method Description: // - Event handler to update the taskbar progress indicator // - Upon receiving the event, we ask the underlying logic for the taskbar state/progress values // of the last active control // Arguments: // - sender: not used // - args: not used void AppHost::SetTaskbarProgress(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::Foundation::IInspectable& /*args*/) { if (_logic) { const auto state = gsl::narrow_cast(_logic.GetLastActiveControlTaskbarState()); const auto progress = gsl::narrow_cast(_logic.GetLastActiveControlTaskbarProgress()); _window->SetTaskbarProgress(state, progress); } } void _buildArgsFromCommandline(std::vector& args) { if (auto commandline{ GetCommandLineW() }) { int argc = 0; // Get the argv, and turn them into a hstring array to pass to the app. wil::unique_any argv{ CommandLineToArgvW(commandline, &argc) }; if (argv) { for (auto& elem : wil::make_range(argv.get(), argc)) { args.emplace_back(elem); } } } if (args.empty()) { args.emplace_back(L"wt.exe"); } } // Method Description: // - Retrieve any commandline args passed on the commandline, and pass them to // the WindowManager, to ask if we should become a window process. // - If we should create a window, then pass the arguments to the app logic for // processing. // - If we shouldn't become a window, set _shouldCreateWindow to false and exit // immediately. // - If the logic determined there's an error while processing that commandline, // display a message box to the user with the text of the error, and exit. // * We display a message box because we're a Win32 application (not a // console app), and the shell has undoubtedly returned to the foreground // of the console. Text emitted here might mix unexpectedly with output // from the shell process. // Arguments: // - // Return Value: // - void AppHost::_HandleCommandlineArgs() { std::vector args; _buildArgsFromCommandline(args); std::wstring cwd{ wil::GetCurrentDirectoryW() }; Remoting::CommandlineArgs eventArgs{ { args }, { cwd } }; _windowManager.ProposeCommandline(eventArgs); _shouldCreateWindow = _windowManager.ShouldCreateWindow(); if (!_shouldCreateWindow) { return; } if (auto peasant{ _windowManager.CurrentWindow() }) { if (auto args{ peasant.InitialArgs() }) { const auto result = _logic.SetStartupCommandline(args.Args()); const auto message = _logic.ParseCommandlineMessage(); if (!message.empty()) { const auto displayHelp = result == 0; const auto messageTitle = displayHelp ? IDS_HELP_DIALOG_TITLE : IDS_ERROR_DIALOG_TITLE; const auto messageIcon = displayHelp ? MB_ICONWARNING : MB_ICONERROR; // TODO:GH#4134: polish this dialog more, to make the text more // like msiexec /? MessageBoxW(nullptr, message.data(), GetStringResource(messageTitle).data(), MB_OK | messageIcon); if (_logic.ShouldExitEarly()) { ExitProcess(result); } } } // After handling the initial args, hookup the callback for handling // future commandline invocations. When our peasant is told to execute a // commandline (in the future), it'll trigger this callback, that we'll // use to send the actions to the app. peasant.ExecuteCommandlineRequested({ this, &AppHost::_DispatchCommandline }); } // TODO:projects/5 if we end up not creating a new window, we crash. I'm // thinking this is because the XAML host is not happy about being torn // down before it has a chance to do really anything. Is there some way // to get the app logic without instantiating the entire app? or at // least the parts we'll need for remoting? } // Method Description: // - Initializes the XAML island, creates the terminal app, and sets the // island's content to that of the terminal app's content. Also registers some // callbacks with TermApp. // !!! IMPORTANT!!! // This must be called *AFTER* WindowsXamlManager::InitializeForCurrentThread. // If it isn't, then we won't be able to create the XAML island. // Arguments: // - // Return Value: // - void AppHost::Initialize() { _window->Initialize(); if (auto withWindow{ _logic.try_as() }) { withWindow->Initialize(_window->GetHandle()); } if (_useNonClientArea) { // Register our callback for when the app's non-client content changes. // This has to be done _before_ App::Create, as the app might set the // content in Create. _logic.SetTitleBarContent({ this, &AppHost::_UpdateTitleBarContent }); } // Register the 'X' button of the window for a warning experience of multiple // tabs opened, this is consistent with Alt+F4 closing _window->WindowCloseButtonClicked([this]() { _logic.WindowCloseButtonClicked(); }); // Add an event handler to plumb clicks in the titlebar area down to the // application layer. _window->DragRegionClicked([this]() { _logic.TitlebarClicked(); }); _logic.RequestedThemeChanged({ this, &AppHost::_UpdateTheme }); _logic.FullscreenChanged({ this, &AppHost::_FullscreenChanged }); _logic.FocusModeChanged({ this, &AppHost::_FocusModeChanged }); _logic.AlwaysOnTopChanged({ this, &AppHost::_AlwaysOnTopChanged }); _logic.RaiseVisualBell({ this, &AppHost::_RaiseVisualBell }); _logic.Create(); _logic.TitleChanged({ this, &AppHost::AppTitleChanged }); _logic.LastTabClosed({ this, &AppHost::LastTabClosed }); _logic.SetTaskbarProgress({ this, &AppHost::SetTaskbarProgress }); _window->UpdateTitle(_logic.Title()); // Set up the content of the application. If the app has a custom titlebar, // set that content as well. _window->SetContent(_logic.GetRoot()); _window->OnAppInitialized(); // THIS IS A HACK // // We've got a weird crash that happens terribly inconsistently, but pretty // readily on migrie's laptop, only in Debug mode. Apparently, there's some // weird ref-counting magic that goes on during teardown, and our // Application doesn't get closed quite right, which can cause us to crash // into the debugger. This of course, only happens on exit, and happens // somewhere in the XamlHost.dll code. // // Crazily, if we _manually leak the Application_ here, then the crash // doesn't happen. This doesn't matter, because we really want the // Application to live for _the entire lifetime of the process_, so the only // time when this object would actually need to get cleaned up is _during // exit_. So we can safely leak this Application object, and have it just // get cleaned up normally when our process exits. ::winrt::TerminalApp::App a{ _app }; ::winrt::detach_abi(a); } // Method Description: // - Called when the app's title changes. Fires off a window message so we can // update the window's title on the main thread. // Arguments: // - sender: unused // - newTitle: the string to use as the new window title // Return Value: // - void AppHost::AppTitleChanged(const winrt::Windows::Foundation::IInspectable& /*sender*/, winrt::hstring newTitle) { _window->UpdateTitle(newTitle); } // Method Description: // - Called when no tab is remaining to close the window. // Arguments: // - sender: unused // - LastTabClosedEventArgs: unused // Return Value: // - void AppHost::LastTabClosed(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::TerminalApp::LastTabClosedEventArgs& /*args*/) { _window->Close(); } // Method Description: // - Resize the window we're about to create to the appropriate dimensions, as // specified in the settings. This will be called during the handling of // WM_CREATE. We'll load the settings for the app, then get the proposed size // of the terminal from the app. Using that proposed size, we'll resize the // window we're creating, so that it'll match the values in the settings. // Arguments: // - hwnd: The HWND of the window we're about to create. // - proposedRect: The location and size of the window that we're about to // create. We'll use this rect to determine which monitor the window is about // to appear on. // - launchMode: A LaunchMode enum reference that indicates the launch mode // Return Value: // - None void AppHost::_HandleCreateWindow(const HWND hwnd, RECT proposedRect, LaunchMode& launchMode) { launchMode = _logic.GetLaunchMode(); // Acquire the actual initial position auto initialPos = _logic.GetInitialPosition(proposedRect.left, proposedRect.top); proposedRect.left = static_cast(initialPos.X); proposedRect.top = static_cast(initialPos.Y); long adjustedHeight = 0; long adjustedWidth = 0; // Find nearest monitor. HMONITOR hmon = MonitorFromRect(&proposedRect, MONITOR_DEFAULTTONEAREST); // Get nearest monitor information MONITORINFO monitorInfo; monitorInfo.cbSize = sizeof(MONITORINFO); GetMonitorInfo(hmon, &monitorInfo); // This API guarantees that dpix and dpiy will be equal, but neither is an // optional parameter so give two UINTs. UINT dpix = USER_DEFAULT_SCREEN_DPI; UINT dpiy = USER_DEFAULT_SCREEN_DPI; // If this fails, we'll use the default of 96. GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); // We need to check if the top left point of the titlebar of the window is within any screen RECT offScreenTestRect; offScreenTestRect.left = proposedRect.left; offScreenTestRect.top = proposedRect.top; offScreenTestRect.right = offScreenTestRect.left + 1; offScreenTestRect.bottom = offScreenTestRect.top + 1; bool isTitlebarIntersectWithMonitors = false; EnumDisplayMonitors( nullptr, &offScreenTestRect, [](HMONITOR, HDC, LPRECT, LPARAM lParam) -> BOOL { auto intersectWithMonitor = reinterpret_cast(lParam); *intersectWithMonitor = true; // Continue the enumeration return FALSE; }, reinterpret_cast(&isTitlebarIntersectWithMonitors)); if (!isTitlebarIntersectWithMonitors) { // If the title bar is out-of-screen, we set the initial position to // the top left corner of the nearest monitor proposedRect.left = monitorInfo.rcWork.left; proposedRect.top = monitorInfo.rcWork.top; } auto initialSize = _logic.GetLaunchDimensions(dpix); const short islandWidth = Utils::ClampToShortMax( static_cast(ceil(initialSize.Width)), 1); const short islandHeight = Utils::ClampToShortMax( static_cast(ceil(initialSize.Height)), 1); // Get the size of a window we'd need to host that client rect. This will // add the titlebar space. const auto nonClientSize = _window->GetTotalNonClientExclusiveSize(dpix); adjustedWidth = islandWidth + nonClientSize.cx; adjustedHeight = islandHeight + nonClientSize.cy; const COORD origin{ gsl::narrow(proposedRect.left), gsl::narrow(proposedRect.top) }; const COORD dimensions{ Utils::ClampToShortMax(adjustedWidth, 1), Utils::ClampToShortMax(adjustedHeight, 1) }; const auto newPos = Viewport::FromDimensions(origin, dimensions); bool succeeded = SetWindowPos(hwnd, nullptr, newPos.Left(), newPos.Top(), newPos.Width(), newPos.Height(), SWP_NOACTIVATE | SWP_NOZORDER); // Refresh the dpi of HWND because the dpi where the window will launch may be different // at this time _window->RefreshCurrentDPI(); // If we can't resize the window, that's really okay. We can just go on with // the originally proposed window size. LOG_LAST_ERROR_IF(!succeeded); TraceLoggingWrite( g_hWindowsTerminalProvider, "WindowCreated", TraceLoggingDescription("Event emitted upon creating the application window"), TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); } // Method Description: // - Called when the app wants to set its titlebar content. We'll take the // UIElement and set the Content property of our Titlebar that element. // Arguments: // - sender: unused // - arg: the UIElement to use as the new Titlebar content. // Return Value: // - void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::UIElement& arg) { if (_useNonClientArea) { (static_cast(_window.get()))->SetTitlebarContent(arg); } } // Method Description: // - Called when the app wants to change its theme. We'll forward this to the // IslandWindow, so it can update the root UI element of the entire XAML tree. // Arguments: // - sender: unused // - arg: the ElementTheme to use as the new theme for the UI // Return Value: // - void AppHost::_UpdateTheme(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::ElementTheme& arg) { _window->OnApplicationThemeChanged(arg); } void AppHost::_FocusModeChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { _window->FocusModeChanged(_logic.FocusMode()); } void AppHost::_FullscreenChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { _window->FullscreenChanged(_logic.Fullscreen()); } void AppHost::_AlwaysOnTopChanged(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { _window->SetAlwaysOnTop(_logic.AlwaysOnTop()); } // Method Description // - Called when the app wants to flash the taskbar, indicating to the user that // something needs their attention // Arguments // - void AppHost::_RaiseVisualBell(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::Foundation::IInspectable&) { _window->FlashTaskbar(); } // Method Description: // - Called when the IslandWindow has received a WM_MOUSEWHEEL message. This can // happen on some laptops, where their trackpads won't scroll inactive windows // _ever_. // - We're going to take that message and manually plumb it through to our // TermControl's, or anything else that implements IMouseWheelListener. // - See GH#979 for more details. // Arguments: // - coord: The Window-relative, logical coordinates location of the mouse during this event. // - delta: the wheel delta that triggered this event. // Return Value: // - void AppHost::_WindowMouseWheeled(const til::point coord, const int32_t delta) { if (_logic) { // Find all the elements that are underneath the mouse auto elems = winrt::Windows::UI::Xaml::Media::VisualTreeHelper::FindElementsInHostCoordinates(coord, _logic.GetRoot()); for (const auto& e : elems) { // If that element has implemented IMouseWheelListener, call OnMouseWheel on that element. if (auto control{ e.try_as() }) { try { // Translate the event to the coordinate space of the control // we're attempting to dispatch it to const auto transform = e.TransformToVisual(nullptr); const til::point controlOrigin{ til::math::flooring, transform.TransformPoint(til::point{ 0, 0 }) }; const til::point offsetPoint = coord - controlOrigin; const auto lButtonDown = WI_IsFlagSet(GetKeyState(VK_LBUTTON), KeyPressed); const auto mButtonDown = WI_IsFlagSet(GetKeyState(VK_MBUTTON), KeyPressed); const auto rButtonDown = WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed); if (control.OnMouseWheel(offsetPoint, delta, lButtonDown, mButtonDown, rButtonDown)) { // If the element handled the mouse wheel event, don't // continue to iterate over the remaining controls. break; } } CATCH_LOG(); } } } } bool AppHost::HasWindow() { return _shouldCreateWindow; } void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable /*sender*/, winrt::Microsoft::Terminal::Remoting::CommandlineArgs args) { _logic.ExecuteCommandline(args.Args()); }