From 2c192130926de1ad4c255090261917851ebdb1e0 Mon Sep 17 00:00:00 2001 From: Eltos Date: Sat, 9 May 2026 18:31:17 +0200 Subject: [PATCH] Drop SharpClipboard, use own clipboard monitoring implementation Closes #102 --- PasteIntoFile/Dialog.cs | 24 ++- PasteIntoFile/KeyboardHook.cs | 133 ---------------- PasteIntoFile/Main.cs | 74 +++++---- PasteIntoFile/PasteIntoFile.csproj | 3 +- PasteIntoFile/SystemEventMonitor.cs | 229 ++++++++++++++++++++++++++++ 5 files changed, 275 insertions(+), 188 deletions(-) delete mode 100644 PasteIntoFile/KeyboardHook.cs create mode 100644 PasteIntoFile/SystemEventMonitor.cs diff --git a/PasteIntoFile/Dialog.cs b/PasteIntoFile/Dialog.cs index 2acb18f..8cea0ed 100644 --- a/PasteIntoFile/Dialog.cs +++ b/PasteIntoFile/Dialog.cs @@ -8,7 +8,6 @@ using System.Windows.Forms; using PasteIntoFile.Properties; using WK.Libraries.BetterFolderBrowserNS; -using WK.Libraries.SharpClipboardNS; namespace PasteIntoFile { public sealed partial class Dialog : MasterForm { @@ -16,20 +15,14 @@ public sealed partial class Dialog : MasterForm { private int saveCount = 0; private bool _formLoaded = false; - private SharpClipboard _clipMonitor; + private SystemEventMonitor eventMonitor = new SystemEventMonitor(); private bool _disableUiEvents = false; private bool _topMostPreviousState = false; private const string DYNAMIC_EXTENSION = "*"; // special value to determine extension dynamically - public SharpClipboard clipMonitor { - get { - if (_clipMonitor == null) _clipMonitor = new SharpClipboard(); - return _clipMonitor; - } - } protected override void OnFormClosed(FormClosedEventArgs e) { // leave the clipboard monitoring chain in a clean way, otherwise the chain will break when the program exits - clipMonitor?.StopMonitoring(); + eventMonitor?.StopClipboardMonitoring(); base.OnFormClosed(e); } @@ -118,8 +111,8 @@ public Dialog( BringToFrontForced(); // register clipboard monitor - clipMonitor.ClipboardChanged += ClipboardChanged; - FormClosing += (s, e) => clipMonitor.ClipboardChanged -= ClipboardChanged; + eventMonitor.StartClipboardMonitoring(); + eventMonitor.ClipboardChanged += ClipboardChanged; } else { @@ -286,7 +279,7 @@ private bool readClipboard() { - private void ClipboardChanged(Object sender, SharpClipboard.ClipboardChangedEventArgs e) { + private void ClipboardChanged(Object sender, EventArgs e) { // Only process update if live update enabled, or in batch mode if (!chkEnableLiveClipboardUpdate.Checked && !chkContinuousMode.Checked) return; @@ -465,9 +458,10 @@ string save(bool overwriteIfExists = false, bool? clearClipboardOverwrite = fals } if (clearClipboardOverwrite ?? Settings.Default.clrClipboard) { - clipMonitor.MonitorClipboard = false; // to prevent callback during batch mode - Clipboard.Clear(); - clipMonitor.MonitorClipboard = true; + // Prevent callback during batch mode + eventMonitor.CallWithoutClipboardMonitoring(() => { + Clipboard.Clear(); + }); } rememberExtension(content, comExt.Text); diff --git a/PasteIntoFile/KeyboardHook.cs b/PasteIntoFile/KeyboardHook.cs deleted file mode 100644 index 7f5e339..0000000 --- a/PasteIntoFile/KeyboardHook.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Windows.Forms; - -namespace PasteIntoFile { - /// - /// Register global Keyboard Hotkey - /// Thankfully taken from https://stackoverflow.com/a/27309185/13324744 - /// - public sealed class KeyboardHook : IDisposable { - // Registers a hot key with Windows. - [DllImport("user32.dll", SetLastError = true)] - private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); - // Unregisters the hot key with Windows. - [DllImport("user32.dll")] - private static extern bool UnregisterHotKey(IntPtr hWnd, int id); - - /// - /// Represents the window that is used internally to get the messages. - /// - private sealed class Window : NativeWindow, IDisposable { - private static int WM_HOTKEY = 0x0312; - - public Window() { - // create the handle for the window. - CreateHandle(new CreateParams()); - } - - /// - /// Overridden to get the notifications. - /// - /// - protected override void WndProc(ref Message m) { - base.WndProc(ref m); - - // check if we got a hot key pressed. - if (m.Msg == WM_HOTKEY) { - // get the keys. - Keys key = (Keys)(((int)m.LParam >> 16) & 0xFFFF); - ModifierKeys modifier = (ModifierKeys)((int)m.LParam & 0xFFFF); - - // invoke the event to notify the parent. - if (KeyPressed != null) - KeyPressed(this, new KeyPressedEventArgs(modifier, key)); - } - } - - public event EventHandler KeyPressed; - - #region IDisposable Members - - public void Dispose() { - DestroyHandle(); - } - - #endregion - } - - private Window _window = new Window(); - private int _currentId; - - public KeyboardHook() { - // register the event of the inner native window. - _window.KeyPressed += delegate (object sender, KeyPressedEventArgs args) { - if (KeyPressed != null) - KeyPressed(this, args); - }; - } - - /// - /// Registers a hot key in the system. - /// - /// The modifiers that are associated with the hot key. - /// The key itself that is associated with the hot key. - public void RegisterHotKey(ModifierKeys modifier, Keys key) { - // increment the counter. - _currentId += 1; - - // register the hot key. - if (!RegisterHotKey(_window.Handle, _currentId, (uint)modifier, (uint)key)) { - var error = new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); - throw new InvalidOperationException($"Registration of HotKey {modifier.ToString().Replace(", ", "+").ToUpper()}+{key} failed with error {error.NativeErrorCode}: {error.Message}"); - } - } - - /// - /// A hot key has been pressed. - /// - public event EventHandler KeyPressed; - - #region IDisposable Members - - public void Dispose() { - // unregister all the registered hot keys. - for (int i = _currentId; i > 0; i--) { - UnregisterHotKey(_window.Handle, i); - } - - // dispose the inner native window. - _window.Dispose(); - } - - #endregion - } - - /// - /// Event Args for the event that is fired after the hot key has been pressed. - /// - public class KeyPressedEventArgs : EventArgs { - private ModifierKeys _modifier; - private Keys _key; - - internal KeyPressedEventArgs(ModifierKeys modifier, Keys key) { - _modifier = modifier; - _key = key; - } - - public ModifierKeys Modifier => _modifier; - - public Keys Key => _key; - } - - /// - /// The enumeration of possible modifiers. - /// - [Flags] - public enum ModifierKeys : uint { - Alt = 1, - Control = 2, - Shift = 4, - Win = 8 - } -} diff --git a/PasteIntoFile/Main.cs b/PasteIntoFile/Main.cs index cb5a174..09f7a79 100644 --- a/PasteIntoFile/Main.cs +++ b/PasteIntoFile/Main.cs @@ -14,7 +14,6 @@ using CommandLine.Text; using Microsoft.Toolkit.Uwp.Notifications; using PasteIntoFile.Properties; -using WK.Libraries.SharpClipboardNS; #if PORTABLE using Bluegrams.Application; #endif @@ -273,56 +272,55 @@ static int RunTray(ArgsTray args = null) { } + var monitor = new SystemEventMonitor(); + // Register hotkeys - KeyboardHook paste = new KeyboardHook(); - paste.KeyPressed += (s, e) => { - var arg = new ArgsPaste(); - arg.Directory = ExplorerUtil.GetActiveExplorerPath(); - RunPaste(arg); - }; - paste.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt, Keys.V); - paste.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Shift, Keys.V); - paste.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Control, Keys.V); - paste.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Shift | ModifierKeys.Control, Keys.V); - - KeyboardHook copy = new KeyboardHook(); - copy.KeyPressed += (s, e) => { - var files = ExplorerUtil.GetActiveExplorerSelectedFiles(); - if (files.Count == 1) { - var arg = new ArgsCopy(); - arg.FilePath = files.Item(0).Path; - RunCopy(arg); - } else { - MessageBox.Show(Resources.str_copy_failed_not_single_file, Resources.app_title, MessageBoxButtons.OK, MessageBoxIcon.Error); + monitor.KeyPressed += (s, e) => { + if (e.Key == Keys.V) { + // Paste hotkey + var arg = new ArgsPaste(); + arg.Directory = ExplorerUtil.GetActiveExplorerPath(); + RunPaste(arg); + } else if (e.Key == Keys.C) { + // Copy hotkey + var files = ExplorerUtil.GetActiveExplorerSelectedFiles(); + if (files.Count == 1) { + var arg = new ArgsCopy(); + arg.FilePath = files.Item(0).Path; + RunCopy(arg); + } else { + MessageBox.Show(Resources.str_copy_failed_not_single_file, Resources.app_title, MessageBoxButtons.OK, MessageBoxIcon.Error); + } } }; - copy.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt, Keys.C); + // Paste hotkeys (with different modifier combinations) + monitor.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt, Keys.V); + monitor.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Shift, Keys.V); + monitor.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Control, Keys.V); + monitor.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt | ModifierKeys.Shift | ModifierKeys.Control, Keys.V); + // Copy hotkey + monitor.RegisterHotKey(ModifierKeys.Win | ModifierKeys.Alt, Keys.C); + // Register clipboard observer for patching - SharpClipboard clipMonitor = null; if (Settings.Default.trayPatchingEnabled) { bool skipFirst = true; - void PatchClipboard(object s, SharpClipboard.ClipboardChangedEventArgs e) { + monitor.ClipboardChanged += (s, e) => { if (skipFirst) { skipFirst = false; return; } Settings.Default.Reload(); // load modifications made from other instance if (!Settings.Default.trayPatchingEnabled) return; // allow to temporarily disable if (Settings.Default.continuousMode) return; // don't interfere with batch mode + if (PatchedClipboardContents() is IDataObject data) { // TODO: This is experimental (might impact performance, might break proprietary formats used internally by other programs, not 100% stable) - // Temporarily pausing monitoring seams unstable with the SharpClipboard library, so close and re-create the monitor instead - - // Stop monitoring and leave clipboard chain cleanly - clipMonitor.MonitorClipboard = false; - clipMonitor.StopMonitoring(); - // Re-write clipboard contents - Clipboard.SetDataObject(data, false); - // Create a new monitor to handle future updates - clipMonitor = new SharpClipboard(); - clipMonitor.ClipboardChanged += PatchClipboard; + monitor.CallWithoutClipboardMonitoring(() => { + // Re-write clipboard contents with patched version + Clipboard.SetDataObject(data, false); + }); } - } - clipMonitor = new SharpClipboard(); - clipMonitor.ClipboardChanged += PatchClipboard; + }; + + monitor.StartClipboardMonitoring(); } // Tray icon @@ -345,7 +343,7 @@ void PatchClipboard(object s, SharpClipboard.ClipboardChangedEventArgs e) { Application.Run(); // leave the clipboard monitoring chain in a clean way, otherwise the chain will break when the program exits - clipMonitor?.StopMonitoring(); + monitor.StopClipboardMonitoring(); icon.Visible = false; diff --git a/PasteIntoFile/PasteIntoFile.csproj b/PasteIntoFile/PasteIntoFile.csproj index 5e306eb..ec7043b 100644 --- a/PasteIntoFile/PasteIntoFile.csproj +++ b/PasteIntoFile/PasteIntoFile.csproj @@ -52,7 +52,6 @@ - @@ -76,7 +75,7 @@ - + diff --git a/PasteIntoFile/SystemEventMonitor.cs b/PasteIntoFile/SystemEventMonitor.cs new file mode 100644 index 0000000..29d5fd6 --- /dev/null +++ b/PasteIntoFile/SystemEventMonitor.cs @@ -0,0 +1,229 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; + +namespace PasteIntoFile { + /// + /// Monitor and react to global events like hotkeys and clipboard updates + /// + public sealed class SystemEventMonitor : IDisposable { + // Registers a hot key with Windows. + [DllImport("user32.dll", SetLastError = true)] + private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk); + // Unregisters the hot key with Windows. + [DllImport("user32.dll")] + private static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + // Clipboard listener APIs + [DllImport("user32.dll", SetLastError = true)] + private static extern bool AddClipboardFormatListener(IntPtr hwnd); + [DllImport("user32.dll", SetLastError = true)] + private static extern bool RemoveClipboardFormatListener(IntPtr hwnd); + + /// + /// Represents the window that is used internally to get the messages. + /// + private sealed class Window : NativeWindow, IDisposable { + private static int WM_HOTKEY = 0x0312; + private static int WM_CLIPBOARDUPDATE = 0x031D; + + public Window() { + // create the handle for the window. + CreateHandle(new CreateParams()); + } + + /// + /// Overridden to get the notifications. + /// + /// + protected override void WndProc(ref Message m) { + base.WndProc(ref m); + + // check if we got a hot key pressed. + if (m.Msg == WM_HOTKEY) { + // get the keys. + Keys key = (Keys)(((int)m.LParam >> 16) & 0xFFFF); + ModifierKeys modifier = (ModifierKeys)((int)m.LParam & 0xFFFF); + + // invoke the event to notify the parent. + KeyPressed?.Invoke(this, new KeyPressedEventArgs(modifier, key)); + } + + // check if we got a clipboard update + if (m.Msg == WM_CLIPBOARDUPDATE) { + ClipboardChanged?.Invoke(this, EventArgs.Empty); + } + } + + public event EventHandler KeyPressed; + public event EventHandler ClipboardChanged; + + #region IDisposable Members + + public void Dispose() { + DestroyHandle(); + } + + #endregion + } + + private Window _window; + private int _currentId; + + // clipboard monitoring state + private volatile bool _clipboardMonitoring; + + public SystemEventMonitor() { + _window = new Window(); + + // register the event of the inner native window. + _window.KeyPressed += delegate (object sender, KeyPressedEventArgs args) { + KeyPressed?.Invoke(this, args); + }; + + // Map window clipboard events to the monitor, with safe access handling + _window.ClipboardChanged += (s, e) => { + if (!_clipboardMonitoring) return; + + // Ensure clipboard is readable before firing event + for (var i = 0; i < 10; i++) { + try { + Clipboard.GetDataObject(); + // Clipboard access works, fire event + ClipboardChanged?.Invoke(this, EventArgs.Empty); + break; + } catch { + Thread.Sleep(100); + } + } + + }; + } + + /// + /// Registers a hot key in the system. + /// + /// The modifiers that are associated with the hot key. + /// The key itself that is associated with the hot key. + public void RegisterHotKey(ModifierKeys modifier, Keys key) { + // increment the counter. + _currentId += 1; + + // register the hot key. + if (!RegisterHotKey(_window.Handle, _currentId, (uint)modifier, (uint)key)) { + var error = new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + throw new InvalidOperationException($"Registration of HotKey {modifier.ToString().Replace(", ", "+").ToUpper()}+{key} failed with error {error.NativeErrorCode}: {error.Message}"); + } + } + + /// + /// A hot key has been pressed. + /// + public event EventHandler KeyPressed; + + /// + /// Fired when the clipboard was changed and the new content is accessible + /// + public event EventHandler ClipboardChanged; + + public bool IsClipboardMonitoring => _clipboardMonitoring; + + /// + /// Starts monitoring the clipboard for changes. + /// When the clipboard content changes and is accessible, the ClipboardChanged event will be fired. + /// + /// + public void StartClipboardMonitoring() { + if (_clipboardMonitoring) return; + if (_window == null) throw new InvalidOperationException("Internal window not created."); + + if (!AddClipboardFormatListener(_window.Handle)) { + var err = new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + throw new InvalidOperationException($"AddClipboardFormatListener failed: {err.NativeErrorCode} {err.Message}"); + } + + _clipboardMonitoring = true; + } + + /// + /// Stops monitoring the clipboard for changes. + /// + public void StopClipboardMonitoring() { + if (!_clipboardMonitoring) return; + try { + RemoveClipboardFormatListener(_window.Handle); + } catch { + // ignored + } + _clipboardMonitoring = false; + } + + /// + /// Calls a callback while temporarily stopping clipboard monitoring. + /// The callback may alter the clipboard without triggering the ClipboardChanged event. + /// + /// + public void CallWithoutClipboardMonitoring(Action callback) { + var wasMonitoring = _clipboardMonitoring; + try { + StopClipboardMonitoring(); + } catch { + // ignored + } + try { + callback?.Invoke(); + } finally { + if (wasMonitoring) { + StartClipboardMonitoring(); + } + } + } + + #region IDisposable Members + + public void Dispose() { + // stop clipboard monitoring + StopClipboardMonitoring(); + + // unregister all the registered hot keys. + for (var i = _currentId; i > 0; i--) { + UnregisterHotKey(_window.Handle, i); + } + + // dispose the inner native window. + _window.Dispose(); + _window = null; + } + + #endregion + } + + /// + /// Event Args for the event that is fired after the hot key has been pressed. + /// + public class KeyPressedEventArgs : EventArgs { + private ModifierKeys _modifier; + private Keys _key; + + internal KeyPressedEventArgs(ModifierKeys modifier, Keys key) { + _modifier = modifier; + _key = key; + } + + public ModifierKeys Modifier => _modifier; + + public Keys Key => _key; + } + + /// + /// The enumeration of possible modifiers. + /// + [Flags] + public enum ModifierKeys : uint { + Alt = 1, + Control = 2, + Shift = 4, + Win = 8 + } +}