Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Copy RTF data to the clipboard #3535

Merged
merged 4 commits into from
Nov 13, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 182 additions & 1 deletion src/buffer/out/textBuffer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows, const int fontHeightPo
}

const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col > startOffset)
if (col >= startOffset)
anirudhrb marked this conversation as resolved.
Show resolved Hide resolved
{
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
for (const auto c : unescapedText)
Expand Down Expand Up @@ -1240,3 +1240,184 @@ std::string TextBuffer::GenHTML(const TextAndColor& rows, const int fontHeightPo
return {};
}
}

// Routine Description:
// - Generates an RTF document based on the passed in text and color data
// RTF 1.5 Spec: https://www.biblioscape.com/rtf15_spec.htm
// Arguments:
// - rows - the text and color data we will format & encapsulate
// - backgroundColor - default background color for characters, also used in padding
// - fontHeightPoints - the unscaled font height
// - fontFaceName - the name of the font used
// - htmlTitle - value used in title tag of html header. Used to name the application
// Return Value:
// - string containing the generated RTF
std::string TextBuffer::GenRTF(const TextAndColor& rows, const int fontHeightPoints, const std::wstring_view fontFaceName, const COLORREF backgroundColor)
{
try
{
std::ostringstream rtfBuilder;

// start rtf
rtfBuilder << "{";

// Standard RTF header.
// This is similar to the header gnerated by WordPad.
// \ansi - specifies that the ANSI char set is used in the current doc
// \ansicpg1252 - represents the ANSI code page which is used to perform the Unicode to ANSI conversion when writing RTF text
// \deff0 - specifes that the default font for the document is the one at index 0 in the font table
// \nouicompat - ?
rtfBuilder << "\\rtf1\\ansi\\ansicpg1252\\deff0\\nouicompat";
anirudhrb marked this conversation as resolved.
Show resolved Hide resolved

// font table
rtfBuilder << "{\\fonttbl{\\f0\\fmodern\\fcharset0 " << ConvertToA(CP_UTF8, fontFaceName) << ";}}";

// map to keep track of colors:
// keys are colors represented by COLORREF
// values are indices of the corresponding colors in the color table
std::unordered_map<COLORREF, int> colorMap;
int nextColorIndex = 1; // leave 0 for the default color and start from 1.

// RTF color table
std::ostringstream colorTableBuilder;
colorTableBuilder << "{\\colortbl ;";
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(backgroundColor))
<< "\\green" << static_cast<int>(GetGValue(backgroundColor))
<< "\\blue" << static_cast<int>(GetBValue(backgroundColor))
<< ";";
colorMap[backgroundColor] = nextColorIndex++;

// content
std::ostringstream contentBuilder;
contentBuilder << "\\viewkind4\\uc4";

// paragraph styles
// \fs specificies font size in half-points i.e. \fs20 results in a font size
// of 10 pts. That's why, font size is multiplied by 2 here.
contentBuilder << "\\pard\\slmult1\\f0\\fs" << std::to_string(2 * fontHeightPoints)
<< "\\highlight1"
<< " ";

std::optional<COLORREF> fgColor = std::nullopt;
std::optional<COLORREF> bkColor = std::nullopt;
for (size_t row = 0; row < rows.text.size(); ++row)
{
size_t startOffset = 0;

if (row != 0)
{
contentBuilder << "\\line "; // new line
}

for (size_t col = 0; col < rows.text.at(row).length(); ++col)
{
const bool isLastCharInRow =
col == rows.text.at(row).length() - 1 ||
rows.text.at(row).at(col + 1) == '\r' ||
rows.text.at(row).at(col + 1) == '\n';

bool colorChanged = false;
if (!fgColor.has_value() || rows.FgAttr.at(row).at(col) != fgColor.value())
{
fgColor = rows.FgAttr.at(row).at(col);
colorChanged = true;
}

if (!bkColor.has_value() || rows.BkAttr.at(row).at(col) != bkColor.value())
{
bkColor = rows.BkAttr.at(row).at(col);
colorChanged = true;
}

const auto writeAccumulatedChars = [&](bool includeCurrent) {
if (col >= startOffset)
{
const auto unescapedText = ConvertToA(CP_UTF8, std::wstring_view(rows.text.at(row)).substr(startOffset, col - startOffset + includeCurrent));
for (const auto c : unescapedText)
{
switch (c)
{
case '\\':
case '{':
case '}':
contentBuilder << "\\" << c;
break;
default:
contentBuilder << c;
}
}

startOffset = col;
}
};

if (colorChanged)
{
writeAccumulatedChars(false);

int bkColorIndex, fgColorIndex;
if (colorMap.find(bkColor.value()) != colorMap.end())
{
// color already exists in the map, just retrieve the index
bkColorIndex = colorMap[bkColor.value()];
}
else
{
// color not present in the map, so add it
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(bkColor.value()))
<< "\\green" << static_cast<int>(GetGValue(bkColor.value()))
<< "\\blue" << static_cast<int>(GetBValue(bkColor.value()))
<< ";";
colorMap[bkColor.value()] = nextColorIndex;
bkColorIndex = nextColorIndex++;
}

if (colorMap.find(fgColor.value()) != colorMap.end())
{
// color already exists in the map, just retrieve the index
fgColorIndex = colorMap[fgColor.value()];
}
else
{
// color not present in the map, so add it
colorTableBuilder << "\\red" << static_cast<int>(GetRValue(fgColor.value()))
<< "\\green" << static_cast<int>(GetGValue(fgColor.value()))
<< "\\blue" << static_cast<int>(GetBValue(fgColor.value()))
<< ";";
colorMap[fgColor.value()] = nextColorIndex;
fgColorIndex = nextColorIndex++;
}

contentBuilder << "\\highglight" << bkColorIndex
<< "\\cf" << fgColorIndex
<< " ";
}

if (isLastCharInRow)
{
writeAccumulatedChars(true);
break;
}
}
}

// end colortbl
colorTableBuilder << "}";

// add color table to the final RTF
rtfBuilder << colorTableBuilder.str();

// add the text content to the final RTF
rtfBuilder << contentBuilder.str();

// end rtf
rtfBuilder << "}";

return rtfBuilder.str();
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
return {};
}
}
5 changes: 5 additions & 0 deletions src/buffer/out/textBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ class TextBuffer final
const COLORREF backgroundColor,
const std::string& htmlTitle);

static std::string GenRTF(const TextAndColor& rows,
const int fontHeightPoints,
const std::wstring_view fontFaceName,
const COLORREF backgroundColor);
anirudhrb marked this conversation as resolved.
Show resolved Hide resolved

private:
std::deque<ROW> _storage;
Cursor _cursor;
Expand Down
7 changes: 7 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,13 @@ namespace winrt::TerminalApp::implementation
dataPack.SetHtmlFormat(htmlData);
}

// copy rtf data to dataPack
const auto rtfData = copiedData.Rtf();
if (!rtfData.empty())
{
dataPack.SetRtf(rtfData);
}

try
{
Clipboard::SetContent(dataPack);
Expand Down
10 changes: 9 additions & 1 deletion src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1468,13 +1468,21 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_settings.DefaultBackground(),
"Windows Terminal");

// convert to RTF format
const auto rtfData = TextBuffer::GenRTF(bufferData,
_actualFont.GetUnscaledSize().Y,
_actualFont.GetFaceName(),
_settings.DefaultBackground());

if (!_terminal->IsCopyOnSelectActive())
{
_terminal->ClearSelection();
}

// send data up for clipboard
auto copyArgs = winrt::make_self<CopyToClipboardEventArgs>(winrt::hstring(textData.data(), gsl::narrow<winrt::hstring::size_type>(textData.size())), winrt::to_hstring(htmlData));
auto copyArgs = winrt::make_self<CopyToClipboardEventArgs>(winrt::hstring(textData.data(), gsl::narrow<winrt::hstring::size_type>(textData.size())),
winrt::to_hstring(htmlData),
winrt::to_hstring(rtfData));
_clipboardCopyHandlers(*this, *copyArgs);
return true;
}
Expand Down
7 changes: 5 additions & 2 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,19 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
public CopyToClipboardEventArgsT<CopyToClipboardEventArgs>
{
public:
CopyToClipboardEventArgs(hstring text, hstring html) :
CopyToClipboardEventArgs(hstring text, hstring html, hstring rtf) :
_text(text),
_html(html) {}
_html(html),
_rtf(rtf) {}

hstring Text() { return _text; };
hstring Html() { return _html; };
hstring Rtf() { return _rtf; };

private:
hstring _text;
hstring _html;
hstring _rtf;
};

struct PasteFromClipboardEventArgs :
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalControl/TermControl.idl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace Microsoft.Terminal.TerminalControl
{
String Text { get; };
String Html { get; };
String Rtf { get; };
}

runtimeclass PasteFromClipboardEventArgs
Expand Down