Skip to content

Commit

Permalink
Merge pull request pywinauto#938 from AleksandrPanov/mouse-tween
Browse files Browse the repository at this point in the history
added mouse tween
  • Loading branch information
vasily-v-ryabov authored Jun 11, 2020
2 parents 639811f + 605deb1 commit 187b84d
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 25 deletions.
37 changes: 31 additions & 6 deletions pywinauto/base_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@
except ImportError:
ImageGrab = None

from time import sleep
from .actionlogger import ActionLogger

from .mouse import _get_cursor_pos

#=========================================================================
def remove_non_alphanumeric_symbols(s):
Expand Down Expand Up @@ -633,7 +634,8 @@ def click_input(
pressed = "",
absolute = False,
key_down = True,
key_up = True):
key_up = True,
fast_move = False):
"""Click at the specified coordinates
* **button** The mouse button to click. One of 'left', 'right',
Expand Down Expand Up @@ -708,12 +710,34 @@ def release_mouse_input(
)

#-----------------------------------------------------------
def move_mouse_input(self, coords=(0, 0), pressed="", absolute=True):
def move_mouse_input(self, coords=(0, 0), pressed="", absolute=True, duration=0.0):
"""Move the mouse"""
if not absolute:
self.actions.log('Moving mouse to relative (client) coordinates ' + str(coords).replace('\n', ', '))

self.click_input(button='move', coords=coords, absolute=absolute, pressed=pressed)
coords = self.client_to_screen(coords) # make coords absolute

if not isinstance(duration, float):
raise TypeError("duration must be float (in seconds)")

minimum_duration = 0.05
if duration >= minimum_duration:
x_start, y_start = _get_cursor_pos()
delta_x = coords[0] - x_start
delta_y = coords[1] - y_start
max_delta = max(abs(delta_x), abs(delta_y))
num_steps = max_delta
sleep_amount = duration / max(num_steps, 1)
if sleep_amount < minimum_duration:
num_steps = int(num_steps * sleep_amount / minimum_duration)
sleep_amount = minimum_duration
delta_x /= max(num_steps, 1)
delta_y /= max(num_steps, 1)
for step in range(num_steps):
self.click_input(button='move',
coords=(x_start + int(delta_x * step), y_start + int(delta_y * step)),
absolute=True, pressed=pressed, fast_move=True)
sleep(sleep_amount)
self.click_input(button='move', coords=coords, absolute=True, pressed=pressed)

self.wait_for_idle()
return self
Expand All @@ -734,7 +758,8 @@ def drag_mouse_input(self,
src=None,
button="left",
pressed="",
absolute=True):
absolute=True,
duration=0.0):
"""Click on **src**, drag it and drop on **dst**
* **dst** is a destination wrapper object or just coordinates.
Expand Down
35 changes: 27 additions & 8 deletions pywinauto/controls/win_base_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ def click_input(
pressed = "",
absolute = False,
key_down = True,
key_up = True):
key_up = True,
fast_move = False):
"""Click at the specified coordinates
* **button** The mouse button to click. One of 'left', 'right',
Expand Down Expand Up @@ -212,7 +213,7 @@ def click_input(

_perform_click_input(button, coords, double, button_down, button_up,
wheel_dist=wheel_dist, pressed=pressed,
key_down=key_down, key_up=key_up)
key_down=key_down, key_up=key_up, fast_move=fast_move)

if message:
self.actions.log(message)
Expand All @@ -223,7 +224,8 @@ def drag_mouse_input(self,
src=None,
button="left",
pressed="",
absolute=True):
absolute=True,
duration=None):
"""Click on **src**, drag it and drop on **dst**
* **dst** is a destination wrapper object or just coordinates.
Expand All @@ -241,6 +243,15 @@ def drag_mouse_input(self,
if dst == src:
raise AttributeError("Can't drag-n-drop on itself")

if not isinstance(duration, float) and duration is not None:
raise TypeError("duration must be float (in seconds) or None")

if isinstance(duration, float):
total_pause = 0.5 + Timings.before_drag_wait + Timings.before_drop_wait + Timings.after_drag_n_drop_wait
if duration < total_pause:
raise ValueError("duration must be >= " + str(total_pause))
duration -= total_pause

if isinstance(src, WinBaseWrapper):
press_coords = src._calc_click_coords()
elif isinstance(src, win32structures.POINT):
Expand All @@ -258,16 +269,24 @@ def drag_mouse_input(self,

self.press_mouse_input(button, press_coords, pressed, absolute=absolute)
time.sleep(Timings.before_drag_wait)
for i in range(5):
self.move_mouse_input((press_coords[0] + i, press_coords[1]), pressed=pressed, absolute=absolute) # "left"
time.sleep(Timings.drag_n_drop_move_mouse_wait)
self.move_mouse_input(release_coords, pressed=pressed, absolute=absolute) # "left"

if duration is None:
duration = 0.0
# this is necessary for testDragMouseInput
for i in range(5):
self.move_mouse_input((press_coords[0] + i, press_coords[1]), pressed=pressed, absolute=absolute)
time.sleep(Timings.drag_n_drop_move_mouse_wait)

self.move_mouse_input(release_coords, pressed=pressed, absolute=absolute, duration=duration)

self.move_mouse_input(release_coords, pressed=pressed, absolute=absolute) # "left"
time.sleep(Timings.before_drop_wait)

self.release_mouse_input(button, release_coords, pressed, absolute=absolute)
time.sleep(Timings.after_drag_n_drop_wait)
return self

#-----------------------------------------------------------
# -----------------------------------------------------------
def type_keys(
self,
keys,
Expand Down
55 changes: 44 additions & 11 deletions pywinauto/mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

import sys
import time
from math import ceil
if sys.platform == 'win32':
import pywintypes
import win32api
Expand All @@ -52,6 +53,9 @@

if sys.platform == 'win32':

def _get_cursor_pos(): # get global coordinates
return win32api.GetCursorPos()

def _set_cursor_pos(coords):
"""Wrapped SetCursorPos that handles non-active desktop case (coords is a tuple)"""
try:
Expand All @@ -72,6 +76,7 @@ def _perform_click_input(
pressed="",
key_down=True,
key_up=True,
fast_move=False
):
"""Perform a click action using SendInput
Expand Down Expand Up @@ -134,7 +139,8 @@ def _perform_click_input(

# set the cursor position
_set_cursor_pos((coords[0], coords[1]))
time.sleep(Timings.after_setcursorpos_wait)
if not fast_move:
time.sleep(Timings.after_setcursorpos_wait)
if win32api.GetCursorPos() != (coords[0], coords[1]):
_set_cursor_pos((coords[0], coords[1]))
time.sleep(Timings.after_setcursorpos_wait)
Expand All @@ -159,16 +165,16 @@ def _perform_click_input(
if button.lower() == 'move':
x_res = win32functions.GetSystemMetrics(win32defines.SM_CXSCREEN)
y_res = win32functions.GetSystemMetrics(win32defines.SM_CYSCREEN)
x_coord = int(float(coords[0]) * (65535. / float(x_res - 1)))
y_coord = int(float(coords[1]) * (65535. / float(y_res - 1)))
x_coord = int(ceil(coords[0] * 65535 / (x_res - 1.))) # in Python 2.7 return float val
y_coord = int(ceil(coords[1] * 65535 / (y_res - 1.))) # in Python 2.7 return float val
win32api.mouse_event(dw_flags, x_coord, y_coord, dw_data)
else:
for event in events:
if event == win32defines.MOUSEEVENTF_MOVE:
x_res = win32functions.GetSystemMetrics(win32defines.SM_CXSCREEN)
y_res = win32functions.GetSystemMetrics(win32defines.SM_CYSCREEN)
x_coord = int(float(coords[0]) * (65535. / float(x_res - 1)))
y_coord = int(float(coords[1]) * (65535. / float(y_res - 1)))
x_coord = int(ceil(coords[0] * 65535 / (x_res - 1.))) # in Python 2.7 return float val
y_coord = int(ceil(coords[1] * 65535 / (y_res - 1.))) # in Python 2.7 return float val
win32api.mouse_event(
win32defines.MOUSEEVENTF_MOVE | win32defines.MOUSEEVENTF_ABSOLUTE,
x_coord, y_coord, dw_data)
Expand All @@ -177,7 +183,8 @@ def _perform_click_input(
event | win32defines.MOUSEEVENTF_ABSOLUTE,
coords[0], coords[1], dw_data)

time.sleep(Timings.after_clickinput_wait)
if not fast_move:
time.sleep(Timings.after_clickinput_wait)

if ('control' in keyboard_keys) and key_up:
keyboard.VirtualKeyAction(keyboard.VK_CONTROL, down=False).run()
Expand All @@ -189,16 +196,23 @@ def _perform_click_input(

else:
_display = Display()

# TODO: check this method
def _get_cursor_pos(): # get global coordinate
data = _display.screen().root.query_pointer()._data
return data["root_x"], data["root_y"]

def _perform_click_input(button='left', coords=(0, 0),
button_down=True, button_up=True, double=False,
wheel_dist=0, pressed="", key_down=True, key_up=True):
wheel_dist=0, pressed="", key_down=True, key_up=True,
fast_move=False):
"""Perform a click action using Python-xlib"""
#Move mouse
x = int(coords[0])
y = int(coords[1])
fake_input(_display, X.MotionNotify, x=x, y=y)
_display.sync()

if not fast_move:
_display.sync()
if button == 'wheel':
if wheel_dist == 0:
return
Expand Down Expand Up @@ -238,9 +252,28 @@ def right_click(coords=(0, 0)):
_perform_click_input(button='right', coords=coords)


def move(coords=(0, 0)):
def move(coords=(0, 0), duration=0.0):
"""Move the mouse"""
_perform_click_input(button='move',coords=coords,button_down=False,button_up=False)
if not isinstance(duration, float):
raise TypeError("duration must be float (in seconds)")
minimum_duration = 0.05
if duration >= minimum_duration:
x_start, y_start = _get_cursor_pos()
delta_x = coords[0] - x_start
delta_y = coords[1] - y_start
max_delta = max(abs(delta_x), abs(delta_y))
num_steps = max_delta
sleep_amount = duration / max(num_steps, 1)
if sleep_amount < minimum_duration:
num_steps = int(num_steps * sleep_amount / minimum_duration)
sleep_amount = minimum_duration
delta_x /= max(num_steps, 1)
delta_y /= max(num_steps, 1)
for step in range(num_steps):
_perform_click_input(button='move', coords=(x_start + int(delta_x*step), y_start + int(delta_y*step)),
button_down=False, button_up=False, fast_move=True)
time.sleep(sleep_amount)
_perform_click_input(button='move', coords=coords, button_down=False, button_up=False)


def press(button='left', coords=(0, 0)):
Expand Down
50 changes: 50 additions & 0 deletions pywinauto/unittests/test_mouse.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,56 @@ def test_wheel_click(self):
self.assertTrue("Mouse Release" in data)
self.assertTrue("MiddleButton" in data)

# TODO: make the feature and the tests cross-platform (duration param)
if sys.platform == "win32":
def test_mouse_can_move_cursor(self):
coord = (0, 1)
mouse.move(coord)
self.assertEqual(coord, mouse._get_cursor_pos())

mouse.move((-200, -300))
self.assertEqual((0, 0), mouse._get_cursor_pos())

def test_mouse_fail_on_int_duration_and_float_coord(self):
self.assertRaises(TypeError, mouse.move, coord=(0, 0), duration=1)
self.assertRaises(TypeError, mouse.move, coord=(0.0, 0))

def test_mouse_tween(self):
coord = (401, 301)
mouse.move(coord, duration=0.5)
self.assertEqual(coord, mouse._get_cursor_pos())

mouse.move(coord, duration=0.5)
self.assertEqual(coord, mouse._get_cursor_pos())

def test_move_mouse_input_tween(self):
coord = (1, 2)
self.dlg.move_mouse_input(coords=coord, absolute=True)
self.assertEqual(coord, mouse._get_cursor_pos())
coord = (501, 401)
self.dlg.move_mouse_input(coords=coord, absolute=True, duration=0.5)
self.assertEqual(coord, mouse._get_cursor_pos())
self.dlg.move_mouse_input(coords=coord, absolute=True, duration=0.5)
self.assertEqual(coord, mouse._get_cursor_pos())

def test_drag_mouse_input_tween(self):
rect = self.dlg.rectangle()
x0, y0 = rect.left, rect.top
x1, y1 = 10, 50
x0_curs, y0_curs = (rect.left + rect.right) // 2, rect.top + 10
x1_curs, y1_curs = (rect.right - rect.left) // 2 + x1, 10 + y1

mouse.move((x0_curs, y0_curs))
self.assertEqual((x0_curs, y0_curs), mouse._get_cursor_pos())

self.dlg.drag_mouse_input(src=(x0_curs, y0_curs), dst=(x1_curs, y1_curs), absolute=True)
rect = self.dlg.rectangle()
self.assertEqual((rect.left, rect.top), (x1, y1))

self.dlg.drag_mouse_input(src=(x1_curs, y1_curs), dst=(x0_curs, y0_curs), absolute=True, duration=1.0)
rect = self.dlg.rectangle()
self.assertEqual((rect.left, rect.top), (x0, y0))

if sys.platform != 'win32':
def test_swapped_buttons(self):
current_map = self.display.get_pointer_mapping()
Expand Down

0 comments on commit 187b84d

Please sign in to comment.