def __init__(self, parent=None, width=9, height=6, dpi=None): self.fig = Figure(figsize=(width, height), facecolor=(.94,.94,.94), dpi=dpi) FigureCanvas.__init__(self, self.fig) self.setParent(parent) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) self.axes = self.fig.add_axes([0.06, 0.10, 0.9, 0.85], facecolor=(.94,.94,.94)) self.compute_initial_figure() # horizontal x range span axxspan = self.fig.add_axes([0.06, 0.05, 0.9, 0.02]) axxspan.axis([-0.2, 1.2, -0.2, 1.2]) axxspan.tick_params('y', labelright = False ,labelleft = False ,length=0) self.xspan = SpanSelector(axxspan, self.onselectX, 'horizontal',useblit=True, span_stays=True, rectprops=dict(alpha=0.5, facecolor='blue')) # vertical y range span axyspan = self.fig.add_axes([0.02, 0.10, 0.01, 0.85]) axyspan.axis([-0.2, 1.2, -0.2, 1.2]) axyspan.tick_params('x', labelbottom = False ,labeltop = False ,length=0) self.yspan = SpanSelector(axyspan, self.onselectY, 'vertical',useblit=True, span_stays=True, rectprops=dict(alpha=0.5, facecolor='blue')) # reset x y spans axReset = self.fig.add_axes([0.01, 0.05, 0.03, 0.03],frameon=False, ) self.bnReset = Button(axReset, 'Reset') self.bnReset.on_clicked( self.xyReset ) # contextMenu acExportPlot = QAction(self.tr("Export plot"), self) FigureCanvas.connect(acExportPlot,SIGNAL('triggered()'), self, SLOT('exportPlot()') ) FigureCanvas.addAction(self, acExportPlot ) FigureCanvas.setContextMenuPolicy(self, Qt.ActionsContextMenu )
def __init__(self, parent=None, width=9, height=6, dpi=None): self.fig = Figure(figsize=(width, height), facecolor=(.94,.94,.94), dpi=dpi) FigureCanvas.__init__(self, self.fig) self.setParent(parent) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) # contextMenu acExportPlot = QAction(self.tr("Export plot"), self) FigureCanvas.connect(acExportPlot,SIGNAL('triggered()'), self, SLOT('exportPlot()') ) FigureCanvas.addAction(self, acExportPlot ) FigureCanvas.setContextMenuPolicy(self, Qt.ActionsContextMenu )
def __init__(self, parent=None, width=9, height=6, dpi=None): self.fig = Figure(figsize=(width, height), facecolor=(.94, .94, .94), dpi=dpi) FigureCanvas.__init__(self, self.fig) self.setParent(parent) FigureCanvas.setSizePolicy(self, QSizePolicy.Expanding, QSizePolicy.Expanding) FigureCanvas.updateGeometry(self) # contextMenu acExportPlot = QAction(self.tr("Export plot"), self) FigureCanvas.connect(acExportPlot, SIGNAL('triggered()'), self, SLOT('exportPlot()')) FigureCanvas.addAction(self, acExportPlot) FigureCanvas.setContextMenuPolicy(self, Qt.ActionsContextMenu)
class XYView(View): AUTOFIT_MARGIN = 0.03 # 3% # See http://matplotlib.org/api/markers_api.html: CURVE_MARKERS = [ "o" ,# circle "*", # star "+", # plus "x", # x "s", # square "p", # pentagon "h", # hexagon1 "8", # octagon "D", # diamond "^", # triangle_up "<", # triangle_left ">", # triangle_right "1", # tri_down "2", # tri_up "3", # tri_left "4", # tri_right "v", # triangle_down "H", # hexagon2 "d", # thin diamond "", # NO MARKER ] _DEFAULT_LEGEND_STATE = False # for test purposes mainly - initial status of the legend def __init__(self, controller): View.__init__(self, controller) self._eventHandler = EventHandler() self._curveViews = {} # key: curve (model) ID, value: CurveView self._salomeViewID = None self._mplFigure = None self._mplAxes = None self._mplCanvas = None self._plotWidget = None self._sgPyQt = self._controller._sgPyQt self._toolbar = None self._mplNavigationActions = {} self._toobarMPL = None self._grid = None self._currCrv = None # current curve selected in the view self._legend = None self._legendLoc = "right" # "right" or "bottom" self._fitArea = False self._zoomPan = False self._dragOnDrop = False self._move = False self._patch = None self._xdata = None self._ydata = None self._defaultLineStyle = None self._last_point = None self._lastMarkerID = -1 self._blockLogSignal = False self._axisXSciNotation = False self._axisYSciNotation = False self._prevTitle = None def __repaintOK(self): """ To be called inside XYView each time a low-level expansive matplotlib methods is to be invoked. @return False if painting is currently locked, in which case it will also register the current XYView as needing a refresh when unlocked """ ret = self._controller._plotManager.isRepaintLocked() if ret: self._controller._plotManager.registerRepaint(self._model) return (not ret) def appendCurve(self, curveID): newC = CurveView(self._controller, self) newC.setModel(self._model._curves[curveID]) newC.setMPLAxes(self._mplAxes) newC.draw() newC.setMarker(self.getMarker(go_next=True)) self._curveViews[curveID] = newC def removeCurve(self, curveID): v = self._curveViews.pop(curveID) v.erase() if self._currCrv is not None and self._currCrv.getID() == curveID: self._currCrv = None def cleanBeforeClose(self): """ Clean some items to avoid accumulating stuff in memory """ self._mplFigure.clear() plt.close(self._mplFigure) self._plotWidget.clearAll() # For memory debugging only: import gc gc.collect() def repaint(self): if self.__repaintOK(): Logger.Debug("XYView::draw") self._mplCanvas.draw() def onXLabelChange(self): if self.__repaintOK(): self._mplAxes.set_xlabel(self._model._xlabel) self.repaint() def onYLabelChange(self): if self.__repaintOK(): self._mplAxes.set_ylabel(self._model._ylabel) self.repaint() def onTitleChange(self): if self.__repaintOK(): self._mplAxes.set_title(self._model._title) self.updateViewTitle() self.repaint() def onCurveTitleChange(self): # Updating the legend should suffice self.showHideLegend() def onClearAll(self): """ Just does an update with a reset of the marker cycle. """ if self.__repaintOK(): self._lastMarkerID = -1 self.update() def onPick(self, event): """ MPL callback when picking """ if event.mouseevent.button == 1: selected_id = -1 a = event.artist for crv_id, cv in self._curveViews.items(): if cv._mplLines[0] is a: selected_id = crv_id # Use the plotmanager so that other plot sets get their current reset: self._controller._plotManager.setCurrentCurve(selected_id) def createAndAddLocalAction(self, icon_file, short_name): return self._toolbar.addAction(self._sgPyQt.loadIcon("CURVEPLOT", icon_file), short_name) def createPlotWidget(self): self._mplFigure = Figure((8.0,5.0), dpi=100) self._mplCanvas = FigureCanvasQTAgg(self._mplFigure) self._mplCanvas.installEventFilter(self._eventHandler) self._mplCanvas.mpl_connect('pick_event', self.onPick) self._mplAxes = self._mplFigure.add_subplot(1, 1, 1) self._plotWidget = PlotWidget() self._toobarMPL = NavigationToolbar2QT(self._mplCanvas, None) for act in self._toobarMPL.actions(): actionName = str(act.text()).strip() self._mplNavigationActions[actionName] = act self._plotWidget.setCentralWidget(self._mplCanvas) self._toolbar = self._plotWidget.toolBar self.populateToolbar() self._popupMenu = QtGui.QMenu() self._popupMenu.addAction(self._actionLegend) # Connect evenement for the graphic scene self._mplCanvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._mplCanvas.customContextMenuRequested.connect(self.onContextMenu) self._mplCanvas.mpl_connect('scroll_event', self.onScroll) self._mplCanvas.mpl_connect('button_press_event', self.onMousePress) def populateToolbar(self): # Action to dump view in a file a = self.createAndAddLocalAction("dump_view.png", trQ("DUMP_VIEW_TXT")) a.triggered.connect(self.dumpView) self._toolbar.addSeparator() # Actions to manipulate the scene a = self.createAndAddLocalAction("fit_all.png", trQ("FIT_ALL_TXT")) a.triggered.connect(self.autoFit) # Zoom and pan are mutually exclusive but can be both de-activated: self._zoomAction = self.createAndAddLocalAction("fit_area.png", trQ("FIT_AREA_TXT")) self._zoomAction.triggered.connect(self.zoomArea) self._zoomAction.setCheckable(True) self._panAction = self.createAndAddLocalAction("zoom_pan.png", trQ("ZOOM_PAN_TXT")) self._panAction.triggered.connect(self.pan) self._panAction.setCheckable(True) self._toolbar.addSeparator() # Actions to change the representation of curves self._curveActionGroup = QtGui.QActionGroup(self._plotWidget) self._pointsAction = self.createAndAddLocalAction("draw_points.png", trQ("DRAW_POINTS_TXT")) self._pointsAction.setCheckable(True) self._linesAction = self.createAndAddLocalAction("draw_lines.png", trQ("DRAW_LINES_TXT")) self._linesAction.setCheckable(True) self._curveActionGroup.addAction(self._pointsAction) self._curveActionGroup.addAction(self._linesAction) self._linesAction.setChecked(True) self._curveActionGroup.triggered.connect(self.changeModeCurve) self._curveActionGroup.setExclusive(True) self._toolbar.addSeparator() # Actions to draw horizontal curves as linear or logarithmic self._horActionGroup = QtGui.QActionGroup(self._plotWidget) self._horLinearAction = self.createAndAddLocalAction("hor_linear.png", trQ("HOR_LINEAR_TXT")) self._horLinearAction.setCheckable(True) self._horLogarithmicAction = self.createAndAddLocalAction("hor_logarithmic.png", trQ("HOR_LOGARITHMIC_TXT")) self._horLogarithmicAction.setCheckable(True) self._horActionGroup.addAction(self._horLinearAction) self._horActionGroup.addAction(self._horLogarithmicAction) self._horLinearAction.setChecked(True) self._horActionGroup.triggered.connect(self.onViewHorizontalMode) self._toolbar.addSeparator() # Actions to draw vertical curves as linear or logarithmic self._verActionGroup = QtGui.QActionGroup(self._plotWidget) self._verLinearAction = self.createAndAddLocalAction("ver_linear.png", trQ("VER_LINEAR_TXT")) self._verLinearAction.setCheckable(True) self._verLogarithmicAction = self.createAndAddLocalAction("ver_logarithmic.png", trQ("VER_LOGARITHMIC_TXT")) self._verLogarithmicAction.setCheckable(True) self._verActionGroup.addAction(self._verLinearAction) self._verActionGroup.addAction(self._verLogarithmicAction) self._verLinearAction.setChecked(True) self._verActionGroup.triggered.connect(self.onViewVerticalMode) self._verActionGroup.setExclusive(True) self._toolbar.addSeparator() # Action to show or hide the legend self._actionLegend = self.createAndAddLocalAction("legend.png", trQ("SHOW_LEGEND_TXT")) self._actionLegend.setCheckable(True) self._actionLegend.triggered.connect(self.showHideLegend) if self._DEFAULT_LEGEND_STATE: self._actionLegend.setChecked(True) self._toolbar.addSeparator() # Action to set the preferences a = self.createAndAddLocalAction("settings.png", trQ("SETTINGS_TXT")) a.triggered.connect(self.onSettings) pass def dumpView(self): # Choice of the view backup file filters = [] for form in ["IMAGES_FILES", "PDF_FILES", "POSTSCRIPT_FILES", "ENCAPSULATED_POSTSCRIPT_FILES"]: filters.append(trQ(form)) fileName = self._sgPyQt.getFileName(self._sgPyQt.getDesktop(), "", filters, trQ("DUMP_VIEW_FILE"), False ) if not fileName.isEmpty(): name = str(fileName) self._mplAxes.figure.savefig(name) pass def autoFit(self, check=True, repaint=True): if self.__repaintOK(): self._mplAxes.relim() xm, xM = self._mplAxes.xaxis.get_data_interval() ym, yM = self._mplAxes.yaxis.get_data_interval() i = yM-ym self._mplAxes.axis([xm, xM, ym-i*self.AUTOFIT_MARGIN, yM+i*self.AUTOFIT_MARGIN]) if repaint: self.repaint() def zoomArea(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._panAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Zoom"].trigger() def pan(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._zoomAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Pan"].trigger() def getMarker(self, go_next=False): if go_next: self._lastMarkerID = (self._lastMarkerID+1) % len(self.CURVE_MARKERS) return self.CURVE_MARKERS[self._lastMarkerID] def changeModeCurve(self, repaint=True): if not self.__repaintOK(): return action = self._curveActionGroup.checkedAction() if action is self._pointsAction : for crv_view in self._curveViews.values(): crv_view.setLineStyle("None") elif action is self._linesAction : for crv_view in self._curveViews.values(): crv_view.setLineStyle("-") else : raise NotImplementedError if repaint: self.repaint() def setXLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_xscale('log') self._horLogarithmicAction.setChecked(True) else: self._mplAxes.set_xscale('linear') self._horLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setYLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_yscale('log') self._verLogarithmicAction.setChecked(True) else: self._mplAxes.set_yscale('linear') self._verLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setXSciNotation(self, sciNotation, repaint=True): self._axisXSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def setYSciNotation(self, sciNotation, repaint=True): self._axisYSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def onViewHorizontalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._horActionGroup.checkedAction() if action is self._horLinearAction: self.setXLog(False, repaint) elif action is self._horLogarithmicAction: self.setXLog(True, repaint) else: raise NotImplementedError def onViewVerticalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._verActionGroup.checkedAction() if action is self._verLinearAction: self.setYLog(False, repaint) elif action is self._verLogarithmicAction: self.setYLog(True, repaint) else: raise NotImplementedError if repaint: self.repaint() def __adjustFigureMargins(self, withLegend): """ Adjust figure margins to make room for the legend """ if withLegend: leg = self._legend bbox = leg.get_window_extent() # In axes coordinates: bbox2 = bbox.transformed(leg.figure.transFigure.inverted()) if self._legendLoc == "right": self._mplFigure.subplots_adjust(right=1.0-(bbox2.width+0.02)) elif self._legendLoc == "bottom": self._mplFigure.subplots_adjust(bottom=bbox2.height+0.1) else: # Reset to default (rc) values self._mplFigure.subplots_adjust(bottom=0.1, right=0.9) def setLegendVisible(self, visible, repaint=True): if visible and not self._actionLegend.isChecked(): self._actionLegend.setChecked(True) self.showHideLegend(repaint=repaint) if not visible and self._actionLegend.isChecked(): self._actionLegend.setChecked(False) self.showHideLegend(repaint=repaint) def showHideLegend(self, actionChecked=None, repaint=True): if not self.__repaintOK(): # Show/hide legend is extremely costly return show = self._actionLegend.isChecked() nCurves = len(self._curveViews) if nCurves > 10: fontSize = 'x-small' else: fontSize = None if nCurves == 0: # Remove legend leg = self._mplAxes.legend() if leg is not None: leg.remove() if show and nCurves > 0: # Recreate legend from scratch if self._legend is not None: self._legend = None self._mplAxes._legend = None if self._legendLoc == "bottom": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(0.0, -0.05, 1.0, -0.05), borderaxespad=0.0, mode="expand", fancybox=True, shadow=True, ncol=3, prop={'size':fontSize, 'style': 'italic'}) elif self._legendLoc == "right": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(1.02,1.0), borderaxespad=0.0, ncol=1, fancybox=True, shadow=True, prop={'size':fontSize, 'style': 'italic'}) else: raise Exception("Invalid legend placement! Must be 'bottom' or 'right'") # Canvas must be drawn so we can adjust the figure placement: self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=True) else: if self._legend is None: # Nothing to do return else: self._legend.set_visible(False) self._legend = None self._mplAxes._legend = None self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=False) curr_crv = self._model._currentCurve if curr_crv is None: curr_title = None else: curr_title = curr_crv.getTitle() if self._legend is not None: for label in self._legend.get_texts() : text = label.get_text() if (text == curr_title): label.set_backgroundcolor('0.85') else : label.set_backgroundcolor('white') if repaint: self.repaint() def onSettings(self, trigger=False, dlg_test=None): dlg = dlg_test or PlotSettings() dlg.titleEdit.setText(self._mplAxes.get_title()) dlg.axisXTitleEdit.setText(self._mplAxes.get_xlabel()) dlg.axisYTitleEdit.setText(self._mplAxes.get_ylabel()) dlg.gridCheckBox.setChecked(self._mplAxes.xaxis._gridOnMajor) # could not find a relevant API to check this dlg.axisXSciCheckBox.setChecked(self._axisXSciNotation) dlg.axisYSciCheckBox.setChecked(self._axisYSciNotation) xmin, xmax = self._mplAxes.get_xlim() ymin, ymax = self._mplAxes.get_ylim() xminText = "%g" %xmin xmaxText = "%g" %xmax yminText = "%g" %ymin ymaxText = "%g" %ymax dlg.axisXMinEdit.setText(xminText) dlg.axisXMaxEdit.setText(xmaxText) dlg.axisYMinEdit.setText(yminText) dlg.axisYMaxEdit.setText(ymaxText) # List of markers dlg.markerCurve.clear() for marker in self.CURVE_MARKERS : dlg.markerCurve.addItem(marker) curr_crv = self._model.getCurrentCurve() if not curr_crv is None: dlg.colorCurve.setEnabled(True) dlg.markerCurve.setEnabled(True) name = curr_crv.getTitle() dlg.nameCurve.setText(name) view = self._curveViews[curr_crv.getID()] marker = view.getMarker() color = view.getColor() index = dlg.markerCurve.findText(marker) dlg.markerCurve.setCurrentIndex(index) rgb = colors.colorConverter.to_rgb(color) dlg.setRGB(rgb[0],rgb[1],rgb[2]) else : dlg.colorCurve.setEnabled(False) dlg.markerCurve.setEnabled(False) dlg.nameCurve.setText("") view = None if self._legend is None: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) else : if self._legend.get_visible(): dlg.showLegendCheckBox.setChecked(True) dlg.legendPositionComboBox.setEnabled(True) if self._legendLoc == "bottom": dlg.legendPositionComboBox.setCurrentIndex(0) elif self._legendLoc == "right" : dlg.legendPositionComboBox.setCurrentIndex(1) else : dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) if dlg.exec_(): # Title self._model.setTitle(dlg.titleEdit.text()) # Axis self._model.setXLabel(dlg.axisXTitleEdit.text()) self._model.setYLabel(dlg.axisYTitleEdit.text()) # Grid if dlg.gridCheckBox.isChecked() : self._mplAxes.grid(True) else : self._mplAxes.grid(False) # Legend if dlg.showLegendCheckBox.isChecked(): self._actionLegend.setChecked(True) if dlg.legendPositionComboBox.currentIndex() == 0 : self._legendLoc = "bottom" elif dlg.legendPositionComboBox.currentIndex() == 1 : self._legendLoc = "right" else : self._actionLegend.setChecked(False) xminText = dlg.axisXMinEdit.text() xmaxText = dlg.axisXMaxEdit.text() yminText = dlg.axisYMinEdit.text() ymaxText = dlg.axisYMaxEdit.text() self._mplAxes.axis([float(xminText), float(xmaxText), float(yminText), float(ymaxText)] ) self._axisXSciNotation = dlg.axisXSciCheckBox.isChecked() self._axisYSciNotation = dlg.axisYSciCheckBox.isChecked() self.changeFormatAxis() # Color and marker of the curve if view: view.setColor(dlg.getRGB()) view.setMarker(self.CURVE_MARKERS[dlg.markerCurve.currentIndex()]) self.showHideLegend(repaint=True) self._mplCanvas.draw() pass def updateViewTitle(self): s = "" if self._model._title != "": s = " - %s" % self._model._title title = "CurvePlot (%d)%s" % (self._model.getID(), s) self._sgPyQt.setViewTitle(self._salomeViewID, title) def onCurrentPlotSetChange(self): """ Avoid a unnecessary call to update() when just switching current plot set! """ pass def onCurrentCurveChange(self): curr_crv2 = self._model.getCurrentCurve() if curr_crv2 != self._currCrv: if self._currCrv is not None: view = self._curveViews[self._currCrv.getID()] view.toggleHighlight(False) if not curr_crv2 is None: view = self._curveViews[curr_crv2.getID()] view.toggleHighlight(True) self._currCrv = curr_crv2 self.showHideLegend(repaint=False) # redo legend self.repaint() def changeFormatAxis(self) : if not self.__repaintOK(): return # don't try to switch to sci notation if we are not using the # matplotlib.ticker.ScalarFormatter (i.e. if in Log for ex.) if self._horLinearAction.isChecked(): if self._axisXSciNotation : self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='x') else : self._mplAxes.ticklabel_format(style='plain',axis='x') if self._verLinearAction.isChecked(): if self._axisYSciNotation : self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='y') else : self._mplAxes.ticklabel_format(style='plain',axis='y') def update(self): if self._salomeViewID is None: self.createPlotWidget() self._salomeViewID = self._sgPyQt.createView("CurvePlot", self._plotWidget) Logger.Debug("Creating SALOME view ID=%d" % self._salomeViewID) self._sgPyQt.setViewVisible(self._salomeViewID, True) self.updateViewTitle() # Check list of curve views: set_mod = set(self._model._curves.keys()) set_view = set(self._curveViews.keys()) # Deleted/Added curves: dels = set_view - set_mod added = set_mod - set_view for d in dels: self.removeCurve(d) if not len(self._curveViews): # Reset color cycle self._mplAxes.set_color_cycle(None) for a in added: self.appendCurve(a) # Axes labels and title self._mplAxes.set_xlabel(self._model._xlabel) self._mplAxes.set_ylabel(self._model._ylabel) self._mplAxes.set_title(self._model._title) self.onViewHorizontalMode(repaint=False) self.onViewVerticalMode(repaint=False) self.changeModeCurve(repaint=False) self.showHideLegend(repaint=False) # The canvas is repainted anyway (needed to get legend bounding box) self.changeFormatAxis() # Redo auto-fit self.autoFit(repaint=False) self.repaint() def onDataChange(self): # the rest is done in the CurveView: self.autoFit(repaint=True) def onMousePress(self, event): if event.button == 3 : if self._panAction.isChecked(): self._panAction.setChecked(False) if self._zoomAction.isChecked(): self._zoomAction.setChecked(False) def onContextMenu(self, position): pos = self._mplCanvas.mapToGlobal(QtCore.QPoint(position.x(),position.y())) self._popupMenu.exec_(pos) def onScroll(self, event): # Event location (x and y) xdata = event.xdata ydata = event.ydata cur_xlim = self._mplAxes.get_xlim() cur_ylim = self._mplAxes.get_ylim() base_scale = 2. if event.button == 'down': # deal with zoom in scale_factor = 1 / base_scale elif event.button == 'up': # deal with zoom out scale_factor = base_scale else: # deal with something that should never happen scale_factor = 1 new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0]) rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0]) self._mplAxes.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)]) self._mplAxes.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)]) self.repaint() pass def onPressEvent(self, event): if event.button == 3 : #self._mplCanvas.emit(QtCore.SIGNAL("button_release_event()")) canvasSize = event.canvas.geometry() point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) self._popupMenu.exec_(point) else : print "Press event on the other button" #if event.button == 3 : # canvasSize = event.canvas.geometry() # point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) # self._popupMenu.move(point) # self._popupMenu.show() def onMotionEvent(self, event): print "OnMotionEvent ",event.button #if event.button == 3 : # event.button = None # return True def onReleaseEvent(self, event): print "OnReleaseEvent ",event.button
class Calculatrice(Panel_simple): __titre__ = u"Calculatrice" # Donner un titre a chaque module def __init__(self, *args, **kw): Panel_simple.__init__(self, *args, **kw) self.interprete = Interprete(calcul_exact = self.param("calcul_exact"), ecriture_scientifique = self.param("ecriture_scientifique"), changer_separateurs = self.param("changer_separateurs"), separateurs_personnels = self.param("separateurs_personnels"), copie_automatique = self.param("copie_automatique"), formatage_OOo = self.param("formatage_OOo"), formatage_LaTeX = self.param("formatage_LaTeX"), ecriture_scientifique_decimales = self.param("ecriture_scientifique_decimales"), precision_calcul = self.param("precision_calcul"), precision_affichage = self.param("precision_affichage"), simpify = True, ) ## self.entrees = wx.BoxSizer(wx.HORIZONTAL) ## self.entree = wx.TextCtrl(self, size = (550, -1), style = wx.TE_PROCESS_ENTER) ## self.entrees.Add(self.entree, 1, wx.ALL|wx.GROW, 5) ## self.valider = wx.Button(self, wx.ID_OK) ## self.entrees.Add(self.valider, 0, wx.ALL, 5) self.entree = entree = LigneCommande(self, longueur = 550, action = self.affichage_resultat) entree.setToolTip(u"[Maj]+[Entrée] pour une valeur approchée.") self.entree.texte.setContextMenuPolicy(Qt.CustomContextMenu) self.entree.texte.customContextMenuRequested.connect(self.EvtMenu) self.sizer = sizer = QVBoxLayout() sizer.addWidget(entree) self.corps = corps = QHBoxLayout() sizer.addLayout(corps) self.gauche = gauche = QVBoxLayout() corps.addLayout(gauche, 1) self.resultats = resultats = QTextEdit(self) resultats.setMinimumSize(450, 310) resultats.setReadOnly(True) gauche.addWidget(resultats) self.figure = Figure(figsize=(5,1.3), frameon=True, facecolor="w") self.visualisation = FigureCanvas(self.figure) self.axes = axes = self.figure.add_axes([0, 0, 1, 1], frameon=False) axes.axison = False self.pp_texte = axes.text(0.5, 0.5, "", horizontalalignment='center', verticalalignment='center', transform = axes.transAxes, size=18) gauche.addWidget(self.visualisation) # wx.ALL|wx.ALIGN_CENTER self.visualisation.setContextMenuPolicy(Qt.CustomContextMenu) self.visualisation.customContextMenuRequested.connect(self.EvtMenuVisualisation) ### Pave numerique de la calculatrice ### # On construit le pavé de la calculatrice. # Chaque bouton du pavé doit provoquer l'insertion de la commande correspondante. self.pave = pave = QVBoxLayout() corps.addLayout(pave) # pave.setSpacing(1) boutons = ["2nde", "ans", "ouv", "ferm", "egal", "7", "8", "9", "div", "x", "4", "5", "6", "mul", "y", "1", "2", "3", "minus", "z", "0", "pt", "pow", "plus", "t", "rac", "sin", "cos", "tan", "exp", "i", "pi", "e", "abs", "mod"] inserer = ["", "ans()", "(", ")", "=", "7", "8", "9", "/", "x", "4", "5", "6", "*", "y", "1", "2", "3", "-", "z", "0", ".", "^", "+", "t", "sqrt(", ("sin(", "asin(", "sinus / arcsinus"), ("cos(", "acos(", "cosinus / arccosinus"), ("tan(", "atan(", "tangente / arctangente"), ("exp(", "ln(", "exponentielle / logarithme neperien"), ("i", "cbrt(", "i / racine cubique"), ("pi", "sinh(", "pi / sinus hyperbolique"), ("e", "cosh", "e / cosinus hyperbolique"), ("abs(", "tanh", "valeur absolue / tangente hyperbolique"), (" mod ", "log10(", "modulo / logarithme decimal")] self.seconde = False # indique si la touche 2nde est activee. def action(event = None): self.seconde = not self.seconde if self.seconde: self.message(u"Touche [2nde] activée.") else: self.message("") self.actions = [action] for i in range(len(boutons)): # On aligne les boutons de la calculatrice par rangées de 5. if i%5 == 0: self.rangee = rangee = QHBoxLayout() rangee.addStretch(1) pave.addLayout(rangee) # Ensuite, on construit une liste de fonctions, parallèlement à la liste des boutons. if i > 0: #XXX: ce serait plus propre en utilisant partial(). def action(event = None, entree = self.entree, j = i): if type(inserer[j]) == tuple: entree.insert(inserer[j][self.seconde]) else: entree.insert(inserer[j]) n = entree.cursorPosition() entree.setFocus() entree.setCursorPosition(n) self.seconde = False self.message("") self.actions.append(action) bouton = QPushButton() pix = png('btn_' + boutons[i]) bouton.setIcon(QIcon(pix)) bouton.setIconSize(pix.size()) bouton.setFlat(True) # bouton.setSize() # bouton.SetBackgroundColour(self.GetBackgroundColour()) rangee.addWidget(bouton) if i%5 == 4: rangee.addStretch(2) # A chaque bouton, on associe une fonction de la liste. bouton.clicked.connect(self.actions[i]) if type(inserer[i]) == tuple: bouton.setToolTip(inserer[i][2]) # self.pave.Add(QHBoxLayout()) ### Liste des options ### # En dessous du pavé apparait la liste des différents modes de fonctionnement de la calculatrice. # Calcul exact ligne = QHBoxLayout() pave.addLayout(ligne) self.cb_calcul_exact = QCheckBox(self) self.cb_calcul_exact.setChecked(not self.param("calcul_exact")) ligne.addWidget(self.cb_calcul_exact) #, flag = wx.ALIGN_CENTER_VERTICAL) ligne.addWidget(QLabel(u"Valeur approchée."))#, flag = wx.ALIGN_CENTER_VERTICAL) ligne.addStretch() self.cb_calcul_exact.stateChanged.connect(self.EvtCalculExact) # Notation scientifique ligne = QHBoxLayout() pave.addLayout(ligne) self.cb_notation_sci = QCheckBox(self) self.cb_notation_sci.setChecked(self.param("ecriture_scientifique")) ligne.addWidget(self.cb_notation_sci)#, flag = wx.ALIGN_CENTER_VERTICAL) self.st_notation_sci = QLabel(u"Écriture scientifique (arrondie à ") ligne.addWidget(self.st_notation_sci)#, flag = wx.ALIGN_CENTER_VERTICAL) self.sc_decimales = sd = QSpinBox(self) # size = (45, -1) sd.setRange(0, 11) self.sc_decimales.setValue(self.param("ecriture_scientifique_decimales")) ligne.addWidget(self.sc_decimales)#, flag = wx.ALIGN_CENTER_VERTICAL) self.st_decimales = QLabel(u" décimales).") ligne.addWidget(self.st_decimales)#, flag = wx.ALIGN_CENTER_VERTICAL) ligne.addStretch() self.EvtCalculExact() self.cb_notation_sci.stateChanged.connect(self.EvtNotationScientifique) self.sc_decimales.valueChanged.connect(self.EvtNotationScientifique) # Copie du résultat dans le presse-papier ligne = QHBoxLayout() pave.addLayout(ligne) self.cb_copie_automatique = QCheckBox(self) self.cb_copie_automatique.setChecked(self.param("copie_automatique")) ligne.addWidget(self.cb_copie_automatique)#, flag = wx.ALIGN_CENTER_VERTICAL) ligne.addWidget(QLabel(u"Copie du résultat dans le presse-papier."))#, flag = wx.ALIGN_CENTER_VERTICAL) ligne.addStretch() self.cb_copie_automatique.stateChanged.connect(self.EvtCopieAutomatique) # En mode LaTeX ligne = QHBoxLayout() pave.addLayout(ligne) self.cb_copie_automatique_LaTeX = QCheckBox(self) self.cb_copie_automatique_LaTeX.setChecked(self.param("copie_automatique_LaTeX")) ligne.addWidget(self.cb_copie_automatique_LaTeX) self.st_copie_automatique_LaTeX = QLabel(u"Copie au format LaTeX (si possible).") ligne.addWidget(self.st_copie_automatique_LaTeX) ligne.addStretch() self.EvtCopieAutomatique() self.cb_copie_automatique_LaTeX.stateChanged.connect(self.EvtCopieAutomatiqueLatex) # Autres options self.options = [(u"Virgule comme séparateur décimal.", u"changer_separateurs"), ## (u"Copie du résultat dans le presse-papier.", u"copie_automatique"), (u"Accepter la syntaxe OpenOffice.org", u"formatage_OOo"), (u"Accepter la syntaxe LaTeX", u"formatage_LaTeX"), ] self.options_box = [] for i in range(len(self.options)): ligne = QHBoxLayout() pave.addLayout(ligne) self.options_box.append(QCheckBox(self)) ligne.addWidget(self.options_box[i]) self.options_box[i].setChecked(self.param(self.options[i][1])) def action(event, chaine = self.options[i][1], entree = self.entree, self = self): self.param(chaine, not self.param(chaine)) entree.setFocus() self.options_box[i].stateChanged.connect(action) ligne.addWidget(QLabel(self.options[i][0])) ligne.addStretch() self.setLayout(self.sizer) # self.adjustSize() #TODO: # self.entree.texte.Bind(wx.EVT_RIGHT_DOWN, self.EvtMenu) # self.visualisation.Bind(wx.EVT_RIGHT_DOWN, self.EvtMenuVisualisation) self.initialiser() def activer(self): # Actions à effectuer lorsque l'onglet devient actif self.entree.setFocus() def _sauvegarder(self, fgeo): fgeo.contenu["Calculatrice"] = [{}] fgeo.contenu["Calculatrice"][0]["Historique"] = [repr(self.entree.historique)] # fgeo.contenu["Calculatrice"][0]["Resultats"] = [repr(self.interprete.derniers_resultats)] fgeo.contenu["Calculatrice"][0]["Affichage"] = [self.resultats.toPlainText()] fgeo.contenu["Calculatrice"][0]["Etat_interne"] = [self.interprete.save_state()] fgeo.contenu["Calculatrice"][0]["Options"] = [{}] for i in range(len(self.options)): fgeo.contenu["Calculatrice"][0]["Options"][0][self.options[i][1]] = [str(self.options_box[i].isChecked())] def _ouvrir(self, fgeo): if fgeo.contenu.has_key("Calculatrice"): calc = fgeo.contenu["Calculatrice"][0] self.initialiser() self.entree.historique = eval_safe(calc["Historique"][0]) # self.interprete.derniers_resultats = securite.eval_safe(calc["Resultats"][0]) self.resultats.setPlainText(calc["Affichage"][0] + "\n") self.interprete.load_state(calc["Etat_interne"][0]) liste = calc["Options"][0].items() options = [option for aide, option in self.options] for key, value in liste: value = eval_safe(value[0]) self.param(key, value) if key in options: self.options_box[options.index(key)].setChecked(value) # il faudrait encore sauvegarder les variables, mais la encore, 2 problemes : # - pb de securite pour evaluer les variables # - pb pour obtenir le code source d'une fonction. # Pour remedier a cela, il faut envisager de : # - creer un module d'interpretation securisee. # - rajouter a chaque fonction un attribut __code__, ou creer une nouvelle classe. def modifier_pp_texte(self, chaine): u"""Modifier le résultat affiché en LaTeX (pretty print).""" if self.param("latex"): chaine = "$" + chaine + "$" else: chaine = chaine.replace("\\mapsto", "\\rightarrow") if chaine.startswith(r"$\begin{bmatrix}"): chaine = chaine.replace(r"\begin{bmatrix}", r'\left({') chaine = chaine.replace(r"\end{bmatrix}", r'}\right)') chaine = chaine.replace(r"&", r'\,') self.pp_texte.set_text(chaine) self.visualisation.draw() def vers_presse_papier(self, event = None, texte = None): if texte is None: texte = self.dernier_resultat Panel_simple.vers_presse_papier(texte) def copier_latex(self, event = None): self.vers_presse_papier(texte = self.interprete.latex_dernier_resultat.strip("$")) def initialiser(self, event = None): self.dernier_resultat = "" # dernier resultat, sous forme de chaine formatee pour l'affichage self.entree.initialiser() self.interprete.initialiser() self.resultats.clear() def affichage_resultat(self, commande, **kw): # Commandes spéciales: if commande in ('clear', 'clear()', 'efface', 'efface()'): self.initialiser() self.modifier_pp_texte(u"Calculatrice réinitialisée.") return self.modifie = True try: try: if kw["shift"]: self.interprete.calcul_exact = False resultat, latex = self.interprete.evaluer(commande) if latex == "$?$": # provoque une erreur (matplotlib 0.99.1.1) latex = u"Désolé, je ne sais pas faire..." finally: self.interprete.calcul_exact = self.param('calcul_exact') aide = resultat.startswith("\n== Aide sur ") #LaTeX debug("Expression LaTeX: " + latex) try: self.modifier_pp_texte((latex or resultat) if not aide else '') except Exception: print_error() self.modifier_pp_texte("<Affichage impossible>") #Presse-papier self.dernier_resultat = resultat if self.param("copie_automatique"): if self.param("copie_automatique_LaTeX"): self.copier_latex() else: self.vers_presse_papier() # TextCtrl numero = str(len(self.interprete.derniers_resultats)) # Évite le décalage entre la première ligne et les suivantes (matrices) if "\n" in resultat and not aide: resultat = "\n" + "\n".join(20*" " + ligne for ligne in resultat.split("\n")) self.resultats.insertPlainText(u" Calcul n\xb0" + numero + " : " + uu(commande) + u"\n Résultat :" + " "*(4+len(numero)) + resultat + "\n__________________\n\n") self.message(u"Calcul effectué." + self.interprete.warning) self.entree.clear() # self.resultats.setCursorPosition(len(self.resultats.plainText())) # self.resultats.setFocus() # self.resultats.ScrollLines(1) self.entree.setFocus() except Exception: self.message(u"Calcul impossible.") self.entree.setFocus() if param.debug: raise def insere(self, event=None, nom='', parentheses=True): entree = self.entree deb, fin = entree.getSelection() if parentheses: entree.setCursorPosition(fin) entree.insert(")") entree.setCursorPosition(deb) entree.insert(nom + "(") entree.setFocus() if deb == fin: final = fin + len(nom) + 1 else: final = fin + len(nom) + 2 else: entree.insert(nom) final = fin + len(nom) entree.setFocus() entree.setCursorPosition(final) def EvtMenu(self, event): # if not event.ControlDown(): # event.Skip() # return # entree.setSelection(final, final) menu = QMenu() menu.setWindowTitle(u"Fonctions mathématiques") debut = True for rubrique in __classement__: if not debut: menu.addSeparator() debut = False for titre, nom, doc in filter(None, __classement__[rubrique]): action = menu.addAction(titre, partial(self.insere, nom=nom, parentheses=(rubrique != "Symboles"))) # Pas de parenthèses après un symbole. action.setToolTip(doc) menu.exec_(QCursor.pos()) def EvtMenuVisualisation(self, event): menu = QMenu() action = menu.addAction("Copier LaTeX", self.copier_latex) action.setToolTip("Copier le code LaTeX dans le presse-papier.") menu.exec_(QCursor.pos()) # self.PopupMenu(menu) # menu.Destroy() def param(self, parametre, valeur = no_argument, defaut = False): if valeur is not no_argument: setattr(self.interprete, parametre, valeur) return Panel_simple.param(self, parametre = parametre, valeur = valeur, defaut = defaut) def EvtCalculExact(self, event = None): valeur = self.cb_calcul_exact.isChecked() self.param("calcul_exact", not valeur) if valeur: self.cb_notation_sci.setEnabled(True) self.st_notation_sci.setEnabled(True) self.sc_decimales.setEnabled(True) self.st_decimales.setEnabled(True) else: self.cb_notation_sci.setEnabled(False) self.st_notation_sci.setEnabled(False) self.sc_decimales.setEnabled(False) self.st_decimales.setEnabled(False) def EvtNotationScientifique(self, event = None): self.param("ecriture_scientifique", self.cb_notation_sci.isChecked()) self.param("ecriture_scientifique_decimales", self.sc_decimales.value()) def EvtCopieAutomatique(self, event = None): valeur = self.cb_copie_automatique.isChecked() self.param("copie_automatique", valeur) if valeur: self.cb_copie_automatique_LaTeX.setEnabled(True) self.st_copie_automatique_LaTeX.setEnabled(True) else: self.cb_copie_automatique_LaTeX.setEnabled(False) self.st_copie_automatique_LaTeX.setEnabled(False) def EvtCopieAutomatiqueLatex(self, event = None): self.param("copie_automatique_LaTeX", self.cb_copie_automatique_LaTeX.isChecked()) def EtatInterne(self, event): contenu = self.interprete.save_state() h = FenCode(self, u"État interne de l'inteprète", contenu, self.interprete.load_state) h.show()
class Calculatrice(Panel_simple): titre = u"Calculatrice" # Donner un titre a chaque module def __init__(self, *args, **kw): Panel_simple.__init__(self, *args, **kw) self.interprete = Interprete( calcul_exact=self.param("calcul_exact"), ecriture_scientifique=self.param("ecriture_scientifique"), formatage_OOo=self.param("formatage_OOo"), formatage_LaTeX=self.param("formatage_LaTeX"), ecriture_scientifique_decimales=self.param("ecriture_scientifique_decimales"), precision_calcul=self.param("precision_calcul"), precision_affichage=self.param("precision_affichage"), simpify=True, ) bouton = BoutonValider(self) bouton.setToolTip(u"Laissez appuyé pour changer de mode.") self.entree = entree = LigneCommande(self, longueur=550, action=self.affichage_resultat, bouton=bouton) entree.setToolTip(u"[Maj]+[Entrée] pour une valeur approchée.") self.entree.texte.setContextMenuPolicy(Qt.CustomContextMenu) self.entree.texte.customContextMenuRequested.connect(self.EvtMenu) self.sizer = sizer = QVBoxLayout() sizer.addWidget(entree) self.corps = corps = QHBoxLayout() sizer.addLayout(corps) self.resultats = resultats = QTextEdit(self) resultats.setMinimumSize(450, 310) resultats.setReadOnly(True) corps.addWidget(resultats, 1) corps.addWidget(OngletsCalc(self)) self.figure = Figure(figsize=(5, 1.3), frameon=True, facecolor="w") self.visualisation = FigureCanvas(self.figure) self.axes = axes = self.figure.add_axes([0, 0, 1, 1], frameon=False) axes.axison = False self.pp_texte = axes.text( 0.5, 0.5, "", horizontalalignment="center", verticalalignment="center", transform=axes.transAxes, size=18 ) self.visualisation.setContextMenuPolicy(Qt.CustomContextMenu) self.visualisation.customContextMenuRequested.connect(self.EvtMenuVisualisation) sizer.addWidget(self.visualisation) self.setLayout(self.sizer) self.initialiser() def activer(self): Panel_simple.activer(self) # Actions à effectuer lorsque l'onglet devient actif self.entree.setFocus() def _sauvegarder(self, fgeo): fgeo.contenu["Calculatrice"] = [{}] fgeo.contenu["Calculatrice"][0]["Historique"] = [repr(self.entree.historique)] # fgeo.contenu["Calculatrice"][0]["Resultats"] = [repr(self.interprete.derniers_resultats)] fgeo.contenu["Calculatrice"][0]["Affichage"] = [self.resultats.toPlainText()] fgeo.contenu["Calculatrice"][0]["Etat_interne"] = [self.interprete.save_state()] ##fgeo.contenu["Calculatrice"][0]["Options"] = [{}] ##for i in range(len(self.options)): ##fgeo.contenu["Calculatrice"][0]["Options"][0][self.options[i][1]] = [str(self.options_box[i].isChecked())] def _ouvrir(self, fgeo): if fgeo.contenu.has_key("Calculatrice"): calc = fgeo.contenu["Calculatrice"][0] self.initialiser() self.entree.historique = eval_safe(calc["Historique"][0]) # self.interprete.derniers_resultats = securite.eval_safe(calc["Resultats"][0]) resultats = calc["Affichage"][0] if resultats: resultats += "\n\n" self.resultats.setPlainText(resultats) self.resultats.moveCursor(QTextCursor.End) self.interprete.load_state(calc["Etat_interne"][0]) ##liste = calc["Options"][0].items() ##options = [option for aide, option in self.options] ##for key, value in liste: ##value = eval_safe(value[0]) ##self.param(key, value) ##if key in options: ##self.options_box[options.index(key)].setChecked(value) # il faudrait encore sauvegarder les variables, mais la encore, 2 problemes : # - pb de securite pour evaluer les variables # - pb pour obtenir le code source d'une fonction. # Pour remedier a cela, il faut envisager de : # - creer un module d'interpretation securisee. # - rajouter a chaque fonction un attribut __code__, ou creer une nouvelle classe. def modifier_pp_texte(self, chaine): u"""Modifier le résultat affiché en LaTeX (pretty print).""" if self.param("latex"): # On utilise directement LaTeX pour le rendu chaine = "$" + chaine + "$" else: # On utilise le parser matplotlib.mathtext, moins complet mais bien # plus rapide. Certaines adaptations doivent être faites. chaine = latex2mathtext(chaine) self.pp_texte.set_text(chaine) self.visualisation.draw() def vers_presse_papier(self, event=None, texte=None): if texte is None: texte = self.dernier_resultat Panel_simple.vers_presse_papier(texte) def copier_latex(self, event=None): self.vers_presse_papier(texte=self.interprete.latex_dernier_resultat.strip("$")) def initialiser(self, event=None): self.dernier_resultat = "" # dernier resultat, sous forme de chaine formatee pour l'affichage self.entree.initialiser() self.interprete.initialiser() self.resultats.clear() def affichage_resultat(self, commande, **kw): # Commandes spéciales: if commande in ("clear", "clear()", "efface", "efface()"): self.initialiser() self.modifier_pp_texte(u"Calculatrice réinitialisée.") return self.modifie = True try: try: ##self.parent.parent.application.processEvents() if kw.get("shift"): self.interprete.calcul_exact = False resultat, latex = self.interprete.evaluer(commande) if latex == "$?$": # provoque une erreur (matplotlib 0.99.1.1) latex = u"Désolé, je ne sais pas faire..." finally: self.interprete.calcul_exact = self.param("calcul_exact") self.entree.bouton.mode_normal() aide = resultat.startswith("\n== Aide sur ") if aide: latex = "" elif not latex: latex = resultat # LaTeX debug("Expression LaTeX: " + latex) try: try: # Affichage en LaTeX si possible. self.modifier_pp_texte(latex) except Exception: print_error() # Sinon, affichage en texte simple. # `matplotlib.mathtext` est encore loin d'être # pleinement compatible avec LaTeX ! self.modifier_pp_texte(resultat) except Exception: print_error() # Si tout a raté... mais ça ne devrait jamais arrivé. self.modifier_pp_texte("<Affichage impossible>") # Presse-papier self.dernier_resultat = resultat if self.param("copie_automatique"): if self.param("copie_automatique_LaTeX"): self.copier_latex() else: self.vers_presse_papier() # TextCtrl numero = str(len(self.interprete.derniers_resultats)) # Évite le décalage entre la première ligne et les suivantes (matrices) if "\n" in resultat and not aide: resultat = "\n" + "\n".join(20 * " " + ligne for ligne in resultat.split("\n")) self.resultats.moveCursor(QTextCursor.End) self.resultats.insertPlainText( u" Calcul n\xb0" + numero + " : " + uu(commande) + u"\n Résultat :" + " " * (4 + len(numero)) + resultat + "\n__________________\n\n" ) self.resultats.moveCursor(QTextCursor.End) self.message(u"Calcul effectué." + self.interprete.warning) self.entree.clear() # self.resultats.setCursorPosition(len(self.resultats.plainText())) # self.resultats.setFocus() # self.resultats.ScrollLines(1) self.entree.setFocus() except Exception: self.message(u"Calcul impossible.") self.entree.setFocus() if param.debug: raise def insere(self, event=None, nom="", parentheses=True): entree = self.entree deb, fin = entree.getSelection() if parentheses: entree.setCursorPosition(fin) entree.insert(")") entree.setCursorPosition(deb) entree.insert(nom + "(") entree.setFocus() if deb == fin: final = fin + len(nom) + 1 else: final = fin + len(nom) + 2 else: entree.insert(nom) final = fin + len(nom) entree.setFocus() entree.setCursorPosition(final) def EvtMenu(self, event): # if not event.ControlDown(): # event.Skip() # return # entree.setSelection(final, final) menu = QMenu() menu.setWindowTitle(u"Fonctions mathématiques") debut = True for rubrique in __classement__: if not debut: menu.addSeparator() debut = False for titre, nom, doc in filter(None, __classement__[rubrique]): action = menu.addAction(titre, partial(self.insere, nom=nom, parentheses=(rubrique != "Symboles"))) # Pas de parenthèses après un symbole. action.setToolTip(doc) menu.exec_(QCursor.pos()) def EvtMenuVisualisation(self, event): menu = QMenu() action = menu.addAction("Copier LaTeX", self.copier_latex) action.setToolTip("Copier le code LaTeX dans le presse-papier.") menu.exec_(QCursor.pos()) # self.PopupMenu(menu) # menu.Destroy() def param(self, parametre, valeur=no_argument, defaut=False): if valeur is not no_argument: setattr(self.interprete, parametre, valeur) return Panel_simple.param(self, parametre=parametre, valeur=valeur, defaut=defaut) def EtatInterne(self, event): contenu = self.interprete.save_state() h = FenCode(self, u"État interne de l'inteprète", contenu, self.interprete.load_state) h.show()
class XYView(View): AUTOFIT_MARGIN = 0.03 # 3% # See http://matplotlib.org/api/markers_api.html: CURVE_MARKERS = [ "o", # circle "*", # star "+", # plus "x", # x "s", # square "p", # pentagon "h", # hexagon1 "8", # octagon "D", # diamond "^", # triangle_up "<", # triangle_left ">", # triangle_right "1", # tri_down "2", # tri_up "3", # tri_left "4", # tri_right "v", # triangle_down "H", # hexagon2 "d", # thin diamond "", # NO MARKER ] _DEFAULT_LEGEND_STATE = False # for test purposes mainly - initial status of the legend def __init__(self, controller): View.__init__(self, controller) self._eventHandler = EventHandler() self._curveViews = {} # key: curve (model) ID, value: CurveView self._salomeViewID = None self._mplFigure = None self._mplAxes = None self._mplCanvas = None self._plotWidget = None self._sgPyQt = self._controller._sgPyQt self._toolbar = None self._mplNavigationActions = {} self._toobarMPL = None self._grid = None self._currCrv = None # current curve selected in the view self._legend = None self._legendLoc = "right" # "right" or "bottom" self._fitArea = False self._zoomPan = False self._dragOnDrop = False self._move = False self._patch = None self._xdata = None self._ydata = None self._defaultLineStyle = None self._last_point = None self._lastMarkerID = -1 self._blockLogSignal = False self._axisXSciNotation = False self._axisYSciNotation = False self._prevTitle = None def __repaintOK(self): """ To be called inside XYView each time a low-level expansive matplotlib methods is to be invoked. @return False if painting is currently locked, in which case it will also register the current XYView as needing a refresh when unlocked """ ret = self._controller._plotManager.isRepaintLocked() if ret: self._controller._plotManager.registerRepaint(self._model) return (not ret) def appendCurve(self, curveID): newC = CurveView(self._controller, self) newC.setModel(self._model._curves[curveID]) newC.setMPLAxes(self._mplAxes) newC.draw() newC.setMarker(self.getMarker(go_next=True)) self._curveViews[curveID] = newC def removeCurve(self, curveID): v = self._curveViews.pop(curveID) v.erase() if self._currCrv is not None and self._currCrv.getID() == curveID: self._currCrv = None def cleanBeforeClose(self): """ Clean some items to avoid accumulating stuff in memory """ self._mplFigure.clear() plt.close(self._mplFigure) self._plotWidget.clearAll() # For memory debugging only: import gc gc.collect() def repaint(self): if self.__repaintOK(): Logger.Debug("XYView::draw") self._mplCanvas.draw() def onXLabelChange(self): if self.__repaintOK(): self._mplAxes.set_xlabel(self._model._xlabel) self.repaint() def onYLabelChange(self): if self.__repaintOK(): self._mplAxes.set_ylabel(self._model._ylabel) self.repaint() def onTitleChange(self): if self.__repaintOK(): self._mplAxes.set_title(self._model._title) self.updateViewTitle() self.repaint() def onCurveTitleChange(self): # Updating the legend should suffice self.showHideLegend() def onClearAll(self): """ Just does an update with a reset of the marker cycle. """ if self.__repaintOK(): self._lastMarkerID = -1 self.update() def onPick(self, event): """ MPL callback when picking """ if event.mouseevent.button == 1: selected_id = -1 a = event.artist for crv_id, cv in self._curveViews.items(): if cv._mplLines[0] is a: selected_id = crv_id # Use the plotmanager so that other plot sets get their current reset: self._controller._plotManager.setCurrentCurve(selected_id) def createAndAddLocalAction(self, icon_file, short_name): return self._toolbar.addAction( self._sgPyQt.loadIcon("CURVEPLOT", icon_file), short_name) def createPlotWidget(self): self._mplFigure = Figure((8.0, 5.0), dpi=100) self._mplCanvas = FigureCanvasQTAgg(self._mplFigure) self._mplCanvas.installEventFilter(self._eventHandler) self._mplCanvas.mpl_connect('pick_event', self.onPick) self._mplAxes = self._mplFigure.add_subplot(1, 1, 1) self._plotWidget = PlotWidget() self._toobarMPL = NavigationToolbar2QT(self._mplCanvas, None) for act in self._toobarMPL.actions(): actionName = str(act.text()).strip() self._mplNavigationActions[actionName] = act self._plotWidget.setCentralWidget(self._mplCanvas) self._toolbar = self._plotWidget.toolBar self.populateToolbar() self._popupMenu = QtGui.QMenu() self._popupMenu.addAction(self._actionLegend) # Connect evenement for the graphic scene self._mplCanvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._mplCanvas.customContextMenuRequested.connect(self.onContextMenu) self._mplCanvas.mpl_connect('scroll_event', self.onScroll) self._mplCanvas.mpl_connect('button_press_event', self.onMousePress) def populateToolbar(self): # Action to dump view in a file a = self.createAndAddLocalAction("dump_view.png", trQ("DUMP_VIEW_TXT")) a.triggered.connect(self.dumpView) self._toolbar.addSeparator() # Actions to manipulate the scene a = self.createAndAddLocalAction("fit_all.png", trQ("FIT_ALL_TXT")) a.triggered.connect(self.autoFit) # Zoom and pan are mutually exclusive but can be both de-activated: self._zoomAction = self.createAndAddLocalAction( "fit_area.png", trQ("FIT_AREA_TXT")) self._zoomAction.triggered.connect(self.zoomArea) self._zoomAction.setCheckable(True) self._panAction = self.createAndAddLocalAction("zoom_pan.png", trQ("ZOOM_PAN_TXT")) self._panAction.triggered.connect(self.pan) self._panAction.setCheckable(True) self._toolbar.addSeparator() # Actions to change the representation of curves self._curveActionGroup = QtGui.QActionGroup(self._plotWidget) self._pointsAction = self.createAndAddLocalAction( "draw_points.png", trQ("DRAW_POINTS_TXT")) self._pointsAction.setCheckable(True) self._linesAction = self.createAndAddLocalAction( "draw_lines.png", trQ("DRAW_LINES_TXT")) self._linesAction.setCheckable(True) self._curveActionGroup.addAction(self._pointsAction) self._curveActionGroup.addAction(self._linesAction) self._linesAction.setChecked(True) self._curveActionGroup.triggered.connect(self.changeModeCurve) self._curveActionGroup.setExclusive(True) self._toolbar.addSeparator() # Actions to draw horizontal curves as linear or logarithmic self._horActionGroup = QtGui.QActionGroup(self._plotWidget) self._horLinearAction = self.createAndAddLocalAction( "hor_linear.png", trQ("HOR_LINEAR_TXT")) self._horLinearAction.setCheckable(True) self._horLogarithmicAction = self.createAndAddLocalAction( "hor_logarithmic.png", trQ("HOR_LOGARITHMIC_TXT")) self._horLogarithmicAction.setCheckable(True) self._horActionGroup.addAction(self._horLinearAction) self._horActionGroup.addAction(self._horLogarithmicAction) self._horLinearAction.setChecked(True) self._horActionGroup.triggered.connect(self.onViewHorizontalMode) self._toolbar.addSeparator() # Actions to draw vertical curves as linear or logarithmic self._verActionGroup = QtGui.QActionGroup(self._plotWidget) self._verLinearAction = self.createAndAddLocalAction( "ver_linear.png", trQ("VER_LINEAR_TXT")) self._verLinearAction.setCheckable(True) self._verLogarithmicAction = self.createAndAddLocalAction( "ver_logarithmic.png", trQ("VER_LOGARITHMIC_TXT")) self._verLogarithmicAction.setCheckable(True) self._verActionGroup.addAction(self._verLinearAction) self._verActionGroup.addAction(self._verLogarithmicAction) self._verLinearAction.setChecked(True) self._verActionGroup.triggered.connect(self.onViewVerticalMode) self._verActionGroup.setExclusive(True) self._toolbar.addSeparator() # Action to show or hide the legend self._actionLegend = self.createAndAddLocalAction( "legend.png", trQ("SHOW_LEGEND_TXT")) self._actionLegend.setCheckable(True) self._actionLegend.triggered.connect(self.showHideLegend) if self._DEFAULT_LEGEND_STATE: self._actionLegend.setChecked(True) self._toolbar.addSeparator() # Action to set the preferences a = self.createAndAddLocalAction("settings.png", trQ("SETTINGS_TXT")) a.triggered.connect(self.onSettings) pass def dumpView(self): # Choice of the view backup file filters = [] for form in [ "IMAGES_FILES", "PDF_FILES", "POSTSCRIPT_FILES", "ENCAPSULATED_POSTSCRIPT_FILES" ]: filters.append(trQ(form)) fileName = self._sgPyQt.getFileName(self._sgPyQt.getDesktop(), "", filters, trQ("DUMP_VIEW_FILE"), False) if not fileName.isEmpty(): name = str(fileName) self._mplAxes.figure.savefig(name) pass def autoFit(self, check=True, repaint=True): if self.__repaintOK(): self._mplAxes.relim() xm, xM = self._mplAxes.xaxis.get_data_interval() ym, yM = self._mplAxes.yaxis.get_data_interval() i = yM - ym self._mplAxes.axis([ xm, xM, ym - i * self.AUTOFIT_MARGIN, yM + i * self.AUTOFIT_MARGIN ]) if repaint: self.repaint() def zoomArea(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._panAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Zoom"].trigger() def pan(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._zoomAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Pan"].trigger() def getMarker(self, go_next=False): if go_next: self._lastMarkerID = (self._lastMarkerID + 1) % len( self.CURVE_MARKERS) return self.CURVE_MARKERS[self._lastMarkerID] def changeModeCurve(self, repaint=True): if not self.__repaintOK(): return action = self._curveActionGroup.checkedAction() if action is self._pointsAction: for crv_view in self._curveViews.values(): crv_view.setLineStyle("None") elif action is self._linesAction: for crv_view in self._curveViews.values(): crv_view.setLineStyle("-") else: raise NotImplementedError if repaint: self.repaint() def setXLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_xscale('log') self._horLogarithmicAction.setChecked(True) else: self._mplAxes.set_xscale('linear') self._horLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setYLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_yscale('log') self._verLogarithmicAction.setChecked(True) else: self._mplAxes.set_yscale('linear') self._verLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setXSciNotation(self, sciNotation, repaint=True): self._axisXSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def setYSciNotation(self, sciNotation, repaint=True): self._axisYSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def onViewHorizontalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._horActionGroup.checkedAction() if action is self._horLinearAction: self.setXLog(False, repaint) elif action is self._horLogarithmicAction: self.setXLog(True, repaint) else: raise NotImplementedError def onViewVerticalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._verActionGroup.checkedAction() if action is self._verLinearAction: self.setYLog(False, repaint) elif action is self._verLogarithmicAction: self.setYLog(True, repaint) else: raise NotImplementedError if repaint: self.repaint() def __adjustFigureMargins(self, withLegend): """ Adjust figure margins to make room for the legend """ if withLegend: leg = self._legend bbox = leg.get_window_extent() # In axes coordinates: bbox2 = bbox.transformed(leg.figure.transFigure.inverted()) if self._legendLoc == "right": self._mplFigure.subplots_adjust(right=1.0 - (bbox2.width + 0.02)) elif self._legendLoc == "bottom": self._mplFigure.subplots_adjust(bottom=bbox2.height + 0.1) else: # Reset to default (rc) values self._mplFigure.subplots_adjust(bottom=0.1, right=0.9) def setLegendVisible(self, visible, repaint=True): if visible and not self._actionLegend.isChecked(): self._actionLegend.setChecked(True) self.showHideLegend(repaint=repaint) if not visible and self._actionLegend.isChecked(): self._actionLegend.setChecked(False) self.showHideLegend(repaint=repaint) def showHideLegend(self, actionChecked=None, repaint=True): if not self.__repaintOK(): # Show/hide legend is extremely costly return show = self._actionLegend.isChecked() nCurves = len(self._curveViews) if nCurves > 10: fontSize = 'x-small' else: fontSize = None if nCurves == 0: # Remove legend leg = self._mplAxes.legend() if leg is not None: leg.remove() if show and nCurves > 0: # Recreate legend from scratch if self._legend is not None: self._legend = None self._mplAxes._legend = None if self._legendLoc == "bottom": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(0.0, -0.05, 1.0, -0.05), borderaxespad=0.0, mode="expand", fancybox=True, shadow=True, ncol=3, prop={ 'size': fontSize, 'style': 'italic' }) elif self._legendLoc == "right": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(1.02, 1.0), borderaxespad=0.0, ncol=1, fancybox=True, shadow=True, prop={ 'size': fontSize, 'style': 'italic' }) else: raise Exception( "Invalid legend placement! Must be 'bottom' or 'right'") # Canvas must be drawn so we can adjust the figure placement: self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=True) else: if self._legend is None: # Nothing to do return else: self._legend.set_visible(False) self._legend = None self._mplAxes._legend = None self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=False) curr_crv = self._model._currentCurve if curr_crv is None: curr_title = None else: curr_title = curr_crv.getTitle() if self._legend is not None: for label in self._legend.get_texts(): text = label.get_text() if (text == curr_title): label.set_backgroundcolor('0.85') else: label.set_backgroundcolor('white') if repaint: self.repaint() def onSettings(self, trigger=False, dlg_test=None): dlg = dlg_test or PlotSettings() dlg.titleEdit.setText(self._mplAxes.get_title()) dlg.axisXTitleEdit.setText(self._mplAxes.get_xlabel()) dlg.axisYTitleEdit.setText(self._mplAxes.get_ylabel()) dlg.gridCheckBox.setChecked( self._mplAxes.xaxis._gridOnMajor ) # could not find a relevant API to check this dlg.axisXSciCheckBox.setChecked(self._axisXSciNotation) dlg.axisYSciCheckBox.setChecked(self._axisYSciNotation) xmin, xmax = self._mplAxes.get_xlim() ymin, ymax = self._mplAxes.get_ylim() xminText = "%g" % xmin xmaxText = "%g" % xmax yminText = "%g" % ymin ymaxText = "%g" % ymax dlg.axisXMinEdit.setText(xminText) dlg.axisXMaxEdit.setText(xmaxText) dlg.axisYMinEdit.setText(yminText) dlg.axisYMaxEdit.setText(ymaxText) # List of markers dlg.markerCurve.clear() for marker in self.CURVE_MARKERS: dlg.markerCurve.addItem(marker) curr_crv = self._model.getCurrentCurve() if not curr_crv is None: dlg.colorCurve.setEnabled(True) dlg.markerCurve.setEnabled(True) name = curr_crv.getTitle() dlg.nameCurve.setText(name) view = self._curveViews[curr_crv.getID()] marker = view.getMarker() color = view.getColor() index = dlg.markerCurve.findText(marker) dlg.markerCurve.setCurrentIndex(index) rgb = colors.colorConverter.to_rgb(color) dlg.setRGB(rgb[0], rgb[1], rgb[2]) else: dlg.colorCurve.setEnabled(False) dlg.markerCurve.setEnabled(False) dlg.nameCurve.setText("") view = None if self._legend is None: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) else: if self._legend.get_visible(): dlg.showLegendCheckBox.setChecked(True) dlg.legendPositionComboBox.setEnabled(True) if self._legendLoc == "bottom": dlg.legendPositionComboBox.setCurrentIndex(0) elif self._legendLoc == "right": dlg.legendPositionComboBox.setCurrentIndex(1) else: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) if dlg.exec_(): # Title self._model.setTitle(dlg.titleEdit.text()) # Axis self._model.setXLabel(dlg.axisXTitleEdit.text()) self._model.setYLabel(dlg.axisYTitleEdit.text()) # Grid if dlg.gridCheckBox.isChecked(): self._mplAxes.grid(True) else: self._mplAxes.grid(False) # Legend if dlg.showLegendCheckBox.isChecked(): self._actionLegend.setChecked(True) if dlg.legendPositionComboBox.currentIndex() == 0: self._legendLoc = "bottom" elif dlg.legendPositionComboBox.currentIndex() == 1: self._legendLoc = "right" else: self._actionLegend.setChecked(False) xminText = dlg.axisXMinEdit.text() xmaxText = dlg.axisXMaxEdit.text() yminText = dlg.axisYMinEdit.text() ymaxText = dlg.axisYMaxEdit.text() self._mplAxes.axis([ float(xminText), float(xmaxText), float(yminText), float(ymaxText) ]) self._axisXSciNotation = dlg.axisXSciCheckBox.isChecked() self._axisYSciNotation = dlg.axisYSciCheckBox.isChecked() self.changeFormatAxis() # Color and marker of the curve if view: view.setColor(dlg.getRGB()) view.setMarker( self.CURVE_MARKERS[dlg.markerCurve.currentIndex()]) self.showHideLegend(repaint=True) self._mplCanvas.draw() pass def updateViewTitle(self): s = "" if self._model._title != "": s = " - %s" % self._model._title title = "CurvePlot (%d)%s" % (self._model.getID(), s) self._sgPyQt.setViewTitle(self._salomeViewID, title) def onCurrentPlotSetChange(self): """ Avoid a unnecessary call to update() when just switching current plot set! """ pass def onCurrentCurveChange(self): curr_crv2 = self._model.getCurrentCurve() if curr_crv2 != self._currCrv: if self._currCrv is not None: view = self._curveViews[self._currCrv.getID()] view.toggleHighlight(False) if not curr_crv2 is None: view = self._curveViews[curr_crv2.getID()] view.toggleHighlight(True) self._currCrv = curr_crv2 self.showHideLegend(repaint=False) # redo legend self.repaint() def changeFormatAxis(self): if not self.__repaintOK(): return # don't try to switch to sci notation if we are not using the # matplotlib.ticker.ScalarFormatter (i.e. if in Log for ex.) if self._horLinearAction.isChecked(): if self._axisXSciNotation: self._mplAxes.ticklabel_format(style='sci', scilimits=(0, 0), axis='x') else: self._mplAxes.ticklabel_format(style='plain', axis='x') if self._verLinearAction.isChecked(): if self._axisYSciNotation: self._mplAxes.ticklabel_format(style='sci', scilimits=(0, 0), axis='y') else: self._mplAxes.ticklabel_format(style='plain', axis='y') def update(self): if self._salomeViewID is None: self.createPlotWidget() self._salomeViewID = self._sgPyQt.createView( "CurvePlot", self._plotWidget) Logger.Debug("Creating SALOME view ID=%d" % self._salomeViewID) self._sgPyQt.setViewVisible(self._salomeViewID, True) self.updateViewTitle() # Check list of curve views: set_mod = set(self._model._curves.keys()) set_view = set(self._curveViews.keys()) # Deleted/Added curves: dels = set_view - set_mod added = set_mod - set_view for d in dels: self.removeCurve(d) if not len(self._curveViews): # Reset color cycle self._mplAxes.set_color_cycle(None) for a in added: self.appendCurve(a) # Axes labels and title self._mplAxes.set_xlabel(self._model._xlabel) self._mplAxes.set_ylabel(self._model._ylabel) self._mplAxes.set_title(self._model._title) self.onViewHorizontalMode(repaint=False) self.onViewVerticalMode(repaint=False) self.changeModeCurve(repaint=False) self.showHideLegend( repaint=False ) # The canvas is repainted anyway (needed to get legend bounding box) self.changeFormatAxis() # Redo auto-fit self.autoFit(repaint=False) self.repaint() def onDataChange(self): # the rest is done in the CurveView: self.autoFit(repaint=True) def onMousePress(self, event): if event.button == 3: if self._panAction.isChecked(): self._panAction.setChecked(False) if self._zoomAction.isChecked(): self._zoomAction.setChecked(False) def onContextMenu(self, position): pos = self._mplCanvas.mapToGlobal( QtCore.QPoint(position.x(), position.y())) self._popupMenu.exec_(pos) def onScroll(self, event): # Event location (x and y) xdata = event.xdata ydata = event.ydata cur_xlim = self._mplAxes.get_xlim() cur_ylim = self._mplAxes.get_ylim() base_scale = 2. if event.button == 'down': # deal with zoom in scale_factor = 1 / base_scale elif event.button == 'up': # deal with zoom out scale_factor = base_scale else: # deal with something that should never happen scale_factor = 1 new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor relx = (cur_xlim[1] - xdata) / (cur_xlim[1] - cur_xlim[0]) rely = (cur_ylim[1] - ydata) / (cur_ylim[1] - cur_ylim[0]) self._mplAxes.set_xlim( [xdata - new_width * (1 - relx), xdata + new_width * (relx)]) self._mplAxes.set_ylim( [ydata - new_height * (1 - rely), ydata + new_height * (rely)]) self.repaint() pass def onPressEvent(self, event): if event.button == 3: #self._mplCanvas.emit(QtCore.SIGNAL("button_release_event()")) canvasSize = event.canvas.geometry() point = event.canvas.mapToGlobal( QtCore.QPoint(event.x, canvasSize.height() - event.y)) self._popupMenu.exec_(point) else: print "Press event on the other button" #if event.button == 3 : # canvasSize = event.canvas.geometry() # point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) # self._popupMenu.move(point) # self._popupMenu.show() def onMotionEvent(self, event): print "OnMotionEvent ", event.button #if event.button == 3 : # event.button = None # return True def onReleaseEvent(self, event): print "OnReleaseEvent ", event.button