controls/DebugVariablePanel/DebugVariableGraphicPanel.py
changeset 1364 e9e17d3b2849
parent 1363 e87e0166d0a7
child 1365 debc97102b23
equal deleted inserted replaced
1363:e87e0166d0a7 1364:e9e17d3b2849
     1 #!/usr/bin/env python
       
     2 # -*- coding: utf-8 -*-
       
     3 
       
     4 #This file is part of PLCOpenEditor, a library implementing an IEC 61131-3 editor
       
     5 #based on the plcopen standard. 
       
     6 #
       
     7 #Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD
       
     8 #
       
     9 #See COPYING file for copyrights details.
       
    10 #
       
    11 #This library is free software; you can redistribute it and/or
       
    12 #modify it under the terms of the GNU General Public
       
    13 #License as published by the Free Software Foundation; either
       
    14 #version 2.1 of the License, or (at your option) any later version.
       
    15 #
       
    16 #This library is distributed in the hope that it will be useful,
       
    17 #but WITHOUT ANY WARRANTY; without even the implied warranty of
       
    18 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
       
    19 #General Public License for more details.
       
    20 #
       
    21 #You should have received a copy of the GNU General Public
       
    22 #License along with this library; if not, write to the Free Software
       
    23 #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
       
    24 
       
    25 from types import TupleType
       
    26 import math
       
    27 import numpy
       
    28 
       
    29 import wx
       
    30 import wx.lib.buttons
       
    31 
       
    32 import matplotlib
       
    33 import matplotlib.pyplot
       
    34 from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap
       
    35 
       
    36 from editors.DebugViewer import DebugViewer
       
    37 from util.BitmapLibrary import GetBitmap
       
    38 
       
    39 from DebugVariableItem import DebugVariableItem
       
    40 from DebugVariableTextViewer import DebugVariableTextViewer
       
    41 from DebugVariableGraphicViewer import *
       
    42 
       
    43 MILLISECOND = 1000000       # Number of nanosecond in a millisecond
       
    44 SECOND = 1000 * MILLISECOND # Number of nanosecond in a second
       
    45 MINUTE = 60 * SECOND        # Number of nanosecond in a minute
       
    46 HOUR = 60 * MINUTE          # Number of nanosecond in a hour
       
    47 DAY = 24 * HOUR             # Number of nanosecond in a day
       
    48 
       
    49 # List of values possible for graph range
       
    50 # Format is [(time_in_plain_text, value_in_nanosecond),...]
       
    51 RANGE_VALUES = \
       
    52     [("%dms" % i, i * MILLISECOND) for i in (10, 20, 50, 100, 200, 500)] + \
       
    53     [("%ds" % i, i * SECOND) for i in (1, 2, 5, 10, 20, 30)] + \
       
    54     [("%dm" % i, i * MINUTE) for i in (1, 2, 5, 10, 20, 30)] + \
       
    55     [("%dh" % i, i * HOUR) for i in (1, 2, 3, 6, 12, 24)]
       
    56 
       
    57 # Scrollbar increment in pixel
       
    58 SCROLLBAR_UNIT = 10
       
    59 
       
    60 def compute_mask(x, y):
       
    61     return [(xp if xp == yp else "*")
       
    62             for xp, yp in zip(x, y)]
       
    63 
       
    64 def NextTick(variables):
       
    65     next_tick = None
       
    66     for item, data in variables:
       
    67         if len(data) == 0:
       
    68             continue
       
    69         
       
    70         next_tick = (data[0][0]
       
    71                      if next_tick is None
       
    72                      else min(next_tick, data[0][0]))
       
    73     
       
    74     return next_tick
       
    75 
       
    76 #-------------------------------------------------------------------------------
       
    77 #                    Debug Variable Graphic Panel Drop Target
       
    78 #-------------------------------------------------------------------------------
       
    79 
       
    80 """
       
    81 Class that implements a custom drop target class for Debug Variable Graphic
       
    82 Panel
       
    83 """
       
    84 
       
    85 class DebugVariableDropTarget(wx.TextDropTarget):
       
    86     
       
    87     def __init__(self, window):
       
    88         """
       
    89         Constructor
       
    90         @param window: Reference to the Debug Variable Panel
       
    91         """
       
    92         wx.TextDropTarget.__init__(self)
       
    93         self.ParentWindow = window
       
    94     
       
    95     def __del__(self):
       
    96         """
       
    97         Destructor
       
    98         """
       
    99         # Remove reference to Debug Variable Panel
       
   100         self.ParentWindow = None
       
   101     
       
   102     def OnDragOver(self, x, y, d):
       
   103         """
       
   104         Function called when mouse is dragged over Drop Target
       
   105         @param x: X coordinate of mouse pointer
       
   106         @param y: Y coordinate of mouse pointer
       
   107         @param d: Suggested default for return value
       
   108         """
       
   109        # Signal Debug Variable Panel to refresh highlight giving mouse position
       
   110         self.ParentWindow.RefreshHighlight(x, y)
       
   111         return wx.TextDropTarget.OnDragOver(self, x, y, d)
       
   112         
       
   113     def OnDropText(self, x, y, data):
       
   114         """
       
   115         Function called when mouse is released in Drop Target
       
   116         @param x: X coordinate of mouse pointer
       
   117         @param y: Y coordinate of mouse pointer
       
   118         @param data: Text associated to drag'n drop
       
   119         """
       
   120         # Signal Debug Variable Panel to reset highlight
       
   121         self.ParentWindow.ResetHighlight()
       
   122         
       
   123         message = None
       
   124         
       
   125         # Check that data is valid regarding DebugVariablePanel
       
   126         try:
       
   127             values = eval(data)
       
   128             if not isinstance(values, TupleType):
       
   129                 raise ValueError
       
   130         except:
       
   131             message = _("Invalid value \"%s\" for debug variable")%data
       
   132             values = None
       
   133             
       
   134         # Display message if data is invalid
       
   135         if message is not None:
       
   136             wx.CallAfter(self.ShowMessage, message)
       
   137         
       
   138         # Data contain a reference to a variable to debug
       
   139         elif values[1] == "debug":
       
   140             
       
   141             # Drag'n Drop is an internal is an internal move inside Debug
       
   142             # Variable Panel 
       
   143             if len(values) > 2 and values[2] == "move":
       
   144                 self.ParentWindow.MoveValue(values[0])
       
   145             
       
   146             # Drag'n Drop was initiated by another control of Beremiz
       
   147             else:
       
   148                 self.ParentWindow.InsertValue(values[0], force=True)
       
   149     
       
   150     def OnLeave(self):
       
   151         """
       
   152         Function called when mouse is leave Drop Target
       
   153         """
       
   154         # Signal Debug Variable Panel to reset highlight
       
   155         self.ParentWindow.ResetHighlight()
       
   156         return wx.TextDropTarget.OnLeave(self)
       
   157     
       
   158     def ShowMessage(self, message):
       
   159         """
       
   160         Show error message in Error Dialog
       
   161         @param message: Error message to display
       
   162         """
       
   163         dialog = wx.MessageDialog(self.ParentWindow, 
       
   164                                   message, 
       
   165                                   _("Error"), 
       
   166                                   wx.OK|wx.ICON_ERROR)
       
   167         dialog.ShowModal()
       
   168         dialog.Destroy()
       
   169 
       
   170 
       
   171 #-------------------------------------------------------------------------------
       
   172 #                      Debug Variable Graphic Panel Class
       
   173 #-------------------------------------------------------------------------------
       
   174 
       
   175 """
       
   176 Class that implements a Viewer that display variable values as a graphs
       
   177 """
       
   178 
       
   179 class DebugVariableGraphicPanel(wx.Panel, DebugViewer):
       
   180     
       
   181     def __init__(self, parent, producer, window):
       
   182         """
       
   183         Constructor
       
   184         @param parent: Reference to the parent wx.Window
       
   185         @param producer: Object receiving debug value and dispatching them to
       
   186         consumers
       
   187         @param window: Reference to Beremiz frame
       
   188         """
       
   189         wx.Panel.__init__(self, parent, style=wx.SP_3D|wx.TAB_TRAVERSAL)
       
   190         
       
   191         # Save Reference to Beremiz frame
       
   192         self.ParentWindow = window
       
   193         
       
   194         # Variable storing flag indicating that variable displayed in table
       
   195         # received new value and then table need to be refreshed
       
   196         self.HasNewData = False
       
   197         
       
   198         # Variable storing flag indicating that refresh has been forced, and
       
   199         # that next time refresh is possible, it will be done even if no new
       
   200         # data is available
       
   201         self.Force = False
       
   202         
       
   203         self.SetBackgroundColour(wx.WHITE)
       
   204         
       
   205         main_sizer = wx.BoxSizer(wx.VERTICAL)
       
   206         
       
   207         self.Ticks = numpy.array([]) # List of tick received
       
   208         self.StartTick = 0           # Tick starting range of data displayed
       
   209         self.Fixed = False           # Flag that range of data is fixed
       
   210         self.CursorTick = None       # Tick of cursor for displaying values
       
   211         
       
   212         self.DraggingAxesPanel = None
       
   213         self.DraggingAxesBoundingBox = None
       
   214         self.DraggingAxesMousePos = None
       
   215         self.VetoScrollEvent = False
       
   216         
       
   217         self.VariableNameMask = []
       
   218         
       
   219         self.GraphicPanels = []
       
   220         
       
   221         graphics_button_sizer = wx.BoxSizer(wx.HORIZONTAL)
       
   222         main_sizer.AddSizer(graphics_button_sizer, border=5, flag=wx.GROW|wx.ALL)
       
   223         
       
   224         range_label = wx.StaticText(self, label=_('Range:'))
       
   225         graphics_button_sizer.AddWindow(range_label, flag=wx.ALIGN_CENTER_VERTICAL)
       
   226         
       
   227         self.CanvasRange = wx.ComboBox(self, style=wx.CB_READONLY)
       
   228         self.Bind(wx.EVT_COMBOBOX, self.OnRangeChanged, self.CanvasRange)
       
   229         graphics_button_sizer.AddWindow(self.CanvasRange, 1, 
       
   230               border=5, flag=wx.LEFT|wx.ALIGN_CENTER_VERTICAL)
       
   231         
       
   232         self.CanvasRange.Clear()
       
   233         default_range_idx = 0
       
   234         for idx, (text, value) in enumerate(RANGE_VALUES):
       
   235             self.CanvasRange.Append(text)
       
   236             if text == "1s":
       
   237                 default_range_idx = idx
       
   238         self.CanvasRange.SetSelection(default_range_idx)
       
   239         
       
   240         for name, bitmap, help in [
       
   241             ("CurrentButton", "current", _("Go to current value")),
       
   242             ("ExportGraphButton", "export_graph", _("Export graph values to clipboard"))]:
       
   243             button = wx.lib.buttons.GenBitmapButton(self, 
       
   244                   bitmap=GetBitmap(bitmap), 
       
   245                   size=wx.Size(28, 28), style=wx.NO_BORDER)
       
   246             button.SetToolTipString(help)
       
   247             setattr(self, name, button)
       
   248             self.Bind(wx.EVT_BUTTON, getattr(self, "On" + name), button)
       
   249             graphics_button_sizer.AddWindow(button, border=5, flag=wx.LEFT)
       
   250         
       
   251         self.CanvasPosition = wx.ScrollBar(self, 
       
   252               size=wx.Size(0, 16), style=wx.SB_HORIZONTAL)
       
   253         self.CanvasPosition.Bind(wx.EVT_SCROLL_THUMBTRACK, 
       
   254               self.OnPositionChanging, self.CanvasPosition)
       
   255         self.CanvasPosition.Bind(wx.EVT_SCROLL_LINEUP, 
       
   256               self.OnPositionChanging, self.CanvasPosition)
       
   257         self.CanvasPosition.Bind(wx.EVT_SCROLL_LINEDOWN, 
       
   258               self.OnPositionChanging, self.CanvasPosition)
       
   259         self.CanvasPosition.Bind(wx.EVT_SCROLL_PAGEUP, 
       
   260               self.OnPositionChanging, self.CanvasPosition)
       
   261         self.CanvasPosition.Bind(wx.EVT_SCROLL_PAGEDOWN, 
       
   262               self.OnPositionChanging, self.CanvasPosition)
       
   263         main_sizer.AddWindow(self.CanvasPosition, border=5, flag=wx.GROW|wx.LEFT|wx.RIGHT|wx.BOTTOM)
       
   264         
       
   265         self.TickSizer = wx.BoxSizer(wx.HORIZONTAL)
       
   266         main_sizer.AddSizer(self.TickSizer, border=5, flag=wx.ALL|wx.GROW)
       
   267         
       
   268         self.TickLabel = wx.StaticText(self)
       
   269         self.TickSizer.AddWindow(self.TickLabel, border=5, flag=wx.RIGHT)
       
   270         
       
   271         self.MaskLabel = wx.TextCtrl(self, style=wx.TE_READONLY|wx.TE_CENTER|wx.NO_BORDER)
       
   272         self.TickSizer.AddWindow(self.MaskLabel, 1, border=5, flag=wx.RIGHT|wx.GROW)
       
   273         
       
   274         self.TickTimeLabel = wx.StaticText(self)
       
   275         self.TickSizer.AddWindow(self.TickTimeLabel)
       
   276         
       
   277         self.GraphicsWindow = wx.ScrolledWindow(self, style=wx.HSCROLL|wx.VSCROLL)
       
   278         self.GraphicsWindow.SetBackgroundColour(wx.WHITE)
       
   279         self.GraphicsWindow.SetDropTarget(DebugVariableDropTarget(self))
       
   280         self.GraphicsWindow.Bind(wx.EVT_ERASE_BACKGROUND, self.OnGraphicsWindowEraseBackground)
       
   281         self.GraphicsWindow.Bind(wx.EVT_PAINT, self.OnGraphicsWindowPaint)
       
   282         self.GraphicsWindow.Bind(wx.EVT_SIZE, self.OnGraphicsWindowResize)
       
   283         self.GraphicsWindow.Bind(wx.EVT_MOUSEWHEEL, self.OnGraphicsWindowMouseWheel)
       
   284         
       
   285         main_sizer.AddWindow(self.GraphicsWindow, 1, flag=wx.GROW)
       
   286         
       
   287         self.GraphicsSizer = wx.BoxSizer(wx.VERTICAL)
       
   288         self.GraphicsWindow.SetSizer(self.GraphicsSizer)
       
   289     
       
   290         DebugViewer.__init__(self, producer, True)
       
   291         
       
   292         self.SetSizer(main_sizer)
       
   293     
       
   294     def SetTickTime(self, ticktime=0):
       
   295         """
       
   296         Set Ticktime for calculate data range according to time range selected
       
   297         @param ticktime: Ticktime to apply to range (default: 0)
       
   298         """
       
   299         # Save ticktime
       
   300         self.Ticktime = ticktime
       
   301         
       
   302         # Set ticktime to millisecond if undefined
       
   303         if self.Ticktime == 0:
       
   304             self.Ticktime = MILLISECOND
       
   305         
       
   306         # Calculate range to apply to data
       
   307         self.CurrentRange = RANGE_VALUES[
       
   308             self.CanvasRange.GetSelection()][1] / self.Ticktime
       
   309     
       
   310     def SetDataProducer(self, producer):
       
   311         """
       
   312         Set Data Producer
       
   313         @param producer: Data Producer
       
   314         """
       
   315         DebugViewer.SetDataProducer(self, producer)
       
   316         
       
   317         # Set ticktime if data producer is available
       
   318         if self.DataProducer is not None:
       
   319             self.SetTickTime(self.DataProducer.GetTicktime())
       
   320     
       
   321     def RefreshNewData(self, *args, **kwargs):
       
   322         """
       
   323         Called to refresh Panel according to values received by variables
       
   324         Can receive any parameters (not used here)
       
   325         """
       
   326         # Refresh graphs if new data is available or refresh is forced
       
   327         if self.HasNewData or self.Force:
       
   328             self.HasNewData = False
       
   329             self.RefreshView()
       
   330         
       
   331         DebugViewer.RefreshNewData(self, *args, **kwargs)
       
   332     
       
   333     def NewDataAvailable(self, ticks, *args, **kwargs):
       
   334         """
       
   335         Called by DataProducer for each tick captured or by panel to refresh
       
   336         graphs
       
   337         @param tick: PLC tick captured
       
   338         All other parameters are passed to refresh function 
       
   339         """
       
   340         # If tick given
       
   341         if ticks is not None:
       
   342             tick = ticks[-1]
       
   343             
       
   344             # Save tick as start tick for range if data is still empty
       
   345             if len(self.Ticks) == 0:
       
   346                 self.StartTick = ticks[0]
       
   347             
       
   348             # Add tick to list of ticks received
       
   349             self.Ticks = numpy.append(self.Ticks, ticks)
       
   350             
       
   351             # Update start tick for range if range follow ticks received
       
   352             if not self.Fixed or tick < self.StartTick + self.CurrentRange:
       
   353                 self.StartTick = max(self.StartTick, tick - self.CurrentRange)
       
   354             
       
   355             # Force refresh if graph is fixed because range of data received
       
   356             # is too small to fill data range selected
       
   357             if self.Fixed and \
       
   358                self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
       
   359                 self.Force = True
       
   360             
       
   361             self.HasNewData = False
       
   362             self.RefreshView()
       
   363             
       
   364         else:
       
   365             DebugViewer.NewDataAvailable(self, ticks, *args, **kwargs)
       
   366     
       
   367     def ForceRefresh(self):
       
   368         """
       
   369         Called to force refresh of graphs
       
   370         """
       
   371         self.Force = True
       
   372         wx.CallAfter(self.NewDataAvailable, None, True)
       
   373     
       
   374     def SetCursorTick(self, cursor_tick):
       
   375         """
       
   376         Set Cursor for displaying values of items at a tick given
       
   377         @param cursor_tick: Tick of cursor
       
   378         """
       
   379         # Save cursor tick
       
   380         self.CursorTick = cursor_tick
       
   381         self.Fixed = cursor_tick is not None
       
   382         self.UpdateCursorTick() 
       
   383     
       
   384     def MoveCursorTick(self, move):
       
   385         if self.CursorTick is not None:
       
   386             cursor_tick = max(self.Ticks[0], 
       
   387                           min(self.CursorTick + move, 
       
   388                               self.Ticks[-1]))
       
   389             cursor_tick_idx = numpy.argmin(numpy.abs(self.Ticks - cursor_tick))
       
   390             if self.Ticks[cursor_tick_idx] == self.CursorTick:
       
   391                 cursor_tick_idx = max(0, 
       
   392                                   min(cursor_tick_idx + abs(move) / move, 
       
   393                                       len(self.Ticks) - 1))
       
   394             self.CursorTick = self.Ticks[cursor_tick_idx]
       
   395             self.StartTick = max(self.Ticks[
       
   396                                 numpy.argmin(numpy.abs(self.Ticks - 
       
   397                                         self.CursorTick + self.CurrentRange))],
       
   398                              min(self.StartTick, self.CursorTick))
       
   399             self.RefreshCanvasPosition()
       
   400             self.UpdateCursorTick() 
       
   401             
       
   402     def ResetCursorTick(self):
       
   403         self.CursorTick = None
       
   404         self.Fixed = False
       
   405         self.UpdateCursorTick()
       
   406     
       
   407     def UpdateCursorTick(self):
       
   408         for panel in self.GraphicPanels:
       
   409             if isinstance(panel, DebugVariableGraphicViewer):
       
   410                 panel.SetCursorTick(self.CursorTick)
       
   411         self.ForceRefresh()
       
   412     
       
   413     def StartDragNDrop(self, panel, item, x_mouse, y_mouse, x_mouse_start, y_mouse_start):
       
   414         if len(panel.GetItems()) > 1:
       
   415             self.DraggingAxesPanel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL)
       
   416             self.DraggingAxesPanel.SetCursorTick(self.CursorTick)
       
   417             width, height = panel.GetSize()
       
   418             self.DraggingAxesPanel.SetSize(wx.Size(width, height))
       
   419             self.DraggingAxesPanel.ResetGraphics()
       
   420             self.DraggingAxesPanel.SetPosition(wx.Point(0, -height))
       
   421         else:
       
   422             self.DraggingAxesPanel = panel
       
   423         self.DraggingAxesBoundingBox = panel.GetAxesBoundingBox(parent_coordinate=True)
       
   424         self.DraggingAxesMousePos = wx.Point(
       
   425             x_mouse_start - self.DraggingAxesBoundingBox.x, 
       
   426             y_mouse_start - self.DraggingAxesBoundingBox.y)
       
   427         self.MoveDragNDrop(x_mouse, y_mouse)
       
   428         
       
   429     def MoveDragNDrop(self, x_mouse, y_mouse):
       
   430         self.DraggingAxesBoundingBox.x = x_mouse - self.DraggingAxesMousePos.x
       
   431         self.DraggingAxesBoundingBox.y = y_mouse - self.DraggingAxesMousePos.y
       
   432         self.RefreshHighlight(x_mouse, y_mouse)
       
   433     
       
   434     def RefreshHighlight(self, x_mouse, y_mouse):
       
   435         for idx, panel in enumerate(self.GraphicPanels):
       
   436             x, y = panel.GetPosition()
       
   437             width, height = panel.GetSize()
       
   438             rect = wx.Rect(x, y, width, height)
       
   439             if (rect.InsideXY(x_mouse, y_mouse) or 
       
   440                 idx == 0 and y_mouse < 0 or
       
   441                 idx == len(self.GraphicPanels) - 1 and y_mouse > panel.GetPosition()[1]):
       
   442                 panel.RefreshHighlight(x_mouse - x, y_mouse - y)
       
   443             else:
       
   444                 panel.SetHighlight(HIGHLIGHT_NONE)
       
   445         if wx.Platform == "__WXMSW__":
       
   446             self.RefreshView()
       
   447         else:
       
   448             self.ForceRefresh()
       
   449     
       
   450     def ResetHighlight(self):
       
   451         for panel in self.GraphicPanels:
       
   452             panel.SetHighlight(HIGHLIGHT_NONE)
       
   453         if wx.Platform == "__WXMSW__":
       
   454             self.RefreshView()
       
   455         else:
       
   456             self.ForceRefresh()
       
   457     
       
   458     def IsDragging(self):
       
   459         return self.DraggingAxesPanel is not None
       
   460     
       
   461     def GetDraggingAxesClippingRegion(self, panel):
       
   462         x, y = panel.GetPosition()
       
   463         width, height = panel.GetSize()
       
   464         bbox = wx.Rect(x, y, width, height)
       
   465         bbox = bbox.Intersect(self.DraggingAxesBoundingBox)
       
   466         bbox.x -= x
       
   467         bbox.y -= y
       
   468         return bbox
       
   469     
       
   470     def GetDraggingAxesPosition(self, panel):
       
   471         x, y = panel.GetPosition()
       
   472         return wx.Point(self.DraggingAxesBoundingBox.x - x,
       
   473                         self.DraggingAxesBoundingBox.y - y)
       
   474     
       
   475     def StopDragNDrop(self, variable, x_mouse, y_mouse):
       
   476         if self.DraggingAxesPanel not in self.GraphicPanels:
       
   477             self.DraggingAxesPanel.Destroy()
       
   478         self.DraggingAxesPanel = None
       
   479         self.DraggingAxesBoundingBox = None
       
   480         self.DraggingAxesMousePos = None
       
   481         for idx, panel in enumerate(self.GraphicPanels):
       
   482             panel.SetHighlight(HIGHLIGHT_NONE)
       
   483             xw, yw = panel.GetPosition()
       
   484             width, height = panel.GetSize()
       
   485             bbox = wx.Rect(xw, yw, width, height)
       
   486             if bbox.InsideXY(x_mouse, y_mouse):
       
   487                 panel.ShowButtons(True)
       
   488                 merge_type = GRAPH_PARALLEL
       
   489                 if isinstance(panel, DebugVariableTextViewer) or panel.Is3DCanvas():
       
   490                     if y_mouse > yw + height / 2:
       
   491                         idx += 1
       
   492                     wx.CallAfter(self.MoveValue, variable, idx, True)
       
   493                 else:
       
   494                     rect = panel.GetAxesBoundingBox(True)
       
   495                     if rect.InsideXY(x_mouse, y_mouse):
       
   496                         merge_rect = wx.Rect(rect.x, rect.y, rect.width / 2., rect.height)
       
   497                         if merge_rect.InsideXY(x_mouse, y_mouse):
       
   498                             merge_type = GRAPH_ORTHOGONAL
       
   499                         wx.CallAfter(self.MergeGraphs, variable, idx, merge_type, force=True)
       
   500                     else:
       
   501                         if y_mouse > yw + height / 2:
       
   502                             idx += 1
       
   503                         wx.CallAfter(self.MoveValue, variable, idx, True)
       
   504                 self.ForceRefresh()
       
   505                 return 
       
   506         width, height = self.GraphicsWindow.GetVirtualSize()
       
   507         rect = wx.Rect(0, 0, width, height)
       
   508         if rect.InsideXY(x_mouse, y_mouse):
       
   509             wx.CallAfter(self.MoveValue, variable, len(self.GraphicPanels), True)
       
   510         self.ForceRefresh()
       
   511     
       
   512     def RefreshGraphicsSizer(self):
       
   513         self.GraphicsSizer.Clear()
       
   514         
       
   515         for panel in self.GraphicPanels:
       
   516             self.GraphicsSizer.AddWindow(panel, flag=wx.GROW)
       
   517             
       
   518         self.GraphicsSizer.Layout()
       
   519         self.RefreshGraphicsWindowScrollbars()
       
   520     
       
   521     def RefreshView(self):
       
   522         self.RefreshCanvasPosition()
       
   523         
       
   524         width, height = self.GraphicsWindow.GetVirtualSize()
       
   525         bitmap = wx.EmptyBitmap(width, height)
       
   526         dc = wx.BufferedDC(wx.ClientDC(self.GraphicsWindow), bitmap)
       
   527         dc.Clear()
       
   528         dc.BeginDrawing()
       
   529         if self.DraggingAxesPanel is not None:
       
   530             destBBox = self.DraggingAxesBoundingBox
       
   531             srcBBox = self.DraggingAxesPanel.GetAxesBoundingBox()
       
   532             
       
   533             srcBmp = _convert_agg_to_wx_bitmap(self.DraggingAxesPanel.get_renderer(), None)
       
   534             srcDC = wx.MemoryDC()
       
   535             srcDC.SelectObject(srcBmp)
       
   536                 
       
   537             dc.Blit(destBBox.x, destBBox.y, 
       
   538                     int(destBBox.width), int(destBBox.height), 
       
   539                     srcDC, srcBBox.x, srcBBox.y)
       
   540         dc.EndDrawing()
       
   541         
       
   542         if not self.Fixed or self.Force:
       
   543             self.Force = False
       
   544             refresh_graphics = True
       
   545         else:
       
   546             refresh_graphics = False
       
   547         
       
   548         if self.DraggingAxesPanel is not None and self.DraggingAxesPanel not in self.GraphicPanels:
       
   549             self.DraggingAxesPanel.RefreshViewer(refresh_graphics)
       
   550         for panel in self.GraphicPanels:
       
   551             if isinstance(panel, DebugVariableGraphicViewer):
       
   552                 panel.RefreshViewer(refresh_graphics)
       
   553             else:
       
   554                 panel.RefreshViewer()
       
   555         
       
   556         if self.CursorTick is not None:
       
   557             tick = self.CursorTick
       
   558         elif len(self.Ticks) > 0:
       
   559             tick = self.Ticks[-1]
       
   560         else:
       
   561             tick = None
       
   562         if tick is not None:
       
   563             self.TickLabel.SetLabel("Tick: %d" % tick)
       
   564             tick_duration = int(tick * self.Ticktime)
       
   565             not_null = False
       
   566             duration = ""
       
   567             for value, format in [(tick_duration / DAY, "%dd"),
       
   568                                   ((tick_duration % DAY) / HOUR, "%dh"),
       
   569                                   ((tick_duration % HOUR) / MINUTE, "%dm"),
       
   570                                   ((tick_duration % MINUTE) / SECOND, "%ds")]:
       
   571                 
       
   572                 if value > 0 or not_null:
       
   573                     duration += format % value
       
   574                     not_null = True
       
   575             
       
   576             duration += "%gms" % (float(tick_duration % SECOND) / MILLISECOND) 
       
   577             self.TickTimeLabel.SetLabel("t: %s" % duration)
       
   578         else:
       
   579             self.TickLabel.SetLabel("")
       
   580             self.TickTimeLabel.SetLabel("")
       
   581         self.TickSizer.Layout()
       
   582     
       
   583     def SubscribeAllDataConsumers(self):
       
   584         DebugViewer.SubscribeAllDataConsumers(self)
       
   585         
       
   586         if self.DataProducer is not None:
       
   587             if self.DataProducer is not None:
       
   588                 self.SetTickTime(self.DataProducer.GetTicktime())
       
   589         
       
   590         self.ResetCursorTick()
       
   591         
       
   592         for panel in self.GraphicPanels[:]:
       
   593             panel.SubscribeAllDataConsumers()
       
   594             if panel.ItemsIsEmpty():
       
   595                 if panel.HasCapture():
       
   596                     panel.ReleaseMouse()
       
   597                 self.GraphicPanels.remove(panel)
       
   598                 panel.Destroy()
       
   599         
       
   600         self.ResetVariableNameMask()
       
   601         self.RefreshGraphicsSizer()
       
   602         self.ForceRefresh()
       
   603     
       
   604     def ResetView(self):
       
   605         self.UnsubscribeAllDataConsumers()
       
   606         
       
   607         self.Fixed = False
       
   608         for panel in self.GraphicPanels:
       
   609             panel.Destroy()
       
   610         self.GraphicPanels = []
       
   611         self.ResetVariableNameMask()
       
   612         self.RefreshGraphicsSizer()
       
   613     
       
   614     def SetCanvasPosition(self, tick):
       
   615         tick = max(self.Ticks[0], min(tick, self.Ticks[-1] - self.CurrentRange))
       
   616         self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - tick))]
       
   617         self.Fixed = True
       
   618         self.RefreshCanvasPosition()
       
   619         self.ForceRefresh()
       
   620     
       
   621     def RefreshCanvasPosition(self):
       
   622         if len(self.Ticks) > 0:
       
   623             pos = int(self.StartTick - self.Ticks[0])
       
   624             range = int(self.Ticks[-1] - self.Ticks[0])
       
   625         else:
       
   626             pos = 0
       
   627             range = 0
       
   628         self.CanvasPosition.SetScrollbar(pos, self.CurrentRange, range, self.CurrentRange)
       
   629     
       
   630     def ChangeRange(self, dir, tick=None):
       
   631         current_range = self.CurrentRange
       
   632         current_range_idx = self.CanvasRange.GetSelection()
       
   633         new_range_idx = max(0, min(current_range_idx + dir, len(RANGE_VALUES) - 1))
       
   634         if new_range_idx != current_range_idx:
       
   635             self.CanvasRange.SetSelection(new_range_idx)
       
   636             self.CurrentRange = RANGE_VALUES[new_range_idx][1] / self.Ticktime
       
   637             if len(self.Ticks) > 0:
       
   638                 if tick is None:
       
   639                     tick = self.StartTick + self.CurrentRange / 2.
       
   640                 new_start_tick = min(tick - (tick - self.StartTick) * self.CurrentRange / current_range,
       
   641                                      self.Ticks[-1] - self.CurrentRange)
       
   642                 self.StartTick = self.Ticks[numpy.argmin(numpy.abs(self.Ticks - new_start_tick))]
       
   643                 self.Fixed = new_start_tick < self.Ticks[-1] - self.CurrentRange
       
   644             self.ForceRefresh()
       
   645     
       
   646     def RefreshRange(self):
       
   647         if len(self.Ticks) > 0:
       
   648             if self.Fixed and self.Ticks[-1] - self.Ticks[0] < self.CurrentRange:
       
   649                 self.Fixed = False
       
   650             if self.Fixed:
       
   651                 self.StartTick = min(self.StartTick, self.Ticks[-1] - self.CurrentRange)
       
   652             else:
       
   653                 self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange)
       
   654         self.ForceRefresh()
       
   655     
       
   656     def OnRangeChanged(self, event):
       
   657         try:
       
   658             self.CurrentRange = RANGE_VALUES[self.CanvasRange.GetSelection()][1] / self.Ticktime
       
   659         except ValueError, e:
       
   660             self.CanvasRange.SetValue(str(self.CurrentRange))
       
   661         wx.CallAfter(self.RefreshRange)
       
   662         event.Skip()
       
   663     
       
   664     def OnCurrentButton(self, event):
       
   665         if len(self.Ticks) > 0:
       
   666             self.StartTick = max(self.Ticks[0], self.Ticks[-1] - self.CurrentRange)
       
   667             self.ResetCursorTick()
       
   668         event.Skip()
       
   669     
       
   670     def CopyDataToClipboard(self, variables):
       
   671         text = "tick;%s;\n" % ";".join([item.GetVariable() for item, data in variables])
       
   672         next_tick = NextTick(variables)
       
   673         while next_tick is not None:
       
   674             values = []
       
   675             for item, data in variables:
       
   676                 if len(data) > 0:
       
   677                     if next_tick == data[0][0]:
       
   678                         var_type = item.GetVariableType()
       
   679                         if var_type in ["STRING", "WSTRING"]:
       
   680                             value = item.GetRawValue(int(data.pop(0)[2]))
       
   681                             if var_type == "STRING":
       
   682                                 values.append("'%s'" % value)
       
   683                             else:
       
   684                                 values.append('"%s"' % value)
       
   685                         else:
       
   686                             values.append("%.3f" % data.pop(0)[1])
       
   687                     else:
       
   688                         values.append("")
       
   689                 else:
       
   690                     values.append("")
       
   691             text += "%d;%s;\n" % (next_tick, ";".join(values))
       
   692             next_tick = NextTick(variables)
       
   693         self.ParentWindow.SetCopyBuffer(text)
       
   694     
       
   695     def OnExportGraphButton(self, event):
       
   696         items = reduce(lambda x, y: x + y,
       
   697                        [panel.GetItems() for panel in self.GraphicPanels],
       
   698                        [])
       
   699         variables = [(item, [entry for entry in item.GetData()])
       
   700                      for item in items
       
   701                      if item.IsNumVariable()]
       
   702         wx.CallAfter(self.CopyDataToClipboard, variables)
       
   703         event.Skip()
       
   704     
       
   705     def OnPositionChanging(self, event):
       
   706         if len(self.Ticks) > 0:
       
   707             self.StartTick = self.Ticks[0] + event.GetPosition()
       
   708             self.Fixed = True
       
   709             self.ForceRefresh()
       
   710         event.Skip()
       
   711     
       
   712     def GetRange(self):
       
   713         return self.StartTick, self.StartTick + self.CurrentRange
       
   714     
       
   715     def GetViewerIndex(self, viewer):
       
   716         if viewer in self.GraphicPanels:
       
   717             return self.GraphicPanels.index(viewer)
       
   718         return None
       
   719     
       
   720     def IsViewerFirst(self, viewer):
       
   721         return viewer == self.GraphicPanels[0]
       
   722     
       
   723     def HighlightPreviousViewer(self, viewer):
       
   724         if self.IsViewerFirst(viewer):
       
   725             return
       
   726         idx = self.GetViewerIndex(viewer)
       
   727         if idx is None:
       
   728             return
       
   729         self.GraphicPanels[idx-1].SetHighlight(HIGHLIGHT_AFTER)
       
   730     
       
   731     def ResetVariableNameMask(self):
       
   732         items = []
       
   733         for panel in self.GraphicPanels:
       
   734             items.extend(panel.GetItems())
       
   735         if len(items) > 1:
       
   736             self.VariableNameMask = reduce(compute_mask,
       
   737                 [item.GetVariable().split('.') for item in items])
       
   738         elif len(items) > 0:
       
   739             self.VariableNameMask = items[0].GetVariable().split('.')[:-1] + ['*']
       
   740         else:
       
   741             self.VariableNameMask = []
       
   742         self.MaskLabel.ChangeValue(".".join(self.VariableNameMask))
       
   743         self.MaskLabel.SetInsertionPoint(self.MaskLabel.GetLastPosition())
       
   744             
       
   745     def GetVariableNameMask(self):
       
   746         return self.VariableNameMask
       
   747     
       
   748     def InsertValue(self, iec_path, idx = None, force=False, graph=False):
       
   749         for panel in self.GraphicPanels:
       
   750             if panel.GetItem(iec_path) is not None:
       
   751                 if graph and isinstance(panel, DebugVariableTextViewer):
       
   752                     self.ToggleViewerType(panel)
       
   753                 return
       
   754         if idx is None:
       
   755             idx = len(self.GraphicPanels)
       
   756         item = DebugVariableItem(self, iec_path, True)
       
   757         result = self.AddDataConsumer(iec_path.upper(), item)
       
   758         if result is not None or force:
       
   759             
       
   760             self.Freeze()
       
   761             if item.IsNumVariable() and graph:
       
   762                 panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL)
       
   763                 if self.CursorTick is not None:
       
   764                     panel.SetCursorTick(self.CursorTick)
       
   765             else:
       
   766                 panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item])
       
   767             if idx is not None:
       
   768                 self.GraphicPanels.insert(idx, panel)
       
   769             else:
       
   770                 self.GraphicPanels.append(panel)
       
   771             self.ResetVariableNameMask()
       
   772             self.RefreshGraphicsSizer()
       
   773             self.Thaw()
       
   774             self.ForceRefresh()
       
   775     
       
   776     def MoveValue(self, iec_path, idx = None, graph=False):
       
   777         if idx is None:
       
   778             idx = len(self.GraphicPanels)
       
   779         source_panel = None
       
   780         item = None
       
   781         for panel in self.GraphicPanels:
       
   782             item = panel.GetItem(iec_path)
       
   783             if item is not None:
       
   784                 source_panel = panel
       
   785                 break
       
   786         if source_panel is not None:
       
   787             source_panel_idx = self.GraphicPanels.index(source_panel)
       
   788             
       
   789             if (len(source_panel.GetItems()) == 1):
       
   790                 
       
   791                 if source_panel_idx < idx:
       
   792                     self.GraphicPanels.insert(idx, source_panel)
       
   793                     self.GraphicPanels.pop(source_panel_idx)
       
   794                 elif source_panel_idx > idx:
       
   795                     self.GraphicPanels.pop(source_panel_idx)
       
   796                     self.GraphicPanels.insert(idx, source_panel)
       
   797                 else:
       
   798                     return
       
   799                 
       
   800             else:
       
   801                 source_panel.RemoveItem(item)
       
   802                 source_size = source_panel.GetSize()
       
   803                 if item.IsNumVariable() and graph:
       
   804                     panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, [item], GRAPH_PARALLEL)
       
   805                     panel.SetCanvasHeight(source_size.height)
       
   806                     if self.CursorTick is not None:
       
   807                         panel.SetCursorTick(self.CursorTick)
       
   808                 
       
   809                 else:
       
   810                     panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item])
       
   811                 
       
   812                 self.GraphicPanels.insert(idx, panel)
       
   813                 
       
   814                 if source_panel.ItemsIsEmpty():
       
   815                     if source_panel.HasCapture():
       
   816                         source_panel.ReleaseMouse()
       
   817                     source_panel.Destroy()
       
   818                     self.GraphicPanels.remove(source_panel)
       
   819                 
       
   820             self.ResetVariableNameMask()
       
   821             self.RefreshGraphicsSizer()
       
   822             self.ForceRefresh()
       
   823     
       
   824     def MergeGraphs(self, source, target_idx, merge_type, force=False):
       
   825         source_item = None
       
   826         source_panel = None
       
   827         for panel in self.GraphicPanels:
       
   828             source_item = panel.GetItem(source)
       
   829             if source_item is not None:
       
   830                 source_panel = panel
       
   831                 break
       
   832         if source_item is None:
       
   833             item = DebugVariableItem(self, source, True)
       
   834             if item.IsNumVariable():
       
   835                 result = self.AddDataConsumer(source.upper(), item)
       
   836                 if result is not None or force:
       
   837                     source_item = item
       
   838         if source_item is not None and source_item.IsNumVariable():
       
   839             if source_panel is not None:
       
   840                 source_size = source_panel.GetSize()
       
   841             else:
       
   842                 source_size = None
       
   843             target_panel = self.GraphicPanels[target_idx]
       
   844             graph_type = target_panel.GraphType
       
   845             if target_panel != source_panel:
       
   846                 if (merge_type == GRAPH_PARALLEL and graph_type != merge_type or
       
   847                     merge_type == GRAPH_ORTHOGONAL and 
       
   848                     (graph_type == GRAPH_PARALLEL and len(target_panel.Items) > 1 or
       
   849                      graph_type == GRAPH_ORTHOGONAL and len(target_panel.Items) >= 3)):
       
   850                     return
       
   851                 
       
   852                 if source_panel is not None:
       
   853                     source_panel.RemoveItem(source_item)
       
   854                     if source_panel.ItemsIsEmpty():
       
   855                         if source_panel.HasCapture():
       
   856                             source_panel.ReleaseMouse()
       
   857                         source_panel.Destroy()
       
   858                         self.GraphicPanels.remove(source_panel)
       
   859             elif (merge_type != graph_type and len(target_panel.Items) == 2):
       
   860                 target_panel.RemoveItem(source_item)
       
   861             else:
       
   862                 target_panel = None
       
   863                 
       
   864             if target_panel is not None:
       
   865                 target_panel.AddItem(source_item)
       
   866                 target_panel.GraphType = merge_type
       
   867                 size = target_panel.GetSize()
       
   868                 if merge_type == GRAPH_ORTHOGONAL:
       
   869                     target_panel.SetCanvasHeight(size.width)
       
   870                 elif source_size is not None and source_panel != target_panel:
       
   871                     target_panel.SetCanvasHeight(size.height + source_size.height)
       
   872                 else:
       
   873                     target_panel.SetCanvasHeight(size.height)
       
   874                 target_panel.ResetGraphics()
       
   875                 
       
   876                 self.ResetVariableNameMask()
       
   877                 self.RefreshGraphicsSizer()
       
   878                 self.ForceRefresh()
       
   879     
       
   880     def DeleteValue(self, source_panel, item=None):
       
   881         source_idx = self.GetViewerIndex(source_panel)
       
   882         if source_idx is not None:
       
   883             
       
   884             if item is None:
       
   885                 source_panel.ClearItems()
       
   886                 source_panel.Destroy()
       
   887                 self.GraphicPanels.remove(source_panel)
       
   888                 self.ResetVariableNameMask()
       
   889                 self.RefreshGraphicsSizer()
       
   890             else:
       
   891                 source_panel.RemoveItem(item)
       
   892                 if source_panel.ItemsIsEmpty():
       
   893                     source_panel.Destroy()
       
   894                     self.GraphicPanels.remove(source_panel)
       
   895                     self.ResetVariableNameMask()
       
   896                     self.RefreshGraphicsSizer()
       
   897             if len(self.GraphicPanels) == 0:
       
   898                 self.Fixed = False
       
   899                 self.ResetCursorTick()
       
   900             self.ForceRefresh()
       
   901     
       
   902     def ToggleViewerType(self, panel):
       
   903         panel_idx = self.GetViewerIndex(panel)
       
   904         if panel_idx is not None:
       
   905             self.GraphicPanels.remove(panel)
       
   906             items = panel.GetItems()
       
   907             if isinstance(panel, DebugVariableGraphicViewer):
       
   908                 for idx, item in enumerate(items):
       
   909                     new_panel = DebugVariableTextViewer(self.GraphicsWindow, self, [item])
       
   910                     self.GraphicPanels.insert(panel_idx + idx, new_panel)
       
   911             else:
       
   912                 new_panel = DebugVariableGraphicViewer(self.GraphicsWindow, self, items, GRAPH_PARALLEL)
       
   913                 self.GraphicPanels.insert(panel_idx, new_panel)
       
   914             panel.Destroy()
       
   915         self.RefreshGraphicsSizer()
       
   916         self.ForceRefresh()
       
   917     
       
   918     def ResetGraphicsValues(self):
       
   919         self.Ticks = numpy.array([])
       
   920         self.StartTick = 0
       
   921         for panel in self.GraphicPanels:
       
   922             panel.ResetItemsData()
       
   923         self.ResetCursorTick()
       
   924 
       
   925     def RefreshGraphicsWindowScrollbars(self):
       
   926         xstart, ystart = self.GraphicsWindow.GetViewStart()
       
   927         window_size = self.GraphicsWindow.GetClientSize()
       
   928         vwidth, vheight = self.GraphicsSizer.GetMinSize()
       
   929         posx = max(0, min(xstart, (vwidth - window_size[0]) / SCROLLBAR_UNIT))
       
   930         posy = max(0, min(ystart, (vheight - window_size[1]) / SCROLLBAR_UNIT))
       
   931         self.GraphicsWindow.Scroll(posx, posy)
       
   932         self.GraphicsWindow.SetScrollbars(SCROLLBAR_UNIT, SCROLLBAR_UNIT, 
       
   933                 vwidth / SCROLLBAR_UNIT, vheight / SCROLLBAR_UNIT, posx, posy)
       
   934     
       
   935     def OnGraphicsWindowEraseBackground(self, event):
       
   936         pass
       
   937     
       
   938     def OnGraphicsWindowPaint(self, event):
       
   939         self.RefreshView()
       
   940         event.Skip()
       
   941     
       
   942     def OnGraphicsWindowResize(self, event):
       
   943         size = self.GetSize()
       
   944         for panel in self.GraphicPanels:
       
   945             panel_size = panel.GetSize()
       
   946             if (isinstance(panel, DebugVariableGraphicViewer) and 
       
   947                 panel.GraphType == GRAPH_ORTHOGONAL and 
       
   948                 panel_size.width == panel_size.height):
       
   949                 panel.SetCanvasHeight(size.width)
       
   950         self.RefreshGraphicsWindowScrollbars()
       
   951         self.GraphicsSizer.Layout()
       
   952         event.Skip()
       
   953 
       
   954     def OnGraphicsWindowMouseWheel(self, event):
       
   955         if self.VetoScrollEvent:
       
   956             self.VetoScrollEvent = False
       
   957         else:
       
   958             event.Skip()