Source code for lackey

""" Sikuli script implementation for Python

See https://github.com/glitchassassin/lackey
"""

from zipfile import ZipFile
try:
    import Tkinter as tk
    import tkFileDialog
    import tkMessageBox
except ImportError:
    import tkinter as tk
    from tkinter import filedialog as tkFileDialog
    from tkinter import messagebox as tkMessageBox

import platform
import keyboard
try:
    import thread
except ImportError:
    import _thread as thread
import sys
import time
import os
import warnings
import requests

## Lackey sub-files

#from .PlatformManagerWindows import PlatformManagerWindows
from .KeyCodes import Button, Key, KeyModifier
from .RegionMatching import Pattern, Region, Match, Screen, ObserveEvent, PlatformManager, FOREVER
from .Geometry import Location
from .InputEmulation import Mouse, Keyboard
from .App import App
from .Exceptions import FindFailed, ImageMissing
from .SettingsDebug import Debug, Settings, DebugMaster, SettingsMaster
from .SikuliGui import PopupInput, PopupList, PopupTextarea
from ._version import __version__

from . import ImportHandler

VALID_PLATFORMS = ["Windows", "Darwin"]

## Define script abort hotkey (Alt+Shift+C)

def _abort_script():
    thread.interrupt_main()

keyboard.add_hotkey("alt+shift+c", _abort_script, suppress=True)

## Sikuli patching: Functions that map to the global Screen region
## Don't try this at home, kids!

# First, save the native functions by remapping them with a trailing underscore:

type_ = type
input_ = input
exit_ = sys.exit
#zip_ = zip


# Deprecated underscore functions

def _exit(code):
    warnings.warn("Please use exit_ instead.", DeprecationWarning)
    return exit_(code)

def _input(prompt):
    warnings.warn("Please use input_ instead.", DeprecationWarning)
    return input_(prompt)

def _type(obj):
    warnings.warn("Please use type_ instead.", DeprecationWarning)
    return type_(obj)


## Sikuli Convenience Functions

[docs]def sleep(seconds): """ Convenience function. Pauses script for `seconds`. """ time.sleep(seconds)
[docs]def exit(value): """ Convenience function. Exits with code `value`. """ sys.exit(value)
[docs]def setShowActions(value): """ Convenience function. Sets "show actions" setting (True or False) """ Settings.ShowActions = bool(value)
[docs]def getBundlePath(): """ Convenience function. Returns the path of the \\*.sikuli bundle. """ return Settings.BundlePath
[docs]def getBundleFolder(): """ Convenience function. Same as `getBundlePath()` plus the OS default path separator. """ return getBundlePath() + os.path.sep
[docs]def setBundlePath(path): """ Convenience function. Changes the path of the \\*.sikuli bundle. """ if os.path.exists(path): Settings.BundlePath = path else: raise OSError("File not found: " + path)
[docs]def getImagePath(): """ Convenience function. Returns a list of paths to search for images. """ return [getBundlePath()] + Settings.ImagePaths
[docs]def addImagePath(new_path): """ Convenience function. Adds a path to the list of paths to search for images. Can be a URL (but must be accessible). """ if os.path.exists(new_path): Settings.ImagePaths.append(new_path) elif "http://" in new_path or "https://" in new_path: request = requests.get(new_path) if request.status_code < 400: # Path exists Settings.ImagePaths.append(new_path) else: raise OSError("Unable to connect to " + new_path) else: raise OSError("File not found: " + new_path)
[docs]def addHTTPImagePath(new_path): """ Convenience function. Same as `addImagePath()`. """ addImagePath(new_path)
[docs]def getParentPath(): """ Convenience function. Returns the parent folder of the \\*.sikuli bundle. """ return os.path.dirname(Settings.BundlePath)
[docs]def getParentFolder(): """ Convenience function. Same as `getParentPath()` plus the OS default path separator. """ return getParentPath() + os.path.sep
[docs]def makePath(*args): """ Convenience function. Returns a path from a series of path components. Same as `os.path.join`. """ return os.path.join(*args)
[docs]def makeFolder(*args): """ Convenience function. Same as `makePath()` plus the OS default path separator. """ return makePath(*args) + os.path.sep
## Sikuli implements the unzip() file, below. Included here to avoid breaking old ## scripts. ``zipfile()`` is coded here, but not included in Sikuli, so I've ## commented it out for the time being. Note that ``zip`` is a reserved keyword ## in Python.
[docs]def unzip(from_file, to_folder): """ Convenience function. Extracts files from the zip file `fromFile` into the folder `toFolder`. """ with ZipFile(os.path.abspath(from_file), 'r') as to_unzip: to_unzip.extractall(os.path.abspath(to_folder))
#def zipfile(fromFolder, toFile): # with ZipFile(toFile, 'w') as to_zip: # for root, dirs, files in os.walk(fromFolder): # for file in files: # to_zip.write(os.path.join(root, file)) ## Popup/input dialogs
[docs]def popat(*args): """ Convenience function. Sets the popup location (currently not used). """ if len(args) == 2 and isinstance(args[0], int) and isinstance(args[1], int): # popat(x,y) Settings.PopupLocation = Location(args[0], args[1]) elif len(args) == 1 and isinstance(args[0], Location): # popat(location) Settings.PopupLocation = args[0] elif len(args) == 1 and isinstance(args[0], Region): Settings.PopupLocation = args[0].getCenter() elif len(args) == 0: Settings.PopupLocation = SCREEN.getCenter() else: raise TypeError("Unrecognized parameter(s) for popat")
[docs]def popError(text, title="Lackey Error"): """ Creates an error dialog with the specified text. """ root = tk.Tk() root.withdraw() tkMessageBox.showerror(title, text)
[docs]def popAsk(text, title="Lackey Decision"): """ Creates a yes-no dialog with the specified text. """ root = tk.Tk() root.withdraw() return tkMessageBox.askyesno(title, text)
# Be aware this overwrites the Python input() command-line function.
[docs]def input(msg="", default="", title="Lackey Input", hidden=False): """ Creates an input dialog with the specified message and default text. If `hidden`, creates a password dialog instead. Returns the entered value. """ root = tk.Tk() input_text = tk.StringVar() input_text.set(default) PopupInput(root, msg, title, hidden, input_text) root.focus_force() root.mainloop() return str(input_text.get())
[docs]def inputText(message="", title="Lackey Input", lines=9, width=20, text=""): """ Creates a textarea dialog with the specified message and default text. Returns the entered value. """ root = tk.Tk() input_text = tk.StringVar() input_text.set(text) PopupTextarea(root, message, title, lines, width, input_text) root.focus_force() root.mainloop() return str(input_text.get())
[docs]def select(message="", title="Lackey Input", options=None, default=None): """ Creates a dropdown selection dialog with the specified message and options `default` must be one of the options. Returns the selected value. """ if options is None or len(options) == 0: return "" if default is None: default = options[0] if default not in options: raise ValueError("<<default>> not in options[]") root = tk.Tk() input_text = tk.StringVar() input_text.set(message) PopupList(root, message, title, options, default, input_text) root.focus_force() root.mainloop() return str(input_text.get())
[docs]def popFile(title="Lackey Open File"): """ Creates a file selection dialog with the specified message and options. Returns the selected file. """ root = tk.Tk() root.withdraw() return str(tkFileDialog.askopenfilename(title=title))
# If this is a valid platform, set up initial Screen object. Otherwise, might be ReadTheDocs if platform.system() in VALID_PLATFORMS: SCREEN = Screen(0) for prop in dir(SCREEN): if callable(getattr(SCREEN, prop, None)) and prop[0] != "_": # Property is a method, and is not private. Dump it into the global namespace. globals()[prop] = getattr(SCREEN, prop, None)