#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
# Dragonfly is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Dragonfly is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with Dragonfly. If not, see
# <http://www.gnu.org/licenses/>.
#
"""
Mouse action
============================================================================
This section describes the :class:`Mouse` action object. This type of
action is used for controlling the mouse cursor and clicking mouse
button.
Below you'll find some simple examples of :class:`Mouse` usage, followed
by a detailed description of the available mouse events.
Example mouse actions
............................................................................
The following code moves the mouse cursor to the center of the foreground
window (``(0.5, 0.5)``) and then clicks the left mouse button once
(``left``)::
# Parentheses ("(...)") give foreground-window-relative locations.
# Fractional locations ("0.5", "0.9") denote a location relative to
# the window or desktop, where "0.0, 0.0" is the top-left corner
# and "1.0, 1.0" is the bottom-right corner.
action = Mouse("(0.5, 0.5), left")
action.execute()
The line below moves the mouse cursor to 100 pixels left of the primary
monitor's left edge (if possible) and 250 pixels down from its top edge
(``[-100, 250]``), and then double clicks the right mouse button
(``right:2``)::
# Square brackets ("[...]") give desktop-relative locations.
# Integer locations ("1", "100", etc.) denote numbers of pixels.
# Negative numbers ("-100") are counted from the left-edge of the
# primary monitor. They are used to access monitors above or to the
# left of the primary monitor.
Mouse("[-100, 250], right:2").execute()
The following command drags the mouse from the top right corner of the
foreground window (``(0.9, 10), left:down``) to the bottom left corner
(``(25, -0.1), left:up``)::
Mouse("(0.9, 10), left:down, (25, -0.1), left:up").execute()
The code below moves the mouse cursor 25 pixels right and 25 pixels up
(``<25, -25>``)::
# Angle brackets ("<...>") move the cursor from its current position
# by the given number of pixels.
Mouse("<25, -25>").execute()
Mouse specification format
............................................................................
The *spec* argument passed to the :class:`Mouse` constructor specifies
which mouse events will be emulated. It is a string consisting of one or
more comma-separated elements. Each of these elements has one of the
following possible formats:
Mouse movement actions:
- move the cursor relative to the top-left corner of the desktop monitor
containing coordinates ``[0, 0]`` (i.e. the primary monitor):
``[`` *number* ``,`` *number* ``]``
- move the cursor relative to the foreground window:
``(`` *number* ``,`` *number* ``)``
- move the cursor relative to its current position:
``<`` *pixels* ``,`` *pixels* ``>``
In the above specifications, the *number* and *pixels* have the
following meanings:
- *number* -- can specify a number of pixels or a fraction of
the reference window or desktop. For example:
- ``(10, 10)`` -- 10 pixels to the right and down from the
foreground window's left-top corner
- ``(0.5, 0.5)`` -- center of the foreground window
- *pixels* -- specifies the number of pixels
Mouse button-press action:
*keyname* [``:`` *repeat*] [``/`` *pause*]
- *keyname* -- Specifies which mouse button to click:
- ``left`` -- left mouse button key
- ``middle`` -- middle mouse button key
- ``right`` -- right mouse button key
- ``four`` -- fourth mouse button key
- ``five`` -- fifth mouse button key
- ``wheelup`` -- mouse wheel up
- ``stepup`` -- mouse wheel up 1/3
- ``wheeldown`` -- mouse wheel down
- ``stepdown`` -- mouse wheel down 1/3
- ``wheelright`` -- mouse wheel right
- ``stepright`` -- mouse wheel right 1/3
- ``wheelleft`` -- mouse wheel left
- ``stepleft`` -- mouse wheel left 1/3
- *repeat* -- Specifies how many times the button should be clicked:
- ``0`` -- don't click the button, this is a no-op
- ``1`` -- normal button click
- ``2`` -- double-click
- ``3`` -- triple-click
- *pause* --
Specifies how long to pause *after* clicking the button. The value
should be an integer giving in hundredths of a second. For example,
``/100`` would mean one second, and ``/50`` half a second.
Mouse button-hold or button-release action:
*keyname* ``:`` *hold-or-release* [``/`` *pause*]
- *keyname* -- Specifies which mouse button to click; same as above.
- *hold-or-release* --
Specified whether the button will be held down or released:
- ``down`` -- hold the button down
- ``up`` -- release the button
- *pause* --
Specifies how long to pause *after* clicking the button; same as above.
Mouse across platforms
............................................................................
Please note that there are some platforms which do not support emulating
every mouse button listed above. If an unsupported mouse button (*keyname*)
is specified and the :class:`Mouse` action executed, an error is raised. For
instance, scrolling the mouse wheel horizontally (e.g. *wheelleft*) is not,
by default, a supported operation on X11::
ValueError: Unsupported scroll event: wheelleft
Fortunately, this particular problem can be fixed by installing the *pynput*
library::
pip install pynput
On MacOS, however, Dragonfly cannot be used to scroll horizontally.
Mouse class reference
............................................................................
"""
# pylint: disable=R0201
# Suppress warnings about handler functions defined as Mouse methods.
from dragonfly.actions.action_base import DynStrActionBase, ActionError
from dragonfly.windows.window import Window
from dragonfly.actions.mouse import (
ButtonEvent, PauseEvent, MoveRelativeEvent,
MoveScreenEvent, MoveWindowEvent, PLATFORM_BUTTON_FLAGS,
PLATFORM_WHEEL_FLAGS
)
# Imported for backwards-compatibility: these functions used to live here.
from dragonfly.actions.mouse import get_cursor_position, set_cursor_position
[docs]class Mouse(DynStrActionBase):
""" Action that sends mouse events. """
def __init__(self, spec=None, static=False):
"""
Arguments:
- *spec* (*str*) -- the mouse actions to execute
- *static* (boolean) --
if *True*, do not dynamically interpret *spec*
when executing this action
"""
DynStrActionBase.__init__(self, spec=spec, static=static)
def _parse_spec(self, spec):
""" Convert the given *spec* to keyboard events. """
events = []
parts = self._split_parts(spec)
for part in parts:
handled = False
for handler in self._handlers:
try:
if handler(self, part, events):
handled = True
break
except Exception:
continue
if not handled:
raise ActionError("Invalid mouse spec: %r (in %r)"
% (part, spec))
return events
def _parse_position_pair(self, spec):
parts = spec.split(",")
if len(parts) != 2:
raise ValueError("Invalid position pair spec: %r" % spec)
h_origin, h_value = self._parse_position(parts[0])
v_origin, v_value = self._parse_position(parts[1])
return (h_origin, h_value, v_origin, v_value)
def _process_window_position(self, spec, events):
if not spec.startswith("(") or not spec.endswith(")"):
return False
h_origin, h_value, v_origin, v_value = self._parse_position_pair(spec[1:-1])
event = MoveWindowEvent(h_origin, h_value, v_origin, v_value)
events.append(event)
return True
def _process_screen_position(self, spec, events):
if not spec.startswith("[") or not spec.endswith("]"):
return False
_, h_value, _, v_value = self._parse_position_pair(spec[1:-1])
event = MoveScreenEvent(True, h_value, True, v_value)
events.append(event)
return True
def _process_relative_position(self, spec, events):
if not spec.startswith("<") or not spec.endswith(">"):
return False
parts = spec[1:-1].split(",")
if len(parts) != 2:
return False
horizontal = int(parts[0])
vertical = int(parts[1])
event = MoveRelativeEvent(horizontal, vertical)
events.append(event)
return True
_button_flags = PLATFORM_BUTTON_FLAGS
_wheel_flags = PLATFORM_WHEEL_FLAGS
def _process_button(self, spec, events):
parts = spec.split(":", 1)
button = parts[0].strip()
if len(parts) == 1: special = 1
else: special = parts[1].strip()
if button in self._button_flags:
flag_down, flag_up = self._button_flags[button]
if special == "down":
event = ButtonEvent(flag_down)
elif special == "up":
event = ButtonEvent(flag_up)
else:
try:
repeat = int(special)
except ValueError:
return False
flag_series = (flag_down, flag_up) * repeat
event = ButtonEvent(*flag_series)
elif button in self._wheel_flags:
flag = self._wheel_flags[button]
try:
repeat = int(special)
except ValueError:
return False
flag = (flag[0], repeat * flag[1])
event = ButtonEvent(flag)
else:
return False
events.append(event)
return True
def _process_pause(self, spec, events):
if not spec.startswith("/"):
return False
interval = float(spec[1:]) / 100
event = PauseEvent(interval)
events.append(event)
return True
_handlers = [
_process_window_position,
_process_screen_position,
_process_relative_position,
_process_button,
_process_pause,
]
def _parse_position(self, spec):
spec = spec.strip()
if spec.startswith("-"): origin = False
else: origin = True
if spec.find(".") != -1: value = float(spec)
else: value = int(spec)
return (origin, value)
def _split_parts(self, spec):
delimiters = ["()", "[]", "<>"]
parts = spec.split(",")
items = []
while parts:
part = parts.pop(0).strip()
found_delimiter = False
for begin, end in delimiters:
if begin not in part:
continue
item = [part]
while parts:
part = parts.pop(0)
item.append(part)
if end in part:
break
if end not in part:
raise Exception("No closing delimiter found for %r" % item[0])
new_item = ",".join(item)
found_delimiter = True
break
if not found_delimiter:
new_item = part
if "/" in new_item:
before, after = new_item.split("/", 1)
items.append(before.strip())
items.append("/" + after.strip())
else:
items.append(new_item)
return items
#-----------------------------------------------------------------------
def _execute_events(self, events):
""" Send events. """
window = Window.get_foreground()
for event in events:
event.execute(window)
def __str__(self):
return '<{}>'.format(self._spec)