diff options
Diffstat (limited to 'lib/taurus/qt/qtgui/plot/qwtdialog.py')
-rw-r--r-- | lib/taurus/qt/qtgui/plot/qwtdialog.py | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/lib/taurus/qt/qtgui/plot/qwtdialog.py b/lib/taurus/qt/qtgui/plot/qwtdialog.py new file mode 100644 index 00000000..65fac896 --- /dev/null +++ b/lib/taurus/qt/qtgui/plot/qwtdialog.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python + +############################################################################# +## +## This file is part of Taurus +## +## http://taurus-scada.org +## +## Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +## Taurus is free software: you can redistribute it and/or modify +## it under the terms of the GNU Lesser General Public License as published by +## the Free Software Foundation, either version 3 of the License, or +## (at your option) any later version. +## +## Taurus is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public License +## along with Taurus. If not, see <http://www.gnu.org/licenses/>. +## +############################################################################# + +""" +qwtdialog.py: Dialogs for Taurusplot +""" + +import time + +from taurus.external.qt import Qt, Qwt5 +from taurus.qt.qtgui.util.ui import UILoadable + + +#class TaurusPlotConfigCapable: +# """This class is aimed to act as an interface for class TaurusPlot. Every class that uses +# TaurusPlotConfigDialog class (as TaurusPlot does) should inherit TaurusPlotConfigCapable class +# and overwrite the methods defined below in order to avoid firing exceptions""" +# def __init__(self, parent = None): +# pass +# +# def showConfigDialog(self): +# raise NotImplementedError, "TaurusPlotConfigCapable: method showConfigDialog() not implemented in child class" +# +# def setAxisScale(self,axis,min,max): +# raise NotImplementedError, "TaurusPlotConfigCapable: method setAxisScale() not implemented in child class" +# +# def setAxisAutoScale(self, axis=None): +# raise NotImplementedError, "TaurusPlotConfigCapable: method setAxisAutoScale() not implemented in child class" +# +# def setAxisScaleType(self, axis=None, scale=None): +# raise NotImplementedError, "TaurusPlotConfigCapable: method setAxisScaleType() not implemented in child class" + + +@UILoadable(with_ui='ui') +class TaurusPlotConfigDialog(Qt.QDialog): + """This class is used to build and manage the plot configuration dialog. It + has been designed using the qt designer application, and then loaded to this + widget. Hence, if you need to modify the dialog, you can use the + TaurusPlotConfigDialog.ui file (under ui directory) to make it easier.""" + + def __init__(self, parent=None, flags=Qt.Qt.WindowFlags()): + self.parent = parent + #if parent==None or not isinstance(parent, TaurusPlotConfigCapable): + # raise NotImplementedError, "Parent object doesn't implement TaurusPlotConfigCapable class" + #call qt designer generated functions to initialize GUI + Qt.QDialog.__init__(self, parent, flags) + self.loadUi() + + #insert the CurvesAppearanceWidget + #(@TODO:must be changed to be done directly in the ui, but I couldn't make the widget available to TaurusDesigner) + from curvesAppearanceChooserDlg import CurvesAppearanceChooser + layout=Qt.QVBoxLayout() + self.curvesAppearanceChooser=CurvesAppearanceChooser(None) + layout.addWidget(self.curvesAppearanceChooser) + self.ui.curveAppearanceGB.setLayout(layout) + + #set the values for the CurvesAppearanceChooser + self.curvesAppearanceChooser.setCurves(self.parent.getCurveAppearancePropertiesDict()) + + #insert valid values into mode combobox (linear or logarihtmic) and set selected one in combobox + self.ui.xModeComboBox.insertItem(0,"Linear") + self.ui.xModeComboBox.insertItem(1,"Logarithmic") + self.ui.y1ModeComboBox.insertItem(0,"Linear") + self.ui.y1ModeComboBox.insertItem(1,"Logarithmic") + self.ui.y2ModeComboBox.insertItem(0,"Linear") + self.ui.y2ModeComboBox.insertItem(1,"Logarithmic") + + #insert valid values into peaks combobox (max, min or both) and set selected one in combobox + self.ui.peaksComboBox.insertItem(0,"Both") + self.ui.peaksComboBox.insertItem(1,"Max") + self.ui.peaksComboBox.insertItem(2,"Min") + self.ui.peaksComboBox.insertItem(3,"None") + + #Init X axis group + self.ui.xRangeCB.setVisible(False) + self.ui.xLabelRange.setVisible(False) + if self.parent.getXIsTime(): + # adapt the X axis group for time-based measurements + self.ui.xRangeCB.addItems(["", + "1 m", + "1 h", + "1 d", + "1 w", + "30 d", + "1 y"]) + self.ui.xGroupBox.setTitle("Time") + self.ui.xLabelMin.setText("Start") + self.ui.xLabelMax.setText("End") + timetooltip="""It accepts both absolute data-times (e.g. "25/10/1917 21:45:01") \n"""\ + """or relative ones with format <+|-><number><s|m|h|d|w|y> (e.g. "-12h").\n"""\ + """The keyword "now" is also accepted.""" + self.ui.xEditMin.setToolTip(timetooltip) + self.ui.xEditMax.setToolTip(timetooltip) + else: + # The default settings are ok for non-time plots + self.ui.xEditMin.setValidator(Qt.QDoubleValidator(self)) + self.ui.xEditMax.setValidator(Qt.QDoubleValidator(self)) + self.ui.xRangeCB.setValidator(Qt.QDoubleValidator(self)) + self.ui.xRangeCB.addItems(["", + "10", + "100", + "1000", + "10000", + "100000", + "1000000"]) + + self.ui.xDynScaleCheckBox.setChecked(self.parent.getXDynScale()) + auto=self.parent.axisAutoScale(Qwt5.QwtPlot.xBottom) + self.ui.xAutoGroupBox.setChecked(not auto) + + self.setXDynScale(self.parent.getXDynScale()) #this call already initialises the edit boxes of the X axis + self.ui.xDynScaleCheckBox.setVisible(self.parent.isXDynScaleSupported()) + + #Init Y axes groups + self._populateYAxisScales() + self.ui.y1AutoGroupBox.setChecked(not self.parent.axisAutoScale(Qwt5.QwtPlot.yLeft)) + self.ui.y2AutoGroupBox.setChecked(not self.parent.axisAutoScale(Qwt5.QwtPlot.yRight)) + + #set validators for Y min/max edits + self.ui.y1EditMin.setValidator(Qt.QDoubleValidator(self)) + self.ui.y1EditMax.setValidator(Qt.QDoubleValidator(self)) + self.ui.y2EditMin.setValidator(Qt.QDoubleValidator(self)) + self.ui.y2EditMax.setValidator(Qt.QDoubleValidator(self)) + + #populate initial axis mode (linear or logarithmic) values from plot. Warning!: we're using + #qwt QwtPlot.axis enum type to relate our GUI combo name and qwt axis name (enum type). + axes = [self.ui.y1ModeComboBox, self.ui.y2ModeComboBox, self.ui.xModeComboBox] + for axis in range(len(axes)): + scaleType = self.parent.axisScaleEngine(axis).transformation().type() + if scaleType == Qwt5.QwtScaleTransformation.Linear: + axes[axis].setCurrentIndex(0) + elif scaleType == Qwt5.QwtScaleTransformation.Log10: + axes[axis].setCurrentIndex(1) + else: + raise TypeError, "TaurusPlotConfigDialog::__init__(): unexpected axis scale type (linear or logarihtmic expected)" + self.ui.xModeComboBox.setEnabled(not self.parent.getXIsTime()) + + #determine which axes are visible + if not self.parent.axisEnabled(Qwt5.QwtPlot.xBottom): + self.ui.xGroupBox.setVisible(False) + + #populate initial peaks from parent + if self.parent._showMaxPeaks and self.parent._showMinPeaks: + self.ui.peaksComboBox.setCurrentIndex(0) + elif self.parent._showMaxPeaks: + self.ui.peaksComboBox.setCurrentIndex(1) + elif self.parent._showMinPeaks: + self.ui.peaksComboBox.setCurrentIndex(2) + else: + self.ui.peaksComboBox.setCurrentIndex(3) + + #connect signals + self.connect(self.ui.buttonBox.button(Qt.QDialogButtonBox.Close), Qt.SIGNAL("clicked()"), self.hide) + self.connect(self.ui.buttonBox.button(Qt.QDialogButtonBox.Apply), Qt.SIGNAL("clicked()"), self.apply) + self.connect(self.ui.xAutoGroupBox, Qt.SIGNAL("toggled (bool)"),self.toggledAutoScale) + self.connect(self.ui.y1AutoGroupBox, Qt.SIGNAL("toggled (bool)"),self.toggledAutoScale) + self.connect(self.ui.y2AutoGroupBox, Qt.SIGNAL("toggled (bool)"),self.toggledAutoScale) +# self.connect(self.ui.xEditMin, Qt.SIGNAL("returnPressed()"),self.apply) +# self.connect(self.ui.xEditMax, Qt.SIGNAL("returnPressed()"),self.apply) +# self.connect(self.ui.y1EditMin, Qt.SIGNAL("returnPressed()"),self.apply) +# self.connect(self.ui.y1EditMax, Qt.SIGNAL("returnPressed()"),self.apply) +# self.connect(self.ui.y2EditMin, Qt.SIGNAL("returnPressed()"),self.apply) +# self.connect(self.ui.y2EditMax, Qt.SIGNAL("returnPressed()"),self.apply) + self.connect(self.ui.xModeComboBox, Qt.SIGNAL("currentIndexChanged(const QString&)"),self.modeComboChanged) + self.connect(self.ui.xDynScaleCheckBox, Qt.SIGNAL("toggled (bool)"),self.setXDynScale) + #self.connect(self.ui.xRangeCB, Qt.SIGNAL("currentIndexChanged(const QString&)"),self.apply) + self.connect(self.ui.y1ModeComboBox, Qt.SIGNAL("currentIndexChanged(const QString&)"),self.modeComboChanged) + self.connect(self.ui.y2ModeComboBox, Qt.SIGNAL("currentIndexChanged(const QString&)"),self.modeComboChanged) + self.connect(self.ui.peaksComboBox, Qt.SIGNAL("currentIndexChanged(int)"),self.peaksComboChanged) + #self.connect(self.curvesAppearanceChooser, Qt.SIGNAL("controlChanged"),self.apply) #"autoapply" mode for *all* the curve appearance controls + self.connect(self.curvesAppearanceChooser.assignToY1BT, Qt.SIGNAL("clicked()"),self.setCurvesYAxis) + self.connect(self.curvesAppearanceChooser.assignToY2BT, Qt.SIGNAL("clicked()"),self.setCurvesYAxis) + self.connect(self.curvesAppearanceChooser.bckgndBT, Qt.SIGNAL("clicked()"),self.changeBackgroundColor) + self.connect(self.curvesAppearanceChooser.changeTitlesBT, Qt.SIGNAL("clicked()"),self.onChangeTitles) + self.connect(self.curvesAppearanceChooser.curvesLW, Qt.SIGNAL("CurveTitleEdited"),self.onCurveTitleEdited) + + #finally adjust size + self.adjustSize() + + def _populateXAxisScales(self): + xMin = self.parent.axisScaleDiv(Qwt5.QwtPlot.xBottom).lowerBound() + xMax = self.parent.axisScaleDiv(Qwt5.QwtPlot.xBottom).upperBound() + if self.parent.getXIsTime(): + self.ui.xEditMin.setText(time.strftime('%Y/%m/%d %H:%M:%S',time.localtime(int(xMin)))) + self.ui.xEditMax.setText(time.strftime('%Y/%m/%d %H:%M:%S',time.localtime(int(xMax)))) + else: + self.ui.xEditMin.setText(str(xMin)) + self.ui.xEditMax.setText(str(xMax)) + + def _populateYAxisScales(self, axis=None): + if axis is None or axis==Qwt5.QwtPlot.yLeft: + self.ui.y1EditMin.setText(str(self.parent.axisScaleDiv(Qwt5.QwtPlot.yLeft).lowerBound())) + self.ui.y1EditMax.setText(str(self.parent.axisScaleDiv(Qwt5.QwtPlot.yLeft).upperBound())) + if axis is None or axis==Qwt5.QwtPlot.yRight: + self.ui.y2EditMin.setText(str(self.parent.axisScaleDiv(Qwt5.QwtPlot.yRight).lowerBound())) + self.ui.y2EditMax.setText(str(self.parent.axisScaleDiv(Qwt5.QwtPlot.yRight).upperBound())) + + def deltatime2str(self,dt, fuzzy=False): + '''converts a time diff in secs to a string. + If fuzzy=True it returns an approx time diff in s, min, hours or days''' + dt=float(dt) + if not fuzzy:return "%g s"%dt + if dt<2: return "%3.3g s"%dt + elif dt<120 : return "%g s"%round(dt,0) + elif dt<7200: return "%g m"%round(dt/60,0) + elif dt<172800: return "%g h"%round(dt/3600,0) + else: return "%g d"%round(dt/86400,0) + + def str2deltatime(self,strtime): + '''Translates a time string to seconds + examples of valid relative times are: "now", "NOW", "Now", "-1d", "3w", "- 3.6e3 s",... + examples of non-valid relative times:, "now + 2h", "-5", "3H" (unit names are case-sensitive) + ''' + strtime=str(strtime).strip() + timeunits={'s':1, 'm':60, 'h':3600, 'd':3600*24, 'w': 3600*24*7, 'y':3600*24*365} + if strtime.lower()=="now": return time.time() + if strtime[-1] in timeunits.keys(): + try: return float(strtime[:-1])*timeunits[strtime[-1]] + except Exception,e: + print '[str2deltatime] No format could be applied to "%s"'%strtime + return None + else: + print '[str2deltatime] Non-supported unit "%s"'%strtime[-1] + return None + + def strtime2epoch(self,strtime): + ''' + Translates a str into an epoch value. It accepts "absolute" time notation as well as + "relative to current time notation" (by expliciting a "+" or "-" prefix) + (see str2deltatime for relative time notation). + + examples of valid absolute times: "2008-3-25 14:21:59", "25/03/08 14:21", "03-25-2008",... + + It returns None if strtime couldn't be interpreted + ''' + import time + t=None + strtime=str(strtime).strip() + + #try with relative time keywords + if strtime[0] in ['+','-']: + reltime=self.str2deltatime(strtime) + if reltime is not None: return time.time()+reltime + + if strtime.lower()=="now": return time.time() + + #try with absolute time + hour_fts = ['%H:%M:%S','%H:%M','']#,"%M'",'%S"' + date_fts = ['%Y-%m-%d','%y-%m-%d','%Y/%m/%d','%y/%m/%d','%d-%m-%Y','%d/%m/%Y','%m-%d-%Y','%m/%d/%Y'] + + tfs = [] + for d in date_fts: + for h in hour_fts: + if h: + tfs.append('%s %s'%(d,h)) + else: + tfs.append(d) + + for tf in tfs: + try: + #print 'trying ',tf + t = time.strptime(strtime,tf) + if t[0]<1970: + lt = time.localtime() + # (2008, 9, 23, 15, 44, 24, 1, 267, 1) + t = (lt[0],lt[1],lt[2],t[3],t[4],lt[5],lt[6],lt[7],lt[8]) + break + except Exception,e: + pass + if t: + return time.mktime(t) + else: + print 'No format could be applied to "%s"'%strtime + return None + + + def validate(self): + '''validates the inputs in the dialog. + If validation is ok, it returns a tuple containing min/max values for each axis (None if they are autoscaled) + If validation failed, it returns False. + + .. note:: + the values of the max/min boxes are already validated thanks to their attached QDoubleValidators + (except for xMin/xMax in time Mode, but this case is handled by strtime2epoch) + ''' + xMin=xMax=y1Min=y1Max=y2Min=y2Max=None + + #Validating X axis values + xMin=self.parent.axisScaleDiv(Qwt5.QwtPlot.xBottom).lowerBound() + xMax=self.parent.axisScaleDiv(Qwt5.QwtPlot.xBottom).upperBound() + if self.ui.xDynScaleCheckBox.isChecked(): + if self.parent.getXIsTime(): + rg = abs(self.str2deltatime(str(self.ui.xRangeCB.currentText()))) + ok1= (rg is not None) + else: + try: + rg, ok1 = float(self.ui.xRangeCB.currentText()), True + except: + rg, ok1 = 0.0, False + xMin=xMax-rg + if not ok1 or (xMin >= xMax): + Qt.QMessageBox.warning(self, "Invalid parameters for X axis range") + return False + elif self.ui.xAutoGroupBox.isChecked(): + if self.parent.getXIsTime(): #time mode + xMin = self.strtime2epoch(self.ui.xEditMin.text()) + xMax = self.strtime2epoch(self.ui.xEditMax.text()) + ok1= (xMin is not None) + ok2= (xMax is not None) + else: #XY mode + try: + xMin, ok1 = float(self.ui.xEditMin.text()), True + except: + xMin, ok1 = 0.0, False + try: + xMax, ok2 = float(self.ui.xEditMax.text()), True + except: + xMax, ok2 = 0.0, False + if not (ok1 and ok2) or (xMin >= xMax): + Qt.QMessageBox.warning(self, "Invalid parameters for X axis", + "Min value must be strictly less than max value") + return False + + #Validating Y1 axis values + if self.ui.y1AutoGroupBox.isChecked(): + try: + y1Min, ok1 = float(self.ui.y1EditMin.text()), True + except: + y1Min, ok1 = 0.0, False + try: + y1Max, ok2 = float(self.ui.y1EditMax.text()), True + except: + y1Max, ok2 = 0.0, False + if not(ok1 and ok2) or (y1Min >= y1Max): + Qt.QMessageBox.warning(self, "Invalid parameters for Y1 axis", + "Min value must be strictly less than max value") + return False + + #Validating Y2 axis values + if self.ui.y2AutoGroupBox.isChecked(): + try: + y2Min, ok1 = float(self.ui.y2EditMin.text()), True + except: + y2Min, ok1 = 0.0, False + try: + y2Max, ok2 = float(self.ui.y2EditMax.text()), True + except: + y2Max, ok2 = 0.0, False + if not (ok1 and ok2) or (y2Min >= y2Max): + Qt.QMessageBox.warning(self, "Invalid parameters for Y2 axis", + "Min value must be strictly less than max value") + return False + + #if we reached this point, the input is valid + return (xMin,xMax,y1Min,y1Max,y2Min,y2Max) + + def apply(self): + """This will apply the values set in the dialog. Note that some of them + are not necessary to be set, since they're already are set when changing the item selected""" + + #Apply the changes from the curves Appearance widget (they are already validated). + prop,names=self.curvesAppearanceChooser.onApply() + self.parent.onCurveAppearanceChanged(prop,names) + + #validate the rest of the inputs + v=self.validate() + if not v: return + xMin,xMax,y1Min,y1Max,y2Min,y2Max=v + + self.parent.setAxisScale(Qwt5.QwtPlot.xBottom,xMin, xMax) + self.parent.setAxisScale(Qwt5.QwtPlot.yLeft, y1Min, y1Max) + self.parent.setAxisScale(Qwt5.QwtPlot.yRight, y2Min, y2Max) + + self.parent._xMax,self.parent._xMin=xMax,xMin + + def toggledAutoScale(self, toggled): + """This will catch the group boxes check/uncheck event, and will enable autoscale in case + the event has been unchecking 'disable autoscale'""" + #get and check who fired the signal + groupBox = self.sender() + if groupBox is None or not isinstance(groupBox, Qt.QGroupBox): + self.error("TaurusPlotConfig::toggledAutoScale(): event is not recognized") + return + #set the axis autoscale in the corresponding axis + if groupBox.objectName() == "xAutoGroupBox" and not toggled: + self.parent.setAxisAutoScale(Qwt5.QwtPlot.xBottom) + elif groupBox.objectName() == "y1AutoGroupBox" and not toggled: + self.parent.setAxisAutoScale(Qwt5.QwtPlot.yLeft) + elif groupBox.objectName() == "y2AutoGroupBox" and not toggled: + self.parent.setAxisAutoScale(Qwt5.QwtPlot.yRight) + + def modeComboChanged(self, itemSelected): + """This will catch the combo box selection change and will set the corresponding axis scale to + the value passed as parameter""" + #get and check who fired the signal + comboBox = self.sender() + if comboBox is None or not isinstance(comboBox, Qt.QComboBox): + self.error("TaurusPlotConfig::modeComboChanged(): event is not recognized") + return + else: + #set the scale for the corresponding axis + if comboBox.objectName() == "xModeComboBox": + if not self.parent.getXIsTime(): + self.parent.setAxisScaleType(Qwt5.QwtPlot.xBottom,itemSelected) + elif comboBox.objectName() == "y1ModeComboBox": + self.parent.setAxisScaleType(Qwt5.QwtPlot.yLeft,itemSelected) + elif comboBox.objectName() == "y2ModeComboBox": + self.parent.setAxisScaleType(Qwt5.QwtPlot.yRight,itemSelected) + else: + self.error("TaurusPlotConfig::modeComboChanged() invalid axis: " + comboBox.objectName()) + + def setXDynScale(self,checked): + if checked: + if self.parent.getXIsTime(): rg=self.deltatime2str(self.parent.getXAxisRange(),fuzzy=True) + else: rg="%g"%self.parent.getXAxisRange() + self.ui.xRangeCB.setEditText(rg) + self.ui.xAutoGroupBox.setChecked(True) #disable autoscale for the X axis + else: + self._populateXAxisScales() + self.parent.setXDynScale(checked) + + def peaksComboChanged(self, itemSelected): + """This will catch the combo box selection change and will set the corresponding axis to + show peaks""" + if itemSelected == 0: + maxPeak = minPeak = True + elif itemSelected == 1: + maxPeak = True + minPeak = False + elif itemSelected == 2: + maxPeak = False + minPeak = True + else: + maxPeak = minPeak = False + self.parent.showMaxPeaks(maxPeak) + self.parent.showMinPeaks(minPeak) + + def showCalendar(self,target): + return + self.qcalendar = Qt.QCalendarWidget() + self.qcalendar.show() + + def setCurvesYAxis(self, curvesNamesList=None, axis=None): + """ calls the parent's setCurvesYAxis method but it automatically determines the parameters if not given""" + if curvesNamesList is None: + curvesNamesList = self.curvesAppearanceChooser.getSelectedCurveNames() + if axis is None: + sender=self.sender() + if sender is self.curvesAppearanceChooser.assignToY1BT: + axis=Qwt5.QwtPlot.yLeft + elif sender is self.curvesAppearanceChooser.assignToY2BT: + axis=Qwt5.QwtPlot.yRight + else: + raise ValueError("Cannot determine axis from %s"%sender) + #call the parent's setCurvesYAxis() + self.parent.setCurvesYAxis(curvesNamesList,axis) + #update the axis scale edits + self._populateYAxisScales(axis) + + def changeBackgroundColor(self): + """Launches a dialog for choosing the parent's canvas background color""" + plot = self.parent + color = Qt.QColorDialog.getColor(plot.canvasBackground (), self) + if Qt.QColor.isValid(color): + plot.setCanvasBackground(color) + plot.replot() + + def onChangeTitles(self): + '''Calls The parent's changeCurvesTitlesDialog method, with the selected + curves list as the parameter + ''' + curveNamesList = self.curvesAppearanceChooser.getSelectedCurveNames() + newTitlesDict = self.parent.changeCurvesTitlesDialog(curveNamesList=curveNamesList) + self.curvesAppearanceChooser.updateTitles(newTitlesDict) + + def onCurveTitleEdited(self, name, newTitle): + '''slot used when a curve title is edited + + :param name: (str) curve name + :param name: (QString) new title + ''' + newTitlesDict = self.parent.setCurvesTitle(newTitle, [name]) + self.curvesAppearanceChooser.updateTitles(newTitlesDict) |