From 9753aca9f760f57797483381865deb0418979c1c Mon Sep 17 00:00:00 2001 From: Mazen <40980323+mzanm@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:24:39 +0300 Subject: [PATCH] Create copies of win32 functions to not change global state and to fix compatibility with DECTalk add-on --- addon/globalPlugins/autoclip/__init__.py | 35 +++-- addon/globalPlugins/autoclip/winclip.py | 191 ++++++++++++----------- buildVars.py | 8 +- changelog.md | 16 ++ 4 files changed, 143 insertions(+), 107 deletions(-) diff --git a/addon/globalPlugins/autoclip/__init__.py b/addon/globalPlugins/autoclip/__init__.py index ff384b9..765f1a0 100644 --- a/addon/globalPlugins/autoclip/__init__.py +++ b/addon/globalPlugins/autoclip/__init__.py @@ -30,7 +30,8 @@ class ClipboardWatcher: def __init__(self): self.state = False - self.hwnd = winclip.CreateWindow( + self.winclip = winclip.win32clip() + self.hwnd = self.winclip.CreateWindow( 0, "STATIC", None, @@ -41,41 +42,50 @@ def __init__(self): 0, winclip.HWND_MESSAGE, None, - winclip.GetModuleHandle(None), + self.winclip.GetModuleHandle(None), None, ) log.debug("created window {}".format(self.hwnd)) + @staticmethod + def message_text(text, interrupt=False): + if interrupt: + speech.cancelSpeech() + ui.message(text) + def start(self): @ctypes.WINFUNCTYPE(ctypes.c_long, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM) def wndproc(hwnd, msg, wparam, lparam): if msg == winclip.WM_CLIPBOARDUPDATE: queueHandler.queueFunction(queueHandler.eventQueue, self.notify) return 0 - return winclip.DefWindowProc(hwnd, msg, wparam, lparam) + return self.winclip.DefWindowProc(hwnd, msg, wparam, lparam) self.proc = wndproc - self.oldProc = winclip.SetWindowLong( + self.oldProc = self.winclip.SetWindowLong( self.hwnd, winclip.GWL_WNDPROC, ctypes.cast(self.proc, ctypes.c_void_p) ) - res = winclip.AddClipboardFormatListener(self.hwnd) + res = self.winclip.AddClipboardFormatListener(self.hwnd) log.debug("add format listener {}".format(res)) self.state = True def stop(self): - winclip.RemoveClipboardFormatListener(self.hwnd) - winclip.SetWindowLong(self.hwnd, winclip.GWL_WNDPROC, self.oldProc) - winclip.DestroyWindow(self.hwnd) + self.winclip.RemoveClipboardFormatListener(self.hwnd) + self.winclip.SetWindowLong(self.hwnd, winclip.GWL_WNDPROC, self.oldProc) + self.winclip.DestroyWindow(self.hwnd) + self.proc = None + self.oldProc = None self.state = False def notify(self): - with winclip.clipboard(self.hwnd): - data = winclip.get_clipboard_data() + with self.winclip.clipboard(self.hwnd): + data = self.winclip.get_clipboard_data() if data and data.strip(): + interrupt = False if config.conf["autoclip"]["interrupt"]: - speech.cancelSpeech() - queueHandler.queueFunction(queueHandler.eventQueue, ui.message, (data)) + interrupt = True + queueHandler.queueFunction(queueHandler.eventQueue, ClipboardWatcher.message_text, data, interrupt) class GlobalPlugin(globalPluginHandler.GlobalPlugin): @@ -150,6 +160,7 @@ def terminate(self): config.post_configProfileSwitch.unregister(self.onConfigInit) self.toolsMenu.Delete(self.menuItem) self.menuItem = None + self.toolsMenu = None confspec = { diff --git a/addon/globalPlugins/autoclip/winclip.py b/addon/globalPlugins/autoclip/winclip.py index 5592e4a..512fbe9 100644 --- a/addon/globalPlugins/autoclip/winclip.py +++ b/addon/globalPlugins/autoclip/winclip.py @@ -34,95 +34,104 @@ GWL_WNDPROC = -4 HWND_MESSAGE = -3 -OpenClipboard = ctypes.windll.user32.OpenClipboard -OpenClipboard.argtypes = [HWND] -OpenClipboard.restype = BOOL -CloseClipboard = ctypes.windll.user32.CloseClipboard -CloseClipboard.argtypes = [] -CloseClipboard.restype = BOOL - -GetClipboardData = ctypes.windll.user32.GetClipboardData -GetClipboardData.argtypes = [UINT] -GetClipboardData.restype = HANDLE - -AddClipboardFormatListener = ctypes.windll.user32.AddClipboardFormatListener -AddClipboardFormatListener.argtypes = [HWND] -AddClipboardFormatListener.restype = BOOL - -RemoveClipboardFormatListener = ctypes.windll.user32.RemoveClipboardFormatListener -RemoveClipboardFormatListener.argtypes = [HWND] -RemoveClipboardFormatListener.restype = BOOL - -GlobalLock = ctypes.windll.kernel32.GlobalLock -GlobalLock.argtypes = [HGLOBAL] -GlobalLock.restype = LPVOID - -GlobalUnlock = ctypes.windll.kernel32.GlobalUnlock -GlobalUnlock.argtypes = [HGLOBAL] -GlobalUnlock.restype = BOOL - -GetModuleHandle = ctypes.windll.kernel32.GetModuleHandleW -GetModuleHandle.argtypes = [LPCWSTR] -GetModuleHandle.restype = HMODULE - -CreateWindow = ctypes.windll.user32.CreateWindowExW -CreateWindow.argtypes = [ - DWORD, - LPCWSTR, - LPCWSTR, - DWORD, - INT, - INT, - INT, - INT, - HWND, - HMENU, - HINSTANCE, - LPVOID, -] -CreateWindow.restype = HWND - -DestroyWindow = ctypes.windll.user32.DestroyWindow -DestroyWindow.argtypes = [HWND] -DestroyWindow.restype = BOOL - -SetWindowLong = ctypes.windll.user32.SetWindowLongW -SetWindowLong.argtypes = [HWND, INT, ctypes.c_void_p] -SetWindowLong.restype = ctypes.c_long - -DefWindowProc = ctypes.windll.user32.DefWindowProcW -DefWindowProc.argtypes = [HWND, UINT, WPARAM, LPARAM] -DefWindowProc.restype = ctypes.c_long - - -@contextlib.contextmanager -def clipboard(hwnd): - # a program could be opening the clipboard, so we'll try for at least one second to open it - t = time.perf_counter() + 1 - while time.perf_counter() < t: - s = OpenClipboard(hwnd) - if not s: - log.warning("Error while trying to open clipboard.", exc_info=ctypes.WinError()) - if s: - break - time.sleep(0.01) - try: - yield - finally: - CloseClipboard() - - -def get_clipboard_data(format=CF_UNICODETEXT): - handle = GetClipboardData(format) - if not handle: - return "" - locked_handle = GlobalLock(handle) - if not locked_handle: - log.error("unable to lock clipboard handle") - raise ctypes.WinError() - try: - data = ctypes.c_wchar_p(locked_handle).value - return data if data else "" - finally: - GlobalUnlock(handle) +class win32clip: + def __init__(self): + + self.OpenClipboard = copy_func(ctypes.windll.user32.OpenClipboard) + self.OpenClipboard.argtypes = [HWND] + self.OpenClipboard.restype = BOOL + + self.CloseClipboard = copy_func(ctypes.windll.user32.CloseClipboard) + self.CloseClipboard.argtypes = [] + self.CloseClipboard.restype = BOOL + + self.GetClipboardData = copy_func(ctypes.windll.user32.GetClipboardData) + self.GetClipboardData.argtypes = [UINT] + self.GetClipboardData.restype = HANDLE + + self.AddClipboardFormatListener = copy_func(ctypes.windll.user32.AddClipboardFormatListener) + self.AddClipboardFormatListener.argtypes = [HWND] + self.AddClipboardFormatListener.restype = BOOL + + self.RemoveClipboardFormatListener = copy_func(ctypes.windll.user32.RemoveClipboardFormatListener) + self.RemoveClipboardFormatListener.argtypes = [HWND] + self.RemoveClipboardFormatListener.restype = BOOL + + self.GlobalLock = copy_func(ctypes.windll.kernel32.GlobalLock) + self.GlobalLock.argtypes = [HGLOBAL] + self.GlobalLock.restype = LPVOID + + self.GlobalUnlock = copy_func(ctypes.windll.kernel32.GlobalUnlock) + self.GlobalUnlock.argtypes = [HGLOBAL] + self.GlobalUnlock.restype = BOOL + + self.GetModuleHandle = copy_func(ctypes.windll.kernel32.GetModuleHandleW) + self.GetModuleHandle.argtypes = [LPCWSTR] + self.GetModuleHandle.restype = HMODULE + + self.CreateWindow = copy_func(ctypes.windll.user32.CreateWindowExW) + self.CreateWindow.argtypes = [ + DWORD, + LPCWSTR, + LPCWSTR, + DWORD, + INT, + INT, + INT, + INT, + HWND, + HMENU, + HINSTANCE, + LPVOID, + ] + self.CreateWindow.restype = HWND + + self.DestroyWindow = copy_func(ctypes.windll.user32.DestroyWindow) + self.DestroyWindow.argtypes = [HWND] + self.DestroyWindow.restype = BOOL + + self.SetWindowLong = copy_func(ctypes.windll.user32.SetWindowLongW) + self.SetWindowLong.argtypes = [HWND, INT, ctypes.c_void_p] + self.SetWindowLong.restype = ctypes.c_long + + self.DefWindowProc = copy_func(ctypes.windll.user32.DefWindowProcW) + self.DefWindowProc.argtypes = [HWND, UINT, WPARAM, LPARAM] + self.DefWindowProc.restype = ctypes.c_long + + @contextlib.contextmanager + def clipboard(self, hwnd): + # a program could be opening the clipboard, so we'll try for at least half a second to open it + t = time.perf_counter() + 0.5 + while time.perf_counter() < t: + s = self.OpenClipboard(hwnd) + if not s: + log.warning("Error while trying to open clipboard.", exc_info=ctypes.WinError()) + if s: + break + time.sleep(0.01) + try: + yield + finally: + self.CloseClipboard() + + def get_clipboard_data(self, format=CF_UNICODETEXT): + handle = self.GetClipboardData(format) + if not handle: + return "" + locked_handle = self.GlobalLock(handle) + if not locked_handle: + log.error("unable to lock clipboard handle") + raise ctypes.WinError() + try: + data = ctypes.c_wchar_p(locked_handle).value + return data if data else "" + finally: + self.GlobalUnlock(handle) + + +def copy_func(func): + # copy a ctypes func_ptr object + func_type = type(func) + coppied_func = func_type.from_address(ctypes.addressof(func)) + return coppied_func diff --git a/buildVars.py b/buildVars.py index f93e16f..71e75cd 100644 --- a/buildVars.py +++ b/buildVars.py @@ -20,12 +20,12 @@ def _(arg): # Add-on summary, usually the user visible name of the addon. # Translators: Summary for this add-on # to be shown on installation and add-on information found in Add-ons Manager. - "addon_summary": _("Autoclip"), + "addon_summary": _("Autoclip, automatically speak clipboard content when it changes"), # Add-on description # Translators: Long description to be shown for this add-on on add-on information from add-ons manager - "addon_description": _("Automatically read the contents of the clipboard when a change is detected."), + "addon_description": _("Automatically read the contents of the clipboard when a change is detected. Press NVDA + Shift + Control + K to toggle."), # version - "addon_version": "1.0.5", + "addon_version": "1.1.0", # Author(s) "addon_author": "Mazen ", # URL for the add-on documentation support @@ -37,7 +37,7 @@ def _(arg): # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) "addon_minimumNVDAVersion": 2019.3, # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion": 2023.2, + "addon_lastTestedNVDAVersion": 2023.3, # Add-on update channel (default is None, denoting stable releases, # and for development releases, use "dev".) # Do not change unless you know what you are doing! diff --git a/changelog.md b/changelog.md index da3aed7..c8a2120 100644 --- a/changelog.md +++ b/changelog.md @@ -1,19 +1,35 @@ # NVDAAutoclip changelog: +## V1.1.0: + +- enternal code optimizations to fix compatibility issue with DECTalk add-on and other bugs. + +- Updated translations. + + ## V1.0.4: - Added Ukrainian translation by Heorhii. + ## V1.0.3: - Added Vietnamese translation by nguyenninhhoang. + ## V1.0.2: + - the toggle Autoclip script is now in it's own category. The description has been also updated to include the name of the add-on so that it can be found when searching for it in the input gesture dialog. + - You can now toggle Autoclip, automatic clipboard reading in sleep mode + ## V1.0.1: + - the readme is included as add-on help. + ## V1.0.0: + - Initial release. +