mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-10 18:43:54 -06:00
This changes the ConPTY handoff COM server from `REGCLS_SINGLEUSE` to `REGCLS_MULTIPLEUSE`. The former causes a race condition, because handoff runs concurrently with the creation of WinUI windows. This can then result in the a window getting the wrong handoff. It then moves the "root" of ConPTY handoff from `TerminalPage` (WindowEmperor -> AppHost -> TerminalWindow -> TerminalPage) into `WindowEmperor` (WindowEmperor). Closes #19049 ## Validation Steps Performed * Launching cmd from the Start Menu shows a "Command Prompt" tab ✅ * Win+R -> `cmd` creates windows in the foreground ✅ * Win+R -> `cmd /c start /max cmd` creates a fullscreen tab ✅ * This even works for multiple windows, unlike with Canary ✅ * Win+R -> `cmd /c start /min cmd` does not work ❌ * It also doesn't work in Canary, so it's not a bug in this PR ✅
1880 lines
70 KiB
C++
1880 lines
70 KiB
C++
// Copyright (c) Microsoft Corporation.
|
||
// Licensed under the MIT license.
|
||
|
||
#include "pch.h"
|
||
#include "IslandWindow.h"
|
||
#include "../types/inc/Viewport.hpp"
|
||
#include "resource.h"
|
||
#include "icon.h"
|
||
#include <dwmapi.h>
|
||
#include <TerminalThemeHelpers.h>
|
||
#include <CoreWindow.h>
|
||
|
||
extern "C" IMAGE_DOS_HEADER __ImageBase;
|
||
|
||
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::Settings::Model;
|
||
using namespace winrt::Microsoft::Terminal::Control;
|
||
using namespace winrt::Microsoft::Terminal;
|
||
using namespace ::Microsoft::Console::Types;
|
||
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
|
||
|
||
#define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS"
|
||
#define IDM_SYSTEM_MENU_BEGIN 0x1000
|
||
|
||
// WinUI doesn't support the "Hide cursor on input" setting which is why all the modern Windows apps
|
||
// are broken in that respect. We want to support it though, so we implement an imitation of it here.
|
||
// We use the classic ShowCursor() API to hide it on keydown and show it on a select few messages.
|
||
// WinUI's SetPointerCapture() cannot be used for this, because it races with internal WinUI code
|
||
// calling that function and has proven itself to be very unreliable in practice.
|
||
//
|
||
// With UWP half the input stack got split off, and so most input events get rerouted through
|
||
// the CoreInput child window running in another thread (aka InputHost aka Windows.UI.Input).
|
||
// HideCursor() is called by WindowEmperor because WM_KEYDOWN is otherwise sent directly to that
|
||
// CoreInput window. Same for WM_POINTERUPDATE which we use to reliably detect cursor movement.
|
||
// WM_ACTIVATE on the other hand is only sent to each specific window and cannot be hooked by
|
||
// inspecting the MSG struct coming from GetMessage. That's why the code must be here.
|
||
// Best not think about this too much...
|
||
bool IslandWindow::IsCursorHidden() noexcept
|
||
{
|
||
return _cursorHidden;
|
||
}
|
||
|
||
void IslandWindow::HideCursor() noexcept
|
||
{
|
||
static const auto shouldVanish = []() noexcept {
|
||
BOOL shouldVanish = TRUE;
|
||
SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
|
||
return shouldVanish != FALSE;
|
||
}();
|
||
|
||
if (!_cursorHidden && shouldVanish)
|
||
{
|
||
ShowCursor(FALSE);
|
||
_cursorHidden = true;
|
||
}
|
||
}
|
||
|
||
void IslandWindow::ShowCursorMaybe(const UINT message) noexcept
|
||
{
|
||
if (_cursorHidden &&
|
||
(message == WM_ACTIVATE ||
|
||
message == WM_POINTERUPDATE ||
|
||
message == WM_NCPOINTERUPDATE))
|
||
{
|
||
_cursorHidden = false;
|
||
ShowCursor(TRUE);
|
||
}
|
||
}
|
||
|
||
IslandWindow::IslandWindow() noexcept :
|
||
_interopWindowHandle{ nullptr },
|
||
_rootGrid{ nullptr },
|
||
_source{ nullptr },
|
||
_pfnCreateCallback{ nullptr }
|
||
{
|
||
}
|
||
|
||
IslandWindow::~IslandWindow()
|
||
{
|
||
Close();
|
||
}
|
||
|
||
void IslandWindow::Close()
|
||
{
|
||
// GH#15454: Unset the user data for the window. This will prevent future
|
||
// callbacks that come onto our window message loop from being sent to the
|
||
// IslandWindow (or other derived class's) implementation.
|
||
//
|
||
// Specifically, this prevents a pending coroutine from being able to call
|
||
// something like ShowWindow, and have that come back on the IslandWindow
|
||
// message loop, where it'll end up asking XAML something that XAML is no
|
||
// longer able to answer.
|
||
SetWindowLongPtr(_window.get(), GWLP_USERDATA, 0);
|
||
|
||
if (_source)
|
||
{
|
||
// BODGY
|
||
// WinUI will strongly hold onto the first DesktopWindowXamlSource that is created.
|
||
// If we don't manually set the Content() to null first, closing that first window
|
||
// will leak all of its contents permanently.
|
||
_source.Content(nullptr);
|
||
|
||
_source.Close();
|
||
_source = nullptr;
|
||
}
|
||
}
|
||
|
||
HWND IslandWindow::GetInteropHandle() const
|
||
{
|
||
return _interopWindowHandle;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Create the actual window that we'll use for the application.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::MakeWindow() noexcept
|
||
{
|
||
if (_window)
|
||
{
|
||
// no-op if we already have a window.
|
||
return;
|
||
}
|
||
|
||
WNDCLASS wc{};
|
||
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||
wc.hInstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||
wc.lpszClassName = XAML_HOSTING_WINDOW_CLASS_NAME;
|
||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||
wc.lpfnWndProc = WndProc;
|
||
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON));
|
||
RegisterClass(&wc);
|
||
WINRT_ASSERT(!_window);
|
||
|
||
// Create the window with the default size here - During the creation of the
|
||
// window, the system will give us a chance to set its size in WM_CREATE.
|
||
// WM_CREATE will be handled synchronously, before CreateWindow returns.
|
||
//
|
||
// We need WS_EX_NOREDIRECTIONBITMAP for vintage style opacity, GH#603
|
||
//
|
||
// WS_EX_LAYERED acts REAL WEIRD with TerminalTrySetTransparentBackground,
|
||
// but it works just fine when the window is in the TOPMOST group. But if
|
||
// you enable it always, activating the window will remove our DWM frame
|
||
// entirely. Weird.
|
||
WINRT_VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP | (_alwaysOnTop ? WS_EX_TOPMOST : 0),
|
||
wc.lpszClassName,
|
||
L"Windows Terminal",
|
||
WS_OVERLAPPEDWINDOW,
|
||
CW_USEDEFAULT,
|
||
CW_USEDEFAULT,
|
||
CW_USEDEFAULT,
|
||
CW_USEDEFAULT,
|
||
nullptr,
|
||
nullptr,
|
||
wc.hInstance,
|
||
this));
|
||
|
||
WINRT_ASSERT(_window);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Set a callback to be called when we process a WM_CREATE message. This gives
|
||
// the AppHost a chance to resize the window to the proper size.
|
||
// Arguments:
|
||
// - pfn: a function to be called during the handling of WM_CREATE. It takes two
|
||
// parameters:
|
||
// * HWND: the HWND of the window that's being created.
|
||
// * til::rect: The position on the screen that the system has proposed for our
|
||
// window.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::SetCreateCallback(std::function<void(const HWND, const til::rect&)> pfn) noexcept
|
||
{
|
||
_pfnCreateCallback = pfn;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Set a callback to be called when the window is being resized by user. For given
|
||
// requested window dimension (width or height, whichever border is dragged) it should
|
||
// return a resulting window dimension that is actually set. It is used to make the
|
||
// window 'snap' to the underling terminal's character grid.
|
||
// Arguments:
|
||
// - pfn: a function that transforms requested to actual window dimension.
|
||
// pfn's parameters:
|
||
// * widthOrHeight: whether the dimension is width (true) or height (false)
|
||
// * dimension: The requested dimension that comes from user dragging a border
|
||
// of the window. It is in pixels and represents only the client area.
|
||
// pfn's return value:
|
||
// * A dimension of client area that the window should resize to.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::SetSnapDimensionCallback(std::function<float(bool, float)> pfn) noexcept
|
||
{
|
||
_pfnSnapDimensionCallback = pfn;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handles a WM_CREATE message. Calls our create callback, if one's been set.
|
||
// Arguments:
|
||
// - wParam: unused
|
||
// - lParam: the lParam of a WM_CREATE, which is a pointer to a CREATESTRUCTW
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexcept
|
||
{
|
||
// Get proposed window rect from create structure
|
||
auto pcs = reinterpret_cast<CREATESTRUCTW*>(lParam);
|
||
til::rect rc;
|
||
rc.left = pcs->x;
|
||
rc.top = pcs->y;
|
||
rc.right = rc.left + pcs->cx;
|
||
rc.bottom = rc.top + pcs->cy;
|
||
|
||
if (_pfnCreateCallback)
|
||
{
|
||
_pfnCreateCallback(_window.get(), rc);
|
||
}
|
||
|
||
// GH#11561: DO NOT call ShowWindow here. The AppHost will call ShowWindow
|
||
// once the app has completed its initialization.
|
||
|
||
UpdateWindow(_window.get());
|
||
|
||
UpdateWindowIconForActiveMetrics(_window.get());
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handles a WM_SIZING message, which occurs when user drags a window border
|
||
// or corner. It intercepts this resize action and applies 'snapping' i.e.
|
||
// aligns the terminal's size to its cell grid. We're given the window size,
|
||
// which we then adjust based on the terminal's properties (like font size).
|
||
// Arguments:
|
||
// - wParam: Specifies which edge of the window is being dragged.
|
||
// - lParam: Pointer to the requested window rectangle (this is, the one that
|
||
// originates from current drag action). It also acts as the return value
|
||
// (it's a ref parameter).
|
||
// Return Value:
|
||
// - <none>
|
||
LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam)
|
||
{
|
||
if (!_pfnSnapDimensionCallback)
|
||
{
|
||
// If we haven't been given the callback that would adjust the dimension,
|
||
// then we can't do anything, so just bail out.
|
||
return false;
|
||
}
|
||
|
||
auto winRect = reinterpret_cast<LPRECT>(lParam);
|
||
|
||
// If we're the quake window, prevent resizing on all sides except the
|
||
// bottom. This also applies to resizing with the Alt+Space menu
|
||
if (IsQuakeWindow() && wParam != WMSZ_BOTTOM)
|
||
{
|
||
// Stuff our current window size into the lParam, and return true. This
|
||
// will tell User32 to use our current dimensions to resize to.
|
||
::GetWindowRect(_window.get(), winRect);
|
||
return true;
|
||
}
|
||
|
||
// Find nearest monitor.
|
||
auto hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST);
|
||
|
||
// 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. I think it can only fail for
|
||
// bad parameters, which we won't have, so no big deal.
|
||
LOG_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy));
|
||
|
||
const auto nonClientSize = GetTotalNonClientExclusiveSize(dpix);
|
||
const auto dipPerPx = static_cast<float>(USER_DEFAULT_SCREEN_DPI) / static_cast<float>(dpix);
|
||
const auto pxPerDip = static_cast<float>(dpix) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
|
||
|
||
auto clientWidth = winRect->right - winRect->left - nonClientSize.width;
|
||
auto clientHeight = winRect->bottom - winRect->top - nonClientSize.height;
|
||
|
||
if (wParam != WMSZ_TOP && wParam != WMSZ_BOTTOM)
|
||
{
|
||
// If user has dragged anything but the top or bottom border (so e.g. left border,
|
||
// top-right corner etc.), then this means that the width has changed. We thus ask to
|
||
// adjust this new width so that terminal(s) is/are aligned to their character grid(s).
|
||
auto width = clientWidth * dipPerPx;
|
||
width = std::max(width, minimumWidth);
|
||
width = _pfnSnapDimensionCallback(true, width);
|
||
clientWidth = lroundf(width * pxPerDip);
|
||
}
|
||
if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT)
|
||
{
|
||
// Analogous to above, but for height.
|
||
auto height = clientHeight * dipPerPx;
|
||
height = std::max(height, minimumHeight);
|
||
height = _pfnSnapDimensionCallback(false, height);
|
||
clientHeight = lroundf(height * pxPerDip);
|
||
}
|
||
|
||
// Now make the window rectangle match the calculated client width and height,
|
||
// regarding which border the user is dragging. E.g. if user drags left border, then
|
||
// we make sure to adjust the 'left' component of rectangle and not the 'right'. Note
|
||
// that top-left and bottom-left corners also 'include' left border, hence we match
|
||
// this in multi-case switch.
|
||
|
||
// Set width
|
||
switch (wParam)
|
||
{
|
||
case WMSZ_LEFT:
|
||
case WMSZ_TOPLEFT:
|
||
case WMSZ_BOTTOMLEFT:
|
||
winRect->left = winRect->right - (clientWidth + nonClientSize.width);
|
||
break;
|
||
case WMSZ_RIGHT:
|
||
case WMSZ_TOPRIGHT:
|
||
case WMSZ_BOTTOMRIGHT:
|
||
winRect->right = winRect->left + (clientWidth + nonClientSize.width);
|
||
break;
|
||
}
|
||
|
||
// Set height
|
||
switch (wParam)
|
||
{
|
||
case WMSZ_BOTTOM:
|
||
case WMSZ_BOTTOMLEFT:
|
||
case WMSZ_BOTTOMRIGHT:
|
||
winRect->bottom = winRect->top + (clientHeight + nonClientSize.height);
|
||
break;
|
||
case WMSZ_TOP:
|
||
case WMSZ_TOPLEFT:
|
||
case WMSZ_TOPRIGHT:
|
||
winRect->top = winRect->bottom - (clientHeight + nonClientSize.height);
|
||
break;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handle the WM_MOVING message
|
||
// - If we're the quake window, then we don't want to be able to be moved.
|
||
// Immediately return our current window position, which will prevent us from
|
||
// being moved at all.
|
||
// Arguments:
|
||
// - lParam: a LPRECT with the proposed window position, that should be filled
|
||
// with the resultant position.
|
||
// Return Value:
|
||
// - true iff we handled this message.
|
||
LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam)
|
||
{
|
||
auto winRect = reinterpret_cast<LPRECT>(lParam);
|
||
|
||
// If we're the quake window, prevent moving the window. If we don't do
|
||
// this, then Alt+Space...Move will still be able to move the window.
|
||
if (IsQuakeWindow())
|
||
{
|
||
// Stuff our current window into the lParam, and return true. This
|
||
// will tell User32 to use our current position to move to.
|
||
::GetWindowRect(_window.get(), winRect);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Start this window for the first time. This will instantiate our XAML
|
||
// island, set up our root grid, and initialize some other members that only
|
||
// need to be initialized once.
|
||
// - This should only be called once.
|
||
void IslandWindow::Initialize()
|
||
{
|
||
_source = DesktopWindowXamlSource{};
|
||
|
||
auto interop = _source.as<IDesktopWindowXamlSourceNative>();
|
||
winrt::check_hresult(interop->AttachToWindow(_window.get()));
|
||
|
||
// stash the child interop handle so we can resize it when the main hwnd is resized
|
||
interop->get_WindowHandle(&_interopWindowHandle);
|
||
|
||
// Immediately hide our XAML island hwnd. On earlier versions of Windows,
|
||
// this HWND could sometimes appear as an actual window in the taskbar
|
||
// without this!
|
||
ShowWindow(_interopWindowHandle, SW_HIDE);
|
||
|
||
_rootGrid = winrt::Windows::UI::Xaml::Controls::Grid();
|
||
_source.Content(_rootGrid);
|
||
|
||
// initialize the taskbar object
|
||
if (auto taskbar = wil::CoCreateInstanceNoThrow<ITaskbarList3>(CLSID_TaskbarList))
|
||
{
|
||
if (SUCCEEDED(taskbar->HrInit()))
|
||
{
|
||
_taskbar = std::move(taskbar);
|
||
}
|
||
}
|
||
|
||
_systemMenuNextItemId = IDM_SYSTEM_MENU_BEGIN;
|
||
|
||
// Enable vintage opacity by removing the XAML emergency backstop, GH#603.
|
||
// We don't really care if this failed or not.
|
||
TerminalTrySetTransparentBackground(true);
|
||
}
|
||
|
||
void IslandWindow::OnSize(const UINT width, const UINT height)
|
||
{
|
||
// NOTE: This _isn't_ called by NonClientIslandWindow::OnSize. The
|
||
// NonClientIslandWindow has very different logic for positioning the
|
||
// DesktopWindowXamlSource inside its HWND.
|
||
|
||
// update the interop window size
|
||
SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE);
|
||
|
||
if (_rootGrid)
|
||
{
|
||
const auto size = GetLogicalSize();
|
||
_rootGrid.Width(size.Width);
|
||
_rootGrid.Height(size.Height);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Handles a WM_GETMINMAXINFO message, issued before the window sizing starts.
|
||
// This message allows to modify the minimal and maximal dimensions of the window.
|
||
// We focus on minimal dimensions here
|
||
// (the maximal dimension will be calculate upon maximizing)
|
||
// Our goal is to protect against to downsizing to less than minimal allowed dimensions,
|
||
// that might occur in the scenarios where _OnSizing is bypassed.
|
||
// An example of such scenario is anchoring the window to the top/bottom screen border
|
||
// in order to maximize window height (GH# 8026).
|
||
// The computation is similar to what we do in _OnSizing:
|
||
// we need to consider both the client area and non-client exclusive area sizes,
|
||
// while taking DPI into account as well.
|
||
// Arguments:
|
||
// - lParam: Pointer to the requested MINMAXINFO struct,
|
||
// a ptMinTrackSize field of which we want to update with the computed dimensions.
|
||
// It also acts as the return value (it's a ref parameter).
|
||
// Return Value:
|
||
// - <none>
|
||
|
||
void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam)
|
||
{
|
||
// Without a callback we don't know to snap the dimensions of the client area.
|
||
// Should not be a problem, the callback is not set early in the startup
|
||
// The initial dimensions will be set later on
|
||
if (!_pfnSnapDimensionCallback)
|
||
{
|
||
return;
|
||
}
|
||
|
||
auto hmon = MonitorFromWindow(GetHandle(), MONITOR_DEFAULTTONEAREST);
|
||
if (hmon == NULL)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UINT dpix = USER_DEFAULT_SCREEN_DPI;
|
||
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
|
||
GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy);
|
||
|
||
// From now we use dpix for all computations (same as in _OnSizing).
|
||
const auto nonClientSizeScaled = GetTotalNonClientExclusiveSize(dpix);
|
||
const auto pxPerDip = static_cast<float>(dpix) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
|
||
|
||
// We might have been called in WM_CREATE, before we've initialized XAML or
|
||
// our page. That's okay.
|
||
const auto width = _pfnSnapDimensionCallback(true, minimumWidth);
|
||
const auto height = _pfnSnapDimensionCallback(false, minimumHeight);
|
||
|
||
auto lpMinMaxInfo = reinterpret_cast<LPMINMAXINFO>(lParam);
|
||
lpMinMaxInfo->ptMinTrackSize.x = lroundf(width * pxPerDip) + nonClientSizeScaled.width;
|
||
lpMinMaxInfo->ptMinTrackSize.y = lroundf(height * pxPerDip) + nonClientSizeScaled.height;
|
||
}
|
||
|
||
[[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
|
||
{
|
||
if (IsCursorHidden())
|
||
{
|
||
ShowCursorMaybe(message);
|
||
}
|
||
|
||
switch (message)
|
||
{
|
||
case WM_GETMINMAXINFO:
|
||
{
|
||
_OnGetMinMaxInfo(wparam, lparam);
|
||
return 0;
|
||
}
|
||
case WM_CREATE:
|
||
{
|
||
_HandleCreateWindow(wparam, lparam);
|
||
return 0;
|
||
}
|
||
case WM_ENABLE:
|
||
{
|
||
if (_interopWindowHandle != nullptr)
|
||
{
|
||
// send focus to the child window
|
||
SetFocus(_interopWindowHandle);
|
||
}
|
||
break;
|
||
}
|
||
case WM_SETFOCUS:
|
||
{
|
||
if (_interopWindowHandle != nullptr)
|
||
{
|
||
// send focus to the child window
|
||
SetFocus(_interopWindowHandle);
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
case WM_ACTIVATE:
|
||
{
|
||
// wparam = 0 indicates the window was deactivated
|
||
const bool activated = LOWORD(wparam) != 0;
|
||
WindowActivated.raise(activated);
|
||
|
||
if (_autoHideWindow && !activated)
|
||
{
|
||
if (_isQuakeWindow || _minimizeToNotificationArea)
|
||
{
|
||
HideWindow();
|
||
}
|
||
else
|
||
{
|
||
ShowWindow(GetHandle(), SW_MINIMIZE);
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case WM_NCLBUTTONDOWN:
|
||
case WM_NCLBUTTONUP:
|
||
case WM_NCMBUTTONDOWN:
|
||
case WM_NCMBUTTONUP:
|
||
case WM_NCRBUTTONDOWN:
|
||
case WM_NCRBUTTONUP:
|
||
case WM_NCXBUTTONDOWN:
|
||
case WM_NCXBUTTONUP:
|
||
{
|
||
// If we clicked in the titlebar, raise an event so the app host can
|
||
// dispatch an appropriate event.
|
||
DragRegionClicked.raise();
|
||
break;
|
||
}
|
||
case WM_MENUCHAR:
|
||
{
|
||
// GH#891: return this LRESULT here to prevent the app from making a
|
||
// bell when alt+key is pressed. A menu is active and the user presses a
|
||
// key that does not correspond to any mnemonic or accelerator key,
|
||
return MAKELRESULT(0, MNC_CLOSE);
|
||
}
|
||
case WM_SIZING:
|
||
{
|
||
return _OnSizing(wparam, lparam);
|
||
}
|
||
case WM_SIZE:
|
||
{
|
||
if (wparam == SIZE_RESTORED || wparam == SIZE_MAXIMIZED)
|
||
{
|
||
WindowVisibilityChanged.raise(true);
|
||
MaximizeChanged.raise(wparam == SIZE_MAXIMIZED);
|
||
}
|
||
|
||
if (wparam == SIZE_MINIMIZED)
|
||
{
|
||
WindowVisibilityChanged.raise(false);
|
||
if (_isQuakeWindow)
|
||
{
|
||
ShowWindow(GetHandle(), SW_HIDE);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
break;
|
||
}
|
||
case WM_MOVING:
|
||
{
|
||
return _OnMoving(wparam, lparam);
|
||
}
|
||
case WM_MOVE:
|
||
{
|
||
WindowMoved.raise();
|
||
break;
|
||
}
|
||
case WM_CLOSE:
|
||
{
|
||
// If the user wants to close the app by clicking 'X' button,
|
||
// we hand off the close experience to the app layer. If all the tabs
|
||
// are closed, the window will be closed as well.
|
||
WindowCloseButtonClicked.raise();
|
||
return 0;
|
||
}
|
||
case WM_MOUSEWHEEL:
|
||
try
|
||
{
|
||
// This whole handler is a hack for GH#979.
|
||
//
|
||
// On some laptops, their trackpads won't scroll inactive windows
|
||
// _ever_. With our entire window just being one giant XAML Island, the
|
||
// touchpad driver thinks our entire window is inactive, and won't
|
||
// scroll the XAML island. On those types of laptops, we'll get a
|
||
// WM_MOUSEWHEEL here, in our root window, when the trackpad scrolls.
|
||
// We're going to take that message and manually plumb it through to our
|
||
// TermControl's, or anything else that implements IMouseWheelListener.
|
||
|
||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx
|
||
// Important! Do not use the LOWORD or HIWORD macros to extract the x-
|
||
// and y- coordinates of the cursor position because these macros return
|
||
// incorrect results on systems with multiple monitors. Systems with
|
||
// multiple monitors can have negative x- and y- coordinates, and LOWORD
|
||
// and HIWORD treat the coordinates as unsigned quantities.
|
||
const til::point eventPoint{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
|
||
// This mouse event is relative to the display origin, not the window. Convert here.
|
||
const til::rect windowRect{ GetWindowRect() };
|
||
const auto origin = windowRect.origin();
|
||
const auto relative = eventPoint - origin;
|
||
// Convert to logical scaling before raising the event.
|
||
const auto scale = GetCurrentDpiScale();
|
||
const winrt::Windows::Foundation::Point real{ relative.x / scale, relative.y / scale };
|
||
|
||
const auto wheelDelta = static_cast<short>(HIWORD(wparam));
|
||
|
||
// Raise an event, so any listeners can handle the mouse wheel event manually.
|
||
MouseScrolled.raise(real, wheelDelta);
|
||
return 0;
|
||
}
|
||
CATCH_LOG();
|
||
break;
|
||
case WM_THEMECHANGED:
|
||
UpdateWindowIconForActiveMetrics(_window.get());
|
||
return 0;
|
||
case WM_WINDOWPOSCHANGING:
|
||
{
|
||
// GH#10274 - if the quake window gets moved to another monitor via aero
|
||
// snap (win+shift+arrows), then re-adjust the size for the new monitor.
|
||
if (IsQuakeWindow())
|
||
{
|
||
// Retrieve the suggested dimensions and make a rect and size.
|
||
auto lpwpos = (LPWINDOWPOS)lparam;
|
||
|
||
// We only need to apply restrictions if the position is changing.
|
||
// The SWP_ flags are confusing to read. This is
|
||
// "if we're not moving the window, do nothing."
|
||
if (WI_IsFlagSet(lpwpos->flags, SWP_NOMOVE))
|
||
{
|
||
break;
|
||
}
|
||
// Figure out the suggested dimensions and position.
|
||
RECT rcSuggested;
|
||
rcSuggested.left = lpwpos->x;
|
||
rcSuggested.top = lpwpos->y;
|
||
rcSuggested.right = rcSuggested.left + lpwpos->cx;
|
||
rcSuggested.bottom = rcSuggested.top + lpwpos->cy;
|
||
|
||
// Find the bounds of the current monitor, and the monitor that
|
||
// we're suggested to be on.
|
||
|
||
auto current = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST);
|
||
MONITORINFO currentInfo;
|
||
currentInfo.cbSize = sizeof(MONITORINFO);
|
||
GetMonitorInfo(current, ¤tInfo);
|
||
|
||
auto proposed = MonitorFromRect(&rcSuggested, MONITOR_DEFAULTTONEAREST);
|
||
MONITORINFO proposedInfo;
|
||
proposedInfo.cbSize = sizeof(MONITORINFO);
|
||
GetMonitorInfo(proposed, &proposedInfo);
|
||
|
||
// If the monitor changed...
|
||
if (til::rect{ proposedInfo.rcMonitor } !=
|
||
til::rect{ currentInfo.rcMonitor })
|
||
{
|
||
const auto newWindowRect{ _getQuakeModeSize(proposed) };
|
||
|
||
// Inform User32 that we want to be placed at the position
|
||
// and dimensions that _getQuakeModeSize returned. When we
|
||
// snap across monitor boundaries, this will re-evaluate our
|
||
// size for the new monitor.
|
||
lpwpos->x = newWindowRect.left;
|
||
lpwpos->y = newWindowRect.top;
|
||
lpwpos->cx = newWindowRect.width();
|
||
lpwpos->cy = newWindowRect.height();
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case WM_SYSCOMMAND:
|
||
{
|
||
// the low 4 bits contain additional information (that we don't care about)
|
||
auto highBits = wparam & 0xFFF0;
|
||
if (highBits == SC_RESTORE || highBits == SC_MAXIMIZE)
|
||
{
|
||
MaximizeChanged.raise(highBits == SC_MAXIMIZE);
|
||
}
|
||
|
||
if (wparam == SC_RESTORE && _fullscreen)
|
||
{
|
||
ShouldExitFullscreen.raise();
|
||
return 0;
|
||
}
|
||
auto search = _systemMenuItems.find(LOWORD(wparam));
|
||
if (search != _systemMenuItems.end())
|
||
{
|
||
search->second.callback();
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
// TODO: handle messages here...
|
||
return base_type::MessageHandler(message, wparam, lparam);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when the window has been resized (or maximized)
|
||
// Arguments:
|
||
// - width: the new width of the window _in pixels_
|
||
// - height: the new height of the window _in pixels_
|
||
void IslandWindow::OnResize(const UINT width, const UINT height)
|
||
{
|
||
if (_interopWindowHandle)
|
||
{
|
||
OnSize(width, height);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when the window is minimized to the taskbar.
|
||
void IslandWindow::OnMinimize()
|
||
{
|
||
// TODO GH#1989 Stop rendering island content when the app is minimized.
|
||
if (_minimizeToNotificationArea)
|
||
{
|
||
HideWindow();
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when the window is restored from having been minimized.
|
||
void IslandWindow::OnRestore()
|
||
{
|
||
// TODO GH#1989 Stop rendering island content when the app is minimized.
|
||
}
|
||
|
||
void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content)
|
||
{
|
||
_rootGrid.Children().Clear();
|
||
_rootGrid.Children().Append(content);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the dimensions of our non-client area, as a rect where each component
|
||
// represents that side.
|
||
// - The .left will be a negative number, to represent that the actual side of
|
||
// the non-client area is outside the border of our window. It's roughly 8px (
|
||
// * DPI scaling) to the left of the visible border.
|
||
// - The .right component will be positive, indicating that the nonclient border
|
||
// is in the positive-x direction from the edge of our client area.
|
||
// - This will also include our titlebar! It's in the nonclient area for us.
|
||
// Arguments:
|
||
// - dpi: the scaling that we should use to calculate the border sizes.
|
||
// Return Value:
|
||
// - a til::rect whose components represent the margins of the nonclient area,
|
||
// relative to the client area.
|
||
til::rect IslandWindow::GetNonClientFrame(const UINT dpi) const noexcept
|
||
{
|
||
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_window.get(), GWL_STYLE));
|
||
RECT islandFrame{};
|
||
|
||
// If we failed to get the correct window size for whatever reason, log
|
||
// the error and go on. We'll use whatever the control proposed as the
|
||
// size of our window, which will be at least close.
|
||
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi));
|
||
return til::rect{ islandFrame };
|
||
}
|
||
|
||
// Method Description:
|
||
// - Gets the difference between window and client area size.
|
||
// Arguments:
|
||
// - dpi: dpi of a monitor on which the window is placed
|
||
// Return Value
|
||
// - The size difference
|
||
til::size IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept
|
||
{
|
||
const auto islandFrame{ GetNonClientFrame(dpi) };
|
||
return {
|
||
islandFrame.right - islandFrame.left,
|
||
islandFrame.bottom - islandFrame.top
|
||
};
|
||
}
|
||
|
||
void IslandWindow::OnAppInitialized()
|
||
{
|
||
// Do a quick resize to force the island to paint
|
||
const auto size = GetPhysicalSize();
|
||
OnSize(size.width, size.height);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when the app wants to change its theme. We'll update the root UI
|
||
// element of the entire XAML tree, so that all UI elements get the theme
|
||
// applied.
|
||
// Arguments:
|
||
// - arg: the ElementTheme to use as the new theme for the UI
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme)
|
||
{
|
||
_rootGrid.RequestedTheme(requestedTheme);
|
||
// Invalidate the window rect, so that we'll repaint any elements we're
|
||
// drawing ourselves to match the new theme
|
||
::InvalidateRect(_window.get(), nullptr, false);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Updates our focus mode state. See _SetIsBorderless for more details.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::FocusModeChanged(const bool focusMode)
|
||
{
|
||
// Do nothing if the value was unchanged.
|
||
if (focusMode == _borderless)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_SetIsBorderless(focusMode);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Updates our fullscreen state. See _SetIsFullscreen for more details.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::FullscreenChanged(const bool fullscreen)
|
||
{
|
||
// Do nothing if the value was unchanged.
|
||
if (fullscreen == _fullscreen)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_SetIsFullscreen(fullscreen);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Enter or exit the "always on top" state. Before the window is created, this
|
||
// value will later be used when we create the window to create the window on
|
||
// top of all others. After the window is created, it will either enter the
|
||
// group of topmost windows, or exit the group of topmost windows.
|
||
// Arguments:
|
||
// - alwaysOnTop: whether we should be entering or exiting always on top mode.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::SetAlwaysOnTop(const bool alwaysOnTop)
|
||
{
|
||
_alwaysOnTop = alwaysOnTop;
|
||
|
||
const auto hwnd = GetHandle();
|
||
if (hwnd)
|
||
{
|
||
SetWindowPos(hwnd,
|
||
_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST,
|
||
0, // the window dimensions are unused, because we're passing SWP_NOSIZE
|
||
0,
|
||
0,
|
||
0,
|
||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Posts a message to the window message queue that the window visibility has changed
|
||
// and should then be minimized or restored.
|
||
// Arguments:
|
||
// - showOrHide: True for show; false for hide.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::ShowWindowChanged(const bool showOrHide)
|
||
{
|
||
if (const auto hwnd = GetHandle())
|
||
{
|
||
// IMPORTANT!
|
||
//
|
||
// ONLY "restore" if already minimized. If the window is maximized or
|
||
// snapped, a restore will restore-down the window instead.
|
||
if (showOrHide == true && ::IsIconic(hwnd))
|
||
{
|
||
::PostMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
|
||
}
|
||
else if (showOrHide == false)
|
||
{
|
||
::PostMessage(hwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
void IslandWindow::SetShowTabsFullscreen(const bool newShowTabsFullscreen)
|
||
{
|
||
_showTabsFullscreen = newShowTabsFullscreen;
|
||
}
|
||
|
||
// Method Description
|
||
// - Flash the taskbar icon, indicating to the user that something needs their attention
|
||
void IslandWindow::FlashTaskbar()
|
||
{
|
||
// Using 'false' as the boolean argument ensures that the taskbar is
|
||
// only flashed if the app window is not focused
|
||
FlashWindow(_window.get(), false);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Sets the taskbar progress indicator
|
||
// - We follow the ConEmu style for the state and progress values,
|
||
// more details at https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
|
||
// Arguments:
|
||
// - state: indicates the progress state
|
||
// - progress: indicates the progress value
|
||
void IslandWindow::SetTaskbarProgress(const size_t state, const size_t progress)
|
||
{
|
||
if (_taskbar)
|
||
{
|
||
switch (state)
|
||
{
|
||
case 0:
|
||
// removes the taskbar progress indicator
|
||
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
|
||
break;
|
||
case 1:
|
||
// sets the progress value to value given by the 'progress' parameter
|
||
_taskbar->SetProgressState(_window.get(), TBPF_NORMAL);
|
||
_taskbar->SetProgressValue(_window.get(), progress, 100);
|
||
break;
|
||
case 2:
|
||
// sets the progress indicator to an error state
|
||
_taskbar->SetProgressState(_window.get(), TBPF_ERROR);
|
||
_taskbar->SetProgressValue(_window.get(), progress, 100);
|
||
break;
|
||
case 3:
|
||
// sets the progress indicator to an indeterminate state.
|
||
// FIRST, set the progress to "no progress". That'll clear out any
|
||
// progress value from the previous state. Otherwise, a transition
|
||
// from (error,x%) or (warning,x%) to indeterminate will leave the
|
||
// progress value unchanged, and not show the spinner.
|
||
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
|
||
_taskbar->SetProgressState(_window.get(), TBPF_INDETERMINATE);
|
||
break;
|
||
case 4:
|
||
// sets the progress indicator to a pause state
|
||
_taskbar->SetProgressState(_window.get(), TBPF_PAUSED);
|
||
_taskbar->SetProgressValue(_window.get(), progress, 100);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// From GdiEngine::s_SetWindowLongWHelper
|
||
static void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept
|
||
{
|
||
// SetWindowLong has strange error handling. On success, it returns the
|
||
// previous Window Long value and doesn't modify the Last Error state. To
|
||
// deal with this, we set the last error to 0/S_OK first, call it, and if
|
||
// the previous long was 0, we check if the error was non-zero before
|
||
// reporting. Otherwise, we'll get an "Error: The operation has completed
|
||
// successfully." and there will be another screenshot on the internet
|
||
// making fun of Windows. See:
|
||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
|
||
SetLastError(0);
|
||
const auto lResult = SetWindowLongW(hWnd, nIndex, dwNewLong);
|
||
if (0 == lResult)
|
||
{
|
||
LOG_LAST_ERROR_IF(0 != GetLastError());
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - This is a helper to figure out what the window styles should be, given the
|
||
// current state of flags like borderless mode and fullscreen mode.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - a LONG with the appropriate flags set for our current window mode, to be used with GWL_STYLE
|
||
LONG IslandWindow::_getDesiredWindowStyle() const
|
||
{
|
||
auto windowStyle = GetWindowLongW(GetHandle(), GWL_STYLE);
|
||
|
||
// If we're both fullscreen and borderless, fullscreen mode takes precedence.
|
||
|
||
if (_fullscreen)
|
||
{
|
||
// When moving to fullscreen, remove WS_OVERLAPPEDWINDOW, which specifies
|
||
// styles for non-fullscreen windows (e.g. caption bar), and add the
|
||
// WS_POPUP style to allow us to size ourselves to the monitor size.
|
||
// Do the reverse when restoring from fullscreen.
|
||
// Doing these modifications to that window will cause a vista-style
|
||
// window frame to briefly appear when entering and exiting fullscreen.
|
||
WI_ClearFlag(windowStyle, WS_BORDER);
|
||
WI_ClearFlag(windowStyle, WS_SIZEBOX);
|
||
WI_ClearAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
|
||
|
||
WI_SetFlag(windowStyle, WS_POPUP);
|
||
return windowStyle;
|
||
}
|
||
else if (_borderless)
|
||
{
|
||
// When moving to borderless, remove WS_OVERLAPPEDWINDOW, which
|
||
// specifies styles for non-fullscreen windows (e.g. caption bar), and
|
||
// add the WS_BORDER and WS_SIZEBOX styles. This allows us to still have
|
||
// a small resizing frame, but without a full titlebar, nor caption
|
||
// buttons.
|
||
|
||
WI_ClearAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
|
||
WI_ClearFlag(windowStyle, WS_POPUP);
|
||
|
||
WI_SetFlag(windowStyle, WS_BORDER);
|
||
WI_SetFlag(windowStyle, WS_SIZEBOX);
|
||
return windowStyle;
|
||
}
|
||
|
||
// Here, we're not in either fullscreen or borderless mode. Return to
|
||
// WS_OVERLAPPEDWINDOW.
|
||
WI_ClearFlag(windowStyle, WS_POPUP);
|
||
WI_ClearFlag(windowStyle, WS_BORDER);
|
||
WI_ClearFlag(windowStyle, WS_SIZEBOX);
|
||
|
||
WI_SetAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
|
||
|
||
return windowStyle;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Enable or disable focus mode. When entering focus mode, we'll
|
||
// need to manually hide the entire titlebar.
|
||
// - When we're entering focus we need to do some additional modification
|
||
// of our window styles. However, the NonClientIslandWindow very explicitly
|
||
// _doesn't_ need to do these steps.
|
||
// Arguments:
|
||
// - borderlessEnabled: If true, we're entering focus mode. If false, we're leaving.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_SetIsBorderless(const bool borderlessEnabled)
|
||
{
|
||
_borderless = borderlessEnabled;
|
||
|
||
const auto hWnd = GetHandle();
|
||
|
||
// First, modify regular window styles as appropriate
|
||
auto windowStyle = _getDesiredWindowStyle();
|
||
SetWindowLongWHelper(hWnd, GWL_STYLE, windowStyle);
|
||
|
||
// Now modify extended window styles as appropriate
|
||
// When moving to fullscreen, remove the window edge style to avoid an
|
||
// ugly border when not focused.
|
||
auto exWindowStyle = GetWindowLongW(hWnd, GWL_EXSTYLE);
|
||
WI_UpdateFlag(exWindowStyle, WS_EX_WINDOWEDGE, !_fullscreen);
|
||
SetWindowLongWHelper(hWnd, GWL_EXSTYLE, exWindowStyle);
|
||
|
||
// Resize the window, with SWP_FRAMECHANGED, to trigger user32 to
|
||
// recalculate the non/client areas
|
||
const til::rect windowPos{ GetWindowRect() };
|
||
|
||
SetWindowPos(GetHandle(),
|
||
HWND_TOP,
|
||
windowPos.left,
|
||
windowPos.top,
|
||
windowPos.width(),
|
||
windowPos.height(),
|
||
SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when entering fullscreen, with the window's current monitor rect and work area.
|
||
// - The current window position, dpi, work area, and maximized state are stored, and the
|
||
// window is positioned to the monitor rect.
|
||
void IslandWindow::_SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork)
|
||
{
|
||
const auto hWnd = GetHandle();
|
||
|
||
::GetWindowRect(hWnd, &_rcWindowBeforeFullscreen);
|
||
_dpiBeforeFullscreen = GetDpiForWindow(hWnd);
|
||
_fWasMaximizedBeforeFullscreen = IsZoomed(hWnd);
|
||
_rcWorkBeforeFullscreen = rcWork;
|
||
|
||
SetWindowPos(hWnd,
|
||
HWND_TOP,
|
||
rcMonitor.left,
|
||
rcMonitor.top,
|
||
rcMonitor.right - rcMonitor.left,
|
||
rcMonitor.bottom - rcMonitor.top,
|
||
SWP_FRAMECHANGED);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Called when exiting fullscreen, with the window's current monitor work area.
|
||
// - The window is restored to its previous position, migrating that previous position to the
|
||
// window's current monitor (if the current work area or window DPI have changed).
|
||
// - A fullscreen window's monitor can be changed by win+shift+left/right hotkeys or monitor
|
||
// topology changes (for example unplugging a monitor or disconnecting a remote session).
|
||
void IslandWindow::_RestoreFullscreenPosition(const RECT& rcWork)
|
||
{
|
||
const auto hWnd = GetHandle();
|
||
|
||
// If the window was previously maximized, re-maximize the window.
|
||
if (_fWasMaximizedBeforeFullscreen)
|
||
{
|
||
ShowWindow(hWnd, SW_SHOWMAXIMIZED);
|
||
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
|
||
|
||
return;
|
||
}
|
||
|
||
// Start with the stored window position.
|
||
auto rcRestore = _rcWindowBeforeFullscreen;
|
||
|
||
// If the window DPI has changed, re-size the stored position by the change in DPI. This
|
||
// ensures the window restores to the same logical size (even if to a monitor with a different
|
||
// DPI/ scale factor).
|
||
auto dpiWindow = GetDpiForWindow(hWnd);
|
||
rcRestore.right = rcRestore.left + MulDiv(rcRestore.right - rcRestore.left, dpiWindow, _dpiBeforeFullscreen);
|
||
rcRestore.bottom = rcRestore.top + MulDiv(rcRestore.bottom - rcRestore.top, dpiWindow, _dpiBeforeFullscreen);
|
||
|
||
// Offset the stored position by the difference in work area.
|
||
OffsetRect(&rcRestore,
|
||
rcWork.left - _rcWorkBeforeFullscreen.left,
|
||
rcWork.top - _rcWorkBeforeFullscreen.top);
|
||
|
||
const til::size ncSize{ GetTotalNonClientExclusiveSize(dpiWindow) };
|
||
|
||
auto rcWorkAdjusted = rcWork;
|
||
|
||
// GH#10199 - adjust the size of the "work" rect by the size of our borders.
|
||
// We want to make sure the window is restored within the bounds of the
|
||
// monitor we're on, but it's totally fine if the invisible borders are
|
||
// outside the monitor.
|
||
const auto halfWidth{ ncSize.width / 2 };
|
||
const auto halfHeight{ ncSize.height / 2 };
|
||
|
||
rcWorkAdjusted.left -= halfWidth;
|
||
rcWorkAdjusted.right += halfWidth;
|
||
rcWorkAdjusted.top -= halfHeight;
|
||
rcWorkAdjusted.bottom += halfHeight;
|
||
|
||
// Enforce that our position is entirely within the bounds of our work area.
|
||
// Prefer the top-left be on-screen rather than bottom-right (right before left, bottom before top).
|
||
if (rcRestore.right > rcWorkAdjusted.right)
|
||
{
|
||
OffsetRect(&rcRestore, rcWorkAdjusted.right - rcRestore.right, 0);
|
||
}
|
||
if (rcRestore.left < rcWorkAdjusted.left)
|
||
{
|
||
OffsetRect(&rcRestore, rcWorkAdjusted.left - rcRestore.left, 0);
|
||
}
|
||
if (rcRestore.bottom > rcWorkAdjusted.bottom)
|
||
{
|
||
OffsetRect(&rcRestore, 0, rcWorkAdjusted.bottom - rcRestore.bottom);
|
||
}
|
||
if (rcRestore.top < rcWorkAdjusted.top)
|
||
{
|
||
OffsetRect(&rcRestore, 0, rcWorkAdjusted.top - rcRestore.top);
|
||
}
|
||
|
||
// Show the window at the computed position.
|
||
SetWindowPos(hWnd,
|
||
HWND_TOP,
|
||
rcRestore.left,
|
||
rcRestore.top,
|
||
rcRestore.right - rcRestore.left,
|
||
rcRestore.bottom - rcRestore.top,
|
||
SWP_SHOWWINDOW | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Controls setting us into or out of fullscreen mode. Largely taken from
|
||
// Window::SetIsFullscreen in conhost.
|
||
// - When entering fullscreen mode, we'll save the current window size and
|
||
// location, and expand to take the entire monitor size. When leaving, we'll
|
||
// use that saved size to restore back to.
|
||
// Arguments:
|
||
// - fullscreenEnabled true if we should enable fullscreen mode, false to disable.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
|
||
{
|
||
// It is possible to enter _SetIsFullscreen even if we're already in full
|
||
// screen. Use the old is in fullscreen flag to gate checks that rely on the
|
||
// current state.
|
||
const auto fChangingFullscreen = (fullscreenEnabled != _fullscreen);
|
||
_fullscreen = fullscreenEnabled;
|
||
|
||
const auto hWnd = GetHandle();
|
||
|
||
// First, modify regular window styles as appropriate
|
||
auto windowStyle = _getDesiredWindowStyle();
|
||
SetWindowLongWHelper(hWnd, GWL_STYLE, windowStyle);
|
||
|
||
// Now modify extended window styles as appropriate
|
||
// When moving to fullscreen, remove the window edge style to avoid an
|
||
// ugly border when not focused.
|
||
auto exWindowStyle = GetWindowLongW(hWnd, GWL_EXSTYLE);
|
||
WI_UpdateFlag(exWindowStyle, WS_EX_WINDOWEDGE, !_fullscreen);
|
||
SetWindowLongWHelper(hWnd, GWL_EXSTYLE, exWindowStyle);
|
||
|
||
// Only change the window position if changing fullscreen state.
|
||
if (fChangingFullscreen)
|
||
{
|
||
// Get the monitor info for the window's current monitor.
|
||
MONITORINFO mi = {};
|
||
mi.cbSize = sizeof(mi);
|
||
GetMonitorInfo(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &mi);
|
||
|
||
if (_fullscreen)
|
||
{
|
||
// Store the window's current position and size the window to the monitor.
|
||
_SetFullscreenPosition(mi.rcMonitor, mi.rcWork);
|
||
}
|
||
else
|
||
{
|
||
// Restore the stored window position.
|
||
_RestoreFullscreenPosition(mi.rcWork);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Summon the window, or possibly dismiss it. If toggleVisibility is true,
|
||
// then we'll dismiss (minimize) the window if it's currently active.
|
||
// Otherwise, we'll always just try to activate the window.
|
||
// Arguments:
|
||
// - toggleVisibility: controls how we should behave when already in the foreground.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::SummonWindow(winrt::TerminalApp::SummonWindowBehavior args)
|
||
{
|
||
const auto toggleVisibility = args ? args.ToggleVisibility() : false;
|
||
const auto toMonitor = args ? args.ToMonitor() : winrt::TerminalApp::MonitorBehavior::InPlace;
|
||
auto dropdownDuration = args ? args.DropdownDuration() : 0;
|
||
|
||
// If the user requested an animation, let's check if animations are enabled in the OS.
|
||
if (dropdownDuration > 0)
|
||
{
|
||
auto animationsEnabled = TRUE;
|
||
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
|
||
if (!animationsEnabled)
|
||
{
|
||
// The OS has animations disabled - we should respect that and
|
||
// disable the animation here.
|
||
//
|
||
// We're doing this here, rather than in _doSlideAnimation, to
|
||
// preempt any other specific behavior that
|
||
// _globalActivateWindow/_globalDismissWindow might do if they think
|
||
// there should be an animation (like making the window appear with
|
||
// SetWindowPlacement rather than ShowWindow)
|
||
dropdownDuration = 0;
|
||
}
|
||
}
|
||
|
||
// * If the user doesn't want to toggleVisibility, then just always try to
|
||
// activate.
|
||
// * If the user does want to toggleVisibility,
|
||
// - If we're the foreground window, ToMonitor == ToMouse, and the mouse is on the monitor we are
|
||
// - activate the window
|
||
// - else
|
||
// - dismiss the window
|
||
if (toggleVisibility && GetForegroundWindow() == _window.get())
|
||
{
|
||
auto handled = false;
|
||
|
||
// They want to toggle the window when it is the FG window, and we are
|
||
// the FG window. However, if we're on a different monitor than the
|
||
// mouse, then we should move to that monitor instead of dismissing.
|
||
if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToMouse)
|
||
{
|
||
const til::rect cursorMonitorRect{ _getMonitorForCursor().rcMonitor };
|
||
const til::rect currentMonitorRect{ _getMonitorForWindow(GetHandle()).rcMonitor };
|
||
if (cursorMonitorRect != currentMonitorRect)
|
||
{
|
||
// We're not on the same monitor as the mouse. Go to that monitor.
|
||
_globalActivateWindow(dropdownDuration, toMonitor);
|
||
handled = true;
|
||
}
|
||
}
|
||
|
||
if (!handled)
|
||
{
|
||
_globalDismissWindow(dropdownDuration);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_globalActivateWindow(dropdownDuration, toMonitor);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Helper for performing a sliding animation. This will animate our _Xaml
|
||
// Island_, either growing down or shrinking up, using SetWindowRgn.
|
||
// - This function does the entire animation on the main thread (the UI thread),
|
||
// and **DOES NOT YIELD IT**. The window will be animating for the entire
|
||
// duration of dropdownDuration.
|
||
// - At the end of the animation, we'll reset the window region, so that it's as
|
||
// if nothing occurred.
|
||
// Arguments:
|
||
// - dropdownDuration: The duration to play the animation, in milliseconds. If
|
||
// 0, we won't perform a dropdown animation.
|
||
// - down: if true, increase the height from top to bottom. otherwise, decrease
|
||
// the height, from bottom to top.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_doSlideAnimation(const uint32_t dropdownDuration, const bool down)
|
||
{
|
||
til::rect fullWindowSize{ GetWindowRect() };
|
||
const auto fullHeight = fullWindowSize.height();
|
||
|
||
const double animationDuration = dropdownDuration; // use floating-point math throughout
|
||
const auto start = std::chrono::system_clock::now();
|
||
|
||
// Do at most dropdownDuration frames. After that, just bail straight to the
|
||
// final state.
|
||
for (uint32_t i = 0; i < dropdownDuration; i++)
|
||
{
|
||
const auto end = std::chrono::system_clock::now();
|
||
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
|
||
const auto dt = static_cast<double>(millis.count());
|
||
|
||
if (dt > animationDuration)
|
||
{
|
||
break;
|
||
}
|
||
|
||
// If going down, increase the height over time. If going up, decrease the height.
|
||
const auto currentHeight = ::base::saturated_cast<int>(
|
||
down ? ((dt / animationDuration) * fullHeight) :
|
||
((1.0 - (dt / animationDuration)) * fullHeight));
|
||
|
||
wil::unique_hrgn rgn{ CreateRectRgn(0, 0, fullWindowSize.width(), currentHeight) };
|
||
SetWindowRgn(_interopWindowHandle, rgn.get(), true);
|
||
|
||
// Go immediately into another frame. This prevents the window from
|
||
// doing anything else (tearing our state). A Sleep() here will cause a
|
||
// weird stutter, and causes the animation to not be as smooth.
|
||
}
|
||
|
||
// Reset the window.
|
||
SetWindowRgn(_interopWindowHandle, nullptr, true);
|
||
}
|
||
|
||
void IslandWindow::_dropdownWindow(const uint32_t dropdownDuration,
|
||
const winrt::TerminalApp::MonitorBehavior toMonitor)
|
||
{
|
||
// First, get the window that's currently in the foreground. We'll need
|
||
// _this_ window to be able to appear on top of. If we just use
|
||
// GetForegroundWindow after the SetWindowPlacement call, _we_ will be the
|
||
// foreground window.
|
||
const auto oldForegroundWindow = GetForegroundWindow();
|
||
|
||
// First, restore the window. SetWindowPlacement has a fun undocumented
|
||
// piece of functionality where it will restore the window position
|
||
// _without_ the animation, so use that instead of ShowWindow(SW_RESTORE).
|
||
WINDOWPLACEMENT wpc{};
|
||
wpc.length = sizeof(WINDOWPLACEMENT);
|
||
GetWindowPlacement(_window.get(), &wpc);
|
||
|
||
// If the window is hidden, SW_SHOW it first.
|
||
if (!IsWindowVisible(GetHandle()))
|
||
{
|
||
wpc.showCmd = SW_SHOW;
|
||
SetWindowPlacement(_window.get(), &wpc);
|
||
}
|
||
wpc.showCmd = SW_RESTORE;
|
||
SetWindowPlacement(_window.get(), &wpc);
|
||
|
||
// Possibly go to the monitor of the mouse / old foreground window.
|
||
_moveToMonitor(oldForegroundWindow, toMonitor);
|
||
|
||
// Now that we're visible, animate the dropdown.
|
||
_doSlideAnimation(dropdownDuration, true);
|
||
}
|
||
|
||
void IslandWindow::_slideUpWindow(const uint32_t dropdownDuration)
|
||
{
|
||
// First, animate the window sliding up.
|
||
_doSlideAnimation(dropdownDuration, false);
|
||
|
||
// Then, use SetWindowPlacement to minimize without the animation.
|
||
WINDOWPLACEMENT wpc{};
|
||
wpc.length = sizeof(WINDOWPLACEMENT);
|
||
GetWindowPlacement(_window.get(), &wpc);
|
||
wpc.showCmd = SW_MINIMIZE;
|
||
SetWindowPlacement(_window.get(), &wpc);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Force activate this window. This method will bring us to the foreground and
|
||
// activate us. If the window is minimized, it will restore the window. If the
|
||
// window is on another desktop, the OS will switch to that desktop.
|
||
// - If the window is minimized, and dropdownDuration is greater than 0, we'll
|
||
// perform a "slide in" animation. We won't do this if the window is already
|
||
// on the screen (since that seems silly).
|
||
// Arguments:
|
||
// - dropdownDuration: The duration to play the dropdown animation, in
|
||
// milliseconds. If 0, we won't perform a dropdown animation.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_globalActivateWindow(const uint32_t dropdownDuration,
|
||
const winrt::TerminalApp::MonitorBehavior toMonitor)
|
||
{
|
||
// First, get the window that's currently in the foreground. We'll need
|
||
// _this_ window to be able to appear on top of. If we just use
|
||
// GetForegroundWindow after the SetWindowPlacement/ShowWindow call, _we_
|
||
// will be the foreground window.
|
||
const auto oldForegroundWindow = GetForegroundWindow();
|
||
|
||
// From: https://stackoverflow.com/a/59659421
|
||
// > The trick is to make windows ‘think’ that our process and the target
|
||
// > window (hwnd) are related by attaching the threads (using
|
||
// > AttachThreadInput API) and using an alternative API: BringWindowToTop.
|
||
// If the window is minimized, then restore it. We don't want to do this
|
||
// always though, because if you SW_RESTORE a maximized window, it will
|
||
// restore-down the window.
|
||
if (IsIconic(_window.get()))
|
||
{
|
||
if (dropdownDuration > 0)
|
||
{
|
||
_dropdownWindow(dropdownDuration, toMonitor);
|
||
}
|
||
else
|
||
{
|
||
// If the window is hidden, SW_SHOW it first. Note that hidden !=
|
||
// minimized. A hidden window doesn't appear in the taskbar, while a
|
||
// minimized window will. If you don't do this, then we won't be
|
||
// able to properly set this as the foreground window.
|
||
if (!IsWindowVisible(GetHandle()))
|
||
{
|
||
ShowWindow(_window.get(), SW_SHOW);
|
||
}
|
||
ShowWindow(_window.get(), SW_RESTORE);
|
||
|
||
// Once we've been restored, throw us on the active monitor.
|
||
_moveToMonitor(oldForegroundWindow, toMonitor);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// Try first to send a message to the current foreground window. If it's not responding, it may
|
||
// be waiting on us to finish launching. Passing SMTO_NOTIMEOUTIFNOTHUNG means that we get the same
|
||
// behavior as before--that is, waiting for the message loop--but we've done an early return if
|
||
// it turns out that it was hung.
|
||
// SendMessageTimeoutW returns nonzero if it succeeds.
|
||
if (0 != SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_NOTIMEOUTIFNOTHUNG | SMTO_BLOCK | SMTO_ABORTIFHUNG, 1000, nullptr))
|
||
{
|
||
const auto windowThreadProcessId = GetWindowThreadProcessId(oldForegroundWindow, nullptr);
|
||
const auto currentThreadId = GetCurrentThreadId();
|
||
|
||
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true));
|
||
// Just in case, add the thread detach as a scope_exit, to make _sure_ we do it.
|
||
auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() {
|
||
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false));
|
||
});
|
||
LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get()));
|
||
ShowWindow(_window.get(), SW_SHOW);
|
||
|
||
// Activate the window too. This will force us to the virtual desktop this
|
||
// window is on, if it's on another virtual desktop.
|
||
LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get()));
|
||
|
||
// Throw us on the active monitor.
|
||
_moveToMonitor(oldForegroundWindow, toMonitor);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Minimize the window. This is called when the window is summoned, but is
|
||
// already active
|
||
// - If dropdownDuration is greater than 0, we'll perform a "slide in"
|
||
// animation, before minimizing the window.
|
||
// Arguments:
|
||
// - dropdownDuration: The duration to play the slide-up animation, in
|
||
// milliseconds. If 0, we won't perform a slide-up animation.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_globalDismissWindow(const uint32_t dropdownDuration)
|
||
{
|
||
if (dropdownDuration > 0)
|
||
{
|
||
_slideUpWindow(dropdownDuration);
|
||
}
|
||
else
|
||
{
|
||
ShowWindow(_window.get(), SW_MINIMIZE);
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the monitor the mouse cursor is currently on
|
||
// Arguments:
|
||
// - dropdownDuration: The duration to play the slide-up animation, in
|
||
// milliseconds. If 0, we won't perform a slide-up animation.
|
||
// Return Value:
|
||
// - The MONITORINFO for the monitor the mouse cursor is on
|
||
MONITORINFO IslandWindow::_getMonitorForCursor()
|
||
{
|
||
POINT p{};
|
||
GetCursorPos(&p);
|
||
|
||
// Get the monitor info for the window's current monitor.
|
||
MONITORINFO activeMonitor{};
|
||
activeMonitor.cbSize = sizeof(activeMonitor);
|
||
GetMonitorInfo(MonitorFromPoint(p, MONITOR_DEFAULTTONEAREST), &activeMonitor);
|
||
return activeMonitor;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the monitor for a given HWND
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - The MONITORINFO for the given HWND
|
||
MONITORINFO IslandWindow::_getMonitorForWindow(HWND foregroundWindow)
|
||
{
|
||
// Get the monitor info for the window's current monitor.
|
||
MONITORINFO activeMonitor{};
|
||
activeMonitor.cbSize = sizeof(activeMonitor);
|
||
GetMonitorInfo(MonitorFromWindow(foregroundWindow, MONITOR_DEFAULTTONEAREST), &activeMonitor);
|
||
return activeMonitor;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Based on the value in toMonitor, move the window to the monitor of the
|
||
// given HWND, the monitor of the mouse pointer, or just leave it where it is.
|
||
// Arguments:
|
||
// - oldForegroundWindow: when toMonitor is ToCurrent, we'll move to the monitor
|
||
// of this HWND. Otherwise, this param is ignored.
|
||
// - toMonitor: Controls which monitor we should move to.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_moveToMonitor(HWND oldForegroundWindow, winrt::TerminalApp::MonitorBehavior toMonitor)
|
||
{
|
||
if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToCurrent)
|
||
{
|
||
_moveToMonitorOf(oldForegroundWindow);
|
||
}
|
||
else if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToMouse)
|
||
{
|
||
_moveToMonitorOfMouse();
|
||
}
|
||
}
|
||
|
||
// Method Description:
|
||
// - Move our window to the monitor the mouse is on.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_moveToMonitorOfMouse()
|
||
{
|
||
_moveToMonitor(_getMonitorForCursor());
|
||
}
|
||
|
||
// Method Description:
|
||
// - Move our window to the monitor that the given HWND is on.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_moveToMonitorOf(HWND foregroundWindow)
|
||
{
|
||
_moveToMonitor(_getMonitorForWindow(foregroundWindow));
|
||
}
|
||
|
||
// Method Description:
|
||
// - Move our window to the given monitor. This will do nothing if we're already
|
||
// on that monitor.
|
||
// - We'll retain the same relative position on the new monitor as we had on the
|
||
// old monitor.
|
||
// Arguments:
|
||
// - activeMonitor: the monitor to move to.
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_moveToMonitor(const MONITORINFO activeMonitor)
|
||
{
|
||
// Get the monitor info for the window's current monitor.
|
||
const auto currentMonitor = _getMonitorForWindow(GetHandle());
|
||
|
||
const til::rect currentRect{ currentMonitor.rcMonitor };
|
||
const til::rect activeRect{ activeMonitor.rcMonitor };
|
||
if (currentRect != activeRect)
|
||
{
|
||
const til::rect currentWindowRect{ GetWindowRect() };
|
||
const auto offset{ currentWindowRect.origin() - currentRect.origin() };
|
||
const auto newOrigin{ activeRect.origin() + offset };
|
||
|
||
SetWindowPos(GetHandle(),
|
||
0,
|
||
newOrigin.x,
|
||
newOrigin.y,
|
||
currentWindowRect.width(),
|
||
currentWindowRect.height(),
|
||
SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE);
|
||
|
||
// GH#10274, GH#10182: Re-evaluate the size of the quake window when we
|
||
// move to another monitor.
|
||
if (IsQuakeWindow())
|
||
{
|
||
_enterQuakeMode();
|
||
}
|
||
}
|
||
}
|
||
|
||
bool IslandWindow::IsQuakeWindow() const noexcept
|
||
{
|
||
return _isQuakeWindow;
|
||
}
|
||
|
||
void IslandWindow::IsQuakeWindow(bool isQuakeWindow) noexcept
|
||
{
|
||
if (_isQuakeWindow != isQuakeWindow)
|
||
{
|
||
_isQuakeWindow = isQuakeWindow;
|
||
// Don't enter quake mode if we don't have an HWND yet
|
||
if (IsQuakeWindow() && _window)
|
||
{
|
||
_enterQuakeMode();
|
||
}
|
||
}
|
||
}
|
||
|
||
void IslandWindow::SetAutoHideWindow(bool autoHideWindow) noexcept
|
||
{
|
||
_autoHideWindow = autoHideWindow;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Enter quake mode for the monitor this window is currently on. This involves
|
||
// resizing it to the top half of the monitor.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
void IslandWindow::_enterQuakeMode()
|
||
{
|
||
if (!_window)
|
||
{
|
||
return;
|
||
}
|
||
|
||
auto windowRect = GetWindowRect();
|
||
auto hmon = MonitorFromRect(&windowRect, MONITOR_DEFAULTTONEAREST);
|
||
|
||
// Get the size and position of the window that we should occupy
|
||
const auto newRect{ _getQuakeModeSize(hmon) };
|
||
|
||
SetWindowPos(GetHandle(),
|
||
HWND_TOP,
|
||
newRect.left,
|
||
newRect.top,
|
||
newRect.width(),
|
||
newRect.height(),
|
||
SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||
}
|
||
|
||
// Method Description:
|
||
// - Get the size and position of the window that a "quake mode" should occupy
|
||
// on the given monitor.
|
||
// - The window will occupy the top half of the monitor.
|
||
// Arguments:
|
||
// - <none>
|
||
// Return Value:
|
||
// - <none>
|
||
til::rect IslandWindow::_getQuakeModeSize(HMONITOR hmon)
|
||
{
|
||
MONITORINFO nearestMonitorInfo;
|
||
|
||
UINT dpix = USER_DEFAULT_SCREEN_DPI;
|
||
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
|
||
// If this fails, we'll use the default of 96. I think it can only fail for
|
||
// bad parameters, which we won't have, so no big deal.
|
||
LOG_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy));
|
||
|
||
nearestMonitorInfo.cbSize = sizeof(MONITORINFO);
|
||
// Get monitor dimensions:
|
||
GetMonitorInfo(hmon, &nearestMonitorInfo);
|
||
const til::size desktopDimensions{ (nearestMonitorInfo.rcWork.right - nearestMonitorInfo.rcWork.left),
|
||
(nearestMonitorInfo.rcWork.bottom - nearestMonitorInfo.rcWork.top) };
|
||
|
||
// If we just use rcWork by itself, we'll fail to account for the invisible
|
||
// space reserved for the resize handles. So retrieve that size here.
|
||
const til::size ncSize{ GetTotalNonClientExclusiveSize(dpix) };
|
||
const auto availableSpace = desktopDimensions + ncSize;
|
||
|
||
// GH#10201 - The borders are still visible in quake mode, so make us 1px
|
||
// smaller on either side to account for that, so they don't hang onto
|
||
// adjacent monitors.
|
||
const til::point origin{
|
||
::base::ClampSub(nearestMonitorInfo.rcWork.left, (ncSize.width / 2)) + 1,
|
||
(nearestMonitorInfo.rcWork.top)
|
||
};
|
||
const til::size dimensions{
|
||
availableSpace.width - 2,
|
||
availableSpace.height / 2
|
||
};
|
||
|
||
return { origin, dimensions };
|
||
}
|
||
|
||
void IslandWindow::HideWindow()
|
||
{
|
||
ShowWindow(GetHandle(), SW_HIDE);
|
||
}
|
||
|
||
void IslandWindow::SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept
|
||
{
|
||
_minimizeToNotificationArea = MinimizeToNotificationArea;
|
||
}
|
||
|
||
// Method Description:
|
||
// - Opens the window's system menu.
|
||
// - The system menu is the menu that opens when the user presses Alt+Space or
|
||
// right clicks on the title bar.
|
||
// - Before updating the menu, we update the buttons like "Maximize" and
|
||
// "Restore" so that they are grayed out depending on the window's state.
|
||
// Arguments:
|
||
// - cursorX: the cursor's X position in screen coordinates
|
||
// - cursorY: the cursor's Y position in screen coordinates
|
||
void IslandWindow::OpenSystemMenu(const std::optional<int> mouseX, const std::optional<int> mouseY) const noexcept
|
||
{
|
||
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
|
||
|
||
WINDOWPLACEMENT placement;
|
||
if (!GetWindowPlacement(_window.get(), &placement))
|
||
{
|
||
return;
|
||
}
|
||
const auto isMaximized = placement.showCmd == SW_SHOWMAXIMIZED;
|
||
|
||
// Update the options based on window state.
|
||
MENUITEMINFO mii;
|
||
mii.cbSize = sizeof(MENUITEMINFO);
|
||
mii.fMask = MIIM_STATE;
|
||
mii.fType = MFT_STRING;
|
||
auto setState = [&](UINT item, bool enabled) {
|
||
mii.fState = enabled ? MF_ENABLED : MF_DISABLED;
|
||
SetMenuItemInfo(systemMenu, item, FALSE, &mii);
|
||
};
|
||
setState(SC_RESTORE, isMaximized);
|
||
setState(SC_MOVE, !isMaximized);
|
||
setState(SC_SIZE, !isMaximized);
|
||
setState(SC_MINIMIZE, true);
|
||
setState(SC_MAXIMIZE, !isMaximized);
|
||
setState(SC_CLOSE, true);
|
||
SetMenuDefaultItem(systemMenu, UINT_MAX, FALSE);
|
||
|
||
int xPos;
|
||
int yPos;
|
||
if (mouseX && mouseY)
|
||
{
|
||
xPos = mouseX.value();
|
||
yPos = mouseY.value();
|
||
}
|
||
else
|
||
{
|
||
RECT windowPos;
|
||
::GetWindowRect(GetHandle(), &windowPos);
|
||
xPos = windowPos.left;
|
||
yPos = windowPos.top;
|
||
}
|
||
const auto ret = TrackPopupMenu(systemMenu, TPM_RETURNCMD, xPos, yPos, 0, _window.get(), nullptr);
|
||
if (ret != 0)
|
||
{
|
||
PostMessage(_window.get(), WM_SYSCOMMAND, ret, 0);
|
||
}
|
||
}
|
||
|
||
void IslandWindow::AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate<void()> callback)
|
||
{
|
||
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
|
||
auto wID = _systemMenuNextItemId;
|
||
|
||
MENUITEMINFOW item;
|
||
item.cbSize = sizeof(MENUITEMINFOW);
|
||
item.fMask = MIIM_STATE | MIIM_ID | MIIM_STRING;
|
||
item.fState = MF_ENABLED;
|
||
item.wID = wID;
|
||
item.dwTypeData = const_cast<LPWSTR>(itemLabel.c_str());
|
||
item.cch = static_cast<UINT>(itemLabel.size());
|
||
|
||
if (LOG_LAST_ERROR_IF(!InsertMenuItemW(systemMenu, wID, FALSE, &item)))
|
||
{
|
||
return;
|
||
}
|
||
_systemMenuItems.insert({ wID, { itemLabel, callback } });
|
||
_systemMenuNextItemId++;
|
||
}
|
||
|
||
void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel)
|
||
{
|
||
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
|
||
auto itemCount = GetMenuItemCount(systemMenu);
|
||
if (LOG_LAST_ERROR_IF(itemCount == -1))
|
||
{
|
||
return;
|
||
}
|
||
|
||
auto it = std::find_if(_systemMenuItems.begin(), _systemMenuItems.end(), [&itemLabel](const std::pair<UINT, SystemMenuItemInfo>& elem) {
|
||
return elem.second.label == itemLabel;
|
||
});
|
||
if (it == _systemMenuItems.end())
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (LOG_LAST_ERROR_IF(!DeleteMenu(systemMenu, it->first, MF_BYCOMMAND)))
|
||
{
|
||
return;
|
||
}
|
||
_systemMenuItems.erase(it->first);
|
||
}
|
||
|
||
void IslandWindow::_resetSystemMenu()
|
||
{
|
||
// GetSystemMenu(..., true) will revert the menu to the default state.
|
||
GetSystemMenu(_window.get(), TRUE);
|
||
}
|
||
|
||
void IslandWindow::UseDarkTheme(const bool v)
|
||
{
|
||
const BOOL attribute = v ? TRUE : FALSE;
|
||
std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_USE_IMMERSIVE_DARK_MODE, &attribute, sizeof(attribute));
|
||
}
|
||
|
||
void IslandWindow::UseMica(const bool newValue, const double /*titlebarOpacity*/)
|
||
{
|
||
// This block of code enables Mica for our window. By all accounts, this
|
||
// version of the code will only work on Windows 11, SV2. There's a slightly
|
||
// different API surface for enabling Mica on Windows 11 22000.0.
|
||
//
|
||
// This API was only publicly supported as of Windows 11 SV2, 22621. Before
|
||
// that version, this API will just return an error and do nothing silently.
|
||
|
||
const int attribute = newValue ? DWMSBT_MAINWINDOW : DWMSBT_NONE;
|
||
std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_SYSTEMBACKDROP_TYPE, &attribute, sizeof(attribute));
|
||
}
|
||
|
||
// Method Description:
|
||
// - This method is called when the window receives the WM_NCCREATE message.
|
||
// Return Value:
|
||
// - The value returned from the window proc.
|
||
[[nodiscard]] LRESULT IslandWindow::OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept
|
||
{
|
||
const auto ret = BaseWindow::OnNcCreate(wParam, lParam);
|
||
if (!ret)
|
||
{
|
||
return FALSE;
|
||
}
|
||
|
||
// This is a hack to make the window borders dark instead of light.
|
||
// It must be done before WM_NCPAINT so that the borders are rendered with
|
||
// the correct theme.
|
||
// For more information, see GH#6620.
|
||
//
|
||
// Theoretically, we don't need this anymore, since _updateTheme will update
|
||
// the darkness of our window. However, we're keeping this call to prevent
|
||
// the window from appearing as a white rectangle for a frame before we load
|
||
// the rest of the settings.
|
||
UseDarkTheme(true);
|
||
|
||
return TRUE;
|
||
}
|