diff --git a/.gitignore b/.gitignore index 7d721df..3045307 100644 --- a/.gitignore +++ b/.gitignore @@ -153,3 +153,4 @@ cython_debug/ /documentation/.#* .rst *.orig +/.merge_file_* diff --git a/natlink_test/requires_natlink/test_actions.py b/natlink_test/requires_natlink/test_actions.py index 49c24df..c45a045 100644 --- a/natlink_test/requires_natlink/test_actions.py +++ b/natlink_test/requires_natlink/test_actions.py @@ -8,9 +8,9 @@ import pytest # from dtactions import natlinkclipboard -# from dtactions.unimacro import unimacroactions as actions # old style -from dtactions.unimacro import newactions as actions -from dtactions.unimacro import unimacroutils +# from dtactions import unimacroactions as actions # old style +from dtactions import unimacroactions as actions +from dtactions import unimacroutils try: from dtactions.__init__ import getThisDir, checkDirectory @@ -47,6 +47,7 @@ def xxx_test_keystroke(): keystroke('{shift+left 11}{ctrl+x}') time.sleep(0.01) result = unimacroutils.getClipboard() + #Hello world assert result == 'Hello world' # def test_save_return_clipboard(): @@ -73,3 +74,4 @@ def test_save_return_clipboard(): if __name__ == "__main__": pytest.main(['test_actions.py']) + # diff --git a/natlink_test/requires_natlink/unittestClipboard.py b/natlink_test/requires_natlink/unittestClipboard.py index 556e3b0..bd483f6 100644 --- a/natlink_test/requires_natlink/unittestClipboard.py +++ b/natlink_test/requires_natlink/unittestClipboard.py @@ -17,7 +17,7 @@ import win32gui from dtactions import natlinkclipboard from dtactions import autohotkeyactions as ahk -from dtactions.unimacro import unimacroutils +from dtactions import unimacroutils from dtactions.sendkeys import sendkeys try: from dtactions.__init__ import getThisDir, checkDirectory diff --git a/pyproject.toml b/pyproject.toml index 56d6c97..bca7d27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ maintainers = [{name = "Quintijn Hoogenboom", email="q.hoogenboom@antenna.nl"}, dynamic = ["description"] requires-python = ">=3.9" readme = "README.md" -version="1.6.4.dev2" +version="1.6.4.dev3" dependencies= [ "pywin32 >= 300", "debugpy", @@ -69,3 +69,8 @@ python_files = [ ] +[tool.ruff] +include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py","tests/**/*.py"] + +[tool.pyright] +include = ["pyproject.toml", "src/**/*.py", "scripts/**/*.py","tests/**/*.py"] diff --git a/src/dtactions/__init__.py b/src/dtactions/__init__.py index 8418559..a5847a2 100644 --- a/src/dtactions/__init__.py +++ b/src/dtactions/__init__.py @@ -1,43 +1,30 @@ """dtactions __init__ -including utility functions, - - getThisDir, for getting the calling directory of module (in site-packages, also when this is a valid symlink), - - findInSitePackages, supporting getThisDir - - checkDirectory, check existence of directory, with create=True, do create if directory does not exist yet. Note: -as user, having pipped the package, the scripts run from the site-packages directory, no editing in source files is meant to be done - -as developer, you need to clone the package, then `build_package` and, + -as developer, you need to clone the package, after a `pip uninstall dtactions` do: `pip install -e .` from the root of this project. -Start with the following lines near the top of your python file: + +You can retrieve the DtactionsDirectory and DtactionsUserDirectory from here: ``` -try: - from dtactions.__init__ import getThisDir, checkDirectory -except ModuleNotFoundError: - print(f'Run this module after "build_package" and "flit install --symlink"\n') - raise - -thisDir = getThisDir(__file__) # optional: ... , warnings=True) +from dtactions import getDtactionsDirectory, getDtactionsUserDirectory + print(f'dtactions_directory: "{getDtactionsDirectory()}"') + print(f'dtactions_user_directory: "{getDtactionsUserDirectory()}"') + ``` -Also retrieve the DtactionsDirectory and DtactionsUserDirectory from here: +Or with the "Path" versions: ``` -from Dtactionscore.__init__ import getDtactionsDirectory, getDtactionsUserDirectory - -print(f'DtactionsDirectory: {getDtactionsDirectory()}) -print(f'DtactionsUserDirectory: {getDtactionsUserDirectory()}) +from dtactions import getDtactionsPath, getDtactionsUserPath + print(f'dtactions_user_path: "{getDtactionsUserPath()}", (type: "{Type}"') + Type = type(getDtactionsPath()) + print(f'dtactions_path: "{getDtactionsPath()}", (type: "{Type}")') ``` -NOTE: these two directories can also be obtained via Dtactionsstatus: -``` -import natlinkstatus -status = natlinkstatus.DtactionsStatus() -status.getDtactionsDirectory() -status.getDtactionsUserDirectory() -``` """ import importlib.metadata @@ -45,118 +32,54 @@ ##---------------------- -import sys import os from pathlib import Path, WindowsPath +# +# def get_home_path() -> Path: +# """get home path, can be tweaked by pytest testing +# """ +# return WindowsPath.home() -def getDtactionsDirectory(): +def getDtactionsDirectory() -> str: """return the root directory of dtactions """ - return getThisDir(__file__) + return str(Path(__file__).parent) -def getDtactionsUserDirectory(): - """get the NatlinkUserDirectory - - Here are config files, especially .natlink - - By default the users home directory is taken. This directory can be overridden by setting - the environment variable DICTATIONTOOLBOXUSER to an existing directory. - Restart then the calling program - """ - # default dtHome: - dictation_toolbox_user = os.getenv("DICTATIONTOOLBOXUSER") - if dictation_toolbox_user: - dtHome = Path(dictation_toolbox_user) - if not dtHome.is_dir(): - dtHome = WindowsPath.home() - print(f'dtactions.getDtactionsUserDirectory: environment variable DICTATIONTOOLBOXUSER does not point to a valid directory: "{dictation_toolbox_user}", take "{dtHome}"') - else: - dtHome = WindowsPath.home() - - dtactions_ini_folder = dtHome / ".dtactions" - if not dtactions_ini_folder.is_dir(): - dtactions_ini_folder.mkdir() #make it if it doesn't exist - return str(dtactions_ini_folder) - -def getThisDir(fileOfModule, warnings=False): - """get directory of calling module, if possible in site-packages - - call at top of module with `getThisDir(__file__)` - - If you want to get warnings (each one only once, pass `warnings = True`) - - More above and in the explanation of findInSitePackages. +def getDtactionsPath() -> Path: + """return the root directory of dtactions as Path instance """ - thisFile = Path(fileOfModule) - thisDir = thisFile.parent - thisDir = findInSitePackages(thisDir, warnings=warnings) - return thisDir + return Path(__file__).parent -def findInSitePackages(directory, warnings): - """get corresponding directory in site-packages - - For users, just having pipped this package, the "directory" is returned, assuming it is in - the site-packages. - - For developers, the directory is either - --in a clone from github. - The corresponding directory in site-packages should be a symlink, - otherwise there was no "flit install --symlink" yet. - --a directory in the site-packages. This directory should be a symlink to a cloned dir. - - The site-packages directory is returned, but the actual files accessed are in the cloned directory. - - To get this "GOOD" situation, you perform the steps as pointed out above (or in the README.md file) +def getDtactionsUserDirectory() -> str: + """get the UserDirectory for Dtactions - When problems arise, set warnings=True, in the call, preferably when calling getThisDir in the calling module. - """ - dirstr = str(directory) - if dirstr.find('\\src\\') < 0: - if warnings: - warning(f'directory {dirstr} not connected to a github clone directory, changes will not persist across updates...') - return directory - - commonpart = dirstr.rsplit('\\src\\', maxsplit=1)[-1] - spDir = Path(sys.prefix, 'Lib', 'site-packages', commonpart) - if spDir.is_dir(): - spResolve = spDir.resolve() - if spResolve == spDir: - if warnings: - warning(f'corresponding site-packages directory is not a symlink: {spDir}.\nPlease use "flit install --symlink" when you want to test this package') - elif spResolve == directory: - # print(f'directory is symlink: {spDir} and resolves to {directory} all right') - ## return the symbolic link in the site-packages directory, that resolves to directory!! - return spDir - else: - if warnings: - warning(f'directory is symlink: {spDir} but does NOT resolve to {directory}, but to {spResolve}') - else: - if warnings: - warning('findInSitePackages, not a valid directory in site-packages, no "flit install --symlink" yet: {spDir}') - return directory - -def checkDirectory(directory, create=None): - """check existence of directory path - - create == False, None (default): raise OSError if directory is not there + This is by default your-home-directory/.dtactions (so decoupled from the Natlink user directory) + + You can override this by setting environment variable `DTACTIONS_USERDIR` + to a valid directory of your choice - create == True: create if not existent yet... - raise OSError if something strange happens... - returns None """ - if not isinstance(directory, Path): - directory = Path(directory) - if directory.is_dir(): - return - if create is False: - raise OSError(f'Cannot find directory {directory}, but it should be there.') - if directory.exists(): - raise OSError(f'path exists, but is not a directory: {directory}') - directory.mkdir(parents=True) - if directory.is_dir(): - print('created directory: {directory}') - else: - raise OSError(f'did not manage to create directory: {directory}') + # default dtHome: + dta_user_dir = os.getenv("DTACTIONS_USERDIR") + if dta_user_dir: + if Path(dta_user_dir).is_dir(): + return str(dta_user_dir) + print(f'WARNING, dtactions.getDtactionsUserDirectory: environment variable DTACTIONS_USERDIR does not point to a valid directory: "{dta_user_dir}"') + + home_path = WindowsPath.home() + + dtactions_ini_path = home_path/".dtactions" + if not dtactions_ini_path.is_dir(): + dtactions_ini_path.mkdir() #make it if it doesn't exist + if not dtactions_ini_path.is_dir(): + raise IOError(f'dtactions.__init__: dtactions_ini_path cannot be created "{dtactions_ini_path}"' ) + return str(dtactions_ini_path) + +def getDtactionsUserPath() -> Path: + """the "Path" version of getDtactionsUserDirectory + """ + return Path(getDtactionsUserDirectory()) + warningTexts = [] def warning(text): @@ -172,5 +95,12 @@ def warning(text): if __name__ == "__main__": + print(f'dtactions_directory: "{getDtactionsDirectory()}"') print(f'dtactions_user_directory: "{getDtactionsUserDirectory()}"') + + Type = type(getDtactionsPath()) + print(f'dtactions_path: "{getDtactionsPath()}", (type: "{Type}")') + + Type = type(getDtactionsUserPath()) + print(f'dtactions_user_path: "{getDtactionsUserPath()}", (type: "{Type}"') diff --git a/src/dtactions/autohotkeyactions.py b/src/dtactions/autohotkeyactions.py index 9948578..368a8dc 100644 --- a/src/dtactions/autohotkeyactions.py +++ b/src/dtactions/autohotkeyactions.py @@ -30,6 +30,7 @@ See getProgInfo and getCurrentModule below. """ +#pylint:disable = W0107 import subprocess import shutil import filecmp @@ -40,16 +41,11 @@ from textwrap import dedent from dtactions.sendkeys import sendkeys ## get thisDir and other basics... -try: - from dtactions.__init__ import getThisDir, checkDirectory -except ModuleNotFoundError: - print('Run this module after "build_package" and "flit install --symlink"\n') - raise +this_path = Path(__file__).parent - -dtactions = thisDir = getThisDir(__file__) -sampleAhkDirectory = dtactions/'samples'/'autohotkey' -checkDirectory(sampleAhkDirectory) +sampleAhkDirectory = this_path/'samples'/'autohotkey' +# TODO: fix this again, but first unimacroactions! +# checkDirectory(sampleAhkDirectory) ahkexe = None ahkscriptfolder = None @@ -603,7 +599,8 @@ def GetAhkScriptFolder(): return ahkscriptfolder scriptfolder = Path.home()/".autohotkey" - checkDirectory(scriptfolder, create=True) + # TODO: check ahk checkDirectory function + # checkDirectory(scriptfolder, create=True) ahkscriptfolder = scriptfolder copySampleAhkScripts(sampleAhkDirectory, ahkscriptfolder) return scriptfolder diff --git a/src/dtactions/unimacro/ding.wav b/src/dtactions/ding.wav similarity index 100% rename from src/dtactions/unimacro/ding.wav rename to src/dtactions/ding.wav diff --git a/src/dtactions/unimacro/extenvvars.py b/src/dtactions/extenvvars.py similarity index 89% rename from src/dtactions/unimacro/extenvvars.py rename to src/dtactions/extenvvars.py index e037f3f..7a5c6a9 100644 --- a/src/dtactions/unimacro/extenvvars.py +++ b/src/dtactions/extenvvars.py @@ -121,6 +121,39 @@ def getFolderFromLibraryName(self, name): if name in ['LOCAL_APPDATA']: return platformdirs.windows.get_win_folder("CSIDL_LOCAL_APPDATA") + # # General case, try via shellcon! TODO: QH + # try: + # CSIDL_variable = 'CSIDL_%s'% name + # shellnumber = getattr(shellcon,CSIDL_variable, -1) + # print(f'getFolderFromLibraryName, shellnumber of "{CSIDL_variable}": {shellnumber}') + # except: + # print('getExtendedEnv, cannot find CSIDL_variable for: "%s"'% name) + # return '' + # if shellnumber < 0: + # # on some systems have SYSTEMROOT instead of SYSTEM: + # print('getExtendedEnv, cannot find CSIDL_variable for (returns -1): "%s"'% name) + # try: + # csidl_const = shellnumber + # # copied from platformdirs/windows.py: + # buf = ctypes.create_unicode_buffer(1024) + # windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker + # windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + # result = buf.value + # # # result = shell.SHGetFolderPath (0, shellnumber, 0, 0) + # # result = ctypes.windll.shell32.SHGetFolderPathW(0, shellnumber, 0, 0) + # print(f'result from SHGetFolderPathW: {result}') + # except: + # print('getFolderFromLibraryName, cannot path for CSIDL: "%s"'% name) + # return '' + + + ## extra cases: + # if name in ['Quick access', 'Snelle toegang']: + # templatesfolder =self. getExtendedEnv('TEMPLATES') + # if isdir(templatesfolder): + # QuickAccess = normpath(join(templatesfolder, "..", "Libraries")) + # if isdir(QuickAccess): + # return QuickAccess if name.upper() == 'DROPBOX': return self.getDropboxFolder() if name.upper() in ['ONEDRIVE']: @@ -283,7 +316,7 @@ def getExtendedEnv(self, var, noCache=False, displayMessage=True): result = buf.value # # result = shell.SHGetFolderPath (0, shellnumber, 0, 0) # result = ctypes.windll.shell32.SHGetFolderPathW(0, shellnumber, 0, 0) - # print(f'result for "{var}" via SHGetFolderPathW: {result}') + print(f'result from SHGetFolderPathW: {result}') except: if displayMessage: print('getExtendedEnv, cannot find in os.environ or CSIDL: "%s"'% var) @@ -304,12 +337,11 @@ def getDirectoryFromNatlinkstatus(self, envvar): # try if function in natlinkstatus: if not status: return None - envvar_capped = envvar.lower() - for extra in ('', 'directory', 'dir'): - var2 = envvar_capped + extra - funcName = f'get{var2}' - func = getattr(status, funcName, None) - if func: + for extra in ('', 'Directory', 'Dir'): + var2 = envvar + extra + if var2 in status.__dict__: + funcName = f'get{var2}' + func = getattr(status, funcName) result = func() if result: return result diff --git a/src/dtactions/unimacro/inivars.py b/src/dtactions/inivars.py similarity index 99% rename from src/dtactions/unimacro/inivars.py rename to src/dtactions/inivars.py index d352c97..543caf6 100644 --- a/src/dtactions/unimacro/inivars.py +++ b/src/dtactions/inivars.py @@ -10,7 +10,7 @@ from collections import UserDict from pathlib import Path -from dtactions.unimacro import utilsqh +from dtactions import utilsqh from natlinkcore import readwritefile diff --git a/src/dtactions/samples/unimacro/unimacroactions.ini b/src/dtactions/samples/unimacroactions.ini similarity index 97% rename from src/dtactions/samples/unimacro/unimacroactions.ini rename to src/dtactions/samples/unimacroactions.ini index af603a9..364c971 100644 --- a/src/dtactions/samples/unimacro/unimacroactions.ini +++ b/src/dtactions/samples/unimacroactions.ini @@ -31,11 +31,9 @@ txt = notepad [bringup emacs] name = emacs -path = C:\emacs-22.3\bin\runemacs.exe [bringup email] -name = outlook [bringup excel] @@ -46,18 +44,8 @@ name = excel name = iexplore -[bringup ibmrad] -args = -product com.ibm.rational.rad.product.v75.ide -name = javaw -path = C:\Program Files\IBM\SDP\eclipse.exe -title = Java - Rational - - -[bringup idle] - - [bringup internet] -name = firefox +name = chrome [bringup notepad] @@ -72,7 +60,7 @@ title = messages from python macros bat = cmd cmd = cmd txt = notepad - +ini = notepad [bringup powerpoint] name = powerpnt @@ -335,6 +323,17 @@ searchgoforward = {f3} selectioncomment = {ctrl+3} selectionuncomment = {ctrl+shift+3} taskrefresh = {ctrl+w} LW() {shift+ctrl+t} +duplicate = {ctrl+c}{ctrl+v 2} +leftafterbacksearch n = {extleft n} +leftafterforwardsearch n = {extleft n} +leftbeforebacksearch n = {extleft n} +printproperties = SSK {alt+v} +rightafterbacksearch n = {extright}{extleft} +rightafterforwardsearch n = {extright}{extleft} +rightbeforeforwardsearch n = {extright n} +searchback = {ctrl+f};{alt+u};{enter}{esc} +searchfailed = ALERT; {esc} +searchforward = {f3} [minesweeper] @@ -362,20 +361,6 @@ searchgo = {enter}; W; {esc} taskrefresh = <>; PRINT new messages window -[notepad] -duplicate = {ctrl+c}{ctrl+v 2} -filesaveas = {alt+b}l -leftafterbacksearch n = {extleft n} -leftafterforwardsearch n = {extleft n} -leftbeforebacksearch n = {extleft n} -printproperties = SSK {alt+v} -rightafterbacksearch n = {extright}{extleft} -rightafterforwardsearch n = {extright}{extleft} -rightbeforeforwardsearch n = {extright n} -searchback = {ctrl+f};{alt+u};{enter}{esc} -searchfailed = ALERT; {esc} -searchforward = {f3} -searchgo = {enter};{esc} [opera] diff --git a/src/dtactions/unimacro/actionclasses/__init__.py b/src/dtactions/unimacroactionclasses/__init__.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/__init__.py rename to src/dtactions/unimacroactionclasses/__init__.py diff --git a/src/dtactions/unimacro/actionclasses/actionbases.py b/src/dtactions/unimacroactionclasses/actionbases.py similarity index 98% rename from src/dtactions/unimacro/actionclasses/actionbases.py rename to src/dtactions/unimacroactionclasses/actionbases.py index a1c25cb..bc1c6a9 100644 --- a/src/dtactions/unimacro/actionclasses/actionbases.py +++ b/src/dtactions/unimacroactionclasses/actionbases.py @@ -5,10 +5,10 @@ """ #pylint:disable=C0116, W0201 from dtactions import messagefunctions as mess -from dtactions.unimacro import unimacroutils +from dtactions import unimacroutils class AllActions: - """base class for all actions that run in actionclasses + """base class for all actions that run in unimacroactionclasses """ def __init__(self, progInfo): self.reset(progInfo=progInfo) diff --git a/src/dtactions/unimacro/actionclasses/code-actions.py b/src/dtactions/unimacroactionclasses/code-actions.py similarity index 94% rename from src/dtactions/unimacro/actionclasses/code-actions.py rename to src/dtactions/unimacroactionclasses/code-actions.py index 2eed91e..9673bb7 100644 --- a/src/dtactions/unimacro/actionclasses/code-actions.py +++ b/src/dtactions/unimacroactionclasses/code-actions.py @@ -6,8 +6,8 @@ """ import time import natlink -from dtactions.unimacro.actionclasses.actionbases import AllActions -from dtactions.unimacro.unimacroactions import doAction as action +from dtactions.unimacroactionclasses.actionbases import AllActions +from dtactions.unimacroactions import doAction as action from dtactions.sendkeys import sendkeys as keystroke # from dtactions import natlinkclipboard diff --git a/src/dtactions/unimacro/actionclasses/excel-actions.py b/src/dtactions/unimacroactionclasses/excel-actions.py similarity index 99% rename from src/dtactions/unimacro/actionclasses/excel-actions.py rename to src/dtactions/unimacroactionclasses/excel-actions.py index c9cff35..7901247 100644 --- a/src/dtactions/unimacro/actionclasses/excel-actions.py +++ b/src/dtactions/unimacroactionclasses/excel-actions.py @@ -2,8 +2,8 @@ import os.path from pythoncom import com_error import win32com.client -from dtactions.unimacro.actionclasses.actionbases import AllActions -from dtactions.unimacro import unimacroutils +from dtactions.unimacroactionclasses.actionbases import AllActions +from dtactions import unimacroutils #pylint:disable=R0904, C0209, R0912, R0915, W0702, C0321 class ExcelActions(AllActions): diff --git a/src/dtactions/unimacro/actionclasses/frescobaldi-actions.py b/src/dtactions/unimacroactionclasses/frescobaldi-actions.py similarity index 97% rename from src/dtactions/unimacro/actionclasses/frescobaldi-actions.py rename to src/dtactions/unimacroactionclasses/frescobaldi-actions.py index a29dd84..950ca50 100644 --- a/src/dtactions/unimacro/actionclasses/frescobaldi-actions.py +++ b/src/dtactions/unimacroactionclasses/frescobaldi-actions.py @@ -5,8 +5,8 @@ """ #pylint:disable=C0209 import time -from dtactions.unimacro.actionclasses.actionbases import AllActions -from dtactions.unimacro import unimacroutils +from dtactions.unimacroactionclasses.actionbases import AllActions +from dtactions import unimacroutils from dtactions.sendkeys import sendkeys class FrescobaldiActions(AllActions): diff --git a/src/dtactions/unimacro/actionclasses/komodo-actions.py b/src/dtactions/unimacroactionclasses/komodo-actions.py similarity index 97% rename from src/dtactions/unimacro/actionclasses/komodo-actions.py rename to src/dtactions/unimacroactionclasses/komodo-actions.py index 72af127..cf77e10 100644 --- a/src/dtactions/unimacro/actionclasses/komodo-actions.py +++ b/src/dtactions/unimacroactionclasses/komodo-actions.py @@ -8,7 +8,7 @@ import natlink # from dtactions import messagefunctions as mf -from dtactions.unimacro.actionclasses.actionbases import AllActions +from dtactions.unimacroactionclasses.actionbases import AllActions # from dtactions.unimacro.unimacroactions import doAction as action from dtactions.sendkeys import sendkeys as keystroke diff --git a/src/dtactions/unimacro/actionclasses/notreadykomodo-actions.py b/src/dtactions/unimacroactionclasses/notreadykomodo-actions.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/notreadykomodo-actions.py rename to src/dtactions/unimacroactionclasses/notreadykomodo-actions.py diff --git a/src/dtactions/unimacro/actionclasses/pythonwin-actions.py b/src/dtactions/unimacroactionclasses/pythonwin-actions.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/pythonwin-actions.py rename to src/dtactions/unimacroactionclasses/pythonwin-actions.py diff --git a/src/dtactions/unimacro/actionclasses/uedit32-actions.py b/src/dtactions/unimacroactionclasses/uedit32-actions.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/uedit32-actions.py rename to src/dtactions/unimacroactionclasses/uedit32-actions.py diff --git a/src/dtactions/unimacro/actionclasses/uedit64-actions.py b/src/dtactions/unimacroactionclasses/uedit64-actions.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/uedit64-actions.py rename to src/dtactions/unimacroactionclasses/uedit64-actions.py diff --git a/src/dtactions/unimacro/actionclasses/win32pad-actions.py b/src/dtactions/unimacroactionclasses/win32pad-actions.py similarity index 100% rename from src/dtactions/unimacro/actionclasses/win32pad-actions.py rename to src/dtactions/unimacroactionclasses/win32pad-actions.py diff --git a/src/dtactions/unimacro/unimacroactions.py b/src/dtactions/unimacroactions.py similarity index 97% rename from src/dtactions/unimacro/unimacroactions.py rename to src/dtactions/unimacroactions.py index 8123e0d..38d1b49 100644 --- a/src/dtactions/unimacro/unimacroactions.py +++ b/src/dtactions/unimacroactions.py @@ -4,7 +4,7 @@ # #pylint:disable=C0302, C0116, R0913, R0914, R1710, R0911, R0912, R0915, C0321, W0702, W0613, W0602 #pylint:disable=E1101 -#pylint:disable=C0209, R1728 +#pylint:disable=C0209, R1728, R1735 ##TODO: #pylint:disable=W1514, R1732 """This module contains actions that can be called from natlink grammars. @@ -33,18 +33,18 @@ import win32gui import win32con # import win32com.client +import natlink +from natlinkcore import natlinkutils +from natlinkcore.config import expand_path import dtactions from dtactions import monitorfunctions from dtactions.sendkeys import sendkeys, sendsystemkeys # from dtactions import messagefunctions from dtactions import autohotkeyactions # for AutoHotkey support -from dtactions.unimacro import unimacroutils -from dtactions.unimacro import inivars -# from dtactions.unimacro import actionclasses -import natlink -from natlinkcore import natlinkutils -from natlinkcore.config import expand_path +from dtactions import unimacroutils +from dtactions import inivars +# from dtactions import unimacroactionclasses external_actions_modules = {} # the modules, None if not available (for prog) external_action_instances = {} # the instances, None if not available (for hndle) @@ -55,7 +55,7 @@ class KeystrokeError(Exception): "KeystrokeError" pendingMessage = '' -thisDir = dtactions.getThisDir(__file__) +#TODO: rework into path style... dtactionsDir = dtactions.getDtactionsDirectory() dtactionsUserDir = dtactions.getDtactionsUserDirectory() @@ -63,7 +63,7 @@ class KeystrokeError(Exception): # baseDirectory = thisDir # if not baseDirectory: # raise ImportError( 'no baseDirectory found while loading actions.py, stop loading this module') -sampleDirectory = Path(dtactionsDir)/"samples"/"unimacro" +sampleDirectory = Path(dtactionsDir)/"samples" if not sampleDirectory.is_dir(): raise OSError(f'dtactions: no sample directory for unimacroactions.ini Inifile found: {sampleDirectory}"') @@ -72,7 +72,7 @@ class KeystrokeError(Exception): if not sampleInifile.is_file(): raise OSError(f'no sample Inifile for unimacroactions found: "{sampleInifile}"') -userDirectory = Path(dtactionsUserDir)/'unimacro' +userDirectory = Path(dtactionsUserDir) if not userDirectory.is_dir(): userDirectory.mkdir() @@ -686,7 +686,7 @@ def get_external_module(prog): return external_actions_modules[prog] try: modname = '%s-actions'% str(prog) - _temp = __import__('dtactions.unimacro.actionclasses', fromlist=[modname]) + _temp = __import__('dtactions.unimacroactionclasses', fromlist=[modname]) mod = getattr(_temp, modname) external_actions_modules[prog] = mod print('get_external_module, found actions module: %s'% modname) @@ -1683,7 +1683,11 @@ def do_ALERT(alert=1, **kw): except ValueError: nAlert = 1 for _i in range(nAlert): - natlink.execScript('PlaySound "'+thisDir+'\\ding.wav"') + ding_path = Path(dtactionsDir)/"ding.wav" + if not ding_path.is_file(): + raise OSError('sound file "ding.wav" not present in %s'% dtactionsDir) + exec_string = f'PlaySound "{ding_path}"' + natlink.execScript(exec_string) unimacroutils.Wait(0.1) if micState != 'off': natlink.setMicState(micState) @@ -1917,7 +1921,7 @@ def YesNo(t, title=None, icon=32, alert=None, defaultToSecondButton=0, progInfo= 'title: %s\n'% (tt, icon, title)) unimacroutils.Wait(0.1) newMicState = natlink.getMicState() - result = (newMicState == 'sleeping') + result = newMicState == 'sleeping' if newMicState != 'off': break # ok, either on or sleeping # try again (maximum 3 times) unimacroutils.Wait(0.05) @@ -1960,7 +1964,7 @@ def findCursor(): # special: voicecodeApp = 'emacs' -def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, progInfo=None, comingFrom=None): +def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, progInfo=None): """get a running copy of app in the foreground the full path can be set in section [bringup app], key path @@ -1990,6 +1994,7 @@ def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, pr # voicecodeApp ('emacs') and (optional, but in this case) function voicecodeBringUp # if filepath: + filepath = str(filepath) # in case a Path instance is passed app2 = None while app in ['open', 'edit']: if app == app2: break #open = open... @@ -2049,34 +2054,55 @@ def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, pr else: raise OSError('invalid path for app with PROGRAMFILES %s: %s (expanded: %s)'% (app, appPath, appPath2)) else: - raise OSError('invalid path for app %s: %s (expanded: %s)'% (app, appPath, appPath2)) + print('UnimacroBringUp, invalid path for app %s: %s, revert to default (Notepad)'% (app, appPath)) + appPath = 'notepad' + appName = 'notepad' else: appPath = appName or app appArgs = ini.get("bringup %s"% app, "args") or None else: - appPath = None - appArgs = None - appName = None - if appName: - if filepath: - appName = f'{appName} {filepath}' - else: - appName = filepath + appPath = "" + appArgs = "" + appName = "" + + # code to be simplified, but added filename in the appName, so repeated UnimacroBringUps (AppBringUp of Dragon) + # can refind the opened instance... + + filename = '' if filepath: - filepath = f'""{filepath}""' - # print(f'filepath unimacroactions: |{filepath}|') - if appArgs: - #if filepath.find(" ") > 0: - # insert DOUBLE DOUBLE QUOTES for vba line recognition - filepath = '""'+filepath+'""' - print(f'filepath appArgs unimacroactions: |{filepath}|') - - appArgs = f'{appArgs} {filepath}' - print(f'appArgs unimacroactions: |{appArgs}|') - + filename = Path(filepath).name + if filepath.find(' ') > 0 or filepath.find('\\') > 0: + filepath = f'""{filepath}""' + + + appName = appName or '' + if filename: + if appName: + appName += ' ' + filename else: - appArgs = filepath + appName = filename + + if appName and appName.find(' ') > 0: + appName = f'""{appName}""' + + appPath = appPath or '' + + if appPath and appPath.find(' ') > 0: + appPath = f'""{appPath}""' + + if appPath: + if filepath: + appPath = f'{appPath} {filepath}' + else: + appPath = filepath + + # # print(f'filepath unimacroactions: |{filepath}|') + # if appArgs: + # appArgs = f'{appArgs} {filepath}' + # print(f'appArgs unimacroactions: |{appArgs}|') + # else: + # appArgs = filepath # for future: appWindowStyle = ini.get("bringup %s"% app, "style") or None appDirectory = ini.get("bringup %s"% app, "directory") or None @@ -2113,7 +2139,6 @@ def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, pr if debug > 2: D('bringups: %s'% bringups) if debug: D('delete %s from bringups'% app) del bringups[app] - # do_RW() # if app in ('voicecode', 'dragonpad'): # raise UnimacroError("Oops, BRINGUP voicecoder should not come here at all, bringing up: %s"% app) @@ -2136,12 +2161,15 @@ def UnimacroBringUp(app, filepath=None, title=None, extra=None, modInfo=None, pr # #print 'currentModedule: %s'% repr(natlink.getCurrentModule()) ##do_RW() #print 'unimacrobringup: name: %s, app: %s, args: %s (filepath: %s)'% (appName, appPath, appArgs, filepath) - result = unimacroutils.AppBringUp(appName, appPath, appArgs, appWindowStyle, appDirectory) + result = unimacroutils.AppBringUp(App=appName, Exec=appPath, Args=appArgs, + windowStyle=appWindowStyle, directory=appDirectory) + # print("result of UnimacroBringUp:", result) if extra: doAction(extra) return result + # if do_WTC(): # prog, title, _topchild, classname, hndle = unimacroutils.getProgInfo() # progFull, titleFull, hndle = natlink.getCurrentModule() diff --git a/src/dtactions/unimacroobsolete/delete.ini b/src/dtactions/unimacroobsolete/delete.ini new file mode 100644 index 0000000..e69de29 diff --git a/src/dtactions/unimacroobsolete/empty.ini b/src/dtactions/unimacroobsolete/empty.ini new file mode 100644 index 0000000..e69de29 diff --git a/src/dtactions/unimacroobsolete/getdict.ini b/src/dtactions/unimacroobsolete/getdict.ini new file mode 100644 index 0000000..0cca2d7 --- /dev/null +++ b/src/dtactions/unimacroobsolete/getdict.ini @@ -0,0 +1,15 @@ +[s] +dict = a: b +empty = +empty value = empty +list = key: a, b, c +one = value + +three = + keyempty + key1: value 1 + key2, key3: value1, value2 + +two = + key: value 1 + key2 : value 2 diff --git a/src/dtactions/unimacro/newactions.py b/src/dtactions/unimacroobsolete/newactions.py similarity index 99% rename from src/dtactions/unimacro/newactions.py rename to src/dtactions/unimacroobsolete/newactions.py index a9f4344..7bf74eb 100644 --- a/src/dtactions/unimacro/newactions.py +++ b/src/dtactions/unimacroobsolete/newactions.py @@ -39,8 +39,8 @@ from dtactions import monitorfunctions from dtactions import autohotkeyactions from dtactions import sendkeys -from dtactions.unimacro import unimacroutils -from dtactions.unimacro import inivars +from dtactions import unimacroutils +from dtactions import inivars external_actions_modules = {} # the modules, None if not available (for prog) external_action_instances = {} # the instances, None if not available (for hndle) diff --git a/src/dtactions/unimacroobsolete/set.ini b/src/dtactions/unimacroobsolete/set.ini new file mode 100644 index 0000000..dbfcb96 --- /dev/null +++ b/src/dtactions/unimacroobsolete/set.ini @@ -0,0 +1,22 @@ +[empty 1] + +[empty 2] + + +[empty section] + + +[not empty 1] +empty key 1 = +empty key 2 = +key 1 = " value " +key 2 = " value " + + +[quotes] +double = '" a "' +single = "' a '" + + +[section] +key = value diff --git a/src/dtactions/unimacroobsolete/simple.ini b/src/dtactions/unimacroobsolete/simple.ini new file mode 100644 index 0000000..5df13fa --- /dev/null +++ b/src/dtactions/unimacroobsolete/simple.ini @@ -0,0 +1,3 @@ +[s] +k = v +k2 = v2 diff --git a/src/dtactions/unimacro/unimacroutils.py b/src/dtactions/unimacroobsolete/unimacroutils.py similarity index 100% rename from src/dtactions/unimacro/unimacroutils.py rename to src/dtactions/unimacroobsolete/unimacroutils.py diff --git a/src/dtactions/unimacro/usc_functions.py b/src/dtactions/unimacroobsolete/usc_functions.py similarity index 100% rename from src/dtactions/unimacro/usc_functions.py rename to src/dtactions/unimacroobsolete/usc_functions.py diff --git a/src/dtactions/unimacroutils.py b/src/dtactions/unimacroutils.py new file mode 100644 index 0000000..c898352 --- /dev/null +++ b/src/dtactions/unimacroutils.py @@ -0,0 +1,1895 @@ +# +# unimacroutils.py (in dtactions/unimacro) +# was module natlinkutilsqh.py in unimacro +# +# Quintijn Hoogenboom +# February 2000/.../March 2021/July 2021 +# +# +"""a set of utility functions in dtactions/unimacro + +Previous this was natlinkutilsqh.py in unimacro + +""" +#pylint:disable=C0302, C0116, C0321, R0912, R0913, R0914, R0915, W0613, C0209, W0602, W0621, R1715, W0702 +#pylint:disable=E1101, R1735 +import time +import re +import os +import os.path +import sys +import collections +from pathlib import Path +import win32gui +import win32clipboard +import win32con +# import pywintypes + +import natlink +from natlinkcore import natlinkutils +from natlinkcore import natlinkstatus +from dtactions import monitorfunctions # elaborated version QH +from dtactions import autohotkeyactions + +status = natlinkstatus.NatlinkStatus() + +DEBUG = 0 +# errors for (mainly) the command grammar: + +class NatlinkCommandError(Exception): + """NatlinkCommandError""" +class NatlinkCommandTimeOut(Exception): + """NatlinkCommandTimeOut""" +class UnimacroError(Exception): + """UnimacroError""" + +# default Waiting Times: +defaultWaitingTime = 0.1 # 100 milliseconds +visibleWaitFactor = 3 # default for Wait and W +longWaitFactor = 10 # times 3 for visible wait +shortWaitFactor = 0.3 # times 10 for long wait + # times 0.3 for short wait +debugMode = 0 #-1 + +pendingExecScripts = [] + +ProgInfo = collections.namedtuple('ProgInfo', 'progpath prog title toporchild classname hndle'.split(' ')) + + + +# class CaseInsensitiveSet(set): +# """makes the comparison for adding new elements case insensitive +# Adding takes the capitalisation of the first item added +# """ +# def __hash__(self): +# return hash(self.lower()) +# def __eq__(self, other): +# return self.lower() == other.lower() + +def getLanguage(): + """get current language, 3 characters + """ + return status.get_language() + +def getUserLanguage(): + """get current language, long name + + """ + raise NotImplementedError('getUserLanguage (the long language name) is no longer available, use natlinkstatus function "get_language"') + +# utility functions---------------------------------------- +## matchWindow from natlinkutils: +##def matchWindow(moduleInfo, modName, wndText): +## if len(moduleInfo)<3 or not moduleInfo[0]: return None +## curName = getBaseName(moduleInfo[0].lower() ) +## if curName != modName: return None +## if -1 == moduleInfo[1].find(wndText): return None +## return moduleInfo[2] + +# A utility function which determines whether modInfo matches a specified +# module name. +# If modInfo is not given, it is got here. +# +# Returns module name (program name!) on match and None on mismatch. +# This is a variant on matchWindow (from Joel), but now you can also check only the module, +# and not the (sub) window of this module. +# Also you can specify a window title or a list/tuple of window titles. +# If you specify a window, it can be checked with exact title or exact case. +# If nothing is specified lower case strings are compared, and only part +# of the window title has to be given. +def getCurrentModuleSafe(nWait=5, waitingTime=0.01): + """in case natlink.getCurrentModule returns None, try again a few times... + default 5 x 0.01 = 0.05 (50 milliseconds) + + returns the tuple: + (full file name of foreground program, window title, window handle) + """ + modInfo = natlink.getCurrentModule() + if modInfo: + return modInfo + for i in range(nWait): + time.sleep(waitingTime) + modInfo = natlink.getCurrentModule() + waited = (i+1)*waitingTime + if modInfo: + print(f'getCurrentModuleSafe, found modInfo after {waited} seconds') + return modInfo + + print(f'getCurrentModuleSafe, could not find modInfo after {waited} seconds') + return None + +def matchModule(modName, wantedTitle=None, progInfo=None, titleExact=0, caseExact=0): + """return modName if title matches, otherwise None + """ + if not progInfo: + progInfo = getProgInfo() + if not progInfo.hndle: + return None + modName = progInfo.prog +## print 'modName: %s, basename module: %s'% (modName, getBaseName(modInfo[0])) + if not wantedTitle: + return modName + + winTitle = progInfo.title + if isinstance(wantedTitle, str): + if not caseExact: + wantedTitle = wantedTitle.lower() + winTitle = winTitle.lower() + if titleExact: + if winTitle == wantedTitle: + return modName + else: + if winTitle.find(wantedTitle) >= 0: + return modName + elif isinstance(wantedTitle, (list, tuple)): + for t in wantedTitle: + if matchModule(modName, t, progInfo=progInfo, titleExact=titleExact, caseExact=caseExact): + return modName + return None + +# Matches if only the window title ok. If modInfo is not given, it is extracted here. +# +# The wantedTitle can also be a list or tuple, in that case the routine is called +# recursive. +# titleExact and caseExact can be given. If not strings are converted to lower case +# and a part of the window title may be given. + +def matchTitle(wantedTitle, modInfo=None, titleExact=0, caseExact=0): + """return progName if title matches, otherwise None + """ + #pylint:disable=R0911 + if modInfo is None: + modInfo = natlink.getCurrentModule() + if not (modInfo[0] and len(modInfo)) == 3: + return None + winTitle = modInfo[1] + progName = getProgName(modInfo) + + if isinstance(wantedTitle, str): + if not caseExact: + wantedTitle = wantedTitle.lower() + winTitle = winTitle.lower() + if titleExact: + if winTitle == wantedTitle: + return progName + return False + if winTitle.find(wantedTitle) >= 0: + return progName + return False + if isinstance(wantedTitle, (list, tuple)): + for t in wantedTitle: + if matchTitle(t, modInfo, titleExact, caseExact): + return progName + return False + print("unexpected place for matchTitle (unimacroutils)") + return False + +# Get basename of file: +def getBaseNameLower(name): + """old fashioned way of getting the filename + """ + return os.path.splitext(os.path.split(name)[1])[0].lower() + + +def getProgName(modInfo=None): + """return the name of program (in the foreground) + """ + if not modInfo: + modInfo = natlink.getCurrentModule() + return modInfo[0] + +# ProgInfo = collections.namedtuple('ProgInfo', 'progpath prog title toporchild classname hndle'.split(' ')) + +def getProgInfo(modInfo=None): + """returns program info as namedtuple (progpath, prog, title, toporchild, classname, hndle) + + + now length 6, including the classname, but also a named tuple!! + + prog always lowercase + + title now with mixed (lower and upper case) letters. + + toporchild 'top' or 'child', or '' if no valid window + """ + #pylint:disable=W0702 + if modInfo is None: + try: + modInfo = natlink.getCurrentModule() + # print("modInfo through natlink: %s"% repr(modInfo)) + except natlink.NatError: + progInfo = autohotkeyactions.getProgInfo() + return progInfo + + _hndle = modInfo[2] + if not _hndle: + ## assume desktop, no foreground window, treat as top... + return ProgInfo("", "", "", "top", "", 0) + progpath = modInfo[0] + prog = Path(modInfo[0].lower()).stem + title = modInfo[1] + if isTopWindow(modInfo[2]): + toporchild = 'top' + else: + toporchild = 'child' + + HNDLE = int(modInfo[2]) + + classname = win32gui.GetClassName(HNDLE) + + return ProgInfo(progpath, prog, title, toporchild, classname, HNDLE) + +def getClassName(modInfo=None): + """returns the class name of the foreground window + take modInfo (tuple or int (the handle), or get it here) + """ + if isinstance(modInfo, int): + _hndle = modInfo + elif isinstance(modInfo, tuple): + _hndle = modInfo[2] + else: + _hndle = natlink.getCurrentModule()[2] + if _hndle: + return win32gui.GetClassName(_hndle) + return None + +def matchWindow(criteria, modInfo=None, progInfo=None): + """looks for a matching window based on the dictionary of criteria + + criteria can either be a key with None as value, or a key + with a part of window title as value (string), or a key with + a list of parts of window titles as value. + + special these are: + 'all' (everything matches, value is ignored, so normally None) + 'none' (nothing matches) + 'empty' (matches when no valid progInfo is found) + + progInfo is a tuple: (prog, title, topchild, hndle), + prog being the lower case name of the programme + title being the lower case converted title + topchild being 'top' if top window, 'child' if child window, + if no valid module info + + progInfo may be omitted as well as modInfo. + For best performance progInfo should be given, + otherwise giving mtake unimacro couldodInfo is also faster than omitting it. + + """ + #pylint:disable=R0911, R0912 + #print 'matchwindow(qh): %s'% criteria + if not isinstance(criteria, dict): + print(f'type criteria in matchWindow function should be dict, not: {criteria}') + return None + + if 'all' in criteria: + return 1 + if 'none' in criteria: + return None + + _progpath, prog, title, topchild, _classname, _hndle = progInfo or getProgInfo(modInfo) + if 'empty' in criteria and prog == '': + return 1 + if 'top' in criteria and topchild != 'top': + return None + if 'child' in criteria and topchild != 'child': + return None + if prog in criteria: + pot = criteria[prog] # part of title + if not pot: + return 1 # no title given, so all titles match + if isinstance(pot, str): + return title.find(pot) >= 0 # one part of title given, check this + if isinstance(pot, list): + for t in pot: + if title.find(t) >= 0: # more possibilities for part of title + return 1 + elif 'top' in criteria or 'child' in criteria: + return 1 + return None + +## bringup functionality (moved from natlinkutilsbj to here): +pendingBringUps = [] +pendingExecScripts = [] + +def doPendingBringUps(): + """called from _control""" + #pylint:disable=W0603, W0702 + global pendingBringUps + if not pendingBringUps: + return + for p in pendingBringUps: + try: + #print 'try to do pending bringup: %s'% p + natlink.execScript(p) + except: + print('delayed bringup does not work: %s'% p) + pendingBringUps = [] + + +def doPendingExecScripts(): + """do pending execScripts, if there are any (from gotBegin or initialisation phase) + """ + #pylint:disable=W0603, W0702 + global pendingExecScripts + if not pendingExecScripts: + return + for p in pendingExecScripts: + try: + #print 'try to do pending execScript: %s'% p + natlink.execScript(p) + except: + print('does not work (in doPendingExecScripts): %s'% p) + pendingExecScripts = [] + +def ExecScript(script, callingFrom=None): + """dall execScript, and put in pending list if fails (from gotBegin or initialisation phase) + """ + #pylint:disable=W0603 + global pendingExecScripts + if callingFrom and (getattr(callingFrom, 'status', '') == 'new' or + getattr(callingFrom, 'inGotBegin', 0)): + print(f'pending execScript for {callingFrom.getName()}') + pendingExecScripts.append(script) + return None + + try: + natlink.execScript(script) + except natlink.NatError as t: + print('error in execScript: %s'% script) + print('message: %s'% t) + print('-------') + return None + return 1 + +def AppBringUp(App, Exec=None, Args=None, windowStyle=None, directory=None): ##, callingFrom=None): + """central BringUp function, can maintain old bringups + BJ, extended QH. + can be called from actions (UnimacroBringUp, or (do_)BRINGUP. + + """ + #pylint:disable=W0603, R0913, R0912 + app = str(App).lower() + if app == 'notepad': + app = r'C:\Windows\notepad.exe' + assert os.path.isfile(app) + if Args: + if isinstance(Args, list): + args=' '.join(Args) + else: + args = Args + else: + args = None + +## if (GetOS()!='windows_nt'): +## app=App.lower() +## else: +## app=App + if not Exec: + if args: + cmdline = 'AppBringUp "%s", "%s"'% (app, args) + else: + raise UnimacroError("AppBringup should have at least Exec or Args (now: %s, %s)"% + (Exec, Args)) + else: + ExecString = Exec + # if os.path.isfile(Exec): + # if AppBringUpDoExec83: + # (fullName,ext)=os.path.splitext(Exec) + # if ext=='': + # Exec83=os.path.dirname(Exec)+'\\'+os.path.basename(Exec) + # else: + # Exec83=Exec + # ExecString = Exec83 + + if args: + # komodo, notepad (??) + ExecString += " %s"% args + cmdline = 'AppBringUp "%s","%s"'% (app, ExecString) + + if windowStyle: + cmdline += ',%s'% windowStyle + else: + cmdline += "," + if directory: + cmdline += ', "%s"'% directory + + cmdline = cmdline.strip(", ") + #print "AppBringUp: %s"% cmdline + + # if callingFrom and (getattr(callingFrom, 'status', '') == 'new' or + # getattr(callingFrom, 'inGotBegin', 0)): + # #print 'pending AppBringUp for %s, %s'% (callingFrom.getName(), script) + # pendingBringUps.append(cmdline) + # return None + try: + ## this is a tricky thing, execScript only recognises str, not unicode! + ## as default, ascii or cp1252 or latin-1 is taken, the windows defaults. + _result = natlink.execScript(cmdline) + except natlink.NatError as _t: + #print 'wait for bringup until later: %s'% cmdline + pendingBringUps.append(cmdline) + return None +## print 'ready withAppBringUp' + return 1 + +# word formatting parameters: +wf_WordWasAddedByTheUser = 0x00000001 +wf_InternalUseOnly1 = 0x00000002 +wf_InternalUseOnly2 = 0x00000004 +wf_WordCanNotBeDeleted = 0x00000008 +wf_NormallyCapitalizeTheNextWord = 0x00000010 +wf_AlwaysCapitalizeTheNextWord = 0x00000020 +wf_UppercaseTheNextWord = 0x00000040 +wf_LowercaseTheNextWord = 0x00000080 +wf_NoSpaceFollowingThisWord = 0x00000100 +wf_TwoSpacesFollowingThisWord = 0x00000200 +wf_NoSpacesBetweenWordsWithThisFlagSet = 0x00000400 +wf_TurnCapitalizationModeOn = 0x00000800 +wf_TurnUppercaseModeOn = 0x00001000 +wf_TurnLowercaseModeOn = 0x00002000 +wf_TurnOffSpacingBetweenWords = 0x00004000 +wf_RestoreNormalSpacing = 0x00008000 +wf_InternalUseOnly3 = 0x00010000 +wf_SuppressAfterAWordWhichEndsInAPeriod = 0x00020000 +wf_DoNotApplyFormattingToThisWord = 0x00040000 +wf_DoNotResetTheSpacingState = 0x00080000 +wf_DoNotResetTheCapitalizationState = 0x00100000 +wf_NoSpacePreceedingThisWord = 0x00200000 +wf_RestoreNormalCapitalization = 0x00400000 +wf_FollowThisWordWithOneNewLineCharacters = 0x00800000 +wf_FollowThisWordWithTwoNewLineCharacters = 0x01000000 +wf_DoNotCapitalizeThisWordInATitle = 0x02000000 +wf_InternalUseOnly = 0x04000000 +wf_AddAnExtraSpaceFollowingThisWord = 0x08000000 +wf_InternalUseOnly4 = 0x10000000 +wf_InternalUseOnly5 = 0x20000000 +wf_WordWasAddedByTheVocabularyBuilder = 0x40000000 + +##wf_AddedInVersion8 = 0x20000000 +##wf_DroppedInVersion9 = 0x60000000 ^ 0xffffffff +##print 'dropped: %x'% wf_DroppedInVersion9 +# List of word formatting properties as can be handled in +# getWordInfo and setWordInfo +wordFormatting = { + 'WordWasAddedByTheUser': 0x00000001, + 'InternalUseOnly1': 0x00000002, + 'InternalUseOnly2': 0x00000004, + 'WordCanNotBeDeleted': 0x00000008, + 'NormallyCapitalizeTheNextWord': 0x00000010, + 'AlwaysCapitalizeTheNextWord': 0x00000020, + 'UppercaseTheNextWord': 0x00000040, + 'LowercaseTheNextWord': 0x00000080, + 'NoSpaceFollowingThisWord': 0x00000100, + 'TwoSpacesFollowingThisWord': 0x00000200, + 'NoSpacesBetweenWordsWithThisFlagSet': 0x00000400, + 'TurnCapitalizationModeOn': 0x00000800, + 'TurnUppercaseModeOn': 0x00001000, + 'TurnLowercaseModeOn': 0x00002000, + 'TurnOffSpacingBetweenWords': 0x00004000, + 'RestoreNormalSpacing': 0x00008000, + 'InternalUseOnly3': 0x00010000, + 'SuppressAfterAWordWhichEndsInAPeriod': 0x00020000, + 'DoNotApplyFormattingToThisWord': 0x00040000, + 'DoNotResetTheSpacingState': 0x00080000, + 'DoNotResetTheCapitalizationState': 0x00100000, + 'NoSpacePreceedingThisWord': 0x00200000, + 'RestoreNormalCapitalization': 0x00400000, + 'FollowThisWordWithOneNewLineCharacters': 0x00800000, + 'FollowThisWordWithTwoNewLineCharacters': 0x01000000, + 'DoNotCapitalizeThisWordInATitle': 0x02000000, + 'InternalUseOnly': 0x04000000, + 'AddAnExtraSpaceFollowingThisWord': 0x08000000, + 'InternalUseOnly4': 0x10000000, + 'InternalUseOnly5': 0x20000000, + 'WordWasAddedByTheVocabularyBuilder': 0x40000000, + + } + +def ListOfProperties(props): + """return a list of word properties + """ + l = [] + keyList = list(wordFormatting.keys()) + keyList.sort() + for k in keyList: + if props & wordFormatting[k]: + l.append(k) + return l + + +def makeWordProperties(listOfProps): + """return the number that is the adding of the props + + in version 8 if word was added by user, add special to it + in version 9 drop 0x60000000 from any props number + """ + props = sys.maxsize + for l in listOfProps: + props += wordFormatting[l] + props = sys.maxsize & props + +# add to string 't' the count 'n' +# eg '{PgUp}', '3' --> '{PgUp 3}', +# 'abc', 3 --> 'abccc' +# 'abc', '16' --> 'ab{c 16}' +def doCount(t,n): + """add to string 't' the count 'n' + + eg '{PgUp}', '3' --> '{PgUp 3}', + 'abc', 3 --> 'abccc' + 'abc', '16' --> 'ab{c 16}' + """ + if len(t) == 0: + return t + if isinstance(n, str): + n = int(n) + if n <= 1: + return t + # do the work with braces:a} + if hasBraces(t): + if len(t) > 2 and not hasBraces(t[1:-1]): + # balancing braces { }: + return "%s %s}"% (t[:-1], n) + return t * n + # only last char repeated: + return t[:-1] + t[-1] * n + +def hasBraces(t): + """return True if t has braces + """ + if not (t and isinstance(t, str)): + return None + first = '{' in t or '}' in t + if first: + if '{' in t[1:]: + return None # more than one character, treat as no braces + return first + +def doModifier(t,mod): + """add Shift, Ctrl or Alt to the string + 'a' wordt '{Ctrl+Shift+a}' + '{Left} wordt '{Ctrl+Left}' + """ + if len(t) == 0: + return t + if t.lower().find(mod.lower()) >= 0: + return t # duplicate modifier? + if t[0] == '{' and t[-1] == '}': + return '{%s%s'% (mod, t[1:]) + return '{%s%s}'% (mod, t) + +def doCaps(t): + """make t uppercase if caps > 0 + + strange old function + """ + #pylint:disable=C0200 + for i in range(len(t)): + if t[i].islower(): + t = t[:i] + t[i].capitalize() + t[i+1:] + return t + +# mouseState to be altered when a downstate is asked for. Must be released before other actions +mouseState = 0 # button that is down (left = 1, right=2) +mouseModifier = 0 # could also be 'ctrl' or 'shift'(to do!) +# mouseStartPosition set by RM (rememberMouse()), returned to by CancelMouse() +mouseStartPosition = () # x, y + +# mouse actions, called by MP and RMP from _commands.py) +# mouse = 'left' is most common (and default) input. If mouse = 0, '' of None +# no clicking is done, if the mouse state = 0 +# if the mouseState = 1 (left) or 2 (right) dragging will continue. +# +# nClick = 1 is the normal input, if nClick = -1, the button is pushed down, +# if nClick = 0, the button is released + +# +# for going to Joels routine, double work: +buttons = dict(noclick=0, left=1, right=2, middle=4) +joelsButtons = ['noclick', 'left', 'right', 'middle'] +##print 'buttons: ', buttons + +mouseDown = ['', natlinkutils.wm_lbuttondown, natlinkutils.wm_rbuttondown, natlinkutils.wm_mbuttondown] +mouseUp = ['', natlinkutils.wm_lbuttonup, natlinkutils.wm_rbuttonup, natlinkutils.wm_mbuttonup] + +def doMouse(absorrel, screenorwindow, xpos, ypos, mouse='left', nClick=1, modifier = 0): + """complicated, but complete mouse routine + + - absorrel: 0 = absolute positions (in pixels), 1 = relative positions (between -1.0 and 1.0) + - screenorwindow: where to take the position from: + 0 = complete screen + 1 = active window + 2 = relative to the current position + ## new 2017, relative to current monitor + 3 = relative to current monitor + 4 = relative to current monitor work area (excluding eg task bar and Dragon bar) + 5 = relative to the client area of or window (eg the body of an e-mail) + - xpos, ypos: relative or absolute position horizontal and vertical + - mouse: the button to be clicked: 'left', 'right', 1, 2, or 0 if no button is clicked + - nClick: 1 or 2 OR -1 if the button has to be pushed down. 0 if the button must be released. + + + -combined values for mouse for vocola MP and RMP calls: + leftdouble, rightdouble, middledouble, leftrelease, rightrelease, middlerelease, + leftup, rightup, middleup (same as release), and click (identical with left) and + -also doubleclickleft or doubleleft etc. + - noclick or move + -abbreviations: down for leftdown, up for leftup release for leftrelease. + + Pushing down buttons down must be remembered, and reversed before + other actions are performed. + Therefore the mouse state is remembered in the variable mouseState. + + 2024: because of Dragon 16, with playEvents not working any more, this function restricts to + movements and clicking, not dragging any more. + + + """ + #pylint:disable=W0603 + global mouseState + hndle = natlink.getCurrentModule()[2] + rect = None + if hndle: + rect = win32gui.GetWindowRect(hndle) + xold,yold = natlink.getCursorPos() + # screenorwindow corrensponds with relativeTo in DNS + # only 0 (whole screen), 1 (relative to window), 2 (relative to cursorPos) + # are considered here 5, inside client area + if screenorwindow not in [0,1,2,3,4,5]: # only within current window + print("doMouse, only screenorwindow 0 ... 5 valid:", screenorwindow) + return + + if screenorwindow == 0: + _width, _height, xMin, yMin, xMax, yMax = monitorfunctions.getScreenRectData() + elif screenorwindow == 1: + if hndle: + rect = win32gui.GetWindowRect(hndle) + else: + print("doMouse, no valid foreground window") + return + _width, _height, xMin, yMin, xMax, yMax = getRectData(rect) + elif screenorwindow == 3: + # get current monitor + rect = monitorfunctions.get_current_monitor_rect( (xold, yold) ) + _width, _height, xMin, yMin, xMax, yMax = getRectData(rect) + elif screenorwindow == 4: + # get current monitor work area (skipping task bar and Dragon bar) + rect = monitorfunctions.get_current_monitor_rect_work( (xold, yold) ) + _width, _height, xMin, yMin, xMax, yMax = getRectData(rect) + # print 'w: %s, h: %s, xMin: %s, yMin: %s, xMax: %s, yMax: %s'% ( + # width, height, xMin, yMin, xMax, yMax) + elif screenorwindow == 5: + if hndle: + ## TODOQH??? + _clxold, _clyold = win32gui.ScreenToClient(hndle, (xold, yold) ) + rect = win32gui.GetClientRect(hndle) + else: + print("doMouse, no valid foreground window") + return + # active window + _width, _height, xMin, yMin, xMax, yMax = getRectData(rect) + + if screenorwindow == 2: # relative to current position + xnew = xold + xpos + ynew = yold + ypos + xp, yp = monitorfunctions.get_closest_position( (xnew, ynew) ) + elif screenorwindow in (0, 1, 3, 4, 5): + if absorrel == 1: # relative: + xp, yp = relToCoord(xpos, xMin, xMax), relToCoord(ypos, yMin, yMax) + else: + xp, yp = xpos, ypos # get rid of: getMouseAbsolute(ypos, yMin, yMax) + xp, yp = checkMousePosition(xp,xMin,xMax), checkMousePosition(yp,yMin,yMax) + if screenorwindow == 5: + clPos = (xp, yp) + xp, yp = win32gui.ClientToScreen(hndle, clPos) + # print 'before get_closest_position: (%s, %s)'% (xp, yp) + xp,yp = monitorfunctions.get_closest_position( (xp, yp) ) + # print 'after get_closest_position: (%s, %s)'% (xp, yp) + + if debugMode == -1: + print('Mouse to: %s, %s' % (xp, yp)) + nclick = 1 + onlyMove = 0 + if mouse and isinstance(mouse, str): + # special variables for vocola combined calls: + if mouse in ("noclick", "0"): + nclick = 0 + mouse = "" + elif mouse == 'move': + onlyMove = 1 + nclick = 0 + mouse = "" + else: + # get the click and button from text: + mouse, nclick = catchClick(mouse) + if not nclick is None: + nClick = nclick + if not mouse: + mouse = 'left' + if mouse: + if isinstance(mouse, str): + if mouse not in buttons: + print('doMouse warning: invalid value for mouse: %s (taking left button)'% mouse) + btn = buttons.get(mouse, 1) + else: + btn = mouse + else: + #print 'no click, mouseState: %s'% mouseState + btn = 0 + nclick = 0 + #nClick = nClick or nclick # take things from "mouse" if nClick not used + print('btn: %s, nClick: %s, current mouseState: %s'% (btn, nClick, mouseState)) + if onlyMove: + print('onlyMove to %s, %x'% (xp, yp)) + set_mouse_position(xp, yp) # absolute + elif not mouseState: # ongecompliceerd + if nClick > 0: + set_mouse_position(xp, yp) + # natlink.playEvents([(natlinkutils.wm_mousemove, xp, yp)]) + if btn: + if debugMode == -1: + print('ButtonClick %s, %s' % (btn, nClick)) + else: + ## Wait() # before clicking! + buttonClick(btn, nClick) +## for i in range(nClick): +## print 'click' +## natlink.playEvents([(mouseDown[btn], xp, yp)]) +## natlink.playEvents([(mouseUp[btn], xp, yp)]) +## Wait(0.01) + + elif nClick == 0: + set_mouse_position(xp, yp) + # natlink.playEvents([(natlinkutils.wm_mousemove, xp, yp)]) + elif nClick == -1: + if btn: + if (xold,yold) != (xp, yp): + print('mousedown at old: %s, %s (%s)'% (xold, yold, repr(mouseDown[btn]))) + natlink.playEvents([(mouseDown[btn], xold, yold)]) + + ## Wait() # before clicking! + print('mousedown at new: %s, %s (%s)'% (xp, yp, repr(mouseDown[btn]))) + natlink.playEvents([(mouseDown[btn], xp, yp)]) + mouseState = btn + elif btn: # muis was omlaag! + #print 'btn: %s, wanted mouseState: %s'% (btn, mouseState) + if mouseState != btn or nClick > 0: # change button + if (xold,yold) != (xp, yp): + natlink.playEvents([(mouseDown[mouseState], xold, yold)]) + natlink.playEvents([(mouseDown[mouseState], xp, yp)]) + +## Wait() # before clicking! + natlink.playEvents([(mouseUp[mouseState], xp, yp)]) + mouseState = 0 + doMouse(0, 0, xp, yp, btn, nClick) + # end or continue dragging state: + if (xold,yold) != (xp, yp): + natlink.playEvents([(mouseDown[btn], xold, yold)]) + natlink.playEvents([(mouseDown[btn], xp, yp)]) + if nClick == -1: + pass + else: + natlink.playEvents([(mouseUp[btn], xp, yp)]) + mouseState = 0 + else: + # no btn, but mouseState, simply move: + set_mouse_position(xp, yp) + # natlink.playEvents([(natlinkutils.wm_mousemove, xp, yp)]) + +def catchClick(mouse): + """return reduced mouse command and nClick + + if mouse starts or ends with "doubleclick" or "double", nClick=2 + if mouse starts or ends with "click" , nClick=2 + if mouse starts or ends with "down", nClick=-1 + if mouse starts or endswith "up" or "release", nClick=0 + """ + for clicker, result in [("doubleclick", 2), ("double", 2), + ("click", 1), ("down", -1), + ("up", 0), ("release", 0)]: + if mouse.startswith(clicker) or mouse.endswith(clicker): + mouse = mouse.replace(clicker, "") + return mouse, result + # fall through: + return mouse, None + +##buttons = makedict(left=1, right=2, middle=4) +##joelsButtons = ['', 'left', 'right', 'middle'] +def buttonClick(button='left', nclick=1, modifiers=None): + """do a natspeak buttonclick, but release mouse first if necessary + """ + # make button numeric: + buttons = {'left':1, 'right':2, 'middle':4} + if button in buttons: + button = buttons[button] + if button not in [1, 2, 4]: + raise UnimacroError('buttonClick invalid button: %s'% button) + if nclick not in [1,2]: + raise UnimacroError('buttonClick invalid number of clicks: %s'% nclick) + if mouseState: + releaseMouse() + + # natlinkutils.buttonClick(button, nclick, modifiers) + if button == 1 and nclick == 1: + script = 'ButtonClick' + else: + script = f'ButtonClick {button},{nclick}' + # print(f'buttonClick via execScript: "{script}"') + natlink.execScript(script) + + + # script = f'ButtonClick {button},{nclick}' + # # print(f'buttonClick via execScript: "{script}"') + # natlink.execScript(script) + +def set_mouse_position(xp, yp): + """set to absolute position + + when Dragon is running, with playEvents for version <= 15, + via execScript if version >= 16 + + when Dragon is NOT running, try via Autohotkey (to be done) + """ + xp, yp = int(xp), int(yp) + if natlink.isNatSpeakRunning(): + if natlink.getDNSVersion() <= 15: + natlink.playEvents( [(natlinkutils.wm_mousemove, xp, yp)]) + else: + script = f'SetMousePosition 1, {xp},{yp}' + print(f'set_mouse_position via execScript: "{script}"') + natlink.execScript(script) + else: + print('future: attempt set_mouse_position via autohotkey') + +def releaseMouse(): + """restores the default mouseState + """ + #pylint:disable=W0603 + global mouseState + if mouseState: + (xp,yp) = natlink.getCursorPos() + print('releasing mouse at %s, %s (%s)'% (xp, yp, repr(mouseUp[mouseState]))) + natlink.playEvents([(mouseUp[mouseState], xp, yp)]) + mouseState = 0 + Wait() + +def mousePushDown(mouse='left'): + """only pushes the mouse down + + """ + if mouseState: + endMouse() +## print 'pushing down with %s'% mouse + doMouse(0, 2, 0, 0, mouse, -1) # abs, rel to window, x, y, click + +def endMouse(): + """replaced by releaseMouse""" + releaseMouse() + +def rememberMouse(): + #pylint:disable=W0603 + global mouseStartPosition + mouseStartPosition = natlink.getCursorPos() + +def cancelMouse(): + #pylint:disable=W0603 + global mouseStartPosition + if not mouseStartPosition: + endMouse() + print('cancelMouse, no mouseStartPosition') + return + if mouseState: + doMouse(0, 0, mouseStartPosition[0], mouseStartPosition[1], + mouseState, 0) + else: + doMouse(0, 0, mouseStartPosition[0], mouseStartPosition[1], + 0, 0) + mouseStartPosition = () + +def checkMousePosition(pos, Min, Max): + #print 'checking:', pos, Min, Max + if pos >= Max: return Max - 1 + if pos < Min: return Min + return int(pos) + +def getRectData(rect): + """get from a rect the width, height, xMin, xMax, yMin, yMax + + example: see unittestMouse.py (in unimacro_test directory) + """ + xMin, yMin, xMax, yMax = tuple(rect) + width, height = rect[2] - rect[0], rect[3] - rect[1] + return width, height, xMin, yMin, xMax, yMax + +def coordToRel(x, xMin, xMax, side=0): + """calculate relative coordinate, must be between 0 and 1 + side == 0: calculate from xMin (result positive) (default) + side == 1: calculate from xMax (result negative) + + (can be used for x and y coordinates) + + example: see unittestMouse.py (in unimacro_test directory) + """ + if not xMin <= x < xMax: + return None # None means no result + width = xMax - xMin + if side == 0: + return float(x-xMin)/width + return float(x-xMax)/width + + +def relToCoord(relValue, xMin, xMax): + """calculate coordinate, with relative value given + + if relValue >= 0 calculate from xMin + if relValue < 0 calculate from xMax + + (can be used for x and y coordinates) + + example: see unittestMouse.py (in unimacro_test directory) + """ + width = xMax - xMin + if relValue >= 0: + return int(xMin + float(relValue) * width + 0.5) + return int(xMax + float(relValue) * width + 0.5) + +#def getMouseRelative(rel, range, Min, Max): +# """newer function, range is calculated inside function +# """ +# return relToCoord(rel, Min, Max) + +# def getMouseAbsolute(Pos, Min, Max): +# """strange function was meant for positive Min and Max +# +# obsolete QH 2015 +# +# with multiple screens it sucks +# """ +# +# if Pos < Min: +# return Min +# elif Pos > Max: +# return Max +# return Pos + +cornerDict = {0:"top left", 1:"top right", 2:"bottom left", 3:"bottom right"} +whichDict = {0: "whole screen", 1:"active window", 3:"active monitor", 5:"client area"} +absorrelDict = {0: "absolute", 1:"relative"} + +def getMousePositionActionString(absorrel, which, position): + """return the proper action line (MP or RMP) + parameter as in getMousePosition below + + if invalid, print lines in Messages window and return "" + + """ + mousePos = getMousePosition(absorrel, which, position) + if mousePos is None: + print(("current mouse position is invalid for a Unimacro Shorthand Command with parameters:\n" + "absorrel: %s (%s), which: %s (%s), corner position: %s (%s)"% (absorrel, absorrelDict[absorrel], + which, whichDict[which], + position, cornerDict[position]))) + return "" + x, y = mousePos + if absorrel: + return "RMP(%s, %.6s, %.6s)"% (which, x, y) + return "MP(%s, %s, %s)"% (which, x, y) + +def printMousePosition(absorrel, printAll = 0): + result = getMousePositions(absorrel, printAll=printAll) + print(result) + +def getMousePositions(absorrel, printAll = 0): + """get output for printing the mouse positions + + this function can be invoked by PMP and PRMP and PALLMP. + + these positions are recognised by the Unimacro Shorthand Commands MP and RMP + """ + L = [] + if printAll: + L.append('-'*80) + cornerRange = list(range(4)) + else: + cornerRange = list(range(1)) + if absorrel: # 1: relative: + L.append('RELATIVE MOUSE POSITIONS:') + else: + L.append('ABSOLUTE MOUSE POSITIONS:') + for whichnum, which in whichDict.items(): + L.append('---related to %s:'% which.upper()) + for cornerPos in cornerRange: + L.append("%s: %s"% (cornerDict[cornerPos], getMousePositionActionString(absorrel, whichnum, cornerPos))) + if printAll: + L.append('-'*20) + return '\n'.join(L) + +def getMousePosition(absorrel=0, which=0, position=0): + """get the parameters for doMouse + + absorrel: 0 abs, 1 rel + which: 1: active window, 3: active monitor, 5: client area, 0: whole screen + position: 0: topleft, 1: topright, 2: bottomleft, 3: bottomright + + result: + None if mouseposition is invalid for the choice in question (outside boundaries probably) + + otherwise a 2 tuple: + x, y + """ + #pylint:disable=R0912, W0612 + x, y = currentPos = natlink.getCursorPos() + _hndle = natlink.getCurrentModule()[2] + print(f'which: {which}') + if which == 0: + # complete screen + width, height, xMin, yMin, xMax, yMax = monitorfunctions.getScreenRectData() + if absorrel == 0: # 0 absolute, just return position + # screen absolute, only position: + return x, y + elif which == 1: + # active window + rect = win32gui.GetWindowRect(_hndle) + width, height, xMin, yMin, xMax, yMax = getRectData(rect) + elif which == 3: + # active monitor + rect = monitorfunctions.get_current_monitor_rect( currentPos ) + width, height, xMin, yMin, xMax, yMax = getRectData(rect) + ## + elif which == 5: + # client area coordinates: + x, y = win32gui.ScreenToClient(_hndle, currentPos) + rect = win32gui.GetClientRect(_hndle) + width, height, xMin, yMin, xMax, yMax = getRectData(rect) + else: + raise ValueError(f'getMousePosition, variable "which" should be in (0, 1, 3, 5), not: {which}') + + # now test for boundaries and abs or rel: + if x < xMin or x > xMax or y < yMin or y > yMax: + print('mouse position outside active window') + return None + if absorrel: # 1: relative: + #print 'RELATIVE MOUSE POSITIONS:' + #print '---related to ACTIVE WINDOW:' + if position == 0: # top left + x, y = coordToRel(x, xMin, xMax, 0), coordToRel(y, yMin, yMax, 0) + if position == 1: # top right + x, y = coordToRel(x, xMin, xMax, 1), coordToRel(y, yMin, yMax, 0) + if position == 2: # bottom left + x, y = coordToRel(x, xMin, xMax, 0), coordToRel(y, yMin, yMax, 1) + if position == 3: # bottom right + x, y = coordToRel(x, xMin, xMax, 1), coordToRel(y, yMin, yMax, 1) + return x, y + + # else: # absolute + #print 'ABSOLUTE MOUSE POSITIONS:' + #print '---related to ACTIVE WINDOW:' + if position == 0: # top left + x, y = x-xMin, y-yMin + if position == 1: # top right + x, y = x-xMax, y-yMin + if position == 2: # bottom left + x, y = x-xMin, y-yMax + if position == 3: # bottom right + x, y = x-xMax, y-yMax + return x, y + + +# gives true if the window is a "TOP" window (with a button on the +# windows task bar. False otherwise (it is a child window then) +def isTopWindow(hndle): + """returns 1 if window is top, + + behaviour changed in python 2.3.4, parent = 0, no exception any more + TODO QH: is this correct? + """ + #pylint:disable=W0702 + try: + parent = win32gui.GetParent(hndle) + except: + return 1 + return parent == 0 + + +# remember the current window, needed before waitForTitleChange can be done +# modInfo can be left away +Hndle = None +windowTitle = "" +def clearWindowHandle(): + """set to 0""" + #pylint:disable=W0603 + global Hndle + Hndle = None + +# This is called if the user clicks on the tray icon. We simply cancel +# movement in all cases. +waitingCanceled = 0 +iconState = 0 +iconDirectory = os.path.join(status.getUnimacroDirectory(), 'icons') + +# this sets the icontray for several waiting situations::: +def setTrayIcon(state=None, toolTip=None, comingFrom=None): + """activate the trayIcon depending on the state + + switched off, reactivate later!! + + -If comingFrom is passed it can be either a instance (so the self of the calling grammar) + or a function/method. + -If comingFrom is an instance, a method "onTrayIcon" is taken (if possible) from the instance. + In those cases this function (onTrayIcon) is called if the user clicks on the trayIcon. + + """ + #pylint:disable=W0101, R1710, W0603 + global iconState + return None + if state is None: + natlink.setTrayIcon() + return None # reset! + #print 'natlinkqh setTrayIcon: %s'% state + if state == 'waiting': + toolTip = toolTip or 'unimacro is waiting' + iconName = os.path.join(iconDirectory, 'waiting') + else: + iconName = state + + iconState = (iconState + 1)%2 +## print 'iconName: %s'% iconName + if iconName[1] == ':': + if not iconName.endswith('.ico'): + # absolute path, attach extension: + iconName = iconName + '.ico' + elif iconName in ['right', 'left', 'up', 'down']: + ## only if the 2 is not in the calling function yet... + if iconState: + iconName += '2' + + # if isinstance(comingFrom, instance): + # func = getattr(comingFrom, 'onTrayIcon', None) + # if func: + # toolTip += ' (grammar: %s)'% comingFrom.getName() + # elif isinstance(comingFrom, func): + # func = comingFrom + # elif comingFrom: + # func = None + # #print 'unimacroutils.setTrayIcon, comingFrom not of correct type (%s): %s'% (comingFrom, type(comingFrom)) + # else: + # func = None + func = None + if func is None: + toolTip += " (cannot be canceled)" + try: + natlink.setTrayIcon(iconName,toolTip) + except natlink.NatError: + natlink.setTrayIcon() + else: + try: + natlink.setTrayIcon(iconName,toolTip,func) + except natlink.NatError: + print('cannot set tray icon "%s" (comingFrom: %s, func: %s), try to clear'% (iconName, comingFrom, func)) + natlink.setTrayIcon() + return None +# +# +def clearTrayIcon(): + """clearTrayIcon, reactivate later + """ + #pylint:disable=W0603 + global waitingCanceled, iconState + waitingCanceled = 0 + iconState = 0 + # natlink.setTrayIcon() +# + +def rememberWindow(modInfo=None, progInfo=None, comingFrom=None): + """remember the current window + """ + #pylint:disable=W0603, W0613 + global Hndle, waitingCanceled, windowTitle + # if not hndle true, then raised in error, because + # rememberWindow is called before and not finished correct + waitingCanceled = 0 + modInfo = modInfo or getCurrentModuleSafe() + Hndle = modInfo[2] + windowTitle = modInfo[1] +## print 'set window to %s'% Hndle + if not Hndle: + print('warning, no window to remember: %s'% Hndle) + # print 'rememberWindow, %s, %s'% (Hndle, windowTitle) + return Hndle + +def waitForWindowTitle(titleName, nWait=10, waitingTime=0.1, comingFrom=None): + """wait for window with title to come into the foreground + + waiting for a specific window title (word from...) + all lowercase... + timing out if nWait times is waited (eg 50 times), waitingTime suggested as 0.05 seconds + (2.5 second total then!) + + """ +## print 'WWT, comingFrom: %s'% comingFrom + titleName = titleName.lower() + for _i in range(nWait): +## print 'interrupted? %s'% comingFrom.interrupted + if comingFrom and comingFrom.interrupted: + # clearTrayIcon() + print('waiting canceled (no TrayIcon handling') + return None + + currentTitleName = natlink.getCurrentModule()[1].lower() + # if empty (no window active) or matching: + #print 'checking, currentTitle: %s, wantedTitle: %s'% (currentTitleName, titleName) + if currentTitleName: + if isinstance(titleName, str): + if currentTitleName.find(titleName) >= 0: + # clearTrayIcon() + #print 'string test, found' + return 1 + else: + for t in titleName: + if currentTitleName.find(t) >= 0: + # clearTrayIcon() + #print 'list test, found on %s'% t + #found + return 1 + #print 'waiting, currentTitle: %s, wantedTitle: %s'% (currentTitleName, titleName) + # setTrayIcon('waiting', comingFrom=comingFrom) + Wait(waitingTime, comingFrom=comingFrom) + # clearTrayIcon() + print('Waiting for window title "%s" lasts too long, failed\nGot title: %s' % (titleName, currentTitleName)) + return None + + +def waitForNewWindow(nWait=10, waitingTime=0.1, comingFrom=None, debug=None, progInfo=None): + """wait for a new window to appear in the foreground + + rememberWindow must run before. nWait and waitingTime as suggested above + """ + #pylint:disable=R1710, W0613, W0612 + if Hndle is None: + raise NatlinkCommandError("waitForNewWindow, no valid old hndle, do a rememberWindow() first") + for i in range(nWait): + if waitingCanceled: + # clearTrayIcon() + print('waiting canceled') + return None + + modInfo = getCurrentModuleSafe() + if not modInfo: + print('waitForNewWindow failed, no modInfo') + # clearTrayIcon() + return None + + stepsToBeStable = max(3, i) # if it took longer to bring window in front, test more steps + progN, titleN, hndleN = modInfo + if hndleN != Hndle: + # new window, wait for stable situation + succes = 0 + for j in range(stepsToBeStable*3): + # changed! reset wait a little and OK: + Wait(waitingTime) + newModInfo = getCurrentModuleSafe() + if modInfo == newModInfo: + succes += 1 + if succes >= stepsToBeStable: + break + else: + succes = 0 + modInfo = newModInfo + else: + print("waitForNewWindow: Found new window, but modInfo was not stable more than %s times"% stepsToBeStable) + clearTrayIcon() + return None + if debug and j > stepsToBeStable + 1: + extra = j - stepsToBeStable - 1 + print('title stable times after %s extra steps'% extra) + clearTrayIcon() + return 1 + setTrayIcon('waiting') + Wait(waitingTime) + clearTrayIcon() + print("waiting for new window lasts too long, fail") + return None + +def waitForNewWindowTitle(nWait=10, waitingTime=0.1, comingFrom=None, progInfo=None): + """wait for a new window with a matching title (???) + """ + #pylint:disable=R1710, W0613 + if Hndle is None: + raise NatlinkCommandError("waitForNewWindow, no valid old hndle, do a rememberWindow() first") + for _i in range(nWait): + if waitingCanceled: + clearTrayIcon() + print('waiting canceled') + return None + + modInfo = natlink.getCurrentModule() + if modInfo[1] and modInfo[1] != windowTitle: + # changed! reset wait a little and OK: + Wait(waitingTime) + clearTrayIcon() + return 1 + setTrayIcon('waiting') + Wait(waitingTime) + clearTrayIcon() + print("waiting for new window title lasts too long, fail") + return None + +# return to window that was remembered by rememberWindow. +# nWait and waitingTime as suggested above. +def returnToWindow(nWait=5, waitingTime=0.05, winHandle=None, winTitle=None, **kw): + """return to previous remembered window + + mostly do not specify winHandle and winTitle, as it is set as global + variables hndle and windowTitle + + """ + winHandle = winHandle or Hndle + winTitle = winTitle or windowTitle + if not winHandle: + print('returnToWindow, no window handle to return to, do nothing') + return None + if not win32gui.IsWindow(winHandle): + print('returnToWindow, not a valid window: %s (%s)'% (winHandle, winTitle)) + return None + # go: + # print 'returning to window: %s (%s)'% (winHandle, winTitle) + return SetForegroundWindow(winHandle, waitingTime=waitingTime, nWait=nWait) + + +# for returning a string, int or float: +restring = re.compile(r'[\'"].*[\'"]$') +reint = re.compile(r'[-]?[0-9]+$') +refloat = re.compile(r'[-]?[0-9]*[.][0-9]+$') + +def arg2IntFloatOrString(arg): + """utility function + """ + #pylint:disable=C0321 + if reint.match(arg): return int(arg) + if refloat.match(arg): return float(arg) + return arg + + +def setDebugMode(n): + """set the debug mode for commands that come from the "_commands" grammar + """ + #pylint:disable=W0603 + global debugMode + debugMode = n + + +##def convertToNatSpeakArgString(input): +## if not input: +## return [] +## out = ' ' +## for i in input: +## out = out + `i` + ', ' +## return out[:-2] + + +# Wait some time, if no time is given, +# wait 0.1 seconds = 100 milliseconds +def Wait(tt=None, comingFrom=None): + """wait in milliseconds (version 5) or seconds (version 7) + + assume maximum time is 10 seconds, so if t > 10, milliseconds were given. + + between five and 10 a warning is given! + + no input: defaultWaitingTime is taken + """ + t = tt or defaultWaitingTime + if t > 10: + print('warning, changed waiting time to seconds: %s'%t) + t = t/1000.0 + elif t >= 5: + print('warning, long waiting time: %s'% t) + + if debugMode == -1: + print("Wait %s" % t) + elif debugMode: + t = t*debugMode + if comingFrom is None: + time.sleep(t) + else: + comingFrom.Wait(t) + + +def visibleWait(): + """wait less than defaultWaitingTime: + """ + Wait(defaultWaitingTime*visibleWaitFactor) + + +def shortWait(): + """wait less than defaultWaitingTime: + """ + Wait(defaultWaitingTime*shortWaitFactor) + +def longWait(): + """wait 10 times the defaultWaitingTime: + """ + Wait(defaultWaitingTime*longWaitFactor) + +recharspace = re.compile('^[a-zA-Z- ]+$') +def addWordIfNecessary(w): + """ see if word is already there, if not add. + + """ + w = w.strip() + if not w: + return + if not recharspace.match(w): + print('invalid character in word to add: %s'% w) + return + + isInVoc = natlink.getWordInfo(w,1) is not None + isInActiveVoc = natlink.getWordInfo(w,0) is not None + if isInActiveVoc: + return + try: + if isInVoc: # from backup vocabulary: + print('make backup word active:', w) + natlink.addWord(w,0) + add2logfile(w, 'activated words.txt') + else: + print('adding word ', w) + natlink.addWord(w) + add2logfile(w, 'new words.txt') + + except natlink.InvalidWord: + print('not added to vocabulary, invalid word: %s'% w) + +def deleteWordIfNecessary(w): + """delete word from vocabulary -- if needed + """ + if not w: + return + isInActiveVoc = natlink.getWordInfo(w,0) is not None + if isInActiveVoc: + natlink.deleteWord(w) + +def debugPrint(t): + """print to debug file (is this working???) + """ + if not DEBUG: + return + print(t) + +def GetForegroundWindow(): + """return the handle of the current foreground window + """ + return win32gui.GetForegroundWindow() + + +def SetForegroundWindow(h, waitingTime=0.1, nWait=3, debug=None): + """gets the window in front + + Autohotkey is used if active!! + + When the switch is not made within 3 steps (of default waiting time), + win+b (giving the system tray) is sent, and then the waiting cycle is done again a few times. + + tested in PyTest/unittestClipboard.py: testSwitchingWindows + """ + #pylint:disable=R0912, C0321 + if not h: + raise UnimacroError("no valid handle given for set foreground window: %s"% h) + if autohotkeyactions.ahk_is_active(): + script = f'WinActivate, ahk_id {h}' + ## TODOQH + autohotkeyactions.do_ahk_script(script) + # print(f'result SetForegroundWindow: {result}, wanted hndle: {h}') + curHndle = win32gui.GetForegroundWindow() + if curHndle == h: + # print("autohotkey WinActivate succeeded: %s, wait 0.3 more seconds"% h) + # time.sleep(0.3) + return 1 + print(f'autohotkey did not get window with hndle {h} in the foreground') + return None + + curHndle = win32gui.GetForegroundWindow() + if curHndle == h: + if debug: print('got it in one shot!! %s'% h) + return 1 + + if not win32gui.IsWindow(h): + print('SetForegroundWindow: not a valid window hndle: %s'% h) + return None + + for doKeystroke in [""]: # "{win+b}"]: ####, "{win+m}"]: + result = _setForegroundWindow(h, doKeystroke, waitingTime=waitingTime, nWait=nWait, debug=debug) + if result: + if doKeystroke: + if debug: print('SetForegroundWindow to %s success, after keystroke: %s'% (h, doKeystroke)) + else: + if debug: print('SetForegroundWindow to %s success'% h) + return result + # if doKeystroke: + # if debug: 'return to calling window: %s'% curHndle + # win32gui.SetForegroundWindow(curHndle) + return None + +def _setForegroundWindow(hndle, doKeystroke=None, waitingTime=0.1, nWait=3, debug=None): + """try to switch to hndle + """ + #pylint:disable=W0621, C0321, R0912 + if doKeystroke: + if debug: print('try to get %s in foreground with keystroke: %s'% (hndle, doKeystroke)) + ## TODOQH make keystrokes here + # natlinkutils.playString(doKeystroke) + if win32gui.IsIconic(hndle): + if debug: print('window %s is iconic, try to restore...') + monitorfunctions.restore_window(hndle) + Wait() + for i in range(nWait): + if win32gui.IsIconic(hndle): + if debug and i: + print('window is still iconic, wait longer %s'% i) + Wait(waitingTime) + else: + break + else: + # if debug: print('_setForegroundWindow, %s is still "Iconic"'% h) + return None + + win32gui.SetForegroundWindow(hndle) + + # except pywintypes.error as details: + # if details[0] in [0, 183]: + # pass + # # print 'could not bring to foreground: %s'% hndle + # else: + # raise + for i in range(3): + newH = getCurrentModuleSafe()[2] + if newH == hndle: + Wait() #extra for safety + return 1 + if debug and i: + print("_setForegroundWindow, waiting %s (for %s, current: %s)"% (i, hndle, newH)) + if debug: print('_setForegroundWindow, with keystroke %s no result to set foregroundwindow to %s'% (doKeystroke, hndle)) + return None + +titleHandles = {} +# switch to window with text in the title: +def switchToWindowWithTitle(wantedTitle, caseExact=0, titleExact=0): + """keep cache of title handles for faster execution + + """ + #pylint:disable=W0603, C0321 + global titleHandles + extension = '' + if titleExact: extension += "T" + if caseExact: extension += "C" + functionName = "lookForWindowText" + extension + try: + lookForFunction = globals()[functionName] + except KeyError: + print("invalid function in switchToWindowWithTitle: %s" % functionName) + return None + if len(titleHandles) > 100: # in case too many different switches + print('clearing switchWindow title handles') + titleHandles.clear() + tryHandle = titleHandles.get(wantedTitle, 0) + if tryHandle and not lookForFunction(tryHandle, wantedTitle): + pass + else: + win32gui.EnumWindows(lookForFunction, wantedTitle) + titleHandles[wantedTitle] = natlink.getCurrentModule()[2] + Wait(0.1) # safety + return 1 + +# look for window with part of text, case exact: +def lookForWindowTextC(hwnd, text): + #pylint:disable=C0116 + if win32gui.GetWindowText(hwnd).find(text) >= 0: + SetForegroundWindow(hwnd) + return None + return 1 +# look for window Messages, default case, part of text, all to lower case: +def lookForWindowText(hwnd, text): + #pylint:disable=C0116 + if win32gui.GetWindowText(hwnd).lower().find(text.lower()) >= 0: + SetForegroundWindow(hwnd) + return None + return 1 +# look for window with exact match: +def lookForWindowTextTC(hwnd, text): + #pylint:disable=C0116 + if win32gui.GetWindowText(hwnd).strip() == text.strip(): + SetForegroundWindow(hwnd) + return None + return 1 +# look for window Messages, whole title, but convert to lower case: +def lookForWindowTextT(hwnd, text): + #pylint:disable=C0116 + if win32gui.GetWindowText(hwnd).lower().strip() == text.lower().strip(): + SetForegroundWindow(hwnd) + return None + return 1 + +def returnFromMessagesWindow(): + #pylint:disable=C0116 + if matchModule('natspeak', 'Messages from Python Macros'): + natlink.playString("{Alt+Tab}", natlinkutils.hook_f_systemkeys) + + +# Returns the date on a file or 0 if the file does not exist +def getFileDate(fileName): + #pylint:disable=C0116, C0321 + try: return os.path.getmtime(fileName) + except OSError: return 0 # file not found + + +def printListorString(arg): + #pylint:disable=C0116 + if isinstance(arg, str): + print(arg) + elif isinstance(arg, (list, tuple)): + for l in arg: + print(l) + + +#QH13062003 clipboard helper functions-------------------------------------- + +previousClipboardText = [] + +def saveClipboard(): + """Saves and clears the clipboard, and puts the text content in a global variable + + The global variable "previousClipboardText" + is used to restore to the clipboard in the function "restoreClipboard" + + No input parameters, no result, the global variable is set + + """ + #pylint:disable=W0702 + # global previousClipboardText + t = getClipboard() + previousClipboardText.append(t) + for _i in range(10): + try: + win32clipboard.OpenClipboard() + break + except: + time.sleep(0.1) + continue + else: + print("could not open, save and empty the clipboard") + return + try: + win32clipboard.EmptyClipboard() + finally: + win32clipboard.CloseClipboard() + #print 'previousClipboardText: %s'% repr(previousClipboardText) + +def clearClipboard(): + """clears the clipboard + + No input parameters, no result, + + """ + #pylint:disable=W0702 + # t0 = time.time() + for _i in range(3): + try: + win32clipboard.OpenClipboard() + except: + print('error opening the clipboard') + shortWait() + else: + break + try: + win32clipboard.EmptyClipboard() + finally: + win32clipboard.CloseClipboard() + # print 'clearClipboard: %.4f'% (time.time() - t0,) + +format_unicode = win32con.CF_UNICODETEXT + +def restoreClipboard(): + """Restores the previously saved clipboard text into the clipboard + + No input, no result. The global variable is emptied. + + """ + #pylint:disable=W0702 + # global previousClipboardText + if previousClipboardText: + t = previousClipboardText.pop() + else: + print('No "previousClipboardText" available, empty clipboard...') + t = None + return + try: + for _i in range(10): + try: + win32clipboard.OpenClipboard() + break + except: + time.sleep(0.1) + continue + else: + print("could not restore clipboard") + return + win32clipboard.EmptyClipboard() + if t: + win32clipboard.SetClipboardData(format_unicode, t) + finally: + win32clipboard.CloseClipboard() + +def getClipboard(): + """get clipboard through natlink, and strips off backslash r + + """ + #pylint:disable=W0702 + # t0 = time.time() + t = None + for _i in range(3): + try: + t = natlink.getClipboard() + if not t is None: + # print ' at try', i + break + except: + print('getClipboard, but apparently empty') + shortWait() + else: + break + + if t: + # print 'got clipboard: "%s"'% repr(t) + t = t.replace('\r', '') + return t + print('getClipboard, got clipboard, but empty') + return '' + + + +def setClipboard(t, format=1): + """set clipboard with text + format = win32con.CF_UNICODETEXT (13): as unicode + + """ + #pylint:disable=W0622 + try: + win32clipboard.OpenClipboard() + win32clipboard.EmptyClipboard() + win32clipboard.SetClipboardData(format, t) + except: + print(f'exception in unimacroutils/setClipboard of "{t}"') + finally: + win32clipboard.CloseClipboard() + +def checkLists(one, two): + """returns to lists, only in first, only in second + + if lists are equal 2 empty lists are returned + """ + onlyone = [] + onlytwo = [] + for o in one: + if not o in two: + onlyone.append(o) + for t in two: + if not t in one: + onlytwo.append(t) + return onlyone, onlytwo + +def cleanString(s): + """converts a string with leading and trailing and + intermittent whitespace into a string that is stripped + and has only single spaces between words, newlines are removed. + +>>> cleanString('foo bar') +'foo bar' +>>> cleanString('\\n foo \\n\\n bar ') +'foo bar' + """ + return ' '.join([x.strip() for x in s.split()]) + + + +def cleanParagraphs(t): + """make long paragraphs from selection, cleaning newlines and white space + + multiple empty lines are ignored, so spacing is 2 newlines at most. + + returns the new string + """ + T = t.split('\n\n') + OUT = [] + hadEmptyLine = 0 + for p in T: + if p.strip(): + OUT.append(cleanString(p)) + hadEmptyLine = 0 + else: + if not hadEmptyLine: + OUT.append('') + hadEmptyLine = 1 + return '\n\n'.join(OUT) + + +def stripSpokenForm(t): + """strip the spoken from from a word from the Dragon Vocabulary + """ + + p = t.find('\\') + if p == 0: +## print 'backslash first: |%s|'% t + if t[:4] in ['\\spa']: + return ' ' + if t == '\\\\backslash': + return '\\' + elif p > 0: + return t.split('\\', 1)[0] + return t + +# # get screen parameters: +# monitorfunctions.monitor_info() +# screenRect = monitorfunctions.VIRTUAL_SCREEN[:] +# screenWidth, screenHeight, screenXMin, screenYMin, screenXMax, screenYMax = getRectData(screenRect) +# logFolder = None +# try: +# uud = getUnimacroUserDirectory() +# if uud: +# logFolder = os.path.join(uud, getLanguage() + "_log", getUser()) +# utilsqh.createFolderIfNotExistent(logFolder) +# # print 'natlinkutilsqh, logfolder: %s'% logFolder +# except natlink.NatError: +# pass +# # print 'natlinkutilsqh, no logFolder active' + + +def add2logfile(word, filename): + """add word to the filename in logFolder + + Disabled for now + """ + #pylint:disable=E0602, W0702 + logFile = False + if not logFile: + return # silent + try: + with open(os.path.join(logFolder, filename), 'a', encoding='utf-8') as fp: + fp.write(word + '\n') + print('written to %s: %s' % (os.path.join(logFolder), filename)) + except: + pass + +def _test(): + #pylint:disable=C0415 + import doctest + return doctest.testmod() + +if __name__ == "__main__": + _test() + + natlink.natConnect() + _progInfo = getProgInfo() + print("progInfo: %s"% repr(_progInfo)) + natlink.natDisconnect() + \ No newline at end of file diff --git a/src/dtactions/unimacro/utilsqh.py b/src/dtactions/utilsqh.py similarity index 100% rename from src/dtactions/unimacro/utilsqh.py rename to src/dtactions/utilsqh.py diff --git a/tests/conftest.py b/tests/conftest.py index 2238ee4..2c98a0f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,19 @@ """common fixtures for the dtactions project """ +#pylint:disable = E1101 +# from string import Template +from shutil import copy as file_copy +from pathlib import Path, WindowsPath +from functools import cache +import importlib import pytest import natlink +import dtactions from natlinkcore import natlinkstatus +thisPath = Path(__file__).parent @pytest.fixture(scope="module") -def nl_stat(): +def nl_status(): status = natlinkstatus.NatlinkStatus() return status @@ -13,3 +21,62 @@ def nl_stat(): def nat_conn(): yield natlink.natConnect() natlink.natDisconnect() + + +@cache #only needs to be called once +def dtactions_source_path() -> Path: + return Path(importlib.util.find_spec("dtactions").submodule_search_locations[0]) + +@cache +def dtactions_sample_ini_path() -> Path: + return dtactions_source_path()/"sample_ini" + + +def copy_unimacroactions_ini(): + inifile_name = "unimacroactions.ini" + dtactions_user_path = dtactions.getDtactionsUserPath() + source_file=dtactions_sample_ini_path()/"unimacroactions.ini" + target_file=dtactions_user_path/inifile_name + return file_copy(source_file,target_file) + + +@pytest.fixture() +def dtactions_setup_with_env_var(tmp_path, monkeypatch): + """dta_user_path points to a tmp_path + """ + dta_user_path = tmp_path + print(f'dtactions_setup, dta_userdir: "{dta_user_path}"') + monkeypatch.setenv("DTACTIONS_USERDIR", str(dta_user_path)) + return dta_user_path + +@pytest.fixture() +def dtactions_setup_default(tmp_path, monkeypatch): + """return path of fake_home / '.dtactions' + + """ + dta_test_home_path = tmp_path + def fake_home(): + return dta_test_home_path + + print(f'dtactions_setup, default home path: "{dta_test_home_path}"') + monkeypatch.setenv("DTACTIONS_USERDIR","") + monkeypatch.setattr(WindowsPath, 'home', fake_home) + dta_userdir = dta_test_home_path/'.dtactions' + dta_userdir.mkdir() + return dta_userdir + +@pytest.fixture() +def test_files_path(): + """return path of test_files (used in test_unimacroactions.py) + + """ + return thisPath / 'test_files' + + + + + + +def test_foo(dtactions_setup): + pass + diff --git a/tests/init_test.py b/tests/init_test.py new file mode 100644 index 0000000..95eca2a --- /dev/null +++ b/tests/init_test.py @@ -0,0 +1,4 @@ +from dtactions import getDtactionsPath, getDtactionsUserPath + +print(f'DtactionsPath: {getDtactionsPath()}') +print(f'DtactionsUserPath: {getDtactionsUserPath()}') diff --git a/tests/test_appbringup.py b/tests/test_appbringup.py new file mode 100644 index 0000000..46ee9af --- /dev/null +++ b/tests/test_appbringup.py @@ -0,0 +1,280 @@ +""" +This module tests the possibilities of AppBringUp, the Dragon (dvc) AppBringUp. + +Quintijn Hoogenboom, November 2024 +""" +#pylint:disable = C0415, W0611, R0914 +import time +from shutil import copy as file_copy +import pytest +# from dtactions import natlinkclipboard +# from dtactions import unimacroinivars as inivars # old style +import dtactions +from dtactions import unimacroutils as uu + + +def test_bringup(dtactions_setup_default, tmp_path, nat_conn): + """see if basic bringup works + + """ + from dtactions import unimacroactions as ua + from dtactions import inivars + file_to_bringup = tmp_path/'test file.txt' + with open(file_to_bringup, 'w', encoding='utf-8') as fp: + fp.write('open file testing') + fp.write('\n') + # Notepad appears with the file, close manually... + result = ua.UnimacroBringUp(app='open', filepath=file_to_bringup) + assert result == 1 + prog_info = uu.getProgInfo() + assert prog_info.title.find('test file.txt') >= 0 + ua.doAction('{alt+f4}') + + + +def test_bringup_and_switch_two_files_notepad(dtactions_setup_default, tmp_path, nat_conn): + """see if bringup works with default notepad, and switching between the two... + + """ + from dtactions import unimacroactions as ua + from dtactions import inivars + file_one = tmp_path/'test file one.txt' + with open(file_one, 'w', encoding='utf-8') as fp: + fp.write('open file testing on') + fp.write('\n') + file_two = tmp_path/'testfil\xe9_two.txt' + with open(file_two, 'w', encoding='utf-8') as fp: + fp.write('open file testing two with Montr\xe9al.') + fp.write('\n') + # Notepad appears with the file, close manually... + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(0.3) + prog_info = uu.getProgInfo() + assert prog_info.title.find('test file one') >= 0 + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + time.sleep(0.3) + prog_info = uu.getProgInfo() + assert prog_info.title.find('_two.txt') >= 0 + + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(0.3) + prog_info = uu.getProgInfo() + assert prog_info.title.find('test file one') >= 0 + # close the file: + ua.doAction('{alt+f4}') + + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + assert result == 1 + time.sleep(0.3) + prog_info = uu.getProgInfo() + assert prog_info.title.find('_two.txt') >= 0 + # close the file: + ua.doAction('{alt+f4}') + + +@pytest.mark.skip(reason="switching back to file 1 does not work, try/test manually") +def test_bringup_and_switch_two_files_excel(dtactions_setup_default, tmp_path, test_files_path, nat_conn): + """see if bringup works with excel files, and switching between the two... + + two files appear, but not switching back again. QH 22-11-2024 + + """ + from dtactions import unimacroactions as ua + from dtactions import inivars + file_one = file_two = None + name_one, name_two = 'test excel 1.xlsx', 'test excel 2.xlsx' + for name in name_one, name_two: + source_file = test_files_path/name + target_file= tmp_path/name + file_copy(source_file,target_file) + if file_one is None: + file_one = target_file + else: + file_two = target_file + assert file_one.is_file() + assert file_two.is_file() + + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + assert result == 1 + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + +# @pytest.mark.skip(reason="switching back to file 1 does not work, try/test manually") +def test_bringup_and_switch_two_files_winword(dtactions_setup_default, tmp_path, test_files_path, nat_conn): + """see if bringup works with word docx files, and switching between the two... + + same problem as with excel... switching back does not work... (QH 22-11-2024) + + test manually by inserting function (after ::) in bottom line of file, and removing the + @pytest.mark.skip above. + + """ + from dtactions import unimacroactions as ua + from dtactions import inivars + file_one = file_two = None + name_one, name_two = 'test word 1.docx', 'test word 2.docx' + for name in name_one, name_two: + source_file = test_files_path/name + target_file= tmp_path/name + file_copy(source_file,target_file) + if file_one is None: + file_one = target_file + else: + file_two = target_file + assert file_one.is_file() + assert file_two.is_file() + + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + + # ua.doAction("WINKEY b") # remove focus, gives no improvement. + # ua.doAction("W") + + result = ua.UnimacroBringUp(app='open', filepath=file_one) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + + ua.doAction("WINKEY b") + ua.doAction("W") + + result = ua.UnimacroBringUp(app='open', filepath=file_two) + assert result == 1 + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + + +@pytest.mark.skip(reason="trying vscode TODO: Doug??") +def test_bringup_and_switch_two_files_vscode(dtactions_setup_default, tmp_path, test_files_path, nat_conn): + """see if bringup works with several files, with visual studio as parameters + + """ + from dtactions import inivars + + dta_user_path = dtactions_setup_default + ua_file = dta_user_path/'unimacroactions.ini' + ini = inivars.IniVars(ua_file) + # ini.set('bringup edit', 'py', 'vscode') + # ini.set('bringup edit', 'txt', 'vscode') + # ini.set('bringup vscode', 'path', r'C:\Users\Gebruiker\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Visual Studio Code') + ini.set('bringup edit', 'py', 'komodo') + ini.set('bringup edit', 'txt', 'komodo') + ini.set('bringup komodo', 'path', r'C:\Program Files (x86)\ActiveState Komodo IDE 12\komodo.exe') + ini.write() + + # only now import unimacroactions!! + from dtactions import unimacroactions as ua + + file_one = file_two = None + name_one, name_two = 'ide test txt.txt', 'ide test python.py' + for name in name_one, name_two: + source_file = test_files_path/name + target_file= tmp_path/name + file_copy(source_file,target_file) + if file_one is None: + file_one = target_file + else: + file_two = target_file + assert file_one.is_file() + assert file_two.is_file() + + result = ua.UnimacroBringUp(app='edit', filepath=file_one) + time.sleep(5) # long time starting (another!) Komodo + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + + result = ua.UnimacroBringUp(app='edit', filepath=file_two) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + + # ua.doAction("WINKEY b") # remove focus, gives no improvement. + # ua.doAction("W") + + result = ua.UnimacroBringUp(app='edit', filepath=file_one) + time.sleep(2) + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_one) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + + # ua.doAction("WINKEY b") + # ua.doAction("W") + + result = ua.UnimacroBringUp(app='edit', filepath=file_two) + assert result == 1 + time.sleep(5) # 2 seconds not enough?? + prog_info = uu.getProgInfo() + assert prog_info.title.find(name_two) >= 0 + # close the file: + ua.doAction('{ctrl+f4}') + + +def test_bringup_wrong_file_setting(dtactions_setup_default, tmp_path, nat_conn): + """see if bringup works also with wrong input in unimacroactions.ini + + the file should then be brought up in notepad... + + """ + from dtactions import inivars + + # tweak ini instance: (see basic test above) + dta_user_path = dtactions_setup_default + ua_file = dta_user_path/'unimacroactions.ini' + ini = inivars.IniVars(ua_file) + ini.set('bringup edit', 'py', 'komodo') + ini.set('bringup komodo', 'path', r'C:\invalid\path\to\komodo') + ini.write() + + # only now import unimacroactions!! + from dtactions import unimacroactions as ua + file_to_bringup = tmp_path/'test.py' + with open(file_to_bringup, 'w', encoding='utf-8') as fp: + fp.write('#test edit python file') + fp.write('\n') + # Notepad appears with the file, close manually... + result = ua.UnimacroBringUp(app='edit', filepath=file_to_bringup) + assert result == 1 + time.sleep(0.7) + prog_info = uu.getProgInfo() + assert prog_info.title.find('test.py') >= 0 + ua.doAction('{alt+f4}') + +if __name__ == "__main__": + # pytest.main(['test_appbringup.py::test_bringup_and_switch_two_files_vscode']) + pytest.main(['test_appbringup.py']) diff --git a/tests/test_dtactions_base.py b/tests/test_dtactions_base.py new file mode 100644 index 0000000..d4cd29d --- /dev/null +++ b/tests/test_dtactions_base.py @@ -0,0 +1,68 @@ +r""" +This module tests the basic setup of the dtactions project. + +The directory in which user (ini) files can be put, and other things, +should be in ~\.dtactions, or in a directory that is defined by +env variable DTACTIONS_USERDIR. + +Quintijn Hoogenboom, November 2024 +""" +#pylint:disable = C0415; W0611 +# from pathlib import Path +import pytest + +# thisDir = Path(__file__).parent +# dtactions_path = thisDir.parent + +def test_dtactions_inifile(): + pass + + +def test_dtactions_userdir_env_var(dtactions_setup_with_env_var): + """test if env var DTACTIONS_USERDIR gives correct dtactions_user_dir + """ + dtactions_user_path = dtactions_setup_with_env_var + # print(f'test_dtactions_userdir_env_var, directory: "{dtactions_userdir}"') + assert dtactions_user_path.is_dir() + import dtactions + actual_dta_user_path = dtactions.getDtactionsUserPath() + assert actual_dta_user_path == dtactions_user_path + assert dtactions.getDtactionsUserDirectory() == str(dtactions_user_path) + +def test_sample_unimacroactions_ini_file(dtactions_setup_with_env_var): + """test if the unimacroactions.ini file is copied into the dtactions user dir + and check the validity of that ini file + + """ + dta_user_path = dtactions_setup_with_env_var + assert dta_user_path.is_dir() + import dtactions + + actual_dta_user_path= dtactions.getDtactionsUserPath() + assert actual_dta_user_path == dta_user_path + + from dtactions import unimacroactions + from dtactions import inivars + ua_file = actual_dta_user_path/'unimacroactions.ini' + ini = inivars.IniVars(ua_file) + assert ini + + + +def test_dtactions_userdir_default(dtactions_setup_default): + r"""see if a copy of a sample ini file for dtactions is copied to + ~\.dtactions when no or invalid env variable DTACTIONS_USERDIR is given + + TODO: see question in conftest.py (QH to Doug) + """ + dtactions_user_path = dtactions_setup_default + import dtactions + assert dtactions_user_path.is_dir() + actual_dta_user_path = dtactions.getDtactionsUserPath() + assert actual_dta_user_path == dtactions_user_path + + + + +if __name__ == "__main__": + pytest.main(['test_dtactions_base.py']) diff --git a/tests/test_extenvvars.py b/tests/test_extenvvars.py index 26407d8..b40b759 100644 --- a/tests/test_extenvvars.py +++ b/tests/test_extenvvars.py @@ -10,7 +10,7 @@ from os.path import isdir from pathlib import Path import pytest -from dtactions.unimacro import extenvvars +from dtactions import extenvvars import natlink thisDir = Path(__file__).parent dtactionsDir = thisDir.parent @@ -58,14 +58,14 @@ def test_getNatlinkEnvVariables(envvars, var_name): @pytest.mark.parametrize("var_name", ["unimacrouser", "unimacrodata", "unimacrogrammars"] ) -def test_getUnimacroEnvironmentVariables(envvars, var_name, nlstatus): +def test_getUnimacroEnvironmentVariables(envvars, var_name, nl_status): """testing the unimacro variables, Unimacro can be on or off When Unimacro is not enabled (but possibly installed), the results should be fals """ result = envvars.getExtendedEnv(var_name) - if natlink.isNatSpeakRunning() and nlstatus.unimacroIsEnabled(): + if natlink.isNatSpeakRunning() and nl_status.unimacroIsEnabled(): assert len(result) assert Path(result).is_dir() else: diff --git a/tests/test_files/ide test python.py b/tests/test_files/ide test python.py new file mode 100644 index 0000000..087148f --- /dev/null +++ b/tests/test_files/ide test python.py @@ -0,0 +1,4 @@ +# test python file +import time +print('testing a run of python...') +time.sleep(2) \ No newline at end of file diff --git a/tests/test_files/ide test txt.txt b/tests/test_files/ide test txt.txt new file mode 100644 index 0000000..155e435 --- /dev/null +++ b/tests/test_files/ide test txt.txt @@ -0,0 +1 @@ +test txt file for an ide (Visual Studio?) \ No newline at end of file diff --git a/tests/test_files/test excel 1.xlsx b/tests/test_files/test excel 1.xlsx new file mode 100644 index 0000000..3a05a69 Binary files /dev/null and b/tests/test_files/test excel 1.xlsx differ diff --git a/tests/test_files/test excel 2.xlsx b/tests/test_files/test excel 2.xlsx new file mode 100644 index 0000000..d38a281 Binary files /dev/null and b/tests/test_files/test excel 2.xlsx differ diff --git a/tests/test_files/test word 1.docx b/tests/test_files/test word 1.docx new file mode 100644 index 0000000..506a608 Binary files /dev/null and b/tests/test_files/test word 1.docx differ diff --git a/tests/test_files/test word 2.docx b/tests/test_files/test word 2.docx new file mode 100644 index 0000000..487ce54 Binary files /dev/null and b/tests/test_files/test word 2.docx differ diff --git a/tests/test_inivars.py b/tests/test_inivars.py index 96f83a1..997725d 100644 --- a/tests/test_inivars.py +++ b/tests/test_inivars.py @@ -8,8 +8,8 @@ import pytest # from dtactions import natlinkclipboard -# from dtactions.unimacro import unimacroinivars as inivars # old style -from dtactions.unimacro import inivars +# from dtactions import unimacroinivars as inivars # old style +from dtactions import inivars thisDir = Path(__file__).parent dtactionsDir = thisDir.parent diff --git a/tests/test_unimacroactions.py b/tests/test_unimacroactions.py index 8a168b3..8470018 100644 --- a/tests/test_unimacroactions.py +++ b/tests/test_unimacroactions.py @@ -1,38 +1,103 @@ """ This module tests the unimacroactions module -Quintijn Hoogenboom, December 2022 +Quintijn Hoogenboom, December 2022/November 2024 """ -from pathlib import Path +#pylint:disable = C0415, W0611, W0107 +import time +from shutil import copy as file_copy import pytest -import win32gui # from dtactions import natlinkclipboard -# from dtactions.unimacro import unimacroinivars as inivars # old style -from dtactions.unimacro.unimacroactions import * +# from dtactions import unimacroinivars as inivars # old style +import dtactions +from dtactions import unimacroutils as uu +def test_path_and_inifile_default(dtactions_setup_default): + """test if the unimacroactions.ini file is copied into the dtactions user dir + and check the validity of that ini file + + """ + from dtactions import unimacroactions + from dtactions import inivars + + dta_user_path = dtactions_setup_default + assert dta_user_path.is_dir() + + actual_dta_user_path = dtactions.getDtactionsUserPath() + assert actual_dta_user_path == dta_user_path -thisDir = Path(__file__).parent -dtactionsDir = thisDir.parent + assert (dta_user_path/'unimacroactions.ini').is_file() + + ua_file = dta_user_path/'unimacroactions.ini' + assert ua_file.is_file() + ini = inivars.IniVars(ua_file) + assert ini -def test_matchProgTitleWithDict(): +# +# def test_path_and_inifile_env_var(dtactions_setup_with_env_var): +# """test if the unimacroactions.ini file is copied into the dtactions user dir +# and check the validity of that ini file +# This one cannot co-exist with "test_path_and_inifile_default" above... +# +# Not relevant for unimacroactions testing though. +# +# """ +# from dtactions import unimacroactions +# from dtactions import inivars +# +# +# dta_user_path = dtactions_setup_with_env_var +# assert dta_user_path.is_dir() +# +# actual_dta_user_path = dtactions.getDtactionsUserPath() +# assert actual_dta_user_path == dta_user_path +# +# assert (dta_user_path/'unimacroactions.ini').is_file() +# +# ua_file = dta_user_path/'unimacroactions.ini' +# ini = inivars.IniVars(ua_file) +# assert ini + + + +def test_matchProgTitleWithDict(dtactions_setup_default): """this tests if a prog and title matches with definitions in a Dict the Dict is taken from unimacroactions.ini section [general], key "child behaves like top" or "top behaves like child" """ - child_behaves_like_top = {"natspeak": ["dragon-balk", "dragonbar"]} + from dtactions import unimacroactions as ua + from dtactions import inivars + + # the definition may contain part of the wanted title, but... only in matchPart is True... + child_behaves_like_top = {"natspeak": ["dragon-balk", "ragonbar"]} # no match: - assert matchProgTitleWithDict('prog', 'title', child_behaves_like_top, matchPart=None) is False + assert ua.matchProgTitleWithDict('prog', 'title', child_behaves_like_top, matchPart=None) is False - assert matchProgTitleWithDict('natspeak', 'Dragonbar', child_behaves_like_top) is True - assert matchProgTitleWithDict('natspeak', 'ragonbar', child_behaves_like_top) is True - assert matchProgTitleWithDict('natspeak', 'Dragonbar', child_behaves_like_top, matchPart=True) is True - assert matchProgTitleWithDict('natspeak', 'ragonbar', child_behaves_like_top, matchPart=True) is False + # title must match exact (but case insensitive) + assert ua.matchProgTitleWithDict('natspeak', 'Dragon-balk', child_behaves_like_top) is True + assert ua.matchProgTitleWithDict('natspeak', 'Dragonbar', child_behaves_like_top) is False + assert ua.matchProgTitleWithDict('natspeak', 'Dragonbar', child_behaves_like_top, matchPart=True) is True + # also good: + assert ua.matchProgTitleWithDict('natspeak', 'Dragon-balk', child_behaves_like_top, matchPart=True) is True + + + +def test_do_alert(tmp_path, nat_conn): + """see if bringup works also with wrong input in unimacroactions.ini + + """ + from dtactions import unimacroactions as ua + result = ua.do_ALERT() + assert result == 1 + + + # test SCLIP via unimacro/unimacroactions.py direct run. if __name__ == "__main__": - pytest.main(['test_unimacroutils.py']) + pytest.main(['test_unimacroactions.py']) diff --git a/tests/test_unimacroutils.py b/tests/test_unimacroutils.py index 4ecd49f..7f66bc9 100644 --- a/tests/test_unimacroutils.py +++ b/tests/test_unimacroutils.py @@ -12,9 +12,9 @@ import pytest import win32gui # from dtactions import natlinkclipboard -# from dtactions.unimacro import unimacroinivars as inivars # old style +# from dtactions import unimacroinivars as inivars # old style import natlink -from dtactions.unimacro import unimacroutils +from dtactions import unimacroutils from dtactions import autohotkeyactions