Commits

Ed Blake  committed 54ff883

Refactored large module into package, updated docs.

  • Participants
  • Parent commits be0413a

Comments (0)

Files changed (15)

 :Description: Python ctypes wrapper around AutoHotKey dll.
 :License: BSD-style, see `LICENSES.txt`_
 :Author: Ed Blake <kitsu.eb@gmail.com>
-:Date: Sep. 13 2012
-:Revision: 8
+:Date: Sep. 17 2012
+:Revision: 9
 
 Introduction
 ------------
 * Migrate docs to `ReadTheDocs <http://read-the-docs.readthedocs.org/en/latest/getting_started.html>`_?
 * Extend Script class (ahk.execute is annoying).
 * Debug AHK function calling wrappers (can't use arguments).
-* Figure out if Scripts can have their own thread and context.
 * Finish writing low-level tests.
 * Mock out dll so high-level tests can be written (i.e. for winActive et al).
 * Add examples directory.

File ahk.py

-"""OO wrapper around The AutoHotKey library.
-
-This module provides both direct access to wrapped versions of the functions
-provided by the ahkdll, and also object wrappers around common operations.
-"""
-import ctypes, time, os, sys
-from functools import partial, wraps
-
-# This try/except allows documentation to be generated without access to the dll
-try:
-    _ahk = ctypes.cdll.AutoHotkey #load AutoHotKey dll
-except (WindowsError, OSError):
-    # Try loading the dll from the module directory
-    path = os.path.dirname(__file__)
-    dllpath = os.path.join(path, 'AutoHotKey.dll')
-    try:
-        _ahk = ctypes.cdll.LoadLibrary(os.path.abspath(dllpath))
-    except (WindowsError, OSError):
-        print "Warning: Can't load AutoHotKey.dll, all ahk functions will fail."
-
-__version__ = "0.1.0"
-
-class Function(object):
-    """Object wrapper around ahk functions"""
-    template = "\n{0}{1} {{\n{2}\n}}"
-
-    def __init__(self, name, result, args, body):
-        """
-        Called Functions are automatically transform to their result by calling
-        the provided result function on the return value from the ahk function
-        call.
-
-        :param name: The name of the function to wrap.
-        :type name: str
-        :param result: Type of the expected result.
-        :type result: callable type (default=str)
-        :param args: The argument definition of the function.
-        :type args: str (default='()')
-        :param body: The body of the function (excluding braces).
-        :type body: str (default='')
-        """
-        self.name = name
-        self.definition = self.template.format(name, args, body)
-        execute(self.definition)
-        self.result = result
-        # Because the ahkdll function call function is broken use temp
-        # variables as a work-around.
-        self.tmpname = "tmp{0}".format(str(int(time.time())))
-
-    def __call__(self, *args):
-        """Call the wrapped function and return the converted result."""
-        args = ",".join(str(i) for i in args)
-        # Execute the function and assign the result to the temp variable
-        execute("{0} := {1}({2})".format(
-            self.tmpname, self.name, args))
-        # Get the result from the temp variable
-        result = self.result(get(self.tmpname))
-        # And delete the temp variable value
-        set(self.tmpname, "")
-        return result
-
-class Script(object):
-    """Wrapper around ahk script commands."""
-
-    def __init__(self, script="", filename=None):
-        """
-        Initializes the ahk script engine just like calling the low-level
-        function ahk.start followed by ahk.ready.
-        """
-        self._handle = start(script=script, filename=filename)
-        ready()
-        self._vars = dict()
-        self._funcs = dict()
-        self._tmpname = "tmp{0}".format(str(int(time.time())))
-
-        # Add some default variables
-        self.variable('Clipboard')
-        self.variable('ErrorLevel', kind=partial(int, base=0))
-
-    def __del__(self):
-        """Call terminate to kill the script engine."""
-        terminate()
-
-    def variable(self, name, kind=str, value=''):
-        """Create a new ahk variable wrapper.
-
-        Wrapped variables are tracked by the instance and can be accessed as
-        object attributes. Variables are automatically transformed to their
-        declared kind by calling their kind with their ahk string value as the
-        first argument (just like normal python type conversions).
-
-        Variable wrappers are already provided for the special ahk Clipboard
-        and ErrorLevel variables (Note the case).
-
-        .. note::
-            Variables are not allowed to start with '_' the underscore char!
-        
-        :param name: Then name of the new variable.
-        :type name: str
-        :param kind: Type of the variable (cast on get).
-        :type kind: callable type (default=str)
-        :param value: Initial value of the variable.
-        :type value: match kind or str (default='')
-        :raises: AttributeError if the provided name already exists in the
-            instance as either a variable or an attribute.
-        """
-        if name[0] == '_':
-            raise AttributeError(
-                    "Variable names may not start with an underscore!")
-        if hasattr(self, name):
-            raise AttributeError(
-                    "Name: {0} already exists as an attribute!".format(name))
-        if name in self._vars:
-            raise AttributeError(
-                    "Name: {0} already exists as a variable!".format(name))
-        if name in self._funcs:
-            raise AttributeError(
-                    "Name: {0} already exists as a function!".format(name))
-
-        if set(name, value):
-            self._vars[name] = kind
-        else:
-            raise AttributeError(
-                    "Failure reported by ahk while setting {0}={1}!".format(
-                        name, value))
-
-    def function(self, name, result=str, args='()', body=''):
-        """Create a new ahk function wrapper.
-        
-        Wrapped functions are tracked by the instance and can be accessed as
-        object attributes. Functions are automatically transform to their
-        result by calling the provided result function on the return value from
-        the ahk function call.
-
-        .. note::
-            Function names are not allowed to start with '_' the underscore char!
-
-        :param name: The name of the function to wrap.
-        :type name: str
-        :param result: Type of the expected result.
-        :type result: callable type (default=str)
-        :param args: The argument definition of the function.
-        :type args: str (default='()')
-        :param body: The body of the function (excluding braces).
-        :type body: str (default='')
-        :raises: AttributeError if the indicated name is already used.
-        :returns: Function wrapper object.
-        """
-        if name[0] == '_':
-            raise AttributeError(
-                    "Function names may not start with an underscore!")
-        if hasattr(self, name):
-            raise AttributeError(
-                    "Name: {0} already exists as an attribute!".format(name))
-        if name in self._funcs:
-            raise AttributeError(
-                    "Name: {0} already exists as a function!".format(name))
-        if name in self._vars:
-            raise AttributeError(
-                    "Name: {0} already exists as a variable!".format(name))
-
-        func = Function(name, result, args, body)
-        self._funcs[name] = func
-        return func
-
-    def send(self, keys, mode='SendInput'):
-        """Convenience wrapper to send input to the active window.
-        
-        Sends a ahk formatted series of keystrokes to the active window.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/Send.htm>`_
-        for more details.
-
-        :param keys: The keys to be sent.
-        :type keys: str
-        :param mode: The ahk command used to send keys. Valid modes:
-
-            * Send
-            * SendRaw
-            * SendInput
-            * SendPlay
-            * SendEvent
-
-        :type mode: str
-        """
-        execute("{0} {1}".format(mode, keys))
-
-    def click(self, button="", count=1, x=None, y=None):
-        """Convenience wrapper to the ahk click function.
-
-        Send a mouse click of any type to any coordinate with optional repeats.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/Click.htm>`_
-        for more details.
-        The button argument can actually take a number of options:
-
-            * `blank` - Simple primary click.
-            * right - Auxiliary button click.
-            * wheelup - Send mouse wheel scroll event.
-            * down - Press but don't release the primary button.
-            * rel[ative] - Interpret coordinates in relative mode.
-            * etc.
-
-        Button options can be composed when not mutually exclusive 
-        (e.g. "right down relative").
-
-        Use count=0 with x and y to move the mouse without clicking.
-
-        :param button: The mouse button(s) to click.
-        :type button: str (default="")
-        :param count: Number of times to repeat the click.
-        :type count: int (default 1)
-        :param x: Mouse x-coord.
-        :type x: int
-        :param y: Mouse y-coord.
-        :type y: int
-        """
-        cmd = "Click {0}".format(button)
-        if x or y: # Could fail for x&y = 0
-            cmd += ", {0}, {1}".format(x, y)
-        # Click count must occur "somewhere right of the coordinates"
-        if count != 1:
-            cmd += ", {0}".format(count)
-        execute(cmd+'\n')
-
-    def winActivate(self, title="", text="", 
-                    extitle="", extext="", bottom=False):
-        """Convenience wrapper for ahk WinActivate commands.
-        
-        Used to set a window with provided parameters as active.
-        If all parameters are empty the `last found` window is activated.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/WinActivate.htm>`_
-        for more details.
-
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :param bottom: Whether to act on the bottom-most window.
-        :type bottom: bool (default=False)
-        """
-        cmd = "WinActivate"
-        if bottom:
-            cmd = "WinActivateBottom"
-        if ''.join((title, text, extitle, extext)):
-            cmd = '{0}, "{1}", "{2}", "{3}", "{4}"'.format(
-                cmd, title, text, extitle, extext)
-        #print cmd
-        execute(cmd)
-
-    def winActive(self, title="", text="", extitle="", extext=""):
-        """Convenience wrapper for ahk IfWinActive command.
-        
-        Used to check if a window with provided parameters is active.
-        If all parameters are empty the `last found` window is checked.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinActive.htm>`_
-        for more details.
-
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :returns: The found window's HWND or None.
-        """
-        set(self._tmpname, '')
-        execute('{0} := WinActive("{1}", "{2}", "{3}", "{4}")'.format(
-            self._tmpname, title, text, extitle, extext))
-        result = int(get(self._tmpname), 0)
-        if result == 0:
-            return None
-        return result
-
-    def winExist(self, title="", text="", extitle="", extext=""):
-        """Convenience wrapper for ahk IfWinExist command.
-        
-        Used to check if a window with provided parameters exists.
-        If all parameters are empty the `last found` window is checked.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinExist.htm>`_
-        for more details.
-
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :returns: The found window's HWND or None.
-        """
-        set(self._tmpname, '')
-        execute('{0} := WinExist("{1}", "{2}", "{3}", "{4}")'.format(
-            self._tmpname, title, text, extitle, extext))
-        result = int(get(self._tmpname), 0)
-        if result == 0:
-            return None
-        return result
-
-    def waitActive(self, title="", text="", timeout=5, 
-                   extitle="", extext="", deactivate=False):
-        """Convenience wrapper for ahk WinWaitActive command.
-        
-        Used to wait until a window matching the given parameters is activated.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitActive.htm>`_
-        for more details.
-
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param timeout: How long in seconds to wait for the window to activate.
-            Use None to wait indefinitely.
-        :type timeout: int or None (default=5)
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :param deactivate: Toggle between WinWaitActive and WinWaitNotActive.
-        :type deactivate: bool (default=False)
-        :returns: The True if matching window is activated, else False.
-        """
-        cmd = "WinWaitActive"
-        if deactivate:
-            cmd = "WinWaitNotActive"
-        if timeout is None:
-            timeout = ""
-        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
-                cmd, title, text, timeout, extitle, extext))
-        result = self.ErrorLevel
-        if result != 0:
-            return False
-        return True
-
-    def waitWindow(self, title="", text="", timeout=5, 
-                   extitle="", extext="", closed=False):
-        """Convenience wrapper for ahk WinWait command.
-        
-        Used to wait until a window matching the given parameters exists.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWait.htm>`_,
-        `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitClose.htm>`_
-        for more details.
-
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param timeout: How long in seconds to wait for the window to activate.
-            Use None to wait indefinitely.
-        :type timeout: int or None (default=5)
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :param closed: Toggle between WinWait and WinWaitClose.
-        :type closed: bool (default=False)
-        :returns: The True if matching window is activated, else False.
-        """
-        cmd = "WinWait"
-        if closed:
-            cmd = "WinWaitClose"
-        if timeout is None:
-            timeout = ""
-        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
-                cmd, title, text, timeout, extitle, extext))
-        result = self.ErrorLevel
-        if result != 0:
-            return False
-        return True
-
-    def convert_color(self, color):
-        """Convert ahk color returned as a hex string to tuple of ints."""
-        # Colors are returned as hex (e.g. 0xc0c0c0)
-        r, g, b = color[2:4], color[4:6], color[6:8]
-        return tuple((int(c, 16) for c in (r, g, b)))
-
-    def _color_delta(self, c1, c2):
-        """Compute the total error between to colors."""
-        err = 0.0
-        for i in range(3):
-            err += abs(c1[i] - c2[i])
-        return err/float(255*3)
-
-    def getPixel(self, x=0, y=0, opt='RGB', screen=True):
-        """Convenince wrapper around ahk PixelGetColor
-
-        Gets the pixel color at the indicated coordinates.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/PixelGetColor.htm>`_
-        for more details.
-
-        :param x: The pixel x coordinate (relative to screen).
-        :type x: int
-        :param y: The pixel y coordinate (relative to screen).
-        :type y: int
-        :param opt: Space separated color picking options.
-        :type opt: str (default='RGB')
-        :param screen: Flag to use screen or relative coordinates.
-        :type screen: bool (default=True)
-        """
-        if screen:
-            execute("CoordMode, Pixel, Screen")
-        else:
-            execute("CoordMode, Pixel, Reletive")
-
-        execute("PixelGetColor, {0}, {1}, {2}, RGB".format(
-            self._tmpname, x, y))
-        return self.convert_color(get(self._tmpname))
-
-    def waitPixel(self, x=0, y=0, color=None, 
-                  threshold=0.01, interval=0.5, timeout=False):
-        """Wait until the pixel at given coords changes color.
-
-        This function can wait until the indicated pixel *is* a color,
-        or until it changes color. If a color is not provided the current
-        color of the pixel when the function starts is stored and compared
-        at a regular interval until it changes or until timeout.
-
-        :param x: The pixel x coordinate (relative to screen).
-        :type x: int
-        :param y: The pixel y coordinate (relative to screen).
-        :type y: int
-        :param color: The color to wait for.
-        :type color: tuple(int r, int g, int b) or None
-        :param threshold: Error factor allowed for determining color match.
-        :type threshold: float
-        :param interval: How often the pixel color is checked.
-        :type interval: float
-        :param timeout: How long to wait for a color match.
-        :type timeout: float or None
-        :returns: True if pixel changed, False if timeout.
-        """
-        # Get starting color
-        match = True # We are trying by default to match the provided color
-        if not color:
-            match = False # If not given a color then we are trying to not match
-            color = self.getPixel(x, y)
-
-        # Timing loop
-        start = time.time()
-        last = start
-        now = start
-        while 1:
-            # Get new timing for current iteration
-            last = now
-            now = time.time()
-            # Handle timeout condition
-            if timeout and (now - start) >= timeout:
-                return False
-            # Get new color reading
-            current = self.getPixel(x, y)
-            print color, current
-            print self._color_delta(color, current)
-            # Check for match
-            if self._color_delta(color, current) <= threshold:
-                if match:
-                    return True
-            else:
-                if not match:
-                    return True
-            # Pause
-            time.sleep(interval)
-
-    def message(self, text="Alert", title="Alert", options=0, timeout=None):
-        """Convenience wrapper to the ahk msgbox function.
-
-        Displays a message box with buttons.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/MsgBox.htm>`_
-        for more details.
-
-        :param text: The body text of the msgbox.
-        :type text: str (default='Alert')
-        :param title: The title text of the msgbox.
-        :type title: str (default='Alert')
-        :param options: Button flags bit-field.
-        :type options: int (default=0)
-        :param timeout: Optional timeout, dialog is closed after timeout.
-        :type timeout: int or None (default=None)
-        """
-        cmd = 'MsgBox {0}, {1}, {2}'.format(options, title, text)
-        if timeout:
-            cmd += ', {0}'.format(timeout)
-        execute(cmd)
-
-    def msgResult(self, name='OK'):
-        """Convenience wrapper to the ahk ifMsgBox function.
-
-        Get the clicked status of a button on the last MsgBox.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/ifMsgBox.htm>`_
-        for more details.
-
-        :param name: The name of the button to check.
-        :type name: str (default='OK')
-        :returns: True if button was pressed, None if timeout, False otherwise.
-        """
-        set(self._tmpname, '0')
-        template = 'ifMsgBox, {0}\n\t{1} := 1\nifMsgBox, Timeout\n\t{1} := -1'
-        execute(template.format(name, self._tmpname))
-        result = int(get(self._tmpname))
-        if result == 1:
-            return True
-        elif result == -1:
-            return None
-        return False
-
-    def __getattr__(self, name):
-        """Override attribute lookup to add ahk variable access."""
-        #NOTE __getattr__ is only called on attrs that aren't found normally
-        if name in self._vars:
-            # The clipboard can't be directly read... Others?
-            execute("{0} := {1}".format(self._tmpname, name))
-            return self._vars[name](get(self._tmpname))
-        elif name in self._funcs:
-            return self._funcs[name]
-        else:
-            raise AttributeError("No variable named {0}!".format(name))
-
-    def __setattr__(self, name, value):
-        """Override attribute modification to add ahk variable access."""
-        # Also filter names starting with '_' (special names).
-        if name[0] == '_' or name in self.__dict__:
-            super(Script, self).__setattr__(name, value)
-        elif name in self._vars:
-            set(name, value)
-        elif name in self._funcs:
-            raise AttributeError("Can't assign to function {0}!".format(name))
-        else:
-            raise AttributeError("No variable named {0}!".format(name))
-
-class Control(object):
-    """Wrapper around ahk window control commands."""
-
-    def __init__(self, script, title="", text="", extitle="", extext="", 
-                 store=True):
-        """Initialize a new Control object.
-        
-        Stores information about the window who's controls you will manipulate.
-
-        :param script: Script object to use internally.
-        :type script: ahk.Script instance
-        :param title: Partial window title text to match.
-        :type title: str (default="")
-        :param text: Partial window text to match.
-        :type text: str (default="")
-        :param extitle: Partial window title text to avoid.
-        :type extitle: str (default="")
-        :param extext: Partial window text to avoid.
-        :type extext: str (default="")
-        :param store: Whether to cache the found window.
-        :type store: bool
-        :raises: NameError if store=True but a matching window can't be found.
-        """
-        self.script = script
-        self.params = (title, text, extitle, extext)
-        self.hwnd = None
-        if store:
-            self.hwnd = script.winExist(title, text, extitle, extext)
-            if not self.hwnd:
-                win = "win(title={0}, text={1}, extitle={2}, extext={3})"
-                raise NameError("Can't find matching window:\n\t" + 
-                                win.format(*self.params))
-        # Internal state
-        self._tmpname = "tmp{0}".format(str(int(time.time())))
-        self._cdelay = None # control delay
-        self._kdelay = None # key delay
-
-    def _params(self):
-        """Internal method to clean-up duplicate parameter code."""
-        if self.hwnd:
-            title = "ahk_id {0}".format(self.hwnd)
-            text, extitle, extext = "", "", ""
-        else:
-            title, text, extitle, extext = self.params
-        return title, text, extitle, extext
-
-    def set_delay(self, control='', key=''):
-        """Set the control or key delay used by this Control object.
-
-        Either the control delay, key delay, or both may be set.
-        Setting either to `None` will disable the local delay setting.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/SetControlDelay.htm>`_
-        for more details.
-
-        :param control: The delay to use when modifying a control.
-        :type control: float or None
-        :param key: The delay to use for `send`ing key events.
-        :type key: float or None
-        """
-        if control != '':
-            self._cdelay = control
-        if key != '':
-            self._kdelay = key
-
-    def _delay(method):
-        """Decorator to add delay behavior to Control methods."""
-        @wraps(method)
-        def delayed(self, *args, **kwargs):
-            # Store old values of delay and set new delays
-            if self._cdelay is not None:
-                execute("{0} := A_ControlDelay".format('c'+self._tmpname))
-                execute("SetControlDelay, {0}".format(self._cdelay))
-            if self._kdelay is not None:
-                execute("{0} := A_KeyDelay".format('k'+self._tmpname))
-                execute("SetKeyDelay, {0}".format(self._kdelay))
-            # Run the wrapped method
-            method(self, *args, **kwargs)
-            # Restore saved delay values
-            if self._cdelay is not None:
-                execute("SetControlDelay, %{0}%".format('c'+self._tmpname))
-            if self._kdelay is not None:
-                execute("SetKeyDelay, %{0}%".format('k'+self._tmpname))
-        return delayed
-
-    @_delay
-    def click(self, control="", pos=None, button="", count=1, options=""):
-        """Sends mouse input to a control.
-        
-        The control arg can be a class name, window handle, or text string.
-        Alternately pos as a (x, y) tuple may be provided indicating the mouse
-        coordinates relative to the top-left corner of the application window
-        in which to click. If both control and pos are provided pos is 
-        interpreted relative to the indicated control.
-
-        Default is a single left click, but you can send left/right/middle,
-        wheel up or down left or right with a repeat count.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlClick.htm>`_
-        for more details.
-
-        :param control: The class, HWND, or text of the control to click.
-        :type control: str
-        :param pos: The relative coordinates of at which to click.
-        :type pos: tuple (indexable)
-        :param button: The name of the button to be clicked.
-        :type button: str
-        :param count: Number of times it should be clicked.
-        :type count: int
-        :param options: Optional arguments.
-        :type options: str
-        """
-        title, text, extitle, extext = self._params()
-
-        if pos:
-            pos = "X{0[0]} Y{0[1]}".format(pos)
-            # Add pos option to force coordinate interpretation
-            options += " pos"
-            if not control:
-                control = pos
-            else:
-                options += " " + pos
-        # Setup window id args
-        cmd = "ControlClick, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}"
-        execute(cmd.format(control, title, text, button, count, 
-                           options, extitle, extext))
-
-    @_delay
-    def send(self, control="", keys="", raw=False):
-        """Send keystrokes to a control.
-
-        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlSend.htm>`_
-        for more details.
-
-        :param control: The class, HWND, or text of the control to send to.
-        :type control: str
-        :param keys: Keystrokes in the same format as ahk.Script.send.
-        :type keys: str
-        :param raw: Toggle raw input mode.
-        :type raw: bool (default False)
-        """
-        title, text, extitle, extext = self._params()
-
-        cmd = "ControlSend"
-        if raw:
-            cmd = "ControlSendRaw"
-        cmd = cmd + ", {0}, {1}, {2}, {3}, {4}" 
-        execute(cmd.format(control, keys, title, text, extitle, extext))
-
-    @_delay
-    def setText(self, control="", value=""):
-        """Set the text content of a control.
-
-        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlSetText.htm>`_
-        for more details.
-
-        :param control: The class, HWND, or text of the control to be changed.
-        :type control: str
-        :param value: New text to substitute into the control.
-        :type value: str
-        """
-        title, text, extitle, extext = self._params()
-
-        execute("ControlSetText, {0}, {1}, {2}, {3}, {4}, {5}".format(
-            control, value, title, text, extitle, extext))
-
-    def get_choices(self, control=""):
-        """Retrieve the available values from a ComboBox.
-
-        :param control: The class, HWND, or text of the control to be checked.
-        :type control: str
-        :returns: A list of value strings (in order).
-        """
-        title, text, extitle, extext = self._params()
-
-        execute("ControlGet, {0}, List,,{1},{2},{3},{4}".format(
-            self._tmpname, control, title, text, extitle, extext))
-        return get(self._tmpname).split('\n')
-
-    def get_chosen(self, control=""):
-        """Retrieve the selected value from a ComboBox.
-
-        :param control: The class, HWND, or text of the control to be checked.
-        :type control: str
-        :returns: Selected value as a string.
-        """
-        title, text, extitle, extext = self._params()
-
-        execute("ControlGet, {0}, Choice,,{1},{2},{3},{4}".format(
-            self._tmpname, control, title, text, extitle, extext))
-        return get(self._tmpname)
-
-    @_delay
-    def choose(self, control="", value=""):
-        """Pick an item from a listbox or combobox.
-
-        See `AHK docs <http://www.autohotkey.com/docs/commands/Control.htm>`_
-        for more details.
-
-        :param control: The class, HWND, or text of the control to be changed.
-        :type control: str
-        :param value: Partial text of choice or choice index (starting from 1).
-        :type value: str or int (default="")
-        """
-        title, text, extitle, extext = self._params()
-
-        cmd = "ChooseString"
-        if type(value) not in (str, unicode):
-            cmd = "Choose"
-        execute("Control, {0}, {1}, {2}, {3}, {4}, {5}, {6}".format(  
-            cmd, value, control, title, text, extitle, extext))
-
-    def is_checked(self, control=""):
-        """Check if a checkbox control is checked.
-        
-        :param control: The class, HWND, or text of the control to be checked.
-        :type control: str
-        :returns: True if it was checked, else False.
-        """
-        title, text, extitle, extext = self._params()
-
-        execute("ControlGet, {0}, Checked,,{1},{2},{3},{4}".format(
-            self._tmpname, control, title, text, extitle, extext))
-        result = int(get(self._tmpname))
-        if result == 1:
-            return True
-        return False
-
-    @_delay
-    def check(self, control="", state=None):
-        """Pick an item from a listbox or combobox.
-
-        The state can be set to checked (True), unchecked (False), 
-        or toggle (None), default is toggle.
-        See `AHK docs <http://www.autohotkey.com/docs/commands/Control.htm>`_
-        for more details.
-
-        :param control: The class, HWND, or text of the control to be changed.
-        :type control: str
-        :param state: The desired checked state.
-        :type state: bool or None
-        """
-        title, text, extitle, extext = self._params()
-
-        cmd = "Check"
-        checked = self.is_checked(control)
-        if state is None:
-            if checked:
-                cmd = "Uncheck"
-        elif state and checked or not state and not checked:
-            return # Want to be checked and already checked = nop
-        elif not state and checked:
-            cmd = "Uncheck"
-
-        execute("Control, {0},, {1}, {2}, {3}, {4}, {5}".format(  
-            cmd, control, title, text, extitle, extext))
-
-# Wrapper functions ------------------------------------------------------------
-
-def start(filename=None, script="", options="", params=""):
-    """Wrapper around ahkdll and ahktextdll.
-    
-    Start a new ahk thread from file or a string.
-    Defaults to an empty script with no options or params.
-    Filename is preferred over script if provided.
-
-    .. Note::
-        Using any option besides ahk.start() seems not to work.
-        Specifying a file doesn't cause it to run, passing a string
-        doesn't cause it to be executed?
-
-    :returns: Thread handle for created instance (see thread functions).
-    """
-    #print filename
-    if filename:
-        return _ahk.ahkdll(os.path.abspath(filename), options, params)
-    else:
-        return _ahk.ahktextdll(script, options, params)
-
-def ready(nowait=False, retries=None):
-    """Wrapper around ahkReady.
-    
-    Returns True if ahk is ready to use.
-    By default this polls the dll function until it is ready.
-    By calling with nowait=True the immediate result is returned instead.
-    By calling with retries > 1 state will be checked at most retries times.
-    """
-    if nowait:
-        retries = 1
-    if retries and retries >= 1:
-        for i in range(retries):
-            if _ahk.ahkReady() == 1:
-                return True
-            time.sleep(0.01)
-    else:
-        while 1:
-            if _ahk.ahkReady() == 1:
-                return True
-            time.sleep(0.1)
-    return False
-
-def add_lines(script="", filename=None, duplicates=False, ignore=True):
-    """Wrapper around addFile and addScript.
-
-    Adds lines to the running script from file or string.
-    Lines added from string are evaluated immediately,
-    lines added from file are not evaluated.
-    Defaults to an empty script, no duplicates, and ignore errors.
-    Filename is preferred over script if provided.
-
-    :returns: Pointer address to first line in added script (see execute_line).
-    """
-    if filename:
-        if duplicates:
-            duplicates = 1
-        else:
-            duplicates = 0
-
-        if ignore:
-            if type(ignore) != int:
-                ignore = 1
-        else:
-            ignore = 0
-        return int(_ahk.addFile(os.path.abspath(filename), duplicates, ignore))
-    else:
-        return int(_ahk.addScript(script))
-
-def execute(script):
-    """Wrapper around ahkExec.
-
-    Execute provided ahk commands. No lines are added to the active script.
-
-    :returns: True if successful, else False.
-    """
-    result = _ahk.ahkExec(script)
-    if result == 1:
-        return True
-    return False
-
-def jump(label, nowait=False):
-    """Wrapper around ahkLabel.
-    
-    GoSub/GoTo like function, branch to labeled location.
-    Defaults to nowait=False, i.e. GoSub mode.
-    Using nowait=True, i.e. GoTo mode, is unreliable and may fail entirely.
-
-    :returns: True if label exists, else False.
-    """
-    if nowait:
-        nowait = 1
-    else:
-        nowait = 0
-
-    result = _ahk.ahkLabel(label, nowait)
-    if result == 1:
-        return True
-    return False
-
-def call(func, *args):
-    """Wrapper around ahkFunction.
-
-    Call the indicated function.
-
-    :returns: Result of function call as a string.
-    """
-    params = None
-    if args:
-        string_array = ctypes.c_char_p*len(args)
-        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
-    result = _ahk.ahkFunction(func, params)
-    return ctypes.cast(int(result), ctypes.c_char_p).value
-
-def post(func, *args):
-    """Wrapper around ahkPostFunction.
-
-    Call the indicated function but discard results.
-
-    :returns: True if function exists, else False.
-    """
-    params = None
-    if args:
-        string_array = ctypes.c_char_p*len(args)
-        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
-    result = _ahk.ahkPostFunction(func, params)
-    if result == 0: # 0 if function exists, else -1
-        return True
-    return False
-
-def set(name, value):
-    """Wrapper around ahkassign.
-    
-    Assigns the string `value` to the variable `name`.
-
-    :returns: True for success and False for failure.
-    """
-    if not type(value) in (str, unicode):
-        value = str(value)
-    result = _ahk.ahkassign(name, value)
-    if result == 0: # 0 for success, else -1
-        return True
-    return False
-
-def get(name, pointer=False):
-    """Wrapper around ahkgetvar.
-
-    Get the string value of a variable from ahk.
-    Call with pointer=True to request a reference to the variable.
-
-    :returns: A string representing the value, or a c_char_p.
-    """
-    # Workaround for ahkgetvar always returning pointers
-    result = _ahk.ahkgetvar(name, 0)
-    result = ctypes.cast(int(result), ctypes.c_char_p)
-    if pointer:
-        return result
-    return result.value
-
-def terminate(timeout=1):
-    """Wrapper around ahkTerminate.
-
-    Terminate the script, removing all hotkeys and hotstrings.
-    The default timeout is 1ms, must be positive > 0.
-    """
-    _ahk.ahkTerminate(timeout)
-
-def reload():
-    """Wrapper around ahkReload.
-
-    Terminates and restarts the script.
-    """
-    _ahk.ahkReload()
-
-def find_func(name):
-    """Wrapper around ahkFindFunc.
-
-    Get a pointer address to the named function.
-    To use this you must first create a ctypes CFUNCTION prototype::
-
-        proto = ctypes.CFUNCTION(ctypes.c_int, ctypes.c_char_p)
-
-    Then call the prototype with the address as an argument to get a function::
-
-        func = proto(address)
-
-    Now it can be called::
-
-        result = func(5)
-
-    :returns: The address of the function as an integer.
-    """
-    return int(_ahk.ahkFindFunc(name))
-
-def find_label(name):
-    """Wrapper around ahkFindLabel.
-
-    Get a pointer address to a label...
-
-    :returns: The address of the label as an integer.
-    """
-    return int(_ahk.ahkFindLabel(name))
-
-def pause(pause_=True):
-    """Wrapper around ahkPause.
-
-    Pause or unpause the script.
-    Calling with pause_=True pauses, pause_=False un-pauses. 
-    Calling with pause_=None just reports current state without changing it.
-
-    :returns: True if the script is paused, else False.
-    """
-    # Changed arg name from pause to pause_ to appease pylint:
-    # Redefining name 'pause' from outer scope
-    if pause_:
-        pause_ = 1
-    elif pause_ is None:
-        pause_ = ""
-    else:
-        pause_ = 0
-    result = _ahk.ahkPause(pause_)
-    if result == 1:
-        return True
-    return False
-
-def exec_line(line=None, mode=3, wait=False):
-    """Wrapper around ahkExecuteLine.
-
-    Execute starting from the provided line address.
-    If line=None the address of the first line will be returned.
-    Four modes of execution are available:
-
-        0. No execution, but the next line address is returned.
-        1. Run until a return statement is found.
-        2. Run until the end of the current block.
-        3. (default) execute only one line.
-
-    Setting wait=True will block until end of execution.
-
-    :returns: A line pointer address.
-    """
-    if not line:
-        return int(_ahk.ahkExecuteLine("", 3, 0))
-    elif wait:
-        wait = 1
-    else:
-        wait = 0
-    return int(_ahk.ahkExecuteLine(line, mode, wait))
-

File ahk/__init__.py

+"""OO wrappers around the AutoHotKey library.
+
+This package provides both direct access to wrapped versions of the functions
+provided by the ahkdll, and also object wrappers around common operations.
+"""
+from ahk import *
+from script import Function, Script
+from control import Control
+
+__version__ = "0.2.0"
+
+"""Low-level wrappers around The AutoHotKey library."""
+import ctypes, time, os
+
+# This try/except allows documentation to be generated without access to the dll
+try:
+    _ahk = ctypes.cdll.AutoHotkey #load AutoHotKey dll
+except (WindowsError, OSError):
+    # Try loading the dll from the module directory
+    path = os.path.dirname(__file__)
+    dllpath = os.path.join(path, 'AutoHotKey.dll')
+    try:
+        _ahk = ctypes.cdll.LoadLibrary(os.path.abspath(dllpath))
+    except (WindowsError, OSError):
+        print "Warning: Can't load AutoHotKey.dll, all ahk functions will fail."
+
+
+def start(filename=None, script="", options="", params=""):
+    """Wrapper around ahkdll and ahktextdll.
+    
+    Start a new ahk thread from file or a string.
+    Defaults to an empty script with no options or params.
+    Filename is preferred over script if provided.
+
+    .. Note::
+        Using any option besides ahk.start() seems not to work.
+        Specifying a file doesn't cause it to run, passing a string
+        doesn't cause it to be executed?
+
+    :returns: Thread handle for created instance (see thread functions).
+    """
+    #print filename
+    if filename:
+        return _ahk.ahkdll(os.path.abspath(filename), options, params)
+    else:
+        return _ahk.ahktextdll(script, options, params)
+
+def ready(nowait=False, retries=None):
+    """Wrapper around ahkReady.
+    
+    Returns True if ahk is ready to use.
+    By default this polls the dll function until it is ready.
+    By calling with nowait=True the immediate result is returned instead.
+    By calling with retries > 1 state will be checked at most retries times.
+    """
+    if nowait:
+        retries = 1
+    if retries and retries >= 1:
+        for i in range(retries):
+            if _ahk.ahkReady() == 1:
+                return True
+            time.sleep(0.01)
+    else:
+        while 1:
+            if _ahk.ahkReady() == 1:
+                return True
+            time.sleep(0.1)
+    return False
+
+def add_lines(script="", filename=None, duplicates=False, ignore=True):
+    """Wrapper around addFile and addScript.
+
+    Adds lines to the running script from file or string.
+    Lines added from string are evaluated immediately,
+    lines added from file are not evaluated.
+    Defaults to an empty script, no duplicates, and ignore errors.
+    Filename is preferred over script if provided.
+
+    :returns: Pointer address to first line in added script (see execute_line).
+    """
+    if filename:
+        if duplicates:
+            duplicates = 1
+        else:
+            duplicates = 0
+
+        if ignore:
+            if type(ignore) != int:
+                ignore = 1
+        else:
+            ignore = 0
+        return int(_ahk.addFile(os.path.abspath(filename), duplicates, ignore))
+    else:
+        return int(_ahk.addScript(script))
+
+def execute(script):
+    """Wrapper around ahkExec.
+
+    Execute provided ahk commands. No lines are added to the active script.
+
+    :returns: True if successful, else False.
+    """
+    result = _ahk.ahkExec(script)
+    if result == 1:
+        return True
+    return False
+
+def jump(label, nowait=False):
+    """Wrapper around ahkLabel.
+    
+    GoSub/GoTo like function, branch to labeled location.
+    Defaults to nowait=False, i.e. GoSub mode.
+    Using nowait=True, i.e. GoTo mode, is unreliable and may fail entirely.
+
+    :returns: True if label exists, else False.
+    """
+    if nowait:
+        nowait = 1
+    else:
+        nowait = 0
+
+    result = _ahk.ahkLabel(label, nowait)
+    if result == 1:
+        return True
+    return False
+
+def call(func, *args):
+    """Wrapper around ahkFunction.
+
+    Call the indicated function.
+
+    :returns: Result of function call as a string.
+    """
+    params = None
+    if args:
+        string_array = ctypes.c_char_p*len(args)
+        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
+    result = _ahk.ahkFunction(func, params)
+    return ctypes.cast(int(result), ctypes.c_char_p).value
+
+def post(func, *args):
+    """Wrapper around ahkPostFunction.
+
+    Call the indicated function but discard results.
+
+    :returns: True if function exists, else False.
+    """
+    params = None
+    if args:
+        string_array = ctypes.c_char_p*len(args)
+        params = string_array(*[ctypes.c_char_p(str(arg)) for arg in args])
+    result = _ahk.ahkPostFunction(func, params)
+    if result == 0: # 0 if function exists, else -1
+        return True
+    return False
+
+def set(name, value):
+    """Wrapper around ahkassign.
+    
+    Assigns the string `value` to the variable `name`.
+
+    :returns: True for success and False for failure.
+    """
+    if not type(value) in (str, unicode):
+        value = str(value)
+    result = _ahk.ahkassign(name, value)
+    if result == 0: # 0 for success, else -1
+        return True
+    return False
+
+def get(name, pointer=False):
+    """Wrapper around ahkgetvar.
+
+    Get the string value of a variable from ahk.
+    Call with pointer=True to request a reference to the variable.
+
+    :returns: A string representing the value, or a c_char_p.
+    """
+    # Workaround for ahkgetvar always returning pointers
+    result = _ahk.ahkgetvar(name, 0)
+    result = ctypes.cast(int(result), ctypes.c_char_p)
+    if pointer:
+        return result
+    return result.value
+
+def terminate(timeout=1):
+    """Wrapper around ahkTerminate.
+
+    Terminate the script, removing all hotkeys and hotstrings.
+    The default timeout is 1ms, must be positive > 0.
+    """
+    _ahk.ahkTerminate(timeout)
+
+def reload():
+    """Wrapper around ahkReload.
+
+    Terminates and restarts the script.
+    """
+    _ahk.ahkReload()
+
+def find_func(name):
+    """Wrapper around ahkFindFunc.
+
+    Get a pointer address to the named function.
+    To use this you must first create a ctypes CFUNCTION prototype::
+
+        proto = ctypes.CFUNCTION(ctypes.c_int, ctypes.c_char_p)
+
+    Then call the prototype with the address as an argument to get a function::
+
+        func = proto(address)
+
+    Now it can be called::
+
+        result = func(5)
+
+    :returns: The address of the function as an integer.
+    """
+    return int(_ahk.ahkFindFunc(name))
+
+def find_label(name):
+    """Wrapper around ahkFindLabel.
+
+    Get a pointer address to a label...
+
+    :returns: The address of the label as an integer.
+    """
+    return int(_ahk.ahkFindLabel(name))
+
+def pause(pause_=True):
+    """Wrapper around ahkPause.
+
+    Pause or unpause the script.
+    Calling with pause_=True pauses, pause_=False un-pauses. 
+    Calling with pause_=None just reports current state without changing it.
+
+    :returns: True if the script is paused, else False.
+    """
+    # Changed arg name from pause to pause_ to appease pylint:
+    # Redefining name 'pause' from outer scope
+    if pause_:
+        pause_ = 1
+    elif pause_ is None:
+        pause_ = ""
+    else:
+        pause_ = 0
+    result = _ahk.ahkPause(pause_)
+    if result == 1:
+        return True
+    return False
+
+def exec_line(line=None, mode=3, wait=False):
+    """Wrapper around ahkExecuteLine.
+
+    Execute starting from the provided line address.
+    If line=None the address of the first line will be returned.
+    Four modes of execution are available:
+
+        0. No execution, but the next line address is returned.
+        1. Run until a return statement is found.
+        2. Run until the end of the current block.
+        3. (default) execute only one line.
+
+    Setting wait=True will block until end of execution.
+
+    :returns: A line pointer address.
+    """
+    if not line:
+        return int(_ahk.ahkExecuteLine("", 3, 0))
+    elif wait:
+        wait = 1
+    else:
+        wait = 0
+    return int(_ahk.ahkExecuteLine(line, mode, wait))
+

File ahk/control.py

+"""OO wrapper around the AutoHotKey control manipulation functions.
+
+This module gives a simplified wrapper around the loose collection of AHK
+functions related to manipulation of window controls.
+"""
+import time
+from functools import partial, wraps
+from ahk import *
+
+def _delay(method):
+    """Decorator to add delay behavior to Control methods."""
+    @wraps(method)
+    def delayed(self, *args, **kwargs):
+        """Inner function.""" # For pylint
+        # Store old values of delay and set new delays
+        if self._cdelay is not None:
+            execute("{0} := A_ControlDelay".format('c'+self._tmpname))
+            execute("SetControlDelay, {0}".format(self._cdelay))
+        if self._kdelay is not None:
+            execute("{0} := A_KeyDelay".format('k'+self._tmpname))
+            execute("SetKeyDelay, {0}".format(self._kdelay))
+        # Run the wrapped method
+        method(self, *args, **kwargs)
+        # Restore saved delay values
+        if self._cdelay is not None:
+            execute("SetControlDelay, %{0}%".format('c'+self._tmpname))
+        if self._kdelay is not None:
+            execute("SetKeyDelay, %{0}%".format('k'+self._tmpname))
+    return delayed
+
+class Control(object):
+    """Wrapper around ahk window control commands."""
+
+    def __init__(self, script, title="", text="", extitle="", extext="", 
+                 store=True):
+        """Initialize a new Control object.
+        
+        Stores information about the window who's controls you will manipulate.
+
+        :param script: Script object to use internally.
+        :type script: ahk.Script instance
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param store: Whether to cache the found window.
+        :type store: bool
+        :raises: NameError if store=True but a matching window can't be found.
+        """
+        self.script = script
+        self.params = (title, text, extitle, extext)
+        self.hwnd = None
+        if store:
+            self.hwnd = script.winExist(title, text, extitle, extext)
+            if not self.hwnd:
+                win = "win(title={0}, text={1}, extitle={2}, extext={3})"
+                raise NameError("Can't find matching window:\n\t" + 
+                                win.format(*self.params))
+        # Internal state
+        self._tmpname = "tmp{0}".format(str(int(time.time())))
+        self._cdelay = None # control delay
+        self._kdelay = None # key delay
+
+    def _params(self):
+        """Internal method to clean-up duplicate parameter code."""
+        if self.hwnd:
+            title = "ahk_id {0}".format(self.hwnd)
+            text, extitle, extext = "", "", ""
+        else:
+            title, text, extitle, extext = self.params
+        return title, text, extitle, extext
+
+    def set_delay(self, control='', key=''):
+        """Set the control or key delay used by this Control object.
+
+        Either the control delay, key delay, or both may be set.
+        Setting either to `None` will disable the local delay setting.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/SetControlDelay.htm>`_
+        for more details.
+
+        :param control: The delay to use when modifying a control.
+        :type control: float or None
+        :param key: The delay to use for ``send``-ing key events.
+        :type key: float or None
+        """
+        if control != '':
+            self._cdelay = control
+        if key != '':
+            self._kdelay = key
+
+    @_delay
+    def click(self, control="", pos=None, button="", count=1, options=""):
+        """Sends mouse input to a control.
+        
+        The control arg can be a class name, window handle, or text string.
+        Alternately pos as a (x, y) tuple may be provided indicating the mouse
+        coordinates relative to the top-left corner of the application window
+        in which to click. If both control and pos are provided pos is 
+        interpreted relative to the indicated control.
+
+        Default is a single left click, but you can send left/right/middle,
+        wheel up or down left or right with a repeat count.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlClick.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to click.
+        :type control: str
+        :param pos: The relative coordinates of at which to click.
+        :type pos: tuple (indexable)
+        :param button: The name of the button to be clicked.
+        :type button: str
+        :param count: Number of times it should be clicked.
+        :type count: int
+        :param options: Optional arguments.
+        :type options: str
+        """
+        title, text, extitle, extext = self._params()
+
+        if pos:
+            pos = "X{0[0]} Y{0[1]}".format(pos)
+            # Add pos option to force coordinate interpretation
+            options += " pos"
+            if not control:
+                control = pos
+            else:
+                options += " " + pos
+        # Setup window id args
+        cmd = "ControlClick, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}"
+        execute(cmd.format(control, title, text, button, count, 
+                           options, extitle, extext))
+
+    @_delay
+    def send(self, control="", keys="", raw=False):
+        """Send keystrokes to a control.
+
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlSend.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to send to.
+        :type control: str
+        :param keys: Keystrokes in the same format as ahk.Script.send.
+        :type keys: str
+        :param raw: Toggle raw input mode.
+        :type raw: bool (default False)
+        """
+        title, text, extitle, extext = self._params()
+
+        cmd = "ControlSend"
+        if raw:
+            cmd = "ControlSendRaw"
+        cmd = cmd + ", {0}, {1}, {2}, {3}, {4}" 
+        execute(cmd.format(control, keys, title, text, extitle, extext))
+
+    @_delay
+    def setText(self, control="", value=""):
+        """Set the text content of a control.
+
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ControlSetText.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to be changed.
+        :type control: str
+        :param value: New text to substitute into the control.
+        :type value: str
+        """
+        title, text, extitle, extext = self._params()
+
+        execute("ControlSetText, {0}, {1}, {2}, {3}, {4}, {5}".format(
+            control, value, title, text, extitle, extext))
+
+    def get_choices(self, control=""):
+        """Retrieve the available values from a ComboBox.
+
+        :param control: The class, HWND, or text of the control to be checked.
+        :type control: str
+        :returns: A list of value strings (in order).
+        """
+        title, text, extitle, extext = self._params()
+
+        execute("ControlGet, {0}, List,,{1},{2},{3},{4}".format(
+            self._tmpname, control, title, text, extitle, extext))
+        return get(self._tmpname).split('\n')
+
+    def get_chosen(self, control=""):
+        """Retrieve the selected value from a ComboBox.
+
+        :param control: The class, HWND, or text of the control to be checked.
+        :type control: str
+        :returns: Selected value as a string.
+        """
+        title, text, extitle, extext = self._params()
+
+        execute("ControlGet, {0}, Choice,,{1},{2},{3},{4}".format(
+            self._tmpname, control, title, text, extitle, extext))
+        return get(self._tmpname)
+
+    @_delay
+    def choose(self, control="", value=""):
+        """Pick an item from a listbox or combobox.
+
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Control.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to be changed.
+        :type control: str
+        :param value: Partial text of choice or choice index (starting from 1).
+        :type value: str or int (default="")
+        """
+        title, text, extitle, extext = self._params()
+
+        cmd = "ChooseString"
+        if type(value) not in (str, unicode):
+            cmd = "Choose"
+        execute("Control, {0}, {1}, {2}, {3}, {4}, {5}, {6}".format(  
+            cmd, value, control, title, text, extitle, extext))
+
+    def is_checked(self, control=""):
+        """Check if a checkbox control is checked.
+        
+        :param control: The class, HWND, or text of the control to be checked.
+        :type control: str
+        :returns: True if it was checked, else False.
+        """
+        title, text, extitle, extext = self._params()
+
+        execute("ControlGet, {0}, Checked,,{1},{2},{3},{4}".format(
+            self._tmpname, control, title, text, extitle, extext))
+        result = int(get(self._tmpname))
+        if result == 1:
+            return True
+        return False
+
+    @_delay
+    def check(self, control="", state=None):
+        """Pick an item from a listbox or combobox.
+
+        The state can be set to checked (True), unchecked (False), 
+        or toggle (None), default is toggle.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Control.htm>`_
+        for more details.
+
+        :param control: The class, HWND, or text of the control to be changed.
+        :type control: str
+        :param state: The desired checked state.
+        :type state: bool or None
+        """
+        title, text, extitle, extext = self._params()
+
+        cmd = "Check"
+        checked = self.is_checked(control)
+        if state is None:
+            if checked:
+                cmd = "Uncheck"
+        elif state and checked or not state and not checked:
+            return # Want to be checked and already checked = nop
+        elif not state and checked:
+            cmd = "Uncheck"
+
+        execute("Control, {0},, {1}, {2}, {3}, {4}, {5}".format(  
+            cmd, control, title, text, extitle, extext))

File ahk/script.py

+"""OO wrapper around The AutoHotKey library.
+
+This module contains higher-level object oriented wrappers around AHK scripting.
+"""
+import time
+from functools import partial
+from ahk import *
+
+class Function(object):
+    """Object wrapper around ahk functions"""
+    template = "\n{0}{1} {{\n{2}\n}}"
+
+    def __init__(self, name, result, args, body):
+        """
+        Called Functions are automatically transform to their result by calling
+        the provided result function on the return value from the ahk function
+        call.
+
+        :param name: The name of the function to wrap.
+        :type name: str
+        :param result: Type of the expected result.
+        :type result: callable type (default=str)
+        :param args: The argument definition of the function.
+        :type args: str (default='()')
+        :param body: The body of the function (excluding braces).
+        :type body: str (default='')
+        """
+        self.name = name
+        self.definition = self.template.format(name, args, body)
+        execute(self.definition)
+        self.result = result
+        # Because the ahkdll function call function is broken use temp
+        # variables as a work-around.
+        self.tmpname = "tmp{0}".format(str(int(time.time())))
+
+    def __call__(self, *args):
+        """Call the wrapped function and return the converted result."""
+        args = ",".join(str(i) for i in args)
+        # Execute the function and assign the result to the temp variable
+        execute("{0} := {1}({2})".format(
+            self.tmpname, self.name, args))
+        # Get the result from the temp variable
+        result = self.result(get(self.tmpname))
+        # And delete the temp variable value
+        set(self.tmpname, "")
+        return result
+
+class Script(object):
+    """Wrapper around ahk script commands."""
+
+    def __init__(self, script="", filename=None):
+        """
+        Initializes the ahk script engine just like calling the low-level
+        function ahk.start followed by ahk.ready.
+        """
+        self._handle = start(script=script, filename=filename)
+        ready()
+        self._vars = dict()
+        self._funcs = dict()
+        self._tmpname = "tmp{0}".format(str(int(time.time())))
+
+        # Add some default variables
+        self.variable('Clipboard')
+        self.variable('ErrorLevel', kind=partial(int, base=0))
+
+    def __del__(self):
+        """Call terminate to kill the script engine."""
+        terminate()
+
+    def variable(self, name, kind=str, value=''):
+        """Create a new ahk variable wrapper.
+
+        Wrapped variables are tracked by the instance and can be accessed as
+        object attributes. Variables are automatically transformed to their
+        declared kind by calling their kind with their ahk string value as the
+        first argument (just like normal python type conversions).
+
+        Variable wrappers are already provided for the special ahk Clipboard
+        and ErrorLevel variables (Note the case).
+
+        .. note::
+            Variables are not allowed to start with '_' the underscore char!
+        
+        :param name: Then name of the new variable.
+        :type name: str
+        :param kind: Type of the variable (cast on get).
+        :type kind: callable type (default=str)
+        :param value: Initial value of the variable.
+        :type value: match kind or str (default='')
+        :raises: AttributeError if the provided name already exists in the
+            instance as either a variable or an attribute.
+        """
+        if name[0] == '_':
+            raise AttributeError(
+                    "Variable names may not start with an underscore!")
+        if hasattr(self, name):
+            raise AttributeError(
+                    "Name: {0} already exists as an attribute!".format(name))
+        if name in self._vars:
+            raise AttributeError(
+                    "Name: {0} already exists as a variable!".format(name))
+        if name in self._funcs:
+            raise AttributeError(
+                    "Name: {0} already exists as a function!".format(name))
+
+        if set(name, value):
+            self._vars[name] = kind
+        else:
+            raise AttributeError(
+                    "Failure reported by ahk while setting {0}={1}!".format(
+                        name, value))
+
+    def function(self, name, result=str, args='()', body=''):
+        """Create a new ahk function wrapper.
+        
+        Wrapped functions are tracked by the instance and can be accessed as
+        object attributes. Functions are automatically transform to their
+        result by calling the provided result function on the return value from
+        the ahk function call.
+
+        .. note::
+            Function names are not allowed to start with '_' the underscore char!
+
+        :param name: The name of the function to wrap.
+        :type name: str
+        :param result: Type of the expected result.
+        :type result: callable type (default=str)
+        :param args: The argument definition of the function.
+        :type args: str (default='()')
+        :param body: The body of the function (excluding braces).
+        :type body: str (default='')
+        :raises: AttributeError if the indicated name is already used.
+        :returns: Function wrapper object.
+        """
+        if name[0] == '_':
+            raise AttributeError(
+                    "Function names may not start with an underscore!")
+        if hasattr(self, name):
+            raise AttributeError(
+                    "Name: {0} already exists as an attribute!".format(name))
+        if name in self._funcs:
+            raise AttributeError(
+                    "Name: {0} already exists as a function!".format(name))
+        if name in self._vars:
+            raise AttributeError(
+                    "Name: {0} already exists as a variable!".format(name))
+
+        func = Function(name, result, args, body)
+        self._funcs[name] = func
+        return func
+
+    def send(self, keys, mode='SendInput'):
+        """Convenience wrapper to send input to the active window.
+        
+        Sends a ahk formatted series of keystrokes to the active window.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Send.htm>`_
+        for more details.
+
+        :param keys: The keys to be sent.
+        :type keys: str
+        :param mode: The ahk command used to send keys. Valid modes:
+
+            * Send
+            * SendRaw
+            * SendInput
+            * SendPlay
+            * SendEvent
+
+        :type mode: str
+        """
+        execute("{0} {1}".format(mode, keys))
+
+    def click(self, button="", count=1, x=None, y=None):
+        """Convenience wrapper to the ahk click function.
+
+        Send a mouse click of any type to any coordinate with optional repeats.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/Click.htm>`_
+        for more details.
+        The button argument can actually take a number of options:
+
+            * `blank` - Simple primary click.
+            * right - Auxiliary button click.
+            * wheelup - Send mouse wheel scroll event.
+            * down - Press but don't release the primary button.
+            * rel[ative] - Interpret coordinates in relative mode.
+            * etc.
+
+        Button options can be composed when not mutually exclusive 
+        (e.g. "right down relative").
+
+        Use count=0 with x and y to move the mouse without clicking.
+
+        :param button: The mouse button(s) to click.
+        :type button: str (default="")
+        :param count: Number of times to repeat the click.
+        :type count: int (default 1)
+        :param x: Mouse x-coord.
+        :type x: int
+        :param y: Mouse y-coord.
+        :type y: int
+        """
+        cmd = "Click {0}".format(button)
+        if x or y: # Could fail for x&y = 0
+            cmd += ", {0}, {1}".format(x, y)
+        # Click count must occur "somewhere right of the coordinates"
+        if count != 1:
+            cmd += ", {0}".format(count)
+        execute(cmd+'\n')
+
+    def winActivate(self, title="", text="", 
+                    extitle="", extext="", bottom=False):
+        """Convenience wrapper for ahk WinActivate commands.
+        
+        Used to set a window with provided parameters as active.
+        If all parameters are empty the `last found` window is activated.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinActivate.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param bottom: Whether to act on the bottom-most window.
+        :type bottom: bool (default=False)
+        """
+        cmd = "WinActivate"
+        if bottom:
+            cmd = "WinActivateBottom"
+        if ''.join((title, text, extitle, extext)):
+            cmd = '{0}, "{1}", "{2}", "{3}", "{4}"'.format(
+                cmd, title, text, extitle, extext)
+        #print cmd
+        execute(cmd)
+
+    def winActive(self, title="", text="", extitle="", extext=""):
+        """Convenience wrapper for ahk IfWinActive command.
+        
+        Used to check if a window with provided parameters is active.
+        If all parameters are empty the `last found` window is checked.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinActive.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :returns: The found window's HWND or None.
+        """
+        set(self._tmpname, '')
+        execute('{0} := WinActive("{1}", "{2}", "{3}", "{4}")'.format(
+            self._tmpname, title, text, extitle, extext))
+        result = int(get(self._tmpname), 0)
+        if result == 0:
+            return None
+        return result
+
+    def winExist(self, title="", text="", extitle="", extext=""):
+        """Convenience wrapper for ahk IfWinExist command.
+        
+        Used to check if a window with provided parameters exists.
+        If all parameters are empty the `last found` window is checked.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/IfWinExist.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :returns: The found window's HWND or None.
+        """
+        set(self._tmpname, '')
+        execute('{0} := WinExist("{1}", "{2}", "{3}", "{4}")'.format(
+            self._tmpname, title, text, extitle, extext))
+        result = int(get(self._tmpname), 0)
+        if result == 0:
+            return None
+        return result
+
+    def waitActive(self, title="", text="", timeout=5, 
+                   extitle="", extext="", deactivate=False):
+        """Convenience wrapper for ahk WinWaitActive command.
+        
+        Used to wait until a window matching the given parameters is activated.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitActive.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param timeout: How long in seconds to wait for the window to activate.
+            Use None to wait indefinitely.
+        :type timeout: int or None (default=5)
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param deactivate: Toggle between WinWaitActive and WinWaitNotActive.
+        :type deactivate: bool (default=False)
+        :returns: The True if matching window is activated, else False.
+        """
+        cmd = "WinWaitActive"
+        if deactivate:
+            cmd = "WinWaitNotActive"
+        if timeout is None:
+            timeout = ""
+        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
+                cmd, title, text, timeout, extitle, extext))
+        result = self.ErrorLevel
+        if result != 0:
+            return False
+        return True
+
+    def waitWindow(self, title="", text="", timeout=5, 
+                   extitle="", extext="", closed=False):
+        """Convenience wrapper for ahk WinWait command.
+        
+        Used to wait until a window matching the given parameters exists.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/WinWait.htm>`_,
+        `AHK docs <http://www.autohotkey.com/docs/commands/WinWaitClose.htm>`_
+        for more details.
+
+        :param title: Partial window title text to match.
+        :type title: str (default="")
+        :param text: Partial window text to match.
+        :type text: str (default="")
+        :param timeout: How long in seconds to wait for the window to activate.
+            Use None to wait indefinitely.
+        :type timeout: int or None (default=5)
+        :param extitle: Partial window title text to avoid.
+        :type extitle: str (default="")
+        :param extext: Partial window text to avoid.
+        :type extext: str (default="")
+        :param closed: Toggle between WinWait and WinWaitClose.
+        :type closed: bool (default=False)
+        :returns: The True if matching window is activated, else False.
+        """
+        cmd = "WinWait"
+        if closed:
+            cmd = "WinWaitClose"
+        if timeout is None:
+            timeout = ""
+        execute("{0}, {1}, {2}, {3}, {4}, {5}".format(
+                cmd, title, text, timeout, extitle, extext))
+        result = self.ErrorLevel
+        if result != 0:
+            return False
+        return True
+
+    def convert_color(self, color):
+        """Convert ahk color returned as a hex string to tuple of ints."""
+        # Colors are returned as hex (e.g. 0xc0c0c0)
+        r, g, b = color[2:4], color[4:6], color[6:8]
+        return tuple((int(c, 16) for c in (r, g, b)))
+
+    def _color_delta(self, c1, c2):
+        """Compute the total error between to colors."""
+        err = 0.0
+        for i in range(3):
+            err += abs(c1[i] - c2[i])
+        return err/float(255*3)
+
+    def getPixel(self, x=0, y=0, opt='RGB', screen=True):
+        """Convenince wrapper around ahk PixelGetColor
+
+        Gets the pixel color at the indicated coordinates.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/PixelGetColor.htm>`_
+        for more details.
+
+        :param x: The pixel x coordinate (relative to screen).
+        :type x: int
+        :param y: The pixel y coordinate (relative to screen).
+        :type y: int
+        :param opt: Space separated color picking options.
+        :type opt: str (default='RGB')
+        :param screen: Flag to use screen or relative coordinates.
+        :type screen: bool (default=True)
+        """
+        if screen:
+            execute("CoordMode, Pixel, Screen")
+        else:
+            execute("CoordMode, Pixel, Reletive")
+
+        execute("PixelGetColor, {0}, {1}, {2}, {3}".format(
+            self._tmpname, x, y, opt))
+        return self.convert_color(get(self._tmpname))
+
+    def waitPixel(self, x=0, y=0, color=None, 
+                  threshold=0.01, interval=0.5, timeout=False):
+        """Wait until the pixel at given coords changes color.
+
+        This function can wait until the indicated pixel *is* a color,
+        or until it changes color. If a color is not provided the current
+        color of the pixel when the function starts is stored and compared
+        at a regular interval until it changes or until timeout.
+
+        :param x: The pixel x coordinate (relative to screen).
+        :type x: int
+        :param y: The pixel y coordinate (relative to screen).
+        :type y: int
+        :param color: The color to wait for.
+        :type color: tuple(int r, int g, int b) or None
+        :param threshold: Error factor allowed for determining color match.
+        :type threshold: float
+        :param interval: How often the pixel color is checked.
+        :type interval: float
+        :param timeout: How long to wait for a color match.
+        :type timeout: float or None
+        :returns: True if pixel changed, False if timeout.
+        """
+        # Get starting color
+        match = True # We are trying by default to match the provided color
+        if not color:
+            match = False # If not given a color then we are trying to not match
+            color = self.getPixel(x, y)
+
+        # Timing loop
+        start = time.time()
+        now = start
+        while 1:
+            # Get new timing for current iteration
+            now = time.time()
+            # Handle timeout condition
+            if timeout and (now - start) >= timeout:
+                return False
+            # Get new color reading
+            current = self.getPixel(x, y)
+            #print color, current
+            #print self._color_delta(color, current)
+            # Check for match
+            if self._color_delta(color, current) <= threshold:
+                if match:
+                    return True
+            else:
+                if not match:
+                    return True
+            # Pause
+            time.sleep(interval)
+
+    def message(self, text="Alert", title="Alert", options=0, timeout=None):
+        """Convenience wrapper to the ahk msgbox function.
+
+        Displays a message box with buttons.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/MsgBox.htm>`_
+        for more details.
+
+        :param text: The body text of the msgbox.
+        :type text: str (default='Alert')
+        :param title: The title text of the msgbox.
+        :type title: str (default='Alert')
+        :param options: Button flags bit-field.
+        :type options: int (default=0)
+        :param timeout: Optional timeout, dialog is closed after timeout.
+        :type timeout: int or None (default=None)
+        """
+        cmd = 'MsgBox {0}, {1}, {2}'.format(options, title, text)
+        if timeout:
+            cmd += ', {0}'.format(timeout)
+        execute(cmd)
+
+    def msgResult(self, name='OK'):
+        """Convenience wrapper to the ahk ifMsgBox function.
+
+        Get the clicked status of a button on the last MsgBox.
+        See `AHK docs <http://www.autohotkey.com/docs/commands/ifMsgBox.htm>`_
+        for more details.
+
+        :param name: The name of the button to check.
+        :type name: str (default='OK')
+        :returns: True if button was pressed, None if timeout, False otherwise.
+        """
+        set(self._tmpname, '0')
+        template = 'ifMsgBox, {0}\n\t{1} := 1\nifMsgBox, Timeout\n\t{1} := -1'
+        execute(template.format(name, self._tmpname))
+        result = int(get(self._tmpname))
+        if result == 1:
+            return True
+        elif result == -1:
+            return None
+        return False
+
+    def __getattr__(self, name):
+        """Override attribute lookup to add ahk variable access."""
+        #NOTE __getattr__ is only called on attrs that aren't found normally
+        if name in self._vars:
+            # The clipboard can't be directly read... Others?
+            execute("{0} := {1}".format(self._tmpname, name))
+            return self._vars[name](get(self._tmpname))
+        elif name in self._funcs:
+            return self._funcs[name]
+        else:
+            raise AttributeError("No variable named {0}!".format(name))
+
+    def __setattr__(self, name, value):
+        """Override attribute modification to add ahk variable access."""
+        # Also filter names starting with '_' (special names).
+        if name[0] == '_' or name in self.__dict__:
+            super(Script, self).__setattr__(name, value)
+        elif name in self._vars:
+            set(name, value)
+        elif name in self._funcs:
+            raise AttributeError("Can't assign to function {0}!".format(name))
+        else:
+            raise AttributeError("No variable named {0}!".format(name))
+Function wrappers
+=================
+These are lower level wrappers around the raw dll functions. Functions have
+been combined and simplified where reasonable. Only the minimal
+extraction/conversion has been preformed on c return types.
+
+functions
+---------
+   * :func:`.start`
+   * :func:`.ready`
+   * :func:`.add_lines`
+   * :func:`.execute`
+   * :func:`.jump`
+   * :func:`.call`
+   * :func:`.set`
+   * :func:`.get`
+   * :func:`.terminate`
+   * :func:`.reload`
+   * :func:`.find_func`
+   * :func:`.find_label`
+   * :func:`.pause`
+   * :func:`.exec_line`
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.start
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.ready
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.add_lines
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.execute
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.jump
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.call
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.set
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.get
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.terminate
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.reload
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.find_func
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.find_label
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.pause
+
+-------------------------------------------------------------------------------
+
+.. autofunction:: ahk.exec_line
 # built documents.
 #
 # The short X.Y version.
-version = '0.1.0'
+version = '0.2.0'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.0'
+release = '0.2.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

File doc/control.rst