psychopy.org | Reference | Downloads | Github

Change the mouse-clickable-stimuli on each trial

Hi

I have an experiment with several response buttons. The set of buttons is different on each trial, so I added them all and I make some of them invisible in each trial.

My challenge is to make sure that only the relevant buttons can be clicked on each trial. So I used custom code to create an array called “clickableButtons” that contains the trial-specific list of clickable buttons, and in the mouse’s “clickable stimuli” property I wrote “clickableButtons” (this array).

This could have worked, the problem is that the code generator adds brackets around the value “clickableButtons”, i.e. in the generated code I get this line of code:
for obj in [clickableButtons]:

Obviously this is not what I wanted, I wanted
for obj in clickableButtons:

My current solution is just to edit the generated code. But it’s not ideal, and also I expect problems when I move to online experiments.

Is there a better solution?

Thanks!

Option 1: What happens if you just insert clickableButtons instead of $clickableButtons? I don’t know whether that is possible, but it might be worth a try.
Option 2: You could check mouse presses in a code component instead of using the ‘clickable stimuli’ property of your component. The function you would use is psychopy.event.Mouse.isPressedIn. With your list clickableButtons, you could check only a subset of your buttons.

Hi Lukas, and thanks for this response.

Option 1 doesn’t work - I still get [clickableButtons] in the generated code.

Option 2 - I am not sure I understand how to use it precisely. I still want the mouse click to end the trial, and I still want to save in the output file the reaction time and the button on which the mouse clicked. Can I do this when using custom code?

Thanks!
Dror

Option 1: Sorry, yes, I see now why that wouldn’t work. Could you try $*clickableButtons, or *clickableButtons, or *clickableButtons, with a comma? The asterisk in Python can unpack lists. If your problem is that you get a nested list [clickableButtons], unpacking the inner list may help. Another approach to unpacking the list may be $myClickableButton for myClickableButton in clickableButtons. Does any of these options work?

Option 2: Yes, it is possible. I haven’t tested this code. I simply generated a script with the builder and copied the parts from it that are relevant for you. You’ll have to insert it in the respective tabs. With this code, you don’t need a mouse component; the code creates a mouse object for you. However, if you have a mouse component somewhere else in your experiment with the same name (mouse), you may need to rename the mouse variable in the code below (replace every mouse with mouse2), or you can simply use that mouse component and leave out the Begin experiment part below.

## Begin experiment
mouse = event.Mouse(win=win)


## Begin routine
mouse.tStart = None
mouse.tStop = None
mouse.tStartRefresh = None
mouse.tStopRefresh = None
mouse.status = NOT_STARTED


## Every frame
# *mouse* updates
if mouse.status == NOT_STARTED and t >= 0.0-frameTolerance:
    # keep track of start time/frame for later
    mouse.frameNStart = frameN  # exact frame index
    mouse.tStart = t  # local t and not account for scr refresh
    mouse.tStartRefresh = tThisFlipGlobal  # on global time
    win.timeOnFlip(mouse, 'tStartRefresh')  # time at next scr refresh
    mouse.status = STARTED
    mouse.mouseClock.reset()
    prevButtonState = mouse.getPressed()  # if button is down already this ISN'T a new click
if mouse.status == STARTED:
    # is it time to stop? (based on global clock, using actual start)
    if tThisFlipGlobal > mouse.tStartRefresh + 1.0-frameTolerance:
        # keep track of stop time/frame for later
        mouse.tStop = t  # not accounting for scr refresh
        mouse.frameNStop = frameN  # exact frame index
        win.timeOnFlip(mouse, 'tStopRefresh')  # time at next scr refresh
        mouse.status = FINISHED
if mouse.status == STARTED:  # only update if started and not finished!
    buttons = mouse.getPressed()
    if buttons != prevButtonState:  # button state changed?
        prevButtonState = buttons
        if sum(buttons) > 0:  # state changed to a new click
            # check if the mouse was inside our 'clickable' objects
            gotValidClick = False
            for obj in clickableButtons: # !!! here goes your variable with the clickable objects !!!
                if obj.contains(mouse):
                    gotValidClick = True
                    mouse.clicked_name.append(obj.name)
            # abort routine on response
            continueRoutine = False



## End routine
# store data for thisExp (ExperimentHandler)
x, y = mouse.getPos()
buttons = mouse.getPressed()
if sum(buttons):
    # check if the mouse was inside our 'clickable' objects
    gotValidClick = False
    for obj in [polygon, polygon_2]:
        if obj.contains(mouse):
            gotValidClick = True
            mouse.clicked_name.append(obj.name)
thisExp.addData('mouse.x', x)
thisExp.addData('mouse.y', y)
thisExp.addData('mouse.leftButton', buttons[0])
thisExp.addData('mouse.midButton', buttons[1])
thisExp.addData('mouse.rightButton', buttons[2])
if len(mouse.clicked_name):
    thisExp.addData('mouse.clicked_name', mouse.clicked_name[0])
thisExp.addData('mouse.started', mouse.tStart)
thisExp.addData('mouse.stopped', mouse.tStop)

Thanks again, Lukas!

Unpacking won’t work, you can check and see that the following python code is invalid:
a = [1, 2, 3]
b = [(asterix)a]

The other alternative - writing “xxx for xxx in clickableButtons” - is a cool idea, however, Psychopy doesn’t allow this :frowning:

However, your response gave me another idea, which actually works. I’m writing it here in case someone else encounters a similar problem:
In my case, I have up to 5 response buttons. So I am using 5 variables b1, b2, b3, b4, and b5. I defined these 5 variables in a “begin routine” custom code: if the trial has 5 buttons, I set b1=button1, b2=button2, etc. If the trial has only one button, I set b1=button, b2=button, b3=button etc. (i.e. all 5 variables reference the same button). And so on. Then, in “clickable stimuli”, I write the list of 5 variables:
Clickable Stimuli: b1, b2, b3, b4, b5

Thanks for the help!

2 Likes

I’m glad you solved your problem. Out of interest I’d like to follow up the unpacking issue:

When I type in the same in my shell, it is valid code:

>>> a = [1, 2, 3]
>>> b = [*a]
>>> print(b)
[1, 2, 3]

Do you get a different result?

You’re right!

I was testing it on Python 2.7 by mistake. Indeed in Python 3 this syntax works.

Thanks!
Dror

1 Like

However Psychopy doesn’t allow the *clickableButtons syntax too…

1 Like

Thank you for checking anyway :slight_smile:

Thanks for sharing this. As of right now this still seems to be the best method to achieve this. I’ll share how exactly I had this laid out as I had to add the variable to the before experiment tab to get the mouse component to be able to access it.

I added a code component to the screen with the following code:

Before experiment

let clickable_stimuli = [];

Begin Routine

// Clear the clickable_stimuli array.
clickable_stimuli.length = 0;
if (drawer_locations === "top"){
    clickable_stimuli.push(drawer_top_left_selection);
    clickable_stimuli.push(drawer_top_right_selection);
    // The next two stimuli are deliberately repeated to prevent other stimuli from being clicked instead.
    clickable_stimuli.push(drawer_top_left_selection);
    clickable_stimuli.push(drawer_top_right_selection);
} else if (drawer_locations === "bottom"){
    clickable_stimuli.push(drawer_bottom_left_selection);
    clickable_stimuli.push(drawer_bottom_right_selection);
    // The next two stimuli are deliberately repeated to prevent other stimuli from being clicked instead.
    clickable_stimuli.push(drawer_bottom_left_selection);
    clickable_stimuli.push(drawer_bottom_right_selection);
} else if (drawer_locations === "both"){
    clickable_stimuli.push(drawer_top_left_selection);
    clickable_stimuli.push(drawer_top_right_selection);
    clickable_stimuli.push(drawer_bottom_left_selection);
    clickable_stimuli.push(drawer_bottom_right_selection);
}

Where the stimuli names are the drawers etc.

And in the mouse component:

Clickable stimuli

clickable_stimuli[0], clickable_stimuli[1], clickable_stimuli[2] ,clickable_stimuli[3]

Hopefully this is helpful, and maybe in the future there will be a more elegant way to do this.