Skip to content

Commit

Permalink
Merge pull request #140 from dictation-toolbox/feat/multiplatform-mouse
Browse files Browse the repository at this point in the history
Implement mouse and monitor functionality for X11 & Mac OS
  • Loading branch information
drmfinlay authored Sep 29, 2019
2 parents 60312e6 + 528c1ed commit d88bd71
Show file tree
Hide file tree
Showing 19 changed files with 982 additions and 290 deletions.
18 changes: 18 additions & 0 deletions documentation/monitor_classes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

Monitor classes
============================================================================

The monitor classes are how Dragonfly gets information on the available
monitors (a.k.a. screens, displays) for the current platform.

.. automodule:: dragonfly.windows.base_monitor
:members:

.. automodule:: dragonfly.windows.win32_monitor
:members:

.. automodule:: dragonfly.windows.x11_monitor
:members:

.. automodule:: dragonfly.windows.monitor
:members:
1 change: 1 addition & 0 deletions documentation/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ Contents of Dragonfly's windows sub-package:
:maxdepth: 2

clipboard
monitor_classes
window_classes
3 changes: 1 addition & 2 deletions dragonfly/actions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,17 @@
from .action_key import Key
from .action_text import Text
from .action_paste import Paste
from .action_mouse import Mouse
from .action_waitwindow import WaitWindow
from .action_focuswindow import FocusWindow
from .action_startapp import StartApp, BringApp

if sys.platform.startswith("win"):
# Import Windows only classes and functions.
from .action_mouse import Mouse
from .action_playsound import PlaySound
from .sendinput import (KeyboardInput, MouseInput,
HardwareInput, make_input_array,
send_input_array)
else:
# Import mocked classes and functions for other platforms.
from ..os_dependent_mock import Mouse
from ..os_dependent_mock import PlaySound
168 changes: 15 additions & 153 deletions dragonfly/actions/action_mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,133 +148,16 @@
"""

import time
import win32con

from ctypes import windll, pointer, c_long, c_ulong, Structure
from .sendinput import MouseInput, make_input_array, send_input_array
from .action_base import DynStrActionBase, ActionError
from ..windows.window import Window
from ..windows.monitor import monitors


#---------------------------------------------------------------------------

MOUSEEVENTF_HWHEEL = 0x1000 # taken from https://msdn.microsoft.com/en-us/library/windows/desktop/ms646273(v=vs.85).aspx

#---------------------------------------------------------------------------

class _point_t(Structure):
_fields_ = [
('x', c_long),
('y', c_long),
]

def get_cursor_position():
point = _point_t()
result = windll.user32.GetCursorPos(pointer(point))
if result: return (point.x, point.y)
else: return None

def set_cursor_position(x, y):
result = windll.user32.SetCursorPos(c_long(int(x)), c_long(int(y)))
if result: return False
else: return True


#---------------------------------------------------------------------------

class _EventBase(object):

def execute():
pass


class _Move(_EventBase):

def __init__(self, from_left, horizontal, from_top, vertical):
self.from_left = from_left
self.horizontal = horizontal
self.from_top = from_top
self.vertical = vertical
_EventBase.__init__(self)

def _move_relative(self, rectangle):
if self.from_left: horizontal = rectangle.x1
else: horizontal = rectangle.x2
if isinstance(self.horizontal, float):
distance = self.horizontal * rectangle.dx
else:
distance = self.horizontal
horizontal += distance

if self.from_top: vertical = rectangle.y1
else: vertical = rectangle.y2
if isinstance(self.vertical, float):
distance = self.vertical * rectangle.dy
else:
distance = self.vertical
vertical += distance

self._move_mouse(horizontal, vertical)

def _move_mouse(self, horizontal, vertical):
set_cursor_position(horizontal, vertical)


class _MoveWindow(_Move):

def execute(self, window):
self._move_relative(window.get_position())


class _MoveScreen(_Move):

def execute(self, window):
self._move_relative(monitors[0].rectangle)


class _MoveRelative(_Move):

def __init__(self, horizontal, vertical):
_Move.__init__(self, None, None, None, None)
self.horizontal = horizontal
self.vertical = vertical

def execute(self, window):
position = get_cursor_position()
if not position:
raise ActionError("Failed to retrieve cursor position.")
horizontal = position[0] + self.horizontal
vertical = position[1] + self.vertical
self._move_mouse(horizontal, vertical)


class _Button(_EventBase):

def __init__(self, *flags):
_EventBase.__init__(self)
self._flags = flags

def execute(self, window):
zero = pointer(c_ulong(0))
inputs = [MouseInput(0, 0, flag[1], flag[0], 0, zero)
for flag in self._flags]
array = make_input_array(inputs)
send_input_array(array)


class _Pause(_EventBase):

def __init__(self, interval):
_EventBase.__init__(self)
self._interval = interval

def execute(self, window):
time.sleep(self._interval)
from .mouse import (ButtonEvent, PauseEvent, MoveRelativeEvent,
MoveScreenEvent, MoveWindowEvent, PLATFORM_BUTTON_FLAGS,
PLATFORM_WHEEL_FLAGS)

# Imported for backwards-compatibility: these functions used to live here.
from .mouse import get_cursor_position, set_cursor_position

#---------------------------------------------------------------------------

class Mouse(DynStrActionBase):
""" Action that sends mouse events. """
Expand Down Expand Up @@ -322,15 +205,15 @@ 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 = _MoveWindow(h_origin, h_value, v_origin, v_value)
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_origin, h_value, v_origin, v_value = self._parse_position_pair(spec[1:-1])
event = _MoveScreen(h_origin, h_value, v_origin, v_value)
event = MoveScreenEvent(h_origin, h_value, v_origin, v_value)
events.append(event)
return True

Expand All @@ -342,33 +225,12 @@ def _process_relative_position(self, spec, events):
return False
horizontal = int(parts[0])
vertical = int(parts[1])
event = _MoveRelative(horizontal, vertical)
event = MoveRelativeEvent(horizontal, vertical)
events.append(event)
return True

_button_flags = {
"left": ((win32con.MOUSEEVENTF_LEFTDOWN, 0),
(win32con.MOUSEEVENTF_LEFTUP, 0)),
"right": ((win32con.MOUSEEVENTF_RIGHTDOWN, 0),
(win32con.MOUSEEVENTF_RIGHTUP, 0)),
"middle": ((win32con.MOUSEEVENTF_MIDDLEDOWN, 0),
(win32con.MOUSEEVENTF_MIDDLEUP, 0)),
"four": ((win32con.MOUSEEVENTF_XDOWN, 1),
(win32con.MOUSEEVENTF_XUP, 1)),
"five": ((win32con.MOUSEEVENTF_XDOWN, 2),
(win32con.MOUSEEVENTF_XUP, 2)),
}

_wheel_flags = {
"wheelup": (win32con.MOUSEEVENTF_WHEEL, 120),
"stepup": (win32con.MOUSEEVENTF_WHEEL, 40),
"wheeldown": (win32con.MOUSEEVENTF_WHEEL, -120),
"stepdown": (win32con.MOUSEEVENTF_WHEEL, -40),
"wheelright": (MOUSEEVENTF_HWHEEL, 120),
"stepright": (MOUSEEVENTF_HWHEEL, 40),
"wheelleft": (MOUSEEVENTF_HWHEEL, -120),
"stepleft": (MOUSEEVENTF_HWHEEL, -40),
}
_button_flags = PLATFORM_BUTTON_FLAGS
_wheel_flags = PLATFORM_WHEEL_FLAGS

def _process_button(self, spec, events):
parts = spec.split(":", 1)
Expand All @@ -380,24 +242,24 @@ def _process_button(self, spec, events):
flag_down, flag_up = self._button_flags[button]

if special == "down":
event = _Button(flag_down)
event = ButtonEvent(flag_down)
elif special == "up":
event = _Button(flag_up)
event = ButtonEvent(flag_up)
else:
try:
repeat = int(special)
except ValueError:
return False
flag_series = (flag_down, flag_up) * repeat
event = _Button(*flag_series)
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 = _Button(flag)
event = ButtonEvent(flag)
else:
return False

Expand All @@ -408,7 +270,7 @@ def _process_pause(self, spec, events):
if not spec.startswith("/"):
return False
interval = float(spec[1:]) / 100
event = _Pause(interval)
event = PauseEvent(interval)
events.append(event)
return True

Expand Down
3 changes: 1 addition & 2 deletions dragonfly/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@
from .action_key import Key
from .action_text import Text
from .action_paste import Paste
from .action_mouse import Mouse
from .action_waitwindow import WaitWindow
from .action_focuswindow import FocusWindow
from .action_startapp import StartApp, BringApp

if sys.platform.startswith("win"):
# Import Windows only classes and functions.
from .action_mouse import Mouse
from .action_playsound import PlaySound
else:
# Import mocked classes and functions for other platforms.
from ..os_dependent_mock import Mouse
from ..os_dependent_mock import PlaySound
68 changes: 68 additions & 0 deletions dragonfly/actions/mouse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#
# 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/>.
#


"""
This module initializes the mouse interface for the current platform.
"""

import os
import sys

# Import mouse events common to each platform.
from ._base import (EventBase, PauseEvent, MoveEvent, MoveRelativeEvent,
MoveScreenEvent, MoveWindowEvent)


# Import the mouse functions and classes for the current platform.
# Always use the base classes for building documentation.
DOC_BUILD = bool(os.environ.get("SPHINX_BUILD_RUNNING"))
if sys.platform.startswith("win") and not DOC_BUILD:
from ._win32 import (
ButtonEvent, get_cursor_position, set_cursor_position,
PLATFORM_BUTTON_FLAGS, PLATFORM_WHEEL_FLAGS
)

elif ((os.environ.get("XDG_SESSION_TYPE") == "x11" or
sys.platform == "darwin") and not DOC_BUILD):
from ._pynput import (
ButtonEvent, get_cursor_position, set_cursor_position,
PLATFORM_BUTTON_FLAGS, PLATFORM_WHEEL_FLAGS
)

else:
# No mouse interface is available. Dragonfly can function
# without this functionality, so don't raise an error or log any
# messages. Errors/messages will occur later if the mouse is used.
from ._base import (
BaseButtonEvent as ButtonEvent, get_cursor_position,
set_cursor_position, PLATFORM_BUTTON_FLAGS, PLATFORM_WHEEL_FLAGS
)


# Ensure all button and wheel flag names are accounted for.
# Used for debugging. Uncomment if adding new buttons or aliases.
# _BUTTON_NAMES = ["left", "right", "middle", "four", "five"]
# _WHEEL_NAMES = ["wheelup", "stepup", "wheeldown", "stepdown",
# "wheelright", "stepright", "wheelleft", "stepleft"]
# assert sorted(PLATFORM_BUTTON_FLAGS.keys()) == sorted(_BUTTON_NAMES),\
# "Mouse implementation is missing button flags"
# assert sorted(PLATFORM_WHEEL_FLAGS.keys()) == sorted(_WHEEL_NAMES),\
# "Mouse implementation is missing wheel flags"
Loading

0 comments on commit d88bd71

Please sign in to comment.