#!/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
"""
__all__ = ["TaurusPlotConfigDialog"]
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')
[docs]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)
# this call already initialises the edit boxes of the X axis
self.setXDynScale(self.parent.getXDynScale())
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.getAxisTransformationType(axis)
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.ui.buttonBox.button(
Qt.QDialogButtonBox.Close).clicked.connect(self.hide)
self.ui.buttonBox.button(
Qt.QDialogButtonBox.Apply).clicked.connect(self.apply)
self.ui.xAutoGroupBox.toggled.connect(self.toggledAutoScale)
self.ui.y1AutoGroupBox.toggled.connect(self.toggledAutoScale)
self.ui.y2AutoGroupBox.toggled.connect(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.ui.xModeComboBox.currentIndexChanged.connect(self.modeComboChanged)
self.ui.xDynScaleCheckBox.toggled.connect(self.setXDynScale)
#self.connect(self.ui.xRangeCB, Qt.SIGNAL("currentIndexChanged(const QString&)"),self.apply)
self.ui.y1ModeComboBox.currentIndexChanged.connect(self.modeComboChanged)
self.ui.y2ModeComboBox.currentIndexChanged.connect(self.modeComboChanged)
self.ui.peaksComboBox.currentIndexChanged.connect(self.peaksComboChanged)
# self.connect(self.curvesAppearanceChooser,
# Qt.SIGNAL("controlChanged"),self.apply) #"autoapply" mode for *all*
# the curve appearance controls
self.curvesAppearanceChooser.assignToY1BT.clicked[()].connect(self.setCurvesYAxis)
self.curvesAppearanceChooser.assignToY2BT.clicked[()].connect(self.setCurvesYAxis)
self.curvesAppearanceChooser.bckgndBT.clicked.connect(self.changeBackgroundColor)
self.curvesAppearanceChooser.changeTitlesBT.clicked.connect(self.onChangeTitles)
self.curvesAppearanceChooser.CurveTitleEdited.connect(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()))
[docs] 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)
[docs] 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
[docs] 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
[docs] 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)
[docs] 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
[docs] 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)
[docs] 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())
[docs] 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)
# disable autoscale for the X axis
self.ui.xAutoGroupBox.setChecked(True)
else:
self._populateXAxisScales()
self.parent.setXDynScale(checked)
[docs] 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)
[docs] def showCalendar(self, target):
return
self.qcalendar = Qt.QCalendarWidget()
self.qcalendar.show()
[docs] 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)
[docs] 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()
[docs] 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)
[docs] 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)