// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include #include "menu.hpp" #include "icon.hpp" #include "window.hpp" #include "../../host/dbcs.h" #include "../../host/getset.h" #include "../../host/handle.h" #include "../../host/misc.h" #include "../../host/server.h" #include "../../host/scrolling.hpp" #include "../../host/telemetry.hpp" #include "../inc/ServiceLocator.hpp" using namespace Microsoft::Console::Interactivity::Win32; CONST WCHAR gwszPropertiesDll[] = L"\\console.dll"; CONST WCHAR gwszRelativePropertiesDll[] = L".\\console.dll"; #pragma hdrstop // Initialize static Menu variables. Menu* Menu::s_Instance = nullptr; #pragma region Public Methods Menu::Menu(HMENU hMenu, HMENU hHeirMenu) : _hMenu(hMenu), _hHeirMenu(hHeirMenu) { } // Routine Description: // - This routine allocates and initializes the system menu for the console // Arguments: // - hWnd - The handle to the console's window // Return Value: // - STATUS_SUCCESS or suitable NT error code [[nodiscard]] NTSTATUS Menu::CreateInstance(HWND hWnd) { NTSTATUS status = STATUS_SUCCESS; WCHAR ItemString[32]; // This gets the title bar menu. HMENU hMenu = GetSystemMenu(hWnd, FALSE); HMENU hHeirMenu = LoadMenuW(ServiceLocator::LocateGlobals().hInstance, MAKEINTRESOURCE(ID_CONSOLE_SYSTEMMENU)); Menu* pNewMenu = new (std::nothrow) Menu(hMenu, hHeirMenu); status = NT_TESTNULL(pNewMenu); if (NT_SUCCESS(status)) { int ItemLength; // Load the submenu to the system menu. if (hHeirMenu) { ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, ID_CONSOLE_EDIT, ItemString, ARRAYSIZE(ItemString)); if (ItemLength) { // Append the clipboard menu to system menu. AppendMenuW(hMenu, MF_POPUP | MF_STRING, (UINT_PTR)hHeirMenu, ItemString); } } // Edit the accelerators off of the standard items. // - Edits the indicated control to one word. // - Trim the Accelerator key text off of the end of the standard menu // items because we don't support the accelerators. ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, SC_CLOSE, ItemString, ARRAYSIZE(ItemString)); if (ItemLength != 0) { MENUITEMINFO mii = { 0 }; mii.cbSize = sizeof(mii); mii.fMask = MIIM_STRING | MIIM_BITMAP; mii.dwTypeData = ItemString; mii.hbmpItem = HBMMENU_POPUP_CLOSE; SetMenuItemInfoW(hMenu, SC_CLOSE, FALSE, &mii); } // Add other items to the system menu. ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, ID_CONSOLE_DEFAULTS, ItemString, ARRAYSIZE(ItemString)); if (ItemLength) { AppendMenuW(hMenu, MF_STRING, ID_CONSOLE_DEFAULTS, ItemString); } ItemLength = LoadStringW(ServiceLocator::LocateGlobals().hInstance, ID_CONSOLE_CONTROL, ItemString, ARRAYSIZE(ItemString)); if (ItemLength) { AppendMenuW(hMenu, MF_STRING, ID_CONSOLE_CONTROL, ItemString); } // Set the singleton instance. Menu::s_Instance = pNewMenu; } return status; } Menu* Menu::Instance() { return Menu::s_Instance; } Menu::~Menu() { } // Routine Description: // - this initializes the system menu when a WM_INITMENU message is read. void Menu::Initialize() { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); HMENU const hMenu = _hMenu; HMENU const hHeirMenu = _hHeirMenu; // If the console is iconic, disable Mark and Scroll. if (gci.Flags & CONSOLE_IS_ICONIC) { EnableMenuItem(hHeirMenu, ID_CONSOLE_MARK, MF_GRAYED); EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); } else { // if the console is not iconic // if there are no scroll bars // or we're in mark mode // disable scroll // else // enable scroll // // if we're in scroll mode // disable mark // else // enable mark if (gci.GetActiveOutputBuffer().IsMaximizedBoth() || gci.Flags & CONSOLE_SELECTING) { EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); } else { EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_ENABLED); } if (Scrolling::s_IsInScrollMode()) { EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_GRAYED); } else { EnableMenuItem(hHeirMenu, ID_CONSOLE_SCROLL, MF_ENABLED); } } // If we're selecting or scrolling, disable paste. Otherwise, enable it. if (gci.Flags & (CONSOLE_SELECTING | CONSOLE_SCROLLING)) { EnableMenuItem(hHeirMenu, ID_CONSOLE_PASTE, MF_GRAYED); } else { EnableMenuItem(hHeirMenu, ID_CONSOLE_PASTE, MF_ENABLED); } // If app has active selection, enable copy. Otherwise, disable it. if (gci.Flags & CONSOLE_SELECTING && Selection::Instance().IsAreaSelected()) { EnableMenuItem(hHeirMenu, ID_CONSOLE_COPY, MF_ENABLED); } else { EnableMenuItem(hHeirMenu, ID_CONSOLE_COPY, MF_GRAYED); } // Enable move if not iconic. if (gci.Flags & CONSOLE_IS_ICONIC) { EnableMenuItem(hMenu, SC_MOVE, MF_GRAYED); } else { EnableMenuItem(hMenu, SC_MOVE, MF_ENABLED); } // Enable settings. EnableMenuItem(hMenu, ID_CONSOLE_CONTROL, MF_ENABLED); } #pragma endregion #pragma region Public Static Methods // Displays the properties dialog and updates the window state as necessary. void Menu::s_ShowPropertiesDialog(HWND const hwnd, BOOL const Defaults) { CONSOLE_STATE_INFO StateInfo = { 0 }; if (!Defaults) { THROW_IF_FAILED(Menu::s_GetConsoleState(&StateInfo)); StateInfo.UpdateValues = FALSE; } // The Property sheet is going to copy the data from the values passed in // to it, and potentially overwrite StateInfo.*Title. // However, we just allocated wchar_t[]'s for these values. // Stash the pointers to the arrays we just allocated, so we can free those // arrays correctly. const wchar_t* const allocatedOriginalTitle = StateInfo.OriginalTitle; const wchar_t* const allocatedLinkTitle = StateInfo.LinkTitle; StateInfo.hWnd = hwnd; StateInfo.Defaults = Defaults; StateInfo.fIsV2Console = TRUE; UnlockConsole(); // First try to find the console.dll next to the launched exe, else default to /windows/system32/console.dll HANDLE hLibrary = LoadLibraryExW(gwszRelativePropertiesDll, nullptr, 0); bool fLoadedDll = hLibrary != nullptr; if (!fLoadedDll) { WCHAR wszFilePath[MAX_PATH + 1] = { 0 }; UINT const len = GetSystemDirectoryW(wszFilePath, ARRAYSIZE(wszFilePath)); if (len < ARRAYSIZE(wszFilePath)) { if (SUCCEEDED(StringCchCatW(wszFilePath, ARRAYSIZE(wszFilePath) - len, gwszPropertiesDll))) { wszFilePath[ARRAYSIZE(wszFilePath) - 1] = UNICODE_NULL; hLibrary = LoadLibraryExW(wszFilePath, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); fLoadedDll = hLibrary != nullptr; } } } if (fLoadedDll) { APPLET_PROC const pfnCplApplet = (APPLET_PROC)GetProcAddress((HMODULE)hLibrary, "CPlApplet"); if (pfnCplApplet != nullptr) { (*pfnCplApplet)(hwnd, CPL_INIT, 0, 0); (*pfnCplApplet)(hwnd, CPL_DBLCLK, (LPARAM)&StateInfo, 0); (*pfnCplApplet)(hwnd, CPL_EXIT, 0, 0); } LOG_IF_WIN32_BOOL_FALSE(FreeLibrary((HMODULE)hLibrary)); } LockConsole(); if (!Defaults && StateInfo.UpdateValues) { Menu::s_PropertiesUpdate(&StateInfo); } // s_GetConsoleState may have created new wchar_t[]s for the title and link title. // delete them before they're leaked. if (allocatedOriginalTitle != nullptr) { delete[] allocatedOriginalTitle; } if (allocatedLinkTitle != nullptr) { delete[] allocatedLinkTitle; } } [[nodiscard]] HRESULT Menu::s_GetConsoleState(CONSOLE_STATE_INFO* const pStateInfo) { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); pStateInfo->ScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); pStateInfo->WindowSize = ScreenInfo.GetViewport().Dimensions(); const RECT rcWindow = ServiceLocator::LocateConsoleWindow()->GetWindowRect(); pStateInfo->WindowPosX = rcWindow.left; pStateInfo->WindowPosY = rcWindow.top; const FontInfo& currentFont = ScreenInfo.GetCurrentFont(); pStateInfo->FontFamily = currentFont.GetFamily(); pStateInfo->FontSize = currentFont.GetUnscaledSize(); pStateInfo->FontWeight = currentFont.GetWeight(); LOG_IF_FAILED(StringCchCopyW(pStateInfo->FaceName, ARRAYSIZE(pStateInfo->FaceName), currentFont.GetFaceName().data())); const Cursor& cursor = ScreenInfo.GetTextBuffer().GetCursor(); pStateInfo->CursorSize = cursor.GetSize(); pStateInfo->CursorColor = cursor.GetColor(); pStateInfo->CursorType = static_cast(cursor.GetType()); // Retrieve small icon for use in displaying the dialog LOG_IF_FAILED(Icon::Instance().GetIcons(nullptr, &pStateInfo->hIcon)); pStateInfo->QuickEdit = !!(gci.Flags & CONSOLE_QUICK_EDIT_MODE); pStateInfo->AutoPosition = !!(gci.Flags & CONSOLE_AUTO_POSITION); pStateInfo->InsertMode = gci.GetInsertMode(); pStateInfo->ScreenAttributes = gci.GetFillAttribute(); pStateInfo->PopupAttributes = gci.GetPopupFillAttribute(); // Ensure that attributes are only describing colors to the properties dialog WI_ClearAllFlags(pStateInfo->ScreenAttributes, ~(FG_ATTRS | BG_ATTRS)); WI_ClearAllFlags(pStateInfo->PopupAttributes, ~(FG_ATTRS | BG_ATTRS)); pStateInfo->HistoryBufferSize = gci.GetHistoryBufferSize(); pStateInfo->NumberOfHistoryBuffers = gci.GetNumberOfHistoryBuffers(); pStateInfo->HistoryNoDup = !!(gci.Flags & CONSOLE_HISTORY_NODUP); for (size_t i = 0; i < std::size(pStateInfo->ColorTable); i++) { pStateInfo->ColorTable[i] = gci.GetLegacyColorTableEntry(i); } // Create mutable copies of the titles so the propsheet can do something with them. if (gci.GetOriginalTitle().length() > 0) { pStateInfo->OriginalTitle = new (std::nothrow) wchar_t[gci.GetOriginalTitle().length() + 1]{ UNICODE_NULL }; RETURN_IF_NULL_ALLOC(pStateInfo->OriginalTitle); gci.GetOriginalTitle().copy(pStateInfo->OriginalTitle, gci.GetOriginalTitle().length()); } else { pStateInfo->OriginalTitle = nullptr; } if (gci.GetLinkTitle().length() > 0) { pStateInfo->LinkTitle = new (std::nothrow) wchar_t[gci.GetLinkTitle().length() + 1]{ UNICODE_NULL }; RETURN_IF_NULL_ALLOC(pStateInfo->LinkTitle); gci.GetLinkTitle().copy(pStateInfo->LinkTitle, gci.GetLinkTitle().length()); } else { pStateInfo->LinkTitle = nullptr; } pStateInfo->CodePage = gci.OutputCP; // begin console v2 properties pStateInfo->fIsV2Console = TRUE; pStateInfo->fWrapText = gci.GetWrapText(); pStateInfo->fFilterOnPaste = !!gci.GetFilterOnPaste(); pStateInfo->fCtrlKeyShortcutsDisabled = gci.GetCtrlKeyShortcutsDisabled(); pStateInfo->fLineSelection = gci.GetLineSelection(); pStateInfo->bWindowTransparency = ServiceLocator::LocateConsoleWindow()->GetWindowOpacity(); pStateInfo->InterceptCopyPaste = gci.GetInterceptCopyPaste(); // Get the properties from the settings pStateInfo->DefaultForeground = gci.GetDefaultForegroundColor(); pStateInfo->DefaultBackground = gci.GetDefaultBackgroundColor(); pStateInfo->TerminalScrolling = gci.IsTerminalScrolling(); // end console v2 properties return S_OK; } HMENU Menu::s_GetMenuHandle() { if (Menu::s_Instance) { return Menu::s_Instance->_hMenu; } else { return nullptr; } } HMENU Menu::s_GetHeirMenuHandle() { if (Menu::s_Instance) { return Menu::s_Instance->_hHeirMenu; } else { return nullptr; } } #pragma endregion #pragma region Private Methods // Updates the console state from information sent by the properties dialog box. void Menu::s_PropertiesUpdate(PCONSOLE_STATE_INFO pStateInfo) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); if (gci.OutputCP != pStateInfo->CodePage) { gci.OutputCP = pStateInfo->CodePage; SetConsoleCPInfo(TRUE); } if (gci.CP != pStateInfo->CodePage) { gci.CP = pStateInfo->CodePage; SetConsoleCPInfo(FALSE); } // begin V2 console properties // NOTE: We must set the wrap state before further manipulating the buffer/window. // If we do not, the user will get a different result than the preview (e.g. we'll resize without scroll bars first then turn off wrapping.) gci.SetWrapText(!!pStateInfo->fWrapText); gci.SetFilterOnPaste(!!pStateInfo->fFilterOnPaste); gci.SetCtrlKeyShortcutsDisabled(!!pStateInfo->fCtrlKeyShortcutsDisabled); gci.SetLineSelection(!!pStateInfo->fLineSelection); Selection::Instance().SetLineSelection(!!gci.GetLineSelection()); ServiceLocator::LocateConsoleWindow()->SetWindowOpacity(pStateInfo->bWindowTransparency); ServiceLocator::LocateConsoleWindow()->ApplyWindowOpacity(); // end V2 console properties // Apply font information (must come before all character calculations for window/buffer size). FontInfo fiNewFont(pStateInfo->FaceName, gsl::narrow_cast(pStateInfo->FontFamily), pStateInfo->FontWeight, pStateInfo->FontSize, pStateInfo->CodePage); ScreenInfo.UpdateFont(&fiNewFont); const FontInfo& fontApplied = ScreenInfo.GetCurrentFont(); // Now make sure internal font state reflects the font chosen gci.SetFontFamily(fontApplied.GetFamily()); gci.SetFontSize(fontApplied.GetUnscaledSize()); gci.SetFontWeight(fontApplied.GetWeight()); gci.SetFaceName(fontApplied.GetFaceName()); // Set the cursor properties in the Settings const auto cursorType = static_cast(pStateInfo->CursorType); gci.SetCursorColor(pStateInfo->CursorColor); gci.SetCursorType(cursorType); // Then also apply them to the buffer's cursor ScreenInfo.SetCursorInformation(pStateInfo->CursorSize, ScreenInfo.GetTextBuffer().GetCursor().IsVisible()); ScreenInfo.SetCursorColor(pStateInfo->CursorColor, true); ScreenInfo.SetCursorType(cursorType, true); gci.SetTerminalScrolling(pStateInfo->TerminalScrolling); { // Requested window in characters COORD coordWindow = pStateInfo->WindowSize; // Requested buffer in characters. COORD coordBuffer = pStateInfo->ScreenBufferSize; // First limit the window so it cannot be bigger than the monitor. // Maximum number of characters we could fit on the given monitor. COORD const coordLargest = ScreenInfo.GetLargestWindowSizeInCharacters(); coordWindow.X = std::min(coordLargest.X, coordWindow.X); coordWindow.Y = std::min(coordLargest.Y, coordWindow.Y); if (gci.GetWrapText()) { // Then if wrap text is on, the buffer width gets fixed to the window width value. coordBuffer.X = coordWindow.X; // However, we're not done. The "max window size" is if we had no scroll bar. // We need to adjust slightly more if there's space reserved for a vertical scroll bar // which happens when the buffer Y is taller than the window Y. if (coordBuffer.Y > coordWindow.Y) { // Since we need a scroll bar in the Y direction, clamp the buffer width to make sure that // it is leaving appropriate space for a scroll bar. COORD const coordScrollBars = ScreenInfo.GetScrollBarSizesInCharacters(); SHORT const sMaxBufferWidthWithScroll = coordLargest.X - coordScrollBars.X; coordBuffer.X = std::min(coordBuffer.X, sMaxBufferWidthWithScroll); } } // Now adjust the buffer size first to whatever we want it to be if it's different than before. const COORD coordScreenBufferSize = ScreenInfo.GetBufferSize().Dimensions(); if (coordBuffer.X != coordScreenBufferSize.X || coordBuffer.Y != coordScreenBufferSize.Y) { CommandLine* const pCommandLine = &CommandLine::Instance(); pCommandLine->Hide(FALSE); LOG_IF_FAILED(ScreenInfo.ResizeScreenBuffer(coordBuffer, TRUE)); pCommandLine->Show(); } // Finally, restrict window size to the maximum possible size for the given buffer now that it's processed. COORD const coordMaxForBuffer = ScreenInfo.GetMaxWindowSizeInCharacters(); coordWindow.X = std::min(coordWindow.X, coordMaxForBuffer.X); coordWindow.Y = std::min(coordWindow.Y, coordMaxForBuffer.Y); // Then finish by updating the window. This will update the window size, // as well as the screen buffer's viewport. ServiceLocator::LocateConsoleWindow()->UpdateWindowSize(coordWindow); } if (pStateInfo->QuickEdit) { gci.Flags |= CONSOLE_QUICK_EDIT_MODE; } else { gci.Flags &= ~CONSOLE_QUICK_EDIT_MODE; } if (pStateInfo->AutoPosition) { gci.Flags |= CONSOLE_AUTO_POSITION; } else { gci.Flags &= ~CONSOLE_AUTO_POSITION; POINT pt; pt.x = pStateInfo->WindowPosX; pt.y = pStateInfo->WindowPosY; ServiceLocator::LocateConsoleWindow()->UpdateWindowPosition(pt); } if (gci.GetInsertMode() != !!pStateInfo->InsertMode) { ScreenInfo.SetCursorDBMode(false); gci.SetInsertMode(pStateInfo->InsertMode != FALSE); if (gci.HasPendingCookedRead()) { gci.CookedReadData().SetInsertMode(gci.GetInsertMode()); } } for (size_t i = 0; i < std::size(pStateInfo->ColorTable); i++) { gci.SetLegacyColorTableEntry(i, pStateInfo->ColorTable[i]); } // Ensure that attributes only contain color specification. WI_ClearAllFlags(pStateInfo->ScreenAttributes, ~(FG_ATTRS | BG_ATTRS)); WI_ClearAllFlags(pStateInfo->PopupAttributes, ~(FG_ATTRS | BG_ATTRS)); // Place our new legacy fill attributes in gci // (recall they are already persisted to the reg/link by the propsheet // when it was closed) gci.SetFillAttribute(pStateInfo->ScreenAttributes); gci.SetPopupFillAttribute(pStateInfo->PopupAttributes); // Store our updated Default Color values gci.SetDefaultForegroundColor(pStateInfo->DefaultForeground); gci.SetDefaultBackgroundColor(pStateInfo->DefaultBackground); // Make sure the updated fill attributes are passed on to the TextAttribute class. TextAttribute::SetLegacyDefaultAttributes(pStateInfo->ScreenAttributes); // Set the screen info's default text attributes to defaults - ScreenInfo.SetDefaultAttributes({}, TextAttribute{ gci.GetPopupFillAttribute() }); CommandHistory::s_ResizeAll(pStateInfo->HistoryBufferSize); gci.SetNumberOfHistoryBuffers(pStateInfo->NumberOfHistoryBuffers); if (pStateInfo->HistoryNoDup) { gci.Flags |= CONSOLE_HISTORY_NODUP; } else { gci.Flags &= ~CONSOLE_HISTORY_NODUP; } // Since edit keys are global state only stored once in the registry, post the message to the queue to reload // those properties specifically from the registry in case they were changed. ServiceLocator::LocateConsoleWindow()->PostUpdateExtendedEditKeys(); gci.ConsoleIme.RefreshAreaAttributes(); gci.SetInterceptCopyPaste(!!pStateInfo->InterceptCopyPaste); } #pragma endregion