terminal/src/cascadia/WindowsTerminal/AppHost.cpp
Mike Griese 847749f19e
Force activate existing windows when running a commandline in them (#9137)
This will make sure to summon the terminal window when running a
commandline in it. 
* If the window is on another desktop, the OS will switch to the desktop
  the window is on. 
* If the window is minimized, it will restore it.

This is taken from my quake mode branch. It works aggressively. 848682a,
fee6473, 342d3f2, 5052d31 all had other attempts at doing this, but they
didn't work reliably. Part of the trick is that I don't _think_ Windows
wants one process to be able to move another process into the
foreground. In this case though, we _do_ want to move ourself into the
foreground, and this `AttachThreadInput` hack seems to be the only way
to do it reliably.

References #5000
Uses code authored for #653

Closes https://github.com/microsoft/terminal/projects/5#card-54636373
2021-02-17 21:37:22 +00:00

569 lines
23 KiB
C++

// 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<short>(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
// Inform the WindowManager that it can use us to find the target window for
// a set of commandline args. This needs to be done before
// _HandleCommandlineArgs, because WE might end up being the monarch. That
// would mean we'd need to be responsible for looking that up.
_windowManager.FindTargetWindowRequested({ this, &AppHost::_FindTargetWindow });
// 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<NonClientIslandWindow>(_logic.GetRequestedTheme());
}
else
{
_window = std::make_unique<IslandWindow>();
}
// 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->WindowActivated({ this, &AppHost::_WindowActivated });
_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<size_t>(_logic.GetLastActiveControlTaskbarState());
const auto progress = gsl::narrow_cast<size_t>(_logic.GetLastActiveControlTaskbarProgress());
_window->SetTaskbarProgress(state, progress);
}
}
void _buildArgsFromCommandline(std::vector<winrt::hstring>& 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<LPWSTR*, decltype(&::LocalFree), ::LocalFree> 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:
// - <none>
// Return Value:
// - <none>
void AppHost::_HandleCommandlineArgs()
{
std::vector<winrt::hstring> args;
_buildArgsFromCommandline(args);
std::wstring cwd{ wil::GetCurrentDirectoryW<std::wstring>() };
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.Commandline());
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 });
}
}
// 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:
// - <none>
// Return Value:
// - <none>
void AppHost::Initialize()
{
_window->Initialize();
if (auto withWindow{ _logic.try_as<IInitializeWithWindow>() })
{
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:
// - <none>
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:
// - <none>
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<long>(initialPos.X);
proposedRect.top = static_cast<long>(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<bool*>(lParam);
*intersectWithMonitor = true;
// Continue the enumeration
return FALSE;
},
reinterpret_cast<LPARAM>(&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<long>(ceil(initialSize.Width)), 1);
const short islandHeight = Utils::ClampToShortMax(
static_cast<long>(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<short>(proposedRect.left),
gsl::narrow<short>(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:
// - <none>
void AppHost::_UpdateTitleBarContent(const winrt::Windows::Foundation::IInspectable&, const winrt::Windows::UI::Xaml::UIElement& arg)
{
if (_useNonClientArea)
{
(static_cast<NonClientIslandWindow*>(_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:
// - <none>
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
// - <unused>
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:
// - <none>
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<winrt::Microsoft::Terminal::TerminalControl::IMouseWheelListener>() })
{
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;
}
// Method Description:
// - Event handler for the Peasant::ExecuteCommandlineRequested event. Take the
// provided commandline args, and attempt to parse them and perform the
// actions immediately. The parsing is performed by AppLogic.
// - This is invoked when another wt.exe instance runs something like `wt -w 1
// new-tab`, and the Monarch delegates the commandline to this instance.
// Arguments:
// - args: the bundle of a commandline and working directory to use for this invocation.
// Return Value:
// - <none>
void AppHost::_DispatchCommandline(winrt::Windows::Foundation::IInspectable /*sender*/,
Remoting::CommandlineArgs args)
{
_window->SummonWindow();
_logic.ExecuteCommandline(args.Commandline(), args.CurrentDirectory());
}
// Method Description:
// - Event handler for the WindowManager::FindTargetWindowRequested event. The
// manager will ask us how to figure out what the target window is for a set
// of commandline arguments. We'll take those arguments, and ask AppLogic to
// parse them for us. We'll then set ResultTargetWindow in the given args, so
// the sender can use that result.
// Arguments:
// - args: the bundle of a commandline and working directory to find the correct target window for.
// Return Value:
// - <none>
void AppHost::_FindTargetWindow(const winrt::Windows::Foundation::IInspectable& /*sender*/,
const Remoting::FindTargetWindowArgs& args)
{
const auto targetWindow = _logic.FindTargetWindow(args.Args().Commandline());
args.ResultTargetWindow(targetWindow);
}
winrt::fire_and_forget AppHost::_WindowActivated()
{
co_await winrt::resume_background();
if (auto peasant{ _windowManager.CurrentWindow() })
{
// TODO: projects/5 - in the future, we'll want to actually get the
// desktop GUID in IslandWindow, and bubble that up here, then down to
// the Peasant. For now, we're just leaving space for it.
Remoting::WindowActivatedArgs args{ peasant.GetID(),
winrt::guid{},
winrt::clock().now() };
peasant.ActivateWindow(args);
}
}