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!

3 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.

Hi,

I’m having the same problem. In my experiment I have different set sizes of probes. I used opacity=0 to adjust the visible probes in each trial. However, some of my test participant accidentally clicked on these invisible probes which disrupts the results.

I tried the code but it does not work.
First, when I define the variable in the before experiment tab, I get an error that the variable does not exist. I defined it in the begin routine tab which seemed to work. (I used clickable_stimuli = []; because let clickable_stimuli = []; did not work)

This is what my code looks like right now:

clickable_stimuli = [];
clickable_stimuli.length = 0;
if ((MemSetSize === 3)) {
clickable_stimuli.push(Prac_Probe_1);
clickable_stimuli.push(Prac_Probe_1);
clickable_stimuli.push(Prac_Probe_2);
clickable_stimuli.push(Prac_Probe_3);
} else {
if ((MemSetSize === 4)) {
clickable_stimuli.push(Prac_Probe_1);
clickable_stimuli.push(Prac_Probe_2);
clickable_stimuli.push(Prac_Probe_3);
clickable_stimuli.push(Prac_Probe_4);
}
}

     Prac_mouse.status = PsychoJS.Status.STARTED;
  Prac_mouse.mouseClock.reset();
  prevButtonState = Prac_mouse.getPressed();  // if button is down already this ISN'T a new click
  }
if (Prac_mouse.status === PsychoJS.Status.STARTED) {  // only update if started and not finished!
  _mouseButtons = Prac_mouse.getPressed();
  if (!_mouseButtons.every( (e,i,) => (e == prevButtonState[i]) )) { // button state changed?
    prevButtonState = _mouseButtons;
    if (_mouseButtons.reduce( (e, acc) => (e+acc) ) > 0) { // state changed to a new click
      // check if the mouse was inside our 'clickable' objects
      gotValidClick = false;
      for (const obj of [clickable_stimuli[0], clickable_stimuli[1], clickable_stimuli[2] ,clickable_stimuli[3] , clickable_stimuli[4]]) {
        if (obj.contains(Prac_mouse)) {
          gotValidClick = true;
          Prac_mouse.clicked_name.push(obj.name)
        }
      }
      if (gotValidClick === true) { // abort routine on response
        continueRoutine = false;
      }
    }
  }
}

The error I get is: * TypeError: undefined is not an object (evaluating ‘obj.contains’)

Does anyone have an idea what I’m doing wrong?

As always: thanks in advance.

Best,

Kathrin

What is this for? It doesn’t seem to serve any purpose if you have clickable_stimuli = ; in the previous line.

Yes, absolutely correct. I simply forgot to delete it after putting clickable_stimuli = ; in the begin routine tab.
I delete this line of code and synced the project but I keep getting the error.

What I did now was getting rid of the condition and simply checking:

clickable_stimuli = ;
clickable_stimuli.push(Prac_Probe_1);
clickable_stimuli.push(Prac_Probe_1);
clickable_stimuli.push(Prac_Probe_2);
clickable_stimuli.push(Prac_Probe_3);

I printed the value clickable_stimuli[0] in the console to figure out what causes the problem.
This is the console output:
the name seems to be fine but I 'm not sure how it’s supposed to look like.

grafik

I wonder if the issue is that your clickable list needs to be the names of the clickable objects, not the objects themselves.

Prac_Probe_1 is the name of the object. The object itsself is a variable ($Probe1), defined in a condition file. I tried out using the object instead of the name of the clickable object but that didn’t work either. (clickable_stimuli.push(Probe1)).

Try

clickable_stimuli.push('Prac_Probe_1');

Unfortunately, it did not work. I made sure that the project synced correctly and checked the source code.
But I keep getting another error:

  • obj.contains is not a function. (In ‘obj.contains(Prac_mouse)’, ‘obj.contains’ is undefined)

I can’t wrap my head around this error.
Apparently, the list works, as the console output seems to show exactly the objects that should be clickable.
The crucial line seems to be fine, too.
for (const obj of [clickable_stimuli[0], clickable_stimuli[1], clickable_stimuli[2] ,clickable_stimuli[3] , clickable_stimuli[4]]) {

Here’s some code that I used in End Routine for a similar task when I had to check whether the object clicked was new or not. prac_img1 etc are the names of image components and image1 etc are the image variables (set by spreadsheet).

pracList = [[prac_img1, image1],[prac_img2, image2],[prac_img3, image3],[prac_img4, image4],[prac_img5, image5],[prac_img6, image6],[prac_img7, image7],[prac_img8, image8],[prac_img9, image9],[prac_img10, image10],[prac_img11, image11],[prac_img12, image12]]
for Idx in range(12):
    if prac_mouse.isPressedIn(pracList[Idx][0]):
        img_clicked = pracList[Idx][1]
thisExp.addData('img_clicked', img_clicked)

if img_clicked in prac_clicklist:
    prac_error = 1
else:
    prac_clicklist.append(img_clicked) 
if prac_reset == 1:
    prac_clicklist=[]

The routine was set to end on a valid click with clickable stimuli prac_img1, prac_img2, prac_img3, prac_img4, prac_img5, prac_img6, prac_img7, prac_img8, prac_img9, prac_img10, prac_img11, prac_img12