// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "_output.h" #include "stream.h" #include "scrolling.hpp" #include "../interactivity/inc/ServiceLocator.hpp" using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::Types; Selection::Selection() : _fSelectionVisible(false), _ulSavedCursorSize(0), _fSavedCursorVisible(false), _savedCursorType(CursorType::Legacy), _dwSelectionFlags(0), _fLineSelection(true), _fUseAlternateSelection(false), _allowMouseDragSelection{ true } { ZeroMemory((void*)&_srSelectionRect, sizeof(_srSelectionRect)); ZeroMemory((void*)&_coordSelectionAnchor, sizeof(_coordSelectionAnchor)); ZeroMemory((void*)&_coordSavedCursorPosition, sizeof(_coordSavedCursorPosition)); } Selection& Selection::Instance() { static std::unique_ptr _instance{ new Selection() }; return *_instance; } // Routine Description: // - Determines the line-by-line selection rectangles based on global selection state. // Arguments: // - - Uses internal state to know what area is selected already. // Return Value: // - Returns a vector where each SMALL_RECT is one Row worth of the area to be selected. // - Returns empty vector if no rows are selected. // - Throws exceptions for out of memory issues std::vector Selection::GetSelectionRects() const { if (!_fSelectionVisible) { return std::vector(); } const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& screenInfo = gci.GetActiveOutputBuffer(); // _coordSelectionAnchor is at one of the corners of _srSelectionRects // endSelectionAnchor is at the exact opposite corner COORD endSelectionAnchor; endSelectionAnchor.X = (_coordSelectionAnchor.X == _srSelectionRect.Left) ? _srSelectionRect.Right : _srSelectionRect.Left; endSelectionAnchor.Y = (_coordSelectionAnchor.Y == _srSelectionRect.Top) ? _srSelectionRect.Bottom : _srSelectionRect.Top; const auto blockSelection = !IsLineSelection(); return screenInfo.GetTextBuffer().GetTextRects(_coordSelectionAnchor, endSelectionAnchor, blockSelection, false); } // Routine Description: // - Shows the selection area in the window if one is available and not already showing. // Arguments: // // Return Value: // void Selection::ShowSelection() { _SetSelectionVisibility(true); } // Routine Description: // - Hides the selection area in the window if one is available and already showing. // Arguments: // // Return Value: // void Selection::HideSelection() { _SetSelectionVisibility(false); } // Routine Description: // - Changes the visibility of the selection area on the screen. // - Used to turn the selection area on or off. // Arguments: // - fMakeVisible - If TRUE, we're turning the selection ON. // - If FALSE, we're turning the selection OFF. // Return Value: // void Selection::_SetSelectionVisibility(const bool fMakeVisible) { if (IsInSelectingState() && IsAreaSelected()) { if (fMakeVisible == _fSelectionVisible) { return; } _fSelectionVisible = fMakeVisible; _PaintSelection(); } LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); } // Routine Description: // - Inverts the selected region on the current screen buffer. // - Reads the selected area, selection mode, and active screen buffer // from the global properties and dispatches a GDI invert on the selected text area. // Arguments: // - // Return Value: // - void Selection::_PaintSelection() const { if (ServiceLocator::LocateGlobals().pRender != nullptr) { ServiceLocator::LocateGlobals().pRender->TriggerSelection(); } } // Routine Description: // - Starts the selection with the given initial position // Arguments: // - coordBufferPos - Position in which user started a selection // Return Value: // - void Selection::InitializeMouseSelection(const COORD coordBufferPos) { Scrolling::s_ClearScroll(); // set flags _SetSelectingState(true); _dwSelectionFlags = CONSOLE_MOUSE_SELECTION | CONSOLE_SELECTION_NOT_EMPTY; // store anchor and rectangle of selection _coordSelectionAnchor = coordBufferPos; // since we've started with just a point, the rectangle is 1x1 on the point given _srSelectionRect.Left = coordBufferPos.X; _srSelectionRect.Right = coordBufferPos.X; _srSelectionRect.Top = coordBufferPos.Y; _srSelectionRect.Bottom = coordBufferPos.Y; // Check for ALT-Mouse Down "use alternate selection" // If in box mode, use line mode. If in line mode, use box mode. CheckAndSetAlternateSelection(); // set window title to mouse selection mode IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) { pWindow->UpdateWindowText(); LOG_IF_FAILED(pWindow->SignalUia(UIA_Text_TextSelectionChangedEventId)); } // Fire off an event to let accessibility apps know the selection has changed. auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); if (pNotifier) { pNotifier->NotifyConsoleCaretEvent(IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection, PACKCOORD(coordBufferPos)); } } // Routine Description: // - Modifies both ends of the current selection. // - Intended for use with functions that help auto-complete a selection area (e.g. double clicking) // Arguments: // - coordSelectionStart - Replaces the selection anchor, a.k.a. where the selection started from originally. // - coordSelectionEnd - The linear final position or opposite corner of the anchor to represent the complete selection area. // Return Value: // - void Selection::AdjustSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd) { // modify the anchor and then just use extend to adjust the other portion of the selection rectangle _coordSelectionAnchor = coordSelectionStart; ExtendSelection(coordSelectionEnd); _allowMouseDragSelection = false; } // Routine Description: // - Extends the selection out to the given position from the initial anchor point. // This means that a coordinate farther away will make the rectangle larger and a closer one will shrink it. // Arguments: // - coordBufferPos - Position to extend/contract the current selection up to. // Return Value: // - void Selection::ExtendSelection(_In_ COORD coordBufferPos) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); _allowMouseDragSelection = true; // ensure position is within buffer bounds. Not less than 0 and not greater than the screen buffer size. try { screenInfo.GetTerminalBufferSize().Clamp(coordBufferPos); } CATCH_LOG_RETURN(); if (!IsAreaSelected()) { // we should only be extending a selection that has no area yet if we're coming from mark mode. // if not, just return. if (IsMouseInitiatedSelection()) { return; } // scroll if necessary to make cursor visible. screenInfo.MakeCursorVisible(coordBufferPos, false); _dwSelectionFlags |= CONSOLE_SELECTION_NOT_EMPTY; _srSelectionRect.Left = _srSelectionRect.Right = _coordSelectionAnchor.X; _srSelectionRect.Top = _srSelectionRect.Bottom = _coordSelectionAnchor.Y; ShowSelection(); } else { // scroll if necessary to make cursor visible. screenInfo.MakeCursorVisible(coordBufferPos, false); } // remember previous selection rect SMALL_RECT srNewSelection = _srSelectionRect; // update selection rect // this adjusts the rectangle dimensions based on which way the move was requested // in respect to the original selection position (the anchor) if (coordBufferPos.X <= _coordSelectionAnchor.X) { srNewSelection.Left = coordBufferPos.X; srNewSelection.Right = _coordSelectionAnchor.X; } else if (coordBufferPos.X > _coordSelectionAnchor.X) { srNewSelection.Right = coordBufferPos.X; srNewSelection.Left = _coordSelectionAnchor.X; } if (coordBufferPos.Y <= _coordSelectionAnchor.Y) { srNewSelection.Top = coordBufferPos.Y; srNewSelection.Bottom = _coordSelectionAnchor.Y; } else if (coordBufferPos.Y > _coordSelectionAnchor.Y) { srNewSelection.Bottom = coordBufferPos.Y; srNewSelection.Top = _coordSelectionAnchor.Y; } // This function is called on WM_MOUSEMOVE. // Prevent triggering an invalidation just because the mouse moved // in the same cell without changing the actual (visible) selection. if (_srSelectionRect == srNewSelection) { return; } // call special update method to modify the displayed selection in-place // NOTE: Using HideSelection, editing the rectangle, then ShowSelection will cause flicker. //_PaintUpdateSelection(&srNewSelection); _srSelectionRect = srNewSelection; _PaintSelection(); // Fire off an event to let accessibility apps know the selection has changed. auto pNotifier = ServiceLocator::LocateAccessibilityNotifier(); if (pNotifier) { pNotifier->NotifyConsoleCaretEvent(IAccessibilityNotifier::ConsoleCaretEventFlags::CaretSelection, PACKCOORD(coordBufferPos)); } LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); } // Routine Description: // - Cancels any mouse selection state to return to normal mode. // Arguments: // - (Uses global state) // Return Value: // - void Selection::_CancelMouseSelection() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); // invert old select rect. if we're selecting by mouse, we // always have a selection rect. HideSelection(); // turn off selection flag _SetSelectingState(false); IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) { pWindow->UpdateWindowText(); } // Mark the cursor position as changed so we'll fire off a win event. ScreenInfo.GetTextBuffer().GetCursor().SetHasMoved(true); } // Routine Description: // - Cancels any mark mode selection state to return to normal mode. // Arguments: // - // Return Value: // - void Selection::_CancelMarkSelection() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& ScreenInfo = gci.GetActiveOutputBuffer(); // Hide existing selection, if we have one. if (IsAreaSelected()) { HideSelection(); } // Turn off selection flag. _SetSelectingState(false); IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) { pWindow->UpdateWindowText(); } // restore text cursor _RestoreDataToCursor(ScreenInfo.GetTextBuffer().GetCursor()); } // Routine Description: // - If a selection exists, clears it and restores the state. // Will also unblock a blocked write if one exists. // Arguments: // - (Uses global state) // Return Value: // - void Selection::ClearSelection() { ClearSelection(false); } // Routine Description: // - If a selection exists, clears it and restores the state. // - Will only unblock a write if not starting a new selection. // Arguments: // - fStartingNewSelection - If we're going to start another selection right away, we'll keep the write blocked. // Return Value: // - void Selection::ClearSelection(const bool fStartingNewSelection) { if (IsInSelectingState()) { if (IsMouseInitiatedSelection()) { _CancelMouseSelection(); } else { _CancelMarkSelection(); } LOG_IF_FAILED(ServiceLocator::LocateConsoleWindow()->SignalUia(UIA_Text_TextSelectionChangedEventId)); _dwSelectionFlags = 0; // If we were using alternate selection, cancel it here before starting a new area. _fUseAlternateSelection = false; // Only unblock if we're not immediately starting a new selection. Otherwise stay blocked. if (!fStartingNewSelection) { UnblockWriteConsole(CONSOLE_SELECTING); } } } // Routine Description: // - Colors all text in the given rectangle with the color attribute provided. // - This does not validate whether there is a valid selection right now or not. // It is assumed to already be in a proper selecting state and the given rectangle should be highlighted with the given color unconditionally. // Arguments: // - psrRect - Rectangular area to fill with color // - attr - The color attributes to apply void Selection::ColorSelection(const SMALL_RECT& srRect, const TextAttribute attr) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // Read selection rectangle, assumed already clipped to buffer. SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); COORD coordTargetSize; coordTargetSize.X = CalcWindowSizeX(srRect); coordTargetSize.Y = CalcWindowSizeY(srRect); COORD coordTarget; coordTarget.X = srRect.Left; coordTarget.Y = srRect.Top; // Now color the selection a line at a time. for (; (coordTarget.Y < srRect.Top + coordTargetSize.Y); ++coordTarget.Y) { const size_t cchWrite = gsl::narrow(coordTargetSize.X); try { screenInfo.Write(OutputCellIterator(attr, cchWrite), coordTarget); } CATCH_LOG(); } } // Routine Description: // - Given two points in the buffer space, color the selection between the two with the given attribute. // - This will create an internal selection rectangle covering the two points, assume a line selection, // and use the first point as the anchor for the selection (as if the mouse click started at that point) // Arguments: // - coordSelectionStart - Anchor point (start of selection) for the region to be colored // - coordSelectionEnd - Other point referencing the rectangle inscribing the selection area // - attr - Color to apply to region. void Selection::ColorSelection(const COORD coordSelectionStart, const COORD coordSelectionEnd, const TextAttribute attr) { // Extract row-by-row selection rectangles for the selection area. try { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto& screenInfo = gci.GetActiveOutputBuffer(); const auto rectangles = screenInfo.GetTextBuffer().GetTextRects(coordSelectionStart, coordSelectionEnd, false, true); for (const auto& rect : rectangles) { ColorSelection(rect, attr); } } CATCH_LOG(); } // Routine Description: // - Enters mark mode selection. Prepares the cursor to move around to select a region and sets up state variables. // Arguments: // - // Return Value: // - void Selection::InitializeMarkSelection() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // clear any existing selection. ClearSelection(true); Scrolling::s_ClearScroll(); // set flags _SetSelectingState(true); _dwSelectionFlags = 0; // save old cursor position and make console cursor into selection cursor. SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); const auto& cursor = screenInfo.GetTextBuffer().GetCursor(); _SaveCursorData(cursor); screenInfo.SetCursorInformation(100, TRUE); const COORD coordPosition = cursor.GetPosition(); LOG_IF_FAILED(screenInfo.SetCursorPosition(coordPosition, true)); // set the cursor position as the anchor position // it will get updated as the cursor moves for mark mode, // but it serves to prepare us for the inevitable start of the selection with Shift+Arrow Key _coordSelectionAnchor = coordPosition; // set frame title text IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (pWindow != nullptr) { pWindow->UpdateWindowText(); LOG_IF_FAILED(pWindow->SignalUia(UIA_Text_TextSelectionChangedEventId)); } } // Routine Description: // - Resets the current selection and selects a new region from the start to end coordinates // Arguments: // - coordStart - Position to start selection area from // - coordEnd - Position to select up to // Return Value: // - void Selection::SelectNewRegion(const COORD coordStart, const COORD coordEnd) { // clear existing selection if applicable ClearSelection(); // initialize selection InitializeMouseSelection(coordStart); ShowSelection(); // extend selection ExtendSelection(coordEnd); } // Routine Description: // - Creates a new selection region of "all" available text. // The meaning of "all" can vary. If we have input text, then "all" is just the input text. // If we have no input text, "all" is the entire valid screen buffer (output text and the prompt) // Arguments: // - (Uses global state) // Return Value: // - void Selection::SelectAll() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // save the old window position SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer(); COORD coordWindowOrigin = screenInfo.GetViewport().Origin(); // Get existing selection rectangle parameters const bool fOldSelectionExisted = IsAreaSelected(); const SMALL_RECT srOldSelection = _srSelectionRect; const COORD coordOldAnchor = _coordSelectionAnchor; // Attempt to get the boundaries of the current input line. COORD coordInputStart; COORD coordInputEnd; const bool fHasInputArea = s_GetInputLineBoundaries(&coordInputStart, &coordInputEnd); // These variables will be used to specify the new selection area when we're done COORD coordNewSelStart; COORD coordNewSelEnd; // Now evaluate conditions and attempt to assign a new selection area. if (!fHasInputArea) { // If there's no input area, just select the entire valid text region. GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); } else { if (!fOldSelectionExisted) { // Temporary workaround until MSFT: 614579 is completed. const auto bufferSize = screenInfo.GetBufferSize(); COORD coordOneAfterEnd = coordInputEnd; bufferSize.IncrementInBounds(coordOneAfterEnd); if (s_IsWithinBoundaries(screenInfo.GetTextBuffer().GetCursor().GetPosition(), coordInputStart, coordInputEnd)) { // If there was no previous selection and the cursor is within the input line, select the input line only coordNewSelStart = coordInputStart; coordNewSelEnd = coordInputEnd; } else if (s_IsWithinBoundaries(screenInfo.GetTextBuffer().GetCursor().GetPosition(), coordOneAfterEnd, coordOneAfterEnd)) { // Temporary workaround until MSFT: 614579 is completed. // Select only the input line if the cursor is one after the final position of the input line. coordNewSelStart = coordInputStart; coordNewSelEnd = coordInputEnd; } else { // otherwise if the cursor is elsewhere, select everything GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); } } else { // This is the complex case. We had an existing selection and we have an input area. // To figure this out, we need the anchor (the point where the selection starts) and its opposite corner COORD coordOldAnchorOpposite = Utils::s_GetOppositeCorner(srOldSelection, coordOldAnchor); // Check if both anchor and opposite corner fall within the input line const bool fIsOldSelWithinInput = s_IsWithinBoundaries(coordOldAnchor, coordInputStart, coordInputEnd) && s_IsWithinBoundaries(coordOldAnchorOpposite, coordInputStart, coordInputEnd); // Check if both anchor and opposite corner are exactly the bounds of the input line const bool fAllInputSelected = ((Utils::s_CompareCoords(coordInputStart, coordOldAnchor) == 0 && Utils::s_CompareCoords(coordInputEnd, coordOldAnchorOpposite) == 0) || (Utils::s_CompareCoords(coordInputStart, coordOldAnchorOpposite) == 0 && Utils::s_CompareCoords(coordInputEnd, coordOldAnchor) == 0)); if (fIsOldSelWithinInput && !fAllInputSelected) { // If it's within the input area and the whole input is not selected, then select just the input coordNewSelStart = coordInputStart; coordNewSelEnd = coordInputEnd; } else { // Otherwise just select the whole valid area GetValidAreaBoundaries(&coordNewSelStart, &coordNewSelEnd); } } } // If we're in box selection, adjust end coordinate to end of line and start coordinate to start of line // or it won't be selecting all the text. if (!IsLineSelection()) { coordNewSelStart.X = 0; coordNewSelEnd.X = screenInfo.GetBufferSize().RightInclusive(); } SelectNewRegion(coordNewSelStart, coordNewSelEnd); // restore the old window position LOG_IF_FAILED(screenInfo.SetViewportOrigin(true, coordWindowOrigin, true)); }