Laurent@814: #!/usr/bin/env python
Laurent@814: # -*- coding: utf-8 -*-
Laurent@814: 
andrej@1571: # This file is part of Beremiz, a Integrated Development Environment for
andrej@1571: # programming IEC 61131-3 automates supporting plcopen standard and CanFestival.
Laurent@814: #
andrej@1571: # Copyright (C) 2012: Edouard TISSERANT and Laurent BESSARD
Laurent@814: #
andrej@1571: # See COPYING file for copyrights details.
Laurent@814: #
andrej@1571: # This program is free software; you can redistribute it and/or
andrej@1571: # modify it under the terms of the GNU General Public License
andrej@1571: # as published by the Free Software Foundation; either version 2
andrej@1571: # of the License, or (at your option) any later version.
Laurent@814: #
andrej@1571: # This program is distributed in the hope that it will be useful,
andrej@1571: # but WITHOUT ANY WARRANTY; without even the implied warranty of
andrej@1571: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
andrej@1571: # GNU General Public License for more details.
Laurent@814: #
andrej@1571: # You should have received a copy of the GNU General Public License
andrej@1571: # along with this program; if not, write to the Free Software
andrej@1571: # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
Laurent@814: 
andrej@1853: 
andrej@1853: from __future__ import absolute_import
andrej@2437: from __future__ import division
Laurent@887: from time import time as gettime
Edouard@1919: from cycler import cycler
andrej@1832: 
Laurent@887: import numpy
Laurent@814: import wx
Laurent@1198: import matplotlib
Laurent@1198: import matplotlib.pyplot
Laurent@1198: from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
Laurent@1198: from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap
Laurent@1198: from matplotlib.backends.backend_agg import FigureCanvasAgg
Laurent@1198: from mpl_toolkits.mplot3d import Axes3D
andrej@2432: from six.moves import xrange
Laurent@1200: 
Laurent@1200: from editors.DebugViewer import REFRESH_PERIOD
andrej@1853: from controls.DebugVariablePanel.DebugVariableViewer import *
andrej@1853: from controls.DebugVariablePanel.GraphButton import GraphButton
Laurent@1199: 
andrej@1497: 
Laurent@1209: # Graph variable display type
Laurent@1198: GRAPH_PARALLEL, GRAPH_ORTHOGONAL = range(2)
Laurent@1198: 
Laurent@1209: # Canvas height
Laurent@1198: [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200]
Laurent@1198: 
surkovsv93@1964: CANVAS_BORDER = (30., 20.)  # Border height on at bottom and top of graph
andrej@1737: CANVAS_PADDING = 8.5        # Border inside graph where no label is drawn
andrej@1737: VALUE_LABEL_HEIGHT = 17.    # Height of variable label in graph
andrej@1737: AXES_LABEL_HEIGHT = 12.75   # Height of variable value in graph
Laurent@1209: 
Laurent@1209: # Colors used cyclically for graph curves
Laurent@1200: COLOR_CYCLE = ['r', 'b', 'g', 'm', 'y', 'k']
Laurent@1209: # Color for graph cursor
Laurent@1200: CURSOR_COLOR = '#800080'
Laurent@1198: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1267: #                      Debug Variable Graphic Viewer Helpers
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1267: 
andrej@1736: 
Laurent@1267: def merge_ranges(ranges):
Laurent@1267:     """
Laurent@1267:     Merge variables data range in a list to return a range of minimal min range
Laurent@1267:     value and maximal max range value extended of 10% for keeping a padding
Laurent@1267:     around graph in canvas
Laurent@1267:     @param ranges: [(range_min_value, range_max_value),...]
Laurent@1267:     @return: merged_range_min_value, merged_range_max_value
Laurent@1267:     """
Laurent@1267:     # Get minimal and maximal range value
Laurent@1267:     min_value = max_value = None
Laurent@1267:     for range_min, range_max in ranges:
Laurent@1267:         # Update minimal range value
Laurent@1267:         if min_value is None:
Laurent@1267:             min_value = range_min
Laurent@1267:         elif range_min is not None:
Laurent@1267:             min_value = min(min_value, range_min)
andrej@1730: 
Laurent@1267:         # Update maximal range value
Laurent@1267:         if max_value is None:
Laurent@1267:             max_value = range_max
Laurent@1267:         elif range_min is not None:
Laurent@1267:             max_value = max(max_value, range_max)
andrej@1730: 
Laurent@1267:     # Calculate range center and width if at least one valid range is defined
Laurent@1267:     if min_value is not None and max_value is not None:
Laurent@1267:         center = (min_value + max_value) / 2.
Laurent@1267:         range_size = max(1.0, max_value - min_value)
andrej@1730: 
Laurent@1267:     # Set default center and with if no valid range is defined
Laurent@1267:     else:
Laurent@1267:         center = 0.5
Laurent@1267:         range_size = 1.0
andrej@1730: 
Laurent@1267:     # Return range expended from 10 %
Laurent@1267:     return center - range_size * 0.55, center + range_size * 0.55
Laurent@1267: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1209: #                   Debug Variable Graphic Viewer Drop Target
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1209: 
Laurent@1209: 
Laurent@1209: class DebugVariableGraphicDropTarget(wx.TextDropTarget):
andrej@1736:     """
andrej@1736:     Class that implements a custom drop target class for Debug Variable Graphic
andrej@1736:     Viewer
andrej@1736:     """
andrej@1730: 
Laurent@1200:     def __init__(self, parent, window):
Laurent@1209:         """
Laurent@1209:         Constructor
Laurent@1209:         @param parent: Reference to Debug Variable Graphic Viewer
Laurent@1209:         @param window: Reference to the Debug Variable Panel
Laurent@1209:         """
Laurent@1200:         wx.TextDropTarget.__init__(self)
Laurent@1200:         self.ParentControl = parent
Laurent@1198:         self.ParentWindow = window
andrej@1730: 
Laurent@1198:     def __del__(self):
Laurent@1209:         """
Laurent@1209:         Destructor
Laurent@1209:         """
Laurent@1209:         # Remove reference to Debug Variable Graphic Viewer and Debug Variable
Laurent@1209:         # Panel
Laurent@1200:         self.ParentControl = None
Laurent@1198:         self.ParentWindow = None
andrej@1730: 
Laurent@1200:     def OnDragOver(self, x, y, d):
Laurent@1209:         """
Laurent@1209:         Function called when mouse is dragged over Drop Target
Laurent@1209:         @param x: X coordinate of mouse pointer
Laurent@1209:         @param y: Y coordinate of mouse pointer
Laurent@1209:         @param d: Suggested default for return value
Laurent@1209:         """
Laurent@1209:         # Signal parent that mouse is dragged over
Laurent@1200:         self.ParentControl.OnMouseDragging(x, y)
andrej@1730: 
Laurent@1200:         return wx.TextDropTarget.OnDragOver(self, x, y, d)
andrej@1730: 
Laurent@1200:     def OnDropText(self, x, y, data):
Laurent@1209:         """
Laurent@1215:         Function called when mouse is released in Drop Target
Laurent@1209:         @param x: X coordinate of mouse pointer
Laurent@1209:         @param y: Y coordinate of mouse pointer
Laurent@1209:         @param data: Text associated to drag'n drop
Laurent@1209:         """
Laurent@1218:         # Signal Debug Variable Panel to reset highlight
Laurent@1218:         self.ParentWindow.ResetHighlight()
andrej@1730: 
Laurent@1200:         message = None
andrej@1730: 
Laurent@1209:         # Check that data is valid regarding DebugVariablePanel
Laurent@1200:         try:
Laurent@1200:             values = eval(data)
andrej@2450:             if not isinstance(values, tuple):
Laurent@1207:                 raise ValueError
andrej@1780:         except Exception:
andrej@1734:             message = _("Invalid value \"%s\" for debug variable") % data
Laurent@1200:             values = None
andrej@1730: 
Laurent@1209:         # Display message if data is invalid
Laurent@1200:         if message is not None:
Laurent@1200:             wx.CallAfter(self.ShowMessage, message)
andrej@1730: 
Laurent@1209:         # Data contain a reference to a variable to debug
Laurent@1200:         elif values[1] == "debug":
Laurent@1200:             target_idx = self.ParentControl.GetIndex()
andrej@1730: 
Laurent@1209:             # If mouse is dropped in graph canvas bounding box and graph is
Laurent@1209:             # not 3D canvas, graphs will be merged
Laurent@1209:             rect = self.ParentControl.GetAxesBoundingBox()
Laurent@1209:             if not self.ParentControl.Is3DCanvas() and rect.InsideXY(x, y):
Laurent@1209:                 # Default merge type is parallel
Laurent@1209:                 merge_type = GRAPH_PARALLEL
andrej@1730: 
Laurent@1209:                 # If mouse is dropped in left part of graph canvas, graph
Laurent@1209:                 # wall be merged orthogonally
andrej@1730:                 merge_rect = wx.Rect(rect.x, rect.y,
Laurent@1209:                                      rect.width / 2., rect.height)
Laurent@1209:                 if merge_rect.InsideXY(x, y):
Laurent@1209:                     merge_type = GRAPH_ORTHOGONAL
andrej@1730: 
Laurent@1209:                 # Merge graphs
andrej@1730:                 wx.CallAfter(self.ParentWindow.MergeGraphs,
andrej@1730:                              values[0], target_idx,
Laurent@1209:                              merge_type, force=True)
andrej@1730: 
Laurent@1209:             else:
andrej@1847:                 _width, height = self.ParentControl.GetSize()
andrej@1730: 
Laurent@1209:                 # Get Before which Viewer the variable has to be moved or added
Laurent@1209:                 # according to the position of mouse in Viewer.
andrej@2437:                 if y > height // 2:
Laurent@1200:                     target_idx += 1
andrej@1730: 
Laurent@1209:                 # Drag'n Drop is an internal is an internal move inside Debug
andrej@1730:                 # Variable Panel
Laurent@1209:                 if len(values) > 2 and values[2] == "move":
andrej@1730:                     self.ParentWindow.MoveValue(values[0],
Laurent@1209:                                                 target_idx)
andrej@1730: 
Laurent@1209:                 # Drag'n Drop was initiated by another control of Beremiz
Laurent@1200:                 else:
andrej@1730:                     self.ParentWindow.InsertValue(values[0],
andrej@1730:                                                   target_idx,
Laurent@1209:                                                   force=True)
andrej@1730: 
Laurent@1200:     def OnLeave(self):
Laurent@1209:         """
Laurent@1209:         Function called when mouse is leave Drop Target
Laurent@1209:         """
Laurent@1209:         # Signal Debug Variable Panel to reset highlight
Laurent@1200:         self.ParentWindow.ResetHighlight()
Laurent@1200:         return wx.TextDropTarget.OnLeave(self)
andrej@1730: 
Laurent@1200:     def ShowMessage(self, message):
Laurent@1209:         """
Laurent@1209:         Show error message in Error Dialog
Laurent@1209:         @param message: Error message to display
Laurent@1209:         """
andrej@1730:         dialog = wx.MessageDialog(self.ParentWindow,
andrej@1730:                                   message,
andrej@1730:                                   _("Error"),
andrej@1745:                                   wx.OK | wx.ICON_ERROR)
Laurent@1200:         dialog.ShowModal()
Laurent@1200:         dialog.Destroy()
Laurent@1200: 
Laurent@1200: 
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1209: #                      Debug Variable Graphic Viewer Class
andrej@1782: # -------------------------------------------------------------------------------
Laurent@1209: 
Laurent@1209: 
Laurent@1200: class DebugVariableGraphicViewer(DebugVariableViewer, FigureCanvas):
andrej@1736:     """
andrej@1736:     Class that implements a Viewer that display variable values as a graphs
andrej@1736:     """
andrej@1730: 
Laurent@1198:     def __init__(self, parent, window, items, graph_type):
Laurent@1209:         """
Laurent@1209:         Constructor
Laurent@1209:         @param parent: Parent wx.Window of DebugVariableText
Laurent@1209:         @param window: Reference to the Debug Variable Panel
Laurent@1209:         @param items: List of DebugVariableItem displayed by Viewer
Laurent@1209:         @param graph_type: Graph display type (Parallel or orthogonal)
Laurent@1209:         """
Laurent@1198:         DebugVariableViewer.__init__(self, window, items)
andrej@1730: 
Laurent@1209:         self.GraphType = graph_type        # Graph type display
Laurent@1209:         self.CursorTick = None             # Tick of the graph cursor
andrej@1730: 
Laurent@1209:         # Mouse position when start dragging
Laurent@1198:         self.MouseStartPos = None
Laurent@1209:         # Tick when moving tick start
Laurent@1198:         self.StartCursorTick = None
Laurent@1209:         # Canvas size when starting to resize canvas
andrej@1730:         self.CanvasStartSize = None
andrej@1730: 
Laurent@1209:         # List of current displayed contextual buttons
Laurent@1198:         self.ContextualButtons = []
Laurent@1209:         # Reference to item for which contextual buttons was displayed
Laurent@1198:         self.ContextualButtonsItem = None
andrej@1730: 
Laurent@1267:         # Flag indicating that zoom fit current displayed data range or whole
Laurent@1267:         # data range if False
Laurent@1267:         self.ZoomFit = False
andrej@1730: 
Laurent@1209:         # Create figure for drawing graphs
Laurent@1198:         self.Figure = matplotlib.figure.Figure(facecolor='w')
Laurent@1209:         # Defined border around figure in canvas
andrej@1730:         self.Figure.subplotpars.update(top=0.95, left=0.1,
Laurent@1209:                                        bottom=0.1, right=0.95)
andrej@1730: 
Laurent@1198:         FigureCanvas.__init__(self, parent, -1, self.Figure)
Laurent@1198:         self.SetWindowStyle(wx.WANTS_CHARS)
Laurent@1198:         self.SetBackgroundColour(wx.WHITE)
andrej@1730: 
Laurent@1209:         # Bind wx events
Laurent@1214:         self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
Laurent@1198:         self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
Laurent@1198:         self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter)
Laurent@1198:         self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave)
Laurent@1198:         self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
Laurent@1198:         self.Bind(wx.EVT_SIZE, self.OnResize)
andrej@1730: 
Laurent@1209:         # Set canvas min size
Laurent@1198:         canvas_size = self.GetCanvasMinSize()
Laurent@1198:         self.SetMinSize(canvas_size)
andrej@1730: 
Laurent@1209:         # Define Viewer drop target
Laurent@1209:         self.SetDropTarget(DebugVariableGraphicDropTarget(self, window))
andrej@1730: 
Laurent@1209:         # Connect matplotlib events
Laurent@1198:         self.mpl_connect('button_press_event', self.OnCanvasButtonPressed)
Laurent@1198:         self.mpl_connect('motion_notify_event', self.OnCanvasMotion)
Laurent@1198:         self.mpl_connect('button_release_event', self.OnCanvasButtonReleased)
Laurent@1198:         self.mpl_connect('scroll_event', self.OnCanvasScroll)
andrej@1730: 
Laurent@1267:         # Add buttons for zooming on current displayed data range
Laurent@1267:         self.Buttons.append(
andrej@1878:             GraphButton(0, 0, "fit_graph", self.OnZoomFitButton))
andrej@1730: 
Laurent@1209:         # Add buttons for changing canvas size with predefined height
Laurent@1209:         for size, bitmap in zip(
Laurent@1209:                 [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI],
Laurent@1209:                 ["minimize_graph", "middle_graph", "maximize_graph"]):
andrej@1878:             self.Buttons.append(GraphButton(0, 0, bitmap,
andrej@1878:                                             self.GetOnChangeSizeButton(size)))
andrej@1730: 
Laurent@1209:         # Add buttons for exporting graph values to clipboard and close graph
Laurent@1209:         for bitmap, callback in [
Laurent@1209:                 ("export_graph_mini", self.OnExportGraphButton),
Laurent@1209:                 ("delete_graph", self.OnCloseButton)]:
Laurent@1199:             self.Buttons.append(GraphButton(0, 0, bitmap, callback))
andrej@1730: 
Laurent@1209:         # Update graphs elements
Laurent@1198:         self.ResetGraphics()
Laurent@1198:         self.RefreshLabelsPosition(canvas_size.height)
andrej@1730: 
Laurent@1209:     def AddItem(self, item):
Laurent@1209:         """
Laurent@1209:         Add an item to the list of items displayed by Viewer
Laurent@1209:         @param item: Item to add to the list
Laurent@1209:         """
Laurent@1209:         DebugVariableViewer.AddItem(self, item)
Laurent@1209:         self.ResetGraphics()
andrej@1730: 
Laurent@1209:     def RemoveItem(self, item):
Laurent@1209:         """
Laurent@1209:         Remove an item from the list of items displayed by Viewer
Laurent@1209:         @param item: Item to remove from the list
Laurent@1209:         """
Laurent@1209:         DebugVariableViewer.RemoveItem(self, item)
andrej@1730: 
Laurent@1209:         # If list of items is not empty
Laurent@1209:         if not self.ItemsIsEmpty():
Laurent@1209:             # Return to parallel graph if there is only one item
Laurent@1209:             # especially if it's actually orthogonal
Laurent@1209:             if len(self.Items) == 1:
Laurent@1209:                 self.GraphType = GRAPH_PARALLEL
Laurent@1209:             self.ResetGraphics()
andrej@1730: 
Laurent@1212:     def SetCursorTick(self, cursor_tick):
Laurent@1212:         """
Laurent@1212:         Set cursor tick
Laurent@1212:         @param cursor_tick: Cursor tick
Laurent@1212:         """
Laurent@1212:         self.CursorTick = cursor_tick
andrej@1730: 
Laurent@1267:     def SetZoomFit(self, zoom_fit):
Laurent@1267:         """
Laurent@1267:         Set flag indicating that zoom fit current displayed data range
Laurent@1267:         @param zoom_fit: Flag for zoom fit (False: zoom fit whole data range)
Laurent@1267:         """
andrej@1730:         # Flag is different from the actual one
Laurent@1267:         if zoom_fit != self.ZoomFit:
Laurent@1267:             # Save new flag value
Laurent@1267:             self.ZoomFit = zoom_fit
andrej@1730: 
Laurent@1267:             # Update button for zoom fit bitmap
Laurent@1267:             self.Buttons[0].SetBitmap("full_graph" if zoom_fit else "fit_graph")
andrej@1730: 
Laurent@1267:             # Refresh canvas
Laurent@1267:             self.RefreshViewer()
andrej@1730: 
Laurent@1209:     def SubscribeAllDataConsumers(self):
Laurent@1209:         """
Laurent@1209:         Function that unsubscribe and remove every item that store values of
Laurent@1209:         a variable that doesn't exist in PLC anymore
Laurent@1209:         """
Laurent@1209:         DebugVariableViewer.SubscribeAllDataConsumers(self)
andrej@1730: 
Laurent@1267:         # Graph still have data to display
Laurent@1209:         if not self.ItemsIsEmpty():
Laurent@1267:             # Reset flag indicating that zoom fit current displayed data range
Laurent@1267:             self.SetZoomFit(False)
andrej@1730: 
Laurent@1209:             self.ResetGraphics()
andrej@1730: 
Laurent@1209:     def Is3DCanvas(self):
Laurent@1209:         """
Laurent@1209:         Return if Viewer is a 3D canvas
Laurent@1209:         @return: True if Viewer is a 3D canvas
Laurent@1209:         """
Laurent@1209:         return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3
andrej@1730: 
Laurent@1198:     def GetButtons(self):
Laurent@1209:         """
Laurent@1209:         Return list of buttons defined in Viewer
Laurent@1209:         @return: List of buttons
Laurent@1209:         """
Laurent@1209:         # Add contextual buttons to default buttons
Laurent@1198:         return self.Buttons + self.ContextualButtons
andrej@1730: 
Laurent@1209:     def PopupContextualButtons(self, item, rect, direction=wx.RIGHT):
Laurent@1209:         """
Laurent@1209:         Show contextual menu for item aside a label of this item defined
Laurent@1209:         by the bounding box of label in figure
Laurent@1209:         @param item: Item for which contextual is shown
Laurent@1209:         @param rect: Bounding box of label aside which drawing menu
Laurent@1209:         @param direction: Direction in which buttons must be drawn
Laurent@1209:         """
Laurent@1209:         # Return immediately if contextual menu for item is already shown
Laurent@1209:         if self.ContextualButtonsItem == item:
Laurent@1209:             return
andrej@1730: 
Laurent@1209:         # Close already shown contextual menu
Laurent@1209:         self.DismissContextualButtons()
andrej@1730: 
Laurent@1209:         # Save item for which contextual menu is shown
Laurent@1209:         self.ContextualButtonsItem = item
andrej@1730: 
Laurent@1209:         # If item variable is forced, add button for release variable to
Laurent@1209:         # contextual menu
Laurent@1209:         if self.ContextualButtonsItem.IsForced():
Laurent@1209:             self.ContextualButtons.append(
Laurent@1209:                 GraphButton(0, 0, "release", self.OnReleaseItemButton))
andrej@1730: 
Laurent@1209:         # Add other buttons to contextual menu
Laurent@1209:         for bitmap, callback in [
Laurent@1209:                 ("force", self.OnForceItemButton),
Laurent@1209:                 ("export_graph_mini", self.OnExportItemGraphButton),
Laurent@1209:                 ("delete_graph", self.OnRemoveItemButton)]:
andrej@1878:             self.ContextualButtons.append(GraphButton(0, 0, bitmap, callback))
andrej@1730: 
Laurent@1209:         # If buttons are shown at left side or upper side of rect, positions
Laurent@1209:         # will be set in reverse order
Laurent@1209:         buttons = self.ContextualButtons[:]
Laurent@1209:         if direction in [wx.TOP, wx.LEFT]:
andrej@1757:             buttons.reverse()
andrej@1730: 
Laurent@1209:         # Set contextual menu buttons position aside rect depending on
Laurent@1209:         # direction given
Laurent@1209:         offset = 0
Laurent@1209:         for button in buttons:
Laurent@1209:             w, h = button.GetSize()
Laurent@1209:             if direction in [wx.LEFT, wx.RIGHT]:
Laurent@1209:                 x = rect.x + (- w - offset
andrej@1768:                               if direction == wx.LEFT
andrej@1768:                               else rect.width + offset)
andrej@2437:                 y = rect.y + (rect.height - h) // 2
Laurent@1209:                 offset += w
Laurent@1209:             else:
andrej@2437:                 x = rect.x + (rect.width - w) // 2
Laurent@1209:                 y = rect.y + (- h - offset
Laurent@1209:                               if direction == wx.TOP
Laurent@1209:                               else rect.height + offset)
Laurent@1209:                 offset += h
Laurent@1209:             button.SetPosition(x, y)
Laurent@1209:             button.Show()
andrej@1730: 
Laurent@1209:         # Refresh canvas
Laurent@1209:         self.ParentWindow.ForceRefresh()
andrej@1730: 
Laurent@1209:     def DismissContextualButtons(self):
Laurent@1209:         """
Laurent@1209:         Close current shown contextual menu
Laurent@1209:         """
Laurent@1209:         # Return immediately if no contextual menu is shown
Laurent@1198:         if self.ContextualButtonsItem is None:
Laurent@1209:             return
andrej@1730: 
Laurent@1209:         # Reset variables corresponding to contextual menu
Laurent@1209:         self.ContextualButtonsItem = None
Laurent@1209:         self.ContextualButtons = []
andrej@1730: 
Laurent@1209:         # Refresh canvas
Laurent@1209:         self.ParentWindow.ForceRefresh()
andrej@1730: 
Laurent@1198:     def IsOverContextualButton(self, x, y):
Laurent@1209:         """
Laurent@1209:         Return if point is over one contextual button of Viewer
Laurent@1209:         @param x: X coordinate of point
Laurent@1209:         @param y: Y coordinate of point
Laurent@1209:         @return: contextual button where point is over
Laurent@1209:         """
Laurent@1198:         for button in self.ContextualButtons:
Laurent@1198:             if button.HitTest(x, y):
Laurent@1209:                 return button
Laurent@1209:         return None
andrej@1730: 
Laurent@1209:     def ExportGraph(self, item=None):
Laurent@1209:         """
Laurent@1209:         Export item(s) data to clipboard in CSV format
Laurent@1209:         @param item: Item from which data to export, all items if None
Laurent@1209:         (default None)
Laurent@1209:         """
Laurent@1209:         self.ParentWindow.CopyDataToClipboard(
Laurent@1209:             [(item, [entry for entry in item.GetData()])
andrej@1730:              for item in (self.Items
andrej@1730:                           if item is None
Laurent@1209:                           else [item])])
andrej@1730: 
Laurent@1267:     def OnZoomFitButton(self):
Laurent@1267:         """
Laurent@1267:         Function called when Viewer Zoom Fit button is pressed
Laurent@1267:         """
Laurent@1267:         # Toggle zoom fit flag value
Laurent@1267:         self.SetZoomFit(not self.ZoomFit)
andrej@1730: 
Laurent@1209:     def GetOnChangeSizeButton(self, height):
Laurent@1209:         """
Laurent@1209:         Function that generate callback function for change Viewer height to
Laurent@1209:         pre-defined height button
Laurent@1209:         @param height: Height that change Viewer to
Laurent@1209:         @return: callback function
Laurent@1209:         """
Laurent@1198:         def OnChangeSizeButton():
Laurent@1264:             self.SetCanvasHeight(height)
Laurent@1198:         return OnChangeSizeButton
andrej@1730: 
Laurent@1198:     def OnExportGraphButton(self):
Laurent@1209:         """
Laurent@1209:         Function called when Viewer Export button is pressed
Laurent@1209:         """
Laurent@1209:         # Export data of every item in Viewer
Laurent@1198:         self.ExportGraph()
andrej@1730: 
Laurent@1209:     def OnForceItemButton(self):
Laurent@1209:         """
Laurent@1209:         Function called when contextual menu Force button is pressed
Laurent@1209:         """
andrej@1730:         # Open dialog for forcing item variable value
Laurent@1200:         self.ForceValue(self.ContextualButtonsItem)
Laurent@1209:         # Close contextual menu
Laurent@1198:         self.DismissContextualButtons()
andrej@1730: 
Laurent@1209:     def OnReleaseItemButton(self):
Laurent@1209:         """
Laurent@1209:         Function called when contextual menu Release button is pressed
Laurent@1209:         """
andrej@1730:         # Release item variable value
Laurent@1200:         self.ReleaseValue(self.ContextualButtonsItem)
Laurent@1209:         # Close contextual menu
Laurent@1198:         self.DismissContextualButtons()
andrej@1730: 
Laurent@1198:     def OnExportItemGraphButton(self):
Laurent@1209:         """
Laurent@1209:         Function called when contextual menu Export button is pressed
Laurent@1209:         """
Laurent@1209:         # Export data of item variable
Laurent@1200:         self.ExportGraph(self.ContextualButtonsItem)
Laurent@1209:         # Close contextual menu
Laurent@1198:         self.DismissContextualButtons()
andrej@1730: 
andrej@1730:     def OnRemoveItemButton(self):
Laurent@1209:         """
Laurent@1209:         Function called when contextual menu Remove button is pressed
Laurent@1209:         """
Laurent@1209:         # Remove item from Viewer
andrej@1730:         wx.CallAfter(self.ParentWindow.DeleteValue, self,
Laurent@1198:                      self.ContextualButtonsItem)
Laurent@1209:         # Close contextual menu
Laurent@1198:         self.DismissContextualButtons()
andrej@1730: 
Laurent@1209:     def HandleCursorMove(self, event):
Laurent@1212:         """
Laurent@1212:         Update Cursor position according to mouse position and graph type
Laurent@1212:         @param event: Mouse event
Laurent@1212:         """
Laurent@1209:         start_tick, end_tick = self.ParentWindow.GetRange()
Laurent@1209:         cursor_tick = None
Laurent@1209:         items = self.ItemsDict.values()
andrej@1730: 
Laurent@1212:         # Graph is orthogonal
Laurent@1209:         if self.GraphType == GRAPH_ORTHOGONAL:
Laurent@1212:             # Extract items data displayed in canvas figure
Laurent@1267:             start_tick = max(start_tick, self.GetItemsMinCommonTick())
Laurent@1267:             end_tick = max(end_tick, start_tick)
Laurent@1209:             x_data = items[0].GetData(start_tick, end_tick)
Laurent@1209:             y_data = items[1].GetData(start_tick, end_tick)
andrej@1730: 
Laurent@1212:             # Search for the nearest point from mouse position
Laurent@1209:             if len(x_data) > 0 and len(y_data) > 0:
andrej@1730:                 length = min(len(x_data), len(y_data))
andrej@1764:                 d = numpy.sqrt((x_data[:length, 1]-event.xdata) ** 2 +
andrej@1740:                                (y_data[:length, 1]-event.ydata) ** 2)
andrej@1730: 
Laurent@1212:                 # Set cursor tick to the tick of this point
Laurent@1209:                 cursor_tick = x_data[numpy.argmin(d), 0]
andrej@1730: 
Laurent@1212:         # Graph is parallel
Laurent@1209:         else:
Laurent@1212:             # Extract items tick
Laurent@1209:             data = items[0].GetData(start_tick, end_tick)
andrej@1730: 
Laurent@1212:             # Search for point that tick is the nearest from mouse X position
Laurent@1212:             # and set cursor tick to the tick of this point
Laurent@1209:             if len(data) > 0:
Laurent@1212:                 cursor_tick = data[numpy.argmin(
andrej@1878:                     numpy.abs(data[:, 0] - event.xdata)), 0]
andrej@1730: 
Laurent@1212:         # Update cursor tick
Laurent@1209:         if cursor_tick is not None:
Laurent@1209:             self.ParentWindow.SetCursorTick(cursor_tick)
andrej@1730: 
Laurent@1209:     def OnCanvasButtonPressed(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when a button of mouse is pressed
Laurent@1209:         @param event: Mouse event
Laurent@1209:         """
Laurent@1212:         # Get mouse position, graph Y coordinate is inverted in matplotlib
Laurent@1212:         # comparing to wx
andrej@1847:         _width, height = self.GetSize()
Laurent@1209:         x, y = event.x, height - event.y
andrej@1730: 
Laurent@1209:         # Return immediately if mouse is over a button
Laurent@1209:         if self.IsOverButton(x, y):
andrej@1730:             return
andrej@1730: 
Laurent@1209:         # Mouse was clicked inside graph figure
Laurent@1209:         if event.inaxes == self.Axes:
andrej@1730: 
Laurent@1209:             # Find if it was on an item label
Laurent@1209:             item_idx = None
Laurent@1209:             # Check every label paired with corresponding item
andrej@1730:             for i, t in ([pair for pair in enumerate(self.AxesLabels)] +
Laurent@1209:                          [pair for pair in enumerate(self.Labels)]):
Laurent@1209:                 # Get label bounding box
Laurent@1209:                 (x0, y0), (x1, y1) = t.get_window_extent().get_points()
Laurent@1209:                 rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0)
Laurent@1209:                 # Check if mouse was over label
Laurent@1209:                 if rect.InsideXY(x, y):
Laurent@1209:                     item_idx = i
Laurent@1209:                     break
andrej@1730: 
Laurent@1209:             # If an item label have been clicked
Laurent@1209:             if item_idx is not None:
Laurent@1209:                 # Hide buttons and contextual buttons
Laurent@1209:                 self.ShowButtons(False)
Laurent@1209:                 self.DismissContextualButtons()
andrej@1730: 
Laurent@1209:                 # Start a drag'n drop from mouse position in wx coordinate of
Laurent@1209:                 # parent
Laurent@1209:                 xw, yw = self.GetPosition()
andrej@1768:                 self.ParentWindow.StartDragNDrop(
andrej@1768:                     self, self.ItemsDict.values()[item_idx],
andrej@1737:                     x + xw, y + yw,  # Current mouse position
andrej@1737:                     x + xw, y + yw)  # Mouse position when button was clicked
andrej@1730: 
Laurent@1209:             # Don't handle mouse button if canvas is 3D and let matplotlib do
Laurent@1209:             # the default behavior (rotate 3D axes)
Laurent@1209:             elif not self.Is3DCanvas():
Laurent@1209:                 # Save mouse position when clicked
Laurent@1209:                 self.MouseStartPos = wx.Point(x, y)
andrej@1730: 
Laurent@1209:                 # Mouse button was left button, start moving cursor
Laurent@1209:                 if event.button == 1:
Laurent@1209:                     # Save current tick in case a drag'n drop is initiate to
Laurent@1209:                     # restore it
Laurent@1209:                     self.StartCursorTick = self.CursorTick
andrej@1730: 
Laurent@1209:                     self.HandleCursorMove(event)
andrej@1730: 
Laurent@1209:                 # Mouse button is middle button and graph is parallel, start
Laurent@1209:                 # moving graph along X coordinate (tick)
Laurent@1209:                 elif event.button == 2 and self.GraphType == GRAPH_PARALLEL:
Laurent@1209:                     self.StartCursorTick = self.ParentWindow.GetRange()[0]
andrej@1730: 
Laurent@1209:         # Mouse was clicked outside graph figure and over resize highlight with
Laurent@1209:         # left button, start resizing Viewer
Laurent@1209:         elif event.button == 1 and event.y <= 5:
Laurent@1209:             self.MouseStartPos = wx.Point(x, y)
Laurent@1209:             self.CanvasStartSize = height
andrej@1730: 
Laurent@1209:     def OnCanvasButtonReleased(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when a button of mouse is released
Laurent@1209:         @param event: Mouse event
Laurent@1209:         """
Laurent@1209:         # If a drag'n drop is in progress, stop it
Laurent@1209:         if self.ParentWindow.IsDragging():
andrej@1847:             _width, height = self.GetSize()
Laurent@1209:             xw, yw = self.GetPosition()
Laurent@1209:             item = self.ParentWindow.DraggingAxesPanel.ItemsDict.values()[0]
Laurent@1209:             # Give mouse position in wx coordinate of parent
Laurent@1209:             self.ParentWindow.StopDragNDrop(item.GetVariable(),
andrej@1768:                                             xw + event.x, yw + height - event.y)
andrej@1730: 
Laurent@1209:         else:
Laurent@1209:             # Reset any move in progress
Laurent@1209:             self.MouseStartPos = None
Laurent@1209:             self.CanvasStartSize = None
andrej@1730: 
Laurent@1209:             # Handle button under mouse if it exist
andrej@1847:             _width, height = self.GetSize()
Laurent@1209:             self.HandleButton(event.x, height - event.y)
andrej@1730: 
Laurent@1209:     def OnCanvasMotion(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when a button of mouse is moved over Viewer
Laurent@1209:         @param event: Mouse event
Laurent@1209:         """
andrej@1847:         _width, height = self.GetSize()
andrej@1730: 
Laurent@1209:         # If a drag'n drop is in progress, move canvas dragged
Laurent@1209:         if self.ParentWindow.IsDragging():
Laurent@1209:             xw, yw = self.GetPosition()
Laurent@1209:             # Give mouse position in wx coordinate of parent
Laurent@1209:             self.ParentWindow.MoveDragNDrop(
andrej@1730:                 xw + event.x,
Laurent@1209:                 yw + height - event.y)
andrej@1730: 
andrej@1730:         # If a Viewer resize is in progress, change Viewer size
Laurent@1209:         elif event.button == 1 and self.CanvasStartSize is not None:
andrej@1847:             _width, height = self.GetSize()
Laurent@1264:             self.SetCanvasHeight(
Laurent@1209:                 self.CanvasStartSize + height - event.y - self.MouseStartPos.y)
andrej@1730: 
Laurent@1209:         # If no button is pressed, show or hide contextual buttons or resize
Laurent@1209:         # highlight
Laurent@1209:         elif event.button is None:
Laurent@1209:             # Compute direction for items label according graph type
andrej@1737:             if self.GraphType == GRAPH_PARALLEL:  # Graph is parallel
Laurent@1209:                 directions = [wx.RIGHT] * len(self.AxesLabels) + \
Laurent@1209:                              [wx.LEFT] * len(self.Labels)
andrej@1768:             elif len(self.AxesLabels) > 0:         # Graph is orthogonal in 2D
andrej@1768:                 directions = [wx.RIGHT, wx.TOP,    # Directions for AxesLabels
andrej@1768:                               wx.LEFT, wx.BOTTOM]  # Directions for Labels
andrej@1737:             else:  # Graph is orthogonal in 3D
Laurent@1209:                 directions = [wx.LEFT] * len(self.Labels)
andrej@1730: 
Laurent@1209:             # Find if mouse is over an item label
Laurent@1209:             item_idx = None
Laurent@1209:             menu_direction = None
Laurent@1209:             for (i, t), dir in zip(
andrej@1730:                     [pair for pair in enumerate(self.AxesLabels)] +
andrej@1730:                     [pair for pair in enumerate(self.Labels)],
Laurent@1209:                     directions):
Laurent@1209:                 # Check every label paired with corresponding item
Laurent@1209:                 (x0, y0), (x1, y1) = t.get_window_extent().get_points()
Laurent@1209:                 rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0)
Laurent@1209:                 # Check if mouse was over label
Laurent@1209:                 if rect.InsideXY(event.x, height - event.y):
Laurent@1209:                     item_idx = i
Laurent@1209:                     menu_direction = dir
Laurent@1209:                     break
andrej@1730: 
andrej@1730:             # If mouse is over an item label,
Laurent@1209:             if item_idx is not None:
Laurent@1209:                 self.PopupContextualButtons(
andrej@1730:                     self.ItemsDict.values()[item_idx],
Laurent@1209:                     rect, menu_direction)
Laurent@1209:                 return
andrej@1730: 
Laurent@1209:             # If mouse isn't over a contextual menu, hide the current shown one
andrej@1730:             # if it exists
Laurent@1209:             if self.IsOverContextualButton(event.x, height - event.y) is None:
Laurent@1209:                 self.DismissContextualButtons()
andrej@1730: 
Laurent@1209:             # Update resize highlight
Laurent@1209:             if event.y <= 5:
Laurent@1209:                 if self.SetHighlight(HIGHLIGHT_RESIZE):
Laurent@1209:                     self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS))
Laurent@1209:                     self.ParentWindow.ForceRefresh()
Laurent@1209:             else:
Laurent@1209:                 if self.SetHighlight(HIGHLIGHT_NONE):
Laurent@1209:                     self.SetCursor(wx.NullCursor)
Laurent@1209:                     self.ParentWindow.ForceRefresh()
andrej@1730: 
andrej@1730:         # Handle buttons if canvas is not 3D
Laurent@1209:         elif not self.Is3DCanvas():
andrej@1730: 
Laurent@1209:             # If left button is pressed
Laurent@1209:             if event.button == 1:
andrej@1730: 
Laurent@1209:                 # Mouse is inside graph figure
Laurent@1209:                 if event.inaxes == self.Axes:
andrej@1730: 
Laurent@1209:                     # If a cursor move is in progress, update cursor position
Laurent@1209:                     if self.MouseStartPos is not None:
Laurent@1209:                         self.HandleCursorMove(event)
andrej@1730: 
Laurent@1209:                 # Mouse is outside graph figure, cursor move is in progress and
Laurent@1209:                 # there is only one item in Viewer, start a drag'n drop
Laurent@1209:                 elif self.MouseStartPos is not None and len(self.Items) == 1:
Laurent@1209:                     xw, yw = self.GetPosition()
Laurent@1209:                     self.ParentWindow.SetCursorTick(self.StartCursorTick)
andrej@1768:                     self.ParentWindow.StartDragNDrop(
andrej@1768:                         self, self.ItemsDict.values()[0],
Laurent@1209:                         # Current mouse position
Laurent@1209:                         event.x + xw, height - event.y + yw,
Laurent@1209:                         # Mouse position when button was clicked
Laurent@1209:                         self.MouseStartPos.x + xw,
Laurent@1209:                         self.MouseStartPos.y + yw)
andrej@1730: 
Laurent@1209:             # If middle button is pressed and moving graph along X coordinate
Laurent@1209:             # is in progress
andrej@1767:             elif (event.button == 2 and
andrej@1767:                   self.GraphType == GRAPH_PARALLEL and
andrej@1767:                   self.MouseStartPos is not None):
Laurent@1209:                 start_tick, end_tick = self.ParentWindow.GetRange()
Laurent@1209:                 rect = self.GetAxesBoundingBox()
andrej@1730: 
Laurent@1209:                 # Move graph along X coordinate
Laurent@1209:                 self.ParentWindow.SetCanvasPosition(
andrej@1730:                     self.StartCursorTick +
Laurent@1209:                     (self.MouseStartPos.x - event.x) *
andrej@2437:                     (end_tick - start_tick) // rect.width)
andrej@1730: 
Laurent@1209:     def OnCanvasScroll(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when a wheel mouse is use in Viewer
Laurent@1209:         @param event: Mouse event
Laurent@1209:         """
Laurent@1209:         # Change X range of graphs if mouse is in canvas figure and ctrl is
Laurent@1209:         # pressed
Laurent@1209:         if event.inaxes is not None and event.guiEvent.ControlDown():
andrej@1730: 
Laurent@1209:             # Calculate position of fixed tick point according to graph type
Laurent@1209:             # and mouse position
Laurent@1209:             if self.GraphType == GRAPH_ORTHOGONAL:
Laurent@1209:                 start_tick, end_tick = self.ParentWindow.GetRange()
Laurent@1209:                 tick = (start_tick + end_tick) / 2.
Laurent@1209:             else:
Laurent@1209:                 tick = event.xdata
andrej@2437:             self.ParentWindow.ChangeRange(int(-event.step) // 3, tick)
andrej@1730: 
Laurent@1209:             # Vetoing event to prevent parent panel to be scrolled
Laurent@1209:             self.ParentWindow.VetoScrollEvent = True
andrej@1730: 
Laurent@1214:     def OnLeftDClick(self, event):
Laurent@1214:         """
Laurent@1214:         Function called when a left mouse button is double clicked
Laurent@1214:         @param event: Mouse event
Laurent@1214:         """
Laurent@1214:         # Check that double click was done inside figure
Laurent@1214:         pos = event.GetPosition()
Laurent@1214:         rect = self.GetAxesBoundingBox()
Laurent@1214:         if rect.InsideXY(pos.x, pos.y):
Laurent@1214:             # Reset Cursor tick to value before starting clicking
Laurent@1214:             self.ParentWindow.SetCursorTick(self.StartCursorTick)
Laurent@1214:             # Toggle to text Viewer(s)
Laurent@1214:             self.ParentWindow.ToggleViewerType(self)
andrej@1730: 
Laurent@1214:         else:
Laurent@1214:             event.Skip()
andrej@1730: 
Laurent@1209:     # Cursor tick move for each arrow key
Laurent@1209:     KEY_CURSOR_INCREMENT = {
Laurent@1209:         wx.WXK_LEFT: -1,
Laurent@1209:         wx.WXK_RIGHT: 1,
Laurent@1267:         wx.WXK_UP: 10,
Laurent@1267:         wx.WXK_DOWN: -10}
andrej@1730: 
Laurent@1209:     def OnKeyDown(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when key is pressed
Laurent@1209:         @param event: wx.KeyEvent
Laurent@1209:         """
Laurent@1209:         # If cursor is shown and arrow key is pressed, move cursor tick
Laurent@1209:         if self.CursorTick is not None:
Laurent@1209:             move = self.KEY_CURSOR_INCREMENT.get(event.GetKeyCode(), None)
Laurent@1209:             if move is not None:
Laurent@1209:                 self.ParentWindow.MoveCursorTick(move)
Laurent@1209:         event.Skip()
andrej@1730: 
Laurent@1200:     def OnLeave(self, event):
Laurent@1209:         """
Laurent@1209:         Function called when mouse leave Viewer
Laurent@1209:         @param event: wx.MouseEvent
Laurent@1209:         """
Laurent@1209:         # If Viewer is not resizing, reset resize highlight
Laurent@1209:         if self.CanvasStartSize is None:
Laurent@1209:             self.SetHighlight(HIGHLIGHT_NONE)
Laurent@1209:             self.SetCursor(wx.NullCursor)
Laurent@1200:             DebugVariableViewer.OnLeave(self, event)
Laurent@1200:         else:
Laurent@1200:             event.Skip()
andrej@1730: 
Laurent@1212:     def GetCanvasMinSize(self):
Laurent@1212:         """
Laurent@1212:         Return the minimum size of Viewer so that all items label can be
Laurent@1212:         displayed
Laurent@1212:         @return: wx.Size containing Viewer minimum size
Laurent@1212:         """
Laurent@1212:         # The minimum height take in account the height of all items, padding
Laurent@1212:         # inside figure and border around figure
andrej@1730:         return wx.Size(200,
andrej@1768:                        CANVAS_BORDER[0] + CANVAS_BORDER[1] +
andrej@1768:                        2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items))
andrej@1730: 
Laurent@1264:     def SetCanvasHeight(self, height):
Laurent@1212:         """
Laurent@1212:         Set Viewer size checking that it respects Viewer minimum size
Laurent@1212:         @param height: Viewer height
Laurent@1212:         """
Laurent@1264:         min_width, min_height = self.GetCanvasMinSize()
Laurent@1264:         height = max(height, min_height)
Laurent@1264:         self.SetMinSize(wx.Size(min_width, height))
Laurent@1212:         self.RefreshLabelsPosition(height)
Laurent@1212:         self.ParentWindow.RefreshGraphicsSizer()
andrej@1730: 
Laurent@1212:     def GetAxesBoundingBox(self, parent_coordinate=False):
Laurent@1212:         """
Laurent@1212:         Return figure bounding box in wx coordinate
Laurent@1212:         @param parent_coordinate: True if use parent coordinate (default False)
Laurent@1212:         """
Laurent@1212:         # Calculate figure bounding box. Y coordinate is inverted in matplotlib
Laurent@1212:         # figure comparing to wx panel
Laurent@1212:         width, height = self.GetSize()
Laurent@1212:         ax, ay, aw, ah = self.figure.gca().get_position().bounds
Laurent@1212:         bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1,
Laurent@1212:                        aw * width + 2, ah * height + 1)
andrej@1730: 
Laurent@1212:         # If parent_coordinate, add Viewer position in parent
Laurent@1212:         if parent_coordinate:
Laurent@1212:             xw, yw = self.GetPosition()
Laurent@1212:             bbox.x += xw
Laurent@1212:             bbox.y += yw
andrej@1730: 
Laurent@1212:         return bbox
andrej@1730: 
Laurent@1212:     def RefreshHighlight(self, x, y):
Laurent@1212:         """
Laurent@1212:         Refresh Viewer highlight according to mouse position
Laurent@1212:         @param x: X coordinate of mouse pointer
Laurent@1212:         @param y: Y coordinate of mouse pointer
Laurent@1212:         """
andrej@1847:         _width, height = self.GetSize()
andrej@1730: 
Laurent@1212:         # Mouse is over Viewer figure and graph is not 3D
Laurent@1212:         bbox = self.GetAxesBoundingBox()
Laurent@1212:         if bbox.InsideXY(x, y) and not self.Is3DCanvas():
andrej@2437:             rect = wx.Rect(bbox.x, bbox.y, bbox.width // 2, bbox.height)
Laurent@1212:             # Mouse is over Viewer left part of figure
Laurent@1212:             if rect.InsideXY(x, y):
Laurent@1212:                 self.SetHighlight(HIGHLIGHT_LEFT)
andrej@1730: 
Laurent@1212:             # Mouse is over Viewer right part of figure
Laurent@1212:             else:
Laurent@1212:                 self.SetHighlight(HIGHLIGHT_RIGHT)
andrej@1730: 
Laurent@1212:         # Mouse is over upper part of Viewer
andrej@2437:         elif y < height // 2:
Laurent@1212:             # Viewer is upper one in Debug Variable Panel, show highlight
Laurent@1212:             if self.ParentWindow.IsViewerFirst(self):
Laurent@1212:                 self.SetHighlight(HIGHLIGHT_BEFORE)
andrej@1730: 
Laurent@1212:             # Viewer is not the upper one, show highlight in previous one
Laurent@1212:             # It prevents highlight to move when mouse leave one Viewer to
Laurent@1212:             # another
Laurent@1212:             else:
Laurent@1212:                 self.SetHighlight(HIGHLIGHT_NONE)
Laurent@1212:                 self.ParentWindow.HighlightPreviousViewer(self)
andrej@1730: 
Laurent@1212:         # Mouse is over lower part of Viewer
Laurent@1212:         else:
Laurent@1212:             self.SetHighlight(HIGHLIGHT_AFTER)
andrej@1730: 
Laurent@1212:     def OnAxesMotion(self, event):
Laurent@1212:         """
Laurent@1212:         Function overriding default function called when mouse is dragged for
Laurent@1212:         rotating graph preventing refresh to be called too quickly
Laurent@1212:         @param event: Mouse event
Laurent@1212:         """
Laurent@1212:         if self.Is3DCanvas():
Laurent@1212:             # Call default function at most 10 times per second
Laurent@1212:             current_time = gettime()
Laurent@1212:             if current_time - self.LastMotionTime > REFRESH_PERIOD:
Laurent@1212:                 self.LastMotionTime = current_time
Laurent@1212:                 Axes3D._on_move(self.Axes, event)
andrej@1730: 
Laurent@1212:     def GetAddTextFunction(self):
Laurent@1212:         """
Laurent@1212:         Return function for adding text in figure according to graph type
Laurent@1212:         @return: Function adding text to figure
Laurent@1212:         """
Laurent@1212:         text_func = (self.Axes.text2D if self.Is3DCanvas() else self.Axes.text)
andrej@1750: 
Laurent@1212:         def AddText(*args, **kwargs):
Laurent@1212:             args = [0, 0, ""]
Laurent@1212:             kwargs["transform"] = self.Axes.transAxes
Laurent@1212:             return text_func(*args, **kwargs)
Laurent@1212:         return AddText
andrej@1497: 
andrej@1497:     def SetAxesColor(self, color):
Edouard@1919:         self.Axes.set_prop_cycle(cycler('color', color))
andrej@1730: 
Laurent@1212:     def ResetGraphics(self):
Laurent@1212:         """
Laurent@1212:         Reset figure and graphical elements displayed in it
andrej@1730:         Called any time list of items or graph type change
Laurent@1212:         """
Laurent@1212:         # Clear figure from any axes defined
Laurent@1212:         self.Figure.clear()
andrej@1730: 
Laurent@1212:         # Add 3D projection if graph is in 3D
Laurent@1212:         if self.Is3DCanvas():
Laurent@1212:             self.Axes = self.Figure.gca(projection='3d')
andrej@1497:             self.SetAxesColor(['b'])
andrej@1730: 
andrej@1730:             # Override function to prevent too much refresh when graph is
Laurent@1212:             # rotated
Laurent@1212:             self.LastMotionTime = gettime()
Laurent@1212:             setattr(self.Axes, "_on_move", self.OnAxesMotion)
andrej@1730: 
Laurent@1212:             # Init graph mouse event so that graph can be rotated
Laurent@1212:             self.Axes.mouse_init()
andrej@1730: 
Laurent@1212:             # Set size of Z axis labels
Laurent@1212:             self.Axes.tick_params(axis='z', labelsize='small')
andrej@1730: 
Laurent@1212:         else:
Laurent@1212:             self.Axes = self.Figure.gca()
andrej@1497:             self.SetAxesColor(COLOR_CYCLE)
andrej@1730: 
Laurent@1212:         # Set size of X and Y axis labels
Laurent@1212:         self.Axes.tick_params(axis='x', labelsize='small')
Laurent@1212:         self.Axes.tick_params(axis='y', labelsize='small')
andrej@1730: 
Laurent@1212:         # Init variables storing graphical elements added to figure
andrej@1737:         self.Plots = []       # List of curves
andrej@1737:         self.VLine = None     # Vertical line for cursor
andrej@1737:         self.HLine = None     # Horizontal line for cursor (only orthogonal 2D)
andrej@1737:         self.AxesLabels = []  # List of items variable path text label
andrej@1737:         self.Labels = []      # List of items text label
andrej@1730: 
andrej@1730:         # Get function to add a text in figure according to graph type
Laurent@1212:         add_text_func = self.GetAddTextFunction()
andrej@1730: 
Laurent@1212:         # Graph type is parallel or orthogonal in 3D
Laurent@1212:         if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
Laurent@1212:             num_item = len(self.Items)
Laurent@1212:             for idx in xrange(num_item):
andrej@1730: 
Laurent@1212:                 # Get color from color cycle (black if only one item)
andrej@1767:                 color = ('k' if num_item == 1 else
andrej@1767:                          COLOR_CYCLE[idx % len(COLOR_CYCLE)])
andrej@1730: 
Laurent@1212:                 # In 3D graph items variable label are not displayed as text
Laurent@1212:                 # in figure, but as axis title
Laurent@1212:                 if not self.Is3DCanvas():
Laurent@1212:                     # Items variable labels are in figure upper left corner
Laurent@1212:                     self.AxesLabels.append(
Laurent@1212:                         add_text_func(size='small', color=color,
Laurent@1212:                                       verticalalignment='top'))
andrej@1730: 
Laurent@1212:                 # Items variable labels are in figure lower right corner
Laurent@1212:                 self.Labels.append(
andrej@1730:                     add_text_func(size='large', color=color,
Laurent@1212:                                   horizontalalignment='right'))
andrej@1730: 
Laurent@1212:         # Graph type is orthogonal in 2D
Laurent@1212:         else:
Laurent@1212:             # X coordinate labels are in figure lower side
Laurent@1212:             self.AxesLabels.append(add_text_func(size='small'))
Laurent@1212:             self.Labels.append(
Laurent@1212:                 add_text_func(size='large',
Laurent@1212:                               horizontalalignment='right'))
andrej@1730: 
Laurent@1212:             # Y coordinate labels are vertical and in figure left side
Laurent@1212:             self.AxesLabels.append(
Laurent@1264:                 add_text_func(size='small', rotation='vertical',
Laurent@1264:                               verticalalignment='bottom'))
Laurent@1212:             self.Labels.append(
Laurent@1212:                 add_text_func(size='large', rotation='vertical',
Laurent@1212:                               verticalalignment='top'))
andrej@1730: 
Laurent@1212:         # Refresh position of labels according to Viewer size
andrej@1847:         _width, height = self.GetSize()
Laurent@1212:         self.RefreshLabelsPosition(height)
andrej@1730: 
Laurent@1198:     def RefreshLabelsPosition(self, height):
Laurent@1212:         """
Laurent@1212:         Function called when mouse leave Viewer
Laurent@1212:         @param event: wx.MouseEvent
Laurent@1212:         """
Laurent@1212:         # Figure position like text position in figure are expressed is ratio
Laurent@1212:         # canvas size and figure size. As we want that border around figure and
Laurent@1212:         # text position in figure don't change when canvas size change, we
Laurent@1212:         # expressed border and text position in pixel on screen and apply the
Laurent@1212:         # ratio calculated hereafter to get border and text position in
Laurent@1214:         # matplotlib coordinate
andrej@1737:         canvas_ratio = 1. / height  # Divide by canvas height in pixel
Laurent@1212:         graph_ratio = 1. / (
andrej@1785:             (1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) *
andrej@1785:             height)             # Divide by figure height in pixel
andrej@1730: 
Laurent@1212:         # Update position of figure (keeping up and bottom border the same
Laurent@1212:         # size)
Laurent@1198:         self.Figure.subplotpars.update(
andrej@1744:             top=1.0 - CANVAS_BORDER[1] * canvas_ratio,
andrej@1744:             bottom=CANVAS_BORDER[0] * canvas_ratio)
andrej@1730: 
Laurent@1212:         # Update position of items labels
Laurent@1198:         if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
Laurent@1198:             num_item = len(self.Items)
Laurent@1198:             for idx in xrange(num_item):
andrej@1730: 
Laurent@1212:                 # In 3D graph items variable label are not displayed
Laurent@1198:                 if not self.Is3DCanvas():
Laurent@1212:                     # Items variable labels are in figure upper left corner
Laurent@1198:                     self.AxesLabels[idx].set_position(
andrej@1730:                         (0.05,
andrej@1730:                          1.0 - (CANVAS_PADDING +
Laurent@1212:                                 AXES_LABEL_HEIGHT * idx) * graph_ratio))
andrej@1730: 
Laurent@1212:                 # Items variable labels are in figure lower right corner
Laurent@1198:                 self.Labels[idx].set_position(
andrej@1730:                     (0.95,
andrej@1730:                      CANVAS_PADDING * graph_ratio +
Laurent@1198:                      (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio))
Laurent@1198:         else:
Laurent@1212:             # X coordinate labels are in figure lower side
Laurent@1212:             self.AxesLabels[0].set_position(
andrej@1878:                 (0.1, CANVAS_PADDING * graph_ratio))
Laurent@1212:             self.Labels[0].set_position(
andrej@1878:                 (0.95, CANVAS_PADDING * graph_ratio))
andrej@1730: 
Laurent@1212:             # Y coordinate labels are vertical and in figure left side
Laurent@1212:             self.AxesLabels[1].set_position(
andrej@1878:                 (0.05, 2 * CANVAS_PADDING * graph_ratio))
Laurent@1212:             self.Labels[1].set_position(
andrej@1878:                 (0.05, 1.0 - CANVAS_PADDING * graph_ratio))
andrej@1730: 
Laurent@1212:         # Update subplots
Laurent@1198:         self.Figure.subplots_adjust()
andrej@1730: 
Laurent@1198:     def RefreshViewer(self, refresh_graphics=True):
Laurent@1267:         """
Laurent@1267:         Function called to refresh displayed by matplotlib canvas
Laurent@1267:         @param refresh_graphics: Flag indicating that graphs have to be
Laurent@1267:         refreshed (False: only label values have to be refreshed)
Laurent@1267:         """
Laurent@1267:         # Refresh graphs if needed
Laurent@1198:         if refresh_graphics:
Laurent@1267:             # Get tick range of values to display
Laurent@1198:             start_tick, end_tick = self.ParentWindow.GetRange()
andrej@1730: 
Laurent@1267:             # Graph is parallel
andrej@1730:             if self.GraphType == GRAPH_PARALLEL:
Laurent@1267:                 # Init list of data range for each variable displayed
Laurent@1267:                 ranges = []
andrej@1730: 
Laurent@1267:                 # Get data and range for each variable displayed
Laurent@1198:                 for idx, item in enumerate(self.Items):
Laurent@1267:                     data, min_value, max_value = item.GetDataAndValueRange(
andrej@1878:                         start_tick, end_tick, not self.ZoomFit)
andrej@1730: 
Laurent@1267:                     # Check that data is not empty
Laurent@1198:                     if data is not None:
Laurent@1267:                         # Add variable range to list of variable data range
Laurent@1267:                         ranges.append((min_value, max_value))
andrej@1730: 
Laurent@1267:                         # Add plot to canvas if not yet created
Laurent@1198:                         if len(self.Plots) <= idx:
Laurent@1198:                             self.Plots.append(
Laurent@1198:                                 self.Axes.plot(data[:, 0], data[:, 1])[0])
andrej@1730: 
Laurent@1267:                         # Set data to already created plot in canvas
Laurent@1198:                         else:
Laurent@1198:                             self.Plots[idx].set_data(data[:, 0], data[:, 1])
andrej@1730: 
Laurent@1267:                 # Get X and Y axis ranges
Laurent@1267:                 x_min, x_max = start_tick, end_tick
Laurent@1267:                 y_min, y_max = merge_ranges(ranges)
andrej@1730: 
Laurent@1267:                 # Display cursor in canvas if a cursor tick is defined and it is
Laurent@1267:                 # include in values tick range
andrej@1766:                 if self.CursorTick is not None and \
andrej@1766:                    start_tick <= self.CursorTick <= end_tick:
andrej@1730: 
Laurent@1267:                     # Define a vertical line to display cursor position if no
Laurent@1267:                     # line is already defined
Laurent@1198:                     if self.VLine is None:
andrej@1730:                         self.VLine = self.Axes.axvline(self.CursorTick,
Laurent@1267:                                                        color=CURSOR_COLOR)
andrej@1730: 
Laurent@1267:                     # Set value of vertical line if already defined
Laurent@1198:                     else:
Laurent@1198:                         self.VLine.set_xdata((self.CursorTick, self.CursorTick))
Laurent@1198:                     self.VLine.set_visible(True)
andrej@1730: 
Laurent@1267:                 # Hide vertical line if cursor tick is not defined or reset
Laurent@1267:                 elif self.VLine is not None:
Laurent@1267:                     self.VLine.set_visible(False)
andrej@1730: 
Laurent@1267:             # Graph is orthogonal
Laurent@929:             else:
Laurent@1267:                 # Update tick range, removing ticks that don't have a value for
Laurent@1267:                 # each variable
Laurent@1267:                 start_tick = max(start_tick, self.GetItemsMinCommonTick())
Laurent@1267:                 end_tick = max(end_tick, start_tick)
Laurent@1200:                 items = self.ItemsDict.values()
andrej@1730: 
Laurent@1267:                 # Get data and range for first variable (X coordinate)
Laurent@1267:                 x_data, x_min, x_max = items[0].GetDataAndValueRange(
andrej@1878:                     start_tick, end_tick, not self.ZoomFit)
andrej@1878: 
Laurent@1267:                 # Get data and range for second variable (Y coordinate)
Laurent@1267:                 y_data, y_min, y_max = items[1].GetDataAndValueRange(
andrej@1878:                     start_tick, end_tick, not self.ZoomFit)
andrej@1730: 
Laurent@1267:                 # Normalize X and Y coordinates value range
Laurent@1267:                 x_min, x_max = merge_ranges([(x_min, x_max)])
Laurent@1267:                 y_min, y_max = merge_ranges([(y_min, y_max)])
andrej@1730: 
andrej@1730:                 # Get X and Y coordinates for cursor if cursor tick is defined
Laurent@1198:                 if self.CursorTick is not None:
andrej@1847:                     x_cursor, _x_forced = items[0].GetValue(
andrej@1878:                         self.CursorTick, raw=True)
andrej@1847:                     y_cursor, _y_forced = items[1].GetValue(
andrej@1878:                         self.CursorTick, raw=True)
andrej@1730: 
Laurent@1267:                 # Get common data length so that each value has an x and y
Laurent@1267:                 # coordinate
Laurent@1267:                 length = (min(len(x_data), len(y_data))
Laurent@1267:                           if x_data is not None and y_data is not None
Laurent@1267:                           else 0)
andrej@1730: 
andrej@1730:                 # Graph is orthogonal 2D
Laurent@1198:                 if len(self.Items) < 3:
andrej@1730: 
Laurent@1267:                     # Check that x and y data are not empty
Laurent@1198:                     if x_data is not None and y_data is not None:
andrej@1730: 
Laurent@1267:                         # Add plot to canvas if not yet created
Laurent@1198:                         if len(self.Plots) == 0:
Laurent@1198:                             self.Plots.append(
andrej@1730:                                 self.Axes.plot(x_data[:, 1][:length],
Laurent@1198:                                                y_data[:, 1][:length])[0])
andrej@1730: 
Laurent@1267:                         # Set data to already created plot in canvas
Laurent@1198:                         else:
Laurent@1198:                             self.Plots[0].set_data(
andrej@1730:                                 x_data[:, 1][:length],
Laurent@1198:                                 y_data[:, 1][:length])
andrej@1730: 
Laurent@1267:                     # Display cursor in canvas if a cursor tick is defined and it is
Laurent@1267:                     # include in values tick range
andrej@1766:                     if self.CursorTick is not None and \
andrej@1766:                        start_tick <= self.CursorTick <= end_tick:
andrej@1730: 
Laurent@1267:                         # Define a vertical line to display cursor x coordinate
Laurent@1267:                         # if no line is already defined
Laurent@924:                         if self.VLine is None:
andrej@1730:                             self.VLine = self.Axes.axvline(x_cursor,
Laurent@1267:                                                            color=CURSOR_COLOR)
Laurent@1267:                         # Set value of vertical line if already defined
Laurent@924:                         else:
Laurent@1198:                             self.VLine.set_xdata((x_cursor, x_cursor))
andrej@1730: 
Laurent@1267:                         # Define a horizontal line to display cursor y
Laurent@1267:                         # coordinate if no line is already defined
Laurent@1198:                         if self.HLine is None:
andrej@1730:                             self.HLine = self.Axes.axhline(y_cursor,
Laurent@1267:                                                            color=CURSOR_COLOR)
Laurent@1267:                         # Set value of horizontal line if already defined
Laurent@1198:                         else:
Laurent@1198:                             self.HLine.set_ydata((y_cursor, y_cursor))
andrej@1730: 
Laurent@924:                         self.VLine.set_visible(True)
Laurent@1198:                         self.HLine.set_visible(True)
andrej@1730: 
Laurent@1267:                     # Hide vertical and horizontal line if cursor tick is not
Laurent@1267:                     # defined or reset
Laurent@924:                     else:
Laurent@924:                         if self.VLine is not None:
Laurent@924:                             self.VLine.set_visible(False)
Laurent@1198:                         if self.HLine is not None:
Laurent@1198:                             self.HLine.set_visible(False)
andrej@1730: 
Laurent@1267:                 # Graph is orthogonal 3D
Laurent@916:                 else:
Laurent@1267:                     # Remove all plots already defined in 3D canvas
Laurent@1198:                     while len(self.Axes.lines) > 0:
Laurent@1198:                         self.Axes.lines.pop()
andrej@1730: 
Laurent@1267:                     # Get data and range for third variable (Z coordinate)
Laurent@1267:                     z_data, z_min, z_max = items[2].GetDataAndValueRange(
andrej@1878:                         start_tick, end_tick, not self.ZoomFit)
andrej@1730: 
Laurent@1267:                     # Normalize Z coordinate value range
Laurent@1267:                     z_min, z_max = merge_ranges([(z_min, z_max)])
andrej@1730: 
Laurent@1267:                     # Check that x, y and z data are not empty
andrej@1766:                     if x_data is not None and \
andrej@1766:                        y_data is not None and \
andrej@1766:                        z_data is not None:
andrej@1730: 
Laurent@1267:                         # Get common data length so that each value has an x, y
Laurent@1267:                         # and z coordinate
Laurent@1198:                         length = min(length, len(z_data))
andrej@1730: 
Laurent@1267:                         # Add plot to canvas
Laurent@1198:                         self.Axes.plot(x_data[:, 1][:length],
Laurent@1198:                                        y_data[:, 1][:length],
andrej@1744:                                        zs=z_data[:, 1][:length])
andrej@1730: 
Laurent@1267:                     # Display cursor in canvas if a cursor tick is defined and
Laurent@1267:                     # it is include in values tick range
andrej@1766:                     if self.CursorTick is not None and \
andrej@1766:                        start_tick <= self.CursorTick <= end_tick:
andrej@1730: 
Laurent@1267:                         # Get Z coordinate for cursor
andrej@1847:                         z_cursor, _z_forced = items[2].GetValue(
andrej@1878:                             self.CursorTick, raw=True)
andrej@1730: 
Laurent@1267:                         # Add 3 lines parallel to x, y and z axis to display
Laurent@1267:                         # cursor position in 3D
Laurent@1198:                         for kwargs in [{"xs": numpy.array([x_min, x_max])},
Laurent@1198:                                        {"ys": numpy.array([y_min, y_max])},
Laurent@1198:                                        {"zs": numpy.array([z_min, z_max])}]:
Laurent@1267:                             for param, value in [
Laurent@1267:                                     ("xs", numpy.array([x_cursor, x_cursor])),
Laurent@1267:                                     ("ys", numpy.array([y_cursor, y_cursor])),
Laurent@1267:                                     ("zs", numpy.array([z_cursor, z_cursor]))]:
Laurent@1198:                                 kwargs.setdefault(param, value)
Laurent@1200:                             kwargs["color"] = CURSOR_COLOR
Laurent@1198:                             self.Axes.plot(**kwargs)
andrej@1730: 
Laurent@1267:                     # Set Z axis limits
Laurent@1267:                     self.Axes.set_zlim(z_min, z_max)
andrej@1730: 
Laurent@1267:             # Set X and Y axis limits
Laurent@1198:             self.Axes.set_xlim(x_min, x_max)
Laurent@1198:             self.Axes.set_ylim(y_min, y_max)
andrej@1730: 
Laurent@1267:         # Get value and forced flag for each variable displayed in graph
Laurent@1267:         # If cursor tick is not defined get value and flag of last received
Laurent@1267:         # or get value and flag of variable at cursor tick
andrej@2440:         args = [(
andrej@1878:             item.GetValue(self.CursorTick)
andrej@1878:             if self.CursorTick is not None
andrej@2440:             else (item.GetValue(), item.IsForced())) for item in self.Items]
andrej@2440:         values, forced = zip(*args)
andrej@1730: 
Laurent@1267:         # Get path of each variable displayed simplified using panel variable
Laurent@1267:         # name mask
andrej@1730:         labels = [item.GetVariable(self.ParentWindow.GetVariableNameMask())
Laurent@1267:                   for item in self.Items]
andrej@1730: 
andrej@1730:         # Get style for each variable according to
Laurent@1198:         styles = map(lambda x: {True: 'italic', False: 'normal'}[x], forced)
andrej@1730: 
Laurent@1267:         # Graph is orthogonal 3D, set variables path as 3D axis label
Laurent@1198:         if self.Is3DCanvas():
andrej@1730:             for idx, label_func in enumerate([self.Axes.set_xlabel,
Laurent@1198:                                               self.Axes.set_ylabel,
Laurent@1198:                                               self.Axes.set_zlabel]):
Laurent@1267:                 label_func(labels[idx], fontdict={'size': 'small',
Laurent@1267:                                                   'color': COLOR_CYCLE[idx]})
andrej@1730: 
Laurent@1267:         # Graph is not orthogonal 3D, set variables path in axes labels
Laurent@1198:         else:
Laurent@1198:             for label, text in zip(self.AxesLabels, labels):
Laurent@1198:                 label.set_text(text)
andrej@1730: 
Laurent@1267:         # Set value label text and style according to value and forced flag for
Laurent@1267:         # each variable displayed
Laurent@1198:         for label, value, style in zip(self.Labels, values, styles):
Laurent@1198:             label.set_text(value)
Laurent@1198:             label.set_style(style)
andrej@1730: 
Laurent@1267:         # Refresh figure
Laurent@1198:         self.draw()
Laurent@1198: 
Laurent@1209:     def draw(self, drawDC=None):
Laurent@1209:         """
Laurent@1209:         Render the figure.
Laurent@1209:         """
Laurent@1209:         # Render figure using agg
Laurent@1209:         FigureCanvasAgg.draw(self)
andrej@1730: 
Laurent@1209:         # Get bitmap of figure rendered
Laurent@1209:         self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
andrej@1730:         if wx.VERSION < (3, 0, 0):
andrej@1485:             self.bitmap.UseAlpha()
andrej@1730: 
Laurent@1209:         # Create DC for rendering graphics in bitmap
Laurent@1209:         destDC = wx.MemoryDC()
Laurent@1209:         destDC.SelectObject(self.bitmap)
andrej@1730: 
Laurent@1209:         # Get Graphics Context for DC, for anti-aliased and transparent
Laurent@1209:         # rendering
Laurent@1209:         destGC = wx.GCDC(destDC)
andrej@1730: 
Laurent@1209:         destGC.BeginDrawing()
andrej@1730: 
Laurent@1209:         # Get canvas size and figure bounding box in canvas
Laurent@1209:         width, height = self.GetSize()
Laurent@1209:         bbox = self.GetAxesBoundingBox()
andrej@1730: 
Laurent@1209:         # If highlight to display is resize, draw thick grey line at bottom
andrej@1730:         # side of canvas
Laurent@1209:         if self.Highlight == HIGHLIGHT_RESIZE:
andrej@1823:             destGC.SetPen(HIGHLIGHT['RESIZE_PEN'])
andrej@1823:             destGC.SetBrush(HIGHLIGHT['RESIZE_BRUSH'])
Laurent@1209:             destGC.DrawRectangle(0, height - 5, width, 5)
andrej@1730: 
Laurent@1209:         # If highlight to display is merging graph, draw 50% transparent blue
Laurent@1209:         # rectangle on left or right part of figure depending on highlight type
Laurent@1209:         elif self.Highlight in [HIGHLIGHT_LEFT, HIGHLIGHT_RIGHT]:
andrej@1823:             destGC.SetPen(HIGHLIGHT['DROP_PEN'])
andrej@1823:             destGC.SetBrush(HIGHLIGHT['DROP_BRUSH'])
andrej@1730: 
andrej@2437:             x_offset = (bbox.width // 2
Laurent@1209:                         if self.Highlight == HIGHLIGHT_RIGHT
Laurent@1209:                         else 0)
andrej@1730:             destGC.DrawRectangle(bbox.x + x_offset, bbox.y,
andrej@2437:                                  bbox.width // 2, bbox.height)
andrej@1730: 
Laurent@1209:         # Draw other Viewer common elements
Laurent@1209:         self.DrawCommonElements(destGC, self.GetButtons())
andrej@1730: 
Laurent@1209:         destGC.EndDrawing()
andrej@1730: 
Laurent@1209:         self._isDrawn = True
Laurent@1209:         self.gui_repaint(drawDC=drawDC)