summaryrefslogtreecommitdiff
path: root/taurus/lib/taurus/qt/qtgui/plot/curvesAppearanceChooserDlg.py
blob: ebb043eee53123bef8cfa6e875881bab5e4af3a3 (plain)
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))