diff -r d22fcdfae8d0 -r 953a8f14040a controls/DebugVariablePanel/DebugVariableGraphicViewer.py --- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py Mon Jun 03 00:18:13 2013 +0200 +++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py Mon Jun 03 00:30:55 2013 +0200 @@ -42,21 +42,23 @@ from DebugVariableViewer import * from GraphButton import GraphButton +# Graph variable display type GRAPH_PARALLEL, GRAPH_ORTHOGONAL = range(2) -#CANVAS_SIZE_TYPES +# Canvas height [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = [0, 100, 200] -DEFAULT_CANVAS_HEIGHT = 200. -CANVAS_BORDER = (20., 10.) -CANVAS_PADDING = 8.5 -VALUE_LABEL_HEIGHT = 17. -AXES_LABEL_HEIGHT = 12.75 - +CANVAS_BORDER = (20., 10.) # Border height on at bottom and top of graph +CANVAS_PADDING = 8.5 # Border inside graph where no label is drawn +VALUE_LABEL_HEIGHT = 17. # Height of variable label in graph +AXES_LABEL_HEIGHT = 12.75 # Height of variable value in graph + +# Colors used cyclically for graph curves COLOR_CYCLE = ['r', 'b', 'g', 'm', 'y', 'k'] +# Color for graph cursor CURSOR_COLOR = '#800080' -def OrthogonalData(item, start_tick, end_tick): +def OrthogonalDataAndRange(item, start_tick, end_tick): data = item.GetData(start_tick, end_tick) min_value, max_value = item.GetValueRange() if min_value is not None and max_value is not None: @@ -67,23 +69,57 @@ range = 1.0 return data, center - range * 0.55, center + range * 0.55 -class DebugVariableDropTarget(wx.TextDropTarget): +#------------------------------------------------------------------------------- +# Debug Variable Graphic Viewer Drop Target +#------------------------------------------------------------------------------- + +""" +Class that implements a custom drop target class for Debug Variable Graphic Viewer +""" + +class DebugVariableGraphicDropTarget(wx.TextDropTarget): def __init__(self, parent, window): + """ + Constructor + @param parent: Reference to Debug Variable Graphic Viewer + @param window: Reference to the Debug Variable Panel + """ wx.TextDropTarget.__init__(self) self.ParentControl = parent self.ParentWindow = window def __del__(self): + """ + Destructor + """ + # Remove reference to Debug Variable Graphic Viewer and Debug Variable + # Panel self.ParentControl = None self.ParentWindow = None def OnDragOver(self, x, y, d): + """ + Function called when mouse is dragged over Drop Target + @param x: X coordinate of mouse pointer + @param y: Y coordinate of mouse pointer + @param d: Suggested default for return value + """ + # Signal parent that mouse is dragged over self.ParentControl.OnMouseDragging(x, y) + return wx.TextDropTarget.OnDragOver(self, x, y, d) def OnDropText(self, x, y, data): + """ + Function called when mouse is dragged over Drop Target + @param x: X coordinate of mouse pointer + @param y: Y coordinate of mouse pointer + @param data: Text associated to drag'n drop + """ message = None + + # Check that data is valid regarding DebugVariablePanel try: values = eval(data) if not isinstance(values, TupleType): @@ -92,209 +128,640 @@ message = _("Invalid value \"%s\" for debug variable")%data values = None + # Display message if data is invalid if message is not None: wx.CallAfter(self.ShowMessage, message) + # Data contain a reference to a variable to debug elif values[1] == "debug": - width, height = self.ParentControl.GetSize() target_idx = self.ParentControl.GetIndex() - merge_type = GRAPH_PARALLEL - if self.ParentControl.Is3DCanvas(): + + # If mouse is dropped in graph canvas bounding box and graph is + # not 3D canvas, graphs will be merged + rect = self.ParentControl.GetAxesBoundingBox() + if not self.ParentControl.Is3DCanvas() and rect.InsideXY(x, y): + # Default merge type is parallel + merge_type = GRAPH_PARALLEL + + # If mouse is dropped in left part of graph canvas, graph + # wall be merged orthogonally + merge_rect = wx.Rect(rect.x, rect.y, + rect.width / 2., rect.height) + if merge_rect.InsideXY(x, y): + merge_type = GRAPH_ORTHOGONAL + + # Merge graphs + wx.CallAfter(self.ParentWindow.MergeGraphs, + values[0], target_idx, + merge_type, force=True) + + else: + width, height = self.ParentControl.GetSize() + + # Get Before which Viewer the variable has to be moved or added + # according to the position of mouse in Viewer. if y > height / 2: target_idx += 1 - if len(values) > 1 and values[2] == "move": - self.ParentWindow.MoveValue(values[0], target_idx) + + # Drag'n Drop is an internal is an internal move inside Debug + # Variable Panel + if len(values) > 2 and values[2] == "move": + self.ParentWindow.MoveValue(values[0], + target_idx) + + # Drag'n Drop was initiated by another control of Beremiz else: - self.ParentWindow.InsertValue(values[0], target_idx, force=True) - - else: - rect = self.ParentControl.GetAxesBoundingBox() - if rect.InsideXY(x, y): - merge_rect = wx.Rect(rect.x, rect.y, rect.width / 2., rect.height) - if merge_rect.InsideXY(x, y): - merge_type = GRAPH_ORTHOGONAL - wx.CallAfter(self.ParentWindow.MergeGraphs, values[0], target_idx, merge_type, force=True) - else: - if y > height / 2: - target_idx += 1 - if len(values) > 2 and values[2] == "move": - self.ParentWindow.MoveValue(values[0], target_idx) - else: - self.ParentWindow.InsertValue(values[0], target_idx, force=True) + self.ParentWindow.InsertValue(values[0], + target_idx, + force=True) def OnLeave(self): + """ + Function called when mouse is leave Drop Target + """ + # Signal Debug Variable Panel to reset highlight self.ParentWindow.ResetHighlight() return wx.TextDropTarget.OnLeave(self) def ShowMessage(self, message): + """ + Show error message in Error Dialog + @param message: Error message to display + """ dialog = wx.MessageDialog(self.ParentWindow, message, _("Error"), wx.OK|wx.ICON_ERROR) dialog.ShowModal() dialog.Destroy() +#------------------------------------------------------------------------------- +# Debug Variable Graphic Viewer Class +#------------------------------------------------------------------------------- + +""" +Class that implements a Viewer that display variable values as a graphs +""" + class DebugVariableGraphicViewer(DebugVariableViewer, FigureCanvas): def __init__(self, parent, window, items, graph_type): + """ + Constructor + @param parent: Parent wx.Window of DebugVariableText + @param window: Reference to the Debug Variable Panel + @param items: List of DebugVariableItem displayed by Viewer + @param graph_type: Graph display type (Parallel or orthogonal) + """ DebugVariableViewer.__init__(self, window, items) - self.CanvasSize = SIZE_MINI - self.GraphType = graph_type - self.CursorTick = None + self.GraphType = graph_type # Graph type display + self.CursorTick = None # Tick of the graph cursor + + # Mouse position when start dragging self.MouseStartPos = None + # Tick when moving tick start self.StartCursorTick = None - self.CanvasStartSize = None + # Canvas size when starting to resize canvas + self.CanvasStartSize = None + + # List of current displayed contextual buttons self.ContextualButtons = [] + # Reference to item for which contextual buttons was displayed self.ContextualButtonsItem = None + # Create figure for drawing graphs self.Figure = matplotlib.figure.Figure(facecolor='w') - self.Figure.subplotpars.update(top=0.95, left=0.1, bottom=0.1, right=0.95) + # Defined border around figure in canvas + self.Figure.subplotpars.update(top=0.95, left=0.1, + bottom=0.1, right=0.95) FigureCanvas.__init__(self, parent, -1, self.Figure) self.SetWindowStyle(wx.WANTS_CHARS) self.SetBackgroundColour(wx.WHITE) + + # Bind wx events self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) self.Bind(wx.EVT_ENTER_WINDOW, self.OnEnter) self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeave) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) self.Bind(wx.EVT_SIZE, self.OnResize) + # Set canvas min size canvas_size = self.GetCanvasMinSize() self.SetMinSize(canvas_size) - self.SetDropTarget(DebugVariableDropTarget(self, window)) + + # Define Viewer drop target + self.SetDropTarget(DebugVariableGraphicDropTarget(self, window)) + + # Connect matplotlib events self.mpl_connect('button_press_event', self.OnCanvasButtonPressed) self.mpl_connect('motion_notify_event', self.OnCanvasMotion) self.mpl_connect('button_release_event', self.OnCanvasButtonReleased) self.mpl_connect('scroll_event', self.OnCanvasScroll) - for size, bitmap in zip([SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI], - ["minimize_graph", "middle_graph", "maximize_graph"]): - self.Buttons.append(GraphButton(0, 0, bitmap, self.GetOnChangeSizeButton(size))) - for bitmap, callback in [("export_graph_mini", self.OnExportGraphButton), - ("delete_graph", self.OnCloseButton)]: + # Add buttons for changing canvas size with predefined height + for size, bitmap in zip( + [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI], + ["minimize_graph", "middle_graph", "maximize_graph"]): + self.Buttons.append( + GraphButton(0, 0, bitmap, + self.GetOnChangeSizeButton(size))) + + # Add buttons for exporting graph values to clipboard and close graph + for bitmap, callback in [ + ("export_graph_mini", self.OnExportGraphButton), + ("delete_graph", self.OnCloseButton)]: self.Buttons.append(GraphButton(0, 0, bitmap, callback)) + # Update graphs elements self.ResetGraphics() self.RefreshLabelsPosition(canvas_size.height) - self.ShowButtons(False) - - def draw(self, drawDC=None): - FigureCanvasAgg.draw(self) - - self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) - self.bitmap.UseAlpha() - width, height = self.GetSize() - bbox = self.GetAxesBoundingBox() - - destDC = wx.MemoryDC() - destDC.SelectObject(self.bitmap) - - destGC = wx.GCDC(destDC) - - destGC.BeginDrawing() - if self.Highlight == HIGHLIGHT_RESIZE: - destGC.SetPen(HIGHLIGHT_RESIZE_PEN) - destGC.SetBrush(HIGHLIGHT_RESIZE_BRUSH) - destGC.DrawRectangle(0, height - 5, width, 5) - else: - destGC.SetPen(HIGHLIGHT_DROP_PEN) - destGC.SetBrush(HIGHLIGHT_DROP_BRUSH) - if self.Highlight == HIGHLIGHT_LEFT: - destGC.DrawRectangle(bbox.x, bbox.y, - bbox.width / 2, bbox.height) - elif self.Highlight == HIGHLIGHT_RIGHT: - destGC.DrawRectangle(bbox.x + bbox.width / 2, bbox.y, - bbox.width / 2, bbox.height) - - self.DrawCommonElements(destGC, self.GetButtons()) - - destGC.EndDrawing() - - self._isDrawn = True - self.gui_repaint(drawDC=drawDC) + + def AddItem(self, item): + """ + Add an item to the list of items displayed by Viewer + @param item: Item to add to the list + """ + DebugVariableViewer.AddItem(self, item) + self.ResetGraphics() + + def RemoveItem(self, item): + """ + Remove an item from the list of items displayed by Viewer + @param item: Item to remove from the list + """ + DebugVariableViewer.RemoveItem(self, item) + + # If list of items is not empty + if not self.ItemsIsEmpty(): + # Return to parallel graph if there is only one item + # especially if it's actually orthogonal + if len(self.Items) == 1: + self.GraphType = GRAPH_PARALLEL + self.ResetGraphics() + + def SubscribeAllDataConsumers(self): + """ + Function that unsubscribe and remove every item that store values of + a variable that doesn't exist in PLC anymore + """ + DebugVariableViewer.SubscribeAllDataConsumers(self) + if not self.ItemsIsEmpty(): + self.ResetGraphics() + + def Is3DCanvas(self): + """ + Return if Viewer is a 3D canvas + @return: True if Viewer is a 3D canvas + """ + return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3 def GetButtons(self): + """ + Return list of buttons defined in Viewer + @return: List of buttons + """ + # Add contextual buttons to default buttons return self.Buttons + self.ContextualButtons - def PopupContextualButtons(self, item, rect, style=wx.RIGHT): - if self.ContextualButtonsItem is not None and item != self.ContextualButtonsItem: - self.DismissContextualButtons() - + def PopupContextualButtons(self, item, rect, direction=wx.RIGHT): + """ + Show contextual menu for item aside a label of this item defined + by the bounding box of label in figure + @param item: Item for which contextual is shown + @param rect: Bounding box of label aside which drawing menu + @param direction: Direction in which buttons must be drawn + """ + # Return immediately if contextual menu for item is already shown + if self.ContextualButtonsItem == item: + return + + # Close already shown contextual menu + self.DismissContextualButtons() + + # Save item for which contextual menu is shown + self.ContextualButtonsItem = item + + # If item variable is forced, add button for release variable to + # contextual menu + if self.ContextualButtonsItem.IsForced(): + self.ContextualButtons.append( + GraphButton(0, 0, "release", self.OnReleaseItemButton)) + + # Add other buttons to contextual menu + for bitmap, callback in [ + ("force", self.OnForceItemButton), + ("export_graph_mini", self.OnExportItemGraphButton), + ("delete_graph", self.OnRemoveItemButton)]: + self.ContextualButtons.append( + GraphButton(0, 0, bitmap, callback)) + + # If buttons are shown at left side or upper side of rect, positions + # will be set in reverse order + buttons = self.ContextualButtons[:] + if direction in [wx.TOP, wx.LEFT]: + buttons.reverse() + + # Set contextual menu buttons position aside rect depending on + # direction given + offset = 0 + for button in buttons: + w, h = button.GetSize() + if direction in [wx.LEFT, wx.RIGHT]: + x = rect.x + (- w - offset + if direction == wx.LEFT + else rect.width + offset) + y = rect.y + (rect.height - h) / 2 + offset += w + else: + x = rect.x + (rect.width - w ) / 2 + y = rect.y + (- h - offset + if direction == wx.TOP + else rect.height + offset) + offset += h + button.SetPosition(x, y) + button.Show() + + # Refresh canvas + self.ParentWindow.ForceRefresh() + + def DismissContextualButtons(self): + """ + Close current shown contextual menu + """ + # Return immediately if no contextual menu is shown if self.ContextualButtonsItem is None: - self.ContextualButtonsItem = item - - if self.ContextualButtonsItem.IsForced(): - self.ContextualButtons.append( - GraphButton(0, 0, "release", self.OnReleaseButton)) - for bitmap, callback in [("force", self.OnForceButton), - ("export_graph_mini", self.OnExportItemGraphButton), - ("delete_graph", self.OnRemoveItemButton)]: - self.ContextualButtons.append(GraphButton(0, 0, bitmap, callback)) - - offset = 0 - buttons = self.ContextualButtons[:] - if style in [wx.TOP, wx.LEFT]: - buttons.reverse() - for button in buttons: - w, h = button.GetSize() - if style in [wx.LEFT, wx.RIGHT]: - x = rect.x + (- w - offset - if style == wx.LEFT - else rect.width + offset) - y = rect.y + (rect.height - h) / 2 - offset += w - else: - x = rect.x + (rect.width - w ) / 2 - y = rect.y + (- h - offset - if style == wx.TOP - else rect.height + offset) - offset += h - button.SetPosition(x, y) - self.ParentWindow.ForceRefresh() - - def DismissContextualButtons(self): - if self.ContextualButtonsItem is not None: - self.ContextualButtonsItem = None - self.ContextualButtons = [] - self.ParentWindow.ForceRefresh() + return + + # Reset variables corresponding to contextual menu + self.ContextualButtonsItem = None + self.ContextualButtons = [] + + # Refresh canvas + self.ParentWindow.ForceRefresh() def IsOverContextualButton(self, x, y): + """ + Return if point is over one contextual button of Viewer + @param x: X coordinate of point + @param y: Y coordinate of point + @return: contextual button where point is over + """ for button in self.ContextualButtons: if button.HitTest(x, y): - return True - return False - - def SetMinSize(self, size): - wx.Window.SetMinSize(self, size) - wx.CallAfter(self.RefreshButtonsPosition) - - def GetOnChangeSizeButton(self, size): + return button + return None + + def ExportGraph(self, item=None): + """ + Export item(s) data to clipboard in CSV format + @param item: Item from which data to export, all items if None + (default None) + """ + self.ParentWindow.CopyDataToClipboard( + [(item, [entry for entry in item.GetData()]) + for item in (self.Items + if item is None + else [item])]) + + def GetOnChangeSizeButton(self, height): + """ + Function that generate callback function for change Viewer height to + pre-defined height button + @param height: Height that change Viewer to + @return: callback function + """ def OnChangeSizeButton(): - self.CanvasSize = size - self.SetCanvasSize(200, self.CanvasSize) + self.SetCanvasSize(200, height) return OnChangeSizeButton def OnExportGraphButton(self): + """ + Function called when Viewer Export button is pressed + """ + # Export data of every item in Viewer self.ExportGraph() - def OnForceButton(self): + def OnForceItemButton(self): + """ + Function called when contextual menu Force button is pressed + """ + # Open dialog for forcing item variable value self.ForceValue(self.ContextualButtonsItem) + # Close contextual menu self.DismissContextualButtons() - def OnReleaseButton(self): + def OnReleaseItemButton(self): + """ + Function called when contextual menu Release button is pressed + """ + # Release item variable value self.ReleaseValue(self.ContextualButtonsItem) + # Close contextual menu self.DismissContextualButtons() def OnExportItemGraphButton(self): + """ + Function called when contextual menu Export button is pressed + """ + # Export data of item variable self.ExportGraph(self.ContextualButtonsItem) + # Close contextual menu self.DismissContextualButtons() def OnRemoveItemButton(self): + """ + Function called when contextual menu Remove button is pressed + """ + # Remove item from Viewer wx.CallAfter(self.ParentWindow.DeleteValue, self, self.ContextualButtonsItem) + # Close contextual menu self.DismissContextualButtons() + def HandleCursorMove(self, event): + start_tick, end_tick = self.ParentWindow.GetRange() + cursor_tick = None + items = self.ItemsDict.values() + if self.GraphType == GRAPH_ORTHOGONAL: + x_data = items[0].GetData(start_tick, end_tick) + y_data = items[1].GetData(start_tick, end_tick) + 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) + cursor_tick = x_data[numpy.argmin(d), 0] + else: + data = items[0].GetData(start_tick, end_tick) + if len(data) > 0: + cursor_tick = data[numpy.argmin(numpy.abs(data[:,0] - event.xdata)), 0] + if cursor_tick is not None: + self.ParentWindow.SetCursorTick(cursor_tick) + + def OnCanvasButtonPressed(self, event): + """ + Function called when a button of mouse is pressed + @param event: Mouse event + """ + # Get mouse position, graph coordinates are inverted comparing to wx + # coordinates + width, height = self.GetSize() + x, y = event.x, height - event.y + + # Return immediately if mouse is over a button + if self.IsOverButton(x, y): + return + + # Mouse was clicked inside graph figure + if event.inaxes == self.Axes: + + # Find if it was on an item label + item_idx = None + # Check every label paired with corresponding item + for i, t in ([pair for pair in enumerate(self.AxesLabels)] + + [pair for pair in enumerate(self.Labels)]): + # Get label bounding box + (x0, y0), (x1, y1) = t.get_window_extent().get_points() + rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) + # Check if mouse was over label + if rect.InsideXY(x, y): + item_idx = i + break + + # If an item label have been clicked + if item_idx is not None: + # Hide buttons and contextual buttons + self.ShowButtons(False) + self.DismissContextualButtons() + + # Start a drag'n drop from mouse position in wx coordinate of + # parent + xw, yw = self.GetPosition() + self.ParentWindow.StartDragNDrop(self, + self.ItemsDict.values()[item_idx], + x + xw, y + yw, # Current mouse position + x + xw, y + yw) # Mouse position when button was clicked + + # Don't handle mouse button if canvas is 3D and let matplotlib do + # the default behavior (rotate 3D axes) + elif not self.Is3DCanvas(): + # Save mouse position when clicked + self.MouseStartPos = wx.Point(x, y) + + # Mouse button was left button, start moving cursor + if event.button == 1: + # Save current tick in case a drag'n drop is initiate to + # restore it + self.StartCursorTick = self.CursorTick + + self.HandleCursorMove(event) + + # Mouse button is middle button and graph is parallel, start + # moving graph along X coordinate (tick) + elif event.button == 2 and self.GraphType == GRAPH_PARALLEL: + self.StartCursorTick = self.ParentWindow.GetRange()[0] + + # Mouse was clicked outside graph figure and over resize highlight with + # left button, start resizing Viewer + elif event.button == 1 and event.y <= 5: + self.MouseStartPos = wx.Point(x, y) + self.CanvasStartSize = height + + def OnCanvasButtonReleased(self, event): + """ + Function called when a button of mouse is released + @param event: Mouse event + """ + # If a drag'n drop is in progress, stop it + if self.ParentWindow.IsDragging(): + width, height = self.GetSize() + xw, yw = self.GetPosition() + item = self.ParentWindow.DraggingAxesPanel.ItemsDict.values()[0] + # Give mouse position in wx coordinate of parent + self.ParentWindow.StopDragNDrop(item.GetVariable(), + xw + event.x, yw + height - event.y) + + else: + # Reset any move in progress + self.MouseStartPos = None + self.StartCursorTick = None + self.CanvasStartSize = None + + # Handle button under mouse if it exist + width, height = self.GetSize() + self.HandleButton(event.x, height - event.y) + + def OnCanvasMotion(self, event): + """ + Function called when a button of mouse is moved over Viewer + @param event: Mouse event + """ + width, height = self.GetSize() + + # If a drag'n drop is in progress, move canvas dragged + if self.ParentWindow.IsDragging(): + xw, yw = self.GetPosition() + # Give mouse position in wx coordinate of parent + self.ParentWindow.MoveDragNDrop( + xw + event.x, + yw + height - event.y) + + # If a Viewer resize is in progress, change Viewer size + elif event.button == 1 and self.CanvasStartSize is not None: + width, height = self.GetSize() + self.SetCanvasSize(width, + self.CanvasStartSize + height - event.y - self.MouseStartPos.y) + + # If no button is pressed, show or hide contextual buttons or resize + # highlight + elif event.button is None: + # Compute direction for items label according graph type + if self.GraphType == GRAPH_PARALLEL: # Graph is parallel + directions = [wx.RIGHT] * len(self.AxesLabels) + \ + [wx.LEFT] * len(self.Labels) + elif len(self.AxesLabels) > 0: # Graph is orthogonal in 2D + directions = [wx.RIGHT, wx.TOP, # Directions for AxesLabels + wx.LEFT, wx.BOTTOM] # Directions for Labels + else: # Graph is orthogonal in 3D + directions = [wx.LEFT] * len(self.Labels) + + # Find if mouse is over an item label + item_idx = None + menu_direction = None + for (i, t), dir in zip( + [pair for pair in enumerate(self.AxesLabels)] + + [pair for pair in enumerate(self.Labels)], + directions): + # Check every label paired with corresponding item + (x0, y0), (x1, y1) = t.get_window_extent().get_points() + rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) + # Check if mouse was over label + if rect.InsideXY(event.x, height - event.y): + item_idx = i + menu_direction = dir + break + + # If mouse is over an item label, + if item_idx is not None: + self.PopupContextualButtons( + self.ItemsDict.values()[item_idx], + rect, menu_direction) + return + + # If mouse isn't over a contextual menu, hide the current shown one + # if it exists + if self.IsOverContextualButton(event.x, height - event.y) is None: + self.DismissContextualButtons() + + # Update resize highlight + if event.y <= 5: + if self.SetHighlight(HIGHLIGHT_RESIZE): + self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) + self.ParentWindow.ForceRefresh() + else: + if self.SetHighlight(HIGHLIGHT_NONE): + self.SetCursor(wx.NullCursor) + self.ParentWindow.ForceRefresh() + + # Handle buttons if canvas is not 3D + elif not self.Is3DCanvas(): + + # If left button is pressed + if event.button == 1: + + # Mouse is inside graph figure + if event.inaxes == self.Axes: + + # If a cursor move is in progress, update cursor position + if self.MouseStartPos is not None: + self.HandleCursorMove(event) + + # Mouse is outside graph figure, cursor move is in progress and + # there is only one item in Viewer, start a drag'n drop + elif self.MouseStartPos is not None and len(self.Items) == 1: + xw, yw = self.GetPosition() + self.ParentWindow.SetCursorTick(self.StartCursorTick) + self.ParentWindow.StartDragNDrop(self, + self.ItemsDict.values()[0], + # Current mouse position + event.x + xw, height - event.y + yw, + # Mouse position when button was clicked + self.MouseStartPos.x + xw, + self.MouseStartPos.y + yw) + + # If middle button is pressed and moving graph along X coordinate + # is in progress + elif event.button == 2 and self.GraphType == GRAPH_PARALLEL and \ + self.MouseStartPos is not None: + start_tick, end_tick = self.ParentWindow.GetRange() + rect = self.GetAxesBoundingBox() + + # Move graph along X coordinate + self.ParentWindow.SetCanvasPosition( + self.StartCursorTick + + (self.MouseStartPos.x - event.x) * + (end_tick - start_tick) / rect.width) + + def OnCanvasScroll(self, event): + """ + Function called when a wheel mouse is use in Viewer + @param event: Mouse event + """ + # Change X range of graphs if mouse is in canvas figure and ctrl is + # pressed + if event.inaxes is not None and event.guiEvent.ControlDown(): + + # Calculate position of fixed tick point according to graph type + # and mouse position + if self.GraphType == GRAPH_ORTHOGONAL: + start_tick, end_tick = self.ParentWindow.GetRange() + tick = (start_tick + end_tick) / 2. + else: + tick = event.xdata + self.ParentWindow.ChangeRange(int(-event.step) / 3, tick) + + # 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, + wx.WXK_RIGHT: 1, + wx.WXK_UP: -10, + wx.WXK_DOWN: 10} + + def OnKeyDown(self, event): + """ + Function called when key is pressed + @param event: wx.KeyEvent + """ + # If cursor is shown and arrow key is pressed, move cursor tick + if self.CursorTick is not None: + move = self.KEY_CURSOR_INCREMENT.get(event.GetKeyCode(), None) + if move is not None: + self.ParentWindow.MoveCursorTick(move) + event.Skip() + def OnLeave(self, event): - if self.Highlight != HIGHLIGHT_RESIZE or self.CanvasStartSize is None: + """ + Function called when mouse leave Viewer + @param event: wx.MouseEvent + """ + # If Viewer is not resizing, reset resize highlight + if self.CanvasStartSize is None: + self.SetHighlight(HIGHLIGHT_NONE) + self.SetCursor(wx.NullCursor) DebugVariableViewer.OnLeave(self, event) else: event.Skip() @@ -335,7 +802,6 @@ height = max(height, self.GetCanvasMinSize()[1]) self.SetMinSize(wx.Size(width, height)) self.RefreshLabelsPosition(height) - self.RefreshButtonsPosition() self.ParentWindow.RefreshGraphicsSizer() def GetAxesBoundingBox(self, absolute=False): @@ -349,132 +815,6 @@ bbox.y += yw return bbox - def OnCanvasButtonPressed(self, event): - width, height = self.GetSize() - x, y = event.x, height - event.y - if not self.IsOverButton(x, y): - if event.inaxes == self.Axes: - item_idx = None - for i, t in ([pair for pair in enumerate(self.AxesLabels)] + - [pair for pair in enumerate(self.Labels)]): - (x0, y0), (x1, y1) = t.get_window_extent().get_points() - rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) - if rect.InsideXY(x, y): - item_idx = i - break - if item_idx is not None: - self.ShowButtons(False) - self.DismissContextualButtons() - xw, yw = self.GetPosition() - self.ParentWindow.StartDragNDrop(self, - self.ItemsDict.values()[item_idx], x + xw, y + yw, x + xw, y + yw) - elif not self.Is3DCanvas(): - self.MouseStartPos = wx.Point(x, y) - if event.button == 1 and event.inaxes == self.Axes: - self.StartCursorTick = self.CursorTick - self.HandleCursorMove(event) - elif event.button == 2 and self.GraphType == GRAPH_PARALLEL: - width, height = self.GetSize() - start_tick, end_tick = self.ParentWindow.GetRange() - self.StartCursorTick = start_tick - - elif event.button == 1 and event.y <= 5: - self.MouseStartPos = wx.Point(x, y) - self.CanvasStartSize = self.GetSize() - - def OnCanvasButtonReleased(self, event): - if self.ParentWindow.IsDragging(): - width, height = self.GetSize() - xw, yw = self.GetPosition() - self.ParentWindow.StopDragNDrop( - self.ParentWindow.DraggingAxesPanel.ItemsDict.values()[0].GetVariable(), - xw + event.x, - yw + height - event.y) - else: - self.MouseStartPos = None - self.StartCursorTick = None - self.CanvasStartSize = None - width, height = self.GetSize() - self.HandleButton(event.x, height - event.y) - if event.y > 5 and self.SetHighlight(HIGHLIGHT_NONE): - self.SetCursor(wx.NullCursor) - self.ParentWindow.ForceRefresh() - - def OnCanvasMotion(self, event): - width, height = self.GetSize() - if self.ParentWindow.IsDragging(): - xw, yw = self.GetPosition() - self.ParentWindow.MoveDragNDrop( - xw + event.x, - yw + height - event.y) - else: - if not self.Is3DCanvas(): - if event.button == 1 and self.CanvasStartSize is None: - if event.inaxes == self.Axes: - if self.MouseStartPos is not None: - self.HandleCursorMove(event) - elif self.MouseStartPos is not None and len(self.Items) == 1: - xw, yw = self.GetPosition() - self.ParentWindow.SetCursorTick(self.StartCursorTick) - self.ParentWindow.StartDragNDrop(self, - self.ItemsDict.values()[0], - event.x + xw, height - event.y + yw, - self.MouseStartPos.x + xw, self.MouseStartPos.y + yw) - elif event.button == 2 and self.GraphType == GRAPH_PARALLEL and self.MouseStartPos is not None: - start_tick, end_tick = self.ParentWindow.GetRange() - rect = self.GetAxesBoundingBox() - self.ParentWindow.SetCanvasPosition( - self.StartCursorTick + (self.MouseStartPos.x - event.x) * - (end_tick - start_tick) / rect.width) - - if event.button == 1 and self.CanvasStartSize is not None: - width, height = self.GetSize() - self.SetCanvasSize(width, - self.CanvasStartSize.height + height - event.y - self.MouseStartPos.y) - - elif event.button in [None, "up", "down"]: - if self.GraphType == GRAPH_PARALLEL: - orientation = [wx.RIGHT] * len(self.AxesLabels) + [wx.LEFT] * len(self.Labels) - elif len(self.AxesLabels) > 0: - orientation = [wx.RIGHT, wx.TOP, wx.LEFT, wx.BOTTOM] - else: - orientation = [wx.LEFT] * len(self.Labels) - item_idx = None - item_style = None - for (i, t), style in zip([pair for pair in enumerate(self.AxesLabels)] + - [pair for pair in enumerate(self.Labels)], - orientation): - (x0, y0), (x1, y1) = t.get_window_extent().get_points() - rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0) - if rect.InsideXY(event.x, height - event.y): - item_idx = i - item_style = style - break - if item_idx is not None: - self.PopupContextualButtons(self.ItemsDict.values()[item_idx], rect, item_style) - return - if not self.IsOverContextualButton(event.x, height - event.y): - self.DismissContextualButtons() - - if event.y <= 5: - if self.SetHighlight(HIGHLIGHT_RESIZE): - self.SetCursor(wx.StockCursor(wx.CURSOR_SIZENS)) - self.ParentWindow.ForceRefresh() - else: - if self.SetHighlight(HIGHLIGHT_NONE): - self.SetCursor(wx.NullCursor) - self.ParentWindow.ForceRefresh() - - def OnCanvasScroll(self, event): - if event.inaxes is not None and event.guiEvent.ControlDown(): - if self.GraphType == GRAPH_ORTHOGONAL: - start_tick, end_tick = self.ParentWindow.GetRange() - tick = (start_tick + end_tick) / 2. - else: - tick = event.xdata - self.ParentWindow.ChangeRange(int(-event.step) / 3, tick) - self.ParentWindow.VetoScrollEvent = True - def RefreshHighlight(self, x, y): width, height = self.GetSize() bbox = self.GetAxesBoundingBox() @@ -493,49 +833,6 @@ else: self.SetHighlight(HIGHLIGHT_AFTER) - def OnLeave(self, event): - if self.CanvasStartSize is None and self.SetHighlight(HIGHLIGHT_NONE): - self.SetCursor(wx.NullCursor) - self.ParentWindow.ForceRefresh() - DebugVariableViewer.OnLeave(self, event) - - KEY_CURSOR_INCREMENT = { - wx.WXK_LEFT: -1, - wx.WXK_RIGHT: 1, - wx.WXK_UP: -10, - wx.WXK_DOWN: 10} - def OnKeyDown(self, event): - if self.CursorTick is not None: - move = self.KEY_CURSOR_INCREMENT.get(event.GetKeyCode(), None) - if move is not None: - self.ParentWindow.MoveCursorTick(move) - event.Skip() - - def HandleCursorMove(self, event): - start_tick, end_tick = self.ParentWindow.GetRange() - cursor_tick = None - items = self.ItemsDict.values() - if self.GraphType == GRAPH_ORTHOGONAL: - x_data = items[0].GetData(start_tick, end_tick) - y_data = items[1].GetData(start_tick, end_tick) - 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) - cursor_tick = x_data[numpy.argmin(d), 0] - else: - data = items[0].GetData(start_tick, end_tick) - if len(data) > 0: - cursor_tick = data[numpy.argmin(numpy.abs(data[:,0] - event.xdata)), 0] - if cursor_tick is not None: - self.ParentWindow.SetCursorTick(cursor_tick) - - def OnAxesMotion(self, event): - if self.Is3DCanvas(): - current_time = gettime() - if current_time - self.LastMotionTime > REFRESH_PERIOD: - self.LastMotionTime = current_time - Axes3D._on_move(self.Axes, event) - def ResetGraphics(self): self.Figure.clear() if self.Is3DCanvas(): @@ -598,25 +895,6 @@ width, height = self.GetSize() self.RefreshLabelsPosition(height) - def AddItem(self, item): - DebugVariableViewer.AddItem(self, item) - self.ResetGraphics() - - def RemoveItem(self, item): - DebugVariableViewer.RemoveItem(self, item) - if not self.ItemsIsEmpty(): - if len(self.Items) == 1: - self.GraphType = GRAPH_PARALLEL - self.ResetGraphics() - - def SubscribeAllDataConsumers(self): - DebugVariableViewer.SubscribeAllDataConsumers(self) - if not self.ItemsIsEmpty(): - self.ResetGraphics() - - def Is3DCanvas(self): - return self.GraphType == GRAPH_ORTHOGONAL and len(self.Items) == 3 - def SetCursorTick(self, cursor_tick): self.CursorTick = cursor_tick @@ -672,8 +950,8 @@ start_tick = max(start_tick, min_start_tick) end_tick = max(end_tick, min_start_tick) items = self.ItemsDict.values() - x_data, x_min, x_max = OrthogonalData(items[0], start_tick, end_tick) - y_data, y_min, y_max = OrthogonalData(items[1], start_tick, end_tick) + x_data, x_min, x_max = OrthogonalDataAndRange(items[0], start_tick, end_tick) + y_data, y_min, y_max = OrthogonalDataAndRange(items[1], start_tick, end_tick) if self.CursorTick is not None: x_cursor, x_forced = items[0].GetValue(self.CursorTick, raw=True) y_cursor, y_forced = items[1].GetValue(self.CursorTick, raw=True) @@ -710,7 +988,7 @@ else: while len(self.Axes.lines) > 0: self.Axes.lines.pop() - z_data, z_min, z_max = OrthogonalData(items[2], start_tick, end_tick) + z_data, z_min, z_max = OrthogonalDataAndRange(items[2], start_tick, end_tick) if self.CursorTick is not None: z_cursor, z_forced = items[2].GetValue(self.CursorTick, raw=True) if x_data is not None and y_data is not None and z_data is not None: @@ -754,10 +1032,56 @@ self.draw() - def ExportGraph(self, item=None): - if item is not None: - variables = [(item, [entry for entry in item.GetData()])] - else: - variables = [(item, [entry for entry in item.GetData()]) - for item in self.Items] - self.ParentWindow.CopyDataToClipboard(variables) + def draw(self, drawDC=None): + """ + Render the figure. + """ + # Render figure using agg + FigureCanvasAgg.draw(self) + + # Get bitmap of figure rendered + self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) + self.bitmap.UseAlpha() + + # Create DC for rendering graphics in bitmap + destDC = wx.MemoryDC() + destDC.SelectObject(self.bitmap) + + # Get Graphics Context for DC, for anti-aliased and transparent + # rendering + destGC = wx.GCDC(destDC) + + destGC.BeginDrawing() + + # Get canvas size and figure bounding box in canvas + width, height = self.GetSize() + bbox = self.GetAxesBoundingBox() + + # If highlight to display is resize, draw thick grey line at bottom + # side of canvas + if self.Highlight == HIGHLIGHT_RESIZE: + destGC.SetPen(HIGHLIGHT_RESIZE_PEN) + destGC.SetBrush(HIGHLIGHT_RESIZE_BRUSH) + destGC.DrawRectangle(0, height - 5, width, 5) + + # If highlight to display is merging graph, draw 50% transparent blue + # rectangle on left or right part of figure depending on highlight type + elif self.Highlight in [HIGHLIGHT_LEFT, HIGHLIGHT_RIGHT]: + destGC.SetPen(HIGHLIGHT_DROP_PEN) + destGC.SetBrush(HIGHLIGHT_DROP_BRUSH) + + x_offset = (bbox.width / 2 + if self.Highlight == HIGHLIGHT_RIGHT + else 0) + destGC.DrawRectangle(bbox.x + x_offset, bbox.y, + bbox.width / 2, bbox.height) + + # Draw other Viewer common elements + self.DrawCommonElements(destGC, self.GetButtons()) + + destGC.EndDrawing() + + self._isDrawn = True + self.gui_repaint(drawDC=drawDC) + +