1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
|
#!/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/>.
##
#############################################################################
"""
curvesAppearanceChooserDlg.py:
A Qt dialog for choosing plot appearance (symbols and lines)
for a QwtPlot-derived widget (like Taurusplot)
"""
import copy
from taurus.external.qt import Qt, Qwt5
from taurus.core.util.containers import CaselessDict
from taurus.qt.qtgui.resource import getIcon
from taurus.qt.qtgui.util.ui import UILoadable
NamedLineStyles={None:"",
Qt.Qt.NoPen:"No line",
Qt.Qt.SolidLine:"_____",
Qt.Qt.DashLine:"_ _ _",
Qt.Qt.DotLine:".....",
Qt.Qt.DashDotLine:"_._._",
Qt.Qt.DashDotDotLine:".._..",
}
ReverseNamedLineStyles={}
for k,v in NamedLineStyles.iteritems(): ReverseNamedLineStyles[v]=k
NamedCurveStyles={None:"",
Qwt5.QwtPlotCurve.NoCurve:"No curve",
Qwt5.QwtPlotCurve.Lines:"Lines",
Qwt5.QwtPlotCurve.Sticks:"Sticks",
Qwt5.QwtPlotCurve.Steps:"Steps",
Qwt5.QwtPlotCurve.Dots:"Dots"
}
ReverseNamedCurveStyles={}
for k,v in NamedCurveStyles.iteritems(): ReverseNamedCurveStyles[v]=k
NamedSymbolStyles={
None:"",
Qwt5.QwtSymbol.NoSymbol:"No symbol",
Qwt5.QwtSymbol.Ellipse:"Circle",
Qwt5.QwtSymbol.Rect:"Square",
Qwt5.QwtSymbol.Diamond:"Diamond",
Qwt5.QwtSymbol.Triangle:"Triangle",
Qwt5.QwtSymbol.DTriangle:"Down Triangle",
Qwt5.QwtSymbol.UTriangle:"Up triangle",
Qwt5.QwtSymbol.LTriangle:"Left Triangle",
Qwt5.QwtSymbol.RTriangle:"Right Triangle",
Qwt5.QwtSymbol.Cross:"Cross",
Qwt5.QwtSymbol.XCross:"XCross",
Qwt5.QwtSymbol.HLine:"Horizontal line",
Qwt5.QwtSymbol.VLine:"Vertical line",
Qwt5.QwtSymbol.Star1:"Star1",
Qwt5.QwtSymbol.Star2:"Star2",
Qwt5.QwtSymbol.Hexagon:"Hexagon"
}
ReverseNamedSymbolStyles={}
for k,v in NamedSymbolStyles.iteritems(): ReverseNamedSymbolStyles[v]=k
NamedColors=["Black","Red","Blue","Magenta","Green","Cyan","Yellow","Gray","White"]
@UILoadable
class CurvesAppearanceChooser(Qt.QWidget):
"""
A widget for choosing plot appearance for one or more curves.
The current curves properties are passed using the setCurves() method using
a dictionary with the following structure::
curvePropDict={name1:prop1, name2:prop2,...}
where propX is an instance of :class:`CurveAppearanceProperties`
When applying, a signal is emitted and the chosen properties are made
available in a similar dictionary. """
NAME_ROLE = Qt.Qt.UserRole
def __init__(self, parent=None, curvePropDict={}, showButtons=False, autoApply=False, designMode=False):
#try:
super(CurvesAppearanceChooser,self).__init__(parent)
self.loadUi()
self.autoApply=autoApply
self.sStyleCB.insertItems(0,sorted(NamedSymbolStyles.values()))
self.lStyleCB.insertItems(0,NamedLineStyles.values())
self.cStyleCB.insertItems(0,NamedCurveStyles.values())
self.sColorCB.addItem("")
self.lColorCB.addItem("")
if not showButtons:
self.applyBT.hide()
self.resetBT.hide()
for color in NamedColors:
icon=self._colorIcon(color)
self.sColorCB.addItem(icon, "", Qt.QVariant(Qt.QColor(color)))
self.lColorCB.addItem(icon, "", Qt.QVariant(Qt.QColor(color)))
self.__itemsDict = CaselessDict()
self.setCurves(curvePropDict)
self.bckgndBT.setIcon(getIcon(":/color-fill.svg")) #set the icon for the background button (stupid designer limitations forces to do it programatically)
#connections.
# Note: The assignToY1BT and assignToY2BT buttons are not connected to anything
# Their signals are handled by the Config dialog because we haven't got access to the curve objects here
Qt.QObject.connect(self.curvesLW,Qt.SIGNAL("itemSelectionChanged()"),self.onSelectedCurveChanged)
Qt.QObject.connect(self.curvesLW,Qt.SIGNAL("itemChanged(QListWidgetItem *)"),self.onItemChanged)
Qt.QObject.connect(self.applyBT,Qt.SIGNAL("clicked()"),self.onApply)
Qt.QObject.connect(self.resetBT,Qt.SIGNAL("clicked()"),self.onReset)
Qt.QObject.connect(self.sStyleCB,Qt.SIGNAL("currentIndexChanged(const QString&)"),self._onSymbolStyleChanged)
Qt.QObject.connect(self.sStyleCB,Qt.SIGNAL("currentIndexChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.lStyleCB,Qt.SIGNAL("currentIndexChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.sColorCB,Qt.SIGNAL("currentIndexChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.lColorCB,Qt.SIGNAL("currentIndexChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.cStyleCB,Qt.SIGNAL("currentIndexChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.sSizeSB,Qt.SIGNAL("valueChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.lWidthSB,Qt.SIGNAL("valueChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.sFillCB,Qt.SIGNAL("stateChanged(int)"),self.onControlChanged)
Qt.QObject.connect(self.cFillCB,Qt.SIGNAL("stateChanged(int)"),self.onControlChanged)
#except Exception, e:
#print "CURVE APPEARANCE EXCEPTION:",str(e)
def setCurves(self, curvePropDict):
'''Populates the list of curves from the properties dictionary. It uses
the curve title for display, and stores the curve name as the item data
(with role=CurvesAppearanceChooser.NAME_ROLE)
:param curvePropDict: (dict) a dictionary whith keys=curvenames and
values= :class:`CurveAppearanceProperties` object
'''
self.curvePropDict = curvePropDict
self._curvePropDictOrig = copy.deepcopy(curvePropDict)
self.curvesLW.clear()
self.__itemsDict = CaselessDict()
for name,prop in self.curvePropDict.iteritems():
item = Qt.QListWidgetItem(Qt.QString(prop.title), self.curvesLW) #create and insert the item
self.__itemsDict[name] = item
item.setData(self.NAME_ROLE, Qt.QVariant(Qt.QString(name)))
item.setToolTip("<b>Curve Name:</b> %s"%name)
item.setFlags(Qt.Qt.ItemIsEnabled|Qt.Qt.ItemIsSelectable|Qt.Qt.ItemIsUserCheckable|Qt.Qt.ItemIsDragEnabled|Qt.Qt.ItemIsEditable)
self.curvesLW.setCurrentRow(0)
def onItemChanged(self, item):
'''slot used when an item data has changed'''
name = Qt.from_qvariant(item.data(self.NAME_ROLE), str)
previousTitle = self.curvePropDict[name].title
currentTitle = item.text()
if previousTitle!=currentTitle:
self.curvePropDict[name].title = currentTitle
self.curvesLW.emit(Qt.SIGNAL('CurveTitleEdited'), name, currentTitle)
def updateTitles(self, newTitlesDict=None):
'''
Updates the titles of the curves that are displayed in the curves list.
:param newTitlesDict: (dict<str,str>) dictionary with key=curve_name and
value=title
'''
if newTitlesDict is None: return
for name,title in newTitlesDict.iteritems():
self.curvePropDict[name].title = title
self.__itemsDict[name].setText(title)
def getSelectedCurveNames(self):
'''Returns the curve names for the curves selected at the curves list.
*Note*: The names may differ from the displayed text, which
corresponds to the curve titles (this method is what you likely need if
you want to get keys to use in curves or curveProp dicts).
:return: (string_list) the names of the selected curves
'''
return [Qt.from_qvariant(item.data(self.NAME_ROLE), str) for item in self.curvesLW.selectedItems()]
def showProperties(self,prop=None):
'''Updates the dialog to show the given properties.
:param prop: (CurveAppearanceProperties) the properties object
containing what should be shown. If a given property is set
to None, the corresponding widget will show a "neutral"
display
'''
if prop is None: prop=self._shownProp
#set the Style comboboxes
self.sStyleCB.setCurrentIndex(self.sStyleCB.findText(NamedSymbolStyles[prop.sStyle]))
self.lStyleCB.setCurrentIndex(self.lStyleCB.findText(NamedLineStyles[prop.lStyle]))
self.cStyleCB.setCurrentIndex(self.cStyleCB.findText(NamedCurveStyles[prop.cStyle]))
#set sSize and lWidth spinboxes. if prop.sSize is None, it puts -1 (which is the special value for these switchhboxes)
self.sSizeSB.setValue(max(prop.sSize,-1))
self.lWidthSB.setValue(max(prop.lWidth,-1))
#Set the Color combo boxes. The item at index 0 is the empty one in the comboboxes Manage unknown colors by including them
if prop.sColor is None: index=0
else: index=self.sColorCB.findData(Qt.QVariant(Qt.QColor(prop.sColor)))
if index==-1: #if the color is not one of the supported colors, add it to the combobox
index=self.sColorCB.count() #set the index to what will be the added one
self.sColorCB.addItem(self._colorIcon(Qt.QColor(prop.sColor)), "", Qt.QVariant(Qt.QColor(prop.sColor)))
self.sColorCB.setCurrentIndex(index)
if prop.lColor is None: index=0
else: index=self.lColorCB.findData(Qt.QVariant(Qt.QColor(prop.lColor)))
if index==-1: #if the color is not one of the supported colors, add it to the combobox
index=self.lColorCB.count() #set the index to what will be the added one
self.lColorCB.addItem(self._colorIcon(Qt.QColor(prop.lColor)), "", Qt.QVariant(Qt.QColor(prop.lColor)))
self.lColorCB.setCurrentIndex(index)
#set the Fill Checkbox. The prop.sFill value can be in 3 states: True, False and None
if prop.sFill is None: checkState=Qt.Qt.PartiallyChecked
elif prop.sFill: checkState=Qt.Qt.Checked
else: checkState=Qt.Qt.Unchecked
#set the Area Fill Checkbox. The prop.cFill value can be in 3 states: True, False and None
if prop.cFill is None: checkState=Qt.Qt.PartiallyChecked
elif prop.cFill: checkState=Qt.Qt.Checked
else: checkState=Qt.Qt.Unchecked
self.cFillCB.setCheckState(checkState)
def onControlChanged(self,*args):
'''slot to be called whenever a control widget is changed. It emmits a
'controlChanged signal and applies the change if in autoapply mode.
It ignores any arguments passed'''
self.emit(Qt.SIGNAL("controlChanged"))
if self.autoApply: self.onApply()
def onSelectedCurveChanged(self):
"""Updates the shown properties when the curve selection changes"""
plist=[self.curvePropDict[name] for name in self.getSelectedCurveNames()]
if len(plist)==0: plist=[CurveAppearanceProperties()]
self._shownProp=CurveAppearanceProperties.merge(plist)
self.showProperties(self._shownProp)
def _onSymbolStyleChanged(self, text):
'''Slot called when the Symbol style is changed, to ensure that symbols
are visible if you choose them
:param text: (str) the new symbol style label
'''
text=str(text)
if self.sSizeSB.value()<2 and not text in ["","No symbol"]:
self.sSizeSB.setValue(3) #a symbol size of 0 is invisible and 1 means you should use cStyle=dots
def getShownProperties(self):
"""Returns a copy of the currently shown properties and updates
self._shownProp
:return: (CurveAppearanceProperties)
"""
prop=CurveAppearanceProperties()
#get the values from the Style comboboxes. Note that the empty string ("") translates into None
prop.sStyle=ReverseNamedSymbolStyles[str(self.sStyleCB.currentText())]
prop.lStyle=ReverseNamedLineStyles[str(self.lStyleCB.currentText())]
prop.cStyle=ReverseNamedCurveStyles[str(self.cStyleCB.currentText())]
#get sSize and lWidth from the spinboxes
prop.sSize=self.sSizeSB.value()
prop.lWidth=self.lWidthSB.value()
if prop.sSize<0: prop.sSize=None
if prop.lWidth<0: prop.lWidth=None
#Get the Color combo boxes. The item at index 0 is the empty one in the comboboxes
index=self.sColorCB.currentIndex()
if index==0:prop.sColor=None
else:prop.sColor=Qt.QColor(self.sColorCB.itemData(index))
index=self.lColorCB.currentIndex()
if index==0:prop.lColor=None
else:prop.lColor=Qt.QColor(self.lColorCB.itemData(index))
#get the sFill from the Checkbox.
checkState=self.sFillCB.checkState()
if checkState==Qt.Qt.PartiallyChecked: prop.sFill=None
else: prop.sFill=bool(checkState)
#get the cFill from the Checkbox.
checkState=self.cFillCB.checkState()
if checkState==Qt.Qt.PartiallyChecked: prop.cFill=None
else: prop.cFill=bool(checkState)
#store the props
self._shownProp=copy.deepcopy(prop)
return copy.deepcopy(prop)
def onApply(self):
"""Apply does 2 things:
- It updates `self.curvePropDict` using the current values
choosen in the dialog
- It emits a curveAppearanceChanged signal that indicates the names
of the curves that changed and the new properties. (The names and
the properties are returned by the function as well)
:return: (tuple<CurveAppearanceProperties,list>) a tuple containing the
curve properties and a list of the selected curve names (as a
list<str>)
"""
names= self.getSelectedCurveNames()
prop=self.getShownProperties()
#Update self.curvePropDict for selected properties
for n in names:
self.curvePropDict[n]=CurveAppearanceProperties.merge([self.curvePropDict[n],prop],
conflict=CurveAppearanceProperties.inConflict_update_a)
#emit a (PyQt) signal telling what properties (first argument) need to be applied to which curves (second argument)
self.emit(Qt.SIGNAL("curveAppearanceChanged"),prop,names)
#return both values
return prop,names
def onReset(self):
'''slot to be called when the reset action is triggered. It reverts to
the original situation'''
self.setCurves(self._curvePropDictOrig)
self.curvesLW.clearSelection()
def _colorIcon(self,color,w=10,h=10):
#to do: create a border
pixmap=Qt.QPixmap(w,h)
pixmap.fill(Qt.QColor(color))
return Qt.QIcon(pixmap)
class CurveAppearanceProperties(object):
'''An object describing the appearance of a TaurusCurve'''
def __init__(self, sStyle=None, sSize=None, sColor=None, sFill=None,
lStyle=None, lWidth=None, lColor=None, cStyle=None,
yAxis=None, cFill=None, title=None, visible=None):
"""
Creator of :class:`CurveAppearanceProperties`
Possible keyword arguments are:
- sStyle= symbolstyle
- sSize= int
- sColor= color
- sFill= bool
- lStyle= linestyle
- lWidth= int
- lColor= color
- cStyle= curvestyle
- cFill= bool
- yAxis= axis
- visible = bool
- title= title
Where:
- color is a color that QColor() understands (i.e. a
Qt.Qt.GlobalColor, a color name, or a Qt.Qcolor)
- symbolstyle is one of Qwt5.QwtSymbol.Style
- linestyle is one of Qt.Qt.PenStyle
- curvestyle is one of Qwt5.QwtPlotCurve.CurveStyle
- axis is one of Qwt5.QwtPlot.Axis
- title is something that Qwt5.QwtText() accepts in its constructor
(i.e. a QwtText, QString or any basestring)
"""
self.sStyle = sStyle
self.sSize = sSize
self.sColor = sColor
self.sFill = sFill
self.lStyle = lStyle
self.lWidth = lWidth
self.lColor = lColor
self.cStyle = cStyle
self.cFill = cFill
self.yAxis = yAxis
self.title = title
self.visible = visible
self.propertyList = ["sStyle","sSize","sColor","sFill","lStyle","lWidth",
"lColor","cStyle","cFill","yAxis", "title", "visible"]
def _print(self):
"""Just for debug"""
print "-"*77
for k in self.propertyList: print k+"= ",self.__getattribute__(k)
print "-"*77
@staticmethod
def inConflict_update_a(a,b):
"""This function can be passed to CurvesAppearance.merge()
if one wants to update prop1 with prop2 except for those
attributes of prop2 that are set to None"""
if b is None: return a
else: return b
@staticmethod
def inConflict_none(a,b):
"""In case of conflict, returns None"""
return None
def conflictsWith(self, other, strict=True):
"""returns a list of attribute names that are in conflict between this self and other"""
result = []
for aname in self.propertyList:
vself = getattr(self, aname)
vother = getattr(other, aname)
if (vself != vother) and (strict or not(vself is None or vother is None)):
result.append(aname)
return result
@classmethod
def merge(self, plist, attributes=None, conflict=None):
"""returns a CurveAppearanceProperties object formed by merging a list
of other CurveAppearanceProperties objects
**Note:** This is a class method, so it can be called without previously
instantiating an object
:param plist: (sequence<CurveAppearanceProperties>) objects to be merged
:param attributes: (sequence<str>) the name of the attributes to
consider for the merge. If None, all the attributes
will be merged
:param conflict: (callable) a function that takes 2 objects (having a
different attribute)and returns a value that solves the
conflict. If None is given, any conflicting attribute
will be set to None.
:return: (CurveAppearanceProperties) merged properties
"""
n=len(plist)
if n<1: raise ValueError("plist must contain at least 1 member")
plist=copy.deepcopy(plist)
if n==1: return plist[0]
if attributes is None: attributes=["sStyle","sSize","sColor","sFill","lStyle","lWidth","lColor","cStyle","cFill","yAxis","title"]
if conflict is None: conflict=CurveAppearanceProperties.inConflict_none
p=CurveAppearanceProperties()
for a in attributes:
alist=[p.__getattribute__(a) for p in plist]
p.__setattr__(a,alist[0])
for ai in alist[1:]:
if alist[0]!=ai:
# print "MERGING:",alist[0],ai,conflict(alist[0],ai)
p.__setattr__(a,conflict(alist[0],ai))
break
return p
def applyToCurve(self,curve):
"""applies the current properties to a given curve
If a property is set to None, it is not applied to the curve"""
raise DeprecationWarning("CurveAppearanceProperties.applyToCurve() is deprecated. Use TaurusCurve.setAppearanceProperties() instead")
curve.setAppearanceProperties(self)
# s=curve.symbol()
# if self.sStyle is not None: s.setStyle(symbol[self.sStyle])
# if self.sSize is not None: s.setSize(self.sSize)
# if self.sColor is not None: s.brush().setColor(Qt.QColor(self.sColor))
# if self.sFill is not None:
# if self.sFill: s.brush().setStyle(Qt.Qt.SolidPattern)
# else: s.brush().setStyle(Qt.Qt.NoBrush)
# p=curve.pen()
# if self.lStyle is not None: p.setStyle(lineStyles[self.lStyle])
# if self.lWidth is not None: p.setWidth(self.lWidth)
# if self.lColor is not None: p.setColor(Qt.QColor(self.lColor))
# curveStyle=curve.style()
# if self.cStyle is not None: curveStyle.setStyle(self.cStyle)
# if self.cFill is not None:
# if self.cFill:
# color = p.color()
# color.setAlphaF(0.5)
# b = self.brush()
# b.setColor(color)
# b.setStyle(Qt.Qt.SolidPattern)
# else:
# c.brush().setStyle(Qt.Qt.NoBrush)
# if self.yAxis is not None: curve.setYAxis(self.yAxis)
# if self.title is not None: curve.setTitle(Qwt5.QwtText(self.title))
|