Esempio n. 1
0
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()
Esempio n. 2
0
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