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