Visual analog scale: press and hold key

Description of the problem:
This code creates a visual analog scale which moves left/right via keyboard input. As is, the slider moves one discrete step for every key press. We instead want to press and hold these keys to make the slider move continuously - on key-down the slider begins moving until the key is released.

Thanks for the help!

from psychopy import visual, event
from psychopy.hardware import keyboard

def init_objects(x, y):
    kb = keyboard.Keyboard()
    win = visual.Window(size=(x,y), pos=(10, 50), color='white', monitor='testMonitor', fullscr=False, screen=0)
    title = visual.TextStim(win=win, text=None, color='black', pos=[0,.25])
    outline = visual.Rect(win=win, width=1, height=.2)
    outline.lineColor = 'black'
    outline.lineWidth = 2.3
    rect = visual.Rect(win=win, width=.01, height=.2)
    rect.fillColor = 'red'
    pain0 = visual.TextStim(win=win, text='NO PAIN', color='black', height=.06, pos=[-.6, 0])
    pain10 = visual.TextStim(win=win, text='WORST PAIN\nIMAGINABLE', color='black', height=.06, pos=[.65, 0])
    return kb, win, title, outline, rect, pain0, pain10

def reset(title, rect, pain_type):
    title.text = pain_type
    rect.pos = [-.5,0]
    rect.width = .01
    rect.height = .2

def update_win(win, title, outline, rect, pain0, pain10):
    rect.draw()
    outline.draw()
    title.draw()
    pain0.draw()
    pain10.draw()
    win.flip()

def main():

    x = 600
    y = 400
    kb, win, title, outline, rect, pain0, pain10 = init_objects(x, y)

    for pain_type in ['PAIN INTENSITY', 'PAIN UNPLEASANTNESS']:
        
        reset(title, rect, pain_type)

        pain_rating = 0
        while True:
            update_win(win, title, outline, rect, pain0, pain10)
            keys = kb.getKeys(['1', '2', 'return'], waitRelease=False, clear=True)
            if '1' in keys and pain_rating > 0:
                pain_rating -= .5
                rect.width -= .05
                rect.pos[0] -= .025
            elif '2' in keys and pain_rating < 10:
                pain_rating += .5
                rect.width += .05
                rect.pos[0] += .025
            elif 'return' in keys:
                print(f'{pain_type}: {pain_rating}')
                break

if __name__ == '__main__': main()

Hi There,

Here is a demo I used for something similar that could be helpful here to suit your purposes. A bar will continue to fill whilst a ky (the space key) is held.

fillingBar.psyexp (24.3 KB)

By the sounds of it this could be used for what you need?
Hope this helps,
Becca

2 Likes

There’s also a built-in demo in 2021.1.x called progressBar which is very similar :slight_smile:

Thank you both for these very helpful resources! For those who have a similar issue, it was fixed by changing the while loop to:

while True:
            update_win(win, title, outline, rect, pain0, pain10)
            keys = kb.getKeys(['1', '2', 'return'], waitRelease=False, clear=False)
            if keys and not keys[-1].duration: # key is being held down
                key = keys[-1].name
                if key == '1' and pain_rating > 0:
                    pain_rating -= .05
                    rect.width -= .005
                    rect.pos[0] -= .0025
                elif key == '2' and pain_rating < 10:
                    pain_rating += .05
                    rect.width += .005
                    rect.pos[0] += .0025
                elif key == 'return':
                    print(f'{pain_type}: {pain_rating:.2f}' )
                    win.flip()
                    core.wait(1)
                    break
                del keys
1 Like

Hi Becca,
I’ve tried to implement your example to my code where I’m trying to move a marker round a circle scale rather than a linear one. Participants can move a dot around a circle by either pressing ‘1’ (to go clockwise) or ‘4’ (anticlockwise). The code below I have in the ‘Each Frame’ tab, but after a short period of time, the dot gets a mind of it’s own and moves of it’s own accord, ignoring whether a key is being pressed or not. I’m not sure what I’m doing wrong because in my mind, whether a key is being pressed should be assessed in each frame, so when it’s released, the dot should stop. It’s probably something simple… but I can’t see it!
Thanks in advance for any help :slight_smile: Helen

keys = mykb.getKeys(keysWatched, waitRelease = False, clear = False)
if len(keys):# if a key has been pressed
    for i, key in enumerate(keysWatched):
        if keys[-1].name == key: 
          if keys[-1].duration: 
                status[i] = 'up' 
          else:
                status[i] = 'down' 

if status[0] == 'down':
    markerAngle = (markerAngle*cirDivs+1)/cirDivs; #move marker clockwise
elif status[1] == 'down':
    markerAngle = (markerAngle*cirDivs-1)/cirDivs; #move marker anti-clockwise

if status == ['up','up']:
    holder = 1; #do nothing, no button was pressed
else: #either button '1' or '4' is being held down 
    # Calculate the x and y coordinates of the point on the circumference
    marker_x = circleScaleR*numpy.sin(2*numpy.pi*markerAngle)
    marker_y = circleScaleR*numpy.cos(2*numpy.pi*markerAngle)+(-0.15) #displace lower on screen to match placement of circleScale
    marker.setPos([marker_x,marker_y]) #original position of the marker
    
del keys

Thanks all.

The key insight here is that Keyboard tells us about key events which are in the buffer, not the state of individual keys - we are responsible for tracking and managing that ourselves:

if len(keys):# if a key has been pressed
    for i, key in enumerate(keysWatched):
        if keys[-1].name == key:
            if keys[-1].duration:
                status[i] = 'up'
            else:
                status[i] = 'down'