7bfaad4592
Window exits out of fullscreen if it receives SC_RESTORE Closes #10607
948 lines
33 KiB
C++
948 lines
33 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "Clipboard.hpp"
|
|
#include "ConsoleControl.hpp"
|
|
#include "find.h"
|
|
#include "menu.hpp"
|
|
#include "window.hpp"
|
|
#include "windowdpiapi.hpp"
|
|
#include "windowime.hpp"
|
|
#include "windowio.hpp"
|
|
#include "windowmetrics.hpp"
|
|
|
|
#include "../../host/_output.h"
|
|
#include "../../host/output.h"
|
|
#include "../../host/dbcs.h"
|
|
#include "../../host/handle.h"
|
|
#include "../../host/input.h"
|
|
#include "../../host/misc.h"
|
|
#include "../../host/registry.hpp"
|
|
#include "../../host/scrolling.hpp"
|
|
#include "../../host/srvinit.h"
|
|
|
|
#include "../inc/ServiceLocator.hpp"
|
|
|
|
#include "../../inc/conint.h"
|
|
|
|
#include "../interactivity/win32/CustomWindowMessages.h"
|
|
|
|
#include "../interactivity/win32/windowUiaProvider.hpp"
|
|
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
|
|
using namespace Microsoft::Console::Interactivity::Win32;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
// The static and specific window procedures for this class are contained here
|
|
#pragma region Window Procedure
|
|
|
|
[[nodiscard]] LRESULT CALLBACK Window::s_ConsoleWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam)
|
|
{
|
|
// Save the pointer here to the specific window instance when one is created
|
|
if (Message == WM_CREATE)
|
|
{
|
|
const CREATESTRUCT* const pCreateStruct = reinterpret_cast<CREATESTRUCT*>(lParam);
|
|
|
|
Window* const pWindow = reinterpret_cast<Window*>(pCreateStruct->lpCreateParams);
|
|
SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pWindow));
|
|
}
|
|
|
|
// Dispatch the message to the specific class instance
|
|
Window* const pWindow = reinterpret_cast<Window*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
|
|
if (pWindow != nullptr)
|
|
{
|
|
return pWindow->ConsoleWindowProc(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
// If we get this far, call the default window proc
|
|
return DefWindowProcW(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
[[nodiscard]] LRESULT CALLBACK Window::ConsoleWindowProc(_In_ HWND hWnd, _In_ UINT Message, _In_ WPARAM wParam, _In_ LPARAM lParam)
|
|
{
|
|
Globals& g = ServiceLocator::LocateGlobals();
|
|
CONSOLE_INFORMATION& gci = g.getConsoleInformation();
|
|
LRESULT Status = 0;
|
|
BOOL Unlock = TRUE;
|
|
|
|
LockConsole();
|
|
|
|
SCREEN_INFORMATION& ScreenInfo = GetScreenInfo();
|
|
if (hWnd == nullptr) // TODO: this might not be possible anymore
|
|
{
|
|
if (Message == WM_CLOSE)
|
|
{
|
|
_CloseWindow();
|
|
Status = 0;
|
|
}
|
|
else
|
|
{
|
|
Status = DefWindowProcW(hWnd, Message, wParam, lParam);
|
|
}
|
|
|
|
UnlockConsole();
|
|
return Status;
|
|
}
|
|
|
|
switch (Message)
|
|
{
|
|
case WM_CREATE:
|
|
{
|
|
// Load all metrics we'll need.
|
|
_UpdateSystemMetrics();
|
|
|
|
// The system is not great and the window rect is wrong the first time for High DPI (WM_DPICHANGED scales strangely.)
|
|
// So here we have to grab the DPI of the current window (now that we have a window).
|
|
// Then we have to re-propose a window size for our window that is scaled to DPI and SetWindowPos.
|
|
|
|
// First get the new DPI and update all the scaling factors in the console that are affected.
|
|
|
|
// NOTE: GetDpiForWindow can be *WRONG* at this point in time depending on monitor configuration.
|
|
// They won't be correct until the window is actually shown. So instead of using those APIs, figure out the DPI
|
|
// based on the rectangle that is about to be shown using the nearest monitor.
|
|
|
|
// Get proposed window rect from create structure
|
|
CREATESTRUCTW* pcs = (CREATESTRUCTW*)lParam;
|
|
RECT rc;
|
|
rc.left = pcs->x;
|
|
rc.top = pcs->y;
|
|
rc.right = rc.left + pcs->cx;
|
|
rc.bottom = rc.top + pcs->cy;
|
|
|
|
// Find nearest monitor.
|
|
HMONITOR hmon = MonitorFromRect(&rc, 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;
|
|
GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy); // If this fails, we'll use the default of 96.
|
|
|
|
// Pick one and set it to the global DPI.
|
|
ServiceLocator::LocateGlobals().dpi = (int)dpix;
|
|
|
|
_UpdateSystemMetrics(); // scroll bars and cursors and such.
|
|
s_ReinitializeFontsForDPIChange(); // font sizes.
|
|
|
|
// Now re-propose the window size with the same origin.
|
|
RECT rectProposed = { rc.left, rc.top, 0, 0 };
|
|
_CalculateWindowRect(_pSettings->GetWindowSize(), &rectProposed);
|
|
|
|
SetWindowPos(hWnd, nullptr, rectProposed.left, rectProposed.top, RECT_WIDTH(&rectProposed), RECT_HEIGHT(&rectProposed), SWP_NOACTIVATE | SWP_NOZORDER);
|
|
|
|
// Save the proposed window rect dimensions here so we can adjust if the system comes back and changes them on what we asked for.
|
|
ServiceLocator::LocateWindowMetrics<WindowMetrics>()->ConvertWindowRectToClientRect(&rectProposed);
|
|
|
|
break;
|
|
}
|
|
|
|
case WM_DROPFILES:
|
|
{
|
|
_HandleDrop(wParam);
|
|
break;
|
|
}
|
|
|
|
case WM_GETOBJECT:
|
|
{
|
|
Status = _HandleGetObject(hWnd, wParam, lParam);
|
|
break;
|
|
}
|
|
|
|
case WM_DESTROY:
|
|
{
|
|
// signal to uia that they can disconnect our uia provider
|
|
if (_pUiaProvider)
|
|
{
|
|
UiaReturnRawElementProvider(hWnd, 0, 0, nullptr);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_SIZING:
|
|
{
|
|
// Signal that the user changed the window size, so we can return the value later for telemetry. By only
|
|
// sending the data back if the size has changed, helps reduce the amount of telemetry being sent back.
|
|
// WM_SIZING doesn't fire if they resize the window using Win-UpArrow, so we'll miss that scenario. We could
|
|
// listen to the WM_SIZE message instead, but they can fire when the window is being restored from being
|
|
// minimized, and not only when they resize the window.
|
|
Telemetry::Instance().SetWindowSizeChanged();
|
|
goto CallDefWin;
|
|
break;
|
|
}
|
|
|
|
case WM_GETDPISCALEDSIZE:
|
|
{
|
|
// This message will send us the DPI we're about to be changed to.
|
|
// Our goal is to use it to try to figure out the Window Rect that we'll need at that DPI to maintain
|
|
// the same client rendering that we have now.
|
|
|
|
// First retrieve the new DPI and the current DPI.
|
|
DWORD const dpiProposed = (WORD)wParam;
|
|
|
|
// Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that.
|
|
const FontInfo& fiCurrent = ScreenInfo.GetCurrentFont();
|
|
FontInfoDesired fiDesired(fiCurrent);
|
|
FontInfo fiProposed(L"", 0, 0, { 0, 0 }, 0);
|
|
|
|
const HRESULT hr = g.pRender->GetProposedFont(dpiProposed, fiDesired, fiProposed);
|
|
// fiProposal will be updated by the renderer for this new font.
|
|
// GetProposedFont can fail if there's no render engine yet.
|
|
// This can happen if we're headless.
|
|
// Just assume that the font is 1x1 in that case.
|
|
const COORD coordFontProposed = SUCCEEDED(hr) ? fiProposed.GetSize() : COORD({ 1, 1 });
|
|
|
|
// Then from that font size, we need to calculate the client area.
|
|
// Then from the client area we need to calculate the window area (using the proposed DPI scalar here as well.)
|
|
|
|
// Retrieve the additional parameters we need for the math call based on the current window & buffer properties.
|
|
const Viewport viewport = ScreenInfo.GetViewport();
|
|
COORD coordWindowInChars = viewport.Dimensions();
|
|
|
|
const COORD coordBufferSize = ScreenInfo.GetTextBuffer().GetSize().Dimensions();
|
|
|
|
// Now call the math calculation for our proposed size.
|
|
RECT rectProposed = { 0 };
|
|
s_CalculateWindowRect(coordWindowInChars, dpiProposed, coordFontProposed, coordBufferSize, hWnd, &rectProposed);
|
|
|
|
// Prepare where we're going to keep our final suggestion.
|
|
SIZE* const pSuggestionSize = (SIZE*)lParam;
|
|
|
|
pSuggestionSize->cx = RECT_WIDTH(&rectProposed);
|
|
pSuggestionSize->cy = RECT_HEIGHT(&rectProposed);
|
|
|
|
// Format our final suggestion for consumption.
|
|
UnlockConsole();
|
|
return TRUE;
|
|
}
|
|
|
|
case WM_DPICHANGED:
|
|
{
|
|
_fInDPIChange = true;
|
|
ServiceLocator::LocateGlobals().dpi = HIWORD(wParam);
|
|
_UpdateSystemMetrics();
|
|
s_ReinitializeFontsForDPIChange();
|
|
|
|
// This is the RECT that the system suggests.
|
|
RECT* const prcNewScale = (RECT*)lParam;
|
|
SetWindowPos(hWnd,
|
|
HWND_TOP,
|
|
prcNewScale->left,
|
|
prcNewScale->top,
|
|
RECT_WIDTH(prcNewScale),
|
|
RECT_HEIGHT(prcNewScale),
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
|
|
_fInDPIChange = false;
|
|
|
|
break;
|
|
}
|
|
|
|
case WM_ACTIVATE:
|
|
{
|
|
// if we're activated by a mouse click, remember it so
|
|
// we don't pass the click on to the app.
|
|
if (LOWORD(wParam) == WA_CLICKACTIVE)
|
|
{
|
|
gci.Flags |= CONSOLE_IGNORE_NEXT_MOUSE_INPUT;
|
|
}
|
|
goto CallDefWin;
|
|
break;
|
|
}
|
|
|
|
case WM_SETFOCUS:
|
|
{
|
|
gci.ProcessHandleList.ModifyConsoleProcessFocus(TRUE);
|
|
|
|
gci.Flags |= CONSOLE_HAS_FOCUS;
|
|
|
|
gci.GetCursorBlinker().FocusStart();
|
|
|
|
HandleFocusEvent(TRUE);
|
|
|
|
// ActivateTextServices does nothing if already active so this is OK to be called every focus.
|
|
ActivateTextServices(ServiceLocator::LocateConsoleWindow()->GetWindowHandle(), GetImeSuggestionWindowPos, GetTextBoxArea);
|
|
|
|
// set the text area to have focus for accessibility consumers
|
|
if (_pUiaProvider)
|
|
{
|
|
LOG_IF_FAILED(_pUiaProvider->SetTextAreaFocus());
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case WM_KILLFOCUS:
|
|
{
|
|
gci.ProcessHandleList.ModifyConsoleProcessFocus(FALSE);
|
|
|
|
gci.Flags &= ~CONSOLE_HAS_FOCUS;
|
|
|
|
// turn it off when we lose focus.
|
|
gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsOn(false);
|
|
gci.GetCursorBlinker().FocusEnd();
|
|
|
|
HandleFocusEvent(FALSE);
|
|
|
|
break;
|
|
}
|
|
|
|
case WM_PAINT:
|
|
{
|
|
// Since we handle our own minimized window state, we need to
|
|
// check if we're minimized (iconic) and set our internal state flags accordingly.
|
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/dd162483(v=vs.85).aspx
|
|
// NOTE: We will not get called to paint ourselves when minimized because we set an icon when registering the window class.
|
|
// That means this CONSOLE_IS_ICONIC is unnecessary when/if we can decouple the drawing with D2D.
|
|
if (IsIconic(hWnd))
|
|
{
|
|
WI_SetFlag(gci.Flags, CONSOLE_IS_ICONIC);
|
|
}
|
|
else
|
|
{
|
|
WI_ClearFlag(gci.Flags, CONSOLE_IS_ICONIC);
|
|
}
|
|
|
|
LOG_IF_FAILED(_HandlePaint());
|
|
|
|
// NOTE: We cannot let the OS handle this message (meaning do NOT pass to DefWindowProc)
|
|
// or it will cause missing painted regions in scenarios without a DWM (like Core Server SKU).
|
|
// Ensure it is re-validated in this handler so we don't receive infinite WM_PAINTs after
|
|
// we have stored the invalid region data for the next trip around the renderer thread.
|
|
|
|
break;
|
|
}
|
|
|
|
case WM_ERASEBKGND:
|
|
{
|
|
break;
|
|
}
|
|
|
|
case WM_CLOSE:
|
|
{
|
|
// Write the final trace log during the WM_CLOSE message while the console process is still fully alive.
|
|
// This gives us time to query the process for information. We shouldn't really miss any useful
|
|
// telemetry between now and when the process terminates.
|
|
Telemetry::Instance().WriteFinalTraceLog();
|
|
|
|
_CloseWindow();
|
|
break;
|
|
}
|
|
|
|
case WM_SETTINGCHANGE:
|
|
{
|
|
LOG_IF_FAILED(Microsoft::Console::Internal::Theming::TrySetDarkMode(hWnd));
|
|
gci.GetCursorBlinker().SettingsChanged();
|
|
}
|
|
__fallthrough;
|
|
|
|
case WM_DISPLAYCHANGE:
|
|
{
|
|
_UpdateSystemMetrics();
|
|
break;
|
|
}
|
|
|
|
case WM_WINDOWPOSCHANGING:
|
|
{
|
|
// Enforce maximum size here instead of WM_GETMINMAXINFO.
|
|
// If we return it in WM_GETMINMAXINFO, then it will be enforced when snapping across DPI boundaries (bad.)
|
|
|
|
// Retrieve the suggested dimensions and make a rect and size.
|
|
LPWINDOWPOS lpwpos = (LPWINDOWPOS)lParam;
|
|
|
|
// We only need to apply restrictions if the size is changing.
|
|
if (!WI_IsFlagSet(lpwpos->flags, SWP_NOSIZE))
|
|
{
|
|
// Figure out the suggested dimensions
|
|
RECT rcSuggested;
|
|
rcSuggested.left = lpwpos->x;
|
|
rcSuggested.top = lpwpos->y;
|
|
rcSuggested.right = rcSuggested.left + lpwpos->cx;
|
|
rcSuggested.bottom = rcSuggested.top + lpwpos->cy;
|
|
SIZE szSuggested;
|
|
szSuggested.cx = RECT_WIDTH(&rcSuggested);
|
|
szSuggested.cy = RECT_HEIGHT(&rcSuggested);
|
|
|
|
// Figure out the current dimensions for comparison.
|
|
RECT rcCurrent = GetWindowRect();
|
|
|
|
// Determine whether we're being resized by someone dragging the edge or completely moved around.
|
|
bool fIsEdgeResize = false;
|
|
{
|
|
// We can only be edge resizing if our existing rectangle wasn't empty. If it was empty, we're doing the initial create.
|
|
if (!IsRectEmpty(&rcCurrent))
|
|
{
|
|
// If one or two sides are changing, we're being edge resized.
|
|
unsigned int cSidesChanging = 0;
|
|
if (rcCurrent.left != rcSuggested.left)
|
|
{
|
|
cSidesChanging++;
|
|
}
|
|
if (rcCurrent.right != rcSuggested.right)
|
|
{
|
|
cSidesChanging++;
|
|
}
|
|
if (rcCurrent.top != rcSuggested.top)
|
|
{
|
|
cSidesChanging++;
|
|
}
|
|
if (rcCurrent.bottom != rcSuggested.bottom)
|
|
{
|
|
cSidesChanging++;
|
|
}
|
|
|
|
if (cSidesChanging == 1 || cSidesChanging == 2)
|
|
{
|
|
fIsEdgeResize = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the window is maximized, let it do whatever it wants to do.
|
|
// If not, then restrict it to our maximum possible window.
|
|
if (!WI_IsFlagSet(GetWindowStyle(hWnd), WS_MAXIMIZE))
|
|
{
|
|
// Find the related monitor, the maximum pixel size,
|
|
// and the dpi for the suggested rect.
|
|
UINT dpiOfMaximum;
|
|
RECT rcMaximum;
|
|
|
|
if (fIsEdgeResize)
|
|
{
|
|
// If someone's dragging from the edge to resize in one direction, we want to make sure we never grow past the current monitor.
|
|
rcMaximum = ServiceLocator::LocateWindowMetrics<WindowMetrics>()->GetMaxWindowRectInPixels(&rcCurrent, &dpiOfMaximum);
|
|
}
|
|
else
|
|
{
|
|
// In other circumstances, assume we're snapping around or some other jump (TS).
|
|
// Just do whatever we're told using the new suggestion as the restriction monitor.
|
|
rcMaximum = ServiceLocator::LocateWindowMetrics<WindowMetrics>()->GetMaxWindowRectInPixels(&rcSuggested, &dpiOfMaximum);
|
|
}
|
|
|
|
// Only apply the maximum size restriction if the current DPI matches the DPI of the
|
|
// maximum rect. This keeps us from applying the wrong restriction if the monitor
|
|
// we're moving to has a different DPI but we've yet to get notified of that DPI
|
|
// change. If we do apply it, then we'll restrict the console window BEFORE its
|
|
// been resized for the DPI change, so we're likely to shrink the window too much
|
|
// or worse yet, keep it from moving entirely. We'll get a WM_DPICHANGED,
|
|
// resize the window, and then process the restriction in a few window messages.
|
|
if (((int)dpiOfMaximum == g.dpi) &&
|
|
((szSuggested.cx > RECT_WIDTH(&rcMaximum)) || (szSuggested.cy > RECT_HEIGHT(&rcMaximum))))
|
|
{
|
|
lpwpos->cx = std::min(RECT_WIDTH(&rcMaximum), szSuggested.cx);
|
|
lpwpos->cy = std::min(RECT_HEIGHT(&rcMaximum), szSuggested.cy);
|
|
|
|
// We usually add SWP_NOMOVE so that if the user is dragging the left or top edge
|
|
// and hits the restriction, then the window just stops growing, it doesn't
|
|
// move with the mouse. However during DPI changes, we need to allow a move
|
|
// because the RECT from WM_DPICHANGED has been specially crafted by win32k
|
|
// to keep the mouse cursor from jumping away from the caption bar.
|
|
if (!_fInDPIChange)
|
|
{
|
|
lpwpos->flags |= SWP_NOMOVE;
|
|
}
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
}
|
|
|
|
case WM_WINDOWPOSCHANGED:
|
|
{
|
|
// Only handle this if the DPI is the same as last time.
|
|
// If the DPI is different, assume we're about to get a DPICHANGED notification
|
|
// which will have a better suggested rectangle than this one.
|
|
// NOTE: This stopped being possible in RS4 as the DPI now changes when and only when
|
|
// we receive WM_DPICHANGED. We keep this check around so that we perform better downlevel.
|
|
int const dpi = ServiceLocator::LocateHighDpiApi<WindowDpiApi>()->GetDpiForWindow(hWnd);
|
|
if (dpi == ServiceLocator::LocateGlobals().dpi)
|
|
{
|
|
_HandleWindowPosChanged(lParam);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_CONTEXTMENU:
|
|
{
|
|
Telemetry::Instance().SetContextMenuUsed();
|
|
if (DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam) == HTCLIENT)
|
|
{
|
|
HMENU hHeirMenu = Menu::s_GetHeirMenuHandle();
|
|
|
|
Unlock = FALSE;
|
|
UnlockConsole();
|
|
|
|
TrackPopupMenuEx(hHeirMenu,
|
|
TPM_RIGHTBUTTON | (GetSystemMetrics(SM_MENUDROPALIGNMENT) == 0 ? TPM_LEFTALIGN : TPM_RIGHTALIGN),
|
|
GET_X_LPARAM(lParam),
|
|
GET_Y_LPARAM(lParam),
|
|
hWnd,
|
|
nullptr);
|
|
}
|
|
else
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_NCLBUTTONDOWN:
|
|
{
|
|
// allow user to move window even when bigger than the screen
|
|
switch (wParam & 0x00FF)
|
|
{
|
|
case HTCAPTION:
|
|
UnlockConsole();
|
|
Unlock = FALSE;
|
|
SetActiveWindow(hWnd);
|
|
SendMessageTimeoutW(hWnd, WM_SYSCOMMAND, SC_MOVE | wParam, lParam, SMTO_NORMAL, INFINITE, nullptr);
|
|
break;
|
|
default:
|
|
goto CallDefWin;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_KEYDOWN:
|
|
case WM_KEYUP:
|
|
case WM_CHAR:
|
|
case WM_DEADCHAR:
|
|
{
|
|
HandleKeyEvent(hWnd, Message, wParam, lParam, &Unlock);
|
|
break;
|
|
}
|
|
|
|
case WM_SYSKEYDOWN:
|
|
case WM_SYSKEYUP:
|
|
case WM_SYSCHAR:
|
|
case WM_SYSDEADCHAR:
|
|
{
|
|
if (HandleSysKeyEvent(hWnd, Message, wParam, lParam, &Unlock))
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_COMMAND:
|
|
// If this is an edit command from the context menu, treat it like a sys command.
|
|
if ((wParam < ID_CONSOLE_COPY) || (wParam > ID_CONSOLE_SELECTALL))
|
|
{
|
|
break;
|
|
}
|
|
|
|
__fallthrough;
|
|
|
|
case WM_SYSCOMMAND:
|
|
if (wParam == ID_CONSOLE_MARK)
|
|
{
|
|
Selection::Instance().InitializeMarkSelection();
|
|
}
|
|
else if (wParam == ID_CONSOLE_COPY)
|
|
{
|
|
Clipboard::Instance().Copy();
|
|
}
|
|
else if (wParam == ID_CONSOLE_PASTE)
|
|
{
|
|
Clipboard::Instance().Paste();
|
|
}
|
|
else if (wParam == ID_CONSOLE_SCROLL)
|
|
{
|
|
Scrolling::s_DoScroll();
|
|
}
|
|
else if (wParam == ID_CONSOLE_FIND)
|
|
{
|
|
DoFind();
|
|
Unlock = FALSE;
|
|
}
|
|
else if (wParam == ID_CONSOLE_SELECTALL)
|
|
{
|
|
Selection::Instance().SelectAll();
|
|
}
|
|
else if (wParam == ID_CONSOLE_CONTROL)
|
|
{
|
|
Menu::s_ShowPropertiesDialog(hWnd, FALSE);
|
|
}
|
|
else if (wParam == ID_CONSOLE_DEFAULTS)
|
|
{
|
|
Menu::s_ShowPropertiesDialog(hWnd, TRUE);
|
|
}
|
|
else if (wParam == SC_RESTORE && _fIsInFullscreen)
|
|
{
|
|
SetIsFullscreen(false);
|
|
}
|
|
else
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
break;
|
|
|
|
case WM_HSCROLL:
|
|
{
|
|
HorizontalScroll(LOWORD(wParam), HIWORD(wParam));
|
|
break;
|
|
}
|
|
|
|
case WM_VSCROLL:
|
|
{
|
|
VerticalScroll(LOWORD(wParam), HIWORD(wParam));
|
|
break;
|
|
}
|
|
|
|
case WM_INITMENU:
|
|
{
|
|
HandleMenuEvent(WM_INITMENU);
|
|
Menu::Instance()->Initialize();
|
|
break;
|
|
}
|
|
|
|
case WM_MENUSELECT:
|
|
{
|
|
if (HIWORD(wParam) == 0xffff)
|
|
{
|
|
HandleMenuEvent(WM_MENUSELECT);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_MOUSEMOVE:
|
|
case WM_LBUTTONDOWN:
|
|
case WM_LBUTTONUP:
|
|
case WM_LBUTTONDBLCLK:
|
|
case WM_RBUTTONDOWN:
|
|
case WM_RBUTTONUP:
|
|
case WM_RBUTTONDBLCLK:
|
|
case WM_MBUTTONDOWN:
|
|
case WM_MBUTTONUP:
|
|
case WM_MBUTTONDBLCLK:
|
|
case WM_MOUSEWHEEL:
|
|
case WM_MOUSEHWHEEL:
|
|
{
|
|
if (HandleMouseEvent(ScreenInfo, Message, wParam, lParam))
|
|
{
|
|
if (Message != WM_MOUSEWHEEL && Message != WM_MOUSEHWHEEL)
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Don't handle zoom.
|
|
if (wParam & MK_CONTROL)
|
|
{
|
|
goto CallDefWin;
|
|
}
|
|
|
|
Status = 1;
|
|
|
|
bool isMouseWheel = Message == WM_MOUSEWHEEL;
|
|
bool isMouseHWheel = Message == WM_MOUSEHWHEEL;
|
|
|
|
if (isMouseWheel || isMouseHWheel)
|
|
{
|
|
short wheelDelta = (short)HIWORD(wParam);
|
|
bool hasShift = (wParam & MK_SHIFT) ? true : false;
|
|
|
|
Scrolling::s_HandleMouseWheel(isMouseWheel,
|
|
isMouseHWheel,
|
|
wheelDelta,
|
|
hasShift,
|
|
ScreenInfo);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CM_SET_WINDOW_SIZE:
|
|
{
|
|
Status = _InternalSetWindowSize();
|
|
break;
|
|
}
|
|
|
|
case CM_BEEP:
|
|
{
|
|
UnlockConsole();
|
|
Unlock = FALSE;
|
|
|
|
// Don't fall back to Beep() on win32 systems -- if the user configures their system for no sound, we should
|
|
// respect that.
|
|
PlaySoundW((LPCWSTR)SND_ALIAS_SYSTEMHAND, nullptr, SND_ALIAS_ID | SND_ASYNC | SND_SENTRY);
|
|
break;
|
|
}
|
|
|
|
case CM_UPDATE_SCROLL_BARS:
|
|
{
|
|
ScreenInfo.InternalUpdateScrollBars();
|
|
break;
|
|
}
|
|
|
|
case CM_UPDATE_TITLE:
|
|
{
|
|
// SetWindowTextW needs null terminated string so assign view to string.
|
|
const std::wstring titleAndPrefix{ gci.GetTitleAndPrefix() };
|
|
|
|
SetWindowTextW(hWnd, titleAndPrefix.c_str());
|
|
break;
|
|
}
|
|
|
|
case CM_UPDATE_EDITKEYS:
|
|
{
|
|
// Re-read the edit key settings from registry.
|
|
Registry reg(&gci);
|
|
reg.GetEditKeys(nullptr);
|
|
break;
|
|
}
|
|
|
|
#ifdef DBG
|
|
case CM_SET_KEY_STATE:
|
|
{
|
|
const int keyboardInputTableStateSize = 256;
|
|
if (wParam < keyboardInputTableStateSize)
|
|
{
|
|
BYTE keyState[keyboardInputTableStateSize];
|
|
GetKeyboardState(keyState);
|
|
keyState[wParam] = static_cast<BYTE>(lParam);
|
|
SetKeyboardState(keyState);
|
|
}
|
|
else
|
|
{
|
|
LOG_HR_MSG(E_INVALIDARG, "CM_SET_KEY_STATE invalid wParam");
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CM_SET_KEYBOARD_LAYOUT:
|
|
{
|
|
try
|
|
{
|
|
std::wstringstream wss;
|
|
wss << std::setfill(L'0') << std::setw(8) << wParam;
|
|
std::wstring wstr(wss.str());
|
|
LoadKeyboardLayout(wstr.c_str(), KLF_ACTIVATE);
|
|
}
|
|
catch (...)
|
|
{
|
|
LOG_HR_MSG(wil::ResultFromCaughtException(), "CM_SET_KEYBOARD_LAYOUT exception");
|
|
}
|
|
break;
|
|
}
|
|
#endif DBG
|
|
|
|
case EVENT_CONSOLE_CARET:
|
|
case EVENT_CONSOLE_UPDATE_REGION:
|
|
case EVENT_CONSOLE_UPDATE_SIMPLE:
|
|
case EVENT_CONSOLE_UPDATE_SCROLL:
|
|
case EVENT_CONSOLE_LAYOUT:
|
|
case EVENT_CONSOLE_START_APPLICATION:
|
|
case EVENT_CONSOLE_END_APPLICATION:
|
|
{
|
|
NotifyWinEvent(Message, hWnd, (LONG)wParam, (LONG)lParam);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
CallDefWin:
|
|
{
|
|
if (Unlock)
|
|
{
|
|
UnlockConsole();
|
|
Unlock = FALSE;
|
|
}
|
|
Status = DefWindowProcW(hWnd, Message, wParam, lParam);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (Unlock)
|
|
{
|
|
UnlockConsole();
|
|
}
|
|
|
|
return Status;
|
|
}
|
|
|
|
#pragma endregion
|
|
|
|
// Helper handler methods for specific cases within the large window procedure are in this section
|
|
#pragma region Message Handlers
|
|
|
|
void Window::_HandleWindowPosChanged(const LPARAM lParam)
|
|
{
|
|
HWND hWnd = GetWindowHandle();
|
|
SCREEN_INFORMATION& ScreenInfo = GetScreenInfo();
|
|
|
|
LPWINDOWPOS const lpWindowPos = (LPWINDOWPOS)lParam;
|
|
|
|
// If the frame changed, update the system metrics.
|
|
if (WI_IsFlagSet(lpWindowPos->flags, SWP_FRAMECHANGED))
|
|
{
|
|
_UpdateSystemMetrics();
|
|
}
|
|
|
|
// This message is sent as the result of someone calling SetWindowPos(). We use it here to set/clear the
|
|
// CONSOLE_IS_ICONIC bit appropriately. doing so in the WM_SIZE handler is incorrect because the WM_SIZE
|
|
// comes after the WM_ERASEBKGND during SetWindowPos() processing, and the WM_ERASEBKGND needs to know if
|
|
// the console window is iconic or not.
|
|
if (!ScreenInfo.ResizingWindow && (lpWindowPos->cx || lpWindowPos->cy) && !IsIconic(hWnd))
|
|
{
|
|
// calculate the dimensions for the newly proposed window rectangle
|
|
RECT rcNew;
|
|
s_ConvertWindowPosToWindowRect(lpWindowPos, &rcNew);
|
|
ServiceLocator::LocateWindowMetrics<WindowMetrics>()->ConvertWindowRectToClientRect(&rcNew);
|
|
|
|
// If the window is not being resized, including a DPI change, then
|
|
// don't do anything except update our windowrect
|
|
if (!WI_IsFlagSet(lpWindowPos->flags, SWP_NOSIZE) || _fInDPIChange)
|
|
{
|
|
ScreenInfo.ProcessResizeWindow(&rcNew, &_rcClientLast);
|
|
}
|
|
|
|
// now that operations are complete, save the new rectangle size as the last seen value
|
|
_rcClientLast = rcNew;
|
|
}
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This helper method for the window procedure will handle the WM_PAINT message
|
|
// - It will retrieve the invalid rectangle and dispatch that information to the attached renderer
|
|
// (if available). It will then attempt to validate/finalize the paint to appease the system
|
|
// and prevent more WM_PAINTs from coming back (until of course something else causes an invalidation).
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - S_OK if we succeeded. ERROR_INVALID_HANDLE if there is no HWND. E_FAIL if GDI failed for some reason.
|
|
[[nodiscard]] HRESULT Window::_HandlePaint() const
|
|
{
|
|
HWND const hwnd = GetWindowHandle();
|
|
RETURN_HR_IF_NULL(HRESULT_FROM_WIN32(ERROR_INVALID_HANDLE), hwnd);
|
|
|
|
// We have to call BeginPaint to retrieve the invalid rectangle state
|
|
// BeginPaint/EndPaint does a bunch of other magic in the system level
|
|
// that we can't sufficiently replicate with GetInvalidRect/ValidateRect.
|
|
// ---
|
|
// We've tried in the past to not call BeginPaint/EndPaint
|
|
// and under certain circumstances (windows with SW_HIDE, SKUs without DWM, etc.)
|
|
// the system either sends WM_PAINT messages ad nauseum or fails to redraw everything correctly.
|
|
PAINTSTRUCT ps;
|
|
HDC const hdc = BeginPaint(hwnd, &ps);
|
|
RETURN_HR_IF_NULL(E_FAIL, hdc);
|
|
|
|
if (ServiceLocator::LocateGlobals().pRender != nullptr)
|
|
{
|
|
// In lieu of actually painting right now, we're just going to aggregate this information in the renderer
|
|
// and let it paint whenever it feels appropriate.
|
|
RECT const rcUpdate = ps.rcPaint;
|
|
ServiceLocator::LocateGlobals().pRender->TriggerSystemRedraw(&rcUpdate);
|
|
}
|
|
|
|
LOG_IF_WIN32_BOOL_FALSE(EndPaint(hwnd, &ps));
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - This routine is called when ConsoleWindowProc receives a WM_DROPFILES message.
|
|
// - It initially calls DragQueryFile() to calculate the number of files dropped and then DragQueryFile() is called to retrieve the filename.
|
|
// - DoStringPaste() pastes the filename to the console window
|
|
// Arguments:
|
|
// - wParam - Identifies the structure containing the filenames of the dropped files.
|
|
// - Console - Pointer to CONSOLE_INFORMATION structure
|
|
// Return Value:
|
|
// - <none>
|
|
void Window::_HandleDrop(const WPARAM wParam) const
|
|
{
|
|
WCHAR szPath[MAX_PATH];
|
|
BOOL fAddQuotes;
|
|
|
|
if (DragQueryFile((HDROP)wParam, 0, szPath, ARRAYSIZE(szPath)) != 0)
|
|
{
|
|
// Log a telemetry flag saying the user interacted with the Console
|
|
// Only log when DragQueryFile succeeds, because if we don't when the console starts up, we're seeing
|
|
// _HandleDrop get called multiple times (and DragQueryFile fail),
|
|
// which can incorrectly mark this console session as interactive.
|
|
Telemetry::Instance().SetUserInteractive();
|
|
|
|
fAddQuotes = (wcschr(szPath, L' ') != nullptr);
|
|
if (fAddQuotes)
|
|
{
|
|
Clipboard::Instance().StringPaste(L"\"", 1);
|
|
}
|
|
|
|
Clipboard::Instance().StringPaste(szPath, wcslen(szPath));
|
|
|
|
if (fAddQuotes)
|
|
{
|
|
Clipboard::Instance().StringPaste(L"\"", 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] LRESULT Window::_HandleGetObject(const HWND hwnd, const WPARAM wParam, const LPARAM lParam)
|
|
{
|
|
LRESULT retVal = 0;
|
|
|
|
// If we are receiving a request from Microsoft UI Automation framework, then return the basic UIA COM interface.
|
|
if (static_cast<long>(lParam) == static_cast<long>(UiaRootObjectId))
|
|
{
|
|
// NOTE: Deliverable MSFT: 10881045 is required before this will work properly.
|
|
// The UIAutomationCore.dll cannot currently handle the fact that our HWND is assigned to the child PID.
|
|
// It will attempt to set up events/pipes on the wrong PID/HWND combination when called here.
|
|
// A temporary workaround until that is delivered is to disable window handle reparenting using
|
|
// ConsoleControl's ConsoleSetWindowOwner call.
|
|
retVal = UiaReturnRawElementProvider(hwnd, wParam, lParam, _GetUiaProvider());
|
|
}
|
|
// Otherwise, return 0. We don't implement MS Active Accessibility (the other framework that calls WM_GETOBJECT).
|
|
|
|
return retVal;
|
|
}
|
|
|
|
#pragma endregion
|
|
|
|
// Dispatchers are used to post or send a window message into the queue from other portions of the codebase without accessing internal properties directly
|
|
#pragma region Dispatchers
|
|
|
|
BOOL Window::PostUpdateWindowSize() const
|
|
{
|
|
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
|
|
const SCREEN_INFORMATION& ScreenInfo = GetScreenInfo();
|
|
|
|
if (ScreenInfo.ConvScreenInfo != nullptr)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (gci.Flags & CONSOLE_SETTING_WINDOW_SIZE)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
gci.Flags |= CONSOLE_SETTING_WINDOW_SIZE;
|
|
return PostMessageW(GetWindowHandle(), CM_SET_WINDOW_SIZE, (WPARAM)&ScreenInfo, 0);
|
|
}
|
|
|
|
BOOL Window::SendNotifyBeep() const
|
|
{
|
|
return SendNotifyMessageW(GetWindowHandle(), CM_BEEP, 0, 0);
|
|
}
|
|
|
|
BOOL Window::PostUpdateScrollBars() const
|
|
{
|
|
return PostMessageW(GetWindowHandle(), CM_UPDATE_SCROLL_BARS, (WPARAM)&GetScreenInfo(), 0);
|
|
}
|
|
|
|
BOOL Window::PostUpdateExtendedEditKeys() const
|
|
{
|
|
return PostMessageW(GetWindowHandle(), CM_UPDATE_EDITKEYS, 0, 0);
|
|
}
|
|
|
|
#pragma endregion
|