Laurent@978: #!/usr/bin/env python Laurent@978: # -*- coding: utf-8 -*- Laurent@978: andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival. Laurent@978: # andrej@1571: # Copyright (C) 2013: Edouard TISSERANT and Laurent BESSARD Laurent@978: # andrej@1571: # See COPYING file for copyrights details. Laurent@978: # andrej@1571: # This program is free software; you can redistribute it and/or andrej@1571: # modify it under the terms of the GNU General Public License andrej@1571: # as published by the Free Software Foundation; either version 2 andrej@1571: # of the License, or (at your option) any later version. Laurent@978: # andrej@1571: # This program is distributed in the hope that it will be useful, andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the andrej@1571: # GNU General Public License for more details. Laurent@978: # andrej@1571: # You should have received a copy of the GNU General Public License andrej@1571: # along with this program; if not, write to the Free Software andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. andrej@1571: Laurent@978: andrej@1881: from __future__ import absolute_import andrej@2437: from __future__ import division Laurent@978: from datetime import datetime Laurent@981: from time import time as gettime andrej@1832: from weakref import proxy andrej@1832: Laurent@982: import numpy Laurent@978: import wx andrej@2432: from six.moves import xrange Laurent@978: Laurent@1169: from controls.CustomToolTip import CustomToolTip, TOOLTIP_WAIT_PERIOD Laurent@1176: from editors.DebugViewer import DebugViewer, REFRESH_PERIOD Edouard@1902: from runtime.loglevels import LogLevelsCount, LogLevels Laurent@978: from util.BitmapLibrary import GetBitmap andrej@1832: Laurent@978: Laurent@981: THUMB_SIZE_RATIO = 1. / 8. Laurent@978: andrej@1736: Laurent@993: def ArrowPoints(direction, width, height, xoffset, yoffset): Laurent@986: if direction == wx.TOP: Laurent@993: return [wx.Point(xoffset + 1, yoffset + height - 2), andrej@2437: wx.Point(xoffset + width // 2, yoffset + 1), Laurent@993: wx.Point(xoffset + width - 1, yoffset + height - 2)] Laurent@986: else: Laurent@993: return [wx.Point(xoffset + 1, yoffset - height + 1), andrej@2437: wx.Point(xoffset + width // 2, yoffset - 2), Laurent@993: wx.Point(xoffset + width - 1, yoffset - height + 1)] Laurent@986: andrej@1736: Laurent@983: class LogScrollBar(wx.Panel): Edouard@1441: Laurent@978: def __init__(self, parent, size): Laurent@978: wx.Panel.__init__(self, parent, size=size) Laurent@978: self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) Laurent@978: self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) Laurent@978: self.Bind(wx.EVT_MOTION, self.OnMotion) Laurent@988: self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) Laurent@978: self.Bind(wx.EVT_PAINT, self.OnPaint) Laurent@978: self.Bind(wx.EVT_SIZE, self.OnResize) Edouard@1441: andrej@1737: self.ThumbPosition = 0. # -1 <= ThumbPosition <= 1 Laurent@981: self.ThumbScrollingStartPos = None Edouard@1441: Laurent@978: def GetRangeRect(self): Laurent@978: width, height = self.GetClientSize() Laurent@978: return wx.Rect(0, width, width, height - 2 * width) Edouard@1441: Laurent@978: def GetThumbRect(self): andrej@1847: width, _height = self.GetClientSize() Laurent@978: range_rect = self.GetRangeRect() Laurent@981: thumb_size = range_rect.height * THUMB_SIZE_RATIO Laurent@981: thumb_range = range_rect.height - thumb_size Laurent@981: thumb_center_position = (thumb_size + (self.ThumbPosition + 1) * thumb_range) / 2. Laurent@981: thumb_start = int(thumb_center_position - thumb_size / 2.) Laurent@981: thumb_end = int(thumb_center_position + thumb_size / 2.) Laurent@987: return wx.Rect(0, range_rect.y + thumb_start, width, thumb_end - thumb_start) Edouard@1441: Laurent@981: def RefreshThumbPosition(self, thumb_position=None): Laurent@981: if thumb_position is None: Laurent@981: thumb_position = self.ThumbPosition Laurent@978: if self.Parent.IsMessagePanelTop(): Laurent@981: thumb_position = max(0., thumb_position) Laurent@978: if self.Parent.IsMessagePanelBottom(): Laurent@981: thumb_position = min(0., thumb_position) Laurent@981: if thumb_position != self.ThumbPosition: Laurent@981: self.ThumbPosition = thumb_position Laurent@981: self.Parent.SetScrollSpeed(self.ThumbPosition) Laurent@981: self.Refresh() Edouard@1441: Laurent@978: def OnLeftDown(self, event): Laurent@978: self.CaptureMouse() Laurent@978: posx, posy = event.GetPosition() Laurent@978: width, height = self.GetClientSize() Laurent@978: range_rect = self.GetRangeRect() Laurent@978: thumb_rect = self.GetThumbRect() edouard@3303: if range_rect.Contains(posx, posy): edouard@3303: if thumb_rect.Contains(posx, posy): Laurent@981: self.ThumbScrollingStartPos = wx.Point(posx, posy) Laurent@978: elif posy < thumb_rect.y: Laurent@981: self.Parent.ScrollToLast() Laurent@978: elif posy > thumb_rect.y + thumb_rect.height: Laurent@981: self.Parent.ScrollToFirst() Laurent@978: elif posy < width: Laurent@986: self.Parent.ScrollMessagePanelByPage(1) Laurent@978: elif posy > height - width: Laurent@986: self.Parent.ScrollMessagePanelByPage(-1) Laurent@978: event.Skip() Edouard@1441: Laurent@978: def OnLeftUp(self, event): Laurent@981: self.ThumbScrollingStartPos = None Laurent@981: self.RefreshThumbPosition(0.) Laurent@978: if self.HasCapture(): Laurent@978: self.ReleaseMouse() Laurent@978: event.Skip() Edouard@1441: Laurent@978: def OnMotion(self, event): Laurent@981: if event.Dragging() and self.ThumbScrollingStartPos is not None: andrej@1847: _posx, posy = event.GetPosition() Laurent@978: range_rect = self.GetRangeRect() Laurent@981: thumb_size = range_rect.height * THUMB_SIZE_RATIO Laurent@981: thumb_range = range_rect.height - thumb_size Laurent@981: self.RefreshThumbPosition( andrej@2437: max(-1., min((posy - self.ThumbScrollingStartPos.y) * 2. // thumb_range, 1.))) Laurent@978: event.Skip() Edouard@1441: Laurent@978: def OnResize(self, event): Laurent@978: self.Refresh() Laurent@978: event.Skip() Edouard@1441: Laurent@988: def OnEraseBackground(self, event): Laurent@988: pass Edouard@1441: Laurent@978: def OnPaint(self, event): Laurent@978: dc = wx.BufferedPaintDC(self) Laurent@978: dc.Clear() Edouard@1441: Laurent@987: gc = wx.GCDC(dc) Edouard@1441: Laurent@981: width, height = self.GetClientSize() Edouard@1441: Laurent@993: gc.SetPen(wx.Pen(wx.NamedColour("GREY"), 3)) Laurent@987: gc.SetBrush(wx.GREY_BRUSH) Edouard@1441: andrej@2437: gc.DrawLines(ArrowPoints(wx.TOP, width * 0.75, width * 0.5, 2, (width + height) // 4 - 3)) andrej@2437: gc.DrawLines(ArrowPoints(wx.TOP, width * 0.75, width * 0.5, 2, (width + height) // 4 + 3)) andrej@2437: andrej@2437: gc.DrawLines(ArrowPoints(wx.BOTTOM, width * 0.75, width * 0.5, 2, (height * 3 - width) // 4 + 3)) andrej@2437: gc.DrawLines(ArrowPoints(wx.BOTTOM, width * 0.75, width * 0.5, 2, (height * 3 - width) // 4 - 3)) Edouard@1441: Laurent@981: thumb_rect = self.GetThumbRect() Laurent@981: exclusion_rect = wx.Rect(thumb_rect.x, thumb_rect.y, Laurent@981: thumb_rect.width, thumb_rect.height) Laurent@981: if self.Parent.IsMessagePanelTop(): Laurent@981: exclusion_rect.y, exclusion_rect.height = width, exclusion_rect.y + exclusion_rect.height - width Laurent@981: if self.Parent.IsMessagePanelBottom(): Laurent@981: exclusion_rect.height = height - width - exclusion_rect.y Laurent@981: if exclusion_rect != thumb_rect: Laurent@981: colour = wx.NamedColour("LIGHT GREY") Laurent@987: gc.SetPen(wx.Pen(colour)) Laurent@987: gc.SetBrush(wx.Brush(colour)) Edouard@1441: Edouard@1441: gc.DrawRectangle(exclusion_rect.x, exclusion_rect.y, Laurent@981: exclusion_rect.width, exclusion_rect.height) Edouard@1441: Laurent@987: gc.SetPen(wx.GREY_PEN) Laurent@987: gc.SetBrush(wx.GREY_BRUSH) Edouard@1441: Laurent@993: gc.DrawPolygon(ArrowPoints(wx.TOP, width, width, 0, 0)) Edouard@1441: Laurent@993: gc.DrawPolygon(ArrowPoints(wx.BOTTOM, width, width, 0, height)) Edouard@1441: Edouard@1441: gc.DrawRectangle(thumb_rect.x, thumb_rect.y, Laurent@978: thumb_rect.width, thumb_rect.height) Edouard@1441: Laurent@978: event.Skip() Laurent@978: andrej@1749: andrej@1576: BUTTON_SIZE = (70, 15) Laurent@983: andrej@1736: andrej@1831: class LogButton(object): Edouard@1441: Laurent@983: def __init__(self, label, callback): Laurent@983: self.Position = wx.Point(0, 0) Laurent@983: self.Size = wx.Size(*BUTTON_SIZE) Laurent@983: self.Label = label Laurent@983: self.Shown = True Laurent@983: self.Callback = callback Edouard@1441: Laurent@983: def __del__(self): Laurent@983: self.callback = None Edouard@1441: Laurent@983: def GetSize(self): Laurent@983: return self.Size Edouard@1441: Laurent@983: def SetPosition(self, x, y): Laurent@983: self.Position = wx.Point(x, y) Edouard@1441: Laurent@983: def HitTest(self, x, y): Edouard@1441: rect = wx.Rect(self.Position.x, self.Position.y, Laurent@983: self.Size.width, self.Size.height) edouard@3303: if rect.Contains(x, y): Laurent@983: return True Laurent@983: return False Edouard@1441: Laurent@983: def ProcessCallback(self): Laurent@983: if self.Callback is not None: Laurent@983: wx.CallAfter(self.Callback) Edouard@1441: Laurent@983: def Draw(self, dc): Laurent@983: dc.SetPen(wx.TRANSPARENT_PEN) Laurent@983: dc.SetBrush(wx.Brush(wx.NamedColour("LIGHT GREY"))) Edouard@1441: Edouard@1441: dc.DrawRectangle(self.Position.x, self.Position.y, Laurent@983: self.Size.width, self.Size.height) Edouard@1441: Laurent@983: w, h = dc.GetTextExtent(self.Label) Edouard@1441: dc.DrawText(self.Label, andrej@2437: self.Position.x + (self.Size.width - w) // 2, andrej@2437: self.Position.y + (self.Size.height - h) // 2) Laurent@983: andrej@1749: Laurent@978: DATE_INFO_SIZE = 10 Laurent@997: MESSAGE_INFO_SIZE = 18 Laurent@978: andrej@1736: andrej@1831: class LogMessage(object): Edouard@1441: Laurent@978: def __init__(self, tv_sec, tv_nsec, level, level_bitmap, msg): Laurent@1076: self.Date = datetime.utcfromtimestamp(tv_sec) Laurent@978: self.Seconds = self.Date.second + tv_nsec * 1e-9 Laurent@978: self.Date = self.Date.replace(second=0) Laurent@982: self.Timestamp = tv_sec + tv_nsec * 1e-9 Laurent@978: self.Level = level Laurent@978: self.LevelBitmap = level_bitmap Laurent@978: self.Message = msg Laurent@978: self.DrawDate = True Edouard@1441: Laurent@978: def __cmp__(self, other): Laurent@978: if self.Date == other.Date: Laurent@978: return cmp(self.Seconds, other.Seconds) Laurent@978: return cmp(self.Date, other.Date) Edouard@1441: Laurent@993: def GetFullText(self): Laurent@993: date = self.Date.replace(second=int(self.Seconds)) Laurent@993: nsec = (self.Seconds % 1.) * 1e9 Laurent@993: return "%s at %s.%9.9d:\n%s" % ( Laurent@993: LogLevels[self.Level], Laurent@993: str(date), nsec, Laurent@993: self.Message) Edouard@1441: Laurent@978: def Draw(self, dc, offset, width, draw_date): Laurent@978: if draw_date: Laurent@978: datetime_text = self.Date.strftime("%d/%m/%y %H:%M") Laurent@978: dw, dh = dc.GetTextExtent(datetime_text) andrej@2437: dc.DrawText(datetime_text, (width - dw) // 2, offset + (DATE_INFO_SIZE - dh) // 2) Laurent@978: offset += DATE_INFO_SIZE Edouard@1441: Laurent@978: seconds_text = "%12.9f" % self.Seconds Laurent@978: sw, sh = dc.GetTextExtent(seconds_text) andrej@2437: dc.DrawText(seconds_text, 5, offset + (MESSAGE_INFO_SIZE - sh) // 2) Edouard@1441: Laurent@978: bw, bh = self.LevelBitmap.GetWidth(), self.LevelBitmap.GetHeight() andrej@2437: dc.DrawBitmap(self.LevelBitmap, 10 + sw, offset + (MESSAGE_INFO_SIZE - bh) // 2) Edouard@1441: Laurent@993: text = self.Message.replace("\n", " ") andrej@1847: _mw, mh = dc.GetTextExtent(text) andrej@2437: dc.DrawText(text, 15 + sw + bw, offset + (MESSAGE_INFO_SIZE - mh) // 2) Edouard@1441: Laurent@978: def GetHeight(self, draw_date): Laurent@978: if draw_date: Laurent@978: return DATE_INFO_SIZE + MESSAGE_INFO_SIZE Laurent@978: return MESSAGE_INFO_SIZE Laurent@978: andrej@1749: Laurent@978: SECOND = 1 Laurent@978: MINUTE = 60 * SECOND Laurent@978: HOUR = 60 * MINUTE Laurent@978: DAY = 24 * HOUR Laurent@978: Laurent@978: CHANGE_TIMESTAMP_BUTTONS = [(_("1d"), DAY), Laurent@978: (_("1h"), HOUR), Laurent@986: (_("1m"), MINUTE), Laurent@986: (_("1s"), SECOND)] Laurent@978: andrej@1736: Laurent@978: class LogViewer(DebugViewer, wx.Panel): Edouard@1441: Laurent@978: def __init__(self, parent, window): andrej@1745: wx.Panel.__init__(self, parent, style=wx.TAB_TRAVERSAL | wx.SUNKEN_BORDER) Laurent@978: DebugViewer.__init__(self, None, False, False) Edouard@1441: Laurent@978: main_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=5) Laurent@978: main_sizer.AddGrowableCol(0) Laurent@978: main_sizer.AddGrowableRow(1) Edouard@1441: Laurent@978: filter_sizer = wx.BoxSizer(wx.HORIZONTAL) edouard@3303: main_sizer.Add(filter_sizer, border=5, flag=wx.TOP | wx.LEFT | wx.RIGHT | wx.GROW) Edouard@1441: Laurent@978: self.MessageFilter = wx.ComboBox(self, style=wx.CB_READONLY) Laurent@978: self.MessageFilter.Append(_("All")) Laurent@978: levels = LogLevels[:3] Laurent@978: levels.reverse() Laurent@978: for level in levels: Laurent@978: self.MessageFilter.Append(_(level)) Laurent@978: self.Bind(wx.EVT_COMBOBOX, self.OnMessageFilterChanged, self.MessageFilter) edouard@3303: filter_sizer.Add(self.MessageFilter, 1, border=5, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL) Edouard@1441: Laurent@981: self.SearchMessage = wx.SearchCtrl(self, style=wx.TE_PROCESS_ENTER) Laurent@978: self.SearchMessage.ShowSearchButton(True) Laurent@981: self.SearchMessage.ShowCancelButton(True) Laurent@981: self.Bind(wx.EVT_TEXT_ENTER, self.OnSearchMessageChanged, self.SearchMessage) Edouard@1441: self.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, andrej@1768: self.OnSearchMessageSearchButtonClick, self.SearchMessage) Edouard@1441: self.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, andrej@1768: self.OnSearchMessageCancelButtonClick, self.SearchMessage) edouard@3303: filter_sizer.Add(self.SearchMessage, 3, border=5, flag=wx.RIGHT | wx.ALIGN_CENTER_VERTICAL) Edouard@1441: Edouard@1441: self.CleanButton = wx.lib.buttons.GenBitmapButton(self, bitmap=GetBitmap("Clean"), andrej@1768: size=wx.Size(28, 28), style=wx.NO_BORDER) edouard@3303: self.CleanButton.SetToolTip(_("Clean log messages")) Laurent@1093: self.Bind(wx.EVT_BUTTON, self.OnCleanButton, self.CleanButton) edouard@3303: filter_sizer.Add(self.CleanButton) Edouard@1441: Laurent@983: message_panel_sizer = wx.FlexGridSizer(cols=2, hgap=0, rows=1, vgap=0) Laurent@983: message_panel_sizer.AddGrowableCol(0) Laurent@978: message_panel_sizer.AddGrowableRow(0) edouard@3303: main_sizer.Add(message_panel_sizer, border=5, flag=wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.GROW) Edouard@1441: Laurent@978: self.MessagePanel = wx.Panel(self) Laurent@978: if wx.Platform == '__WXMSW__': Laurent@993: self.Font = wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='Courier New') Laurent@978: else: edouard@2704: self.Font = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL, faceName='FreeMono') Laurent@984: self.MessagePanel.Bind(wx.EVT_LEFT_UP, self.OnMessagePanelLeftUp) Laurent@993: self.MessagePanel.Bind(wx.EVT_RIGHT_UP, self.OnMessagePanelRightUp) Laurent@986: self.MessagePanel.Bind(wx.EVT_LEFT_DCLICK, self.OnMessagePanelLeftDCLick) Laurent@993: self.MessagePanel.Bind(wx.EVT_MOTION, self.OnMessagePanelMotion) Laurent@993: self.MessagePanel.Bind(wx.EVT_LEAVE_WINDOW, self.OnMessagePanelLeaveWindow) Laurent@981: self.MessagePanel.Bind(wx.EVT_MOUSEWHEEL, self.OnMessagePanelMouseWheel) Laurent@988: self.MessagePanel.Bind(wx.EVT_ERASE_BACKGROUND, self.OnMessagePanelEraseBackground) Laurent@978: self.MessagePanel.Bind(wx.EVT_PAINT, self.OnMessagePanelPaint) Laurent@978: self.MessagePanel.Bind(wx.EVT_SIZE, self.OnMessagePanelResize) edouard@3303: message_panel_sizer.Add(self.MessagePanel, flag=wx.GROW) Edouard@1441: Laurent@983: self.MessageScrollBar = LogScrollBar(self, wx.Size(16, -1)) edouard@3303: message_panel_sizer.Add(self.MessageScrollBar, flag=wx.GROW) Edouard@1441: Laurent@978: self.SetSizer(main_sizer) Edouard@1441: Laurent@983: self.LeftButtons = [] Edouard@1441: for label, callback in [("+" + text, self.GenerateOnDurationButton(duration)) Laurent@983: for text, duration in CHANGE_TIMESTAMP_BUTTONS]: Laurent@983: self.LeftButtons.append(LogButton(label, callback)) Edouard@1441: Laurent@983: self.RightButtons = [] Edouard@1441: for label, callback in [("-" + text, self.GenerateOnDurationButton(-duration)) Laurent@983: for text, duration in CHANGE_TIMESTAMP_BUTTONS]: Laurent@983: self.RightButtons.append(LogButton(label, callback)) Edouard@1441: Laurent@978: self.MessageFilter.SetSelection(0) Laurent@978: self.LogSource = None Laurent@978: self.ResetLogMessages() Laurent@978: self.ParentWindow = window Edouard@1441: Laurent@979: self.LevelIcons = [GetBitmap("LOG_" + level) for level in LogLevels] Laurent@978: self.LevelFilters = [range(i) for i in xrange(4, 0, -1)] Laurent@978: self.CurrentFilter = self.LevelFilters[0] Laurent@981: self.CurrentSearchValue = "" Edouard@1441: Laurent@981: self.ScrollSpeed = 0. Laurent@981: self.LastStartTime = None Laurent@978: self.ScrollTimer = wx.Timer(self, -1) Laurent@978: self.Bind(wx.EVT_TIMER, self.OnScrollTimer, self.ScrollTimer) Edouard@1441: Laurent@993: self.LastMousePos = None Laurent@993: self.MessageToolTip = None Laurent@993: self.MessageToolTipTimer = wx.Timer(self, -1) Laurent@993: self.Bind(wx.EVT_TIMER, self.OnMessageToolTipTimer, self.MessageToolTipTimer) Edouard@1441: Laurent@978: def __del__(self): Laurent@978: self.ScrollTimer.Stop() Edouard@1441: Laurent@978: def ResetLogMessages(self): andrej@1672: self.ResetLogCounters() Laurent@978: self.OldestMessages = [] Laurent@978: self.LogMessages = [] Laurent@982: self.LogMessagesTimestamp = numpy.array([]) Laurent@978: self.CurrentMessage = None Laurent@978: self.HasNewData = False Edouard@1441: Laurent@978: def SetLogSource(self, log_source): Edouard@1989: self.LogSource = proxy(log_source) if log_source is not None else None Laurent@1093: self.CleanButton.Enable(self.LogSource is not None) Laurent@978: if log_source is not None: Laurent@978: self.ResetLogMessages() andrej@1871: wx.CallAfter(self.RefreshView) Edouard@1441: Laurent@978: def GetLogMessageFromSource(self, msgidx, level): Laurent@978: if self.LogSource is not None: Laurent@978: answer = self.LogSource.GetLogMessage(level, msgidx) Laurent@978: if answer is not None: andrej@1847: msg, _tick, tv_sec, tv_nsec = answer Laurent@978: return LogMessage(tv_sec, tv_nsec, level, self.LevelIcons[level], msg) Laurent@978: return None Edouard@1441: andrej@1672: def ResetLogCounters(self): andrej@1672: self.previous_log_count = [None]*LogLevelsCount andrej@1735: Laurent@978: def SetLogCounters(self, log_count): Laurent@978: new_messages = [] Laurent@978: for level, count, prev in zip(xrange(LogLevelsCount), log_count, self.previous_log_count): Laurent@978: if count is not None and prev != count: Laurent@978: if prev is None: Laurent@1195: dump_end = max(-1, count - 10) Laurent@1195: oldest_message = (-1, None) Laurent@978: else: Laurent@978: dump_end = prev - 1 andrej@1740: for msgidx in xrange(count-1, dump_end, -1): Laurent@978: new_message = self.GetLogMessageFromSource(msgidx, level) Laurent@1093: if new_message is None: Laurent@1195: if prev is None: Laurent@1195: oldest_message = (-1, None) Laurent@1093: break Laurent@1093: if prev is None: Laurent@1195: oldest_message = (msgidx, new_message) Laurent@1093: if len(new_messages) == 0: Laurent@1093: new_messages = [new_message] Laurent@978: else: Laurent@978: new_messages.insert(0, new_message) Laurent@1093: else: Laurent@1093: new_messages.insert(0, new_message) Laurent@1079: if prev is None and len(self.OldestMessages) <= level: Laurent@1195: self.OldestMessages.append(oldest_message) Laurent@978: self.previous_log_count[level] = count Laurent@978: new_messages.sort() Laurent@978: if len(new_messages) > 0: Laurent@978: self.HasNewData = True Laurent@1071: if self.CurrentMessage is not None: Laurent@1071: current_is_last = self.GetNextMessage(self.CurrentMessage)[0] is None Laurent@1071: else: Laurent@1071: current_is_last = True Laurent@978: for new_message in new_messages: Laurent@978: self.LogMessages.append(new_message) Laurent@982: self.LogMessagesTimestamp = numpy.append(self.LogMessagesTimestamp, [new_message.Timestamp]) Laurent@1071: if current_is_last: Laurent@1071: self.ScrollToLast(False) Laurent@993: self.ResetMessageToolTip() Laurent@993: self.MessageToolTipTimer.Stop() Laurent@999: self.ParentWindow.SelectTab(self) Laurent@978: self.NewDataAvailable(None) Edouard@1441: Laurent@982: def FilterLogMessage(self, message, timestamp=None): Edouard@1441: return (message.Level in self.CurrentFilter and Laurent@982: message.Message.find(self.CurrentSearchValue) != -1 and Laurent@982: (timestamp is None or message.Timestamp < timestamp)) Edouard@1441: Laurent@982: def GetMessageByTimestamp(self, timestamp): Laurent@982: if self.CurrentMessage is not None: Laurent@982: msgidx = numpy.argmin(abs(self.LogMessagesTimestamp - timestamp)) Laurent@982: message = self.LogMessages[msgidx] Laurent@982: if self.FilterLogMessage(message) and message.Timestamp > timestamp: Laurent@982: return self.GetPreviousMessage(msgidx, timestamp) Laurent@982: return message, msgidx Laurent@982: return None, None Edouard@1441: Laurent@981: def GetNextMessage(self, msgidx): Laurent@978: while msgidx < len(self.LogMessages) - 1: Laurent@978: message = self.LogMessages[msgidx + 1] Laurent@981: if self.FilterLogMessage(message): Laurent@978: return message, msgidx + 1 Laurent@978: msgidx += 1 Laurent@978: return None, None Edouard@1441: Laurent@982: def GetPreviousMessage(self, msgidx, timestamp=None): Laurent@978: message = None Laurent@978: while 0 < msgidx < len(self.LogMessages): Laurent@978: message = self.LogMessages[msgidx - 1] Laurent@982: if self.FilterLogMessage(message, timestamp): Laurent@978: return message, msgidx - 1 Laurent@978: msgidx -= 1 Laurent@978: if len(self.LogMessages) > 0: Laurent@978: message = self.LogMessages[0] andrej@1847: for _idx, msg in self.OldestMessages: Laurent@1195: if msg is not None and msg > message: Laurent@1195: message = msg Laurent@978: while message is not None: Laurent@978: level = message.Level andrej@1847: oldest_msgidx, _oldest_message = self.OldestMessages[level] Laurent@978: if oldest_msgidx > 0: Laurent@1195: message = self.GetLogMessageFromSource(oldest_msgidx - 1, level) Laurent@1195: if message is not None: Laurent@1195: self.OldestMessages[level] = (oldest_msgidx - 1, message) Laurent@978: else: Laurent@978: self.OldestMessages[level] = (-1, None) Laurent@978: else: Laurent@1195: message = None Laurent@978: self.OldestMessages[level] = (-1, None) Laurent@1195: if message is not None: Laurent@1195: message_idx = 0 Edouard@1441: while (message_idx < len(self.LogMessages) and Laurent@1195: self.LogMessages[message_idx] < message): Laurent@1195: message_idx += 1 Laurent@1195: if len(self.LogMessages) > 0: Laurent@1195: current_message = self.LogMessages[self.CurrentMessage] Laurent@1195: else: Laurent@1195: current_message = message Laurent@1195: self.LogMessages.insert(message_idx, message) Laurent@1195: self.LogMessagesTimestamp = numpy.insert( andrej@1878: self.LogMessagesTimestamp, andrej@1878: [message_idx], andrej@1878: [message.Timestamp]) Laurent@1195: self.CurrentMessage = self.LogMessages.index(current_message) Laurent@1195: if message_idx == 0 and self.FilterLogMessage(message, timestamp): Laurent@1195: return message, 0 andrej@1847: for _idx, msg in self.OldestMessages: Laurent@978: if msg is not None and (message is None or msg > message): Laurent@978: message = msg Laurent@978: return None, None Edouard@1441: Laurent@978: def RefreshNewData(self, *args, **kwargs): Laurent@978: if self.HasNewData: Laurent@978: self.HasNewData = False Laurent@978: self.RefreshView() Laurent@978: DebugViewer.RefreshNewData(self, *args, **kwargs) Edouard@1441: Laurent@978: def RefreshView(self): Laurent@978: width, height = self.MessagePanel.GetClientSize() edouard@3303: bitmap = wx.Bitmap(width, height) Laurent@978: dc = wx.BufferedDC(wx.ClientDC(self.MessagePanel), bitmap) Laurent@978: dc.Clear() Edouard@1441: Laurent@978: if self.CurrentMessage is not None: Edouard@1441: Laurent@993: dc.SetFont(self.Font) Edouard@1441: Laurent@983: for button in self.LeftButtons + self.RightButtons: Laurent@983: button.Draw(dc) Edouard@1441: Laurent@978: message_idx = self.CurrentMessage Laurent@978: message = self.LogMessages[message_idx] Laurent@978: draw_date = True Laurent@978: offset = 5 Laurent@978: while offset < height and message is not None: Laurent@978: message.Draw(dc, offset, width, draw_date) Laurent@978: offset += message.GetHeight(draw_date) Edouard@1441: Laurent@981: previous_message, message_idx = self.GetPreviousMessage(message_idx) Laurent@978: if previous_message is not None: Laurent@978: draw_date = message.Date != previous_message.Date Laurent@978: message = previous_message Edouard@1441: Laurent@981: self.MessageScrollBar.RefreshThumbPosition() Edouard@1441: andrej@1673: def IsPLCLogEmpty(self): andrej@1742: empty = True andrej@1847: for _level, prev in zip(xrange(LogLevelsCount), self.previous_log_count): andrej@1673: if prev is not None: andrej@1742: empty = False andrej@1673: break andrej@1673: return empty andrej@1735: Laurent@978: def IsMessagePanelTop(self, message_idx=None): Laurent@978: if message_idx is None: Laurent@978: message_idx = self.CurrentMessage Laurent@978: if message_idx is not None: Laurent@981: return self.GetNextMessage(message_idx)[0] is None Laurent@978: return True Edouard@1441: Laurent@978: def IsMessagePanelBottom(self, message_idx=None): Laurent@978: if message_idx is None: Laurent@978: message_idx = self.CurrentMessage Laurent@978: if message_idx is not None: andrej@1847: _width, height = self.MessagePanel.GetClientSize() Laurent@978: offset = 5 Laurent@978: message = self.LogMessages[message_idx] Laurent@978: draw_date = True Laurent@978: while message is not None and offset < height: Laurent@978: offset += message.GetHeight(draw_date) Laurent@981: previous_message, message_idx = self.GetPreviousMessage(message_idx) Laurent@978: if previous_message is not None: Laurent@978: draw_date = message.Date != previous_message.Date Laurent@978: message = previous_message Laurent@978: return offset < height Laurent@978: return True Edouard@1441: Laurent@978: def ScrollMessagePanel(self, scroll): Laurent@978: if self.CurrentMessage is not None: Laurent@978: message = self.LogMessages[self.CurrentMessage] Laurent@978: while scroll > 0 and message is not None: Laurent@981: message, msgidx = self.GetNextMessage(self.CurrentMessage) Laurent@978: if message is not None: Laurent@978: self.CurrentMessage = msgidx Laurent@978: scroll -= 1 Laurent@978: while scroll < 0 and message is not None and not self.IsMessagePanelBottom(): Laurent@981: message, msgidx = self.GetPreviousMessage(self.CurrentMessage) Laurent@978: if message is not None: Laurent@978: self.CurrentMessage = msgidx Laurent@978: scroll += 1 Laurent@978: self.RefreshView() Edouard@1441: Laurent@986: def ScrollMessagePanelByPage(self, page): Laurent@986: if self.CurrentMessage is not None: andrej@1847: _width, height = self.MessagePanel.GetClientSize() andrej@2437: message_per_page = max(1, (height - DATE_INFO_SIZE) // MESSAGE_INFO_SIZE - 1) Laurent@986: self.ScrollMessagePanel(page * message_per_page) Edouard@1441: Laurent@982: def ScrollMessagePanelByTimestamp(self, seconds): Laurent@982: if self.CurrentMessage is not None: Laurent@982: current_message = self.LogMessages[self.CurrentMessage] Laurent@982: message, msgidx = self.GetMessageByTimestamp(current_message.Timestamp + seconds) Laurent@982: if message is None or self.IsMessagePanelBottom(msgidx): Laurent@982: self.ScrollToFirst() Laurent@982: else: Laurent@1020: if seconds > 0 and self.CurrentMessage == msgidx and msgidx < len(self.LogMessages) - 1: Laurent@1020: msgidx += 1 Laurent@982: self.CurrentMessage = msgidx Laurent@984: self.RefreshView() Edouard@1441: Laurent@981: def ResetMessagePanel(self): Laurent@978: if len(self.LogMessages) > 0: Laurent@978: self.CurrentMessage = len(self.LogMessages) - 1 Laurent@978: message = self.LogMessages[self.CurrentMessage] Laurent@981: while message is not None and not self.FilterLogMessage(message): Laurent@981: message, self.CurrentMessage = self.GetPreviousMessage(self.CurrentMessage) Laurent@981: self.RefreshView() Edouard@1441: Laurent@981: def OnMessageFilterChanged(self, event): Laurent@981: self.CurrentFilter = self.LevelFilters[self.MessageFilter.GetSelection()] Laurent@981: self.ResetMessagePanel() Laurent@981: event.Skip() Edouard@1441: Laurent@981: def OnSearchMessageChanged(self, event): Laurent@981: self.CurrentSearchValue = self.SearchMessage.GetValue() Laurent@981: self.ResetMessagePanel() Laurent@981: event.Skip() Edouard@1441: Laurent@981: def OnSearchMessageSearchButtonClick(self, event): Laurent@981: self.CurrentSearchValue = self.SearchMessage.GetValue() Laurent@981: self.ResetMessagePanel() Laurent@981: event.Skip() Edouard@1441: Laurent@981: def OnSearchMessageCancelButtonClick(self, event): Laurent@981: self.CurrentSearchValue = "" Laurent@981: self.SearchMessage.SetValue("") Laurent@981: self.ResetMessagePanel() Laurent@981: event.Skip() Edouard@1441: Laurent@1093: def OnCleanButton(self, event): andrej@1673: if self.LogSource is not None and not self.IsPLCLogEmpty(): Laurent@1093: self.LogSource.ResetLogCount() Laurent@1093: self.ResetLogMessages() Laurent@1093: self.RefreshView() Laurent@1093: event.Skip() Edouard@1441: Laurent@981: def GenerateOnDurationButton(self, duration): Laurent@983: def OnDurationButton(): Laurent@982: self.ScrollMessagePanelByTimestamp(duration) Laurent@981: return OnDurationButton Edouard@1441: Laurent@993: def GetCopyMessageToClipboardFunction(self, message): Laurent@993: def CopyMessageToClipboardFunction(event): Laurent@993: self.ParentWindow.SetCopyBuffer(message.GetFullText()) Laurent@993: return CopyMessageToClipboardFunction Edouard@1441: Laurent@993: def GetMessageByScreenPos(self, posx, posy): Laurent@983: if self.CurrentMessage is not None: andrej@1847: _width, height = self.MessagePanel.GetClientSize() Laurent@986: message_idx = self.CurrentMessage Laurent@986: message = self.LogMessages[message_idx] Laurent@986: draw_date = True Laurent@986: offset = 5 Edouard@1441: Laurent@986: while offset < height and message is not None: Laurent@986: if draw_date: Laurent@986: offset += DATE_INFO_SIZE Edouard@1441: Laurent@986: if offset <= posy < offset + MESSAGE_INFO_SIZE: Laurent@993: return message Edouard@1441: Laurent@986: offset += MESSAGE_INFO_SIZE Edouard@1441: Laurent@986: previous_message, message_idx = self.GetPreviousMessage(message_idx) Laurent@986: if previous_message is not None: Laurent@986: draw_date = message.Date != previous_message.Date Laurent@986: message = previous_message Laurent@993: return None Edouard@1441: Laurent@993: def OnMessagePanelLeftUp(self, event): Laurent@993: if self.CurrentMessage is not None: Laurent@993: posx, posy = event.GetPosition() Laurent@993: for button in self.LeftButtons + self.RightButtons: Laurent@993: if button.HitTest(posx, posy): Laurent@993: button.ProcessCallback() Laurent@993: break Laurent@993: event.Skip() Edouard@1441: Laurent@993: def OnMessagePanelRightUp(self, event): Laurent@993: message = self.GetMessageByScreenPos(*event.GetPosition()) Laurent@993: if message is not None: Laurent@993: menu = wx.Menu(title='') Edouard@1441: Edouard@2737: menu_entry = menu.Append(help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL, text=_("Copy")) Edouard@2737: self.Bind(wx.EVT_MENU, self.GetCopyMessageToClipboardFunction(message), menu_entry) Edouard@1441: Laurent@993: self.MessagePanel.PopupMenu(menu) Laurent@993: menu.Destroy() Laurent@993: event.Skip() Edouard@1441: Laurent@993: def OnMessagePanelLeftDCLick(self, event): Laurent@993: message = self.GetMessageByScreenPos(*event.GetPosition()) Laurent@993: if message is not None: Laurent@993: self.SearchMessage.SetFocus() Laurent@993: self.SearchMessage.SetValue(message.Message) Laurent@993: event.Skip() Edouard@1441: Laurent@993: def ResetMessageToolTip(self): Laurent@993: if self.MessageToolTip is not None: Laurent@993: self.MessageToolTip.Destroy() Laurent@993: self.MessageToolTip = None Edouard@1441: Laurent@993: def OnMessageToolTipTimer(self, event): Laurent@993: if self.LastMousePos is not None: Laurent@993: message = self.GetMessageByScreenPos(*self.LastMousePos) Laurent@993: if message is not None: Laurent@993: tooltip_pos = self.MessagePanel.ClientToScreen(self.LastMousePos) Laurent@993: tooltip_pos.x += 10 Laurent@993: tooltip_pos.y += 10 Laurent@1169: self.MessageToolTip = CustomToolTip(self.MessagePanel, message.GetFullText(), False) Laurent@993: self.MessageToolTip.SetFont(self.Font) Laurent@1170: self.MessageToolTip.SetToolTipPosition(tooltip_pos) Laurent@993: self.MessageToolTip.Show() Laurent@993: event.Skip() Edouard@1441: Laurent@993: def OnMessagePanelMotion(self, event): Laurent@993: if not event.Dragging(): Laurent@993: self.ResetMessageToolTip() Laurent@993: self.LastMousePos = event.GetPosition() Laurent@993: self.MessageToolTipTimer.Start(int(TOOLTIP_WAIT_PERIOD * 1000), oneShot=True) Laurent@993: event.Skip() Edouard@1441: Laurent@993: def OnMessagePanelLeaveWindow(self, event): Laurent@993: self.ResetMessageToolTip() Laurent@993: self.LastMousePos = None Laurent@993: self.MessageToolTipTimer.Stop() Laurent@986: event.Skip() Edouard@1441: Laurent@981: def OnMessagePanelMouseWheel(self, event): andrej@2437: self.ScrollMessagePanel(event.GetWheelRotation() // event.GetWheelDelta()) Laurent@981: event.Skip() Edouard@1441: Laurent@988: def OnMessagePanelEraseBackground(self, event): Laurent@988: pass Edouard@1441: Laurent@981: def OnMessagePanelPaint(self, event): Laurent@981: self.RefreshView() Laurent@981: event.Skip() Edouard@1441: Laurent@981: def OnMessagePanelResize(self, event): andrej@1847: width, _height = self.MessagePanel.GetClientSize() Laurent@984: offset = 2 Laurent@984: for button in self.LeftButtons: Laurent@984: button.SetPosition(offset, 2) andrej@1847: w, _h = button.GetSize() Laurent@984: offset += w + 2 Laurent@984: offset = width - 2 Laurent@984: for button in self.RightButtons: andrej@1847: w, _h = button.GetSize() Laurent@984: button.SetPosition(offset - w, 2) Laurent@984: offset -= w + 2 Laurent@981: if self.IsMessagePanelBottom(): Laurent@981: self.ScrollToFirst() Laurent@981: else: Laurent@981: self.RefreshView() Laurent@981: event.Skip() Edouard@1441: Laurent@981: def OnScrollTimer(self, event): Laurent@981: if self.ScrollSpeed != 0.: Laurent@981: speed_norm = abs(self.ScrollSpeed) Laurent@981: period = REFRESH_PERIOD / speed_norm Laurent@981: self.ScrollMessagePanel(-speed_norm / self.ScrollSpeed) Laurent@981: self.LastStartTime = gettime() Laurent@981: self.ScrollTimer.Start(int(period * 1000), True) Laurent@981: event.Skip() Edouard@1441: Laurent@981: def SetScrollSpeed(self, speed): Laurent@981: if speed == 0.: Laurent@981: self.ScrollTimer.Stop() Laurent@981: else: Laurent@981: speed_norm = abs(speed) Laurent@981: period = REFRESH_PERIOD / speed_norm Laurent@981: current_time = gettime() Laurent@981: if self.LastStartTime is not None: Laurent@981: elapsed_time = current_time - self.LastStartTime Laurent@981: if elapsed_time > period: Laurent@981: self.ScrollMessagePanel(-speed_norm / speed) Laurent@981: self.LastStartTime = current_time Laurent@981: else: Laurent@981: period -= elapsed_time Laurent@981: else: Laurent@981: self.LastStartTime = current_time Laurent@981: self.ScrollTimer.Start(int(period * 1000), True) Edouard@1441: self.ScrollSpeed = speed Edouard@1441: Laurent@1071: def ScrollToLast(self, refresh=True): Laurent@981: if len(self.LogMessages) > 0: Laurent@981: self.CurrentMessage = len(self.LogMessages) - 1 Laurent@981: message = self.LogMessages[self.CurrentMessage] Laurent@981: if not self.FilterLogMessage(message): Laurent@981: message, self.CurrentMessage = self.GetPreviousMessage(self.CurrentMessage) Laurent@1071: if refresh: Laurent@1071: self.RefreshView() Laurent@981: Laurent@981: def ScrollToFirst(self): Laurent@978: if len(self.LogMessages) > 0: Laurent@978: message_idx = 0 Laurent@978: message = self.LogMessages[message_idx] Laurent@981: if not self.FilterLogMessage(message): Laurent@981: next_message, msgidx = self.GetNextMessage(message_idx) Laurent@978: if next_message is not None: Laurent@978: message_idx = msgidx Laurent@978: message = next_message Laurent@978: while message is not None: Laurent@981: message, msgidx = self.GetPreviousMessage(message_idx) Laurent@978: if message is not None: Laurent@978: message_idx = msgidx Laurent@978: message = self.LogMessages[message_idx] Laurent@981: if self.FilterLogMessage(message): Laurent@978: while message is not None: Laurent@981: message, msgidx = self.GetNextMessage(message_idx) Laurent@978: if message is not None: Laurent@978: if not self.IsMessagePanelBottom(msgidx): Laurent@978: break Laurent@978: message_idx = msgidx Laurent@978: self.CurrentMessage = message_idx Laurent@978: else: Laurent@978: self.CurrentMessage = None Laurent@978: self.RefreshView()