terminal/src/interactivity/win32/windowproc.cpp
Yoshiko 3f1befb06e
Fix Touch Keyboard invocation issue (#11389)
This fixes an issue that Touch Keyboard is not invoked when user taps on the PowerShell. 

Before this change, it was returning small rectangle on the right of the cursor. Touch Keyboard should be invoked by tapping anywhere inside the console.

## PR Checklist
* [ ] Closes #xxx
* [x] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [ ] Tests added/passed
* [ ] Documentation updated. If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/terminal) and link it here: #xxx
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. Issue number where discussion took place: #xxx

## Detailed Description of the Pull Request / Additional comments
ITfContextOwner::GetScreenExt is used to define rectangle that can invoke Touch Keyboard. 
https://docs.microsoft.com/en-us/windows/win32/api/msctf/nf-msctf-itfcontextowner-getscreenext

## Validation Steps Performed
* [x] Touch keyboard was invoked by tapping inside the Console while Hardware Keyboard was not attached.
* [x] Selecting text worked as expected without invoking touch keyboard.
* [x] Long tapping the console invoked Touch Keyboard. I would like to confirm if this is the expected behavior.
2021-10-04 14:29:56 +00:00

944 lines
32 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
{
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