Skip to content

Add Ctrl+C and Ctrl+A keyboard shortcuts to ScriptConsole output window#3258

Merged
jmcouffin merged 1 commit intopyrevitlabs:developfrom
tay0thman:tay0thman-feat-ctrlc-copy-1729
Apr 3, 2026
Merged

Add Ctrl+C and Ctrl+A keyboard shortcuts to ScriptConsole output window#3258
jmcouffin merged 1 commit intopyrevitlabs:developfrom
tay0thman:tay0thman-feat-ctrlc-copy-1729

Conversation

@tay0thman
Copy link
Copy Markdown
Contributor

The hook is installed on window creation and disposed on close.

Add Ctrl+C and Ctrl+A keyboard shortcuts to ScriptConsole output window

Description

The pyRevit output window hosts a WebBrowser control inside a WPF MetroWindow via WindowsFormsHost. Revit's message loop intercepts Ctrl+C via IOleInPlaceActiveObject.TranslateAccelerator before WPF, WinForms, IMessageFilter, or JavaScript events can see the keystroke.

This adds a Win32 low-level keyboard hook (WH_KEYBOARD_LL) that intercepts Ctrl+C and Ctrl+A at the OS level, before Revit's accelerator table processes them. The hook only activates when the ScriptConsole is the active window with renderer focus, and forwards the keystrokes to HtmlDocument.ExecCommand for native browser copy.


Testing

Tested on Preflight checks, HTML output, PrintMD Output, Linkfy Output and Emojis Supported.

Select text in output window → Ctrl+C → paste → only selected text copied
Ctrl+A → all output text selected
Ctrl+A → Ctrl+C → paste → all text copied
No selection → Ctrl+C → no crash
Right-click → Copy → still works
"Copy All Text" button → still copies everything
Other Revit windows unaffected (Ctrl+C still works for Revit's own copy)
Multiple output windows → each has independent hook
Close output window → hook properly disposed


Related Issues

If applicable, link the issues resolved by this pull request:


Additional Notes

Problem
The pyRevit output window (ScriptConsole) renders print() output as HTML inside a System.Windows.Forms.WebBrowser control hosted in a WPF MetroWindow via WindowsFormsHost. Users can select text and copy via right-click → Copy, but the standard Ctrl+C keyboard shortcut does nothing.

Why this is hard
Revit's main message loop calls IOleInPlaceActiveObject.TranslateAccelerator which intercepts Ctrl+C for its own Copy command before any of the following can see the keystroke:

WPF PreviewKeyDown (tunneling events) — never fires
WinForms PreviewKeyDown on the WebBrowser — never fires
IMessageFilter.PreFilterMessage — never fires
ComponentDispatcher.ThreadPreprocessMessage — never fires
InputManager.Current.PreProcessInput — never fires
JavaScript document.onkeydown — never fires

Solution
A Win32 low-level keyboard hook (WH_KEYBOARD_LL via SetWindowsHookEx) intercepts keystrokes at the OS level, before Revit's message loop processes them. The hook only activates when the ScriptConsole window is active and the WebBrowser renderer has focus, forwarding:

Ctrl+C → HtmlDocument.ExecCommand("Copy") — copies selected text to clipboard
Ctrl+A → HtmlDocument.ExecCommand("SelectAll") — selects all text

The hook is installed when the ScriptConsole is created and unhooked when the window closes.

Copy link
Copy Markdown
Contributor

@devloai devloai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary:

Adds a Win32 low-level keyboard hook (WH_KEYBOARD_LL) to the ScriptConsole output window to intercept Ctrl+C (copy) and Ctrl+A (select all) before Revit's IOleInPlaceActiveObject.TranslateAccelerator can consume them. The hook delegates to HtmlDocument.ExecCommand for native browser clipboard operations, is installed on construction, and disposed on window close.

Review Summary:

The approach is sound and well-motivated — a low-level hook is genuinely the only way to intercept keystrokes ahead of Revit's accelerator table. However, there are two functional bugs: (1) the focus guard uses renderer != null but doesn't check whether the renderer's ActiveX subtree actually has focus — without ContainsFocus, Ctrl+A will be stolen from stdinBar and other text inputs in the window; (2) Process.MainModule is fragile and unnecessary for WH_KEYBOARD_LL — the established pattern is GetModuleHandle(null). Additionally, installing the hook during InitializeComponent() (before window construction completes) creates a leak path if a later initialization step throws, since Window_Closing only fires for windows that were successfully shown.

Suggestions

  • Add SetWindowsHookEx failure detection: log or warn when _hookId == IntPtr.Zero after installation to aid diagnostics if the hook silently fails to install. Apply
  • Consider hooking WM_SYSKEYDOWN (0x0104) in addition to WM_KEYDOWN to handle edge cases where Revit routes modified keystrokes through the system-key path. Apply

…t window

Fixes pyrevitlabs#1729

The pyRevit output window hosts a WebBrowser control inside a WPF
MetroWindow via WindowsFormsHost. Revit's message loop intercepts
Ctrl+C via IOleInPlaceActiveObject.TranslateAccelerator before WPF,
WinForms, IMessageFilter, or JavaScript events can see the keystroke.

This adds a Win32 low-level keyboard hook (WH_KEYBOARD_LL) that
intercepts Ctrl+C and Ctrl+A at the OS level, before Revit's
accelerator table processes them. The hook only activates when the
ScriptConsole is the active window with renderer focus, and forwards
the keystrokes to HtmlDocument.ExecCommand for native browser copy.

The hook is installed on window creation and disposed on close.
@tay0thman tay0thman force-pushed the tay0thman-feat-ctrlc-copy-1729 branch from d0dff6b to e4381f4 Compare April 3, 2026 17:28
@tay0thman
Copy link
Copy Markdown
Contributor Author

@devloai I pushed a new commit addressing your comments below, review and report back:
renderer.ContainsFocus — hook only fires when the browser's ActiveX subtree has focus, not stdinBar
GetModuleHandle(null) — no more Process.MainModule boilerplate
Debug.WriteLine in catch — exceptions logged instead of swallowed
Hook moved to Window_Loaded — no leak if constructor throws

@devloai
Copy link
Copy Markdown
Contributor

devloai bot commented Apr 3, 2026

All 4 previously raised issues have been addressed — looks clean now ✅

# Issue Status
1 Process.MainModule fragility ✅ Fixed — GetModuleHandle(null)
2 Missing renderer focus check ✅ Fixed — renderer.ContainsFocus
3 Empty catch {} swallowing errors ✅ Fixed — Debug.WriteLine with message
4 Hook installed before window fully loaded ✅ Fixed — moved to Window_Loaded

No new issues introduced. The implementation looks solid — the guard conditions (IsActive, ContainsFocus, ActiveDocument != null) are correctly layered, lifecycle management is symmetric (Loaded/Closing), and the _proc delegate is kept alive via the field to prevent GC collection.

@jmcouffin jmcouffin merged commit b0a5f9b into pyrevitlabs:develop Apr 3, 2026
@jmcouffin
Copy link
Copy Markdown
Contributor

This is something I wanted to implement 5 or 6 years ago!
Thks @tay0thman

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

📦 New work-in-progress (wip) builds are available for 6.3.0.26094+0727-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

📦 New work-in-progress (wip) builds are available for 6.3.0.26095+0645-wip

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

📦 New public release are available for 6.3.0.26095+0830

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Copy text from print ouptut to clipboard by Ctrl+C

2 participants