# -*- coding: utf-8 -*-
# Part of the PsychoPy library
# Copyright (C) 2012-2020 iSolver Software Solutions (C) 2021 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).
from collections import namedtuple
import numpy as np
from .. import Device, Computer
from ...constants import EventConstants, DeviceConstants
from ...constants import MouseConstants, KeyboardConstants
from ...errors import print2err, printExceptionDetailsToStdErr
from psychopy.tools.monitorunittools import cm2pix, deg2pix, pix2cm, pix2deg
RectangleBorder = namedtuple('RectangleBorderClass', 'left top right bottom')
currentSec = Computer.getTime
# OS 'independent' view of the Mouse Device
[docs]
class MouseDevice(Device):
"""
"""
EVENT_CLASS_NAMES = [
'MouseInputEvent',
'MouseButtonEvent',
'MouseScrollEvent',
'MouseMoveEvent',
'MouseDragEvent',
'MouseButtonPressEvent',
'MouseButtonReleaseEvent',
'MouseMultiClickEvent']
DEVICE_TYPE_ID = DeviceConstants.MOUSE
DEVICE_TYPE_STRING = 'MOUSE'
__slots__ = [
'_lock_mouse_to_display_id',
'_scrollPositionY',
'_position',
'_clipRectsForDisplayID',
'_lastPosition',
'_display_index',
'_last_display_index',
'_isVisible',
'activeButtons']
def __init__(self, *args, **kwargs):
Device.__init__(self, *args, **kwargs)
self._clipRectsForDisplayID = {}
self._lock_mouse_to_display_id = None
self._scrollPositionY = 0
self._position = None
self._lastPosition = None
self._isVisible = 0
self._display_index = None
self._last_display_index = None
self.activeButtons = {
MouseConstants.MOUSE_BUTTON_LEFT: 0,
MouseConstants.MOUSE_BUTTON_RIGHT: 0,
MouseConstants.MOUSE_BUTTON_MIDDLE: 0,
}
def getCurrentButtonStates(self):
"""Returns a list of three booleans, representing the current state of
the MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, MOUSE_BUTTON_RIGHT ioHub
Mouse Device.
Args:
None
Returns:
(left_pressed, middle_pressed, right_pressed): Tuple of 3 bool
values where True == Pressed.
"""
return (self.activeButtons[MouseConstants.MOUSE_BUTTON_LEFT] != 0,
self.activeButtons[MouseConstants.MOUSE_BUTTON_MIDDLE] != 0,
self.activeButtons[MouseConstants.MOUSE_BUTTON_RIGHT] != 0)
def _initialMousePos(self):
"""If getPosition is called prior to any mouse events being received,
this method gets the current system cursor pos using ctypes.
"""
if self._position is None:
print2err('ERROR: _initialMousePos must be overwritten by '
'OS dependent implementation')
[docs]
def getPosition(self, return_display_index=False):
"""Returns the current position of the ioHub Mouse Device. Mouse
Position is in display coordinate units, with 0,0 being the center of
the screen.
Args:
return_display_index: If True, the display index that is
associated with the mouse position will also be returned.
Returns:
tuple: If return_display_index is false (default), return (x,
y) position of mouse. If return_display_index is True return ( (
x,y), display_index).
"""
self._initialMousePos()
if return_display_index is True:
return (tuple(self._position), self._display_index)
return tuple(self._position)
[docs]
def setPosition(self, pos, display_index=None):
"""Sets the current position of the ioHub Mouse Device. Mouse position
( pos ) should be specified in Display coordinate units, with 0,0 being
the center of the screen.
Args:
pos ( (x,y) list or tuple ): The position, in Display
coordinate space, to set the mouse position too.
display_index (int): Optional argument giving the display index
to set the mouse pos within. If None, the active ioHub Display
device index is used.
Returns:
tuple: new (x,y) position of mouse in Display coordinate space.
"""
try:
pos = pos[0], pos[1]
except Exception:
print2err('Warning: Mouse.setPosition: pos must be a list of '
'two numbers, not: ', pos)
return self._position
display = self._display_device
if display_index is None:
display_index = display.getIndex()
if 0 > display_index >= display.getDisplayCount():
print2err('Warning: Mouse.setPosition({},{}) failed. '
'Display Index must be between '
'0 and {}.'.format(pos, display_index,
display.getDisplayCount()-1))
return self._position
px, py = display._displayCoord2Pixel(pos[0], pos[1], display_index)
if not self._isPixPosWithinDisplay((px, py), display_index):
print2err('Warning: Mouse.setPosition({},{}) failed because '
'requested position ({} pix) does not fall within '
'specified display pixel bounds.'.format(pos,
display_index,
(px, py)))
return self._position
mouse_display_index = self.getDisplayIndexForMousePosition((px, py))
if mouse_display_index != display_index:
print2err(
' !!! requested display_index {0} != mouse_display_index '
'{1}'.format(
display_index, mouse_display_index))
print2err(' mouse.setPos did not update mouse pos')
else:
self._lastPosition = self._position
self._position = pos[0], pos[1]
self._last_display_index = self._display_index
self._display_index = mouse_display_index
self._nativeSetMousePos(px, py)
return self._position
def _desktopToWindowPos(self, dpos):
winfos = self._iohub_server._psychopy_windows
for w in winfos.values():
mx, my = dpos
wx, wy = w['pos'][0], w['pos'][1]
ww, wh = w['size'][0], w['size'][1]
if w['useRetina']:
ww = ww / 2
wh = wh / 2
if wx <= mx <= wx+ww:
if wy <= my <= wy + wh:
return w['handle'], mx - wx - ww/2, -(my - wy - wh/2)
return None, None, None
def _pix2windowUnits(self, win_handle, pos):
win = self._iohub_server._psychopy_windows.get(win_handle)
win_units = win['units']
monitor = win['monitor']
pos = np.asarray(pos)
if win_units == 'pix':
return pos
elif win_units == 'norm':
return pos * 2.0 / win['size']
elif win_units == 'cm':
if monitor:
return pix2cm(pos, monitor['monitor'])
else:
# should raise exception?
print2err("iohub Mouse error: Window is using units %s but has no Monitor definition." % win_units)
elif win_units == 'deg':
if monitor:
return pix2deg(pos, monitor['monitor'])
else:
# should raise exception?
print2err("iohub Mouse error: Window is using units %s but has no Monitor definition." % win_units)
elif win_units == 'height':
return pos / float(win['size'][1])
def _windowUnits2pix(self, win_handle, pos):
win = self._iohub_server._psychopy_windows.get(win_handle)
win_units = win['units']
monitor = win['monitor']
pos = np.asarray(pos)
if win_units == 'pix':
return pos
elif win_units == 'norm':
return pos * win['size'] / 2.0
elif win_units == 'cm':
if monitor:
return cm2pix(pos, monitor['monitor'])
else:
# should raise exception?
print2err("iohub Mouse error: Window is using units %s but has no Monitor definition." % win_units)
elif win_units == 'deg':
if monitor:
return deg2pix(pos, monitor['monitor'])
else:
# should raise exception
print2err("iohub Mouse error: Window is using units %s but has no Monitor definition." % win_units)
elif win_units == 'height':
return pos * float(win['size'][1])
def getDisplayIndex(self):
"""
Returns the current display index of the ioHub Mouse Device.
If the display index == the index of the display being used for
stimulus
presentation, then mouse position is in the display's coordinate units.
If the display index != the index of the display being used for
stimulus
presentation, then mouse position is in OS system mouse coordinate
space.
Args:
None
Returns:
(int): index of the Display the mouse is over. Display index's
range from 0 to N-1, where N is the number of Display's active
on the Computer.
"""
return self._display_index
[docs]
def getPositionAndDelta(self, return_display_index=False):
"""Returns a tuple of tuples, being the current position of the ioHub
Mouse Device as an (x,y) tuple, and the amount the mouse position
changed the last time it was updated (dx,dy). Mouse Position and Delta
are in display coordinate units.
Args:
None
Returns:
tuple: ( (x,y), (dx,dy) ) position of mouse, change in mouse
position, both in Display coordinate space.
"""
try:
self._initialMousePos()
cpos = self._position
lpos = self._lastPosition
if lpos is None:
lpos = cpos[0], cpos[1]
change_x = cpos[0] - lpos[0]
change_y = cpos[1] - lpos[1]
if return_display_index is True:
return (cpos, (change_x, change_y), self._display_index)
return cpos, (change_x, change_y)
except Exception as e:
print2err('>>ERROR getPositionAndDelta: ' + str(e))
printExceptionDetailsToStdErr()
if return_display_index is True:
return ((0.0, 0.0), (0.0, 0.0), self._display_index)
return (0.0, 0.0), (0.0, 0.0)
def getDisplayIndexForMousePosition(self, system_mouse_pos):
return self._display_device._getDisplayIndexForNativePixelPosition(
system_mouse_pos)
def _getClippedMousePosForDisplay(self, pixel_pos, display_index):
drti = self._display_device._getRuntimeInfoByIndex(display_index)
left, top, right, bottom = drti['bounds']
mx, my = pixel_pos
if mx < left:
mx = left
elif mx >= right:
mx = right - 1
if my < top:
my = top
elif my >= bottom:
my = bottom - 1
return mx, my
def _validateMousePosForActiveDisplay(self, pixel_pos):
left, top, right, bottom = self._display_device.getBounds()
mx, my = pixel_pos
mousePositionNeedsUpdate = False
if mx < left:
mx = left
mousePositionNeedsUpdate = True
elif mx >= right:
mx = right - 1
mousePositionNeedsUpdate = True
if my < top:
my = top
mousePositionNeedsUpdate = True
elif my >= bottom:
my = bottom - 1
mousePositionNeedsUpdate = True
if mousePositionNeedsUpdate:
return mx, my
return True
def _isPixPosWithinDisplay(self, pixel_pos, display_index):
d = self._display_device._getDisplayIndexForNativePixelPosition(pixel_pos)
return d == display_index
def _nativeSetMousePos(self, px, py):
print2err(
'ERROR: _nativeSetMousePos must be overwritten by OS '
'dependent implementation')
if Computer.platform == 'win32':
from .win32 import Mouse
elif Computer.platform.startswith('linux'):
from .linux2 import Mouse
elif Computer.platform == 'darwin':
from .darwin import Mouse
############# OS Independent Mouse Event Classes ####################
from .. import DeviceEvent
class MouseInputEvent(DeviceEvent):
"""The MouseInputEvent is an abstract class that is the parent of all
MouseInputEvent types that are supported in the ioHub.
Mouse position is mapped to the coordinate space defined in the
ioHub configuration file for the Display.
"""
PARENT_DEVICE = Mouse
EVENT_TYPE_STRING = 'MOUSE_INPUT'
EVENT_TYPE_ID = EventConstants.MOUSE_INPUT
IOHUB_DATA_TABLE = EVENT_TYPE_STRING
_newDataTypes = [
# gives the display index that the mouse was over for the event.
('display_id', np.uint8),
# 1 if button is pressed, 0 if button is released
('button_state', np.uint8),
# 1, 2,or 4, representing left, right, and middle buttons
('button_id', np.uint8),
# sum of currently active button int values
('pressed_buttons', np.uint8),
# x position of the position when the event occurred
('x_position', np.float64),
# y position of the position when the event occurred
('y_position', np.float64),
# horizontal scroll wheel position change when the event occurred (OS X
# only)
('scroll_dx', np.int8),
# horizontal scroll wheel abs. position when the event occurred (OS X
# only)
('scroll_x', np.int16),
# vertical scroll wheel position change when the event occurred
('scroll_dy', np.int8),
# vertical scroll wheel abs. position when the event occurred
('scroll_y', np.int16),
# indicates what modifier keys were active when the mouse event
# occurred.
('modifiers', np.uint32),
# window ID that the mouse was over when the event occurred
('window_id', np.uint64)
# (window does not need to have focus)
]
__slots__ = [e[0] for e in _newDataTypes]
def __init__(self, *args, **kwargs):
#: The id of the display that the mouse was over when the event
#: occurred.
#: Only supported on Windows at this time. Always 0 on other OS's.
self.display_id = None
#: 1 if button is pressed, 0 if button is released
self.button_state = None
#: MouseConstants.MOUSE_BUTTON_LEFT, MouseConstants.MOUSE_BUTTON_RIGHT
#: and MouseConstants.MOUSE_BUTTON_MIDDLE are int constants
#: representing left, right, and middle buttons of the mouse.
self.button_id = None
#: 'All' currently pressed button id's logically OR'ed together.
self.pressed_buttons = None
#: x position of the Mouse when the event occurred; in display
#: coordinate space.
self.x_position = None
#: y position of the Mouse when the event occurred; in display
#: coordinate space.
self.y_position = None
#: Horizontal scroll wheel position change when the event occurred.
#: OS X Only. Always 0 on other OS's.
self.scroll_dx = None
#: Horizontal scroll wheel absolute position when the event occurred.
#: OS X Only. Always 0 on other OS's.
self.scroll_x = None
#: Vertical scroll wheel position change when the event occurred.
self.scroll_dy = None
#: Vertical scroll wheel absolute position when the event occurred.
self.scroll_y = None
#: List of the modifiers that were active when the mouse event
#: occurred,
#: provided in online events as a list of the modifier constant labels
#: specified in iohub.ModifierConstants
#: list: Empty if no modifiers are pressed, otherwise each element is
# the string name of a modifier constant.
self.modifiers = 0
#: Window handle reference that the mouse was over when the event
#: occurred
#: (window does not need to have focus)
self.window_id = None
DeviceEvent.__init__(self, *args, **kwargs)
@classmethod
def _convertFields(cls, event_value_list):
modifier_value_index = cls.CLASS_ATTRIBUTE_NAMES.index('modifiers')
event_value_list[
modifier_value_index] = KeyboardConstants._modifierCodes2Labels(
event_value_list[modifier_value_index])
@classmethod
def createEventAsDict(cls, values):
cls._convertFields(values)
return dict(zip(cls.CLASS_ATTRIBUTE_NAMES, values))
# noinspection PyUnresolvedReferences
@classmethod
def createEventAsNamedTuple(cls, valueList):
cls._convertFields(valueList)
return cls.namedTupleClass(*valueList)
[docs]
class MouseMoveEvent(MouseInputEvent):
"""MouseMoveEvent's occur when the mouse position changes. Mouse position
is mapped to the coordinate space defined in the ioHub configuration file
for the Display.
Event Type ID: EventConstants.MOUSE_MOVE
Event Type String: 'MOUSE_MOVE'
"""
EVENT_TYPE_STRING = 'MOUSE_MOVE'
EVENT_TYPE_ID = EventConstants.MOUSE_MOVE
IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE
__slots__ = []
def __init__(self, *args, **kwargs):
MouseInputEvent.__init__(self, *args, **kwargs)
[docs]
class MouseDragEvent(MouseMoveEvent):
"""MouseDragEvents occur when the mouse position changes and at least one
mouse button is pressed. Mouse position is mapped to the coordinate space
defined in the ioHub configuration file for the Display.
Event Type ID: EventConstants.MOUSE_DRAG
Event Type String: 'MOUSE_DRAG'
"""
EVENT_TYPE_STRING = 'MOUSE_DRAG'
EVENT_TYPE_ID = EventConstants.MOUSE_DRAG
IOHUB_DATA_TABLE = MouseMoveEvent.IOHUB_DATA_TABLE
__slots__ = []
def __init__(self, *args, **kwargs):
MouseMoveEvent.__init__(self, *args, **kwargs)
class MouseButtonEvent(MouseInputEvent):
EVENT_TYPE_STRING = 'MOUSE_BUTTON'
EVENT_TYPE_ID = EventConstants.MOUSE_BUTTON
IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE
__slots__ = []
def __init__(self, *args, **kwargs):
"""
:rtype : MouseButtonEvent
:param args:
:param kwargs:
"""
MouseInputEvent.__init__(self, *args, **kwargs)
class MouseMultiClickEvent(MouseButtonEvent):
"""MouseMultiClickEvent's are created when you rapidly press and release a
mouse button two or more times. This event may never get triggered if your
OS does not support it. The button that was multi clicked (button_id) will
be MouseConstants.MOUSE_BUTTON_LEFT, MouseConstants.MOUSE_BUTTON_RIGHT, or
MouseConstants.MOUSE_BUTTON_MIDDLE, assuming you have a 3 button mouse.
Event Type ID: EventConstants.MOUSE_MULTI_CLICK
Event Type String: 'MOUSE_MULTI_CLICK'
"""
EVENT_TYPE_STRING = 'MOUSE_MULTI_CLICK'
EVENT_TYPE_ID = EventConstants.MOUSE_MULTI_CLICK
IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE
__slots__ = []
def __init__(self, *args, **kwargs):
MouseButtonEvent.__init__(self, *args, **kwargs)