Rewrite DebugVariablePanel
authorLaurent Bessard
Mon, 03 Jun 2013 11:11:46 +0200
changeset 1212 b351d3a7917c
parent 1211 e3f805ba74ca
child 1213 599e43ec921b
Rewrite DebugVariablePanel
controls/DebugVariablePanel/DebugVariableGraphicPanel.py
controls/DebugVariablePanel/DebugVariableGraphicViewer.py
controls/DebugVariablePanel/DebugVariableViewer.py
--- a/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Mon Jun 03 08:41:10 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicPanel.py	Mon Jun 03 11:11:46 2013 +0200
@@ -308,7 +308,7 @@
             self.DraggingAxesPanel.SetPosition(wx.Point(0, -height))
         else:
             self.DraggingAxesPanel = panel
-        self.DraggingAxesBoundingBox = panel.GetAxesBoundingBox(absolute=True)
+        self.DraggingAxesBoundingBox = panel.GetAxesBoundingBox(parent_coordinate=True)
         self.DraggingAxesMousePos = wx.Point(
             x_mouse_start - self.DraggingAxesBoundingBox.x, 
             y_mouse_start - self.DraggingAxesBoundingBox.y)
--- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Mon Jun 03 08:41:10 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Mon Jun 03 11:11:46 2013 +0200
@@ -299,6 +299,13 @@
                 self.GraphType = GRAPH_PARALLEL
             self.ResetGraphics()
     
+    def SetCursorTick(self, cursor_tick):
+        """
+        Set cursor tick
+        @param cursor_tick: Cursor tick
+        """
+        self.CursorTick = cursor_tick
+    
     def SubscribeAllDataConsumers(self):
         """
         Function that unsubscribe and remove every item that store values of
@@ -479,20 +486,44 @@
         self.DismissContextualButtons()
     
     def HandleCursorMove(self, event):
+        """
+        Update Cursor position according to mouse position and graph type
+        @param event: Mouse event
+        """
         start_tick, end_tick = self.ParentWindow.GetRange()
         cursor_tick = None
         items = self.ItemsDict.values()
+        
+        # Graph is orthogonal
         if self.GraphType == GRAPH_ORTHOGONAL:
+            # Extract items data displayed in canvas figure
+            min_start_tick = max(start_tick, self.GetItemsMinCommonTick())
+            start_tick = max(start_tick, min_start_tick)
+            end_tick = max(end_tick, min_start_tick)
             x_data = items[0].GetData(start_tick, end_tick)
             y_data = items[1].GetData(start_tick, end_tick)
+            
+            # Search for the nearest point from mouse position
             if len(x_data) > 0 and len(y_data) > 0:
-                length = min(len(x_data), len(y_data))
-                d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + (y_data[:length,1]-event.ydata) ** 2)
+                length = min(len(x_data), len(y_data)) 
+                d = numpy.sqrt((x_data[:length,1]-event.xdata) ** 2 + \
+                               (y_data[:length,1]-event.ydata) ** 2)
+                
+                # Set cursor tick to the tick of this point
                 cursor_tick = x_data[numpy.argmin(d), 0]
+        
+        # Graph is parallel
         else:
+            # Extract items tick
             data = items[0].GetData(start_tick, end_tick)
+            
+            # Search for point that tick is the nearest from mouse X position
+            # and set cursor tick to the tick of this point
             if len(data) > 0:
-                cursor_tick = data[numpy.argmin(numpy.abs(data[:,0] - event.xdata)), 0]
+                cursor_tick = data[numpy.argmin(
+                        numpy.abs(data[:,0] - event.xdata)), 0]
+        
+        # Update cursor tick
         if cursor_tick is not None:
             self.ParentWindow.SetCursorTick(cursor_tick)
     
@@ -501,8 +532,8 @@
         Function called when a button of mouse is pressed
         @param event: Mouse event
         """
-        # Get mouse position, graph coordinates are inverted comparing to wx
-        # coordinates
+        # Get mouse position, graph Y coordinate is inverted in matplotlib
+        # comparing to wx
         width, height = self.GetSize()
         x, y = event.x, height - event.y
         
@@ -721,19 +752,6 @@
             # Vetoing event to prevent parent panel to be scrolled
             self.ParentWindow.VetoScrollEvent = True
     
-    def OnAxesMotion(self, event):
-        """
-        Function overriding default function called when mouse is dragged for
-        rotating graph preventing refresh to be called too quickly
-        @param event: Mouse event
-        """
-        if self.Is3DCanvas():
-            # Call default function at most 10 times per second
-            current_time = gettime()
-            if current_time - self.LastMotionTime > REFRESH_PERIOD:
-                self.LastMotionTime = current_time
-                Axes3D._on_move(self.Axes, event)
-    
     # Cursor tick move for each arrow key
     KEY_CURSOR_INCREMENT = {
         wx.WXK_LEFT: -1,
@@ -766,138 +784,250 @@
         else:
             event.Skip()
     
+    def GetCanvasMinSize(self):
+        """
+        Return the minimum size of Viewer so that all items label can be
+        displayed
+        @return: wx.Size containing Viewer minimum size
+        """
+        # The minimum height take in account the height of all items, padding
+        # inside figure and border around figure
+        return wx.Size(200, 
+            CANVAS_BORDER[0] + CANVAS_BORDER[1] + 
+            2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items))
+    
+    def SetCanvasSize(self, width, height):
+        """
+        Set Viewer size checking that it respects Viewer minimum size
+        @param width: Viewer width
+        @param height: Viewer height
+        """
+        height = max(height, self.GetCanvasMinSize()[1])
+        self.SetMinSize(wx.Size(width, height))
+        self.RefreshLabelsPosition(height)
+        self.ParentWindow.RefreshGraphicsSizer()
+        
+    def GetAxesBoundingBox(self, parent_coordinate=False):
+        """
+        Return figure bounding box in wx coordinate
+        @param parent_coordinate: True if use parent coordinate (default False)
+        """
+        # Calculate figure bounding box. Y coordinate is inverted in matplotlib
+        # figure comparing to wx panel
+        width, height = self.GetSize()
+        ax, ay, aw, ah = self.figure.gca().get_position().bounds
+        bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1,
+                       aw * width + 2, ah * height + 1)
+        
+        # If parent_coordinate, add Viewer position in parent
+        if parent_coordinate:
+            xw, yw = self.GetPosition()
+            bbox.x += xw
+            bbox.y += yw
+        
+        return bbox
+    
+    def RefreshHighlight(self, x, y):
+        """
+        Refresh Viewer highlight according to mouse position
+        @param x: X coordinate of mouse pointer
+        @param y: Y coordinate of mouse pointer
+        """
+        width, height = self.GetSize()
+        
+        # Mouse is over Viewer figure and graph is not 3D
+        bbox = self.GetAxesBoundingBox()
+        if bbox.InsideXY(x, y) and not self.Is3DCanvas():
+            rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height)
+            # Mouse is over Viewer left part of figure
+            if rect.InsideXY(x, y):
+                self.SetHighlight(HIGHLIGHT_LEFT)
+            
+            # Mouse is over Viewer right part of figure
+            else:
+                self.SetHighlight(HIGHLIGHT_RIGHT)
+        
+        # Mouse is over upper part of Viewer
+        elif y < height / 2:
+            # Viewer is upper one in Debug Variable Panel, show highlight
+            if self.ParentWindow.IsViewerFirst(self):
+                self.SetHighlight(HIGHLIGHT_BEFORE)
+            
+            # Viewer is not the upper one, show highlight in previous one
+            # It prevents highlight to move when mouse leave one Viewer to
+            # another
+            else:
+                self.SetHighlight(HIGHLIGHT_NONE)
+                self.ParentWindow.HighlightPreviousViewer(self)
+        
+        # Mouse is over lower part of Viewer
+        else:
+            self.SetHighlight(HIGHLIGHT_AFTER)
+    
+    def OnAxesMotion(self, event):
+        """
+        Function overriding default function called when mouse is dragged for
+        rotating graph preventing refresh to be called too quickly
+        @param event: Mouse event
+        """
+        if self.Is3DCanvas():
+            # Call default function at most 10 times per second
+            current_time = gettime()
+            if current_time - self.LastMotionTime > REFRESH_PERIOD:
+                self.LastMotionTime = current_time
+                Axes3D._on_move(self.Axes, event)
+    
+    def GetAddTextFunction(self):
+        """
+        Return function for adding text in figure according to graph type
+        @return: Function adding text to figure
+        """
+        text_func = (self.Axes.text2D if self.Is3DCanvas() else self.Axes.text)
+        def AddText(*args, **kwargs):
+            args = [0, 0, ""]
+            kwargs["transform"] = self.Axes.transAxes
+            return text_func(*args, **kwargs)
+        return AddText
+    
+    def ResetGraphics(self):
+        """
+        Reset figure and graphical elements displayed in it
+        Called any time list of items or graph type change 
+        """
+        # Clear figure from any axes defined
+        self.Figure.clear()
+        
+        # Add 3D projection if graph is in 3D
+        if self.Is3DCanvas():
+            self.Axes = self.Figure.gca(projection='3d')
+            self.Axes.set_color_cycle(['b'])
+            
+            # Override function to prevent too much refresh when graph is 
+            # rotated
+            self.LastMotionTime = gettime()
+            setattr(self.Axes, "_on_move", self.OnAxesMotion)
+            
+            # Init graph mouse event so that graph can be rotated
+            self.Axes.mouse_init()
+            
+            # Set size of Z axis labels
+            self.Axes.tick_params(axis='z', labelsize='small')
+        
+        else:
+            self.Axes = self.Figure.gca()
+            self.Axes.set_color_cycle(COLOR_CYCLE)
+        
+        # Set size of X and Y axis labels
+        self.Axes.tick_params(axis='x', labelsize='small')
+        self.Axes.tick_params(axis='y', labelsize='small')
+        
+        # Init variables storing graphical elements added to figure
+        self.Plots = []      # List of curves
+        self.VLine = None    # Vertical line for cursor
+        self.HLine = None    # Horizontal line for cursor (only orthogonal 2D)
+        self.AxesLabels = [] # List of items variable path text label
+        self.Labels = []     # List of items text label
+        
+        # Get function to add a text in figure according to graph type 
+        add_text_func = self.GetAddTextFunction()
+        
+        # Graph type is parallel or orthogonal in 3D
+        if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
+            num_item = len(self.Items)
+            for idx in xrange(num_item):
+                
+                # Get color from color cycle (black if only one item)
+                color = ('k' if num_item == 1
+                             else COLOR_CYCLE[idx % len(COLOR_CYCLE)])
+                
+                # In 3D graph items variable label are not displayed as text
+                # in figure, but as axis title
+                if not self.Is3DCanvas():
+                    # Items variable labels are in figure upper left corner
+                    self.AxesLabels.append(
+                        add_text_func(size='small', color=color,
+                                      verticalalignment='top'))
+                
+                # Items variable labels are in figure lower right corner
+                self.Labels.append(
+                    add_text_func(size='large', color=color, 
+                                  horizontalalignment='right'))
+        
+        # Graph type is orthogonal in 2D
+        else:
+            # X coordinate labels are in figure lower side
+            self.AxesLabels.append(add_text_func(size='small'))
+            self.Labels.append(
+                add_text_func(size='large',
+                              horizontalalignment='right'))
+            
+            # Y coordinate labels are vertical and in figure left side
+            self.AxesLabels.append(
+                add_text_func(size='small', rotation='vertical'))
+            self.Labels.append(
+                add_text_func(size='large', rotation='vertical',
+                              verticalalignment='top'))
+       
+        # Refresh position of labels according to Viewer size
+        width, height = self.GetSize()
+        self.RefreshLabelsPosition(height)
+    
     def RefreshLabelsPosition(self, height):
-        canvas_ratio = 1. / height
-        graph_ratio = 1. / ((1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio) * height)
-        
+        """
+        Function called when mouse leave Viewer
+        @param event: wx.MouseEvent
+        """
+        # Figure position like text position in figure are expressed is ratio
+        # canvas size and figure size. As we want that border around figure and
+        # text position in figure don't change when canvas size change, we
+        # expressed border and text position in pixel on screen and apply the
+        # ratio calculated hereafter to get border and text position in
+        # matplotlib coordinate 
+        canvas_ratio = 1. / height # Divide by canvas height in pixel
+        graph_ratio = 1. / (
+            (1.0 - (CANVAS_BORDER[0] + CANVAS_BORDER[1]) * canvas_ratio)
+             * height)             # Divide by figure height in pixel
+        
+        # Update position of figure (keeping up and bottom border the same
+        # size)
         self.Figure.subplotpars.update(
             top= 1.0 - CANVAS_BORDER[1] * canvas_ratio, 
             bottom= CANVAS_BORDER[0] * canvas_ratio)
         
+        # Update position of items labels
         if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
             num_item = len(self.Items)
             for idx in xrange(num_item):
+                
+                # In 3D graph items variable label are not displayed
                 if not self.Is3DCanvas():
+                    # Items variable labels are in figure upper left corner
                     self.AxesLabels[idx].set_position(
                         (0.05, 
-                         1.0 - (CANVAS_PADDING + AXES_LABEL_HEIGHT * idx) * graph_ratio))
+                         1.0 - (CANVAS_PADDING + 
+                                AXES_LABEL_HEIGHT * idx) * graph_ratio))
+                
+                # Items variable labels are in figure lower right corner
                 self.Labels[idx].set_position(
                     (0.95, 
                      CANVAS_PADDING * graph_ratio + 
                      (num_item - idx - 1) * VALUE_LABEL_HEIGHT * graph_ratio))
         else:
-            self.AxesLabels[0].set_position((0.1, CANVAS_PADDING * graph_ratio))
-            self.Labels[0].set_position((0.95, CANVAS_PADDING * graph_ratio))
-            self.AxesLabels[1].set_position((0.05, 2 * CANVAS_PADDING * graph_ratio))
-            self.Labels[1].set_position((0.05, 1.0 - CANVAS_PADDING * graph_ratio))
-    
+            # X coordinate labels are in figure lower side
+            self.AxesLabels[0].set_position(
+                    (0.1, CANVAS_PADDING * graph_ratio))
+            self.Labels[0].set_position(
+                    (0.95, CANVAS_PADDING * graph_ratio))
+            
+            # Y coordinate labels are vertical and in figure left side
+            self.AxesLabels[1].set_position(
+                    (0.05, 2 * CANVAS_PADDING * graph_ratio))
+            self.Labels[1].set_position(
+                    (0.05, 1.0 - CANVAS_PADDING * graph_ratio))
+        
+        # Update subplots
         self.Figure.subplots_adjust()
     
-    def GetCanvasMinSize(self):
-        return wx.Size(200, 
-                       CANVAS_BORDER[0] + CANVAS_BORDER[1] + 
-                       2 * CANVAS_PADDING + VALUE_LABEL_HEIGHT * len(self.Items))
-    
-    def SetCanvasSize(self, width, height):
-        height = max(height, self.GetCanvasMinSize()[1])
-        self.SetMinSize(wx.Size(width, height))
-        self.RefreshLabelsPosition(height)
-        self.ParentWindow.RefreshGraphicsSizer()
-        
-    def GetAxesBoundingBox(self, absolute=False):
-        width, height = self.GetSize()
-        ax, ay, aw, ah = self.figure.gca().get_position().bounds
-        bbox = wx.Rect(ax * width, height - (ay + ah) * height - 1,
-                       aw * width + 2, ah * height + 1)
-        if absolute:
-            xw, yw = self.GetPosition()
-            bbox.x += xw
-            bbox.y += yw
-        return bbox
-    
-    def RefreshHighlight(self, x, y):
-        width, height = self.GetSize()
-        bbox = self.GetAxesBoundingBox()
-        if bbox.InsideXY(x, y) and not self.Is3DCanvas():
-            rect = wx.Rect(bbox.x, bbox.y, bbox.width / 2, bbox.height)
-            if rect.InsideXY(x, y):
-                self.SetHighlight(HIGHLIGHT_LEFT)
-            else:
-                self.SetHighlight(HIGHLIGHT_RIGHT)
-        elif y < height / 2:
-            if self.ParentWindow.IsViewerFirst(self):
-                self.SetHighlight(HIGHLIGHT_BEFORE)
-            else:
-                self.SetHighlight(HIGHLIGHT_NONE)
-                self.ParentWindow.HighlightPreviousViewer(self)
-        else:
-            self.SetHighlight(HIGHLIGHT_AFTER)
-    
-    def ResetGraphics(self):
-        self.Figure.clear()
-        if self.Is3DCanvas():
-            self.Axes = self.Figure.gca(projection='3d')
-            self.Axes.set_color_cycle(['b'])
-            self.LastMotionTime = gettime()
-            setattr(self.Axes, "_on_move", self.OnAxesMotion)
-            self.Axes.mouse_init()
-            self.Axes.tick_params(axis='z', labelsize='small')
-        else:
-            self.Axes = self.Figure.gca()
-            self.Axes.set_color_cycle(COLOR_CYCLE)
-        self.Axes.tick_params(axis='x', labelsize='small')
-        self.Axes.tick_params(axis='y', labelsize='small')
-        self.Plots = []
-        self.VLine = None
-        self.HLine = None
-        self.Labels = []
-        self.AxesLabels = []
-        if not self.Is3DCanvas():
-            text_func = self.Axes.text
-        else:
-            text_func = self.Axes.text2D
-        if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
-            num_item = len(self.Items)
-            for idx in xrange(num_item):
-                if num_item == 1:
-                    color = 'k'
-                else:
-                    color = COLOR_CYCLE[idx % len(COLOR_CYCLE)]
-                if not self.Is3DCanvas():
-                    self.AxesLabels.append(
-                        text_func(0, 0, "", size='small',
-                                  verticalalignment='top', 
-                                  color=color,
-                                  transform=self.Axes.transAxes))
-                self.Labels.append(
-                    text_func(0, 0, "", size='large', 
-                              horizontalalignment='right',
-                              color=color,
-                              transform=self.Axes.transAxes))
-        else:
-            self.AxesLabels.append(
-                self.Axes.text(0, 0, "", size='small',
-                               transform=self.Axes.transAxes))
-            self.Labels.append(
-                self.Axes.text(0, 0, "", size='large',
-                               horizontalalignment='right',
-                               transform=self.Axes.transAxes))
-            self.AxesLabels.append(
-                self.Axes.text(0, 0, "", size='small',
-                               rotation='vertical',
-                               verticalalignment='bottom',
-                               transform=self.Axes.transAxes))
-            self.Labels.append(
-                self.Axes.text(0, 0, "", size='large',
-                               rotation='vertical',
-                               verticalalignment='top',
-                               transform=self.Axes.transAxes))
-        width, height = self.GetSize()
-        self.RefreshLabelsPosition(height)
-        
-    def SetCursorTick(self, cursor_tick):
-        self.CursorTick = cursor_tick
-        
     def RefreshViewer(self, refresh_graphics=True):
         
         if refresh_graphics:
@@ -944,9 +1074,7 @@
                     if self.VLine is not None:
                         self.VLine.set_visible(False)
             else:
-                min_start_tick = reduce(max, [item.GetData()[0, 0] 
-                                              for item in self.Items
-                                              if len(item.GetData()) > 0], 0)
+                min_start_tick = max(start_tick, self.GetItemsMinCommonTick())
                 start_tick = max(start_tick, min_start_tick)
                 end_tick = max(end_tick, min_start_tick)
                 items = self.ItemsDict.values()
--- a/controls/DebugVariablePanel/DebugVariableViewer.py	Mon Jun 03 08:41:10 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableViewer.py	Mon Jun 03 11:11:46 2013 +0200
@@ -160,6 +160,15 @@
         for item in self.Items:
             item.ResetData()
     
+    def GetItemsMinCommonTick(self):
+        """
+        Return the minimum tick common to all iems displayed in Viewer
+        @return: Minimum common tick between items
+        """
+        return reduce(max, [item.GetData()[0, 0] 
+                            for item in self.Items
+                            if len(item.GetData()) > 0], 0)
+    
     def RefreshViewer(self):
         """
         Method that refresh the content displayed by Viewer