Skip to content

Commit 5f3289c

Browse files
Fix AdvancedPaste auto-copy failing on Electron/Chromium apps
The Ctrl+C fallback in send_copy_selection() was injecting keystrokes without first releasing modifier keys held from the hotkey combination. Target apps received e.g. Win+Shift+Ctrl+C instead of Ctrl+C, which doesn't trigger a copy. Changes: - Release all modifier keys before Ctrl+C and restore them after, matching the existing try_to_paste_as_plain_text() pattern - Extract send_ctrl_c_input() and poll_clipboard_sequence() helpers from inline code for clarity - Increase clipboard polling window from 150ms to 500ms for slower Chromium/Electron clipboard updates - Add warn-level logging on failure paths for customer diagnostics - Check WM_COPY actually changed the clipboard before declaring success Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7051b89 commit 5f3289c

File tree

1 file changed

+124
-70
lines changed
  • src/modules/AdvancedPaste/AdvancedPasteModuleInterface

1 file changed

+124
-70
lines changed

src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp

Lines changed: 124 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -496,108 +496,156 @@ class AdvancedPaste : public PowertoyModuleIface
496496

497497
if (!GetGUIThreadInfo(0, &gui_info))
498498
{
499+
Logger::warn(L"Auto-copy: GetGUIThreadInfo failed (error={})", GetLastError());
499500
return false;
500501
}
501502

502503
HWND target = gui_info.hwndFocus ? gui_info.hwndFocus : gui_info.hwndActive;
503504
if (!target)
504505
{
506+
Logger::warn(L"Auto-copy: no focused or active window found");
505507
return false;
506508
}
507509

508510
DWORD_PTR result = 0;
509-
return SendMessageTimeout(target,
510-
WM_COPY,
511-
0,
512-
0,
513-
SMTO_ABORTIFHUNG | SMTO_BLOCK,
514-
50,
515-
&result) != 0;
511+
auto sendResult = SendMessageTimeout(target, WM_COPY, 0, 0, SMTO_ABORTIFHUNG | SMTO_BLOCK, 50, &result);
512+
return sendResult != 0;
513+
}
514+
515+
// Helper: poll clipboard sequence number for a change from initial_sequence.
516+
// Returns true if the sequence number changed within the given number of polls.
517+
bool poll_clipboard_sequence(DWORD initial_sequence, int poll_attempts, std::chrono::milliseconds poll_delay)
518+
{
519+
for (int poll = 0; poll < poll_attempts; ++poll)
520+
{
521+
if (GetClipboardSequenceNumber() != initial_sequence)
522+
{
523+
return true;
524+
}
525+
std::this_thread::sleep_for(poll_delay);
526+
}
527+
return false;
528+
}
529+
530+
// Helper: send Ctrl+C via SendInput, releasing any held modifier keys first
531+
// (the hotkey combination may still have modifiers physically pressed).
532+
bool send_ctrl_c_input()
533+
{
534+
std::vector<INPUT> inputs;
535+
536+
// Release all modifier keys that are currently held down from the hotkey.
537+
// Without this, the target app sees e.g. Win+Shift+Ctrl+C instead of just Ctrl+C.
538+
try_inject_modifier_key_up(inputs, VK_LCONTROL);
539+
try_inject_modifier_key_up(inputs, VK_RCONTROL);
540+
try_inject_modifier_key_up(inputs, VK_LWIN);
541+
try_inject_modifier_key_up(inputs, VK_RWIN);
542+
try_inject_modifier_key_up(inputs, VK_LSHIFT);
543+
try_inject_modifier_key_up(inputs, VK_RSHIFT);
544+
try_inject_modifier_key_up(inputs, VK_LMENU);
545+
try_inject_modifier_key_up(inputs, VK_RMENU);
546+
547+
// Ctrl down
548+
{
549+
INPUT input_event = {};
550+
input_event.type = INPUT_KEYBOARD;
551+
input_event.ki.wVk = VK_CONTROL;
552+
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
553+
inputs.push_back(input_event);
554+
}
555+
556+
// C down
557+
{
558+
INPUT input_event = {};
559+
input_event.type = INPUT_KEYBOARD;
560+
input_event.ki.wVk = 0x43; // C
561+
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
562+
inputs.push_back(input_event);
563+
}
564+
565+
// C up
566+
{
567+
INPUT input_event = {};
568+
input_event.type = INPUT_KEYBOARD;
569+
input_event.ki.wVk = 0x43; // C
570+
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
571+
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
572+
inputs.push_back(input_event);
573+
}
574+
575+
// Ctrl up
576+
{
577+
INPUT input_event = {};
578+
input_event.type = INPUT_KEYBOARD;
579+
input_event.ki.wVk = VK_CONTROL;
580+
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
581+
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
582+
inputs.push_back(input_event);
583+
}
584+
585+
// Restore modifiers that were held down
586+
try_inject_modifier_key_restore(inputs, VK_LCONTROL);
587+
try_inject_modifier_key_restore(inputs, VK_RCONTROL);
588+
try_inject_modifier_key_restore(inputs, VK_LWIN);
589+
try_inject_modifier_key_restore(inputs, VK_RWIN);
590+
try_inject_modifier_key_restore(inputs, VK_LSHIFT);
591+
try_inject_modifier_key_restore(inputs, VK_RSHIFT);
592+
try_inject_modifier_key_restore(inputs, VK_LMENU);
593+
try_inject_modifier_key_restore(inputs, VK_RMENU);
594+
595+
// Prevent Start Menu from activating after Win key release/restore
596+
INPUT dummyEvent = {};
597+
dummyEvent.type = INPUT_KEYBOARD;
598+
dummyEvent.ki.wVk = 0xFF;
599+
dummyEvent.ki.dwFlags = KEYEVENTF_KEYUP;
600+
inputs.push_back(dummyEvent);
601+
602+
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
603+
if (uSent != inputs.size())
604+
{
605+
DWORD errorCode = GetLastError();
606+
auto errorMessage = get_last_error_message(errorCode);
607+
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
608+
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
609+
return false;
610+
}
611+
return true;
516612
}
517613

518614
bool send_copy_selection()
519615
{
520616
constexpr int copy_attempts = 2;
521-
constexpr auto copy_retry_delay = std::chrono::milliseconds(100);
522-
constexpr int clipboard_poll_attempts = 5;
523-
constexpr auto clipboard_poll_delay = std::chrono::milliseconds(30);
617+
constexpr auto copy_retry_delay = std::chrono::milliseconds(150);
618+
constexpr int clipboard_poll_attempts = 10;
619+
constexpr auto clipboard_poll_delay = std::chrono::milliseconds(50);
524620

525621
bool copy_succeeded = false;
526622
for (int attempt = 0; attempt < copy_attempts; ++attempt)
527623
{
528624
const auto initial_sequence = GetClipboardSequenceNumber();
529-
copy_succeeded = try_send_copy_message();
530625

531-
if (!copy_succeeded)
532-
{
533-
std::vector<INPUT> inputs;
626+
// Strategy 1: Try WM_COPY message (works for standard Win32 controls)
627+
bool wm_copy_sent = try_send_copy_message();
534628

535-
// send Ctrl+C (key downs and key ups)
536-
{
537-
INPUT input_event = {};
538-
input_event.type = INPUT_KEYBOARD;
539-
input_event.ki.wVk = VK_CONTROL;
540-
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
541-
inputs.push_back(input_event);
542-
}
543-
544-
{
545-
INPUT input_event = {};
546-
input_event.type = INPUT_KEYBOARD;
547-
input_event.ki.wVk = 0x43; // C
548-
// Avoid triggering detection by the centralized keyboard hook.
549-
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
550-
inputs.push_back(input_event);
551-
}
552-
553-
{
554-
INPUT input_event = {};
555-
input_event.type = INPUT_KEYBOARD;
556-
input_event.ki.wVk = 0x43; // C
557-
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
558-
// Avoid triggering detection by the centralized keyboard hook.
559-
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
560-
inputs.push_back(input_event);
561-
}
562-
563-
{
564-
INPUT input_event = {};
565-
input_event.type = INPUT_KEYBOARD;
566-
input_event.ki.wVk = VK_CONTROL;
567-
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
568-
input_event.ki.dwExtraInfo = CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG;
569-
inputs.push_back(input_event);
570-
}
571-
572-
auto uSent = SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
573-
if (uSent != inputs.size())
574-
{
575-
DWORD errorCode = GetLastError();
576-
auto errorMessage = get_last_error_message(errorCode);
577-
Logger::error(L"SendInput failed for Ctrl+C. Expected to send {} inputs and sent only {}. {}", inputs.size(), uSent, errorMessage.has_value() ? errorMessage.value() : L"");
578-
Trace::AdvancedPaste_Error(errorCode, errorMessage.has_value() ? errorMessage.value() : L"", L"input.SendInput");
579-
}
580-
else
629+
if (wm_copy_sent)
630+
{
631+
if (poll_clipboard_sequence(initial_sequence, clipboard_poll_attempts, clipboard_poll_delay))
581632
{
582633
copy_succeeded = true;
583634
}
584635
}
585636

586-
if (copy_succeeded)
637+
// Strategy 2: If WM_COPY didn't work, try SendInput Ctrl+C (works for Electron, browsers, etc.)
638+
if (!copy_succeeded)
587639
{
588-
bool sequence_changed = false;
589-
for (int poll_attempt = 0; poll_attempt < clipboard_poll_attempts; ++poll_attempt)
640+
const auto sequence_before_ctrl_c = GetClipboardSequenceNumber();
641+
642+
if (send_ctrl_c_input())
590643
{
591-
if (GetClipboardSequenceNumber() != initial_sequence)
644+
if (poll_clipboard_sequence(sequence_before_ctrl_c, clipboard_poll_attempts, clipboard_poll_delay))
592645
{
593-
sequence_changed = true;
594-
break;
646+
copy_succeeded = true;
595647
}
596-
597-
std::this_thread::sleep_for(clipboard_poll_delay);
598648
}
599-
600-
copy_succeeded = sequence_changed;
601649
}
602650

603651
if (copy_succeeded)
@@ -611,6 +659,11 @@ class AdvancedPaste : public PowertoyModuleIface
611659
}
612660
}
613661

662+
if (!copy_succeeded)
663+
{
664+
Logger::warn(L"Auto-copy: all {} copy attempts failed — the target application did not update the clipboard after WM_COPY and Ctrl+C", copy_attempts);
665+
}
666+
614667
return copy_succeeded;
615668
}
616669

@@ -977,6 +1030,7 @@ class AdvancedPaste : public PowertoyModuleIface
9771030
{
9781031
if (!send_copy_selection())
9791032
{
1033+
Logger::warn(L"Auto-copy: failed to copy selection for custom action index {} — aborting action", custom_action_index);
9801034
return false;
9811035
}
9821036
}

0 commit comments

Comments
 (0)