terminal/src/interactivity/win32/windowproc.cpp

944 lines
32 KiB
C++
Raw Normal View History

// 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"
Accessibility: Set-up UIA Tree (#1691) **The Basics of Accessibility** - [What is a User Interaction Automation (UIA) Tree?](https://docs.microsoft.com/en-us/dotnet/framework/ui-automation/ui-automation-tree-overview) - Other projects (i.e.: Narrator) can take advantage of this UIA tree and are used to present information within it. - Some things like XAML already have a UIA Tree. So some UIA tree navigation and features are already there. It's just a matter of getting them hooked up and looking right. **Accessibility in our Project** There's a few important classes... regarding Accessibility... - **WindowUiaProvider**: This sets up the UIA tree for a window. So this is the top-level for the UIA tree. - **ScreenInfoUiaProvider**: This sets up the UIA tree for a terminal buffer. - **UiaTextRange**: This is essential to interacting with the UIA tree for the terminal buffer. Actually gets portions of the buffer and presents them. regarding the Windows Terminal window... - **BaseWindow**: The foundation to a window. Deals with HWNDs and that kind of stuff. - **IslandWindow**: This extends `BaseWindow` and is actually what holds our Windows Terminal - **NonClientIslandWindow**: An extension of the `IslandWindow` regarding ConHost... - **IConsoleWindow**: This is an interface for the console window. - **Window**: This is the actual window for ConHost. Extends `IConsoleWindow` - `IConsoleWindow` changes: - move into `Microsoft::Console::Types` (a shared space) - Have `IslandWindow` extend it - `WindowUiaProvider` changes: - move into `Microsoft::Console::Types` (a shared space) - Hook up `WindowUiaProvider` to IslandWindow (yay! we now have a tree) ### Changes to the WindowUiaProvider As mentioned earlier, the WindowUiaProvider is the top-level UIA provider for our projects. To reuse as much code as possible, I created `Microsoft::Console::Types::WindowUiaProviderBase`. Any existing functions that reference a `ScreenInfoUiaProvider` were virtual-ized. In each project, a `WindowUiaProvider : WindowUiaProviderBase` was created to define those virtual functions. Note that that will be the main difference between ConHost and Windows Terminal moving forward: how many TextBuffers are on the screen. So, ConHost should be the same as before, with only one `ScreenInfoUiaProvider`, whereas Windows Terminal needs to (1) update which one is on the screen and (2) may have multiple on the screen. 🚨 Windows Terminal doesn't have the `ScreenInfoUiaProvider` hooked up yet. We'll have all the XAML elements in the UIA tree. But, since `TermControl` is a custom XAML Control, I need to hook up the `ScreenInfoUiaProvider` to it. This work will be done in a new PR and resolve GitHub Issue #1352. ### Moved to `Microsoft::Console::Types` These files got moved to a shared area so that they can be used by both ConHost and Windows Terminal. This means that any references to the `ServiceLocator` had to be removed. - `IConsoleWindow` - Windows Terminal: `IslandWindow : IConsoleWindow` - `ScreenInfoUiaProvider` - all references to `ServiceLocator` and `SCREEN_INFORMATION` were removed. `IRenderData` was used to accomplish this. Refer to next section for more details. - `UiaTextRange` - all references to `ServiceLocator` and `SCREEN_INFORMATION` were removed. `IRenderData` was used to accomplish this. Refer to next section for more details. - since most of the functions were `static`, that means that an `IRenderData` had to be added into most of them. ### Changes to IRenderData Since `IRenderData` is now being used to abstract out `ServiceLocator` and `SCREEN_INFORMATION`, I had to add a few functions here: - `bool IsAreaSelected()` - `void ClearSelection()` - `void SelectNewRegion(...)` - `HRESULT SearchForText(...)` `SearchForText()` is a problem here. The overall new design is great! But Windows Terminal doesn't have a way to search for text in the buffer yet, whereas ConHost does. So I'm punting on this issue for now. It looks nasty, but just look at all the other pretty things here. :)
2019-07-30 00:21:15 +02:00
#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();
Fix restore window position when exiting fullscreen (#9737) <!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This change cleans up the Fullscreen implementation for both conhost and Terminal, improving the restore position (where the window goes when exiting fullscreen). Prior to this change the window wasn't guaranteed to restore somewhere on the window's current monitor when exiting fullscreen. With this change the window will restore always to its current monitor, at a reasonable location (and will 'double restore' (to fullscreen->maximize->restore) after monitor changes while fullscreen, which is the expected user behavior. <!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> ## References <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist * [x] Closes #9746 * [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. * [ ] 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 <!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments A fullscreen window's monitor can change. - Win+Shift+left/right migrates a window between monitors. - User could open settings, display, and move the monitor or change its DPI. - The monitor could be unplugged. - The session could be remote and be disconnected. A fullscreen window stores a 'restore position' when entering fullscreen, used to move the window back 'where it was'. BUT, its unexpected for the window to exit fullscreen and jump to another monitor. This means its previous position must be migrated from the old monitor's work area to the new monitor's work area. If a window is maximized, it is sized to the work area. Like with fullscreen, a maximized window has a 'restore position', though unlike with fullscreen the restore position for maximized is stored by the system itself. Migration in cases where a maximized (or fullscreen) window's monitor changes is also taken care of by the system. To restore 'safely' to maximized (after changing window styles) a window must only `SetWindowPos(SWP_FRAMECHANGED)`. While technically a maximized window that becomes fullscreen 'is still maximized' (from Win32's perspective), its prudent to also `ShowWindow(SW_MAXIMIZED)` prior to `SWP_FRAMECHANGED` (to explicitly make the window maximized). If not restoring to maximized, the restore position is adjusted by the new/ old work area. Additionally, the new/ old window DPI is used to adjust the size of the window by the DPI change (keeping the window's logical size the same). - The work area origin is checked first (shifting window rect by the change in origin) - The DPI is checked next, changing right/ bottom (size only) - Each edge of the window is compared against the corresponding edge of the work area, nudging the window back on-screen if hanging offscreen. By shifting right before left, bottom before top, the top-left is guaranteed on-screen. <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tried it out. Seemed to work on my machine. Jk, ran conhost/ terminal on mixed DPI system, max (or not), fullscreen, win+shift+left/ exit fullscreen/ maximize. Monitor unplug, etc.
2021-04-13 18:33:00 +02:00
// 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:
{
Eliminate more transient allocations: Titles and invalid rectangles and bitmap runs and utf8 conversions (#8621) ## References * See also #8617 ## PR Checklist * [x] Supports #3075 * [x] I work here. * [x] Manual test. ## Detailed Description of the Pull Request / Additional comments ### Window Title Generation Every time the renderer checks the title, it's doing two bad things that I've fixed: 1. It's assembling the prefix to the full title doing a concatenation. No one ever gets just the prefix ever after it is set besides the concat. So instead of storing prefix and the title, I store the assembled prefix + title and the bare title. 2. A copy must be made because it was returning `std::wstring` instead of `std::wstring&`. Now it returns the ref. ### Dirty Area Return Every time the renderer checks the dirty area, which is sometimes multiple times per pass (regular text printing, again for selection, etc.), a vector is created off the heap to return the rectangles. The consumers only ever iterate this data. Now we return a span over a rectangle or rectangles that the engine must store itself. 1. For some renderers, it's always a constant 1 element. They update that 1 element when dirty is queried and return it in the span with a span size of 1. 2. For other renderers with more complex behavior, they're already holding a cached vector of rectangles. Now it's effectively giving out the ref to those in the span for iteration. ### Bitmap Runs The `til::bitmap` used a `std::optional<std::vector<til::rectangle>>` inside itself to cache its runs and would clear the optional when the runs became invalidated. Unfortunately doing `.reset()` to clear the optional will destroy the underlying vector and have it release its memory. We know it's about to get reallocated again, so we're just going to make it a `std::pmr::vector` and give it a memory pool. The alternative solution here was to use a `bool` and `std::vector<til::rectangle>` and just flag when the vector was invalid, but that was honestly more code changes and I love excuses to try out PMR now. Also, instead of returning the ref to the vector... I'm just returning a span now. Everyone just iterates it anyway, may as well not share the implementation detail. ### UTF-8 conversions When testing with Terminal and looking at the `conhost.exe`'s PTY renderer, it spends a TON of allocation time on converting all the UTF-16 stuff inside to UTF-8 before it sends it out the PTY. This was because `ConvertToA` was allocating a string inside itself and returning it just to have it freed after printing and looping back around again... as a PTY does. The change here is to use `til::u16u8` that accepts a buffer out parameter so the caller can just hold onto it. ## Validation Steps Performed - [x] `big.txt` in conhost.exe (GDI renderer) - [x] `big.txt` in Terminal (DX, PTY renderer) - [x] Ensure WDDM and BGFX build under Razzle with this change.
2021-02-16 21:52:33 +01:00
// 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