Skip to content

Commit

Permalink
Restore support for pasting files (#16634)
Browse files Browse the repository at this point in the history
TIL: You could Ctrl+V files into Windows Terminal and here I am,
always opening the context menu and selecting "Copy as path"... smh

This restores the support by adding a very rudimentary HDROP handler.
The flip side of the regression is that I learned about this and so
conhost also gets this now, because why not!

Closes #16627

## Validation Steps Performed
* Single files can be pasted in WT and conhost ✅
  • Loading branch information
lhecker authored Feb 1, 2024
1 parent c669afe commit ef96e22
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 77 deletions.
114 changes: 72 additions & 42 deletions src/cascadia/TerminalApp/TerminalPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2628,6 +2628,75 @@ namespace winrt::TerminalApp::implementation
CATCH_LOG();
}

static wil::unique_close_clipboard_call _openClipboard(HWND hwnd)
{
bool success = false;

// OpenClipboard may fail to acquire the internal lock --> retry.
for (DWORD sleep = 10;; sleep *= 2)
{
if (OpenClipboard(hwnd))
{
success = true;
break;
}
// 10 iterations
if (sleep > 10000)
{
break;
}
Sleep(sleep);
}

return wil::unique_close_clipboard_call{ success };
}

static winrt::hstring _extractClipboard()
{
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
{
const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
{
return {};
}

const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
const auto len = wcsnlen(str, maxLen);
return winrt::hstring{ str, gsl::narrow_cast<uint32_t>(len) };
}

// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
if (const auto handle = GetClipboardData(CF_HDROP))
{
const wil::unique_hglobal_locked lock{ handle };
const auto drop = static_cast<HDROP>(lock.get());
if (!drop)
{
return {};
}

const auto cap = DragQueryFileW(drop, 0, nullptr, 0);
if (cap == 0)
{
return {};
}

auto buffer = winrt::impl::hstring_builder{ cap };
const auto len = DragQueryFileW(drop, 0, buffer.data(), cap + 1);
if (len == 0)
{
return {};
}

return buffer.to_hstring();
}

return {};
}

// Function Description:
// - This function is called when the `TermControl` requests that we send
// it the clipboard's content.
Expand All @@ -2647,53 +2716,14 @@ namespace winrt::TerminalApp::implementation
const auto weakThis = get_weak();
const auto dispatcher = Dispatcher();
const auto globalSettings = _settings.GlobalSettings();
winrt::hstring text;

// GetClipboardData might block for up to 30s for delay-rendered contents.
co_await winrt::resume_background();

winrt::hstring text;
if (const auto clipboard = _openClipboard(nullptr))
{
// According to various reports on the internet, OpenClipboard might
// fail to acquire the internal lock, for instance due to rdpclip.exe.
for (int attempts = 1;;)
{
if (OpenClipboard(nullptr))
{
break;
}

if (attempts > 5)
{
co_return;
}

attempts++;
Sleep(10 * attempts);
}

const auto clipboardCleanup = wil::scope_exit([]() {
CloseClipboard();
});

const auto data = GetClipboardData(CF_UNICODETEXT);
if (!data)
{
co_return;
}

const auto str = static_cast<const wchar_t*>(GlobalLock(data));
if (!str)
{
co_return;
}

const auto dataCleanup = wil::scope_exit([&]() {
GlobalUnlock(data);
});

const auto maxLength = GlobalSize(data) / sizeof(wchar_t);
const auto length = wcsnlen(str, maxLength);
text = winrt::hstring{ str, gsl::narrow_cast<uint32_t>(length) };
text = _extractClipboard();
}

if (globalSettings.TrimPaste())
Expand Down
78 changes: 63 additions & 15 deletions src/interactivity/win32/Clipboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,73 @@ void Clipboard::Paste()
return;
}

const auto handle = GetClipboardData(CF_UNICODETEXT);
if (!handle)
// This handles most cases of pasting text as the OS converts most formats to CF_UNICODETEXT automatically.
if (const auto handle = GetClipboardData(CF_UNICODETEXT))
{
return;
const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
{
return;
}

// As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
// CF_UNICODETEXT: [...] A null character signals the end of the data.
// --> Use wcsnlen() to determine the actual length.
// NOTE: Some applications don't add a trailing null character. This includes past conhost versions.
const auto maxLen = GlobalSize(handle) / sizeof(wchar_t);
StringPaste(str, wcsnlen(str, maxLen));
}

// Clear any selection or scrolling that may be active.
Selection::Instance().ClearSelection();
Scrolling::s_ClearScroll();
// We get CF_HDROP when a user copied a file with Ctrl+C in Explorer and pastes that into the terminal (among others).
if (const auto handle = GetClipboardData(CF_HDROP))
{
const wil::unique_hglobal_locked lock{ handle };
const auto drop = static_cast<HDROP>(lock.get());
if (!drop)
{
return;
}

const wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
PasteDrop(drop);
}
}

void Clipboard::PasteDrop(HDROP drop)
{
// NOTE: When asking DragQueryFileW for the required capacity it returns a length without trailing \0,
// but then expects a capacity that includes it. If you don't make space for a trailing \0
// then it will silently (!) cut off the end of the string. A somewhat disappointing API design.
const auto expectedLength = DragQueryFileW(drop, 0, nullptr, 0);
if (expectedLength == 0)
{
return;
}

// As per: https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats
// CF_UNICODETEXT: [...] A null character signals the end of the data.
// --> Use wcsnlen() to determine the actual length.
// NOTE: Some applications don't add a trailing null character. This includes past conhost versions.
const auto maxLen = GlobalSize(handle) / sizeof(WCHAR);
StringPaste(str, wcsnlen(str, maxLen));
// If the path contains spaces, we'll wrap it in quotes and so this allocates +2 characters ahead of time.
// We'll first make DragQueryFileW copy its contents in the middle and then check if that contains spaces.
// If it does, only then we'll add the quotes at the start and end.
// This is preferable over calling StringPaste 3x (an alternative, simpler approach),
// because the pasted content should be treated as a single atomic unit by the InputBuffer.
const auto buffer = std::make_unique_for_overwrite<wchar_t[]>(expectedLength + 2);
auto str = buffer.get() + 1;
size_t len = expectedLength;

const auto actualLength = DragQueryFileW(drop, 0, str, expectedLength + 1);
if (actualLength != expectedLength)
{
return;
}

if (wmemchr(str, L' ', len))
{
str = buffer.get();
len += 2;
til::at(str, 0) = L'"';
til::at(str, len - 1) = L'"';
}

StringPaste(str, len);
}

Clipboard& Clipboard::Instance()
Expand Down Expand Up @@ -109,6 +153,10 @@ void Clipboard::StringPaste(_In_reads_(cchData) const wchar_t* const pData,

try
{
// Clear any selection or scrolling that may be active.
Selection::Instance().ClearSelection();
Scrolling::s_ClearScroll();

const auto vtInputMode = gci.pInputBuffer->IsInVirtualTerminalInputMode();
const auto bracketedPasteMode = gci.GetBracketedPasteMode();
auto inEvents = TextToKeyEvents(pData, cchData, vtInputMode && bracketedPasteMode);
Expand Down
4 changes: 2 additions & 2 deletions src/interactivity/win32/clipboard.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ namespace Microsoft::Console::Interactivity::Win32
static Clipboard& Instance();

void Copy(_In_ const bool fAlsoCopyFormatting = false);
void StringPaste(_In_reads_(cchData) PCWCHAR pwchData,
const size_t cchData);
void Paste();
void PasteDrop(HDROP drop);

private:
static wil::unique_close_clipboard_call _openClipboard(HWND hwnd);
static void _copyToClipboard(UINT format, const void* src, size_t bytes);
static void _copyToClipboardRegisteredFormat(const wchar_t* format, const void* src, size_t bytes);

void StringPaste(_In_reads_(cchData) PCWCHAR pwchData, const size_t cchData);
InputEventQueue TextToKeyEvents(_In_reads_(cchData) const wchar_t* const pData,
const size_t cchData,
const bool bracketedPaste = false);
Expand Down
19 changes: 1 addition & 18 deletions src/interactivity/win32/windowproc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -857,24 +857,7 @@ void Window::_HandleWindowPosChanged(const LPARAM lParam)
// - <none>
void Window::_HandleDrop(const WPARAM wParam) const
{
WCHAR szPath[MAX_PATH];
BOOL fAddQuotes;

if (DragQueryFile((HDROP)wParam, 0, szPath, ARRAYSIZE(szPath)) != 0)
{
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);
}
}
Clipboard::Instance().PasteDrop((HDROP)wParam);
}

[[nodiscard]] LRESULT Window::_HandleGetObject(const HWND hwnd, const WPARAM wParam, const LPARAM lParam)
Expand Down

0 comments on commit ef96e22

Please sign in to comment.