Skip to content

Commit

Permalink
Create copies of win32 functions to not change global state and to fi…
Browse files Browse the repository at this point in the history
…x compatibility with DECTalk add-on
  • Loading branch information
mzanm committed Oct 31, 2023
1 parent fe7e7d8 commit 9753aca
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 107 deletions.
35 changes: 23 additions & 12 deletions addon/globalPlugins/autoclip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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 = {
Expand Down
191 changes: 100 additions & 91 deletions addon/globalPlugins/autoclip/winclip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 4 additions & 4 deletions buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>",
# URL for the add-on documentation support
Expand All @@ -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!
Expand Down
16 changes: 16 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 9753aca

Please sign in to comment.