diff --git a/BibEnter.py b/BibEnter.py index 35395a6d2..f45c26f52 100644 --- a/BibEnter.py +++ b/BibEnter.py @@ -2,7 +2,7 @@ import Utils import Model from EditEntry import DoDNF, DoDNS, DoPull, DoDQ -from NumKeypad import getRiderNumsFromText, enterCodes, validKeyCodes +from Keypad import getRiderNumsFromText, enterCodes, validKeyCodes class BibEnter( wx.Dialog ): def __init__( self, parent, id = wx.ID_ANY ): diff --git a/BibTimeRecord.py b/BibTimeRecord.py index dc53d9d3f..809121936 100644 --- a/BibTimeRecord.py +++ b/BibTimeRecord.py @@ -4,25 +4,31 @@ import Model import Utils +from Log import CrossMgrLogger, getLogger +from ManualTimeEntryPanel import ManualTimeEntryPanel, TimeEntryController from ReorderableGrid import ReorderableGrid from EditEntry import DoDNF, DoDNS, DoPull, DoDQ from InputUtils import enterCodes, validKeyCodes, clearCodes, actionCodes, getRiderNumsFromText, MakeKeypadButton -class BibTimeRecord( wx.Panel ): - def __init__( self, parent, controller, id = wx.ID_ANY ): - super().__init__(parent, id) - # self.SetBackgroundColour( wx.Colour(173, 216, 230) ) - self.SetBackgroundColour( wx.WHITE ) +class BibTimeRecord( ManualTimeEntryPanel, TimeEntryController ): + __log: CrossMgrLogger + + @property + def log(self): + if self.__log is None: + self.__log = getLogger('CrossMgr.BibTimeRecord') + return self.__log + + def __init__( self, parent: wx.Window, controller: ManualTimeEntryPanel|None = None, id = wx.ID_ANY ): + super().__init__(parent=parent, controller=controller, id=id) - self.controller = controller - fontPixels = 36 font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) dc = wx.WindowDC( self ) dc.SetFont( font ) wNum, hNum = dc.GetTextExtent( '999' ) wNum += 8 - hNum += 8 + hNum += 8 outsideBorder = 4 @@ -101,14 +107,7 @@ def handleNumKeypress(self, event): elif keycode in clearCodes: self.numEdit.SetValue( '' ) elif keycode in actionCodes: - if keycode == ord('/'): # DNF - pass - elif keycode == ord('*'): # DNS - pass - elif keycode == ord('-'): # PUL - pass - elif keycode == ord('+'): # DQ - pass + pass elif keycode < 255: if keycode in validKeyCodes: event.Skip() @@ -138,8 +137,7 @@ def doSetTime( self, event ): mainWin = Utils.getMainWin() if mainWin is not None: mainWin.forecastHistory.logNum( int(num) ) - if self.controller: - self.controller.refreshLaps() + self.refreshLaps() if row < self.grid.GetNumberRows(): self.grid.DeleteRows( row, 1 ) @@ -258,6 +256,26 @@ def OnPopupChart( self, event ): mainWin = Utils.getMainWin() if self.bibCur and mainWin: mainWin.forecastHistory.SelectNumShowPage( self.bibCur, 'iChartPage' ) + + def _EnableControls(self) -> None: + self.log.todo('BibTimeRecord._EnableControls() not yet implemented.') + + def _DisableControls(self) -> None: + self.log.todo('BibTimeRecord._DisableControls() not yet implemented.') + + def __SafeSetFocus(self): + try: + super().SetFocus() + try: + self.numEdit.SetFocus() + except Exception as e: + self.log.error( f'Error setting on BibTimeRecord numEdit: {e}' ) + except Exception as e: + self.log.error( f'Error setting on BibTimeRecord: {e}' ) + + def SetFocus(self): + self.__SafeSetFocus() + if __name__ == '__main__': Utils.disable_stdout_buffering() @@ -265,7 +283,12 @@ def OnPopupChart( self, event ): mainWin = wx.Frame(None,title="CrossMan", size=(600,600)) Model.setRace( Model.Race() ) Model.getRace()._populate() - bibTimeRecord = BibTimeRecord(mainWin, None) + + AnonymousTimeEntryController = type('TimeEntryController', (object,), {'refreshLaps': lambda self: print ('Laps refreshed')}) + testController = AnonymousTimeEntryController() + + bibTimeRecord = BibTimeRecord(mainWin, testController) + bibTimeRecord.Disable() bibTimeRecord.refresh() mainWin.Show() app.MainLoop() diff --git a/ForecastHistory.py b/ForecastHistory.py index eb5d5c863..146859897 100644 --- a/ForecastHistory.py +++ b/ForecastHistory.py @@ -7,7 +7,7 @@ from Utils import formatTime, formatTimeGap import ColGrid import OutputStreamer -import NumKeypad +import Keypad from PhotoFinish import TakePhoto, okTakePhoto from GetResults import GetResults, GetResultsWithData, IsRiderFinished from EditEntry import CorrectNumber, SplitNumber, ShiftNumber, InsertNumber, DeleteEntry diff --git a/Keypad.py b/Keypad.py new file mode 100644 index 000000000..f4b4cb92c --- /dev/null +++ b/Keypad.py @@ -0,0 +1,323 @@ +import wx +import os +import datetime +import wx.lib.intctrl +import wx.lib.buttons + +from collections import defaultdict + +import Utils +from GetResults import GetResults +import Model +from EditEntry import DoDNF, DoDNS, DoPull, DoDQ +from InputUtils import enterCodes, validKeyCodes, clearCodes, actionCodes, getRiderNumsFromText, MakeKeypadButton +from Log import getLogger, CrossMgrLogger +from ManualTimeEntryPanel import ManualTimeEntryPanel, TimeEntryController + +SplitterMinPos = 390 +SplitterMaxPos = 530 + +class Keypad( ManualTimeEntryPanel ): + __log: CrossMgrLogger + + @property + def log(self) -> CrossMgrLogger: + if self.__log is None: + self.__log = getLogger('CrossMgr.Keypad') + return self.__log + + def __init__( self, parent: wx.Window, controller: TimeEntryController|None = None, id = wx.ID_ANY ): + super().__init__(parent=parent, controller=controller, id=id) + + fontPixels = 36 + font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + dc = wx.WindowDC( self ) + dc.SetFont( font ) + wNum, hNum = dc.GetTextExtent( '999' ) + wNum += 8 + hNum += 8 + + outsideBorder = 4 + + vsizer = wx.BoxSizer( wx.VERTICAL ) + + # self._panel = wx.Panel( self ) + + self.numEditHS = wx.BoxSizer( wx.HORIZONTAL ) + + self.numEditLabel = wx.StaticText(self, label='{}'.format(_('Bib'))) + self.numEditLabel.SetFont( font ) + + editWidth = 140 + self.numEdit = wx.TextCtrl( self, style=wx.TE_RIGHT | wx.TE_PROCESS_ENTER, + size=(editWidth, int(fontPixels*1.2)) if 'WXMAC' in wx.Platform else (editWidth,-1) ) + self.numEdit.Bind( wx.EVT_CHAR, self.handleNumKeypress ) + self.numEdit.SetFont( font ) + + self.numEditHS.Add( self.numEditLabel, wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL ) + self.numEditHS.Add( self.numEdit, flag=wx.LEFT|wx.EXPAND, border = 4 ) + vsizer.Add( self.numEditHS, flag=wx.EXPAND|wx.LEFT|wx.TOP, border = outsideBorder ) + + #------------------------------------------------------------------------------------------ + self.keypadPanel = wx.Panel( self ) + gbs = wx.GridBagSizer(4, 4) + self.keypadPanel.SetSizer( gbs ) + + rowCur = 0 + numButtonStyle = 0 + self.num = [] + + self.num.append( MakeKeypadButton( self.keypadPanel, label='&0', style=wx.BU_EXACTFIT, font = font) ) + self.num[-1].Bind( wx.EVT_BUTTON, lambda event, aValue = 0 : self.onNumPress(event, aValue) ) + gbs.Add( self.num[0], pos=(3+rowCur,0), span=(1,2), flag=wx.EXPAND ) + + for i in range(9): + self.num.append( MakeKeypadButton( self.keypadPanel, label='&{}'.format(i+1), style=numButtonStyle, size=(wNum,hNum), font = font) ) + self.num[-1].Bind( wx.EVT_BUTTON, lambda event, aValue = i+1 : self.onNumPress(event, aValue) ) + j = 8-i + gbs.Add( self.num[-1], pos=(j//3 + rowCur, 2-j%3) ) + + self.delBtn = MakeKeypadButton( self.keypadPanel, id=wx.ID_DELETE, label=_('&Del'), style=numButtonStyle, size=(wNum,hNum), font = font) + self.delBtn.Bind( wx.EVT_BUTTON, self.onDelPress ) + gbs.Add( self.delBtn, pos=(3+rowCur,2) ) + rowCur += 4 + + self.enterBtn = MakeKeypadButton( self.keypadPanel, id=0, label=_('&Enter'), style=wx.EXPAND|wx.GROW, font = font) + gbs.Add( self.enterBtn, pos=(rowCur,0), span=(1,3), flag=wx.EXPAND ) + self.enterBtn.Bind( wx.EVT_LEFT_DOWN, self.onEnterPress ) + rowCur += 1 + + self.showTouchScreen = False + self.keypadPanel.Show( self.showTouchScreen ) + vsizer.Add( self.keypadPanel, flag=wx.TOP, border=4 ) + + font = wx.Font((0,int(fontPixels*.6)), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + + self._actionButtonSizer = wx.GridSizer( 2, 2, 4, 4 ) + for label, actionFn in [(_('DN&F'),DoDNF), (_('DN&S'),DoDNS), (_('&Pull'),DoPull), (_('D&Q'),DoDQ)]: + btn = MakeKeypadButton( self, label=label, style=wx.EXPAND|wx.GROW, font = font) + btn.Bind( wx.EVT_BUTTON, lambda event, fn = actionFn: self.doAction(fn) ) + self._actionButtonSizer.Add( btn, flag=wx.EXPAND ) + + vsizer.Add( self._actionButtonSizer, flag=wx.EXPAND|wx.TOP, border=4 ) + + self.touchBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'touch24.png'), wx.BITMAP_TYPE_PNG ) + self.touchButton = wx.BitmapButton( self, bitmap = self.touchBitmap ) + self.touchButton.Bind( wx.EVT_BUTTON, self.onToggleTouchScreen) + self.touchButton.SetToolTip(wx.ToolTip(_("Touch Screen Toggle"))) + + vsizer.Add( self.touchButton, flag=wx.TOP|wx.ALIGN_CENTRE, border=12 ) + self.SetSizer( vsizer ) + + def onToggleTouchScreen( self, event ): + self.showTouchScreen ^= True + self.keypadPanel.Show( self.showTouchScreen ) + self.GetSizer().Layout() + # There has to be a better way here to pass this call up + + try: + sashOwner = self.GetParent().GetParent().GetParent() + sashOwner.SetSashPosition( SplitterMinPos if self.showTouchScreen else SplitterMaxPos ) + except Exception as e: + self.log.error('Keypad control was not placed on panel with a sash great-grandparent.') + try: + self.GetParent().GetSizer().Layout() + except Exception: + pass + + def onNumPress( self, event, value ): + self.numEdit.SetInsertionPointEnd() + txt = self.numEdit.GetValue() + txt += '{}'.format(value) + self.numEdit.SetValue( txt ) + self.numEdit.SetInsertionPointEnd() + + def onDelPress( self, event ): + txt = self.numEdit.GetValue() + if txt is not None: + self.numEdit.SetValue( txt[:-1] ) + + def handleNumKeypress(self, event): + keycode = event.GetKeyCode() + if keycode in enterCodes: + self.onEnterPress() + elif keycode in clearCodes: + self.numEdit.SetValue( '' ) + elif keycode in actionCodes: + pass + elif keycode < 255: + if keycode in validKeyCodes: + event.Skip() + else: + Utils.writeLog( 'handleNumKeypress: ignoring keycode < 255: {}'.format(keycode) ) + else: + Utils.writeLog( 'handleNumKeypress: ignoring keycode: >= 255 {}'.format(keycode) ) + event.Skip() + + def onEnterPress( self, event = None ): + nums = getRiderNumsFromText( self.numEdit.GetValue() ) + if nums: + mainWin = Utils.getMainWin() + if mainWin is not None: + mainWin.forecastHistory.logNum( nums ) + self.refreshLaps() + wx.CallAfter( self.numEdit.SetValue, '' ) + + def doAction( self, action ): + race = Model.race + t = race.curRaceTime() if race and race.isRunning() else None + success = False + for num in getRiderNumsFromText( self.numEdit.GetValue() ): + if action(self, num, t): + success = True + if success: + self.numEdit.SetValue( '' ) + wx.CallAfter( Utils.refreshForecastHistory ) + + def _DisableControls(self): + self.numEdit.Disable() + self.enterBtn.Disable() + self.delBtn.Disable() + for b in self.num: + b.Disable() + for b in self._actionButtonSizer.GetChildren(): + if b.IsWindow(): + b.GetWindow().Disable() + + def _EnableControls(self): + self.numEdit.Enable() + self.enterBtn.Enable() + self.delBtn.Enable() + for b in self.num: + b.Enable() + + def __SafeSetFocus(self): + try: + super().SetFocus() + try: + self.numEdit.SetFocus() + except Exception as e: + self.log.error( f'Error setting on Keypad numEdit: {e}' ) + except Exception as e: + self.log.error( f'Error setting on Keypad: {e}' ) + + def SetFocus(self): + self.__SafeSetFocus() + +def getLapInfo( lap, lapsTotal, tCur, tNext, leader ): + race = Model.race + if not race or not race.startTime: + return + info = [] + startTime = race.startTime + + if lap > lapsTotal: + info.append( (_("Last Rider"), (startTime + datetime.timedelta(seconds=tNext)).strftime('%H:%M:%S')) ) + return info + + tLap = tNext - tCur + info.append( (_("Lap"), '{}/{} ({} {})'.format(lap,lapsTotal,lapsTotal-lap, _('to go'))) ) + info.append( (_("Time"), Utils.formatTimeGap(tLap, highPrecision=False)) ) + info.append( (_("Start"), (startTime + datetime.timedelta(seconds=tCur)).strftime('%H:%M:%S')) ) + info.append( (_("End"), (startTime + datetime.timedelta(seconds=tNext)).strftime('%H:%M:%S')) ) + lapDistance = None + try: + bib = int(leader.split()[-1]) + category = race.getCategory( bib ) + lapDistance = category.getLapDistance( lap ) + except Exception: + pass + if lapDistance is not None: + sLap = (lapDistance / tLap) * 60.0*60.0 + info.append( ('', '{:.02f} {}'.format(sLap, 'km/h')) ) + return info + +def getCategoryStats(): + race = Model.race + if not race: + return [] + + isRunning = race.isRunning() + isTimeTrial = race.isTimeTrial + lastRaceTime = race.lastRaceTime() + Finisher = Model.Rider.Finisher + DNS = Model.Rider.DNS + NP = Model.Rider.NP + + statusSortSeq = Model.Rider.statusSortSeq + statusNames = Model.Rider.statusNames + + finishedAll, onCourseAll, statsAll = 0, 0, defaultdict( int ) + + def getStatsStr( finished, onCourse, stats ): + total = finished + onCourse + sum( stats.values() ) + if total: + b = [f'{_("Starters")}({total})'] + if finished: + b.append( f'{_("Finished")}({finished})' ) + b.extend( f'{statusNames[k]}({v})' for k,v in sorted(stats.items(), key = lambda x: statusSortSeq[x[0]]) ) + return f'{_("OnCourse")}({onCourse}) = {" - ".join(b)}' + else: + return '' + + categoryStats = [(_('All'), '')] + for category in race.getCategories(): + finished, onCourse, stats = 0, 0, defaultdict( int ) + for rr in GetResults( category ): + status = rr.status + if status == DNS: + continue + + rider = race.riders[rr.num] + firstTime = rider.firstTime or 0.0 + if isTimeTrial: + if status == NP and lastRaceTime >= firstTime: + status = Finisher # Consider started riders as Finishers, not NP. + else: + if status == Finisher: + status = rider.status # Set status back to the original status (will set back to Pulled). + + if status == Finisher: + if rr.raceTimes: + lastTime, interp = rr.raceTimes[-1], rr.interp[-1] + if isTimeTrial: + # Adjust to the time trial start time. + lastTime += firstTime + else: + lastTime, interp = 0.0, True + + if lastTime <= lastRaceTime and (not interp if isRunning else True): + finished += 1 + else: + onCourse += 1 + else: + stats[status] += 1 + + statsStr = getStatsStr(finished, onCourse, stats) + if statsStr: + categoryStats.append( (f'{category.fullname}', statsStr) ) + + finishedAll += finished + onCourseAll += onCourse + for k, v in stats.items(): + statsAll[k] += v + + categoryStats[0] = ( _('All'), getStatsStr(finishedAll, onCourseAll, statsAll) ) + return categoryStats + + +if __name__ == '__main__': + Utils.disable_stdout_buffering() + app = wx.App(False) + mainWin = wx.Frame(None,title="CrossMgr Keypad", size=(1000,800)) + Model.setRace( Model.Race() ) + model = Model.getRace() + model._populate() + model.enableUSBCamera = False + AnonymousTimeEntryController = type('TimeEntryController', (object,), {'refreshLaps': lambda self: print ('Laps refreshed')}) + testController = AnonymousTimeEntryController() + + numKeypad = Keypad(mainWin, testController) + numKeypad.Disable(reason='This is a test') + mainWin.Show() + app.MainLoop() diff --git a/MainWin.py b/MainWin.py index 2e1c6c5e1..4e7f8b872 100644 --- a/MainWin.py +++ b/MainWin.py @@ -40,7 +40,7 @@ from AddExcelInfo import AddExcelInfo from LogPrintStackStderr import LogPrintStackStderr from ForecastHistory import ForecastHistory -from NumKeypad import NumKeypad +from Record import Record from Actions import Actions from Gantt import Gantt from History import History @@ -680,7 +680,7 @@ def addPage( page, name ): self.attrClassName = [ [ 'actions', Actions, _('Actions') ], - [ 'record', NumKeypad, _('Record') ], + [ 'record', Record, _('Record')], [ 'results', Results, _('Results') ], [ 'pulled', Pulled, _('Pulled') ], [ 'history', History, _('Passings') ], diff --git a/ManualTimeEntryPanel.py b/ManualTimeEntryPanel.py new file mode 100644 index 000000000..109b33a6e --- /dev/null +++ b/ManualTimeEntryPanel.py @@ -0,0 +1,83 @@ +from abc import abstractmethod + +import wx + + +class TimeEntryController: + @abstractmethod + def refreshLaps(self) -> None: + pass + +class ManualTimeEntryPanel( wx.Panel, TimeEntryController ): + _disableReason: str | None = None + _infoBar: wx.InfoBar + _infoBarSizer: wx.BoxSizer + + def __init__(self, parent: wx.Window, controller: TimeEntryController | None = None, id = wx.ID_ANY): + super().__init__(parent=parent, id=id) + self._disableReason = None + self._controller = controller + + self.SetBackgroundColour( wx.WHITE ) + + self._infoBarSizer = wx.BoxSizer(wx.VERTICAL) + self._infoBar = wx.InfoBar(self) + self._infoBarSizer.Add(self._infoBar, 0, wx.EXPAND | wx.ALL, 5) + + self._contentSizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self._contentSizer, False) + + super().SetSizer(self._infoBarSizer) + + self.Layout() + + + def SetSizer(self, sizer: wx.Sizer, deleteOld=True) -> None: + if deleteOld: + self._infoBarSizer.Remove(self._contentSizer) + + self._infoBarSizer.Add(sizer, 1, wx.EXPAND | wx.ALL, 5) + self._contentSizer = sizer + self.Layout() + + def SetSizerAndFit(self, sizer, deleteOld=True): + self.SetSizer(sizer, deleteOld) + self.Fit() + + def GetSizer(self) -> wx.Sizer: + return self._contentSizer + + def refreshLaps(self) -> None: + if self._controller: + self._controller.refreshLaps() + + @abstractmethod + def _DisableControls(self) -> None: + pass + + @abstractmethod + def _EnableControls(self) -> None: + pass + + def Disable(self, disable: bool=True, reason:str | None = None) -> None: + assert isinstance(disable, bool) + if disable is True: + if reason is None: + self._disableReason = None + message = 'Control is disabled.' + else: + self._disableReason = reason + message = f'Control is disabled: {self._disableReason}' + + self._infoBar.ShowMessage(message, wx.ICON_WARNING) + self._DisableControls() + else: + self.Enable() + + def Enable( self, enable=True ) -> None: + if enable is True: + self._disableReason = None + self._infoBar.Dismiss() + self._EnableControls() + else: + self.Disable() \ No newline at end of file diff --git a/NumKeypad.py b/Record.py similarity index 63% rename from NumKeypad.py rename to Record.py index 21d5724b9..983a18f54 100644 --- a/NumKeypad.py +++ b/Record.py @@ -1,768 +1,528 @@ -import wx -import os -import sys -import bisect -import datetime -import wx.lib.intctrl -import wx.lib.buttons - -from collections import defaultdict - -import Utils -from Utils import SetLabel -from GetResults import GetResults, GetResultsWithData, GetLastRider -import Model -from RaceHUD import RaceHUD -from EditEntry import DoDNF, DoDNS, DoPull, DoDQ -from TimeTrialRecord import TimeTrialRecord -from BibTimeRecord import BibTimeRecord -from ClockDigital import ClockDigital -from NonBusyCall import NonBusyCall -from SetLaps import SetLaps -from InputUtils import enterCodes, validKeyCodes, clearCodes, actionCodes, getRiderNumsFromText, MakeKeypadButton -from LapsToGoCount import LapsToGoCountGraph - -SplitterMinPos = 390 -SplitterMaxPos = 530 - -class Keypad( wx.Panel ): - def __init__( self, parent, controller, id = wx.ID_ANY ): - super().__init__(parent, id) - self.SetBackgroundColour( wx.WHITE ) - self.controller = controller - - fontPixels = 36 - font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - dc = wx.WindowDC( self ) - dc.SetFont( font ) - wNum, hNum = dc.GetTextExtent( '999' ) - wNum += 8 - hNum += 8 - - outsideBorder = 4 - - vsizer = wx.BoxSizer( wx.VERTICAL ) - - self.numEditHS = wx.BoxSizer( wx.HORIZONTAL ) - - self.numEditLabel = wx.StaticText(self, label='{}'.format(_('Bib'))) - self.numEditLabel.SetFont( font ) - - editWidth = 140 - self.numEdit = wx.TextCtrl( self, style=wx.TE_RIGHT | wx.TE_PROCESS_ENTER, - size=(editWidth, int(fontPixels*1.2)) if 'WXMAC' in wx.Platform else (editWidth,-1) ) - self.numEdit.Bind( wx.EVT_CHAR, self.handleNumKeypress ) - self.numEdit.SetFont( font ) - - self.numEditHS.Add( self.numEditLabel, wx.ALIGN_CENTRE | wx.ALIGN_CENTRE_VERTICAL ) - self.numEditHS.Add( self.numEdit, flag=wx.LEFT|wx.EXPAND, border = 4 ) - vsizer.Add( self.numEditHS, flag=wx.EXPAND|wx.LEFT|wx.TOP, border = outsideBorder ) - - #------------------------------------------------------------------------------------------ - self.keypadPanel = wx.Panel( self ) - gbs = wx.GridBagSizer(4, 4) - self.keypadPanel.SetSizer( gbs ) - - rowCur = 0 - numButtonStyle = 0 - self.num = [] - - self.num.append( MakeKeypadButton( self.keypadPanel, label='&0', style=wx.BU_EXACTFIT, font = font) ) - self.num[-1].Bind( wx.EVT_BUTTON, lambda event, aValue = 0 : self.onNumPress(event, aValue) ) - gbs.Add( self.num[0], pos=(3+rowCur,0), span=(1,2), flag=wx.EXPAND ) - - for i in range(9): - self.num.append( MakeKeypadButton( self.keypadPanel, label='&{}'.format(i+1), style=numButtonStyle, size=(wNum,hNum), font = font) ) - self.num[-1].Bind( wx.EVT_BUTTON, lambda event, aValue = i+1 : self.onNumPress(event, aValue) ) - j = 8-i - gbs.Add( self.num[-1], pos=(j//3 + rowCur, 2-j%3) ) - - self.delBtn = MakeKeypadButton( self.keypadPanel, id=wx.ID_DELETE, label=_('&Del'), style=numButtonStyle, size=(wNum,hNum), font = font) - self.delBtn.Bind( wx.EVT_BUTTON, self.onDelPress ) - gbs.Add( self.delBtn, pos=(3+rowCur,2) ) - rowCur += 4 - - self.enterBtn = MakeKeypadButton( self.keypadPanel, id=0, label=_('&Enter'), style=wx.EXPAND|wx.GROW, font = font) - gbs.Add( self.enterBtn, pos=(rowCur,0), span=(1,3), flag=wx.EXPAND ) - self.enterBtn.Bind( wx.EVT_LEFT_DOWN, self.onEnterPress ) - rowCur += 1 - - self.showTouchScreen = False - self.keypadPanel.Show( self.showTouchScreen ) - vsizer.Add( self.keypadPanel, flag=wx.TOP, border=4 ) - - font = wx.Font((0,int(fontPixels*.6)), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - - hbs = wx.GridSizer( 2, 2, 4, 4 ) - for label, actionFn in [(_('DN&F'),DoDNF), (_('DN&S'),DoDNS), (_('&Pull'),DoPull), (_('D&Q'),DoDQ)]: - btn = MakeKeypadButton( self, label=label, style=wx.EXPAND|wx.GROW, font = font) - btn.Bind( wx.EVT_BUTTON, lambda event, fn = actionFn: self.doAction(fn) ) - hbs.Add( btn, flag=wx.EXPAND ) - - vsizer.Add( hbs, flag=wx.EXPAND|wx.TOP, border=4 ) - - self.touchBitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'touch24.png'), wx.BITMAP_TYPE_PNG ) - self.touchButton = wx.BitmapButton( self, bitmap = self.touchBitmap ) - self.touchButton.Bind( wx.EVT_BUTTON, self.onToggleTouchScreen) - self.touchButton.SetToolTip(wx.ToolTip(_("Touch Screen Toggle"))) - - vsizer.Add( self.touchButton, flag=wx.TOP|wx.ALIGN_CENTRE, border=12 ) - self.SetSizer( vsizer ) - - def onToggleTouchScreen( self, event ): - self.showTouchScreen ^= True - self.keypadPanel.Show( self.showTouchScreen ) - self.GetSizer().Layout() - self.GetParent().GetParent().GetParent().SetSashPosition( SplitterMaxPos if self.showTouchScreen else SplitterMinPos ) - try: - self.GetParent().GetSizer().Layout() - except Exception: - pass - - def onNumPress( self, event, value ): - self.numEdit.SetInsertionPointEnd() - txt = self.numEdit.GetValue() - txt += '{}'.format(value) - self.numEdit.SetValue( txt ) - self.numEdit.SetInsertionPointEnd() - - def onDelPress( self, event ): - txt = self.numEdit.GetValue() - if txt is not None: - self.numEdit.SetValue( txt[:-1] ) - - def handleNumKeypress(self, event): - keycode = event.GetKeyCode() - if keycode in enterCodes: - self.onEnterPress() - elif keycode in clearCodes: - self.numEdit.SetValue( '' ) - elif keycode in actionCodes: - if keycode == ord('/'): # DNF - pass - elif keycode == ord('*'): # DNS - pass - elif keycode == ord('-'): # PUL - pass - elif keycode == ord('+'): # DQ - pass - elif keycode < 255: - if keycode in validKeyCodes: - event.Skip() - else: - Utils.writeLog( 'handleNumKeypress: ignoring keycode < 255: {}'.format(keycode) ) - else: - Utils.writeLog( 'handleNumKeypress: ignoring keycode: >= 255 {}'.format(keycode) ) - event.Skip() - - def onEnterPress( self, event = None ): - nums = getRiderNumsFromText( self.numEdit.GetValue() ) - if nums: - mainWin = Utils.getMainWin() - if mainWin is not None: - mainWin.forecastHistory.logNum( nums ) - self.controller.refreshLaps() - wx.CallAfter( self.numEdit.SetValue, '' ) - - def doAction( self, action ): - race = Model.race - t = race.curRaceTime() if race and race.isRunning() else None - success = False - for num in getRiderNumsFromText( self.numEdit.GetValue() ): - if action(self, num, t): - success = True - if success: - self.numEdit.SetValue( '' ) - wx.CallAfter( Utils.refreshForecastHistory ) - - def Enable( self, enable ): - wx.Panel.Enable( self, enable ) - -def getLapInfo( lap, lapsTotal, tCur, tNext, leader ): - race = Model.race - if not race or not race.startTime: - return - info = [] - startTime = race.startTime - - if lap > lapsTotal: - info.append( (_("Last Rider"), (startTime + datetime.timedelta(seconds=tNext)).strftime('%H:%M:%S')) ) - return info - - tLap = tNext - tCur - info.append( (_("Lap"), '{}/{} ({} {})'.format(lap,lapsTotal,lapsTotal-lap, _('to go'))) ) - info.append( (_("Time"), Utils.formatTimeGap(tLap, highPrecision=False)) ) - info.append( (_("Start"), (startTime + datetime.timedelta(seconds=tCur)).strftime('%H:%M:%S')) ) - info.append( (_("End"), (startTime + datetime.timedelta(seconds=tNext)).strftime('%H:%M:%S')) ) - lapDistance = None - try: - bib = int(leader.split()[-1]) - category = race.getCategory( bib ) - lapDistance = category.getLapDistance( lap ) - except Exception: - pass - if lapDistance is not None: - sLap = (lapDistance / tLap) * 60.0*60.0 - info.append( ('', '{:.02f} {}'.format(sLap, 'km/h')) ) - return info - -def getCategoryStats(): - race = Model.race - if not race: - return [] - - isRunning = race.isRunning() - isTimeTrial = race.isTimeTrial - lastRaceTime = race.lastRaceTime() - Finisher = Model.Rider.Finisher - DNS = Model.Rider.DNS - NP = Model.Rider.NP - - statusSortSeq = Model.Rider.statusSortSeq - statusNames = Model.Rider.statusNames - - finishedAll, onCourseAll, statsAll = 0, 0, defaultdict( int ) - - def getStatsStr( finished, onCourse, stats ): - total = finished + onCourse + sum( stats.values() ) - if total: - b = [f'{_("Starters")}({total})'] - if finished: - b.append( f'{_("Finished")}({finished})' ) - b.extend( f'{statusNames[k]}({v})' for k,v in sorted(stats.items(), key = lambda x: statusSortSeq[x[0]]) ) - return f'{_("OnCourse")}({onCourse}) = {" - ".join(b)}' - else: - return '' - - categoryStats = [(_('All'), '')] - for category in race.getCategories(): - finished, onCourse, stats = 0, 0, defaultdict( int ) - for rr in GetResults( category ): - status = rr.status - if status == DNS: - continue - - rider = race.riders[rr.num] - firstTime = rider.firstTime or 0.0 - if isTimeTrial: - if status == NP and lastRaceTime >= firstTime: - status = Finisher # Consider started riders as Finishers, not NP. - else: - if status == Finisher: - status = rider.status # Set status back to the original status (will set back to Pulled). - - if status == Finisher: - if rr.raceTimes: - lastTime, interp = rr.raceTimes[-1], rr.interp[-1] - if isTimeTrial: - # Adjust to the time trial start time. - lastTime += firstTime - else: - lastTime, interp = 0.0, True - - if lastTime <= lastRaceTime and (not interp if isRunning else True): - finished += 1 - else: - onCourse += 1 - else: - stats[status] += 1 - - statsStr = getStatsStr(finished, onCourse, stats) - if statsStr: - categoryStats.append( (f'{category.fullname}', statsStr) ) - - finishedAll += finished - onCourseAll += onCourse - for k, v in stats.items(): - statsAll[k] += v - - categoryStats[0] = ( _('All'), getStatsStr(finishedAll, onCourseAll, statsAll) ) - return categoryStats - -class NumKeypad( wx.Panel ): - def __init__( self, parent, id = wx.ID_ANY ): - super().__init__(parent, id) - - self.bell = None - self.lapReminder = {} - - self.SetBackgroundColour( wx.WHITE ) - - self.refreshInputUpdateNonBusy = NonBusyCall( self.refreshInputUpdate, min_millis=1000, max_millis=3000 ) - - fontPixels = 50 - font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - - verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) - horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) - - splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) - splitter.Bind( wx.EVT_PAINT, self.onPaint ) - - panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) - panel.SetSizer( horizontalMainSizer ) - panel.SetBackgroundColour( wx.WHITE ) - - #------------------------------------------------------------------------------- - # Create the edit field, numeric keypad and buttons. - self.notebook = wx.Notebook( panel, style=wx.NB_BOTTOM ) - self.notebook.SetBackgroundColour( wx.WHITE ) - - self.keypad = Keypad( self.notebook, self ) - self.timeTrialRecord = TimeTrialRecord( self.notebook, self ) - self.bibTimeRecord = BibTimeRecord( self.notebook, self ) - - self.notebook.AddPage( self.keypad, _("Bib"), select=True ) - self.notebook.AddPage( self.timeTrialRecord, _("TimeTrial") ) - self.notebook.AddPage( self.bibTimeRecord, _("BibTime") ) - self.notebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onPageChanged ) - horizontalMainSizer.Add( self.notebook, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) - - self.horizontalMainSizer = horizontalMainSizer - - #------------------------------------------------------------------------------ - # Race time. - # - self.raceTime = wx.StaticText( panel, label = '00:00', size=(-1,64)) - self.raceTime.SetFont( font ) - self.raceTime.SetDoubleBuffered( True ) - - verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) - horizontalMainSizer.Add( verticalSubSizer ) - - hs = wx.BoxSizer( wx.HORIZONTAL ) - hs.Add( self.raceTime, flag=wx.LEFT, border=100-40-8 ) - verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALL, border = 2 ) - - #------------------------------------------------------------------------------ - # Lap Management. - # - gbs = wx.GridBagSizer(4, 12) - gbs.SetMinSize( 256, 200 ) - - fontSize = 12 - font = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) - fontBold = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) - - rowCur = 0 - colCur = 0 - - panel.SetMinSize( (256, 60) ) - - line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) - gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND ) - rowCur += 1 - - label = wx.StaticText( panel, label = _("Manual Start") ) - label.SetFont( font ) - gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - - self.raceStartMessage = label - self.raceStartTime = wx.StaticText( panel, label='00:00:00.000' ) - self.raceStartTime.SetFont( font ) - gbs.Add( self.raceStartTime, pos=(rowCur, colCur), span=(1, 1), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - - line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) - gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=2 ) - rowCur += 1 - - label = wx.StaticText( panel, label = '{}:'.format(_("Last Rider")) ) - label.SetFont( font ) - gbs.Add( label, pos=(rowCur, colCur), span=(1,2), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - self.lastRiderOnCourseTime = wx.StaticText( panel, label='00:00:00' ) - self.lastRiderOnCourseTime.SetFont( font ) - gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - self.lastRiderOnCourseName = wx.StaticText( panel ) - self.lastRiderOnCourseName.SetFont( fontBold ) - gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - self.lastRiderOnCourseTeam = wx.StaticText( panel ) - self.lastRiderOnCourseTeam.SetFont( font ) - gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - self.lastRiderOnCourseCategory = wx.StaticText( panel ) - self.lastRiderOnCourseCategory.SetFont( font ) - gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) - rowCur += 1 - - line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) - gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=2 ) - rowCur += 1 - - self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) - - self.photoCount = wx.StaticText( panel, label = "000000", size=(64,-1) ) - self.photoCount.SetFont( font ) - self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 6 ) - - self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) - self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) - - self.photoBitmap = wx.StaticBitmap( panel, bitmap = self.camera_bitmap ) - self.hbClockPhoto.Add( self.photoBitmap, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) - - gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1) ) - rowCur += 1 - - self.clock = ClockDigital( panel, size=(100,24), checkFunc=self.doClockUpdate ) - self.clock.SetBackgroundColour( wx.WHITE ) - gbs.Add( self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE ) - rowCur += 1 - - verticalSubSizer.Add( gbs, 0, flag=wx.LEFT|wx.TOP|wx.EXPAND, border = 3 ) - self.sizerLapInfo = gbs - self.sizerSubVertical = verticalSubSizer - - #------------------------------------------------------------------------------ - # Rider Lap Count. - rcVertical = wx.BoxSizer( wx.VERTICAL ) - self.lapsToGoCountGraph = LapsToGoCountGraph( panel ) - rcVertical.Add( self.lapsToGoCountGraph, 1, flag=wx.EXPAND|wx.TOP|wx.RIGHT, border = 4 ) - - horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) - self.horizontalMainSizer = horizontalMainSizer - - #---------------------------------------------------------------------------------------------- - self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, - lapInfoFunc=getLapInfo, - leftClickFunc=self.doLeftClickHUD, - rightClickFunc=self.doLeftClickHUD, - ) - - splitter.SetMinimumPaneSize( 20 ) - splitter.SplitHorizontally( panel, self.raceHUD, -100 ) - verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) - - self.SetSizer( verticalMainSizer ) - self.isEnabled = True - - self.splitter = splitter - self.firstTimeDraw = True - - self.refreshRaceTime() - - def doLeftClickHUD( self, iWave ): - race = Model.race - if not race: - return - try: - category = race.getCategories()[iWave] - except IndexError: - return - - with SetLaps( self, category=category ) as setLaps: - setLaps.ShowModal() - - def doClockUpdate( self ): - mainWin = Utils.getMainWin() - return not mainWin or mainWin.isShowingPage(self) - - def isKeypadInputMode( self ): - return self.notebook.GetSelection() == 0 - - def isTimeTrialInputMode( self ): - return self.notebook.GetSelection() == 1 - - def isBibTimeInputMode( self ): - return self.notebook.GetSelection() == 2 - - def setTimeTrialInput( self, isTimeTrial=True ): - page = 1 if isTimeTrial else 0 - if self.notebook.GetSelection() != page: - self.notebook.SetSelection( page ) - self.timeTrialRecord.refresh() - - def onPageChanged( self, event ): - if self.isBibTimeInputMode(): - self.bibTimeRecord.refresh() - - def swapKeypadTimeTrialRecord( self ): - self.notebook.SetSelection( 1 - self.notebook.GetSelection() ) - - def refreshRaceHUD( self ): - race = Model.race - if not race or race.isTimeTrial: - self.raceHUD.SetData() - if Utils.mainWin: - Utils.mainWin.updateLapCounter() - return - - categories = race.getCategories( startWaveOnly=True ) - noLap = '' - tCur = race.curRaceTime() if race.isRunning() else None - - def getNoDataCategoryLap( category ): - offset = race.categoryStartOffset(category) - tLapStart = offset if tCur and tCur >= offset else None - cn = race.getNumLapsFromCategory( category ) - if cn and tCur and tCur > offset + 30.0: - cn -= 1 - return ('{}'.format(cn) if cn else noLap, False, tLapStart) - - lapCounter = [getNoDataCategoryLap(category) for category in categories] - categoryToLapCounterIndex = {category:i for i, category in enumerate(categories)} - - if tCur is None or not categories: - self.raceHUD.SetData() - if Utils.mainWin: - Utils.mainWin.updateLapCounter(lapCounter) - return - - Finisher = Model.Rider.Finisher - leaderCategory = categories[0] - - secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 - - def setLapCounter( leaderCategory, category, lapCur, lapMax, tLeaderArrival=sys.float_info.max, tLapStart=None ): - if not category: - return - if not (category == leaderCategory or race.getNumLapsFromCategory(category)): - return - - lapsToGo = max( 0, lapMax - lapCur ) - if secondsBeforeLeaderToFlipLapCounter < tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter+5.0: - v = ('{}'.format(lapsToGo), True, tLapStart) # Flash current lap (about to be flipped). - elif 0.0 <= tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter: - v = ('{}'.format(max(0,lapsToGo-1)), False, tLapStart) # Flip lap counter before leader. - else: - v = ('{}'.format(lapsToGo), False, tLapStart) # Show current lap. - - try: - lapCounter[categoryToLapCounterIndex[category]] = v - except (KeyError, IndexError): - pass - - leader, raceTimes, earlyBellTime = [], [], [] - for category in categories: - results = GetResultsWithData( category ) - if not results or not results[0].status == Finisher or not results[0].raceTimes: - leader.append( category.fullname ) - raceTimes.append( [] ) - continue - - earlyBellTime.append( category.earlyBellTime ) - leader.append( '{} [{}]'.format(category.fullname, results[0].num) ) - for rank, rr in enumerate(results, 1): - if rr.status != Finisher or not rr.raceTimes or len(rr.raceTimes) < 2: - break - - if rank > 1: - catRaceTimes = raceTimes[-1] - if rank <= 10: - # Update the fastest lap times, which may not be the current leader's time. - for i, t in enumerate(rr.raceTimes): - if t < catRaceTimes[i]: - catRaceTimes[i] = t - - # Update the last rider finish time. - if rr.raceTimes[-1] > catRaceTimes[-1]: - catRaceTimes[-1] = rr.raceTimes[-1] - continue - - # Add a copy of the race times. Set the leader's last time as the current last rider finish. - raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) - - # Find the next expected lap arrival. - try: - lapCur = bisect.bisect_left( rr.raceTimes, tCur ) - # Time before leader's arrival. - tLeaderArrival = rr.raceTimes[lapCur] - tCur - except IndexError: - # At the end of the race, use the leader's race time. - # Make sure it is a recorded time, not a projected time. - try: - tLapStart = rr.raceTimes[-2] if rr.interp[-1] else rr.raceTimes[-1] - except Exception: - tLapStart = None - - setLapCounter( - leaderCategory, category, len(rr.raceTimes)-1, len(rr.raceTimes)-1, - tLapStart = tLapStart - ) - continue - - if lapCur <= 1: - tLapStart = race.categoryStartOffset(category) - else: - lapPrev = lapCur-1 - # Make sure we use an actual recorded time - not a projected time. - # A projected time is possible if the leader has a slow lap. - if rr.interp[lapPrev]: - lapPrev -= 1 - try: - tLapStart = rr.raceTimes[lapPrev] if lapPrev else race.categoryStartOffset(category) - except IndexError: - tLapStart = None - - setLapCounter( leaderCategory, category, lapCur, len(rr.raceTimes), tLeaderArrival, tLapStart ) - - if tLeaderArrival is not None: - if 0.0 <= tLeaderArrival <= 3.0: - if category not in self.lapReminder: - self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) - elif category in self.lapReminder: - del self.lapReminder[category] - - # Ensure that the raceTime and leader are sorted the same as the Categories are defined. - self.raceHUD.SetData( raceTimes, leader, tCur if race.isRunning() else None, earlyBellTime ) - if Utils.mainWin: - Utils.mainWin.updateLapCounter( lapCounter ) - self.updateLayout() - - def updateLayout( self ): - self.sizerLapInfo.Layout() - self.sizerSubVertical.Layout() - self.horizontalMainSizer.Layout() - self.Layout() - - def refreshRaceTime( self ): - race = Model.race - - if race is not None: - tRace = race.lastRaceTime() - tStr = Utils.formatTime( tRace ) - if tStr.startswith('0'): - tStr = tStr[1:] - self.refreshRaceHUD() - if race.enableUSBCamera: - self.photoBitmap.Show( True ) - self.photoCount.SetLabel( '{}'.format(race.photoCount) ) - self.photoBitmap.SetBitmap( self.camera_broken_bitmap if Utils.cameraError else self.camera_bitmap ) - else: - self.photoBitmap.Show( False ) - self.photoCount.SetLabel( '' ) - else: - tStr = '' - tRace = None - self.photoBitmap.Show( False ) - self.photoCount.SetLabel( '' ) - self.raceTime.SetLabel( ' ' + tStr ) - - self.hbClockPhoto.Layout() - - mainWin = Utils.mainWin - if mainWin is not None: - try: - mainWin.refreshRaceAnimation() - except Exception: - pass - - raceMessage = { 0:_("Finishers Arriving"), 1:_("Ring Bell"), 2:_("Prepare Bell") } - - def refreshLaps( self ): - wx.CallAfter( self.refreshRaceHUD ) - - def refreshRiderCategoryStatsList( self ): - self.lapsToGoCountGraph.Refresh() - - def refreshLastRiderOnCourse( self ): - race = Model.race - lastRiderOnCourse = GetLastRider( None ) - changed = False - - if lastRiderOnCourse: - maxLength = 24 - rider = race.riders[lastRiderOnCourse.num] - short_name = lastRiderOnCourse.short_name(maxLength) - if short_name: - lastRiderOnCourseName = '{}: {}'.format(lastRiderOnCourse.num, lastRiderOnCourse.short_name()) - else: - lastRiderOnCourseName = '{}'.format(lastRiderOnCourse.num) - - lastRiderOnCourseTeam = '{}'.format( getattr(lastRiderOnCourse, 'Team', '') ) - if len(lastRiderOnCourseTeam) > maxLength: - lastRiderOnCourseTeam = lastRiderOnCourseTeam[:maxLength].strip() + '...' - - category = race.getCategory( lastRiderOnCourse.num ) - lastRiderOnCourseCategory = category.fullname - - t = (lastRiderOnCourse._lastTimeOrig or 0.0) + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) - tFinish = race.startTime + datetime.timedelta( seconds=t ) - lastRiderOnCourseTime = '{} {}'.format(_('Finishing'), tFinish.strftime('%H:%M:%S') ) - else: - lastRiderOnCourseName = '' - lastRiderOnCourseTeam = '' - lastRiderOnCourseCategory = '' - lastRiderOnCourseTime = '' - changed |= SetLabel( self.lastRiderOnCourseName, lastRiderOnCourseName ) - changed |= SetLabel( self.lastRiderOnCourseTeam, lastRiderOnCourseTeam ) - changed |= SetLabel( self.lastRiderOnCourseCategory, lastRiderOnCourseCategory ) - changed |= SetLabel( self.lastRiderOnCourseTime, lastRiderOnCourseTime ) - if changed: - self.updateLayout() - - def refreshAll( self ): - self.refreshRaceTime() - self.refreshLaps() - - def commit( self ): - pass - - def onPaint( self, event ): - if self.firstTimeDraw: - self.firstTimeDraw = False - self.splitter.SetSashPosition( SplitterMinPos ) - event.Skip() - - def refreshInputUpdate( self ): - self.refreshLaps() - self.refreshRiderCategoryStatsList() - self.refreshLastRiderOnCourse() - - def refresh( self ): - self.clock.Start() - - race = Model.race - enable = bool(race and race.isRunning()) - if self.isEnabled != enable: - self.isEnabled = enable - if not enable: - if self.isKeypadInputMode(): - self.keypad.numEdit.SetValue( '' ) - if self.isBibTimeInputMode(): - self.bibTimeRecord.refresh() - - self.photoCount.Show( bool(race and race.enableUSBCamera) ) - self.photoBitmap.Show( bool(race and race.enableUSBCamera) ) - - # Refresh the race start time. - changed = False - rst, rstSource = '', '' - if race and race.startTime: - st = race.startTime - if race.enableJChipIntegration and race.resetStartClockOnFirstTag: - if race.firstRecordedTime: - rstSource = _('Chip Start') - else: - rstSource = _('Waiting...') - else: - rstSource = _('Manual Start') - rst = '{:02d}:{:02d}:{:02d}.{:02d}'.format(st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) - changed |= SetLabel( self.raceStartMessage, rstSource ) - changed |= SetLabel( self.raceStartTime, rst ) - - self.refreshInputUpdateNonBusy() - - if self.isKeypadInputMode(): - wx.CallLater( 100, self.keypad.numEdit.SetFocus ) - elif self.isBibTimeInputMode(): - wx.CallLater( 100, self.bibTimeRecord.numEdit.SetFocus ) - -if __name__ == '__main__': - Utils.disable_stdout_buffering() - app = wx.App(False) - mainWin = wx.Frame(None,title="CrossMan", size=(1000,800)) - Model.setRace( Model.Race() ) - model = Model.getRace() - model._populate() - model.enableUSBCamera = True - numKeypad = NumKeypad(mainWin) - numKeypad.refresh() - mainWin.Show() - app.MainLoop() - - +import bisect +import datetime +import os +import sys +from typing import cast + +import wx + +import Model +import Utils +from BibTimeRecord import BibTimeRecord +from ClockDigital import ClockDigital +from GetResults import GetResultsWithData, GetLastRider +from LapsToGoCount import LapsToGoCountGraph +from Log import getLogger +from ManualTimeEntryPanel import TimeEntryController, ManualTimeEntryPanel +from NonBusyCall import NonBusyCall +from Keypad import Keypad, getLapInfo, SplitterMinPos +from RaceHUD import RaceHUD +from SetLaps import SetLaps +from TimeTrialRecord import TimeTrialRecord +from Utils import SetLabel + + +class Record(wx.Panel, TimeEntryController): + def __init__( self, parent, id = wx.ID_ANY ): + super().__init__(parent, id) + + self.bell = None + self.lapReminder = {} + + self.SetBackgroundColour( wx.WHITE ) + + self.refreshInputUpdateNonBusy = NonBusyCall( self.refreshInputUpdate, min_millis=1000, max_millis=3000 ) + + fontPixels = 50 + font = wx.Font((0,fontPixels), wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + + verticalMainSizer = wx.BoxSizer( wx.VERTICAL ) + horizontalMainSizer = wx.BoxSizer( wx.HORIZONTAL ) + + splitter = wx.SplitterWindow( self, wx.ID_ANY, style = wx.SP_3DSASH ) + splitter.Bind( wx.EVT_PAINT, self.onPaint ) + + panel = wx.Panel( splitter, style=wx.BORDER_SUNKEN ) + panel.SetSizer( horizontalMainSizer ) + panel.SetBackgroundColour( wx.WHITE ) + + #------------------------------------------------------------------------------- + # Create the edit field, numeric keypad and buttons. + self.notebook = wx.Notebook( panel, style=wx.NB_BOTTOM ) + self.notebook.SetBackgroundColour( wx.WHITE ) + + self.keypad = Keypad( self.notebook, self ) + self.timeTrialRecord = TimeTrialRecord( self.notebook ) + self.bibTimeRecord = BibTimeRecord( self.notebook, controller=cast(ManualTimeEntryPanel, self) ) + + self.notebook.AddPage( self.keypad, _("Bib"), select=True ) + self.notebook.AddPage( self.timeTrialRecord, _("TimeTrial") ) + self.notebook.AddPage( self.bibTimeRecord, _("BibTime") ) + self.notebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onPageChanged ) + horizontalMainSizer.Add( self.notebook, 0, flag=wx.TOP|wx.LEFT|wx.EXPAND, border = 4 ) + + self.horizontalMainSizer = horizontalMainSizer + + #------------------------------------------------------------------------------ + # Race time. + # + self.raceTime = wx.StaticText( panel, label = '00:00', size=(-1,64)) + self.raceTime.SetFont( font ) + self.raceTime.SetDoubleBuffered( True ) + + verticalSubSizer = wx.BoxSizer( wx.VERTICAL ) + horizontalMainSizer.Add( verticalSubSizer ) + + hs = wx.BoxSizer( wx.HORIZONTAL ) + hs.Add( self.raceTime, flag=wx.LEFT, border=100-40-8 ) + verticalSubSizer.Add( hs, flag=wx.ALIGN_LEFT | wx.ALL, border = 2 ) + + #------------------------------------------------------------------------------ + # Lap Management. + # + gbs = wx.GridBagSizer(4, 12) + gbs.SetMinSize( 256, 200 ) + + fontSize = 12 + font = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL) + fontBold = wx.Font(fontSize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) + + rowCur = 0 + colCur = 0 + + panel.SetMinSize( (256, 60) ) + + line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) + gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND ) + rowCur += 1 + + label = wx.StaticText( panel, label = _("Manual Start") ) + label.SetFont( font ) + gbs.Add( label, pos=(rowCur, colCur), span=(1,1), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + + self.raceStartMessage = label + self.raceStartTime = wx.StaticText( panel, label='00:00:00.000' ) + self.raceStartTime.SetFont( font ) + gbs.Add( self.raceStartTime, pos=(rowCur, colCur), span=(1, 1), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + + line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) + gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=2 ) + rowCur += 1 + + label = wx.StaticText( panel, label = '{}:'.format(_("Last Rider")) ) + label.SetFont( font ) + gbs.Add( label, pos=(rowCur, colCur), span=(1,2), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + self.lastRiderOnCourseTime = wx.StaticText( panel, label='00:00:00' ) + self.lastRiderOnCourseTime.SetFont( font ) + gbs.Add( self.lastRiderOnCourseTime, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + self.lastRiderOnCourseName = wx.StaticText( panel ) + self.lastRiderOnCourseName.SetFont( fontBold ) + gbs.Add( self.lastRiderOnCourseName, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + self.lastRiderOnCourseTeam = wx.StaticText( panel ) + self.lastRiderOnCourseTeam.SetFont( font ) + gbs.Add( self.lastRiderOnCourseTeam, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + self.lastRiderOnCourseCategory = wx.StaticText( panel ) + self.lastRiderOnCourseCategory.SetFont( font ) + gbs.Add( self.lastRiderOnCourseCategory, pos=(rowCur, colCur), span=(1, 2), flag=wx.EXPAND|wx.ALL, border=3 ) + rowCur += 1 + + line = wx.StaticLine( panel, style=wx.LI_HORIZONTAL ) + gbs.Add( line, pos=(rowCur, 0), span=(1,2), flag=wx.EXPAND|wx.ALL, border=2 ) + rowCur += 1 + + self.hbClockPhoto = wx.BoxSizer( wx.HORIZONTAL ) + + self.photoCount = wx.StaticText( panel, label = "000000", size=(64,-1) ) + self.photoCount.SetFont( font ) + self.hbClockPhoto.Add( self.photoCount, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 6 ) + + self.camera_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera.png'), wx.BITMAP_TYPE_PNG ) + self.camera_broken_bitmap = wx.Bitmap( os.path.join(Utils.getImageFolder(), 'camera_broken.png'), wx.BITMAP_TYPE_PNG ) + + self.photoBitmap = wx.StaticBitmap( panel, bitmap = self.camera_bitmap ) + self.hbClockPhoto.Add( self.photoBitmap, flag=wx.ALIGN_CENTRE_VERTICAL|wx.RIGHT, border = 18 ) + + gbs.Add( self.hbClockPhoto, pos=(rowCur, colCur), span=(1,1) ) + rowCur += 1 + + self.clock = ClockDigital( panel, size=(100,24), checkFunc=self.doClockUpdate ) + self.clock.SetBackgroundColour( wx.WHITE ) + gbs.Add( self.clock, pos=(rowCur, 0), span=(1, 2), flag=wx.ALIGN_CENTRE ) + rowCur += 1 + + verticalSubSizer.Add( gbs, 0, flag=wx.LEFT|wx.TOP|wx.EXPAND, border = 3 ) + self.sizerLapInfo = gbs + self.sizerSubVertical = verticalSubSizer + + #------------------------------------------------------------------------------ + # Rider Lap Count. + rcVertical = wx.BoxSizer( wx.VERTICAL ) + self.lapsToGoCountGraph = LapsToGoCountGraph( panel ) + rcVertical.Add( self.lapsToGoCountGraph, 1, flag=wx.EXPAND|wx.TOP|wx.RIGHT, border = 4 ) + + horizontalMainSizer.Add( rcVertical, 1, flag=wx.EXPAND|wx.LEFT, border = 4 ) + self.horizontalMainSizer = horizontalMainSizer + + #---------------------------------------------------------------------------------------------- + self.raceHUD = RaceHUD( splitter, wx.ID_ANY, style=wx.BORDER_SUNKEN, + lapInfoFunc=getLapInfo, + leftClickFunc=self.doLeftClickHUD, + rightClickFunc=self.doLeftClickHUD, + ) + + splitter.SetMinimumPaneSize( 20 ) + splitter.SplitHorizontally( panel, self.raceHUD, -100 ) + verticalMainSizer.Add( splitter, 1, flag=wx.EXPAND ) + + self.SetSizer( verticalMainSizer ) + self.isEnabled = True + + self.splitter = splitter + self.firstTimeDraw = True + + self.refreshRaceTime() + + def doLeftClickHUD( self, iWave ): + race = Model.race + if not race: + return + try: + category = race.getCategories()[iWave] + except IndexError: + return + + with SetLaps( self, category=category ) as setLaps: + setLaps.ShowModal() + + def doClockUpdate( self ): + mainWin = Utils.getMainWin() + return not mainWin or mainWin.isShowingPage(self) + + def isKeypadInputMode( self ): + return self.notebook.GetSelection() == 0 + + def isTimeTrialInputMode( self ): + return self.notebook.GetSelection() == 1 + + def isBibTimeInputMode( self ): + return self.notebook.GetSelection() == 2 + + def setTimeTrialInput( self, isTimeTrial=True ): + page = 1 if isTimeTrial else 0 + if self.notebook.GetSelection() != page: + self.notebook.SetSelection( page ) + self.timeTrialRecord.refresh() + + def onPageChanged( self, event ): + if self.isBibTimeInputMode(): + self.bibTimeRecord.refresh() + + def swapKeypadTimeTrialRecord( self ): + self.notebook.SetSelection( 1 - self.notebook.GetSelection() ) + + def refreshRaceHUD( self ): + race = Model.race + if not race or race.isTimeTrial: + self.raceHUD.SetData() + if Utils.mainWin: + Utils.mainWin.updateLapCounter() + return + + categories = race.getCategories( startWaveOnly=True ) + noLap = '' + tCur = race.curRaceTime() if race.isRunning() else None + + def getNoDataCategoryLap( category ): + offset = race.categoryStartOffset(category) + tLapStart = offset if tCur and tCur >= offset else None + cn = race.getNumLapsFromCategory( category ) + if cn and tCur and tCur > offset + 30.0: + cn -= 1 + return ('{}'.format(cn) if cn else noLap, False, tLapStart) + + lapCounter = [getNoDataCategoryLap(category) for category in categories] + categoryToLapCounterIndex = {category:i for i, category in enumerate(categories)} + + if tCur is None or not categories: + self.raceHUD.SetData() + if Utils.mainWin: + Utils.mainWin.updateLapCounter(lapCounter) + return + + Finisher = Model.Rider.Finisher + leaderCategory = categories[0] + + secondsBeforeLeaderToFlipLapCounter = race.secondsBeforeLeaderToFlipLapCounter + 1.0 + + def setLapCounter( leaderCategory, category, lapCur, lapMax, tLeaderArrival=sys.float_info.max, tLapStart=None ): + if not category: + return + if not (category == leaderCategory or race.getNumLapsFromCategory(category)): + return + + lapsToGo = max( 0, lapMax - lapCur ) + if secondsBeforeLeaderToFlipLapCounter < tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter+5.0: + v = ('{}'.format(lapsToGo), True, tLapStart) # Flash current lap (about to be flipped). + elif 0.0 <= tLeaderArrival <= secondsBeforeLeaderToFlipLapCounter: + v = ('{}'.format(max(0,lapsToGo-1)), False, tLapStart) # Flip lap counter before leader. + else: + v = ('{}'.format(lapsToGo), False, tLapStart) # Show current lap. + + try: + lapCounter[categoryToLapCounterIndex[category]] = v + except (KeyError, IndexError): + pass + + leader, raceTimes, earlyBellTime = [], [], [] + for category in categories: + results = GetResultsWithData( category ) + if not results or not results[0].status == Finisher or not results[0].raceTimes: + leader.append( category.fullname ) + raceTimes.append( [] ) + continue + + earlyBellTime.append( category.earlyBellTime ) + leader.append( '{} [{}]'.format(category.fullname, results[0].num) ) + for rank, rr in enumerate(results, 1): + if rr.status != Finisher or not rr.raceTimes or len(rr.raceTimes) < 2: + break + + if rank > 1: + catRaceTimes = raceTimes[-1] + if rank <= 10: + # Update the fastest lap times, which may not be the current leader's time. + for i, t in enumerate(rr.raceTimes): + if t < catRaceTimes[i]: + catRaceTimes[i] = t + + # Update the last rider finish time. + if rr.raceTimes[-1] > catRaceTimes[-1]: + catRaceTimes[-1] = rr.raceTimes[-1] + continue + + # Add a copy of the race times. Set the leader's last time as the current last rider finish. + raceTimes.append( rr.raceTimes + [rr.raceTimes[-1]] ) + + # Find the next expected lap arrival. + try: + lapCur = bisect.bisect_left( rr.raceTimes, tCur ) + # Time before leader's arrival. + tLeaderArrival = rr.raceTimes[lapCur] - tCur + except IndexError: + # At the end of the race, use the leader's race time. + # Make sure it is a recorded time, not a projected time. + try: + tLapStart = rr.raceTimes[-2] if rr.interp[-1] else rr.raceTimes[-1] + except Exception: + tLapStart = None + + setLapCounter( + leaderCategory, category, len(rr.raceTimes)-1, len(rr.raceTimes)-1, + tLapStart = tLapStart + ) + continue + + if lapCur <= 1: + tLapStart = race.categoryStartOffset(category) + else: + lapPrev = lapCur-1 + # Make sure we use an actual recorded time - not a projected time. + # A projected time is possible if the leader has a slow lap. + if rr.interp[lapPrev]: + lapPrev -= 1 + try: + tLapStart = rr.raceTimes[lapPrev] if lapPrev else race.categoryStartOffset(category) + except IndexError: + tLapStart = None + + setLapCounter( leaderCategory, category, lapCur, len(rr.raceTimes), tLeaderArrival, tLapStart ) + + if tLeaderArrival is not None: + if 0.0 <= tLeaderArrival <= 3.0: + if category not in self.lapReminder: + self.lapReminder[category] = Utils.PlaySound( 'reminder.wav' ) + elif category in self.lapReminder: + del self.lapReminder[category] + + # Ensure that the raceTime and leader are sorted the same as the Categories are defined. + self.raceHUD.SetData( raceTimes, leader, tCur if race.isRunning() else None, earlyBellTime ) + if Utils.mainWin: + Utils.mainWin.updateLapCounter( lapCounter ) + self.updateLayout() + + def updateLayout( self ): + self.sizerLapInfo.Layout() + self.sizerSubVertical.Layout() + self.horizontalMainSizer.Layout() + self.Layout() + + def refreshRaceTime( self ): + race = Model.race + + if race is not None: + tRace = race.lastRaceTime() + tStr = Utils.formatTime( tRace ) + if tStr.startswith('0'): + tStr = tStr[1:] + self.refreshRaceHUD() + if race.enableUSBCamera: + self.photoBitmap.Show( True ) + self.photoCount.SetLabel( '{}'.format(race.photoCount) ) + self.photoBitmap.SetBitmap( self.camera_broken_bitmap if Utils.cameraError else self.camera_bitmap ) + else: + self.photoBitmap.Show( False ) + self.photoCount.SetLabel( '' ) + else: + tStr = '' + tRace = None + self.photoBitmap.Show( False ) + self.photoCount.SetLabel( '' ) + self.raceTime.SetLabel( ' ' + tStr ) + + self.hbClockPhoto.Layout() + + mainWin = Utils.mainWin + if mainWin is not None: + try: + mainWin.refreshRaceAnimation() + except Exception: + pass + + raceMessage = { 0:_("Finishers Arriving"), 1:_("Ring Bell"), 2:_("Prepare Bell") } + + def refreshLaps( self ): + wx.CallAfter( self.refreshRaceHUD ) + + def refreshRiderCategoryStatsList( self ): + self.lapsToGoCountGraph.Refresh() + + def refreshLastRiderOnCourse( self ): + race = Model.race + lastRiderOnCourse = GetLastRider( None ) + changed = False + + if lastRiderOnCourse: + maxLength = 24 + rider = race.riders[lastRiderOnCourse.num] + short_name = lastRiderOnCourse.short_name(maxLength) + if short_name: + lastRiderOnCourseName = '{}: {}'.format(lastRiderOnCourse.num, lastRiderOnCourse.short_name()) + else: + lastRiderOnCourseName = '{}'.format(lastRiderOnCourse.num) + + lastRiderOnCourseTeam = '{}'.format( getattr(lastRiderOnCourse, 'Team', '') ) + if len(lastRiderOnCourseTeam) > maxLength: + lastRiderOnCourseTeam = lastRiderOnCourseTeam[:maxLength].strip() + '...' + + category = race.getCategory( lastRiderOnCourse.num ) + lastRiderOnCourseCategory = category.fullname + + t = (lastRiderOnCourse._lastTimeOrig or 0.0) + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) + tFinish = race.startTime + datetime.timedelta( seconds=t ) + lastRiderOnCourseTime = '{} {}'.format(_('Finishing'), tFinish.strftime('%H:%M:%S') ) + else: + lastRiderOnCourseName = '' + lastRiderOnCourseTeam = '' + lastRiderOnCourseCategory = '' + lastRiderOnCourseTime = '' + changed |= SetLabel( self.lastRiderOnCourseName, lastRiderOnCourseName ) + changed |= SetLabel( self.lastRiderOnCourseTeam, lastRiderOnCourseTeam ) + changed |= SetLabel( self.lastRiderOnCourseCategory, lastRiderOnCourseCategory ) + changed |= SetLabel( self.lastRiderOnCourseTime, lastRiderOnCourseTime ) + if changed: + self.updateLayout() + + def refreshAll( self ): + self.refreshRaceTime() + self.refreshLaps() + + def commit( self ): + pass + + def onPaint( self, event ): + if self.firstTimeDraw: + self.firstTimeDraw = False + self.splitter.SetSashPosition( SplitterMinPos ) + event.Skip() + + def refreshInputUpdate( self ): + self.refreshLaps() + self.refreshRiderCategoryStatsList() + self.refreshLastRiderOnCourse() + + def refresh( self ): + self.clock.Start() + + race = Model.race + if race is None: + enable = False + disableReason = 'No race is open.' + elif not race.isRunning(): + enable = False + disableReason = 'Race is not running' + else: + enable = True + disableReason = None + + if self.isEnabled != enable: + self.isEnabled = enable + + self.keypad.Disable(not enable, reason=disableReason) + self.timeTrialRecord.Disable(not enable, reason=disableReason) + self.bibTimeRecord.Disable(not enable, reason=disableReason) + + if not enable and self.isKeypadInputMode(): + self.keypad.numEdit.SetValue( '' ) + + if self.isBibTimeInputMode(): + self.bibTimeRecord.refresh() + + self.photoCount.Show( bool(race and race.enableUSBCamera) ) + self.photoBitmap.Show( bool(race and race.enableUSBCamera) ) + + # Refresh the race start time. + changed = False + rst, rstSource = '', '' + if race and race.startTime: + st = race.startTime + if race.enableJChipIntegration and race.resetStartClockOnFirstTag: + if race.firstRecordedTime: + rstSource = _('Chip Start') + else: + rstSource = _('Waiting...') + else: + rstSource = _('Manual Start') + rst = '{:02d}:{:02d}:{:02d}.{:02d}'.format(st.hour, st.minute, st.second, int(st.microsecond / 10000.0)) + changed |= SetLabel( self.raceStartMessage, rstSource ) + changed |= SetLabel( self.raceStartTime, rst ) + + self.refreshInputUpdateNonBusy() + + if self.isKeypadInputMode(): + if self.keypad.numEdit.IsEditable(): + getLogger('Record').debug('Setting focus to keypad edit field.') + wx.CallLater( 100, self.keypad.SetFocus ) + elif self.isBibTimeInputMode(): + if self.bibTimeRecord.numEdit.IsEditable(): + getLogger('Record').debug('Setting focus to bibTimeRecord edit field.') + wx.CallLater( 100, self.bibTimeRecord.SetFocus ) + + +if __name__ == '__main__': + Utils.disable_stdout_buffering() + app = wx.App(False) + mainWin = wx.Frame(None,title="CrossMan", size=(1000,800)) + Model.setRace( Model.Race() ) + model = Model.getRace() + model._populate() + model.enableUSBCamera = True + numKeypad = Record(mainWin) + numKeypad.refresh() + mainWin.Show() + app.MainLoop() diff --git a/TimeTrialRecord.py b/TimeTrialRecord.py index bbd02aa9e..0cf77e774 100644 --- a/TimeTrialRecord.py +++ b/TimeTrialRecord.py @@ -5,6 +5,7 @@ import Model import Utils +from ManualTimeEntryPanel import ManualTimeEntryPanel from ReorderableGrid import ReorderableGrid from HighPrecisionTimeEdit import HighPrecisionTimeEdit from PhotoFinish import TakePhoto @@ -61,12 +62,9 @@ def Reset( self ): def Clone( self ): return HighPrecisionTimeEditor() -class TimeTrialRecord( wx.Panel ): - def __init__( self, parent, controller, id = wx.ID_ANY ): +class TimeTrialRecord( ManualTimeEntryPanel ): + def __init__( self, parent: wx.Window, id = wx.ID_ANY ): super().__init__(parent, id) - self.SetBackgroundColour( wx.WHITE ) - - self.controller = controller self.headerNames = (' {} '.format(_('Time')), ' {} '.format(_('Bib'))) @@ -142,21 +140,20 @@ def __init__( self, parent, controller, id = wx.ID_ANY ): self.Bind(wx.EVT_MENU, self.doRecordTime, id=idRecordAcceleratorId) self.Bind(wx.EVT_MENU, self.doSave, id=idSaveAccelleratorId) self.Bind(wx.EVT_MENU, self.doCleanup, id=idCleanupAccelleratorId) - accel_tbl = wx.AcceleratorTable([ + self._accel_tbl = wx.AcceleratorTable([ (wx.ACCEL_NORMAL, ord('T'), idRecordAcceleratorId), (wx.ACCEL_NORMAL, ord('S'), idSaveAccelleratorId), (wx.ACCEL_NORMAL, ord('C'), idCleanupAccelleratorId), ]) - self.SetAcceleratorTable(accel_tbl) - + self.SetSizer(self.vbs) self.Fit() def doClickLabel( self, event ): if event.GetCol() == 0: self.doRecordTime( event ) - - def doRecordTime( self, event ): + + def doRecordTime( self, _event ): race = Model.race if not race: return @@ -203,7 +200,7 @@ def getTimesBibs( self ): return timesBibs, timesNoBibs - def doSave( self, event ): + def doSave( self, _event ): timesBibs, timesNoBibs = self.getTimesBibs() if timesBibs and Model.race: @@ -227,7 +224,7 @@ def doSave( self, event ): if timesNoBibs: self.grid.SetGridCursor( 0, 1, ) - def doCleanup( self, event ): + def doCleanup( self, _event ): timesBibs, timesNoBibs = self.getTimesBibs() with gridlib.GridUpdateLocker(self.grid): @@ -273,14 +270,29 @@ def refresh( self ): def commit( self ): pass - + + def _DisableControls(self): + self.recordTimeButton.Disable() + self.saveButton.Disable() + self.cleanupButton.Disable() + self.grid.Disable() + self.SetAcceleratorTable(wx.AcceleratorTable([])) + + def _EnableControls(self): + self.recordTimeButton.Enable() + self.saveButton.Enable() + self.cleanupButton.Enable() + self.grid.Enable() + self.SetAcceleratorTable(self._accel_tbl) + if __name__ == '__main__': Utils.disable_stdout_buffering() app = wx.App(False) mainWin = wx.Frame(None,title="CrossMan", size=(600,600)) Model.setRace( Model.Race() ) Model.getRace()._populate() - timeTrialRecord = TimeTrialRecord(mainWin, None) + timeTrialRecord = TimeTrialRecord(mainWin) + # timeTrialRecord.Disable(reason='For testing') timeTrialRecord.refresh() mainWin.Show() app.MainLoop()