Beispiel #1
0
    def __init__(self, frame, id=-1):
        """A panel that shows how the routines will fit together
        """
        self.frame = frame
        self.app = frame.app
        self.dpi = self.app.dpi
        wx.ScrolledWindow.__init__(self,
                                   frame,
                                   id, (0, 0),
                                   size=wx.Size(8 * self.dpi, 3 * self.dpi),
                                   style=wx.HSCROLL | wx.VSCROLL)
        self.SetBackgroundColour(canvasColor)
        self.needUpdate = True
        self.maxWidth = 50 * self.dpi
        self.maxHeight = 2 * self.dpi
        self.mousePos = None
        # if we're adding a loop or routine then add spots to timeline
        # self.drawNearestRoutinePoint = True
        # self.drawNearestLoopPoint = False
        # lists the x-vals of points to draw, eg loop locations:
        self.pointsToDraw = []
        # for flowSize, showLoopInfoInFlow:
        self.appData = self.app.prefs.appData

        # self.SetAutoLayout(True)
        self.SetScrollRate(self.dpi / 4, self.dpi / 4)

        # create a PseudoDC to record our drawing
        self.pdc = PseudoDC()
        if parse_version(wx.__version__) < parse_version('4.0.0a1'):
            self.pdc.DrawRoundedRectangle = self.pdc.DrawRoundedRectangleRect
        self.pen_cache = {}
        self.brush_cache = {}
        # vars for handling mouse clicks
        self.hitradius = 5
        self.dragid = -1
        self.entryPointPosList = []
        self.entryPointIDlist = []
        self.gapsExcluded = []
        # mode can also be 'loopPoint1','loopPoint2','routinePoint'
        self.mode = 'normal'
        self.insertingRoutine = ""

        # for the context menu use the ID of the drawn icon to retrieve
        # the component (loop or routine)
        self.componentFromID = {}
        self.contextMenuLabels = {
            'remove': _translate('remove'),
            'rename': _translate('rename')
        }
        self.contextMenuItems = ['remove', 'rename']
        self.contextItemFromID = {}
        self.contextIDFromItem = {}
        for item in self.contextMenuItems:
            id = wx.NewIdRef()
            self.contextItemFromID[id] = item
            self.contextIDFromItem[item] = id

        # self.btnInsertRoutine = wx.Button(self,-1,
        #                                  'Insert Routine', pos=(10,10))
        # self.btnInsertLoop = wx.Button(self,-1,'Insert Loop', pos=(10,30))
        labelRoutine = _translate('Insert Routine ')
        labelLoop = _translate('Insert Loop     ')
        self.btnInsertRoutine = platebtn.PlateButton(self,
                                                     -1,
                                                     labelRoutine,
                                                     pos=(10, 10))
        self.btnInsertLoop = platebtn.PlateButton(
            self, -1, labelLoop, pos=(10, 30))  # spaces give size for CANCEL

        self.btnInsertRoutine.SetBackgroundColour(canvasColor)
        self.btnInsertLoop.SetBackgroundColour(canvasColor)

        self.labelTextRed = {
            'normal': wx.Colour(250, 10, 10, 250),
            'hlight': wx.Colour(250, 10, 10, 250)
        }
        self.labelTextBlack = {
            'normal': wx.Colour(0, 0, 0, 250),
            'hlight': wx.Colour(250, 250, 250, 250)
        }

        # use self.appData['flowSize'] to index a tuple to get a specific
        # value, eg: (4,6,8)[self.appData['flowSize']]
        self.flowMaxSize = 2  # upper limit on increaseSize

        self.draw()

        # bind events
        self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
        self.Bind(wx.EVT_BUTTON, self.onInsertRoutine, self.btnInsertRoutine)
        self.Bind(wx.EVT_BUTTON, self.setLoopPoint1, self.btnInsertLoop)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

        idClear = wx.NewIdRef()
        self.Bind(wx.EVT_MENU, self.clearMode, id=idClear)
        aTable = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, idClear)
                                      ])
        self.SetAcceleratorTable(aTable)
Beispiel #2
0
class FlowPanel(wx.ScrolledWindow):
    def __init__(self, frame, id=-1):
        """A panel that shows how the routines will fit together
        """
        self.frame = frame
        self.app = frame.app
        self.dpi = self.app.dpi
        wx.ScrolledWindow.__init__(self,
                                   frame,
                                   id, (0, 0),
                                   size=wx.Size(8 * self.dpi, 3 * self.dpi),
                                   style=wx.HSCROLL | wx.VSCROLL)
        self.SetBackgroundColour(canvasColor)
        self.needUpdate = True
        self.maxWidth = 50 * self.dpi
        self.maxHeight = 2 * self.dpi
        self.mousePos = None
        # if we're adding a loop or routine then add spots to timeline
        # self.drawNearestRoutinePoint = True
        # self.drawNearestLoopPoint = False
        # lists the x-vals of points to draw, eg loop locations:
        self.pointsToDraw = []
        # for flowSize, showLoopInfoInFlow:
        self.appData = self.app.prefs.appData

        # self.SetAutoLayout(True)
        self.SetScrollRate(self.dpi / 4, self.dpi / 4)

        # create a PseudoDC to record our drawing
        self.pdc = PseudoDC()
        if parse_version(wx.__version__) < parse_version('4.0.0a1'):
            self.pdc.DrawRoundedRectangle = self.pdc.DrawRoundedRectangleRect
        self.pen_cache = {}
        self.brush_cache = {}
        # vars for handling mouse clicks
        self.hitradius = 5
        self.dragid = -1
        self.entryPointPosList = []
        self.entryPointIDlist = []
        self.gapsExcluded = []
        # mode can also be 'loopPoint1','loopPoint2','routinePoint'
        self.mode = 'normal'
        self.insertingRoutine = ""

        # for the context menu use the ID of the drawn icon to retrieve
        # the component (loop or routine)
        self.componentFromID = {}
        self.contextMenuLabels = {
            'remove': _translate('remove'),
            'rename': _translate('rename')
        }
        self.contextMenuItems = ['remove', 'rename']
        self.contextItemFromID = {}
        self.contextIDFromItem = {}
        for item in self.contextMenuItems:
            id = wx.NewIdRef()
            self.contextItemFromID[id] = item
            self.contextIDFromItem[item] = id

        # self.btnInsertRoutine = wx.Button(self,-1,
        #                                  'Insert Routine', pos=(10,10))
        # self.btnInsertLoop = wx.Button(self,-1,'Insert Loop', pos=(10,30))
        labelRoutine = _translate('Insert Routine ')
        labelLoop = _translate('Insert Loop     ')
        self.btnInsertRoutine = platebtn.PlateButton(self,
                                                     -1,
                                                     labelRoutine,
                                                     pos=(10, 10))
        self.btnInsertLoop = platebtn.PlateButton(
            self, -1, labelLoop, pos=(10, 30))  # spaces give size for CANCEL

        self.btnInsertRoutine.SetBackgroundColour(canvasColor)
        self.btnInsertLoop.SetBackgroundColour(canvasColor)

        self.labelTextRed = {
            'normal': wx.Colour(250, 10, 10, 250),
            'hlight': wx.Colour(250, 10, 10, 250)
        }
        self.labelTextBlack = {
            'normal': wx.Colour(0, 0, 0, 250),
            'hlight': wx.Colour(250, 250, 250, 250)
        }

        # use self.appData['flowSize'] to index a tuple to get a specific
        # value, eg: (4,6,8)[self.appData['flowSize']]
        self.flowMaxSize = 2  # upper limit on increaseSize

        self.draw()

        # bind events
        self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse)
        self.Bind(wx.EVT_BUTTON, self.onInsertRoutine, self.btnInsertRoutine)
        self.Bind(wx.EVT_BUTTON, self.setLoopPoint1, self.btnInsertLoop)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

        idClear = wx.NewIdRef()
        self.Bind(wx.EVT_MENU, self.clearMode, id=idClear)
        aTable = wx.AcceleratorTable([(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, idClear)
                                      ])
        self.SetAcceleratorTable(aTable)

    def clearMode(self, event=None):
        """If we were in middle of doing something (like inserting routine)
        then end it, allowing user to cancel
        """
        self.mode = 'normal'
        self.insertingRoutine = None
        for id in self.entryPointIDlist:
            self.pdc.RemoveId(id)
        self.entryPointPosList = []
        self.entryPointIDlist = []
        self.gapsExcluded = []
        self.draw()
        self.frame.SetStatusText("")
        self.btnInsertRoutine.SetLabel(_translate('Insert Routine'))
        self.btnInsertLoop.SetLabel(_translate('Insert Loop'))
        self.btnInsertRoutine.SetLabelColor(**self.labelTextBlack)
        self.btnInsertLoop.SetLabelColor(**self.labelTextBlack)

    def ConvertEventCoords(self, event):
        xView, yView = self.GetViewStart()
        xDelta, yDelta = self.GetScrollPixelsPerUnit()
        return (event.GetX() + (xView * xDelta),
                event.GetY() + (yView * yDelta))

    def OffsetRect(self, r):
        """Offset the rectangle, r, to appear in the given position
        in the window
        """
        xView, yView = self.GetViewStart()
        xDelta, yDelta = self.GetScrollPixelsPerUnit()
        r.Offset((-(xView * xDelta), -(yView * yDelta)))

    def onInsertRoutine(self, evt):
        """For when the insert Routine button is pressed - bring up
        dialog and present insertion point on flow line.
        see self.insertRoutine() for further info
        """
        if self.mode.startswith('loopPoint'):
            self.clearMode()
        elif self.mode == 'routine':
            # clicked again with label now being "Cancel..."
            self.clearMode()
            return
        self.frame.SetStatusText(
            _translate("Select a Routine to insert (Esc to exit)"))
        menu = wx.Menu()
        self.routinesFromID = {}
        id = wx.NewIdRef()
        menu.Append(id, '(new)')
        self.routinesFromID[id] = '(new)'
        menu.Bind(wx.EVT_MENU, self.insertNewRoutine, id=id)
        for routine in self.frame.exp.routines:
            id = wx.NewIdRef()
            menu.Append(id, routine)
            self.routinesFromID[id] = routine
            menu.Bind(wx.EVT_MENU, self.onInsertRoutineSelect, id=id)
        btnPos = self.btnInsertRoutine.GetRect()
        menuPos = (btnPos[0], btnPos[1] + btnPos[3])
        self.PopupMenu(menu, menuPos)
        menu.Bind(wx.EVT_MENU_CLOSE, self.clearMode)
        menu.Destroy()  # destroy to avoid mem leak

    def insertNewRoutine(self, event):
        """selecting (new) is a short-cut for:
        make new routine, insert it into the flow
        """
        newRoutine = self.frame.routinePanel.createNewRoutine(returnName=True)
        if newRoutine:
            self.routinesFromID[event.GetId()] = newRoutine
            self.onInsertRoutineSelect(event)
        else:
            self.clearMode()

    def onInsertRoutineSelect(self, event):
        """User has selected a routine to be entered so bring up the
        entrypoint marker and await mouse button press.
        see self.insertRoutine() for further info
        """
        self.mode = 'routine'
        self.btnInsertRoutine.SetLabel(_translate('CANCEL Insert'))
        self.btnInsertRoutine.SetLabelColor(**self.labelTextRed)
        self.frame.SetStatusText(
            _translate(
                'Click where you want to insert the Routine, or CANCEL insert.'
            ))
        self.insertingRoutine = self.routinesFromID[event.GetId()]
        x = self.getNearestGapPoint(0)
        self.drawEntryPoints([x])

    def insertRoutine(self, ii):
        """Insert a routine into the Flow knowing its name and location

        onInsertRoutine() the button has been pressed so present menu
        onInsertRoutineSelect() user selected the name so present entry points
        OnMouse() user has selected a point on the timeline to insert entry

        """
        rtn = self.frame.exp.routines[self.insertingRoutine]
        self.frame.exp.flow.addRoutine(rtn, ii)
        self.frame.addToUndoStack("ADD Routine `%s`" % rtn.name)
        # reset flow drawing (remove entry point)
        self.clearMode()

    def setLoopPoint1(self, evt=None):
        """Someone pushed the insert loop button.
        Fetch the dialog
        """
        if self.mode == 'routine':
            self.clearMode()
        # clicked again, label is "Cancel..."
        elif self.mode.startswith('loopPoint'):
            self.clearMode()
            return
        self.btnInsertLoop.SetLabel(_translate('CANCEL insert'))
        self.btnInsertLoop.SetLabelColor(**self.labelTextRed)
        self.mode = 'loopPoint1'
        self.frame.SetStatusText(
            _translate(
                'Click where you want the loop to start/end, or CANCEL insert.'
            ))
        x = self.getNearestGapPoint(0)
        self.drawEntryPoints([x])

    def setLoopPoint2(self, evt=None):
        """We have the location of the first point, waiting to get the second
        """
        self.mode = 'loopPoint2'
        self.frame.SetStatusText(
            _translate('Click the other end for the loop'))
        thisPos = self.entryPointPosList[0]
        self.gapsExcluded = [thisPos]
        self.gapsExcluded.extend(self.getGapPointsCrossingStreams(thisPos))
        # is there more than one available point
        diff = wx.GetMousePosition()[0] - self.GetScreenPosition()[0]
        x = self.getNearestGapPoint(diff, exclude=self.gapsExcluded)
        self.drawEntryPoints([self.entryPointPosList[0], x])
        nAvailableGaps = len(self.gapMidPoints) - len(self.gapsExcluded)
        if nAvailableGaps == 1:
            self.insertLoop()  # there's only one place - use it

    def insertLoop(self, evt=None):
        # bring up listbox to choose the routine to add, and / or a new one
        loopDlg = DlgLoopProperties(frame=self.frame,
                                    helpUrl=self.app.urls['builder.loops'])
        startII = self.gapMidPoints.index(min(self.entryPointPosList))
        endII = self.gapMidPoints.index(max(self.entryPointPosList))
        if loopDlg.OK:
            handler = loopDlg.currentHandler
            self.frame.exp.flow.addLoop(handler,
                                        startPos=startII,
                                        endPos=endII)
            action = "ADD Loop `%s` to Flow" % handler.params['name'].val
            self.frame.addToUndoStack(action)
        self.clearMode()
        self.draw()

    def increaseSize(self, event=None):
        if self.appData['flowSize'] == self.flowMaxSize:
            self.appData['showLoopInfoInFlow'] = True
        self.appData['flowSize'] = min(self.flowMaxSize,
                                       self.appData['flowSize'] + 1)
        self.clearMode()  # redraws

    def decreaseSize(self, event=None):
        if self.appData['flowSize'] == 0:
            self.appData['showLoopInfoInFlow'] = False
        self.appData['flowSize'] = max(0, self.appData['flowSize'] - 1)
        self.clearMode()  # redraws

    def editLoopProperties(self, event=None, loop=None):
        # add routine points to the timeline
        self.setDrawPoints('loops')
        self.draw()
        if 'conditions' in loop.params:
            condOrig = loop.params['conditions'].val
            condFileOrig = loop.params['conditionsFile'].val
        title = loop.params['name'].val + ' Properties'
        loopDlg = DlgLoopProperties(frame=self.frame,
                                    helpUrl=self.app.urls['builder.loops'],
                                    title=title,
                                    loop=loop)
        if loopDlg.OK:
            prevLoop = loop
            if loopDlg.params['loopType'].val == 'staircase':
                loop = loopDlg.stairHandler
            elif loopDlg.params['loopType'].val == 'interleaved staircases':
                loop = loopDlg.multiStairHandler
            else:
                # ['random','sequential', 'fullRandom', ]
                loop = loopDlg.trialHandler
            # if the loop is a whole new class then we can't just update the
            # params
            if loop.getType() != prevLoop.getType():
                # get indices for start and stop points of prev loop
                flow = self.frame.exp.flow
                # find the index of the initiator
                startII = flow.index(prevLoop.initiator)
                # minus one because initiator will have been deleted
                endII = flow.index(prevLoop.terminator) - 1
                # remove old loop completely
                flow.removeComponent(prevLoop)
                # finally insert the new loop
                flow.addLoop(loop, startII, endII)
            self.frame.addToUndoStack("EDIT Loop `%s`" %
                                      (loop.params['name'].val))
        elif 'conditions' in loop.params:
            loop.params['conditions'].val = condOrig
            loop.params['conditionsFile'].val = condFileOrig
        # remove the points from the timeline
        self.setDrawPoints(None)
        self.draw()

    def OnMouse(self, event):
        x, y = self.ConvertEventCoords(event)
        handlerTypes = ('StairHandler', 'TrialHandler', 'MultiStairHandler')
        if self.mode == 'normal':
            if event.LeftDown():
                icons = self.pdc.FindObjectsByBBox(x, y)
                for thisIcon in icons:
                    # might intersect several and only one has a callback
                    if thisIcon in self.componentFromID:
                        comp = self.componentFromID[thisIcon]
                        if comp.getType() in handlerTypes:
                            self.editLoopProperties(loop=comp)
                        if comp.getType() == 'Routine':
                            self.frame.routinePanel.setCurrentRoutine(
                                routine=comp)
            elif event.RightDown():
                icons = self.pdc.FindObjectsByBBox(x, y)
                # todo: clean-up remove `comp`, its unused
                comp = None
                for thisIcon in icons:
                    # might intersect several and only one has a callback
                    if thisIcon in self.componentFromID:
                        # loop through comps looking for Routine, or a Loop if
                        # no routine
                        thisComp = self.componentFromID[thisIcon]
                        if thisComp.getType() in handlerTypes:
                            comp = thisComp  # unused
                            icon = thisIcon
                        if thisComp.getType() == 'Routine':
                            comp = thisComp
                            icon = thisIcon
                            break  # we've found a Routine so stop looking
                try:
                    self._menuComponentID = icon
                    xy = wx.Point(x + self.GetPosition()[0],
                                  y + self.GetPosition()[1])
                    self.showContextMenu(self._menuComponentID, xy=xy)
                except UnboundLocalError:
                    # right click but not on an icon
                    # might as well do something
                    self.Refresh()
        elif self.mode == 'routine':
            if event.LeftDown():
                pt = self.entryPointPosList[0]
                self.insertRoutine(ii=self.gapMidPoints.index(pt))
            else:  # move spot if needed
                point = self.getNearestGapPoint(mouseX=x)
                self.drawEntryPoints([point])
        elif self.mode == 'loopPoint1':
            if event.LeftDown():
                self.setLoopPoint2()
            else:  # move spot if needed
                point = self.getNearestGapPoint(mouseX=x)
                self.drawEntryPoints([point])
        elif self.mode == 'loopPoint2':
            if event.LeftDown():
                self.insertLoop()
            else:  # move spot if needed
                point = self.getNearestGapPoint(mouseX=x,
                                                exclude=self.gapsExcluded)
                self.drawEntryPoints([self.entryPointPosList[0], point])

    def getNearestGapPoint(self, mouseX, exclude=()):
        """Get gap that is nearest to a particular mouse location
        """
        d = 1000000000
        nearest = None
        for point in self.gapMidPoints:
            if point in exclude:
                continue
            if (point - mouseX)**2 < d:
                d = (point - mouseX)**2
                nearest = point
        return nearest

    def getGapPointsCrossingStreams(self, gapPoint):
        """For a given gap point, identify the gap points that are
        excluded by crossing a loop line
        """
        gapArray = numpy.array(self.gapMidPoints)
        nestLevels = numpy.array(self.gapNestLevels)
        thisLevel = nestLevels[gapArray == gapPoint]
        invalidGaps = (gapArray[nestLevels != thisLevel]).tolist()
        return invalidGaps

    def showContextMenu(self, component, xy):
        menu = wx.Menu()
        # get ID
        # the ID is also the index to the element in the flow list
        compID = self._menuComponentID
        flow = self.frame.exp.flow
        component = flow[compID]
        compType = component.getType()
        if compType == 'Routine':
            for item in self.contextMenuItems:
                id = self.contextIDFromItem[item]
                menu.Append(id, self.contextMenuLabels[item])
                menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
            self.frame.PopupMenu(menu, xy)
            # destroy to avoid mem leak:
            menu.Destroy()
        else:
            for item in self.contextMenuItems:
                if item == 'rename':
                    continue
                id = self.contextIDFromItem[item]
                menu.Append(id, self.contextMenuLabels[item])
                menu.Bind(wx.EVT_MENU, self.onContextSelect, id=id)
            self.frame.PopupMenu(menu, xy)
            # destroy to avoid mem leak:
            menu.Destroy()

    def onContextSelect(self, event):
        """Perform a given action on the component chosen
        """
        # get ID
        op = self.contextItemFromID[event.GetId()]
        # the ID is also the index to the element in the flow list
        compID = self._menuComponentID
        flow = self.frame.exp.flow
        component = flow[compID]
        # if we have a Loop Initiator, remove the whole loop
        if component.getType() == 'LoopInitiator':
            component = component.loop
        if op == 'remove':
            self.removeComponent(component, compID)
            self.frame.addToUndoStack("REMOVE `%s` from Flow" %
                                      component.params['name'])
        if op == 'rename':
            self.frame.renameRoutine(component)

    def removeComponent(self, component, compID):
        """Remove either a Routine or a Loop from the Flow
        """
        flow = self.frame.exp.flow
        if component.getType() == 'Routine':
            # check whether this will cause a collapsed loop
            # prev and next elements on flow are a loop init/end
            prevIsLoop = nextIsLoop = False
            if compID > 0:  # there is at least one preceding
                prevIsLoop = (flow[compID - 1]).getType() == 'LoopInitiator'
            if len(flow) > (compID + 1):  # there is at least one more compon
                nextIsLoop = (flow[compID + 1]).getType() == 'LoopTerminator'
            if prevIsLoop and nextIsLoop:
                # because flow[compID+1] is a terminator
                loop = flow[compID + 1].loop
                msg = _translate('The "%s" Loop is about to be deleted as '
                                 'well (by collapsing). OK to proceed?')
                title = _translate('Impending Loop collapse')
                warnDlg = dialogs.MessageDialog(parent=self.frame,
                                                message=msg %
                                                loop.params['name'],
                                                type='Warning',
                                                title=title)
                resp = warnDlg.ShowModal()
                if resp in [wx.ID_CANCEL, wx.ID_NO]:
                    return  # abort
                elif resp == wx.ID_YES:
                    # make recursive calls to this same method until success
                    # remove the loop first
                    self.removeComponent(loop, compID)
                    # because the loop has been removed ID is now one less
                    self.removeComponent(component, compID - 1)
                    return  # have done the removal in final successful call
        # remove name from namespace only if it's a loop;
        # loops exist only in the flow
        elif 'conditionsFile' in component.params:
            conditionsFile = component.params['conditionsFile'].val
            if conditionsFile and conditionsFile not in ['None', '']:
                try:
                    trialList, fieldNames = data.importConditions(
                        conditionsFile, returnFieldNames=True)
                    for fname in fieldNames:
                        self.frame.exp.namespace.remove(fname)
                except Exception:
                    msg = ("Conditions file %s couldn't be found so names not"
                           " removed from namespace")
                    logging.debug(msg % conditionsFile)
            self.frame.exp.namespace.remove(component.params['name'].val)
        # perform the actual removal
        flow.removeComponent(component, id=compID)
        self.draw()

    def OnPaint(self, event):
        # Create a buffered paint DC.  It will create the real
        # wx.PaintDC and then blit the bitmap to it when dc is
        # deleted.
        dc = wx.GCDC(wx.BufferedPaintDC(self))
        # use PrepareDC to set position correctly
        self.PrepareDC(dc)
        # we need to clear the dc BEFORE calling PrepareDC
        bg = wx.Brush(self.GetBackgroundColour())
        dc.SetBackground(bg)
        dc.Clear()
        # create a clipping rect from our position and size
        # and the Update Region
        xv, yv = self.GetViewStart()
        dx, dy = self.GetScrollPixelsPerUnit()
        x, y = (xv * dx, yv * dy)
        rgn = self.GetUpdateRegion()
        rgn.Offset(x, y)
        r = rgn.GetBox()
        # draw to the dc using the calculated clipping rect
        self.pdc.DrawToDCClipped(dc, r)

    def draw(self, evt=None):
        """This is the main function for drawing the Flow panel.
        It should be called whenever something changes in the exp.

        This then makes calls to other drawing functions,
        like drawEntryPoints...
        """
        if not hasattr(self.frame, 'exp'):
            # we haven't yet added an exp
            return
        # retrieve the current flow from the experiment
        expFlow = self.frame.exp.flow
        pdc = self.pdc

        # use the ID of the drawn icon to retrieve component (loop or routine)
        self.componentFromID = {}

        pdc.Clear()  # clear the screen
        pdc.RemoveAll()  # clear all objects (icon buttons)

        font = self.GetFont()

        # draw the main time line
        self.linePos = (2.5 * self.dpi, 0.5 * self.dpi)  # x,y of start
        gap = self.dpi / (6, 4, 2)[self.appData['flowSize']]
        dLoopToBaseLine = (15, 25, 43)[self.appData['flowSize']]
        dBetweenLoops = (20, 24, 30)[self.appData['flowSize']]

        # guess virtual size; nRoutines wide by nLoops high
        # make bigger than needed and shrink later
        nRoutines = len(expFlow)
        nLoops = 0
        for entry in expFlow:
            if entry.getType() == 'LoopInitiator':
                nLoops += 1
        sizeX = nRoutines * self.dpi * 2
        sizeY = nLoops * dBetweenLoops + dLoopToBaseLine * 3
        self.SetVirtualSize(size=(sizeX, sizeY))

        # step through components in flow, get spacing from text size, etc
        currX = self.linePos[0]
        lineId = wx.NewIdRef()
        pdc.DrawLine(x1=self.linePos[0] - gap,
                     y1=self.linePos[1],
                     x2=self.linePos[0],
                     y2=self.linePos[1])
        # NB the loop is itself the key, value is further info about it
        self.loops = {}
        nestLevel = 0
        maxNestLevel = 0
        self.gapMidPoints = [currX - gap / 2]
        self.gapNestLevels = [0]
        for ii, entry in enumerate(expFlow):
            if entry.getType() == 'LoopInitiator':
                # NB the loop is itself the dict key!?
                self.loops[entry.loop] = {
                    'init': currX,
                    'nest': nestLevel,
                    'id': ii
                }
                nestLevel += 1  # start of loop so increment level of nesting
                maxNestLevel = max(nestLevel, maxNestLevel)
            elif entry.getType() == 'LoopTerminator':
                # NB the loop is itself the dict key!
                self.loops[entry.loop]['term'] = currX
                nestLevel -= 1  # end of loop so decrement level of nesting
            elif entry.getType() == 'Routine':
                # just get currX based on text size, don't draw anything yet:
                currX = self.drawFlowRoutine(pdc,
                                             entry,
                                             id=ii,
                                             pos=[currX, self.linePos[1] - 10],
                                             draw=False)
            self.gapMidPoints.append(currX + gap / 2)
            self.gapNestLevels.append(nestLevel)
            pdc.SetId(lineId)
            pdc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 255)))
            pdc.DrawLine(x1=currX,
                         y1=self.linePos[1],
                         x2=currX + gap,
                         y2=self.linePos[1])
            currX += gap
        lineRect = wx.Rect(self.linePos[0] - 2, self.linePos[1] - 2,
                           currX - self.linePos[0] + 2, 4)
        pdc.SetIdBounds(lineId, lineRect)

        # draw the loops first:
        maxHeight = 0
        for thisLoop in self.loops:
            thisInit = self.loops[thisLoop]['init']
            thisTerm = self.loops[thisLoop]['term']
            thisNest = maxNestLevel - self.loops[thisLoop]['nest'] - 1
            thisId = self.loops[thisLoop]['id']
            height = (self.linePos[1] + dLoopToBaseLine +
                      thisNest * dBetweenLoops)
            self.drawLoop(pdc,
                          thisLoop,
                          id=thisId,
                          startX=thisInit,
                          endX=thisTerm,
                          base=self.linePos[1],
                          height=height)
            self.drawLoopStart(pdc, pos=[thisInit, self.linePos[1]])
            self.drawLoopEnd(pdc, pos=[thisTerm, self.linePos[1]])
            if height > maxHeight:
                maxHeight = height

        # draw routines second (over loop lines):
        currX = self.linePos[0]
        for ii, entry in enumerate(expFlow):
            if entry.getType() == 'Routine':
                currX = self.drawFlowRoutine(pdc,
                                             entry,
                                             id=ii,
                                             pos=[currX, self.linePos[1] - 10])
            pdc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 255)))
            pdc.DrawLine(x1=currX,
                         y1=self.linePos[1],
                         x2=currX + gap,
                         y2=self.linePos[1])
            currX += gap

        self.SetVirtualSize(size=(currX + 100, maxHeight + 50))

        self.drawLineStart(pdc, (self.linePos[0] - gap, self.linePos[1]))
        self.drawLineEnd(pdc, (currX, self.linePos[1]))

        # refresh the visible window after drawing (using OnPaint)
        self.Refresh()

    def drawEntryPoints(self, posList):
        ptSize = (3, 4, 5)[self.appData['flowSize']]
        for n, pos in enumerate(posList):
            if n >= len(self.entryPointPosList):
                # draw for first time
                id = wx.NewIdRef()
                self.entryPointIDlist.append(id)
                self.pdc.SetId(id)
                self.pdc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 255)))
                self.pdc.DrawCircle(pos, self.linePos[1], ptSize)
                r = self.pdc.GetIdBounds(id)
                self.OffsetRect(r)
                self.RefreshRect(r, False)
            elif pos == self.entryPointPosList[n]:
                pass  # nothing to see here, move along please :-)
            else:
                # move to new position
                dx = pos - self.entryPointPosList[n]
                dy = 0
                r = self.pdc.GetIdBounds(self.entryPointIDlist[n])
                self.pdc.TranslateId(self.entryPointIDlist[n], dx, dy)
                r2 = self.pdc.GetIdBounds(self.entryPointIDlist[n])
                # combine old and new locations to get redraw area
                rectToRedraw = r.Union(r2)
                rectToRedraw.Inflate(4, 4)
                self.OffsetRect(rectToRedraw)
                self.RefreshRect(rectToRedraw, False)

        self.entryPointPosList = posList
        # refresh the visible window after drawing (using OnPaint)
        self.Refresh()

    def setDrawPoints(self, ptType, startPoint=None):
        """Set the points of 'routines', 'loops', or None
        """
        if ptType == 'routines':
            self.pointsToDraw = self.gapMidPoints
        elif ptType == 'loops':
            self.pointsToDraw = self.gapMidPoints
        else:
            self.pointsToDraw = []

    def drawLineStart(self, dc, pos):
        # draw bar at start of timeline; circle looked bad, offset vertically
        ptSize = (3, 3, 4)[self.appData['flowSize']]
        dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 255)))
        dc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 255)))
        dc.DrawPolygon([[0, -ptSize], [1, -ptSize], [1, ptSize], [0, ptSize]],
                       pos[0], pos[1])

    def drawLineEnd(self, dc, pos):
        # draws arrow at end of timeline
        # tmpId = wx.NewIdRef()
        # dc.SetId(tmpId)
        dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 255)))
        dc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 255)))
        dc.DrawPolygon([[0, -3], [5, 0], [0, 3]], pos[0], pos[1])
        # dc.SetIdBounds(tmpId,wx.Rect(pos[0],pos[1]+3,5,6))

    def drawLoopEnd(self, dc, pos, downwards=True):
        # define the right side of a loop but draw nothing
        # idea: might want an ID for grabbing and relocating the loop endpoint
        tmpId = wx.NewIdRef()
        dc.SetId(tmpId)
        # dc.SetBrush(wx.Brush(wx.Colour(0,0,0, 250)))
        # dc.SetPen(wx.Pen(wx.Colour(0,0,0, 255)))
        size = (3, 4, 5)[self.appData['flowSize']]
        # if downwards:
        #   dc.DrawPolygon([[size, 0], [0, size], [-size, 0]],
        #                  pos[0], pos[1] + 2 * size)  # points down
        # else:
        #   dc.DrawPolygon([[size, size], [0, 0], [-size, size]],
        #   pos[0], pos[1]-3*size)  # points up
        dc.SetIdBounds(
            tmpId, wx.Rect(pos[0] - size, pos[1] - size, 2 * size, 2 * size))
        return

    def drawLoopStart(self, dc, pos, downwards=True):
        # draws direction arrow on left side of a loop
        tmpId = wx.NewIdRef()
        dc.SetId(tmpId)
        dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 250)))
        dc.SetPen(wx.Pen(wx.Colour(0, 0, 0, 255)))
        size = (3, 4, 5)[self.appData['flowSize']]
        offset = (3, 2, 0)[self.appData['flowSize']]
        if downwards:
            dc.DrawPolygon([[size, size], [0, 0], [-size, size]], pos[0],
                           pos[1] + 3 * size - offset)  # points up
        else:
            dc.DrawPolygon([[size, 0], [0, size], [-size, 0]], pos[0],
                           pos[1] - 4 * size)  # points down
        dc.SetIdBounds(
            tmpId, wx.Rect(pos[0] - size, pos[1] - size, 2 * size, 2 * size))

    def drawFlowRoutine(self, dc, routine, id, pos=[0, 0], draw=True):
        """Draw a box to show a routine on the timeline
        draw=False is for a dry-run, esp to compute and return size
        without drawing or setting a pdc ID
        """
        name = routine.name
        if self.appData['flowSize'] == 0 and len(name) > 5:
            name = ' ' + name[:4] + '..'
        else:
            name = ' ' + name + ' '
        if draw:
            dc.SetId(id)
        font = self.GetFont()
        if sys.platform == 'darwin':
            fontSizeDelta = (9, 6, 0)[self.appData['flowSize']]
            font.SetPointSize(1400 / self.dpi - fontSizeDelta)
        elif sys.platform.startswith('linux'):
            fontSizeDelta = (6, 4, 0)[self.appData['flowSize']]
            font.SetPointSize(1400 / self.dpi - fontSizeDelta)
        else:
            fontSizeDelta = (8, 4, 0)[self.appData['flowSize']]
            font.SetPointSize(1000 / self.dpi - fontSizeDelta)

        maxTime, nonSlip = routine.getMaxTime()
        if nonSlip:
            rgbFill = nonSlipFill
            rgbEdge = nonSlipEdge
        else:
            rgbFill = relTimeFill
            rgbEdge = relTimeEdge

        # get size based on text
        self.SetFont(font)
        if draw:
            dc.SetFont(font)
        w, h = self.GetFullTextExtent(name)[0:2]
        pad = (5, 10, 20)[self.appData['flowSize']]
        # draw box
        pos[1] += 2 - self.appData['flowSize']
        rect = wx.Rect(pos[0], pos[1], w + pad, h + pad)
        endX = pos[0] + w + pad
        # the edge should match the text
        if draw:
            dc.SetPen(
                wx.Pen(
                    wx.Colour(rgbEdge[0], rgbEdge[1], rgbEdge[2],
                              wx.ALPHA_OPAQUE)))
            dc.SetBrush(wx.Brush(rgbFill))
            dc.DrawRoundedRectangle(rect, (4, 6, 8)[self.appData['flowSize']])
            # draw text
            dc.SetTextForeground(rgbEdge)
            dc.DrawLabel(name, rect, alignment=wx.ALIGN_CENTRE)
            if nonSlip and self.appData['flowSize'] != 0:
                font.SetPointSize(font.GetPointSize() * 0.6)
                dc.SetFont(font)
                _align = wx.ALIGN_CENTRE | wx.ALIGN_BOTTOM
                dc.DrawLabel("(%.2fs)" % maxTime, rect, alignment=_align)

            self.componentFromID[id] = routine
            # set the area for this component
            dc.SetIdBounds(id, rect)

        return endX

    def drawLoop(self,
                 dc,
                 loop,
                 id,
                 startX,
                 endX,
                 base,
                 height,
                 rgb=(0, 0, 0),
                 downwards=True):
        if downwards:
            up = -1
        else:
            up = +1

        # draw loop itself, as transparent rect with curved corners
        tmpId = wx.NewIdRef()
        dc.SetId(tmpId)
        # extra distance, in both h and w for curve
        curve = (6, 11, 15)[self.appData['flowSize']]
        yy = [base, height + curve * up, height + curve * up / 2,
              height]  # for area
        r, g, b = rgb
        dc.SetPen(wx.Pen(wx.Colour(r, g, b, 200)))
        vertOffset = 0  # 1 is interesting too
        area = wx.Rect(startX, base + vertOffset, endX - startX,
                       max(yy) - min(yy))
        dc.SetBrush(wx.Brush(wx.Colour(0, 0, 0, 0), style=wx.TRANSPARENT))
        # draws outline:
        dc.DrawRoundedRectangle(area, curve)
        dc.SetIdBounds(tmpId, area)

        flowsize = self.appData['flowSize']  # 0, 1, or 2

        # add a name label, loop info, except at smallest size
        name = loop.params['name'].val
        _show = self.appData['showLoopInfoInFlow']
        if _show and flowsize:
            _cond = 'conditions' in list(loop.params)
            if _cond and loop.params['conditions'].val:
                xnumTrials = 'x' + str(len(loop.params['conditions'].val))
            else:
                xnumTrials = ''
            name += '  (' + str(loop.params['nReps'].val) + xnumTrials
            abbrev = [
                '',  # for flowsize == 0
                {
                    'random': 'rand.',
                    'sequential': 'sequ.',
                    'fullRandom': 'f-ran.',
                    'staircase': 'stair.',
                    'interleaved staircases': "int-str."
                },
                {
                    'random': 'random',
                    'sequential': 'sequential',
                    'fullRandom': 'fullRandom',
                    'staircase': 'staircase',
                    'interleaved staircases': "interl'vd stairs"
                }
            ]
            name += ' ' + abbrev[flowsize][loop.params['loopType'].val] + ')'
        if flowsize == 0:
            if len(name) > 9:
                name = ' ' + name[:8] + '..'
            else:
                name = ' ' + name[:9]
        else:
            name = ' ' + name + ' '

        dc.SetId(id)
        font = self.GetFont()
        if sys.platform == 'darwin':
            basePtSize = (650, 750, 900)[flowsize]
        elif sys.platform.startswith('linux'):
            basePtSize = (750, 850, 1000)[flowsize]
        else:
            basePtSize = (700, 750, 800)[flowsize]
        font.SetPointSize(basePtSize / self.dpi)
        self.SetFont(font)
        dc.SetFont(font)

        # get size based on text
        pad = (5, 8, 10)[self.appData['flowSize']]
        w, h = self.GetFullTextExtent(name)[0:2]
        x = startX + (endX - startX) / 2 - w / 2 - pad / 2
        y = (height - h / 2)

        # draw box
        rect = wx.Rect(x, y, w + pad, h + pad)
        # the edge should match the text
        dc.SetPen(wx.Pen(wx.Colour(r, g, b, 100)))
        # try to make the loop fill brighter than the background canvas:
        dc.SetBrush(wx.Brush(wx.Colour(235, 235, 235, 250)))

        dc.DrawRoundedRectangle(rect, (4, 6, 8)[flowsize])
        # draw text
        dc.SetTextForeground([r, g, b])
        dc.DrawText(name, x + pad / 2, y + pad / 2)

        self.componentFromID[id] = loop
        # set the area for this component
        dc.SetIdBounds(id, rect)