From e79a421f3a86266025ab1095d887de75a5067b8e Mon Sep 17 00:00:00 2001 From: Carlos Zamora Date: Mon, 9 Mar 2020 08:17:34 -0700 Subject: [PATCH] Abstract GetTextForClipboard() for UIA (#4578) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request `GetTextForClipboard` already exists in the TextBuffer. It makes sense to use that for UIA as well. This changes the behavior or `GetText()` such that it does not remove leading/trailing whitespace anymore. That is more of an expected behavior. ## References This also contributes to... - #4509: UIA Box Selection - #2447: UIA Signaling for Selection - #1354: UIA support for Wide Glyphs Now that the expansion occurs at before render-time, the selection anchors are an accurate representation of what is selected. We just need to move GetText to the TextBuffer. Then we can have those three issues just rely on code from the text buffer. This also means ConHost gets some of this stuff for free 😀 ## Detailed Description of the Pull Request / Additional comments - `TextBuffer::GetTextForClipboard()` --> `GetText()` - `TextBuffer::GetText()` no longer requires GetForegroundColor/GetBackgroundColor. If either of these are not defined, we return a `TextAndColor` with only the `text` field populated. - renamed a few parameters for copying text to the clipboard for clarity - Updated `UiaTextRange::GetText()` to use `TextBuffer::GetText()` ## Validation Steps Performed Manual tests for UIA using accessibility insights and Windows Terminal's copy action (w/ and w/out shift) Added tests as well. --- src/buffer/out/textBuffer.cpp | 100 ++++---- src/buffer/out/textBuffer.hpp | 10 +- src/cascadia/TerminalApp/TerminalPage.cpp | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 11 +- src/cascadia/TerminalControl/TermControl.h | 2 +- src/cascadia/TerminalControl/TermControl.idl | 2 +- .../TerminalCore/TerminalSelection.cpp | 27 +-- src/host/ut_host/ClipboardTests.cpp | 6 +- src/host/ut_host/TextBufferTests.cpp | 215 +++++++++++++++++- src/interactivity/win32/Clipboard.cpp | 51 ++--- src/interactivity/win32/clipboard.hpp | 6 +- src/types/UiaTextRangeBase.cpp | 85 +++---- src/types/UiaTextRangeBase.hpp | 2 +- 13 files changed, 353 insertions(+), 166 deletions(-) diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp index a6b3324077a..1aaec1bffbc 100644 --- a/src/buffer/out/textBuffer.cpp +++ b/src/buffer/out/textBuffer.cpp @@ -1364,26 +1364,30 @@ void TextBuffer::_ExpandTextRow(SMALL_RECT& textRow) const // Routine Description: // - Retrieves the text data from the selected region and presents it in a clipboard-ready format (given little post-processing). // Arguments: -// - lineSelection - true if entire line is being selected. False otherwise (box selection) -// - trimTrailingWhitespace - setting flag removes trailing whitespace at the end of each row in selection -// - selectionRects - the selection regions from which the data will be extracted from the buffer -// - GetForegroundColor - function used to map TextAttribute to RGB COLORREF for foreground color -// - GetBackgroundColor - function used to map TextAttribute to RGB COLORREF for foreground color +// - includeCRLF - inject CRLF pairs to the end of each line +// - trimTrailingWhitespace - remove the trailing whitespace at the end of each line +// - textRects - the rectangular regions from which the data will be extracted from the buffer (i.e.: selection rects) +// - GetForegroundColor - function used to map TextAttribute to RGB COLORREF for foreground color. If null, only extract the text. +// - GetBackgroundColor - function used to map TextAttribute to RGB COLORREF for background color. If null, only extract the text. // Return Value: // - The text, background color, and foreground color data of the selected region of the text buffer. -const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSelection, - const bool trimTrailingWhitespace, - const std::vector& selectionRects, - std::function GetForegroundColor, - std::function GetBackgroundColor) const +const TextBuffer::TextAndColor TextBuffer::GetText(const bool includeCRLF, + const bool trimTrailingWhitespace, + const std::vector& selectionRects, + std::function GetForegroundColor, + std::function GetBackgroundColor) const { TextAndColor data; + const bool copyTextColor = GetForegroundColor && GetBackgroundColor; // preallocate our vectors to reduce reallocs size_t const rows = selectionRects.size(); data.text.reserve(rows); - data.FgAttr.reserve(rows); - data.BkAttr.reserve(rows); + if (copyTextColor) + { + data.FgAttr.reserve(rows); + data.BkAttr.reserve(rows); + } // for each row in the selection for (UINT i = 0; i < rows; i++) @@ -1402,24 +1406,31 @@ const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSe // preallocate to avoid reallocs selectionText.reserve(gsl::narrow(highlight.Width()) + 2); // + 2 for \r\n if we munged it - selectionFgAttr.reserve(gsl::narrow(highlight.Width()) + 2); - selectionBkAttr.reserve(gsl::narrow(highlight.Width()) + 2); + if (copyTextColor) + { + selectionFgAttr.reserve(gsl::narrow(highlight.Width()) + 2); + selectionBkAttr.reserve(gsl::narrow(highlight.Width()) + 2); + } // copy char data into the string buffer, skipping trailing bytes while (it) { const auto& cell = *it; - auto cellData = cell.TextAttr(); - COLORREF const CellFgAttr = GetForegroundColor(cellData); - COLORREF const CellBkAttr = GetBackgroundColor(cellData); if (!cell.DbcsAttr().IsTrailing()) { selectionText.append(cell.Chars()); - for (const wchar_t wch : cell.Chars()) + + if (copyTextColor) { - selectionFgAttr.push_back(CellFgAttr); - selectionBkAttr.push_back(CellBkAttr); + auto cellData = cell.TextAttr(); + COLORREF const CellFgAttr = GetForegroundColor(cellData); + COLORREF const CellBkAttr = GetBackgroundColor(cellData); + for (const wchar_t wch : cell.Chars()) + { + selectionFgAttr.push_back(CellFgAttr); + selectionBkAttr.push_back(CellBkAttr); + } } } #pragma warning(suppress : 26444) @@ -1427,35 +1438,41 @@ const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSe it++; } - // trim trailing spaces if SHIFT key not held + const bool forcedWrap = GetRowByOffset(iRow).GetCharRow().WasWrapForced(); + if (trimTrailingWhitespace) { - const ROW& Row = GetRowByOffset(iRow); - - // FOR LINE SELECTION ONLY: if the row was wrapped, don't remove the spaces at the end. - if (!lineSelection || !Row.GetCharRow().WasWrapForced()) + // if the row was NOT wrapped... + if (!forcedWrap) { + // remove the spaces at the end (aka trim the trailing whitespace) while (!selectionText.empty() && selectionText.back() == UNICODE_SPACE) { selectionText.pop_back(); - selectionFgAttr.pop_back(); - selectionBkAttr.pop_back(); + if (copyTextColor) + { + selectionFgAttr.pop_back(); + selectionBkAttr.pop_back(); + } } } + } - // apply CR/LF to the end of the final string, unless we're the last line. - // a.k.a if we're earlier than the bottom, then apply CR/LF. - if (i < selectionRects.size() - 1) + // apply CR/LF to the end of the final string, unless we're the last line. + // a.k.a if we're earlier than the bottom, then apply CR/LF. + if (includeCRLF && i < selectionRects.size() - 1) + { + // if the row was NOT wrapped... + if (!forcedWrap) { - // FOR LINE SELECTION ONLY: if the row was wrapped, do not apply CR/LF. - // a.k.a. if the row was NOT wrapped, then we can assume a CR/LF is proper - // always apply \r\n for box selection - if (!lineSelection || !GetRowByOffset(iRow).GetCharRow().WasWrapForced()) - { - COLORREF const Blackness = RGB(0x00, 0x00, 0x00); // cant see CR/LF so just use black FG & BK + // then we can assume a CR/LF is proper + selectionText.push_back(UNICODE_CARRIAGERETURN); + selectionText.push_back(UNICODE_LINEFEED); - selectionText.push_back(UNICODE_CARRIAGERETURN); - selectionText.push_back(UNICODE_LINEFEED); + if (copyTextColor) + { + // cant see CR/LF so just use black FG & BK + COLORREF const Blackness = RGB(0x00, 0x00, 0x00); selectionFgAttr.push_back(Blackness); selectionFgAttr.push_back(Blackness); selectionBkAttr.push_back(Blackness); @@ -1465,8 +1482,11 @@ const TextBuffer::TextAndColor TextBuffer::GetTextForClipboard(const bool lineSe } data.text.emplace_back(std::move(selectionText)); - data.FgAttr.emplace_back(std::move(selectionFgAttr)); - data.BkAttr.emplace_back(std::move(selectionBkAttr)); + if (copyTextColor) + { + data.FgAttr.emplace_back(std::move(selectionFgAttr)); + data.BkAttr.emplace_back(std::move(selectionBkAttr)); + } } return data; diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp index 6949b0f2a43..fe7a1e5d583 100644 --- a/src/buffer/out/textBuffer.hpp +++ b/src/buffer/out/textBuffer.hpp @@ -145,11 +145,11 @@ class TextBuffer final std::vector> BkAttr; }; - const TextAndColor GetTextForClipboard(const bool lineSelection, - const bool trimTrailingWhitespace, - const std::vector& selectionRects, - std::function GetForegroundColor, - std::function GetBackgroundColor) const; + const TextAndColor GetText(const bool lineSelection, + const bool trimTrailingWhitespace, + const std::vector& textRects, + std::function GetForegroundColor = nullptr, + std::function GetBackgroundColor = nullptr) const; static std::string GenHTML(const TextAndColor& rows, const int fontHeightPoints, diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 5b5c6668820..848070fd939 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1345,7 +1345,7 @@ namespace winrt::TerminalApp::implementation bool TerminalPage::_CopyText(const bool trimTrailingWhitespace) { const auto control = _GetActiveControl(); - return control.CopySelectionToClipboard(trimTrailingWhitespace); + return control.CopySelectionToClipboard(!trimTrailingWhitespace); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index fe413f1b3d0..8526e199b2b 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -903,7 +903,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } else { - CopySelectionToClipboard(!shiftEnabled); + CopySelectionToClipboard(shiftEnabled); } } } @@ -1028,7 +1028,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // If the terminal was unfocused AND a click-drag selection happened, copy to clipboard. if (!_unfocusedClickPos || (_unfocusedClickPos && _isClickDragSelection)) { - CopySelectionToClipboard(!shiftEnabled); + CopySelectionToClipboard(shiftEnabled); } } } @@ -1661,9 +1661,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Windows Clipboard (CascadiaWin32:main.cpp). // - CopyOnSelect does NOT clear the selection // Arguments: - // - trimTrailingWhitespace: enable removing any whitespace from copied selection - // and get text to appear on separate lines. - bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace) + // - collapseText: collapse all of the text to one line + bool TermControl::CopySelectionToClipboard(bool collapseText) { // no selection --> nothing to copy if (_terminal == nullptr || !_terminal->IsSelectionActive()) @@ -1671,7 +1670,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation return false; } // extract text from buffer - const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace); + const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(collapseText); // convert text: vector --> string std::wstring textData; diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 11161e9d52f..37d39d00233 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -63,7 +63,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation hstring Title(); hstring GetProfileName() const; - bool CopySelectionToClipboard(bool trimTrailingWhitespace); + bool CopySelectionToClipboard(bool collapseText); void PasteTextFromClipboard(); void Close(); Windows::Foundation::Size CharacterDimensions() const; diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 0a01a329121..a005ac16d5a 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -52,7 +52,7 @@ namespace Microsoft.Terminal.TerminalControl String Title { get; }; - Boolean CopySelectionToClipboard(Boolean trimTrailingWhitespace); + Boolean CopySelectionToClipboard(Boolean collapseText); void PasteTextFromClipboard(); void Close(); Windows.Foundation.Size CharacterDimensions { get; }; diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index 6e0aebaa0ab..6012b639af4 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -16,15 +16,15 @@ using namespace Microsoft::Terminal::Core; * |-position where we double-clicked * _|_ * |word| - * |--| + * |--| * start & pivot-| |-end * * 2. Drag your mouse down a line * - * - * start & pivot-|__________ + * + * start & pivot-|__________ * __|word_______| - * |______| + * |______| * | * |-end & mouse position * @@ -33,7 +33,7 @@ using namespace Microsoft::Terminal::Core; * |-start & mouse position * |________ * ____| ______| - * |___w|ord + * |___w|ord * |-end & pivot * * The pivot never moves until a new selection is created. It ensures that that cell will always be selected. @@ -250,20 +250,21 @@ void Terminal::ClearSelection() // Method Description: // - get wstring text from highlighted portion of text buffer // Arguments: -// - trimTrailingWhitespace: enable removing any whitespace from copied selection -// and get text to appear on separate lines. +// - collapseText: collapse all of the text to one line // Return Value: // - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n -const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const +const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool collapseText) const { + const auto selectionRects = _GetSelectionRects(); + std::function GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1); std::function GetBackgroundColor = std::bind(&Terminal::GetBackgroundColor, this, std::placeholders::_1); - return _buffer->GetTextForClipboard(!_blockSelection, - trimTrailingWhitespace, - _GetSelectionRects(), - GetForegroundColor, - GetBackgroundColor); + return _buffer->GetText(!collapseText, + !collapseText, + selectionRects, + GetForegroundColor, + GetBackgroundColor); } // Method Description: diff --git a/src/host/ut_host/ClipboardTests.cpp b/src/host/ut_host/ClipboardTests.cpp index 2a2436a08a3..e00c685b64a 100644 --- a/src/host/ut_host/ClipboardTests.cpp +++ b/src/host/ut_host/ClipboardTests.cpp @@ -84,10 +84,8 @@ class ClipboardTests selection.emplace_back(SMALL_RECT{ 0, 2, 14, 2 }); selection.emplace_back(SMALL_RECT{ 0, 3, 8, 3 }); - return Clipboard::Instance().RetrieveTextFromBuffer(screenInfo, - fLineSelection, - selection) - .text; + const auto& buffer = screenInfo.GetTextBuffer(); + return buffer.GetText(true, fLineSelection, selection).text; } #pragma prefast(push) diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index 3343b716089..4cde9b055e0 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -150,6 +150,7 @@ class TextBufferTests TEST_METHOD(GetWordBoundaries); TEST_METHOD(GetTextRects); + TEST_METHOD(GetText); }; void TextBufferTests::TestBufferCreate() @@ -2022,11 +2023,24 @@ void TextBufferTests::TestBurrito() void TextBufferTests::WriteLinesToBuffer(const std::vector& text, TextBuffer& buffer) { + const auto bufferSize = buffer.GetSize(); + for (size_t row = 0; row < text.size(); ++row) { auto line = text[row]; - OutputCellIterator iter{ line }; - buffer.WriteLine(iter, { 0, gsl::narrow(row) }); + if (!line.empty()) + { + // TODO GH#780: writing up to (but not past) the end of the line + // should NOT set the wrap flag + std::optional wrap = true; + if (line.size() == static_cast(bufferSize.RightExclusive())) + { + wrap = std::nullopt; + } + + OutputCellIterator iter{ line }; + buffer.Write(iter, { 0, gsl::narrow(row) }, wrap); + } } } @@ -2203,3 +2217,200 @@ void TextBufferTests::GetTextRects() VERIFY_ARE_EQUAL(expected.at(i), result.at(i)); } } + +void TextBufferTests::GetText() +{ + // GetText() is used by... + // - Copying text to the clipboard regularly + // - Copying text to the clipboard, with shift held (collapse to one line) + // - Extracting text from a UiaTextRange + + BEGIN_TEST_METHOD_PROPERTIES() + TEST_METHOD_PROPERTY(L"Data:wrappedText", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:blockSelection", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:includeCRLF", L"{false, true}") + TEST_METHOD_PROPERTY(L"Data:trimTrailingWhitespace", L"{false, true}") + END_TEST_METHOD_PROPERTIES(); + + bool wrappedText; + bool blockSelection; + bool includeCRLF; + bool trimTrailingWhitespace; + VERIFY_SUCCEEDED(TestData::TryGetValue(L"wrappedText", wrappedText), L"Get 'wrappedText' variant"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"blockSelection", blockSelection), L"Get 'blockSelection' variant"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"includeCRLF", includeCRLF), L"Get 'includeCRLF' variant"); + VERIFY_SUCCEEDED(TestData::TryGetValue(L"trimTrailingWhitespace", trimTrailingWhitespace), L"Get 'trimTrailingWhitespace' variant"); + + if (!wrappedText) + { + COORD bufferSize{ 10, 20 }; + UINT cursorSize = 12; + TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Setup: Write lines of text to the buffer + const std::vector bufferText = { L"12345", + L" 345", + L"123 ", + L" 3 " }; + WriteLinesToBuffer(bufferText, *_buffer); + + // simulate a selection from origin to {4,4} + const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 4 }, blockSelection); + + std::wstring result = L""; + const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects).text; + for (auto& text : textData) + { + result += text; + } + + std::wstring expectedText = L""; + if (includeCRLF) + { + if (trimTrailingWhitespace) + { + Log::Comment(L"Standard Copy to Clipboard"); + expectedText += L"12345\r\n"; + expectedText += L" 345\r\n"; + expectedText += L"123\r\n"; + expectedText += L" 3\r\n"; + } + else + { + Log::Comment(L"UI Automation"); + if (blockSelection) + { + expectedText += L"12345\r\n"; + expectedText += L" 345\r\n"; + expectedText += L"123 \r\n"; + expectedText += L" 3 \r\n"; + expectedText += L" "; + } + else + { + expectedText += L"12345 \r\n"; + expectedText += L" 345 \r\n"; + expectedText += L"123 \r\n"; + expectedText += L" 3 \r\n"; + expectedText += L" "; + } + } + } + else + { + if (trimTrailingWhitespace) + { + Log::Comment(L"UNDEFINED"); + expectedText += L"12345"; + expectedText += L" 345"; + expectedText += L"123"; + expectedText += L" 3"; + } + else + { + Log::Comment(L"Shift+Copy to Clipboard"); + if (blockSelection) + { + expectedText += L"12345"; + expectedText += L" 345"; + expectedText += L"123 "; + expectedText += L" 3 "; + expectedText += L" "; + } + else + { + expectedText += L"12345 "; + expectedText += L" 345 "; + expectedText += L"123 "; + expectedText += L" 3 "; + expectedText += L" "; + } + } + } + + // Verify expected output and actual output are the same + VERIFY_ARE_EQUAL(expectedText, result); + } + else + { + // Case 2: Wrapped Text + COORD bufferSize{ 5, 20 }; + UINT cursorSize = 12; + TextAttribute attr{ 0x7f }; + auto _buffer = std::make_unique(bufferSize, attr, cursorSize, _renderTarget); + + // Setup: Write lines of text to the buffer + const std::vector bufferText = { L"1234567", + L"", + L" 345", + L"123 ", + L"" }; + WriteLinesToBuffer(bufferText, *_buffer); + // buffer should look like this: + // ______ + // |12345| <-- wrapped + // |67 | + // | 345| + // |123 | <-- wrapped + // | | + // |_____| + + // simulate a selection from origin to {4,5} + const auto textRects = _buffer->GetTextRects({ 0, 0 }, { 4, 5 }); + + std::wstring result = L""; + const auto textData = _buffer->GetText(includeCRLF, trimTrailingWhitespace, textRects).text; + for (auto& text : textData) + { + result += text; + } + + std::wstring expectedText = L""; + if (includeCRLF) + { + if (trimTrailingWhitespace) + { + Log::Comment(L"Standard Copy to Clipboard"); + expectedText += L"12345"; + expectedText += L"67\r\n"; + expectedText += L" 345\r\n"; + expectedText += L"123 \r\n"; + } + else + { + Log::Comment(L"UI Automation"); + expectedText += L"12345"; + expectedText += L"67 \r\n"; + expectedText += L" 345\r\n"; + expectedText += L"123 "; + expectedText += L" \r\n"; + expectedText += L" "; + } + } + else + { + if (trimTrailingWhitespace) + { + Log::Comment(L"UNDEFINED"); + expectedText += L"12345"; + expectedText += L"67"; + expectedText += L" 345"; + expectedText += L"123 "; + } + else + { + Log::Comment(L"Shift+Copy to Clipboard"); + expectedText += L"12345"; + expectedText += L"67 "; + expectedText += L" 345"; + expectedText += L"123 "; + expectedText += L" "; + expectedText += L" "; + } + } + + // Verify expected output and actual output are the same + VERIFY_ARE_EQUAL(expectedText, result); + } +} diff --git a/src/interactivity/win32/Clipboard.cpp b/src/interactivity/win32/Clipboard.cpp index dd994b1f6d3..4e30b92c7e6 100644 --- a/src/interactivity/win32/Clipboard.cpp +++ b/src/interactivity/win32/Clipboard.cpp @@ -188,10 +188,10 @@ std::deque> Clipboard::TextToKeyEvents(_In_reads_(c // - Copies the selected area onto the global system clipboard. // - NOTE: Throws on allocation and other clipboard failures. // Arguments: -// - fAlsoCopyFormatting - This will also place colored HTML & RTF text onto the clipboard as well as the usual plain text. +// - copyFormatting - This will also place colored HTML & RTF text onto the clipboard as well as the usual plain text. // Return Value: // -void Clipboard::StoreSelectionToClipboard(bool const fAlsoCopyFormatting) +void Clipboard::StoreSelectionToClipboard(bool const copyFormatting) { const auto& selection = Selection::Instance(); @@ -203,40 +203,31 @@ void Clipboard::StoreSelectionToClipboard(bool const fAlsoCopyFormatting) // read selection area. const auto selectionRects = selection.GetSelectionRects(); - const bool lineSelection = Selection::Instance().IsLineSelection(); const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& screenInfo = gci.GetActiveOutputBuffer(); - - const auto text = RetrieveTextFromBuffer(screenInfo, - lineSelection, - selectionRects); - - CopyTextToSystemClipboard(text, fAlsoCopyFormatting); -} - -// Routine Description: -// - Retrieves the text data from the selected region of the text buffer -// Arguments: -// - screenInfo - what is rendered on the screen -// - lineSelection - true if entire line is being selected. False otherwise (box selection) -// - selectionRects - the selection regions from which the data will be extracted from the buffer -TextBuffer::TextAndColor Clipboard::RetrieveTextFromBuffer(const SCREEN_INFORMATION& screenInfo, - const bool lineSelection, - const std::vector& selectionRects) -{ - const auto& buffer = screenInfo.GetTextBuffer(); - const bool trimTrailingWhitespace = !WI_IsFlagSet(GetKeyState(VK_SHIFT), KEY_PRESSED); - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + const auto& buffer = gci.GetActiveOutputBuffer().GetTextBuffer(); std::function GetForegroundColor = std::bind(&CONSOLE_INFORMATION::LookupForegroundColor, &gci, std::placeholders::_1); std::function GetBackgroundColor = std::bind(&CONSOLE_INFORMATION::LookupBackgroundColor, &gci, std::placeholders::_1); - return buffer.GetTextForClipboard(lineSelection, - trimTrailingWhitespace, - selectionRects, - GetForegroundColor, - GetBackgroundColor); + bool includeCRLF, trimTrailingWhitespace; + if (WI_IsFlagSet(GetKeyState(VK_SHIFT), KEY_PRESSED)) + { + // When shift is held, put everything in one line + includeCRLF = trimTrailingWhitespace = false; + } + else + { + includeCRLF = trimTrailingWhitespace = true; + } + + const auto text = buffer.GetText(includeCRLF, + trimTrailingWhitespace, + selectionRects, + GetForegroundColor, + GetBackgroundColor); + + CopyTextToSystemClipboard(text, copyFormatting); } // Routine Description: diff --git a/src/interactivity/win32/clipboard.hpp b/src/interactivity/win32/clipboard.hpp index 17e65b1aab8..cc196c0420e 100644 --- a/src/interactivity/win32/clipboard.hpp +++ b/src/interactivity/win32/clipboard.hpp @@ -40,11 +40,7 @@ namespace Microsoft::Console::Interactivity::Win32 void StoreSelectionToClipboard(_In_ bool const fAlsoCopyFormatting); - TextBuffer::TextAndColor RetrieveTextFromBuffer(const SCREEN_INFORMATION& screenInfo, - const bool lineSelection, - const std::vector& selectionRects); - - void CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, _In_ bool const fAlsoCopyFormatting); + void CopyTextToSystemClipboard(const TextBuffer::TextAndColor& rows, _In_ bool const copyFormatting); void CopyToSystemClipboard(std::string stringToPlaceOnClip, LPCWSTR lpszFormat); bool FilterCharacterOnPaste(_Inout_ WCHAR* const pwch); diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index 0d43c26d44a..39d6d6f24dd 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -498,9 +498,14 @@ try return E_INVALIDARG; } - const auto text = _getTextValue(maxLength); + const auto maxLengthOpt = (maxLength == -1) ? + std::nullopt : + std::optional{ maxLength }; + const auto text = _getTextValue(maxLengthOpt); *pRetVal = SysAllocString(text.c_str()); + RETURN_HR_IF_NULL(E_OUTOFMEMORY, *pRetVal); + UiaTracing::TextRange::GetText(*this, maxLength, text); return S_OK; } @@ -509,12 +514,12 @@ CATCH_RETURN(); // Method Description: // - Helper method for GetText(). Retrieves the text that the UiaTextRange encompasses as a wstring // Arguments: -// - maxLength - the maximum size of the retrieved text. -1 means we don't care about the size. +// - maxLength - the maximum size of the retrieved text. nullopt means we don't care about the size. // Return Value: // - the text that the UiaTextRange encompasses #pragma warning(push) #pragma warning(disable : 26447) // compiler isn't filtering throws inside the try/catch -std::wstring UiaTextRangeBase::_getTextValue(int maxLength) const noexcept +std::wstring UiaTextRangeBase::_getTextValue(std::optional maxLength) const noexcept try { _pData->LockConsole(); @@ -522,69 +527,35 @@ try _pData->UnlockConsole(); }); - if (IsDegenerate()) - { - return {}; - } - - std::wstring result{}; - - // the caller must pass in a value for the max length of the text - // to retrieve. a value of -1 means they don't want the text - // truncated. - const bool getPartialText = maxLength != -1; - - // if _end is at 0, we ignore that row because _end is exclusive - const auto& buffer = _pData->GetTextBuffer(); - const short totalRowsInRange = (_end.X == buffer.GetSize().Left()) ? - base::ClampSub(_end.Y, _start.Y) : - base::ClampAdd(base::ClampSub(_end.Y, _start.Y), base::ClampedNumeric(1)); - const short lastRowInRange = _start.Y + totalRowsInRange - 1; - - short currentScreenInfoRow = 0; - for (short i = 0; i < totalRowsInRange; ++i) + std::wstring textData{}; + if (!IsDegenerate()) { - currentScreenInfoRow = _start.Y + i; - const ROW& row = buffer.GetRowByOffset(currentScreenInfoRow); - if (row.GetCharRow().ContainsText()) - { - const size_t rowRight = row.GetCharRow().MeasureRight(); - size_t startIndex = 0; - size_t endIndex = rowRight; - if (currentScreenInfoRow == _start.Y) - { - startIndex = _start.X; - } + const auto& buffer = _pData->GetTextBuffer(); + const auto bufferSize = buffer.GetSize(); - if (currentScreenInfoRow == _end.Y) - { - // prevent the end from going past the last non-whitespace char in the row - endIndex = std::max(startIndex + 1, std::min(gsl::narrow_cast(_end.X), rowRight)); - } + // convert _end to be inclusive + auto inclusiveEnd{ _end }; + bufferSize.DecrementInBounds(inclusiveEnd, true); - // if startIndex >= endIndex then _start is - // further to the right than the last - // non-whitespace char in the row so there - // wouldn't be any text to grab. - if (startIndex < endIndex) - { - result += row.GetText().substr(startIndex, endIndex - startIndex); - } - } + const auto textRects = buffer.GetTextRects(_start, inclusiveEnd); + const auto bufferData = buffer.GetText(true, + false, + textRects); - if (currentScreenInfoRow != lastRowInRange) + const size_t textDataSize = base::ClampMul(bufferData.text.size(), bufferSize.Width()); + textData.reserve(textDataSize); + for (const auto& text : bufferData.text) { - result += L"\r\n"; + textData += text; } + } - if (getPartialText && result.size() > static_cast(maxLength)) - { - result.resize(maxLength); - break; - } + if (maxLength.has_value()) + { + textData.resize(*maxLength); } - return result; + return textData; } catch (...) { diff --git a/src/types/UiaTextRangeBase.hpp b/src/types/UiaTextRangeBase.hpp index 94c14906018..8389d6c8b28 100644 --- a/src/types/UiaTextRangeBase.hpp +++ b/src/types/UiaTextRangeBase.hpp @@ -153,7 +153,7 @@ namespace Microsoft::Console::Types // This is used by tracing to extract the text value // that the UiaTextRange currently encompasses. // GetText() cannot be used as it's not const - std::wstring _getTextValue(int maxLength = -1) const noexcept; + std::wstring _getTextValue(std::optional maxLength = std::nullopt) const noexcept; RECT _getTerminalRect() const;