#!/usr/bin/env python# -*- coding: utf-8 -*-"""Class for more control over the mouse,including the pointer graphic and bounding box."""# Part of the PsychoPy library# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.# Distributed under the terms of the GNU General Public License (GPL).importosimportpsychopy# so we can get the __path__frompsychopyimportevent,logging,visual,layoutfrompsychopy.visual.basevisualimportMinimalStimimportnumpy
[docs]classCustomMouse(MinimalStim):"""Class for more control over the mouse, including the pointer graphic and bounding box. This is a lazy-imported class, therefore import using full path `from psychopy.visual.custommouse import CustomMouse` when inheriting from it. Seems to work with pyglet or pygame. Not completely tested. Known limitations: * only norm units are working * getRel() always returns [0,0] * mouseMoved() is always False; maybe due to `self.mouse.visible == False` -> held at [0,0] * no idea if clickReset() works Author: Jeremy Gray, 2011 """def__init__(self,win,newPos=None,visible=True,leftLimit=None,topLimit=None,rightLimit=None,bottomLimit=None,showLimitBox=False,clickOnUp=False,pointer=None,name=None,autoLog=None):"""Class for customizing the appearance and behavior of the mouse. Use a custom mouse for extra control over the pointer appearance and function. It's probably slower to render than the regular system mouse. Create your `visual.Window` before creating a CustomMouse. Parameters ---------- win : required, `visual.Window` the window to which this mouse is attached visible : **True** or False makes the mouse invisible if necessary newPos : **None** or [x,y] gives the mouse a particular starting position leftLimit : left edge of a virtual box within which the mouse can move topLimit : top edge of virtual box rightLimit : right edge of virtual box bottomLimit : lower edge of virtual box showLimitBox : default is False display the boundary within which the mouse can move. pointer : The visual display item to use as the pointer; must have .draw() and setPos() methods. If your item has .setOpacity(), you can alter the mouse's opacity. clickOnUp : when to count a mouse click as having occurred default is False, record a click when the mouse is first pressed down. True means record a click when the mouse button is released. """# what local vars are defined (these are the init params) for use by# __repr__self._initParams=dir()self._initParams.remove('self')super(CustomMouse,self).__init__(name=name,autoLog=False)self.autoLog=False# set properly at end of initself.win=winself.mouse=event.Mouse(win=self.win)# maybe inheriting from Mouse would be easier? its not that simpleself.getRel=self.mouse.getRelself.getWheelRel=self.mouse.getWheelRelself.mouseMoved=self.mouse.mouseMoved# FAILSself.mouseMoveTime=self.mouse.mouseMoveTimeself.getPressed=self.mouse.getPressedself.clickReset=self.mouse.clickReset# ???self._pix2windowUnits=self.mouse._pix2windowUnitsself._windowUnits2pix=self.mouse._windowUnits2pix# the graphic to use as the 'mouse' icon (pointer)ifpointer:self.pointer=pointerelse:self.pointer=vm=visual.ShapeStim(win,vertices=[[-0.5,0.5],[-0.5,-0.35],[-0.3,-0.15],[-0.1,-0.5],[0.025,-0.425],[-0.175,-0.1],[0.1,-0.1],[-0.5,0.5],],fillColor="white",lineColor="black",lineWidth=1,anchor="top left",size=layout.Size((20,20),'pix',win),)self.mouse.setVisible(False)# hide the actual (system) mouseself.visible=visible# the custom (virtual) mouseself.leftLimit=self.rightLimit=Noneself.topLimit=self.bottomLimit=Noneself.setLimit(leftLimit=leftLimit,topLimit=topLimit,rightLimit=rightLimit,bottomLimit=bottomLimit)self.showLimitBox=showLimitBoxself.lastPos=Noneself.prevPos=NoneifnewPosisnotNone:self.lastPos=newPoselse:self.lastPos=self.mouse.getPos()# for counting clicks:self.clickOnUp=clickOnUpself.wasDown=False# state of mouse 1 frame prior to current frameself.clicks=0# how many mouse clicks since last resetself.clickButton=0# which button to count clicks for; 0 = left# set autoLog now that params have been initialisedwantLog=autoLogisNoneandself.win.autoLogself.__dict__['autoLog']=autoLogorwantLogifself.autoLog:logging.exp("Created %s = %s"%(self.name,str(self)))def_setPos(self,pos=None):"""internal mouse position management. setting a position here leads to the virtual mouse being out of alignment with the hardware mouse, which leads to an 'invisible wall' effect for the mouse. """ifposisNone:pos=self.getPos()else:self.lastPos=posself.pointer.setPos(pos)defsetPos(self,pos):"""Not implemented yet. Place the mouse at a specific position. """raiseNotImplementedError('setPos is not available for custom mouse')defgetPos(self):"""Returns the mouse's current position. Influenced by changes in .getRel(), constrained to be in its virtual box. """dx,dy=self.getRel()x=min(max(self.lastPos[0]+dx,self.leftLimit),self.rightLimit)y=min(max(self.lastPos[1]+dy,self.bottomLimit),self.topLimit)self.lastPos=numpy.array([x,y])returnself.lastPosdefdraw(self):"""Draw mouse (if it's visible), show the limit box, update the click count. """self._setPos()ifself.showLimitBox:self.box.draw()ifself.visible:self.pointer.draw()isDownNow=self.getPressed()[self.clickButton]ifself.clickOnUp:ifself.wasDownandnotisDownNow:# newly upself.clicks+=1else:ifnotself.wasDownandisDownNow:# newly downself.clicks+=1self.wasDown=isDownNowdefgetClicks(self):"""Return the number of clicks since the last reset"""returnself.clicksdefresetClicks(self):"""Set click count to zero"""self.clicks=0defgetVisible(self):"""Return the mouse's visibility state"""returnself.visibledefsetVisible(self,visible):"""Make the mouse visible or not (pyglet or pygame)."""self.visible=visibledefsetPointer(self,pointer):"""Set the visual item to be drawn as the mouse pointer."""ifhasattr(pointer,'draw')andhasattr(pointer,'setPos'):self.pointer=pointerelse:raiseAttributeError("need .draw() and .setPos() methods"" in pointer")defsetLimit(self,leftLimit=None,topLimit=None,rightLimit=None,bottomLimit=None):"""Set the mouse's bounding box by specifying the edges. """iftype(leftLimit)in(int,float):self.leftLimit=leftLimitelifself.leftLimitisNone:self.leftLimit=-1ifself.win.units=='pix':self.leftLimit=self.win.size[0]/-2.iftype(rightLimit)in(int,float):self.rightLimit=rightLimitelifself.rightLimitisNone:self.rightLimit=.99ifself.win.units=='pix':self.rightLimit=self.win.size[0]/2.0-5iftype(topLimit)in(int,float):self.topLimit=topLimitelifself.topLimitisNone:self.topLimit=1ifself.win.units=='pix':self.topLimit=self.win.size[1]/2.0iftype(bottomLimit)in(int,float):self.bottomLimit=bottomLimitelifself.bottomLimitisNone:self.bottomLimit=-0.98ifself.win.units=='pix':self.bottomLimit=self.win.size[1]/-2.0+10self.box=psychopy.visual.ShapeStim(self.win,vertices=[[self.leftLimit,self.topLimit],[self.rightLimit,self.topLimit],[self.rightLimit,self.bottomLimit],[self.leftLimit,self.bottomLimit],[self.leftLimit,self.topLimit]],opacity=0.35,autoLog=False)# avoid accumulated relative-offsets producing a different effective# limit:self.mouse.setVisible(True)self.lastPos=self.mouse.getPos()# hardware mouse's positionself.mouse.setVisible(False)