class LogViewer(DebugViewer, wx.Panel): def __init__(self, parent, window): wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL | wx.SUNKEN_BORDER) DebugViewer.__init__(self, None, False, False) main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5) main_sizer.AddGrowableCol(0) main_sizer.AddGrowableRow(1) filter_sizer = wx.BoxSizer(wx.HORIZONTAL) main_sizer.AddSizer(filter_sizer, border=5, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW) self.MessageFilter = wx.ComboBox(self, style=wx.CB_READONLY) self.MessageFilter.Append(_("All")) levels = LogLevels[:3] levels.reverse() for level in levels: self.MessageFilter.Append(_(level)) self.Bind(wx.EVT_COMBOBOX, self.OnMessageFilterChanged, self.MessageFilter) filter_sizer.AddWindow(self.MessageFilter, 1, border=5, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL) self.SearchMessage = wx.SearchCtrl(self, style=wx.TE_PROCESS_ENTER) self.SearchMessage.ShowSearchButton(True) self.SearchMessage.ShowCancelButton(True) self.Bind(wx.EVT_TEXT_ENTER, self.OnSearchMessageChanged, self.SearchMessage) self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self.OnSearchMessageSearchButtonClick, self.SearchMessage) self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self.OnSearchMessageCancelButtonClick, self.SearchMessage) filter_sizer.AddWindow(self.SearchMessage, 3, border=5, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL) self.CleanButton = wx.lib.buttons.GenBitmapButton( self, bitmap=GetBitmap("Clean"), size=wx.Size(28, 28), style=wx.NO_BORDER) self.CleanButton.SetToolTipString(_("Clean log messages")) self.Bind(wx.EVT_BUTTON, self.OnCleanButton, self.CleanButton) filter_sizer.AddWindow(self.CleanButton) message_panel_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0) message_panel_sizer.AddGrowableCol(0) message_panel_sizer.AddGrowableRow(0) main_sizer.AddSizer(message_panel_sizer, border=5, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW) self.MessagePanel = wx.Panel(self) if wx.Platform == '__WXMSW__': self.Font = wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier New') else: self.Font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier') self.MessagePanel.Bind(wx.EVT_LEFT_UP, self.OnMessagePanelLeftUp) self.MessagePanel.Bind(wx.EVT_RIGHT_UP, self.OnMessagePanelRightUp) self.MessagePanel.Bind(wx.EVT_LEFT_DCLICK, self.OnMessagePanelLeftDCLick) self.MessagePanel.Bind(wx.EVT_MOTION, self.OnMessagePanelMotion) self.MessagePanel.Bind(wx.EVT_LEAVE_WINDOW, self.OnMessagePanelLeaveWindow) self.MessagePanel.Bind(wx.EVT_MOUSEWHEEL, self.OnMessagePanelMouseWheel) self.MessagePanel.Bind(wx.EVT_ERASE_BACKGROUND, self.OnMessagePanelEraseBackground) self.MessagePanel.Bind(wx.EVT_PAINT, self.OnMessagePanelPaint) self.MessagePanel.Bind(wx.EVT_SIZE, self.OnMessagePanelResize) message_panel_sizer.AddWindow(self.MessagePanel, flag=wx.GROW) self.MessageScrollBar = LogScrollBar(self, wx.Size(16, -1)) message_panel_sizer.AddWindow(self.MessageScrollBar, flag=wx.GROW) self.SetSizer(main_sizer) self.LeftButtons = [] for label, callback in [ ("+" + text, self.GenerateOnDurationButton(duration)) for text, duration in CHANGE_TIMESTAMP_BUTTONS ]: self.LeftButtons.append(LogButton(label, callback)) self.RightButtons = [] for label, callback in [ ("-" + text, self.GenerateOnDurationButton(-duration)) for text, duration in CHANGE_TIMESTAMP_BUTTONS ]: self.RightButtons.append(LogButton(label, callback)) self.MessageFilter.SetSelection(0) self.LogSource = None self.ResetLogMessages() self.ParentWindow = window self.LevelIcons = [GetBitmap("LOG_" + level) for level in LogLevels] self.LevelFilters = [range(i) for i in xrange(4, 0, -1)] self.CurrentFilter = self.LevelFilters[0] self.CurrentSearchValue = "" self.ScrollSpeed = 0. self.LastStartTime = None self.ScrollTimer = wx.Timer(self, -1) self.Bind(wx.EVT_TIMER, self.OnScrollTimer, self.ScrollTimer) self.LastMousePos = None self.MessageToolTip = None self.MessageToolTipTimer = wx.Timer(self, -1) self.Bind(wx.EVT_TIMER, self.OnMessageToolTipTimer, self.MessageToolTipTimer) def __del__(self): self.ScrollTimer.Stop() def ResetLogMessages(self): self.ResetLogCounters() self.OldestMessages = [] self.LogMessages = [] self.LogMessagesTimestamp = numpy.array([]) self.CurrentMessage = None self.HasNewData = False def SetLogSource(self, log_source): self.LogSource = proxy(log_source) if log_source else None self.CleanButton.Enable(self.LogSource is not None) if log_source is not None: self.ResetLogMessages() self.RefreshView() def GetLogMessageFromSource(self, msgidx, level): if self.LogSource is not None: answer = self.LogSource.GetLogMessage(level, msgidx) if answer is not None: msg, tick, tv_sec, tv_nsec = answer return LogMessage(tv_sec, tv_nsec, level, self.LevelIcons[level], msg) return None def ResetLogCounters(self): self.previous_log_count = [None] * LogLevelsCount def SetLogCounters(self, log_count): new_messages = [] for level, count, prev in zip(xrange(LogLevelsCount), log_count, self.previous_log_count): if count is not None and prev != count: if prev is None: dump_end = max(-1, count - 10) oldest_message = (-1, None) else: dump_end = prev - 1 for msgidx in xrange(count - 1, dump_end, -1): new_message = self.GetLogMessageFromSource(msgidx, level) if new_message is None: if prev is None: oldest_message = (-1, None) break if prev is None: oldest_message = (msgidx, new_message) if len(new_messages) == 0: new_messages = [new_message] else: new_messages.insert(0, new_message) else: new_messages.insert(0, new_message) if prev is None and len(self.OldestMessages) <= level: self.OldestMessages.append(oldest_message) self.previous_log_count[level] = count new_messages.sort() if len(new_messages) > 0: self.HasNewData = True if self.CurrentMessage is not None: current_is_last = self.GetNextMessage( self.CurrentMessage)[0] is None else: current_is_last = True for new_message in new_messages: self.LogMessages.append(new_message) self.LogMessagesTimestamp = numpy.append( self.LogMessagesTimestamp, [new_message.Timestamp]) if current_is_last: self.ScrollToLast(False) self.ResetMessageToolTip() self.MessageToolTipTimer.Stop() self.ParentWindow.SelectTab(self) self.NewDataAvailable(None) def FilterLogMessage(self, message, timestamp=None): return (message.Level in self.CurrentFilter and message.Message.find(self.CurrentSearchValue) != -1 and (timestamp is None or message.Timestamp < timestamp)) def GetMessageByTimestamp(self, timestamp): if self.CurrentMessage is not None: msgidx = numpy.argmin(abs(self.LogMessagesTimestamp - timestamp)) message = self.LogMessages[msgidx] if self.FilterLogMessage( message) and message.Timestamp > timestamp: return self.GetPreviousMessage(msgidx, timestamp) return message, msgidx return None, None def GetNextMessage(self, msgidx): while msgidx < len(self.LogMessages) - 1: message = self.LogMessages[msgidx + 1] if self.FilterLogMessage(message): return message, msgidx + 1 msgidx += 1 return None, None def GetPreviousMessage(self, msgidx, timestamp=None): message = None while 0 < msgidx < len(self.LogMessages): message = self.LogMessages[msgidx - 1] if self.FilterLogMessage(message, timestamp): return message, msgidx - 1 msgidx -= 1 if len(self.LogMessages) > 0: message = self.LogMessages[0] for idx, msg in self.OldestMessages: if msg is not None and msg > message: message = msg while message is not None: level = message.Level oldest_msgidx, oldest_message = self.OldestMessages[level] if oldest_msgidx > 0: message = self.GetLogMessageFromSource( oldest_msgidx - 1, level) if message is not None: self.OldestMessages[level] = (oldest_msgidx - 1, message) else: self.OldestMessages[level] = (-1, None) else: message = None self.OldestMessages[level] = (-1, None) if message is not None: message_idx = 0 while (message_idx < len(self.LogMessages) and self.LogMessages[message_idx] < message): message_idx += 1 if len(self.LogMessages) > 0: current_message = self.LogMessages[self.CurrentMessage] else: current_message = message self.LogMessages.insert(message_idx, message) self.LogMessagesTimestamp = numpy.insert( self.LogMessagesTimestamp, [message_idx], [message.Timestamp]) self.CurrentMessage = self.LogMessages.index( current_message) if message_idx == 0 and self.FilterLogMessage( message, timestamp): return message, 0 for idx, msg in self.OldestMessages: if msg is not None and (message is None or msg > message): message = msg return None, None def RefreshNewData(self, *args, **kwargs): if self.HasNewData: self.HasNewData = False self.RefreshView() DebugViewer.RefreshNewData(self, *args, **kwargs) def RefreshView(self): width, height = self.MessagePanel.GetClientSize() bitmap = wx.EmptyBitmap(width, height) dc = wx.BufferedDC(wx.ClientDC(self.MessagePanel), bitmap) dc.Clear() dc.BeginDrawing() if self.CurrentMessage is not None: dc.SetFont(self.Font) for button in self.LeftButtons + self.RightButtons: button.Draw(dc) message_idx = self.CurrentMessage message = self.LogMessages[message_idx] draw_date = True offset = 5 while offset < height and message is not None: message.Draw(dc, offset, width, draw_date) offset += message.GetHeight(draw_date) previous_message, message_idx = self.GetPreviousMessage( message_idx) if previous_message is not None: draw_date = message.Date != previous_message.Date message = previous_message dc.EndDrawing() self.MessageScrollBar.RefreshThumbPosition() def IsPLCLogEmpty(self): empty = True for level, prev in zip(xrange(LogLevelsCount), self.previous_log_count): if prev is not None: empty = False break return empty def IsMessagePanelTop(self, message_idx=None): if message_idx is None: message_idx = self.CurrentMessage if message_idx is not None: return self.GetNextMessage(message_idx)[0] is None return True def IsMessagePanelBottom(self, message_idx=None): if message_idx is None: message_idx = self.CurrentMessage if message_idx is not None: width, height = self.MessagePanel.GetClientSize() offset = 5 message = self.LogMessages[message_idx] draw_date = True while message is not None and offset < height: offset += message.GetHeight(draw_date) previous_message, message_idx = self.GetPreviousMessage( message_idx) if previous_message is not None: draw_date = message.Date != previous_message.Date message = previous_message return offset < height return True def ScrollMessagePanel(self, scroll): if self.CurrentMessage is not None: message = self.LogMessages[self.CurrentMessage] while scroll > 0 and message is not None: message, msgidx = self.GetNextMessage(self.CurrentMessage) if message is not None: self.CurrentMessage = msgidx scroll -= 1 while scroll < 0 and message is not None and not self.IsMessagePanelBottom( ): message, msgidx = self.GetPreviousMessage(self.CurrentMessage) if message is not None: self.CurrentMessage = msgidx scroll += 1 self.RefreshView() def ScrollMessagePanelByPage(self, page): if self.CurrentMessage is not None: width, height = self.MessagePanel.GetClientSize() message_per_page = max( 1, (height - DATE_INFO_SIZE) / MESSAGE_INFO_SIZE - 1) self.ScrollMessagePanel(page * message_per_page) def ScrollMessagePanelByTimestamp(self, seconds): if self.CurrentMessage is not None: current_message = self.LogMessages[self.CurrentMessage] message, msgidx = self.GetMessageByTimestamp( current_message.Timestamp + seconds) if message is None or self.IsMessagePanelBottom(msgidx): self.ScrollToFirst() else: if seconds > 0 and self.CurrentMessage == msgidx and msgidx < len( self.LogMessages) - 1: msgidx += 1 self.CurrentMessage = msgidx self.RefreshView() def ResetMessagePanel(self): if len(self.LogMessages) > 0: self.CurrentMessage = len(self.LogMessages) - 1 message = self.LogMessages[self.CurrentMessage] while message is not None and not self.FilterLogMessage(message): message, self.CurrentMessage = self.GetPreviousMessage( self.CurrentMessage) self.RefreshView() def OnMessageFilterChanged(self, event): self.CurrentFilter = self.LevelFilters[ self.MessageFilter.GetSelection()] self.ResetMessagePanel() event.Skip() def OnSearchMessageChanged(self, event): self.CurrentSearchValue = self.SearchMessage.GetValue() self.ResetMessagePanel() event.Skip() def OnSearchMessageSearchButtonClick(self, event): self.CurrentSearchValue = self.SearchMessage.GetValue() self.ResetMessagePanel() event.Skip() def OnSearchMessageCancelButtonClick(self, event): self.CurrentSearchValue = "" self.SearchMessage.SetValue("") self.ResetMessagePanel() event.Skip() def OnCleanButton(self, event): if self.LogSource is not None and not self.IsPLCLogEmpty(): self.LogSource.ResetLogCount() self.ResetLogMessages() self.RefreshView() event.Skip() def GenerateOnDurationButton(self, duration): def OnDurationButton(): self.ScrollMessagePanelByTimestamp(duration) return OnDurationButton def GetCopyMessageToClipboardFunction(self, message): def CopyMessageToClipboardFunction(event): self.ParentWindow.SetCopyBuffer(message.GetFullText()) return CopyMessageToClipboardFunction def GetMessageByScreenPos(self, posx, posy): if self.CurrentMessage is not None: width, height = self.MessagePanel.GetClientSize() message_idx = self.CurrentMessage message = self.LogMessages[message_idx] draw_date = True offset = 5 while offset < height and message is not None: if draw_date: offset += DATE_INFO_SIZE if offset <= posy < offset + MESSAGE_INFO_SIZE: return message offset += MESSAGE_INFO_SIZE previous_message, message_idx = self.GetPreviousMessage( message_idx) if previous_message is not None: draw_date = message.Date != previous_message.Date message = previous_message return None def OnMessagePanelLeftUp(self, event): if self.CurrentMessage is not None: posx, posy = event.GetPosition() for button in self.LeftButtons + self.RightButtons: if button.HitTest(posx, posy): button.ProcessCallback() break event.Skip() def OnMessagePanelRightUp(self, event): message = self.GetMessageByScreenPos(*event.GetPosition()) if message is not None: menu = wx.Menu(title='') new_id = wx.NewId() menu.Append(help='', id=new_id, kind=wx.ITEM_NORMAL, text=_("Copy")) self.Bind(wx.EVT_MENU, self.GetCopyMessageToClipboardFunction(message), id=new_id) self.MessagePanel.PopupMenu(menu) menu.Destroy() event.Skip() def OnMessagePanelLeftDCLick(self, event): message = self.GetMessageByScreenPos(*event.GetPosition()) if message is not None: self.SearchMessage.SetFocus() self.SearchMessage.SetValue(message.Message) event.Skip() def ResetMessageToolTip(self): if self.MessageToolTip is not None: self.MessageToolTip.Destroy() self.MessageToolTip = None def OnMessageToolTipTimer(self, event): if self.LastMousePos is not None: message = self.GetMessageByScreenPos(*self.LastMousePos) if message is not None: tooltip_pos = self.MessagePanel.ClientToScreen( self.LastMousePos) tooltip_pos.x += 10 tooltip_pos.y += 10 self.MessageToolTip = CustomToolTip(self.MessagePanel, message.GetFullText(), False) self.MessageToolTip.SetFont(self.Font) self.MessageToolTip.SetToolTipPosition(tooltip_pos) self.MessageToolTip.Show() event.Skip() def OnMessagePanelMotion(self, event): if not event.Dragging(): self.ResetMessageToolTip() self.LastMousePos = event.GetPosition() self.MessageToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) event.Skip() def OnMessagePanelLeaveWindow(self, event): self.ResetMessageToolTip() self.LastMousePos = None self.MessageToolTipTimer.Stop() event.Skip() def OnMessagePanelMouseWheel(self, event): self.ScrollMessagePanel(event.GetWheelRotation() / event.GetWheelDelta()) event.Skip() def OnMessagePanelEraseBackground(self, event): pass def OnMessagePanelPaint(self, event): self.RefreshView() event.Skip() def OnMessagePanelResize(self, event): width, height = self.MessagePanel.GetClientSize() offset = 2 for button in self.LeftButtons: button.SetPosition(offset, 2) w, h = button.GetSize() offset += w + 2 offset = width - 2 for button in self.RightButtons: w, h = button.GetSize() button.SetPosition(offset - w, 2) offset -= w + 2 if self.IsMessagePanelBottom(): self.ScrollToFirst() else: self.RefreshView() event.Skip() def OnScrollTimer(self, event): if self.ScrollSpeed != 0.: speed_norm = abs(self.ScrollSpeed) period = REFRESH_PERIOD / speed_norm self.ScrollMessagePanel(-speed_norm / self.ScrollSpeed) self.LastStartTime = gettime() self.ScrollTimer.Start(int(period * 1000), True) event.Skip() def SetScrollSpeed(self, speed): if speed == 0.: self.ScrollTimer.Stop() else: speed_norm = abs(speed) period = REFRESH_PERIOD / speed_norm current_time = gettime() if self.LastStartTime is not None: elapsed_time = current_time - self.LastStartTime if elapsed_time > period: self.ScrollMessagePanel(-speed_norm / speed) self.LastStartTime = current_time else: period -= elapsed_time else: self.LastStartTime = current_time self.ScrollTimer.Start(int(period * 1000), True) self.ScrollSpeed = speed def ScrollToLast(self, refresh=True): if len(self.LogMessages) > 0: self.CurrentMessage = len(self.LogMessages) - 1 message = self.LogMessages[self.CurrentMessage] if not self.FilterLogMessage(message): message, self.CurrentMessage = self.GetPreviousMessage( self.CurrentMessage) if refresh: self.RefreshView() def ScrollToFirst(self): if len(self.LogMessages) > 0: message_idx = 0 message = self.LogMessages[message_idx] if not self.FilterLogMessage(message): next_message, msgidx = self.GetNextMessage(message_idx) if next_message is not None: message_idx = msgidx message = next_message while message is not None: message, msgidx = self.GetPreviousMessage(message_idx) if message is not None: message_idx = msgidx message = self.LogMessages[message_idx] if self.FilterLogMessage(message): while message is not None: message, msgidx = self.GetNextMessage(message_idx) if message is not None: if not self.IsMessagePanelBottom(msgidx): break message_idx = msgidx self.CurrentMessage = message_idx else: self.CurrentMessage = None self.RefreshView()
class ToolTipProducer: def __init__(self, parent): """ Constructor @param parent: Parent Viewer """ self.Parent = parent self.ToolTip = None self.ToolTipPos = None # Timer for firing Tool tip display self.ToolTipTimer = wx.Timer(self.Parent, -1) self.Parent.Bind(wx.EVT_TIMER, self.OnToolTipTimer, self.ToolTipTimer) def __del__(self): """ Destructor """ self.DestroyToolTip() def OnToolTipTimer(self, event): """ Callback for Tool Tip firing timer Event @param event: Tool tip text """ # Get Tool Tip text value = self.GetToolTipValue() if value is not None and self.ToolTipPos is not None: # Create Tool Tip self.ToolTip = CustomToolTip(self.Parent, value) self.ToolTip.SetToolTipPosition(self.ToolTipPos) self.ToolTip.Show() def GetToolTipValue(self): """ Return tool tip text Have to be overridden by inherited classes @return: Tool tip text (None if not overridden) """ return None def DisplayToolTip(self, pos): """ Display Tool tip @param pos: Tool tip position """ # Destroy current displayed Tool tip self.DestroyToolTip() # Save Tool Tip position self.ToolTipPos = pos # Start Tool tip firing timer self.ToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) def SetToolTipText(self, text): """ Set current Tool tip text @param text: Tool tip Text """ if self.ToolTip is not None: self.ToolTip.SetTip(text) def DestroyToolTip(self): """ Destroy current displayed Tool Tip """ # Stop Tool tip firing timer self.ToolTipTimer.Stop() self.ToolTipPos = None # Destroy Tool Tip if self.ToolTip is not None: self.ToolTip.Destroy() self.ToolTip = None