Extended display and retina macbook -- how I dealt with it using decorators

I’ve seen a few posts with questions about macbooks with retina screens & external displays (see Understanding the retina screen scaling of stimuli, Stimuli on mac retina display are in bottom left corner, Problem when extending monitor (retina display and windows creation). I thought it might be useful to share how I fixed my particular problem.

I needed to get an older experiment running on a new macbook. After fixing some exec calls (see Problem with exec('{} = thisTrial[paramName]'.format(paramName))), the experiment ran, but the window on the external display (screen 1) was in the wrong place, and the stimuli were two big and off-center. This is a known problem with retina displays and pyglet (see some of the above posts), but I needed a quick solution rather than waiting for updates to pyglet or psychopy. Fixing the window was easy enough: I set pos = [0,0], and for some reason I also had to set allowGUI=True. Not exactly what I wanted but I could live with it. Next I had to reposition and rescale all the stimuli, but I didn’t want to edit 70 different function calls.

Decorators to the rescue! Python decorators let you redefine python functions. In this case, I needed to “intercept” TextStim and ImageStim, and change the height, size, pos, and wrapWidth arguments. That looked like this:

# Decorator to fix retina problems (JCS 20191212)

# calculate correct offset in degrees of visual angle (the units used by our window)
from psychopy.tools import monitorunittools
pos_offset_0 = -0.25*monitorunittools.pix2deg(resolution[0], mon)
pos_offset_1 = 0.25*monitorunittools.pix2deg(resolution[1], mon)

def StimDecorator(func):
    def wrapper(*args, **kwargs):
        if kwargs['win'].screen == 1:
            if 'height' in kwargs:
                kwargs['height'] = 0.5*kwargs['height']
            if 'pos' in kwargs:
                kwargs['pos'] = (0.5*kwargs['pos'][0] + pos_offset_0, 
                                          0.5*kwargs['pos'][1] + pos_offset_1)
            if 'size' in kwargs:
                kwargs['size'] = (0.5*kwargs['size'][0], 0.5*kwargs['size'][1])
            if 'wrapWidth' in kwargs and kwargs['wrapWidth']:
                kwargs['wrapWidth'] = 0.5*kwargs['wrapWidth']
        return func(*args, **kwargs)
    return wrapper

visual.TextStim = StimDecorator(visual.TextStim)
visual.ImageStim = StimDecorator(visual.ImageStim)

Voila! Things look okay, I didn’t have to change much code, and I can easily change it again or remove it in the future.

3 Likes

Wow, that looks clever. :+1: