From c94d49a475ba62f62c43ee820c3defb22f547b58 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 30 May 2019 00:59:57 +1000 Subject: [PATCH 1/4] Remove dependency on pyWin32, including pythoncom, win32clipboard and win32con. --- nvdaHelper/local/nvdaHelperLocal.def | 1 + nvdaHelper/local/oleUtils.cpp | 44 ++++++++++++++++++++++ nvdaHelper/local/sconscript | 1 + source/NVDAObjects/window/edit.py | 17 +++------ source/api.py | 39 ++++++------------- source/bdDetect.py | 6 ++- source/core.py | 2 +- source/nvda.pyw | 5 +-- source/winInputHook.py | 13 ++++++- source/winKernel.py | 28 ++++++++++++++ source/winUser.py | 56 ++++++++++++++++++++++++++++ 11 files changed, 165 insertions(+), 47 deletions(-) create mode 100644 nvdaHelper/local/oleUtils.cpp diff --git a/nvdaHelper/local/nvdaHelperLocal.def b/nvdaHelper/local/nvdaHelperLocal.def index 07d654c7e80..d4a4f109f14 100644 --- a/nvdaHelper/local/nvdaHelperLocal.def +++ b/nvdaHelper/local/nvdaHelperLocal.def @@ -57,3 +57,4 @@ EXPORTS dllImportTableHooks_unhookSingle audioDucking_shouldDelay logMessage + getOleClipboardText diff --git a/nvdaHelper/local/oleUtils.cpp b/nvdaHelper/local/oleUtils.cpp new file mode 100644 index 00000000000..1bfd6e9c924 --- /dev/null +++ b/nvdaHelper/local/oleUtils.cpp @@ -0,0 +1,44 @@ +/* +This file is a part of the NVDA project. +URL: http://www.nvda-project.org/ +Copyright 2019 NV Access Limited. + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License version 2.0, as published by + the Free Software Foundation. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +This license can be found at: +http://www.gnu.org/licenses/old-licenses/gpl-2.0.html +*/ + +#include +#include +#include + +/* + * Fetches a text representation of the given OLE data object + * @param dataObject an IDataObject interface of an OLE object + * @param text a pointer to a BSTR which will hold the resulting text + * @return S_OK on success or an OLE error code. + */ +HRESULT getOleClipboardText(IDataObject* dataObject, BSTR* text) { + FORMATETC format={CF_UNICODETEXT,nullptr,DVASPECT_CONTENT,-1,TYMED_HGLOBAL}; + STGMEDIUM medium={0}; + HRESULT res=dataObject->GetData(&format,&medium); + if(res!=S_OK) { + LOG_DEBUGWARNING(L"IDataObject::getData failed with error "<0 and not text.isspace(): - try: - win32clipboard.OpenClipboard() - except win32clipboard.error: - return False - try: - win32clipboard.EmptyClipboard() - win32clipboard.SetClipboardData(win32con.CF_UNICODETEXT, text) - finally: - win32clipboard.CloseClipboard() - win32clipboard.OpenClipboard() # there seems to be a bug so to retrieve unicode text we have to reopen the clipboard - try: - got = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT) - finally: - win32clipboard.CloseClipboard() - if got == text: - return True - return False + if not isinstance(text,basestring) or len(text)==0: + return False + import gui + with winUser.openClipboard(gui.mainFrame.Handle): + winUser.emptyClipboard() + winUser.setClipboardData(winUser.CF_UNICODETEXT,text) + got=getClipData() + return got == text def getClipData(): """Receives text from the windows clipboard. @returns: Clipboard text @rtype: string """ - text = "" - win32clipboard.OpenClipboard() - try: - text = win32clipboard.GetClipboardData(win32con.CF_UNICODETEXT) - finally: - win32clipboard.CloseClipboard() - return text + import gui + with winUser.openClipboard(gui.mainFrame.Handle): + return winUser.getClipboardData(winUser.CF_UNICODETEXT) or u"" def getStatusBar(): """Obtain the status bar for the current foreground object. diff --git a/source/bdDetect.py b/source/bdDetect.py index dc823700d85..42b09a6e128 100644 --- a/source/bdDetect.py +++ b/source/bdDetect.py @@ -20,17 +20,19 @@ import hwPortUtils import braille import winKernel +import winUser import core import ctypes from logHandler import log import config import time import thread -from win32con import WM_DEVICECHANGE, DBT_DEVNODES_CHANGED import appModuleHandler from baseObject import AutoPropertyObject import re +DBT_DEVNODES_CHANGED=7 + _driverDevices = OrderedDict() USB_ID_REGEX = re.compile(r"^VID_[0-9A-F]{4}&PID_[0-9A-F]{4}$", re.U) @@ -295,7 +297,7 @@ def rescan(self, usb=True, bluetooth=True, limitToDevices=None): self._startBgScan(usb=usb, bluetooth=bluetooth, limitToDevices=limitToDevices) def handleWindowMessage(self, msg=None, wParam=None): - if msg == WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: + if msg == winUser.WM_DEVICECHANGE and wParam == DBT_DEVNODES_CHANGED: self.rescan(bluetooth=self._detectBluetooth, limitToDevices=self._limitToDevices) def pollBluetoothDevices(self): diff --git a/source/core.py b/source/core.py index 228df2e8f5f..e880dc7b653 100644 --- a/source/core.py +++ b/source/core.py @@ -303,7 +303,7 @@ def onEndSession(evt): import windowUtils class MessageWindow(windowUtils.CustomWindow): className = u"wxWindowClassNR" - #Just define these constants here, so we don't have to import win32con + # Windows constants for power / display changes WM_POWERBROADCAST = 0x218 WM_DISPLAYCHANGE = 0x7e PBT_APMPOWERSTATUSCHANGE = 0xA diff --git a/source/nvda.pyw b/source/nvda.pyw index 21c1e1fafa9..bb612272a8e 100755 --- a/source/nvda.pyw +++ b/source/nvda.pyw @@ -32,7 +32,6 @@ except: import time import argparse -import win32con import globalVars import config import logHandler @@ -122,7 +121,7 @@ parser.add_argument('--ease-of-access',action="store_true",dest='easeOfAccess',d def terminateRunningNVDA(window): processID,threadID=winUser.getWindowThreadProcessID(window) - winUser.PostMessage(window,win32con.WM_QUIT,0,0) + winUser.PostMessage(window,winUser.WM_QUIT,0,0) h=winKernel.openProcess(winKernel.SYNCHRONIZE,False,processID) if not h: # The process is already dead. @@ -219,7 +218,7 @@ log.debug("Debug level logging enabled") if globalVars.appArgs.changeScreenReaderFlag: winUser.setSystemScreenReaderFlag(True) #Accept wm_quit from other processes, even if running with higher privilages -if not ctypes.windll.user32.ChangeWindowMessageFilter(win32con.WM_QUIT,1): +if not ctypes.windll.user32.ChangeWindowMessageFilter(winUser.WM_QUIT,1): raise WinError() # Make this the last application to be shut down and don't display a retry dialog box. winKernel.SetProcessShutdownParameters(0x100, winKernel.SHUTDOWN_NORETRY) diff --git a/source/winInputHook.py b/source/winInputHook.py index abd8f8cd236..df307b5c079 100755 --- a/source/winInputHook.py +++ b/source/winInputHook.py @@ -9,8 +9,17 @@ import time from ctypes import * from ctypes.wintypes import * -from win32con import WM_QUIT, HC_ACTION, WH_KEYBOARD_LL, LLKHF_UP, LLKHF_EXTENDED, LLKHF_INJECTED, WH_MOUSE_LL, LLMHF_INJECTED import watchdog +import winUser + +# Some Windows constants +HC_ACTION = 0 +WH_KEYBOARD_LL = 13 +LLKHF_UP = 128 +LLKHF_EXTENDED = 1 +LLKHF_INJECTED = 16 +WH_MOUSE_LL = 14 +LLMHF_INJECTED = 1 class KBDLLHOOKSTRUCT(Structure): _fields_=[ @@ -97,6 +106,6 @@ def terminate(): raise RuntimeError("winInputHook not running") hookThreadRefCount-=1 if hookThreadRefCount==0: - windll.user32.PostThreadMessageW(hookThread.ident,WM_QUIT,0,0) + windll.user32.PostThreadMessageW(hookThread.ident,winUser.WM_QUIT,0,0) hookThread.join() hookThread=None diff --git a/source/winKernel.py b/source/winKernel.py index 2d827078bd5..98338714c59 100644 --- a/source/winKernel.py +++ b/source/winKernel.py @@ -4,6 +4,7 @@ #This file is covered by the GNU General Public License. #See the file COPYING for more details. +import contextlib import ctypes import ctypes.wintypes from ctypes import * @@ -329,3 +330,30 @@ def DuplicateHandle(sourceProcessHandle, sourceHandle, targetProcessHandle, desi PAPCFUNC = ctypes.WINFUNCTYPE(None, ctypes.wintypes.ULONG) THREAD_SET_CONTEXT = 16 + +GMEM_MOVEABLE=2 + +class HGLOBAL(HANDLE): + + def __init__(self,h,autoFree=True): + super(HGLOBAL,self).__init__(h) + self._autoFree=autoFree + + def __del__(self): + if self and self._autoFree: + windll.kernel32.GlobalFree(self) + + @classmethod + def alloc(cls,flags,size): + h=windll.kernel32.GlobalAlloc(flags,size) + return cls(h) + + @contextlib.contextmanager + def lock(self): + try: + yield windll.kernel32.GlobalLock(self) + finally: + windll.kernel32.GlobalUnlock(self) + + def forget(self): + self.value=None diff --git a/source/winUser.py b/source/winUser.py index a3fff02f8a0..77a29652a44 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -6,8 +6,10 @@ """Functions that wrap Windows API functions from user32.dll""" +import contextlib from ctypes import * from ctypes.wintypes import * +import winKernel #dll handles user32=windll.user32 @@ -95,8 +97,10 @@ class GUITHREADINFO(Structure): CBS_OWNERDRAWVARIABLE=0x0020 CBS_HASSTRINGS=0x00200 WM_NULL=0 +WM_QUIT=18 WM_COPYDATA=74 WM_NOTIFY=78 +WM_DEVICECHANGE=537 WM_USER=1024 #PeekMessage PM_REMOVE=1 @@ -612,3 +616,55 @@ def SendInput(inputs): n = len(inputs) arr = (Input * n)(*inputs) user32.SendInput(n, arr, sizeof(Input)) + +# Windows Clipboard format constants +CF_UNICODETEXT = 13 + +@contextlib.contextmanager +def openClipboard(hwndOwner=None): + """ + A context manager version of OpenClipboard from user32. + Use as the expression of a 'with' statement, and CloseClipboard will automatically be called at the end. + """ + if not windll.user32.OpenClipboard(hwndOwner): + raise RuntimeError("OpenClipboard failed") + try: + yield + finally: + windll.user32.CloseClipboard() + +def emptyClipboard(): + windll.user32.EmptyClipboard() + +def getClipboardData(format): + # We only support unicode text for now + if format!=CF_UNICODETEXT: + raise ValueError("Unsupported format") + # Fetch the data from the clipboard as a global memory handle + h=windll.user32.GetClipboardData(format) + if h: + # Lock the global memory while we fetch the unicode string + # But make sure not to free the memory accidentally -- it is not ours + h=winKernel.HGLOBAL(h,autoFree=False) + with h.lock() as addr: + if addr: + # Read the string from the local memory address + return wstring_at(addr) + +def setClipboardData(format,data): + # For now only unicode is a supported format + if format!=CF_UNICODETEXT: + raise ValueError("Unsupported format") + text=unicode(data) + # Allocate global memory + h=winKernel.HGLOBAL.alloc(winKernel.GMEM_MOVEABLE,(len(text)+1)*2) + # Acquire a lock to the global memory receiving a local memory address + with h.lock() as addr: + # Write the text into the allocated memory + bufLen=len(text)+1 + buf=(c_wchar*bufLen).from_address(addr) + buf.value=text + # Set the clipboard data with the global memory + windll.user32.SetClipboardData(format,h) + # NULL the global memory handle so that it is not freed at the end of scope as the clipboard now has it. + h.forget() From 009f31b9de800a4deb6f28b457724d9addbb89e5 Mon Sep 17 00:00:00 2001 From: Michael Curran Date: Thu, 30 May 2019 07:08:10 +1000 Subject: [PATCH 2/4] nvdaHelperLocal's oleUtils.cpp: fix syntax error. --- nvdaHelper/local/oleUtils.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nvdaHelper/local/oleUtils.cpp b/nvdaHelper/local/oleUtils.cpp index 1bfd6e9c924..5dad47db846 100644 --- a/nvdaHelper/local/oleUtils.cpp +++ b/nvdaHelper/local/oleUtils.cpp @@ -30,7 +30,7 @@ HRESULT getOleClipboardText(IDataObject* dataObject, BSTR* text) { LOG_DEBUGWARNING(L"IDataObject::getData failed with error "< Date: Fri, 31 May 2019 15:16:48 +1000 Subject: [PATCH 3/4] Address review comments. --- nvdaHelper/local/oleUtils.cpp | 2 +- source/NVDAObjects/window/edit.py | 2 +- source/winKernel.py | 26 ++++++++++++++++++++++++++ source/winUser.py | 24 +++++++++++++----------- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/nvdaHelper/local/oleUtils.cpp b/nvdaHelper/local/oleUtils.cpp index 5dad47db846..e09b7da7241 100644 --- a/nvdaHelper/local/oleUtils.cpp +++ b/nvdaHelper/local/oleUtils.cpp @@ -26,7 +26,7 @@ HRESULT getOleClipboardText(IDataObject* dataObject, BSTR* text) { FORMATETC format={CF_UNICODETEXT,nullptr,DVASPECT_CONTENT,-1,TYMED_HGLOBAL}; STGMEDIUM medium={0}; HRESULT res=dataObject->GetData(&format,&medium); - if(res!=S_OK) { + if(FAILED(res)) { LOG_DEBUGWARNING(L"IDataObject::getData failed with error "< Date: Mon, 3 Jun 2019 15:26:12 +1000 Subject: [PATCH 4/4] No longer forceably include win32api via setup.py. Also update what's new. --- source/setup.py | 2 -- user_docs/en/changes.t2t | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/source/setup.py b/source/setup.py index dded378afe0..e105e777cc3 100755 --- a/source/setup.py +++ b/source/setup.py @@ -208,8 +208,6 @@ def getRecursiveDataFiles(dest,source,excludes=()): "nvdaBuiltin", # #3368: bisect was implicitly included with Python 2.7.3, but isn't with 2.7.5. "bisect", - # Also, the previous service executable used win32api, which some add-ons use for various purposes. - "win32api", ], }}, data_files=[ diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index 3e0a2955024..94d28c8faaf 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -15,6 +15,7 @@ What's New in NVDA - inputCore.InputGesture.logIdentifier property has been removed. Now use inputCore.InputGesture.identifiers[0] instead. - speakText and speakCharacter methods on synthDriver objects are no longer supported. this functionality is handled by SynthDriver.speak now. - SynthSetting classes in synthDriverHandler have been removed. Now use driverHandler.DriverSetting classes instead. +- As NVDA no longer depends on pyWin32, modules such as win32api and win32con are no longer available to add-ons. win32api calls can be replaced with direct calls to win32 dll functions via ctypes, and win32con constants should be defined at the top of your files. (#9639) = 2019.3 =