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 wx.version() < "4": 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.NewId() 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.NewId() 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.NewId() 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.NewId() 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.NewId() 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.NewId() 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.NewId() # 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.NewId() 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.NewId() 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.NewId() 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)
class Calendrier(wx.ScrolledWindow): def __init__(self, parent, ID=-1, multiSelections=True, selectionInterdite=False, typeCalendrier="mensuel"): wx.ScrolledWindow.__init__(self, parent, ID, style=wx.NO_BORDER) self.multiSelections = multiSelections self.selectionInterdite = selectionInterdite self.SetMinSize((100, 140)) # Variables à ne surtout pas changer self.caseSurvol = None self.multiSelect = None self.onLeave = True # Variables qui peuvent être changées self.listeSelections = [] self.ecartCases = 2 # Ecart en pixels entre les cases jours d'un mois self.ecartMois = 8 # Ecart en pixels entre chaque mois dan un calendrier annuel self.couleurFond = ( 255, 255, 255 ) #(255, 255, 255) # Couleur du fond du calendrier self.couleurNormal = ( 255, 255, 255 ) #(214, 223, 247) #(175, 225, 251) # Couleur d'un jour normal du lundi au vendredi self.couleurWE = ( 231, 245, 252 ) #(171, 249, 150) # Couleur des samedis et dimanche self.couleurSelect = ( 55, 228, 9 ) # Couleur du fond de la case si celle-ci est sélectionnée self.couleurSurvol = ( 0, 0, 0) # Couleur du bord de la case si celle-ci est survolée self.couleurFontJours = (0, 0, 0) self.couleurVacances = ( 255, 255, 255 ) # Couleur des cases dates d'ouverture de la structure self.couleurFontJoursAvecPresents = (255, 0, 0) self.couleurFerie = (180, 180, 180) # couleur des jours fériés self.headerMois = True self.headerJours = True self.typeCalendrier = typeCalendrier self.moisCalendrier = 2 self.anneeCalendrier = 2008 self.selectExclureWE = True # Inclure les WE quand une période de vacs est sélectionnée dans le menu contextuel # Pour statusBar : try: self.frameParente = self.GetGrandParent().GetGrandParent( ).GetParent() except: pass self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) self.Bind(wx.EVT_MOTION, self.OnMotion) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) self.Bind(wx.EVT_KEY_UP, self.OnKeyUp) self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) self.Bind(wx.EVT_SIZE, self.OnSize) # Init DC : self.CreatePseudoDC() self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None) def MAJpanel(self): self.joursVacances, self.listePeriodesVacs = self.Importation_Vacances( self.anneeCalendrier) self.listeFeriesFixes, self.listeFeriesVariables = self.Importation_Feries( self.anneeCalendrier) self.CreatePseudoDC() self.Refresh() def CreatePseudoDC(self): # create a PseudoDC to record our drawing self.pdc = PseudoDC() self.DoDrawing(self.pdc) 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): xView, yView = self.GetViewStart() xDelta, yDelta = self.GetScrollPixelsPerUnit() if 'phoenix' in wx.PlatformInfo: r.Offset(-(xView * xDelta), -(yView * yDelta)) else: r.OffsetXY(-(xView * xDelta), -(yView * yDelta)) 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.BufferedPaintDC(self) # use PrepateDC to set position correctly if wx.VERSION < (2, 9, 0, 0): 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 DoDrawing(self, dc): dc.RemoveAll() if 'phoenix' not in wx.PlatformInfo: dc.BeginDrawing() self.caseSurvol = None self.Calendrier(dc) if 'phoenix' not in wx.PlatformInfo: dc.EndDrawing() def MAJAffichage(self): self.DoDrawing(self.pdc) self.Refresh() def OnSize(self, event): self.MAJAffichage() event.Skip() def Calendrier(self, dc): try: self.joursVacances except: self.joursVacances, self.listePeriodesVacs = self.Importation_Vacances( self.anneeCalendrier) try: self.listeFeriesFixes except: self.listeFeriesFixes, self.listeFeriesVariables = self.Importation_Feries( self.anneeCalendrier) self.dictCases = {} self.listeCasesJours = [] if 'phoenix' in wx.PlatformInfo: largeur, hauteur = self.GetClientSize() else: largeur, hauteur = self.GetClientSizeTuple() annee = self.anneeCalendrier if self.typeCalendrier == "mensuel": # Création d'un calendrier mensuel mois = self.moisCalendrier self.listeJoursAvecPresents = self.Importation_JoursAvecPresents( annee=annee, mois=mois) self.DrawMonth(dc, mois, annee, 0, 0, largeur, hauteur) else: # Création d'un calendrier annuel largeurMois = largeur / 4.0 hauteurMois = hauteur / 3.0 numMois = 1 self.listeJoursAvecPresents = self.Importation_JoursAvecPresents( annee=annee) for colonne in range(3): y = colonne * (hauteurMois + (self.ecartMois / 2.0)) for ligne in range(4): x = ligne * (largeurMois + (self.ecartMois / 3.0)) self.DrawMonth(dc, numMois, annee, x, y, largeurMois - self.ecartMois, hauteurMois - self.ecartMois) numMois += 1 def OnKeyDown(self, event): keycode = event.GetKeyCode() if keycode == wx.WXK_CONTROL: self.multiSelect = "CONTROL" if keycode == wx.WXK_SHIFT: self.multiSelect = "SHIFT" self.multiSelectWE = True if keycode == wx.WXK_ALT: self.multiSelect = "SHIFT" self.multiSelectWE = False event.Skip() def OnKeyUp(self, event): self.multiSelect = None self.multiSelectWE = False event.Skip() def VerifKeyStates(self): """ est utilisé pour être sûr que le programme a bien remarqué les touches pressées """ etat_Control = wx.GetKeyState(wx.WXK_CONTROL) etat_Shift = wx.GetKeyState(wx.WXK_SHIFT) etat_Alt = wx.GetKeyState(wx.WXK_ALT) if etat_Control == True: self.multiSelect = "CONTROL" if etat_Shift == True: self.multiSelect = "SHIFT" self.multiSelectWE = True if etat_Alt == True: self.multiSelect = "SHIFT" self.multiSelectWE = False def OnLeftDown(self, event): """ Sélection de la case cliquée """ self.VerifKeyStates() if self.multiSelections == False: self.multiSelect = None self.multiSelectWE = False if self.selectionInterdite == True: return x, y = self.ConvertEventCoords(event) listeObjets = self.pdc.FindObjectsByBBox(x, y) IDobjet = 0 if len(listeObjets) != 0: IDobjet = listeObjets[0] date = self.IDobjetEnDate(IDobjet) x = 1 if x == 1: # CASES DATES ----------------------------------------------------------------------- # Si la case est déjà sélectionnée, on la supprime de la liste des sélections if len(self.listeSelections) != 0: if date in self.listeSelections: self.listeSelections.remove(date) self.RedrawCase(IDobjet, survol=True) self.SendDates() return # Ajout de la case à la liste des sélections if self.multiSelect == "CONTROL": # MultiSelections avec la touche CTRL self.listeSelections.append(date) IDobjet = self.DateEnIDobjet(date) self.RedrawCase(IDobjet, survol=True) self.SendDates() elif self.multiSelect == "SHIFT": # MultisSelections avec la touche SHIFT self.listeSelections.append(date) self.listeSelections.sort() posTemp = self.listeSelections.index(date) - 1 previousDate = self.listeSelections[posTemp] jourDebut = previousDate jourFin = date # Si sélection inverse if jourDebut > jourFin: jourDebut = date jourFin = previousDate nbreJours = (jourFin - jourDebut).days dateEnCours = jourDebut for j in range(nbreJours): dateEnCours = dateEnCours + datetime.timedelta(days=1) # si MultiSelectWE=False, on ne garde ni les samedis ni les dimanches if self.multiSelectWE == False: if dateEnCours.isoweekday( ) == 6 or dateEnCours.isoweekday() == 7: continue # Mémorisation de la date if not dateEnCours in self.listeSelections: self.listeSelections.append(dateEnCours) # Si le dernier jour est dans le week-end et si MultiSelectWE=False, on le supprime if self.multiSelectWE == False: if dateEnCours.isoweekday( ) == 6 or dateEnCours.isoweekday() == 7: self.listeSelections.remove(date) self.listeSelections.sort() self.MAJAffichage() self.RedrawCase(IDobjet, survol=True) self.SendDates() else: # Sélection Unique self.listeSelections = [] self.listeSelections.append(date) self.MAJAffichage() self.RedrawCase(IDobjet, survol=True) self.SendDates() else: # CASES NOMS DE JOURS ----------------------------------------------------------------- if self.multiSelections == False: return for caseJour in self.listeCasesJours: if (caseJour[0] <= x <= (caseJour[0] + caseJour[2])) and ( caseJour[1] <= y <= (caseJour[1] + caseJour[3])): # Sélection par colonne if self.multiSelect == "CONTROL" or self.multiSelect == "SHIFT": deselect = [] nbreSemaines = 0 # avec la touche CTRL numJour, mois, annee = caseJour[4] datesMois = calendar.monthcalendar(annee, mois) for semaine in datesMois: selTemp = semaine[numJour] if selTemp != 0: nbreSemaines += 1 dateTemp = datetime.date(annee, mois, selTemp) if dateTemp not in self.listeSelections: self.listeSelections.append(dateTemp) else: deselect.append(dateTemp) # Si tous les jours ont déjà été sélectionnés, c'est que l'on doit désélectionner tout le mois : if len(deselect) == nbreSemaines: for date in deselect: self.listeSelections.remove(date) else: # Sélection Unique tempSelections = [] deselect = [] nbreSemaines = 0 numJour, mois, annee = caseJour[4] datesMois = calendar.monthcalendar(annee, mois) for semaine in datesMois: selTemp = semaine[numJour] if selTemp != 0: nbreSemaines += 1 dateTemp = datetime.date(annee, mois, selTemp) if dateTemp not in self.listeSelections: tempSelections.append(dateTemp) else: deselect.append(dateTemp) # Si tous les jours ont déjà été sélectionnés, c'est que l'on doit désélectionner tout le mois : if len(deselect) == nbreSemaines: self.listeSelections = [] else: # Sélection de tout le mois self.listeSelections = [] for date in tempSelections: self.listeSelections.append(date) for date in deselect: self.listeSelections.append(date) self.SendDates() self.MAJAffichage() event.Skip() def SendDates(self): """ Envoie la liste des dates sélectionnées """ event = SelectDatesEvent(selections=self.listeSelections) wx.PostEvent(self.GetParent(), event) def OnLeave(self, event): if self.onLeave == True: if self.caseSurvol != None: self.RedrawCase(self.caseSurvol, survol=False) self.caseSurvol = None try: wx.GetApp().GetTopWindow().SetStatusText("", 0) except: pass try: wx.GetApp().GetTopWindow().SetStatusText("", 0) except: pass event.Skip() def OnEnter(self, event): txt = _( u"Restez appuyer sur les touches CONTROL, SHIFT ou ALT pour sélectionner plusieurs jours à la fois." ) try: wx.GetApp().GetTopWindow().SetStatusText(txt, 1) except: pass event.Skip() def RedrawCase(self, IDobjet, survol=False): """ Redessine une case """ # DC dc = self.pdc # Efface l'id de la liste des objets du pseudoDC dc.ClearId(IDobjet) # Redessine la case caractCase = self.dictCases[IDobjet] self.DrawCase(dc, caractCase[4], caractCase[0], caractCase[1], caractCase[2], caractCase[3], survol=survol) # Redessine uniquement la zone modifiée r = dc.GetIdBounds(IDobjet) self.OffsetRect(r) self.RefreshRect(r, False) def OnMotion(self, event): # Allume la case x, y = self.ConvertEventCoords(event) listeObjets = self.pdc.FindObjectsByBBox(x, y) IDobjet = 0 if len(listeObjets) != 0: IDobjet = listeObjets[0] # Si cette case est déjà actuellement survolée, on passe... if self.caseSurvol != None: if self.caseSurvol == IDobjet: return # Activation de la case sélectionnée # Si une case a déjà été survolée avant, on l'annule if self.caseSurvol != None: self.RedrawCase(self.caseSurvol, survol=False) # Redessine la nouvelle case self.caseSurvol = IDobjet self.RedrawCase(IDobjet, survol=True) # Ecriture d'un texte dans la statutBar date = self.IDobjetEnDate(IDobjet) listeJours = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") listeMois = (_(u"janvier"), _(u"février"), _(u"mars"), _(u"avril"), _(u"mai"), _(u"juin"), _(u"juillet"), _(u"août"), _(u"septembre"), _(u"octobre"), _(u"novembre"), _(u"décembre")) dateStr = listeJours[date.weekday()] + " " + str( date.day) + " " + listeMois[date.month - 1] + " " + str( date.year) texteStatusBar = dateStr # Si c'est un jour de vacances if date in self.joursVacances: texteStatusBar += _(u" | Jour de vacances") # Si c'est un jour férié if (date.day, date.month) in self.listeFeriesFixes: texteStatusBar += _(u" | Jour férié") else: if date in self.listeFeriesVariables: texteStatusBar += _(u" | Jour férié") # Actualisation la statusBar try: wx.GetApp().GetTopWindow().SetStatusText(texteStatusBar, 0) except: pass return # Si on ne survole aucune case : Désactivation de la case précédemment sélectionnée if self.caseSurvol != None: self.RedrawCase(self.caseSurvol, survol=False) self.caseSurvol = None try: wx.GetApp().GetTopWindow().SetStatusText("", 0) except: pass def DrawMonth(self, dc, mois, annee, xMois, yMois, largMois, hautMois): """ Dessine un mois entier """ # Recherche une liste des dates du mois datesMois = calendar.monthcalendar(annee, mois) nbreSemaines = len(datesMois) # Création de l'entete avec le nom du mois if self.headerMois == True: hautMois, yMois = self.DrawHeaderMois(dc, nbreSemaines, mois, annee, xMois, yMois, largMois, hautMois) # Création de l'entete avec les noms des jours if self.headerJours == True: hautMois, yMois = self.DrawHeaderJours(dc, nbreSemaines, mois, annee, xMois, yMois, largMois, hautMois) # Calcule la taille d'une case largCase = (largMois / 7.0) hautCase = (hautMois / float(nbreSemaines)) # Créée les cases jours for numSemaine in range(nbreSemaines): for numJour in range(7): jour = datesMois[numSemaine][numJour] if jour != 0: # Crée les données de la case x = xMois + (largCase * numJour) y = yMois + (hautCase * numSemaine) l = largCase - self.ecartCases h = hautCase - self.ecartCases texteDate = datetime.date(annee, mois, jour) # Enregistrement des données dans une liste caractCase = (x, y, l, h, texteDate) IDcase = self.DateEnIDobjet(texteDate) self.dictCases[IDcase] = caractCase # Dessin de la case self.DrawCase(dc, texteDate, x, y, l, h) def DateEnIDobjet(self, date): annee = str(date.year) mois = str(date.month) jour = str(date.day) if len(mois) == 1: mois = "0" + mois if len(jour) == 1: jour = "0" + jour IDobjet = int(annee + mois + jour) return IDobjet def IDobjetEnDate(self, IDobjet): IDobjet = str(IDobjet) annee = int(IDobjet[:4]) mois = int(IDobjet[4:6]) jour = int(IDobjet[6:8]) date = datetime.date(annee, mois, jour) return date def DrawHeaderJours(self, dc, nbreSemaines, mois, annee, xMois, yMois, largMois, hautMois): """ Dessine un header comportant les noms des jours """ #self.listeCasesJours = [] listeJours = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") largCase = (largMois / 7.0) hautCase = (hautMois / float(nbreSemaines)) # Réglage de la police dc.SetTextForeground(self.couleurFontJours) taille = self.tailleFont(largCase, hautCase) font = wx.Font(taille, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) dc.SetFont(font) hautHeader = taille * 2 x = 0 for jour in range(7): texte = listeJours[jour] texteComplet = (jour, mois, annee) # Réglage du format du texte en fonction de la taille de la case if largCase < 50: texte = texte[:3] if largCase < 25: texte = texte[0] largTexte, hautTexte = self.GetTextExtent(texte) coordX = xMois + x + (largCase / 2) - (largTexte / 2) coordY = yMois + (hautHeader / 2) - (hautTexte / 2) dc.DrawText(texte, coordX, coordY) # Mémorisation des jours et de leurs coordonnées self.listeCasesJours.append( (coordX, coordY, largTexte, hautTexte, texteComplet)) x += largCase return hautMois - hautHeader, yMois + hautHeader def DrawHeaderMois(self, dc, nbreSemaines, mois, annee, xMois, yMois, largMois, hautMois): """ Dessine un header comportant le nom du mois """ listeMois = (_(u"Janvier"), _(u"Février"), _(u"Mars"), _(u"Avril"), _(u"Mai"), _(u"Juin"), _(u"Juillet"), _(u"Août"), _(u"Septembre"), _(u"Octobre"), _(u"Novembre"), _(u"Décembre")) largCase = (largMois / 7.0) hautCase = (hautMois / float(nbreSemaines)) # Réglage de la police dc.SetTextForeground(self.couleurFontJours) taille = self.tailleFont(largCase, hautCase) font = wx.Font(taille, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) dc.SetFont(font) hautHeader = taille * 3 # Dessin du texte texte = listeMois[mois - 1] + " " + str(annee) largTexte, hautTexte = self.GetTextExtent(texte) dc.DrawText(texte, xMois + (largMois / 2) - (largTexte / 2), yMois + (hautHeader / 2) - (hautTexte / 2)) # Dessin de la ligne dc.SetPen(wx.Pen((210, 210, 210), 1)) dc.DrawLine(xMois + 2, yMois + hautHeader - 2, xMois + largMois - 2, yMois + hautHeader - 2) return hautMois - hautHeader, yMois + hautHeader def DrawCase(self, dc, texteDate, x, y, l, h, survol=False): IDobjet = self.DateEnIDobjet(texteDate) dc.SetId(IDobjet) # Application des couleurs normales dc.SetBrush(wx.Brush(self.couleurNormal)) dc.SetPen(wx.Pen(self.couleurFond, 1)) # Si c'est un jour de vacances if texteDate in self.joursVacances: dc.SetBrush(wx.Brush(self.couleurVacances)) # Si c'est un jour de Week-end jourSemaine = texteDate.isoweekday() if jourSemaine == 6 or jourSemaine == 7: dc.SetBrush(wx.Brush(self.couleurWE)) # Si c'est un jour férié if (texteDate.day, texteDate.month) in self.listeFeriesFixes: dc.SetBrush(wx.Brush(self.couleurFerie)) else: if texteDate in self.listeFeriesVariables: dc.SetBrush(wx.Brush(self.couleurFerie)) # Si c'est une case survolée if survol == True: #dc.SetBrush(wx.Brush('black', wx.TRANSPARENT)) dc.SetPen(wx.Pen(self.couleurSurvol, 1)) # Dessine d'une case sélectionnée if len(self.listeSelections) != 0: if texteDate in self.listeSelections: dc.SetBrush(wx.Brush(self.couleurSelect)) # Dessin de la case dc.DrawRectangle(x, y, l, h) # Dessin du symbole Aujourd'hui if texteDate == datetime.date.today(): dc.SetBrush(wx.Brush((255, 0, 0))) dc.SetPen(wx.Pen((255, 0, 0), 0)) posG = x + l - 2 posH = y + 1 tailleTriangle = 5 dc.DrawPolygon([(posG, posH), (posG - tailleTriangle, posH), (posG, posH + tailleTriangle)]) # Dessin texte if unicode(texteDate) in self.listeJoursAvecPresents: dc.SetTextForeground(self.couleurFontJoursAvecPresents) else: dc.SetTextForeground(self.couleurFontJours) font = self.GetFont() font.SetPointSize(self.tailleFont(l, h)) dc.SetFont(font) dc.DrawText(str(texteDate.day), x + 3, y + 2) # Traitement pour le PseudoDC r = wx.Rect(x, y, l, h) dc.SetIdBounds(IDobjet, r) def tailleFont(self, l, h): # On prend le côté le plus petit if l > h: cote = h else: cote = l # On définit des ordres de grandeur if cote <= 14: return 6 if cote <= 20: return 7 if cote <= 40: return 7 if cote <= 60: return 8 if cote <= 80: return 9 if cote <= 120: return 10 return 12 def SetTypeCalendrier(self, typeCal): self.typeCalendrier = typeCal self.MAJAffichage() def GetTypeCalendrier(self): return self.typeCalendrier def SetMoisAnneeCalendrier(self, mois=0, annee=0): if mois != 0: self.moisCalendrier = mois if annee != 0: self.anneeCalendrier = annee self.MAJAffichage() def GetMoisAnneeCalendrier(self): return self.moisCalendrier, self.anneeCalendrier def GetTypeCalendrier(self): return self.typeCalendrier def Importation_Vacances(self, anneeCalendrier=None): """ Importation des dates de vacances """ req = "SELECT * FROM vacances ORDER BY date_debut;" DB = GestionDB.DB() DB.ExecuterReq(req) listeVacances1 = DB.ResultatReq() DB.Close() listeVacances2 = [] listePeriodesVacs = [] for id, nom, annee, date_debut, date_fin in listeVacances1: datedebut = datetime.date(int(date_debut[:4]), int(date_debut[5:7]), int(date_debut[8:10])) datefin = datetime.date(int(date_fin[:4]), int(date_fin[5:7]), int(date_fin[8:10])) listeVacances2.append(datedebut) listeTemp = [] for x in range((datefin - datedebut).days): # Ajout à la liste des jours de vacances (qui sert au coloriage de la case) datedebut = datedebut + datetime.timedelta(days=1) listeVacances2.append(datedebut) listeTemp.append(datedebut) # Ajout au dictionnaire des vacances (qui sert à sélectionner une période de vacs dans le calendrier) listePeriodesVacs.append((annee, nom, tuple(listeTemp))) return listeVacances2, listePeriodesVacs def Importation_Feries(self, anneeCalendrier=None): """ Importation des dates de vacances """ req = "SELECT * FROM jours_feries;" DB = GestionDB.DB() DB.ExecuterReq(req) listeFeriesTmp = DB.ResultatReq() DB.Close() listeFeriesFixes = [] listeFeriesVariables = [] for ID, type, nom, jour, mois, annee in listeFeriesTmp: if type == "fixe": date = (jour, mois) listeFeriesFixes.append(date) else: date = datetime.date(annee, mois, jour) listeFeriesVariables.append(date) return listeFeriesFixes, listeFeriesVariables def Importation_JoursAvecPresents(self, annee=None, mois=None): return [] ###### MODIFIE ICI ############# if mois == None: dateDebut = str(annee) + "-01-01" dateFin = str(annee) + "-12-31" else: strMois = str(mois) if len(strMois) == 1: strMois = "0" + strMois dateDebut = str(annee) + "-" + strMois + "-01" dateFin = str(annee) + "-" + strMois + "-31" """ Importation des dates ou il y a des présents """ DB = GestionDB.DB() req = """ SELECT date FROM presences WHERE date >= '%s' And date <= '%s' GROUP BY presences.date; """ % (dateDebut, dateFin) DB.executerReq(req) listeDonnees = DB.resultatReq() DB.close() # Transformation en bonne liste listeDonnees2 = [] for date in listeDonnees: listeDonnees2.append(date[0]) return listeDonnees2 def OnContextMenu(self, nomCase): """ Menu contextuel du calendrier """ if self.caseSurvol != None: texteDate = self.IDobjetEnDate(self.caseSurvol) else: texteDate = None if self.selectionInterdite == True: return # Création du menu menu = wx.Menu() if self.multiSelections == True: # Si une date a bien été cliquée : if texteDate != None: # Vérifie si date déjà sélectionnée if texteDate in self.listeSelections: select = True else: select = False # Sélection/déselection du jour cliqué self.popupID1 = wx.NewId() if select == False: texte = _(u"Sélectionner le %02d/%02d/%04d") % ( texteDate.day, texteDate.month, texteDate.year) else: texte = _(u"Désélectionner le %02d/%02d/%04d") % ( texteDate.day, texteDate.month, texteDate.year) menu.Append(self.popupID1, texte) self.Bind(wx.EVT_MENU, self.OnPopup1, id=self.popupID1) menu.AppendSeparator() # Choisir la date d'aujourd'hui self.popupID4 = wx.NewId() menu.Append(self.popupID4, _(u"Sélectionner aujourd'hui")) self.Bind(wx.EVT_MENU, self.OnPopup4, id=self.popupID4) if self.multiSelections == True: # Choisir tout le mois self.popupID5 = wx.NewId() menu.Append(self.popupID5, _(u"Sélectionner tout le mois")) self.Bind(wx.EVT_MENU, self.OnPopup5, id=self.popupID5) # Choisir une période de vacances self.popupID3 = wx.NewId() if len(self.listePeriodesVacs) != 0: sm = wx.Menu() index = 0 self.listePeriodesVacs.reverse() # Seules les 20 dernières périodes sont retenues for annee, nomPeriode, listeJours in self.listePeriodesVacs[: 20]: id = 1000 + index sm.Append(id, nomPeriode + " " + str(annee)) self.Bind(wx.EVT_MENU, self.OnPopup3, id=id) index += 1 # Inclus le sous-menu dans le menu menu.AppendMenu(self.popupID3, _(u"Sélectionner une période de vacances"), sm) # Tout désélectionner self.popupID7 = wx.NewId() menu.Append(self.popupID7, _(u"Tout désélectionner")) self.Bind(wx.EVT_MENU, self.OnPopup7, id=self.popupID7) # Exclure les jours de week-end dans les sélections self.popupID6 = wx.NewId() menu.Append(self.popupID6, _(u"Exclure les week-ends des sélections"), _(u"Exclure les week-ends de la sélection"), wx.ITEM_CHECK) if self.selectExclureWE == True: menu.Check(self.popupID6, True) self.Bind(wx.EVT_MENU, self.OnPopup6, id=self.popupID6) # Aide sur le calendrier menu.AppendSeparator() self.popupID2 = wx.NewId() menu.Append(self.popupID2, _(u"Aide sur le calendrier")) self.Bind(wx.EVT_MENU, self.OnPopup2, id=self.popupID2) # make a submenu #sm = wx.Menu() #sm.Append(self.popupID8, "sub item 1") #sm.Append(self.popupID9, "sub item 1") #menu.AppendMenu(self.popupID7, "Test Submenu", sm) self.onLeave = False self.PopupMenu(menu) menu.Destroy() self.onLeave = True #self.RedrawCase(caseSurvol, survol=True) def OnPopup1(self, event): """ Sélection ou désélection """ texteDate = self.IDobjetEnDate(self.caseSurvol) # Vérifie si date déjà sélectionnée if texteDate in self.listeSelections: select = True else: select = False # Désélection de la date if select == True: self.listeSelections.remove(texteDate) self.SelectJours(self.listeSelections) ## self.RedrawCase(self.caseSurvol, survol=True) return # Sélection de la date else: self.listeSelections.append(texteDate) self.SelectJours(self.listeSelections) ## self.RedrawCase(self.caseSurvol, survol=True) return def OnPopup2(self, event): """ Aide sur le calendrier """ print "Aide..." # FonctionsPerso.Aide(51) def OnPopup3(self, event): """ Sélection d'une période de vacances """ index = event.GetId() - 1000 nomPeriode, annee, listeJours = self.listePeriodesVacs[index] # Mets les jours de vacances dans la liste de sélections # Enleve les week-ends si nécessaires : if self.selectExclureWE == True: listeJoursTmp = [] for jour in listeJours: jourSemaine = jour.isoweekday() if jourSemaine != 6 and jourSemaine != 7: listeJoursTmp.append(jour) listeJours = listeJoursTmp self.SelectJours(list(listeJours)) def OnPopup6(self, event): """ Inclure ou non les week-ends dans la sélection """ self.selectExclureWE = event.IsChecked() def OnPopup4(self, event): """ Choisir la date d'aujourd'hui """ self.SelectJours([ datetime.date.today(), ]) def OnPopup5(self, event): """ Choisir tout le mois """ listeJours = [] datesDuMois = calendar.monthcalendar(self.anneeCalendrier, self.moisCalendrier) for semaine in datesDuMois: for jour in semaine: if jour != 0: jourTmp = datetime.date(year=self.anneeCalendrier, month=self.moisCalendrier, day=jour) if self.selectExclureWE == True: jourSemaine = jourTmp.isoweekday() if jourSemaine != 6 and jourSemaine != 7: listeJours.append(jourTmp) else: listeJours.append(jourTmp) self.SelectJours(listeJours) def OnPopup7(self, event): """ Tout désélectionner """ self.SelectJours(listeJours=[]) def SelectJours(self, listeJours=[]): """ Met à jour l'affichage du calendrier et le planning en fonction des sélections """ self.listeSelections = listeJours if len(listeJours) != 0: # Se place sur le mois ou l'année du premier jour de la période de vacances sélectionnée moisDebut, anneeDebut = listeJours[0].month, listeJours[0].year self.moisCalendrier = moisDebut self.anneeCalendrier = anneeDebut # Actualisation de l'affichage self.SendDates() self.MAJAffichage() # Met à jour l'affichage des contrôles de navigation du calendrier try: self.GetParent().MAJcontrolesNavigation(self.moisCalendrier, self.anneeCalendrier) except: pass def GetSelections(self): return self.listeSelections