Restore support for pasting files (#16634)

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 
This commit is contained in:
Leonard Hecker 2024-02-02 00:49:57 +01:00 committed by GitHub
parent c669afe2a0
commit ef96e225da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 137 additions and 76 deletions

View File

@ -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.
@ -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())

View File

@ -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))
{
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));
}
// 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;
}
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;
}
// Clear any selection or scrolling that may be active.
Selection::Instance().ClearSelection();
Scrolling::s_ClearScroll();
// 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 wil::unique_hglobal_locked lock{ handle };
const auto str = static_cast<const wchar_t*>(lock.get());
if (!str)
const auto actualLength = DragQueryFileW(drop, 0, str, expectedLength + 1);
if (actualLength != expectedLength)
{
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 (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()
@ -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);

View File

@ -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);

View File

@ -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)