Source code for psychopy.iohub.devices.mouse

# -*- 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)
[docs] def getScroll(self): """ Returns the current vertical scroll value for the mouse. The vertical scroll value changes when the scroll wheel on a mouse is moved up or down. The vertical scroll value is in an arbitrary value space ranging for -32648 to +32648. Scroll position is initialize to 0 when the experiment starts. Args: None Returns: int: current vertical scroll value. """ return self._scrollPositionY
[docs] def setScroll(self, s): """ Sets the current vertical scroll value for the mouse. The vertical scroll value changes when the scroll wheel on a mouse is moved up or down. The vertical scroll value is in an arbitrary value space ranging for -32648 to +32648. Scroll position is initialize to 0 when the experiment starts. This method allows you to change the scroll value to anywhere in the valid value range. Args (int): The scroll position you want to set the vertical scroll to. Should be a number between -32648 to +32648. Returns: int: current vertical scroll value. """ if isinstance(s, (int, float, complex)): self._scrollPositionY = s return self._scrollPositionY
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)
[docs] class MouseScrollEvent(MouseInputEvent): """MouseScrollEvent's are generated when the scroll wheel on the Mouse Device (if it has one) is moved. Vertical scrolling is supported on all operating systems, horizontal scrolling is only supported on OS X. Each MouseScrollEvent provides the number of units the wheel was turned in each supported dimension, as well as the absolute scroll value for of each supported dimension. Event Type ID: EventConstants.MOUSE_SCROLL Event Type String: 'MOUSE_SCROLL' """ EVENT_TYPE_STRING = 'MOUSE_SCROLL' EVENT_TYPE_ID = EventConstants.MOUSE_SCROLL IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE __slots__ = [] def __init__(self, *args, **kwargs): """ :rtype : MouseScrollEvent :param args: :param kwargs: """ MouseInputEvent.__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)
[docs] class MouseButtonPressEvent(MouseButtonEvent): """MouseButtonPressEvent's are created when a button on the mouse is pressed. The button_state of the event will equal MouseConstants.MOUSE_BUTTON_STATE_PRESSED, and the button that was pressed (button_id) will be MouseConstants.MOUSE_BUTTON_LEFT, MouseConstants.MOUSE_BUTTON_RIGHT, or MouseConstants.MOUSE_BUTTON_MIDDLE, assuming you have a 3 button mouse. To get the current state of all three buttons on the Mouse Device, the pressed_buttons attribute can be read, which tracks the state of all three mouse buttons as an int that is equal to the sum of any pressed button id's ( MouseConstants.MOUSE_BUTTON_LEFT, MouseConstants.MOUSE_BUTTON_RIGHT, or MouseConstants.MOUSE_BUTTON_MIDDLE ). To tell if a given mouse button was depressed when the event occurred, regardless of which button triggered the event, you can use the following:: isButtonPressed = event.pressed_buttons & MouseConstants.MOUSE_BUTTON_xxx == MouseConstants.MOUSE_BUTTON_xxx where xxx is LEFT, RIGHT, or MIDDLE. For example, if at the time of the event both the left and right mouse buttons were in a pressed state:: buttonToCheck=MouseConstants.MOUSE_BUTTON_RIGHT isButtonPressed = event.pressed_buttons & buttonToCheck == buttonToCheck print isButtonPressed >> True buttonToCheck=MouseConstants.MOUSE_BUTTON_LEFT isButtonPressed = event.pressed_buttons & buttonToCheck == buttonToCheck print isButtonPressed >> True buttonToCheck=MouseConstants.MOUSE_BUTTON_MIDDLE isButtonPressed = event.pressed_buttons & buttonToCheck == buttonToCheck print isButtonPressed >> False Event Type ID: EventConstants.MOUSE_BUTTON_PRESS Event Type String: 'MOUSE_BUTTON_PRESS' """ EVENT_TYPE_STRING = 'MOUSE_BUTTON_PRESS' EVENT_TYPE_ID = EventConstants.MOUSE_BUTTON_PRESS IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE __slots__ = [] def __init__(self, *args, **kwargs): MouseButtonEvent.__init__(self, *args, **kwargs)
[docs] class MouseButtonReleaseEvent(MouseButtonEvent): """MouseButtonUpEvent's are created when a button on the mouse is released. The button_state of the event will equal MouseConstants.MOUSE_BUTTON_STATE_RELEASED, and the button that was pressed (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_BUTTON_RELEASE Event Type String: 'MOUSE_BUTTON_RELEASE' """ EVENT_TYPE_STRING = 'MOUSE_BUTTON_RELEASE' EVENT_TYPE_ID = EventConstants.MOUSE_BUTTON_RELEASE IOHUB_DATA_TABLE = MouseInputEvent.IOHUB_DATA_TABLE __slots__ = [] def __init__(self, *args, **kwargs): MouseButtonEvent.__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)

Back to top