source: orange/Orange/OrangeWidgets/OWGUI.py @ 10939:2aabe197973c

Revision 10939:2aabe197973c, 83.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 22 months ago (diff)

Pass 'label' and 'tooltip' to hSlider call in qwtHSlider function.

Line 
1from PyQt4.QtCore import *
2from PyQt4.QtGui import *
3import math
4#import sys, traceback
5
6YesNo = NoYes = ("No", "Yes")
7groupBoxMargin = 7
8
9import os.path
10
11def id_generator(id):
12    while True:
13        id += 1
14        yield id
15       
16OrangeUserRole = id_generator(Qt.UserRole)
17
18enter_icon = None
19
20def getdeepattr(obj, attr, **argkw):
21    if type(obj) == dict:
22        return obj.get(attr)
23    try:
24        return reduce(lambda o, n: getattr(o, n),  attr.split("."), obj)
25    except:
26# I (JD) commented this out. This is ugly and dangerous.
27# If any widget wants this behavour, it should redefine its __getattr__ to return defaults.
28#        if argkw.has_key("default"):
29#            return argkw["default"]
30#        else:
31            raise AttributeError, "'%s' has no attribute '%s'" % (obj, attr)
32
33
34def getEnterIcon():
35    global enter_icon
36    if not enter_icon:
37        enter_icon = QIcon(os.path.dirname(__file__) + "/icons/Dlg_enter.png")
38    return enter_icon
39
40
41# constructs a box (frame) if not none, and returns the right master widget
42def widgetBox(widget, box=None, orientation='vertical', addSpace=False, sizePolicy = None, margin = -1, spacing = -1, flat = 0, addToLayout = 1):
43    if box:
44        b = QGroupBox(widget)
45        if type(box) in (str, unicode): # if you pass 1 for box, there will be a box, but no text
46            b.setTitle(" "+box.strip()+" ")
47        if margin == -1: margin = groupBoxMargin
48        b.setFlat(flat)
49    else:
50        b = QWidget(widget)
51        if margin == -1: margin = 0
52    if addToLayout and widget.layout() is not None:
53        widget.layout().addWidget(b)
54
55    if isinstance(orientation, QLayout):
56        b.setLayout(orientation)
57    elif orientation == 'horizontal' or not orientation:
58        b.setLayout(QHBoxLayout())
59##        b.setSizePolicy(sizePolicy or QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Minimum))
60    else:
61        b.setLayout(QVBoxLayout())
62##        b.setSizePolicy(sizePolicy or QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed))
63    if sizePolicy:
64        b.setSizePolicy(sizePolicy)
65
66    if spacing == -1: spacing = 4
67    b.layout().setSpacing(spacing)
68    if margin != -1:
69        b.layout().setMargin(margin)
70
71    if addSpace and isinstance(addSpace, bool):
72        separator(widget)
73    elif addSpace and isinstance(addSpace, int):
74        separator(widget, addSpace, addSpace)
75    elif addSpace:
76        separator(widget)
77
78    return b
79
80def indentedBox(widget, sep=20, orientation = True, addSpace=False):
81    r = widgetBox(widget, orientation = "horizontal", spacing=0)
82    separator(r, sep, 0)
83
84    if addSpace and isinstance(addSpace, bool):
85        separator(widget)
86    elif addSpace and isinstance(addSpace, int):
87        separator(widget, 0, addSpace)
88    elif addSpace:
89        separator(widget)
90
91    return widgetBox(r, orientation = orientation)
92
93def widgetLabel(widget, label=None, labelWidth=None, addToLayout = 1):
94    if label is not None:
95        lbl = QLabel(label, widget)
96        if labelWidth:
97            lbl.setFixedSize(labelWidth, lbl.sizeHint().height())
98        if widget.layout() is not None and addToLayout: widget.layout().addWidget(lbl)
99    else:
100        lbl = None
101
102    return lbl
103
104
105import re
106__re_frmt = re.compile(r"(^|[^%])%\((?P<value>[a-zA-Z]\w*)\)")
107
108def label(widget, master, label, labelWidth = None):
109    lbl = QLabel("", widget)
110    if widget.layout() is not None: widget.layout().addWidget(lbl)
111
112    reprint = CallFrontLabel(lbl, label, master)
113    for mo in __re_frmt.finditer(label):
114        master.controlledAttributes[mo.group("value")] = reprint
115    reprint()
116
117    if labelWidth:
118        lbl.setFixedSize(labelWidth, lbl.sizeHint().height())
119
120    return lbl
121
122
123class SpinBoxWFocusOut(QSpinBox):
124    def __init__(self, min, max, step, bi):
125        QSpinBox.__init__(self, bi)
126        self.setRange(min, max)
127        self.setSingleStep(step)
128        self.inSetValue = False
129        self.enterButton = None
130
131    def onChange(self, value):
132        if not self.inSetValue:
133            self.placeHolder.hide()
134            self.enterButton.show()
135
136    def onEnter(self):
137        if self.enterButton.isVisible():
138            self.enterButton.hide()
139            self.placeHolder.show()
140            if self.cback:
141                self.cback(int(str(self.text())))
142            if self.cfunc:
143                self.cfunc()
144
145    # doesn't work: it's probably LineEdit's focusOut that we should (and can't) catch
146    def focusOutEvent(self, *e):
147        QSpinBox.focusOutEvent(self, *e)
148        if self.enterButton and self.enterButton.isVisible():
149            self.onEnter()
150
151    def setValue(self, value):
152        self.inSetValue = True
153        QSpinBox.setValue(self, value)
154        self.inSetValue = False
155
156
157def checkWithSpin(widget, master, label, min, max, checked, value, posttext = None, step = 1, tooltip=None,
158                  checkCallback=None, spinCallback=None, getwidget=None,
159                  labelWidth=None, debuggingEnabled = 1, controlWidth=55,
160                  callbackOnReturn = False):
161    return spin(widget, master, value, min, max, step, None, label, labelWidth, 0, tooltip,
162                spinCallback, debuggingEnabled, controlWidth, callbackOnReturn, checked, checkCallback, posttext)
163
164
165
166def spin(widget, master, value, min, max, step=1,
167         box=None, label=None, labelWidth=None, orientation=None, tooltip=None,
168         callback=None, debuggingEnabled = 1, controlWidth = None, callbackOnReturn = False,
169         checked = "", checkCallback = None, posttext = None, addToLayout=True,
170         alignment = Qt.AlignLeft, keyboardTracking=True):
171    if box or label and not checked:
172        b = widgetBox(widget, box, orientation)
173        hasHBox = orientation == 'horizontal' or not orientation
174    else:
175        b = widget
176        hasHBox = False
177
178    if not hasHBox and (checked or callback and callbackOnReturn or posttext):
179        bi = widgetBox(b, "", 0)
180    else:
181        bi = b
182
183    if checked:
184        wb = checkBox(bi, master, checked, label, labelWidth = labelWidth, callback=checkCallback, debuggingEnabled = debuggingEnabled)
185    elif label:
186        b.label = widgetLabel(b, label, labelWidth)
187
188
189    wa = bi.control = SpinBoxWFocusOut(min, max, step, bi)
190    wa.setAlignment(alignment)
191    wa.setKeyboardTracking(keyboardTracking) # If false it wont emit valueChanged signals while editing the text
192    if addToLayout and bi.layout() is not None:
193        bi.layout().addWidget(wa)
194    # must be defined because of the setText below
195    if controlWidth:
196        wa.setFixedWidth(controlWidth)
197    if tooltip:
198        wa.setToolTip(tooltip)
199    if value:
200        wa.setValue(getdeepattr(master, value))
201
202    cfront, wa.cback, wa.cfunc = connectControl(wa, master, value, callback, not (callback and callbackOnReturn) and "valueChanged(int)", CallFrontSpin(wa))
203
204    if checked:
205        wb.disables = [wa]
206        wb.makeConsistent()
207
208    if callback and callbackOnReturn:
209        wa.enterButton, wa.placeHolder = enterButton(bi, wa.sizeHint().height())
210        QObject.connect(wa, SIGNAL("valueChanged(const QString &)"), wa.onChange)
211        QObject.connect(wa, SIGNAL("editingFinished()"), wa.onEnter)
212        QObject.connect(wa.enterButton, SIGNAL("clicked()"), wa.onEnter)
213        if hasattr(wa, "upButton"):
214            QObject.connect(wa.upButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus())
215            QObject.connect(wa.downButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus())
216
217    if posttext:
218        widgetLabel(bi, posttext)
219
220    if debuggingEnabled and hasattr(master, "_guiElements"):
221        master._guiElements = getattr(master, "_guiElements", []) + [("spin", wa, value, min, max, step, callback)]
222
223    if checked:
224        return wb, wa
225    else:
226        return b
227
228
229class DoubleSpinBoxWFocusOut(QDoubleSpinBox):
230    def __init__(self, min, max, step, bi):
231        QDoubleSpinBox.__init__(self, bi)
232        self.setDecimals(math.ceil(-math.log10(step)))
233        self.setRange(min, max)
234        self.setSingleStep(step)
235        self.inSetValue = False
236        self.enterButton = None
237
238    def onChange(self, value):
239        if not self.inSetValue:
240            self.placeHolder.hide()
241            self.enterButton.show()
242
243    def onEnter(self):
244        if self.enterButton.isVisible():
245            self.enterButton.hide()
246            self.placeHolder.show()
247            if self.cback:
248                self.cback(float(str(self.text()).replace(",", ".")))
249            if self.cfunc:
250                self.cfunc()
251
252    # doesn't work: it's probably LineEdit's focusOut that we should (and can't) catch
253    def focusOutEvent(self, *e):
254        QDoubleSpinBox.focusOutEvent(self, *e)
255        if self.enterButton and self.enterButton.isVisible():
256            self.onEnter()
257
258    def setValue(self, value):
259        self.inSetValue = True
260        QDoubleSpinBox.setValue(self, value)
261        self.inSetValue = False
262       
263def doubleSpin(widget, master, value, min, max, step=1,
264         box=None, label=None, labelWidth=None, orientation=None, tooltip=None,
265         callback=None, debuggingEnabled = 1, controlWidth = None, callbackOnReturn = False,
266         checked = "", checkCallback = None, posttext = None, addToLayout=True, alignment = Qt.AlignLeft,
267         keyboardTracking=True, decimals=None):
268    if box or label and not checked:
269        b = widgetBox(widget, box, orientation)
270        hasHBox = orientation == 'horizontal' or not orientation
271    else:
272        b = widget
273        hasHBox = False
274
275    if not hasHBox and (checked or callback and callbackOnReturn or posttext):
276        bi = widgetBox(b, "", 0)
277    else:
278        bi = b
279
280    if checked:
281        wb = checkBox(bi, master, checked, label, labelWidth = labelWidth, callback=checkCallback, debuggingEnabled = debuggingEnabled)
282    elif label:
283        widgetLabel(b, label, labelWidth)
284
285
286    wa = bi.control = DoubleSpinBoxWFocusOut(min, max, step, bi)
287
288    if decimals is not None:
289        wa.setDecimals(decimals)
290
291    wa.setAlignment(alignment)
292    wa.setKeyboardTracking(keyboardTracking) # If false it wont emit valueChanged signals while editing the text
293    if addToLayout and bi.layout() is not None:
294        bi.layout().addWidget(wa)
295    # must be defined because of the setText below
296    if controlWidth:
297        wa.setFixedWidth(controlWidth)
298    if tooltip:
299        wa.setToolTip(tooltip)
300    if value:
301        wa.setValue(getdeepattr(master, value))
302
303    cfront, wa.cback, wa.cfunc = connectControl(wa, master, value, callback, not (callback and callbackOnReturn) and "valueChanged(double)", CallFrontDoubleSpin(wa))
304
305    if checked:
306        wb.disables = [wa]
307        wb.makeConsistent()
308
309    if callback and callbackOnReturn:
310        wa.enterButton, wa.placeHolder = enterButton(bi, wa.sizeHint().height())
311        QObject.connect(wa, SIGNAL("valueChanged(const QString &)"), wa.onChange)
312        QObject.connect(wa, SIGNAL("editingFinished()"), wa.onEnter)
313        QObject.connect(wa.enterButton, SIGNAL("clicked()"), wa.onEnter)
314        if hasattr(wa, "upButton"):
315            QObject.connect(wa.upButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus())
316            QObject.connect(wa.downButton(), SIGNAL("clicked()"), lambda c=wa.editor(): c.setFocus())
317
318    if posttext:
319        widgetLabel(bi, posttext)
320
321##    if debuggingEnabled and hasattr(master, "_guiElements"):
322##        master._guiElements = getattr(master, "_guiElements", []) + [("spin", wa, value, min, max, step, callback)]
323
324    if checked:
325        return wb, wa
326    else:
327        if b==widget:
328            wa.control = b.control # Backward compatibility
329            return wa
330        else:
331            return b
332
333def checkBox(widget, master, value, label, box=None, tooltip=None, callback=None, getwidget=None, id=None, disabled=0, labelWidth=None, disables = [], addToLayout = 1, debuggingEnabled = 1):
334    if box:
335        b = widgetBox(widget, box, orientation=None)
336    else:
337        b = widget
338    wa = QCheckBox(label, b)
339    if addToLayout and b.layout() is not None:
340        b.layout().addWidget(wa)
341
342    if labelWidth:
343        wa.setFixedSize(labelWidth, wa.sizeHint().height())
344    wa.setChecked(getdeepattr(master, value))
345    if disabled:
346        wa.setDisabled(1)
347    if tooltip:
348        wa.setToolTip(tooltip)
349
350    cfront, cback, cfunc = connectControl(wa, master, value, None, "toggled(bool)", CallFrontCheckBox(wa),
351                                          cfunc = callback and FunctionCallback(master, callback, widget=wa, getwidget=getwidget, id=id))
352    wa.disables = disables or [] # need to create a new instance of list (in case someone would want to append...)
353    wa.makeConsistent = Disabler(wa, master, value)
354    QObject.connect(wa, SIGNAL("toggled(bool)"), wa.makeConsistent)
355    wa.makeConsistent.__call__(value)
356    if debuggingEnabled and hasattr(master, "_guiElements"):
357        master._guiElements = getattr(master, "_guiElements", []) + [("checkBox", wa, value, callback)]
358    return wa
359
360
361def enterButton(parent, height, placeholder = True):
362    button = QToolButton(parent)
363    button.setFixedSize(height, height)
364    button.setIcon(getEnterIcon())
365    if parent.layout() is not None:
366        parent.layout().addWidget(button)
367    if not placeholder:
368        return button
369
370    button.hide()
371    holder = QWidget(parent)
372    holder.setFixedSize(height, height)
373    if parent.layout() is not None:
374        parent.layout().addWidget(holder)
375    return button, holder
376
377
378class LineEditWFocusOut(QLineEdit):
379    def __init__(self, parent, master, callback, focusInCallback=None, placeholder=False):
380        QLineEdit.__init__(self, parent)
381        if parent.layout() is not None:
382            parent.layout().addWidget(self)
383        self.callback = callback
384        self.focusInCallback = focusInCallback
385        if placeholder:
386            self.enterButton, self.placeHolder = enterButton(parent, self.sizeHint().height(), placeholder)
387        else:
388            self.enterButton = enterButton(parent, self.sizeHint().height(), placeholder)
389            self.placeHolder = None
390        QObject.connect(self.enterButton, SIGNAL("clicked()"), self.returnPressed)
391        QObject.connect(self, SIGNAL("textChanged(const QString &)"), self.markChanged)
392        QObject.connect(self, SIGNAL("returnPressed()"), self.returnPressed)
393
394    def markChanged(self, *e):
395        if self.placeHolder:
396            self.placeHolder.hide()
397        self.enterButton.show()
398
399    def markUnchanged(self, *e):
400        self.enterButton.hide()
401        if self.placeHolder:
402            self.placeHolder.show()
403
404    def returnPressed(self):
405        if self.enterButton.isVisible():
406            self.markUnchanged()
407            if hasattr(self, "cback") and self.cback:
408                self.cback(self.text())
409            if self.callback:
410                self.callback()
411
412    def setText(self, t):
413        QLineEdit.setText(self, t)
414        if self.enterButton:
415            self.markUnchanged()
416
417    def focusOutEvent(self, *e):
418        QLineEdit.focusOutEvent(self, *e)
419        self.returnPressed()
420
421    def focusInEvent(self, *e):
422        if self.focusInCallback:
423            self.focusInCallback()
424        return QLineEdit.focusInEvent(self, *e)
425
426
427def lineEdit(widget, master, value,
428             label=None, labelWidth=None, orientation='vertical', box=None, tooltip=None,
429             callback=None, valueType = unicode, validator=None, controlWidth = None, callbackOnType = False, focusInCallback = None, enterPlaceholder=False, **args):
430    if box or label:
431        b = widgetBox(widget, box, orientation)
432        widgetLabel(b, label, labelWidth)
433        hasHBox = orientation == 'horizontal' or not orientation
434    else:
435        b = widget
436        hasHBox = False
437
438    if args.has_key("baseClass"):
439        wa = args["baseClass"](b)
440        wa.enterButton = None
441        if b and b.layout() is not None:
442            b.layout().addWidget(wa)
443    elif focusInCallback or callback and not callbackOnType:
444        if not hasHBox:
445            bi = widgetBox(b, "", 0)
446        else:
447            bi = b
448        wa = LineEditWFocusOut(bi, master, callback, focusInCallback, enterPlaceholder)
449    else:
450        wa = QLineEdit(b)
451        wa.enterButton = None
452        if b and b.layout() is not None:
453            b.layout().addWidget(wa)
454
455    if value:
456        wa.setText(unicode(getdeepattr(master, value)))
457
458    if controlWidth:
459        wa.setFixedWidth(controlWidth)
460
461    if tooltip:
462        wa.setToolTip(tooltip)
463    if validator:
464        wa.setValidator(validator)
465
466    if value:
467        wa.cback = connectControl(wa, master, value, callbackOnType and callback, "textChanged(const QString &)", CallFrontLineEdit(wa), fvcb = value and valueType)[1]
468
469    wa.box = b
470    return wa
471
472
473def button(widget, master, label, callback = None, disabled=0, tooltip=None,
474           debuggingEnabled = 1, width = None, height = None, toggleButton = False,
475           value = "", addToLayout = 1, default=False, autoDefault=False):
476    btn = QPushButton(label, widget)
477    if addToLayout and widget.layout() is not None:
478        widget.layout().addWidget(btn)
479
480    if width:
481        btn.setFixedWidth(width)
482    if height:
483        btn.setFixedHeight(height)
484    btn.setDisabled(disabled)
485    if tooltip:
486        btn.setToolTip(tooltip)
487       
488    if toggleButton or value:
489        btn.setCheckable(True)
490       
491    btn.setDefault(default)
492    btn.setAutoDefault(autoDefault)
493       
494    if value:
495        btn.setChecked(getdeepattr(master, value))
496        cfront, cback, cfunc = connectControl(btn, master, value, None, "toggled(bool)", CallFrontButton(btn),
497                                  cfunc = callback and FunctionCallback(master, callback, widget=btn))
498    elif callback:
499        QObject.connect(btn, SIGNAL("clicked()"), callback)
500       
501    if debuggingEnabled and hasattr(master, "_guiElements"):
502        master._guiElements = getattr(master, "_guiElements", []) + [("button", btn, callback)]
503    return btn
504
505def toolButton(widget, master, label="", callback = None, width = None, height = None, tooltip = None, addToLayout = 1, debuggingEnabled = 1):
506    if not isinstance(label, basestring) and hasattr(label, "__call__"):
507        import warnings
508        warnings.warn("Third positional argument to 'OWGUI.toolButton' must be a string.", DeprecationWarning)
509        label, callback = "", label
510       
511    btn = QToolButton(widget)
512    if addToLayout and widget.layout() is not None:
513        widget.layout().addWidget(btn)
514    if label:
515        btn.setText(label)
516    if width != None:
517        btn.setFixedWidth(width)
518    if height!= None:
519        btn.setFixedHeight(height)
520    if tooltip != None:
521        btn.setToolTip(tooltip)
522    if callback:
523        QObject.connect(btn, SIGNAL("clicked()"), callback)
524    if debuggingEnabled and hasattr(master, "_guiElements"):
525        master._guiElements = getattr(master, "_guiElements", []) + [("button", btn, callback)]
526    return btn
527
528
529def separator(widget, width=4, height=4):
530#    if isinstance(widget.layout(), QVBoxLayout):
531#        return widget.layout().addSpacing(height)
532#    elif isinstance(widget.layout(), QHBoxLayout):
533#        return widget.layout().addSpacing(width)
534#    return None
535    sep = QWidget(widget)
536#    sep.setStyleSheet("background: #000000;")
537    if widget.layout() is not None:
538        widget.layout().addWidget(sep)
539    sep.setFixedSize(width, height)
540    return sep
541
542def rubber(widget):
543    widget.layout().addStretch(100)
544
545def createAttributePixmap(char, color = Qt.black):
546    pixmap = QPixmap(13,13)
547    painter = QPainter()
548    painter.begin(pixmap)
549    painter.setPen( color );
550    painter.setBrush( color );
551    painter.drawRect( 0, 0, 13, 13 );
552    painter.setPen( QColor(Qt.white))
553    painter.drawText(3, 11, char)
554    painter.end()
555    return QIcon(pixmap)
556
557
558attributeIconDict = None
559
560def getAttributeIcons():
561    import orange
562    global attributeIconDict
563    if not attributeIconDict:
564        attributeIconDict = {orange.VarTypes.Continuous: createAttributePixmap("C", QColor(202,0,32)),
565                     orange.VarTypes.Discrete: createAttributePixmap("D", QColor(26,150,65)),
566                     orange.VarTypes.String: createAttributePixmap("S", Qt.black),
567                     -1: createAttributePixmap("?", QColor(128, 128, 128))}
568    return attributeIconDict
569
570
571def listBox(widget, master, value = None, labels = None, box = None, tooltip = None, callback = None, selectionMode = QListWidget.SingleSelection, enableDragDrop = 0, dragDropCallback = None, dataValidityCallback = None, sizeHint = None, debuggingEnabled = 1):
572    bg = box and widgetBox(widget, box, orientation = "horizontal") or widget
573    lb = OrangeListBox(master, value, enableDragDrop, dragDropCallback, dataValidityCallback, sizeHint, bg)
574    lb.box = bg
575    lb.setSelectionMode(selectionMode)
576    if bg.layout() is not None:
577        bg.layout().addWidget(lb)
578
579    if value != None:
580        clist = getdeepattr(master, value)
581        if type(clist) >= ControlledList:
582            clist = ControlledList(clist, lb)
583            master.__setattr__(value, clist)
584
585    lb.ogValue = value
586    lb.ogLabels = labels
587    lb.ogMaster = master
588    if tooltip:
589        lb.setToolTip(tooltip)
590
591    connectControl(lb, master, value, callback, "itemSelectionChanged()", CallFrontListBox(lb), CallBackListBox(lb, master))
592    if hasattr(master, "controlledAttributes") and labels != None:
593        master.controlledAttributes[labels] = CallFrontListBoxLabels(lb)
594    if labels != None:
595        setattr(master, labels, getdeepattr(master, labels))
596    if value != None:
597        setattr(master, value, getdeepattr(master, value))
598    if debuggingEnabled and hasattr(master, "_guiElements"):
599        master._guiElements = getattr(master, "_guiElements", []) + [("listBox", lb, value, callback)]
600    return lb
601
602
603# btnLabels is a list of either char strings or pixmaps
604def radioButtonsInBox(widget, master, value, btnLabels, box=None, tooltips=None, callback=None, debuggingEnabled = 1, addSpace = False, orientation = 'vertical', label = None):
605    if box:
606        bg = widgetBox(widget, box, orientation)
607    else:
608        bg = widget
609
610    bg.group = QButtonGroup(bg)
611
612    if addSpace:
613        separator(widget)
614
615    if not label is None:
616        widgetLabel(bg, label)
617
618    bg.buttons = []
619    bg.ogValue = value
620    for i in range(len(btnLabels)):
621        appendRadioButton(bg, master, value, btnLabels[i], tooltips and tooltips[i], callback = callback)
622
623    connectControl(bg.group, master, value, callback, "buttonClicked (int)", CallFrontRadioButtons(bg), CallBackRadioButton(bg, master))
624
625    if debuggingEnabled and hasattr(master, "_guiElements"):
626        master._guiElements = getattr(master, "_guiElements", []) + [("radioButtonsInBox", bg, value, callback)]
627    return bg
628
629
630def appendRadioButton(bg, master, value, label, tooltip = None, insertInto = None, callback = None, addToLayout=True):
631    dest = insertInto or bg
632
633    if not hasattr(bg, "buttons"):
634        bg.buttons = []
635    i = len(bg.buttons)
636
637    if type(label) in (str, unicode):
638        w = QRadioButton(label)
639    else:
640        w = QRadioButton(unicode(i))
641        w.setIcon(QIcon(label))
642    #w.ogValue = value
643    if addToLayout and dest.layout() is not None:
644        dest.layout().addWidget(w)
645    if not hasattr(bg, "group"):
646        bg.group = QButtonGroup(bg)
647    bg.group.addButton(w)
648
649    w.setChecked(getdeepattr(master, value) == i)
650    bg.buttons.append(w)
651#    if callback == None and hasattr(bg, "callback"):
652#        callback = bg.callback
653#    if callback != None:
654#        connectControl(w, master, value, callback, "clicked()", CallFrontRadioButtons(bg), CallBackRadioButton(w, master, bg))
655    if tooltip:
656        w.setToolTip(tooltip)
657    return w
658
659#def radioButton(widget, master, value, label, box = None, tooltip = None, callback = None, debuggingEnabled = 1):
660#    if box:
661#        bg = widgetBox(widget, box, orientation="horizontal")
662#    else:
663#        bg = widget
664#
665#    if type(label) in (str, unicode):
666#        w = QRadioButton(label, bg)
667#    else:
668#        w = QRadioButton("X")
669#        w.setPixmap(label)
670#    if bg.layout(): bg.layout().addWidget(w)
671#
672#    w.setChecked(getdeepattr(master, value))
673#    if tooltip:
674#        w.setToolTip(tooltip)
675#
676#    connectControl(w, master, value, callback, "stateChanged(int)", CallFrontCheckBox(w))
677#    if debuggingEnabled and hasattr(master, "_guiElements"):
678#        master._guiElements = getattr(master, "_guiElements", []) + [("radioButton", w, value, callback)]
679#    return w
680
681
682def hSlider(widget, master, value, box=None, minValue=0, maxValue=10, step=1, callback=None, label=None, labelFormat=" %d", ticks=0, divideFactor = 1.0, debuggingEnabled = 1, vertical = False, createLabel = 1, tooltip = None, width = None, intOnly = 1):
683    sliderBox = widgetBox(widget, box, orientation = "horizontal")
684    if label:
685        lbl = widgetLabel(sliderBox, label)
686
687    if vertical:
688        sliderOrient = Qt.Vertical
689    else:
690        sliderOrient = Qt.Horizontal
691       
692    if intOnly:
693        slider = QSlider(sliderOrient, sliderBox)
694        slider.setRange(minValue, maxValue)
695        if step != 0:
696            slider.setSingleStep(step)
697            slider.setPageStep(step)
698            slider.setTickInterval(step)
699        signal_signature = "valueChanged(int)"
700    else:
701        slider = FloatSlider(sliderOrient, minValue, maxValue, step)
702        signal_signature = "valueChangedFloat(double)"
703    slider.setValue(getdeepattr(master, value))
704   
705    if tooltip:
706        slider.setToolTip(tooltip)
707
708    if width != None:
709        slider.setFixedWidth(width)
710
711    if sliderBox.layout() is not None:
712        sliderBox.layout().addWidget(slider)
713
714    if ticks:
715        slider.setTickPosition(QSlider.TicksBelow)
716        slider.setTickInterval(ticks)
717
718    if createLabel:
719        label = QLabel(sliderBox)
720        if sliderBox.layout() is not None:
721            sliderBox.layout().addWidget(label)
722        label.setText(labelFormat % minValue)
723        width1 = label.sizeHint().width()
724        label.setText(labelFormat % maxValue)
725        width2 = label.sizeHint().width()
726        label.setFixedSize(max(width1, width2), label.sizeHint().height())
727        txt = labelFormat % (getdeepattr(master, value)/divideFactor)
728        label.setText(txt)
729        label.setLbl = lambda x, l=label, f=labelFormat: l.setText(f % (x/divideFactor))
730        QObject.connect(slider, SIGNAL(signal_signature), label.setLbl)
731
732    connectControl(slider, master, value, callback, signal_signature, CallFrontHSlider(slider))
733    # For compatibility with qwtSlider
734    slider.box = sliderBox
735    if debuggingEnabled and hasattr(master, "_guiElements"):
736        master._guiElements = getattr(master, "_guiElements", []) + [("hSlider", slider, value, minValue, maxValue, step, callback)]
737    return slider
738
739
740def qwtHSlider(widget, master, value, box=None, label=None, labelWidth=None, minValue=1, maxValue=10, step=0.1, precision=1, callback=None, logarithmic=0, ticks=0, maxWidth=80, tooltip = None, showValueLabel = 1, debuggingEnabled = 1, addSpace=False, orientation=0):
741    if not logarithmic:
742        if type(precision) == str:
743            format = precision
744        elif precision == 0:
745            format = " %d"
746        else:
747            format = " %s.%df" % ("%", precision)
748
749        return hSlider(widget, master, value, box, minValue, maxValue, step,
750                       callback, label=label, labelFormat=format,
751                       width=maxWidth, tooltip=tooltip,
752                       debuggingEnabled=debuggingEnabled, intOnly=0)
753
754    import PyQt4.Qwt5 as qwt
755
756    init = getdeepattr(master, value)
757
758    if label:
759        hb = widgetBox(widget, box, orientation) 
760        lbl = widgetLabel(hb, label)
761        if labelWidth:
762            lbl.setFixedSize(labelWidth, lbl.sizeHint().height())
763        if orientation and orientation!="horizontal":
764            separator(hb, height=2)
765            hb = widgetBox(hb, 0)
766    else:
767        hb = widgetBox(widget, box, 0)
768
769    if ticks:
770        slider = qwt.QwtSlider(hb, Qt.Horizontal, qwt.QwtSlider.Bottom, qwt.QwtSlider.BgSlot)
771    else:
772        slider = qwt.QwtSlider(hb, Qt.Horizontal, qwt.QwtSlider.NoScale, qwt.QwtSlider.BgSlot)
773    hb.layout().addWidget(slider)
774
775    slider.setScale(minValue, maxValue, logarithmic) # the third parameter for logaritmic scale
776    slider.setScaleMaxMinor(10)
777    slider.setThumbWidth(20)
778    slider.setThumbLength(12)
779    if maxWidth:
780        slider.setMaximumSize(maxWidth,40)
781    if logarithmic:
782        slider.setRange(math.log10(minValue), math.log10(maxValue), step)
783        slider.setValue(math.log10(init))
784    else:
785        slider.setRange(minValue, maxValue, step)
786        slider.setValue(init)
787    if tooltip:
788        hb.setToolTip(tooltip)
789
790##    format = "%s%d.%df" % ("%", precision+3, precision)
791#    format = " %s.%df" % ("%", precision)
792    if type(precision) == str:  format = precision
793    else:                       format = " %s.%df" % ("%", precision)
794
795    if showValueLabel:
796        lbl = widgetLabel(hb, format % minValue)
797        width1 = lbl.sizeHint().width()
798        lbl.setText(format % maxValue)
799        width2 = lbl.sizeHint().width()
800        lbl.setFixedSize(max(width1, width2), lbl.sizeHint().height())
801        lbl.setText(format % init)
802
803    if logarithmic:
804        cfront = CallFrontLogSlider(slider)
805        cback = ValueCallback(master, value, f=lambda x: 10**x)
806        if showValueLabel: QObject.connect(slider, SIGNAL("valueChanged(double)"), SetLabelCallback(master, lbl, format=format, f=lambda x: 10**x))
807    else:
808        cfront = CallFrontHSlider(slider)
809        cback = ValueCallback(master, value)
810        if showValueLabel: QObject.connect(slider, SIGNAL("valueChanged(double)"), SetLabelCallback(master, lbl, format=format))
811    connectControl(slider, master, value, callback, "valueChanged(double)", cfront, cback)
812    slider.box = hb
813
814    if debuggingEnabled and hasattr(master, "_guiElements"):
815        master._guiElements = getattr(master, "_guiElements", []) + [("qwtHSlider", slider, value, minValue, maxValue, step, callback)]
816    return slider
817
818
819# list box where we can use drag and drop
820class OrangeListBox(QListWidget):
821    def __init__(self, widget, value = None, enableDragDrop = 0, dragDropCallback = None, dataValidityCallback = None, sizeHint = None, *args):
822        self.widget = widget
823        self.value = value
824        QListWidget.__init__(self, *args)
825        self.enableDragDrop = enableDragDrop
826        self.dragDopCallback = dragDropCallback
827        self.dataValidityCallback = dataValidityCallback
828        if not sizeHint:
829            self.defaultSizeHint = QSize(150,100)
830        else:
831            self.defaultSizeHint = sizeHint
832        if enableDragDrop:
833            self.setDragEnabled(1)
834            self.setAcceptDrops(1)
835            self.setDropIndicatorShown(1)
836            #self.setDragDropMode(QAbstractItemView.DragDrop)
837            self.dragStartPosition = 0
838
839    def setAttributes(self, data, attributes):
840        if isinstance(shownAttributes[0], tuple):
841            setattr(self.widget, self.ogLabels, attributes)
842        else:
843            domain = data.domain
844            setattr(self.widget, self.ogLabels, [(domain[a].name, domain[a].varType) for a in attributes])
845
846    def sizeHint(self):
847        return self.defaultSizeHint
848
849
850    def startDrag(self, supportedActions):
851        if not self.enableDragDrop: return
852
853        drag = QDrag(self)
854        mime = QMimeData()
855
856        if not self.ogValue:
857            selectedItems = [i for i in range(self.count()) if self.item(i).isSelected()]
858        else:
859            selectedItems = getdeepattr(self.widget, self.ogValue, default = [])
860
861        mime.setText(str(selectedItems))
862        mime.source = self
863        drag.setMimeData(mime)
864        drag.start(Qt.MoveAction)
865
866    def dragEnterEvent(self, ev):
867        if not self.enableDragDrop: return
868        if self.dataValidityCallback: return self.dataValidityCallback(ev)
869
870        if ev.mimeData().hasText():
871            ev.accept()
872        else:
873            ev.ignore()
874
875
876    def dragMoveEvent(self, ev):
877        if not self.enableDragDrop: return
878        if self.dataValidityCallback: return self.dataValidityCallback(ev)
879
880        if ev.mimeData().hasText():
881            ev.setDropAction(Qt.MoveAction)
882            ev.accept()
883        else:
884            ev.ignore()
885
886    def dropEvent(self, ev):
887        if not self.enableDragDrop: return
888        if ev.mimeData().hasText():
889            item = self.itemAt(ev.pos())
890            if item:
891                index = self.indexFromItem(item).row()
892            else:
893                index = self.count()
894
895            source = ev.mimeData().source
896            selectedItemIndices = eval(str(ev.mimeData().text()))
897
898            if self.ogLabels != None and self.ogValue != None:
899                allSourceItems = getdeepattr(source.widget, source.ogLabels, default = [])
900                selectedItems = [allSourceItems[i] for i in selectedItemIndices]
901                allDestItems = getdeepattr(self.widget, self.ogLabels, default = [])
902
903                if source != self:
904                    setattr(source.widget, source.ogLabels, [item for item in allSourceItems if item not in selectedItems])   # TODO: optimize this code. use the fact that the selectedItemIndices is a sorted list
905                    setattr(self.widget, self.ogLabels, allDestItems[:index] + selectedItems + allDestItems[index:])
906                    setattr(source.widget, source.ogValue, [])  # clear selection in the source widget
907                else:
908                    items = [item for item in allSourceItems if item not in selectedItems]
909                    if index < len(allDestItems):
910                        while index > 0 and index in getdeepattr(self.widget, self.ogValue, default = []):      # if we are dropping items on a selected item, we have to select some previous unselected item as the drop target
911                            index -= 1
912                        destItem = allDestItems[index]
913                        index = items.index(destItem)
914                    else:
915                        index = max(0, index - len(selectedItems))
916                    setattr(self.widget, self.ogLabels, items[:index] + selectedItems + items[index:])
917                setattr(self.widget, self.ogValue, range(index, index+len(selectedItems)))
918            else:       # if we don't have variables ogValue and ogLabel
919                if source != self:
920                    self.insertItems(source.selectedItems())
921                    for index in selectedItemIndices[::-1]:
922                        source.takeItem(index)
923                else:
924                    if index < self.count():
925                        while index > 0 and self.item(index).isSelected():      # if we are dropping items on a selected item, we have to select some previous unselected item as the drop target
926                            index -= 1
927                    items = [source.item(i) for i in selectedItemIndices]
928                    for ind in selectedItemIndices[::-1]:
929                        source.takeItem(ind)
930                        if ind <= index: index-= 1
931                    for item in items[::-1]:
932                        self.insertItem(index, item)
933                    self.clearSelection()
934                    for i in range(index, index+len(items)):
935                        self.item(i).setSelected(1)
936
937            if self.dragDopCallback:        # call the callback
938                self.dragDopCallback()
939            ev.setDropAction(Qt.MoveAction)
940            ev.accept()
941        else:
942            ev.ignore()
943           
944    def updateGeometries(self):
945        """ A workaround for a bug in Qt (see: http://bugreports.qt.nokia.com/browse/QTBUG-14412)
946        """ 
947        if getattr(self, "_updatingGeometriesNow", False):
948#            import sys
949#            print >> sys.stderr, "Suppressing recursive update geometries"
950            return
951        self._updatingGeometriesNow = True
952        try:
953            return QListWidget.updateGeometries(self)
954        finally:
955            self._updatingGeometriesNow = False
956
957
958class SmallWidgetButton(QPushButton):
959    def __init__(self, widget, text = "", pixmap = None, box = None, orientation='vertical', tooltip = None, autoHideWidget = None):
960        #self.parent = parent
961        if pixmap != None:
962            import os
963            iconDir = os.path.join(os.path.dirname(__file__), "icons")
964            if isinstance(pixmap, basestring):
965                if os.path.exists(pixmap):
966                    name = pixmap
967                elif os.path.exists(os.path.join(iconDir, pixmap)):
968                    name = os.path.join(iconDir, pixmap)
969            elif type(pixmap) == QPixmap or type(pixmap) == QIcon:
970                name = pixmap
971            else:
972                name = os.path.join(iconDir, "arrow_down.png")
973            QPushButton.__init__(self, QIcon(name), text, widget)
974        else:
975            QPushButton.__init__(self, text, widget)
976        if widget.layout() is not None:
977            widget.layout().addWidget(self)
978        if tooltip != None:
979            self.setToolTip(tooltip)
980        # create autohide widget and set a layout
981        if autoHideWidget != None:
982            self.autohideWidget = autoHideWidget(None, Qt.Popup)
983        else:           
984            self.autohideWidget = AutoHideWidget(None, Qt.Popup)
985        self.widget = self.autohideWidget
986
987        if isinstance(orientation, QLayout):
988            self.widget.setLayout(orientation)
989        elif orientation == 'horizontal' or not orientation:
990            self.widget.setLayout(QHBoxLayout())
991        else:
992            self.widget.setLayout(QVBoxLayout())
993        #self.widget.layout().setMargin(groupBoxMargin)
994
995        if box:
996            self.widget = widgetBox(self.widget, box, orientation)
997        #self.setStyleSheet("QPushButton:hover { background-color: #F4F2F0; }")
998
999        self.autohideWidget.hide()
1000
1001    def mousePressEvent(self, ev):
1002        QWidget.mousePressEvent(self, ev)
1003        if self.autohideWidget.isVisible():
1004            self.autohideWidget.hide()
1005        else:
1006            #self.widget.move(self.parent.mapToGlobal(QPoint(0, 0)).x(), self.mapToGlobal(QPoint(0, self.height())).y())
1007            self.autohideWidget.move(self.mapToGlobal(QPoint(0, self.height())))
1008            self.autohideWidget.show()
1009
1010
1011class SmallWidgetLabel(QLabel):
1012    def __init__(self, widget, text = "", pixmap = None, box = None, orientation='vertical', tooltip = None):
1013        QLabel.__init__(self, widget)
1014        if text != "":
1015            self.setText("<font color=\"#C10004\">" + text + "</font>")
1016        elif pixmap != None:
1017            import os
1018            iconDir = os.path.join(os.path.dirname(__file__), "icons")
1019            if isinstance(pixmap, basestring):
1020                if os.path.exists(pixmap):
1021                    name = pixmap
1022                elif os.path.exists(os.path.join(iconDir, pixmap)):
1023                    name = os.path.join(iconDir, pixmap)
1024            elif type(pixmap) == QPixmap or type(pixmap) == QIcon:
1025                name = pixmap
1026            else:
1027                name = os.path.join(iconDir, "arrow_down.png")
1028            self.setPixmap(QPixmap(name))
1029        if widget.layout() is not None:
1030            widget.layout().addWidget(self)
1031        if tooltip != None:
1032            self.setToolTip(tooltip)
1033        self.autohideWidget = self.widget = AutoHideWidget(None, Qt.Popup)
1034
1035        if isinstance(orientation, QLayout):
1036            self.widget.setLayout(orientation)
1037        elif orientation == 'horizontal' or not orientation:
1038            self.widget.setLayout(QHBoxLayout())
1039        else:
1040            self.widget.setLayout(QVBoxLayout())
1041
1042        if box:
1043            self.widget = widgetBox(self.widget, box, orientation)
1044
1045        self.autohideWidget.hide()
1046
1047    def mousePressEvent(self, ev):
1048        QLabel.mousePressEvent(self, ev)
1049        if self.autohideWidget.isVisible():
1050            self.autohideWidget.hide()
1051        else:
1052            #self.widget.move(self.parent.mapToGlobal(QPoint(0, 0)).x(), self.mapToGlobal(QPoint(0, self.height())).y())
1053            self.autohideWidget.move(self.mapToGlobal(QPoint(0, self.height())))
1054            self.autohideWidget.show()
1055
1056
1057class AutoHideWidget(QWidget):
1058#    def __init__(self, parent = None):
1059#        QWidget.__init__(self, parent, Qt.Popup)
1060
1061    def leaveEvent(self, ev):
1062        self.hide()
1063
1064
1065
1066class SearchLineEdit(QLineEdit):
1067    def __init__(self, t, searcher):
1068        QLineEdit.__init__(self, t)
1069        self.searcher = searcher
1070
1071    def keyPressEvent(self, e):
1072        k = e.key()
1073        if k == Qt.Key_Down:
1074            curItem = self.searcher.lb.currentItem()
1075            if curItem+1 < self.searcher.lb.count():
1076                self.searcher.lb.setCurrentItem(curItem+1)
1077        elif k == Qt.Key_Up:
1078            curItem = self.searcher.lb.currentItem()
1079            if curItem:
1080                self.searcher.lb.setCurrentItem(curItem-1)
1081        elif k == Qt.Key_Escape:
1082            self.searcher.window.hide()
1083        else:
1084            return QLineEdit.keyPressEvent(self, e)
1085
1086class Searcher:
1087    def __init__(self, control, master):
1088        self.control = control
1089        self.master = master
1090
1091    def __call__(self):
1092        self.window = t = QFrame(self.master, "", QStyle.WStyle_Dialog + QStyle.WStyle_Tool + QStyle.WStyle_Customize + QStyle.WStyle_NormalBorder)
1093        la = QVBoxLayout(t).setAutoAdd(1)
1094        gs = self.master.mapToGlobal(QPoint(0, 0))
1095        gl = self.control.mapToGlobal(QPoint(0, 0))
1096        t.move(gl.x()-gs.x(), gl.y()-gs.y())
1097        self.allItems = [str(self.control.text(i)) for i in range(self.control.count())]
1098        le = SearchLineEdit(t, self)
1099        self.lb = QListBox(t)
1100        for i in self.allItems:
1101            self.lb.insertItem(i)
1102        t.setFixedSize(self.control.width(), 200)
1103        t.show()
1104        le.setFocus()
1105
1106        QObject.connect(le, SIGNAL("textChanged(const QString &)"), self.textChanged)
1107        QObject.connect(le, SIGNAL("returnPressed()"), self.returnPressed)
1108        QObject.connect(self.lb, SIGNAL("clicked(QListBoxItem *)"), self.mouseClicked)
1109
1110    def textChanged(self, s):
1111        s = str(s)
1112        self.lb.clear()
1113        for i in self.allItems:
1114            if s.lower() in i.lower():
1115                self.lb.insertItem(i)
1116
1117    def returnPressed(self):
1118        if self.lb.count():
1119            self.conclude(self.lb.text(max(0, self.lb.currentItem())))
1120        else:
1121            self.window.hide()
1122
1123    def mouseClicked(self, item):
1124        self.conclude(item.text())
1125
1126    def conclude(self, valueQStr):
1127        value = str(valueQStr)
1128        index = self.allItems.index(value)
1129        self.control.setCurrentItem(index)
1130        if self.control.cback:
1131            if self.control.sendSelectedValue:
1132                self.control.cback(value)
1133            else:
1134                self.control.cback(index)
1135        if self.control.cfunc:
1136            self.control.cfunc()
1137
1138        self.window.hide()
1139
1140
1141
1142def comboBox(widget, master, value, box=None, label=None, labelWidth=None, orientation='vertical', items=None, tooltip=None, callback=None, sendSelectedValue = 0, valueType = unicode, control2attributeDict = {}, emptyString = None, editable = 0, searchAttr = False, indent = 0, addToLayout = 1, addSpace = False, debuggingEnabled = 1):
1143    hb = widgetBox(widget, box, orientation)
1144    widgetLabel(hb, label, labelWidth)
1145    if tooltip:
1146        hb.setToolTip(tooltip)
1147    combo = QComboBox(hb)
1148    combo.setEditable(editable)
1149    combo.box = hb
1150
1151    if addSpace:
1152        if isinstance(addSpace, bool):
1153            separator(widget)
1154        elif isinstance(addSpace, int):
1155            separator(widget, height=addSpace)
1156        else:
1157            separator(widget)
1158
1159    if indent:
1160        hb = widgetBox(hb, orientation = "horizontal")
1161        hb.layout().addSpacing(indent)
1162    if hb.layout() is not None and addToLayout:
1163        hb.layout().addWidget(combo)
1164
1165    if items:
1166        combo.addItems([unicode(i) for i in items])
1167        if len(items)>0 and value != None:
1168            if sendSelectedValue and getdeepattr(master, value) in items: combo.setCurrentIndex(items.index(getdeepattr(master, value)))
1169            elif not sendSelectedValue and getdeepattr(master, value) < combo.count():
1170                combo.setCurrentIndex(getdeepattr(master, value))
1171            elif combo.count() > 0:
1172                combo.setCurrentIndex(0)
1173        else:
1174            combo.setDisabled(True)
1175
1176    if value != None:
1177        if sendSelectedValue:
1178            control2attributeDict = dict(control2attributeDict)
1179            if emptyString:
1180                control2attributeDict[emptyString] = ""
1181            connectControl(combo, master, value, callback, "activated( const QString & )",
1182                           CallFrontComboBox(combo, valueType, control2attributeDict),
1183                           ValueCallbackCombo(master, value, valueType, control2attributeDict))
1184        else:
1185            connectControl(combo, master, value, callback, "activated(int)", CallFrontComboBox(combo, None, control2attributeDict))
1186
1187    if debuggingEnabled and hasattr(master, "_guiElements"):
1188        master._guiElements = getattr(master, "_guiElements", []) + [("comboBox", combo, value, sendSelectedValue, valueType, callback)]
1189    return combo
1190
1191
1192def comboBoxWithCaption(widget, master, value, label, box=None, items=None, tooltip=None, callback = None, sendSelectedValue=0, valueType = int, labelWidth = None, debuggingEnabled = 1):
1193    hbox = widgetBox(widget, box = box, orientation="horizontal")
1194    lab = widgetLabel(hbox, label + "  ", labelWidth)
1195    combo = comboBox(hbox, master, value, items = items, tooltip = tooltip, callback = callback, sendSelectedValue = sendSelectedValue, valueType = valueType, debuggingEnabled = debuggingEnabled)
1196    return combo
1197
1198# creates a widget box with a button in the top right edge, that allows you to hide all the widgets in the box and collapse the box to its minimum height
1199class collapsableWidgetBox(QGroupBox):
1200    def __init__(self, widget, box = "", master = None, value = "", orientation = "vertical", callback = None):
1201        QGroupBox.__init__(self, widget)
1202        self.setFlat(1)
1203        if orientation == 'vertical': self.setLayout(QVBoxLayout())
1204        else:                         self.setLayout(QHBoxLayout())
1205
1206        if widget.layout() is not None:
1207            widget.layout().addWidget(self)
1208        if type(box) in (str, unicode): # if you pass 1 for box, there will be a box, but no text
1209            self.setTitle(" " + box.strip() + " ")
1210
1211        self.setCheckable(1)
1212       
1213        self.master = master
1214        self.value = value
1215        self.callback = callback
1216        QObject.connect(self, SIGNAL("clicked()"), self.toggled)
1217
1218
1219    def toggled(self, val = 0):
1220        if self.value:
1221            self.master.__setattr__(self.value, self.isChecked())
1222            self.updateControls()
1223#            self.setFlat(1)
1224        if self.callback != None:
1225            self.callback()
1226
1227    def updateControls(self):
1228        val = self.master.getdeepattr(self.value)
1229        width = self.width()
1230        self.setChecked(val)
1231        self.setFlat(not val)
1232        if not val:
1233            self.setMinimumSize(QSize(width, 0))
1234        else:
1235            self.setMinimumSize(QSize(0, 0))
1236
1237        for c in self.children():
1238            if isinstance(c, QLayout): continue
1239            if val:
1240                c.show()
1241            else:
1242                c.hide()
1243
1244# creates an icon that allows you to show/hide the widgets in the widgets list
1245class widgetHider(QWidget):
1246    def __init__(self, widget, master, value, size = (19,19), widgets = [], tooltip = None):
1247        QWidget.__init__(self, widget)
1248        if widget.layout() is not None:
1249            widget.layout().addWidget(self)
1250        self.value = value
1251        self.master = master
1252
1253        if tooltip:
1254            self.setToolTip(tooltip)
1255
1256        import os
1257        iconDir = os.path.join(os.path.dirname(__file__), "icons")
1258        icon1 = os.path.join(iconDir, "arrow_down.png")
1259        icon2 = os.path.join(iconDir, "arrow_up.png")
1260        self.pixmaps = []
1261
1262        self.pixmaps = [QPixmap(icon1), QPixmap(icon2)]
1263        self.setFixedSize(self.pixmaps[0].size())
1264
1265        self.disables = widgets or [] # need to create a new instance of list (in case someone would want to append...)
1266        self.makeConsistent = Disabler(self, master, value, type = HIDER)
1267        if widgets != []:
1268            self.setWidgets(widgets)
1269
1270    def mousePressEvent(self, ev):
1271        self.master.__setattr__(self.value, not getdeepattr(self.master, self.value))
1272        self.makeConsistent.__call__()
1273
1274
1275    def setWidgets(self, widgets):
1276        self.disables = widgets or []
1277        self.makeConsistent.__call__()
1278
1279    def paintEvent(self, ev):
1280        QWidget.paintEvent(self, ev)
1281
1282        if self.pixmaps != []:
1283            pix = self.pixmaps[getdeepattr(self.master, self.value)]
1284            painter = QPainter(self)
1285            painter.drawPixmap(0, 0, pix)
1286
1287
1288##############################################################################
1289# callback handlers
1290
1291def setStopper(master, sendButton, stopCheckbox, changedFlag, callback):
1292    stopCheckbox.disables.append((-1, sendButton))
1293    sendButton.setDisabled(stopCheckbox.isChecked())
1294    QObject.connect(stopCheckbox, SIGNAL("toggled(bool)"),
1295                   lambda x, master=master, changedFlag=changedFlag, callback=callback: x and getdeepattr(master, changedFlag, default=True) and callback())
1296
1297
1298class ControlledList(list):
1299    def __init__(self, content, listBox = None):
1300        list.__init__(self, content)
1301        self.listBox = listBox
1302
1303    def __reduce__(self):
1304        # cannot pickle self.listBox, but can't discard it (ControlledList may live on)
1305        import copy_reg
1306        return copy_reg._reconstructor, (list, list, ()), None, self.__iter__()
1307
1308    def item2name(self, item):
1309        item = self.listBox.labels[item]
1310        if type(item) == tuple:
1311            return item[1]
1312        else:
1313            return item
1314
1315    def __setitem__(self, index, item):
1316        self.listBox.item(list.__getitem__(self, index)).setSelected(0)
1317        item.setSelected(1)
1318        list.__setitem__(self, index, item)
1319
1320    def __delitem__(self, index):
1321        self.listBox.item(__getitem__(self, index)).setSelected(0)
1322        list.__delitem__(self, index)
1323
1324    def __setslice__(self, start, end, slice):
1325        for i in list.__getslice__(self, start, end):
1326            self.listBox.item(i).setSelected(0)
1327        for i in slice:
1328            self.listBox.item(i).setSelected(1)
1329        list.__setslice__(self, start, end, slice)
1330
1331    def __delslice__(self, start, end):
1332        if not start and end==len(self):
1333            for i in range(self.listBox.count()):
1334                self.listBox.item(i).setSelected(0)
1335        else:
1336            for i in list.__getslice__(self, start, end):
1337                self.listBox.item(i).setSelected(0)
1338        list.__delslice__(self, start, end)
1339
1340    def append(self, item):
1341        list.append(self, item)
1342        item.setSelected(1)
1343
1344    def extend(self, slice):
1345        list.extend(self, slice)
1346        for i in slice:
1347            self.listBox.item(i).setSelected(1)
1348
1349    def insert(self, index, item):
1350        item.setSelected(1)
1351        list.insert(self, index, item)
1352
1353    def pop(self, index=-1):
1354        self.listBox.item(list.__getitem__(self, index)).setSelected(0)
1355        list.pop(self, index)
1356
1357    def remove(self, item):
1358        item.setSelected(0)
1359        list.remove(self, item)
1360
1361
1362def connectControlSignal(control, signal, f):
1363    if type(signal) == tuple:
1364        control, signal = signal
1365    QObject.connect(control, SIGNAL(signal), f)
1366
1367
1368def connectControl(control, master, value, f, signal, cfront, cback = None, cfunc = None, fvcb = None):
1369    cback = cback or value and ValueCallback(master, value, fvcb)
1370    if cback:
1371        if signal:
1372            connectControlSignal(control, signal, cback)
1373        cback.opposite = cfront
1374        if value and cfront and hasattr(master, "controlledAttributes"):
1375            master.controlledAttributes[value] = cfront
1376
1377    cfunc = cfunc or f and FunctionCallback(master, f)
1378    if cfunc:
1379        if signal:
1380            connectControlSignal(control, signal, cfunc)
1381        cfront.opposite = cback, cfunc
1382    else:
1383        cfront.opposite = (cback,)
1384
1385    return cfront, cback, cfunc
1386
1387
1388class ControlledCallback:
1389    def __init__(self, widget, attribute, f = None):
1390        self.widget = widget
1391        self.attribute = attribute
1392        self.f = f
1393        self.disabled = 0
1394        if type(widget) == dict: return     # we can't assign attributes to dict
1395        if not hasattr(widget, "callbackDeposit"):
1396            widget.callbackDeposit = []
1397        widget.callbackDeposit.append(self)
1398       
1399
1400    def acyclic_setattr(self, value):
1401        if self.disabled:
1402            return
1403
1404        if isinstance(value, QString):
1405            value = unicode(value)
1406        if self.f:
1407            if self.f in [int, float] and (not value or type(value) in [str, unicode] and value in "+-"):
1408                value = self.f(0)
1409            else:
1410                value = self.f(value)
1411
1412        opposite = getattr(self, "opposite", None)
1413        if opposite:
1414            try:
1415                opposite.disabled += 1
1416                if type(self.widget) == dict: self.widget[self.attribute] = value
1417                else:                         setattr(self.widget, self.attribute, value)
1418            finally:
1419                opposite.disabled -= 1
1420        else:
1421            if type(self.widget) == dict: self.widget[self.attribute] = value
1422            else:                         setattr(self.widget, self.attribute, value)
1423
1424
1425class ValueCallback(ControlledCallback):
1426    def __call__(self, value):
1427        if value is not None:
1428            try:
1429                self.acyclic_setattr(value)
1430            except:
1431                print "OWGUI.ValueCallback: %s" % value
1432                import traceback, sys
1433                traceback.print_exception(*sys.exc_info())
1434
1435
1436class ValueCallbackCombo(ValueCallback):
1437    def __init__(self, widget, attribute, f = None, control2attributeDict = {}):
1438        ValueCallback.__init__(self, widget, attribute, f)
1439        self.control2attributeDict = control2attributeDict
1440
1441    def __call__(self, value):
1442        value = unicode(value)
1443        return ValueCallback.__call__(self, self.control2attributeDict.get(value, value))
1444
1445
1446
1447class ValueCallbackLineEdit(ControlledCallback):
1448    def __init__(self, control, widget, attribute, f = None):
1449        ControlledCallback.__init__(self, widget, attribute, f)
1450        self.control = control
1451
1452    def __call__(self, value):
1453        if value is not None:
1454            try:
1455                pos = self.control.cursorPosition()
1456                self.acyclic_setattr(value)
1457                self.control.setCursorPosition(pos)
1458            except:
1459                print "invalid value ", value, type(value)
1460
1461
1462class SetLabelCallback:
1463    def __init__(self, widget, label, format = "%5.2f", f = None):
1464        self.widget = widget
1465        self.label = label
1466        self.format = format
1467        self.f = f
1468        if hasattr(widget, "callbackDeposit"):
1469            widget.callbackDeposit.append(self)
1470        self.disabled = 0
1471
1472    def __call__(self, value):
1473        if not self.disabled and value is not None:
1474            if self.f:
1475                value = self.f(value)
1476            self.label.setText(self.format % value)
1477
1478
1479class FunctionCallback:
1480    def __init__(self, master, f, widget=None, id=None, getwidget=None):
1481        self.master = master
1482        self.widget = widget
1483        self.f = f
1484        self.id = id
1485        self.getwidget = getwidget
1486        if hasattr(master, "callbackDeposit"):
1487            master.callbackDeposit.append(self)
1488        self.disabled = 0
1489
1490    def __call__(self, *value):
1491        if not self.disabled and value!=None:
1492            kwds = {}
1493            if self.id <> None:
1494                kwds['id'] = self.id
1495            if self.getwidget:
1496                kwds['widget'] = self.widget
1497            if isinstance(self.f, list):
1498                for f in self.f:
1499                    f(**kwds)
1500            else:
1501                self.f(**kwds)
1502
1503
1504class CallBackListBox:
1505    def __init__(self, control, widget):
1506        self.control = control
1507        self.widget = widget
1508        self.disabled = 0
1509
1510    def __call__(self, *args): # triggered by selectionChange()
1511        if not self.disabled and self.control.ogValue != None:
1512            clist = getdeepattr(self.widget, self.control.ogValue)
1513            list.__delslice__(clist, 0, len(clist))
1514            control = self.control
1515            for i in range(control.count()):
1516                if control.item(i).isSelected():
1517                    list.append(clist, i)
1518            self.widget.__setattr__(self.control.ogValue, clist)
1519
1520
1521class CallBackRadioButton:
1522    def __init__(self, control, widget):
1523        self.control = control
1524        self.widget = widget
1525        self.disabled = False
1526
1527    def __call__(self, *args): # triggered by toggled()
1528        if not self.disabled and self.control.ogValue != None:
1529            arr = [butt.isChecked() for butt in self.control.buttons]
1530            self.widget.__setattr__(self.control.ogValue, arr.index(1))
1531
1532
1533##############################################################################
1534# call fronts (through this a change of the attribute value changes the related control)
1535
1536
1537class ControlledCallFront:
1538    def __init__(self, control):
1539        self.control = control
1540        self.disabled = 0
1541
1542    def __call__(self, *args):
1543        if not self.disabled:
1544            opposite = getattr(self, "opposite", None)
1545            if opposite:
1546                try:
1547                    for op in opposite:
1548                        op.disabled += 1
1549                    self.action(*args)
1550                finally:
1551                    for op in opposite:
1552                        op.disabled -= 1
1553            else:
1554                self.action(*args)
1555
1556
1557class CallFrontSpin(ControlledCallFront):
1558    def action(self, value):
1559        if value is not None:
1560            self.control.setValue(value)
1561
1562
1563class CallFrontDoubleSpin(ControlledCallFront):
1564    def action(self, value):
1565        if value is not None:
1566            self.control.setValue(value)
1567
1568
1569class CallFrontCheckBox(ControlledCallFront):
1570    def action(self, value):
1571        if value != None:
1572            values = [Qt.Unchecked, Qt.Checked, Qt.PartiallyChecked]
1573            self.control.setCheckState(values[value])
1574
1575class CallFrontButton(ControlledCallFront):
1576    def action(self, value):
1577        if value != None:
1578            self.control.setChecked(bool(value))
1579
1580class CallFrontComboBox(ControlledCallFront):
1581    def __init__(self, control, valType = None, control2attributeDict = {}):
1582        ControlledCallFront.__init__(self, control)
1583        self.valType = valType
1584        self.attribute2controlDict = dict([(y, x) for x, y in control2attributeDict.items()])
1585
1586    def action(self, value):
1587        if value is not None:
1588            value = self.attribute2controlDict.get(value, value)
1589            if self.valType:
1590                for i in range(self.control.count()):
1591                    if self.valType(str(self.control.itemText(i))) == value:
1592                        self.control.setCurrentIndex(i)
1593                        return
1594                values = ""
1595                for i in range(self.control.count()):
1596                    values += str(self.control.itemText(i)) + (i < self.control.count()-1 and ", " or ".")
1597                print "unable to set %s to value '%s'. Possible values are %s" % (self.control, value, values)
1598                #import traceback
1599                #traceback.print_stack()
1600            else:
1601                if value < self.control.count():
1602                    self.control.setCurrentIndex(value)
1603
1604
1605class CallFrontHSlider(ControlledCallFront):
1606    def action(self, value):
1607        if value is not None:
1608            self.control.setValue(value)
1609
1610
1611class CallFrontLogSlider(ControlledCallFront):
1612    def action(self, value):
1613        if value is not None:
1614            if value < 1e-30:
1615                print "unable to set ", self.control, "to value ", value, " (value too small)"
1616            else:
1617                self.control.setValue(math.log10(value))
1618
1619
1620class CallFrontLineEdit(ControlledCallFront):
1621    def action(self, value):
1622        self.control.setText(unicode(value))
1623
1624
1625class CallFrontRadioButtons(ControlledCallFront):
1626    def action(self, value):
1627        if value < 0 or value >= len(self.control.buttons):
1628            value = 0
1629        self.control.buttons[value].setChecked(1)
1630
1631
1632class CallFrontListBox(ControlledCallFront):
1633    def action(self, value):
1634        if value is not None:
1635            if not isinstance(value, ControlledList):
1636                setattr(self.control.ogMaster, self.control.ogValue, ControlledList(value, self.control))
1637            for i in range(self.control.count()):
1638                shouldBe = i in value
1639                if shouldBe != self.control.item(i).isSelected():
1640                    self.control.item(i).setSelected(shouldBe)
1641
1642
1643class CallFrontListBoxLabels(ControlledCallFront):
1644    def action(self, value):
1645        icons = getAttributeIcons()
1646        self.control.clear()
1647        if value:
1648            for i in value:
1649                if type(i) == tuple:
1650                    if isinstance(i[1], int):
1651                        self.control.addItem(QListWidgetItem(icons.get(i[1], icons[-1]), i[0]))
1652                    else:
1653                        self.control.addItem( QListWidgetItem(i[0],i[1]) )
1654                else:
1655                    self.control.addItem(i)
1656
1657
1658class CallFrontLabel:
1659    def __init__(self, control, label, master):
1660        self.control = control
1661        self.label = label
1662        self.master = master
1663
1664    def __call__(self, *args):
1665        self.control.setText(self.label % self.master.__dict__)
1666
1667##############################################################################
1668## Disabler is a call-back class for check box that can disable/enable other
1669## widgets according to state (checked/unchecked, enabled/disable) of the
1670## given check box
1671##
1672## Tricky: if self.propagateState is True (default), then if check box is
1673## disabled, the related widgets will be disabled (even if the checkbox is
1674## checked). If self.propagateState is False, the related widgets will be
1675## disabled/enabled if check box is checked/clear, disregarding whether the
1676## check box itself is enabled or not. (If you don't understand, see the code :-)
1677DISABLER = 1
1678HIDER = 2
1679
1680class Disabler:
1681    def __init__(self, widget, master, valueName, propagateState = 1, type = DISABLER):
1682        self.widget = widget
1683        self.master = master
1684        self.valueName = valueName
1685        self.propagateState = propagateState
1686        self.type = type
1687
1688    def __call__(self, *value):
1689        currState = self.widget.isEnabled()
1690
1691        if currState or not self.propagateState:
1692            if len(value):
1693                disabled = not value[0]
1694            else:
1695                disabled = not getdeepattr(self.master, self.valueName)
1696        else:
1697            disabled = 1
1698
1699        for w in self.widget.disables:
1700            if type(w) == tuple:
1701                if isinstance(w[0], int):
1702                    i = 1
1703                    if w[0] == -1:
1704                        disabled = not disabled
1705                else:
1706                    i = 0
1707                if self.type == DISABLER:
1708                    w[i].setDisabled(disabled)
1709                elif self.type == HIDER:
1710                    if disabled: w[i].hide()
1711                    else:        w[i].show()
1712
1713                if hasattr(w[i], "makeConsistent"):
1714                    w[i].makeConsistent()
1715            else:
1716                if self.type == DISABLER:
1717                    w.setDisabled(disabled)
1718                elif self.type == HIDER:
1719                    if disabled: w.hide()
1720                    else:        w.show()
1721
1722##############################################################################
1723# some table related widgets
1724
1725class tableItem(QTableWidgetItem):
1726    def __init__(self, table, x, y, text, editType = None, backColor=None, icon=None, type = QTableWidgetItem.Type):
1727        QTableWidgetItem.__init__(self, type)
1728        if icon:
1729            self.setIcon(QIcon(icon))
1730        if editType != None:
1731            self.setFlags(editType)
1732        else:
1733            self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsSelectable)
1734        if backColor != None:
1735            self.setBackground(QBrush(backColor))
1736        self.setData(Qt.DisplayRole, QVariant(text))        # we add it this way so that text can also be int and sorting will be done properly (as integers and not as text)
1737
1738        table.setItem(x, y, self)
1739
1740
1741import orange
1742
1743TableValueRole = OrangeUserRole.next() # Role to retrieve orange.Value
1744TableClassValueRole = OrangeUserRole.next() # Role to retrieve the class value for the row's example
1745TableDistribution = OrangeUserRole.next() # Role to retrieve the distribution of the column's attribute
1746TableVariable = OrangeUserRole.next() # Role to retrieve the column's variable
1747
1748BarRatioRole = OrangeUserRole.next() # Ratio for drawing distribution bars
1749BarBrushRole = OrangeUserRole.next() # Brush for distribution bar
1750
1751SortOrderRole = OrangeUserRole.next() # Used for sorting
1752
1753
1754class TableBarItem(QItemDelegate):
1755    BarRole = OrangeUserRole.next()
1756    ColorRole = OrangeUserRole.next()
1757    def __init__(self, widget, table = None, color = QColor(255, 170, 127), color_schema=None):
1758        """
1759        :param widget: OWWidget instance
1760        :type widget: :class:`OWWidget.OWWidget
1761        :param table: Table
1762        :type table: :class:`Orange.data.Table`
1763        :param color: Color of the distribution bar.
1764        :type color: :class:`PyQt4.QtCore.QColor`
1765        :param color_schema: If not None it must be an instance of
1766            :class:`OWColorPalette.ColorPaletteGenerator` (note: this
1767            parameter, if set, overrides the ``color``)
1768        :type color_schema: :class:`OWColorPalette.ColorPaletteGenerator`
1769         
1770        """
1771        QItemDelegate.__init__(self, widget)
1772        self.color = color
1773        self.color_schema = color_schema
1774        self.widget = widget
1775        self.table = table
1776
1777    def paint(self, painter, option, index):
1778        painter.save()
1779        self.drawBackground(painter, option, index)
1780        if self.table is None:
1781            table = getattr(index.model(), "examples", None)
1782        else:
1783            table = self.table
1784        ratio = None
1785        ratio, ok = index.data(TableBarItem.BarRole).toDouble()
1786        if ratio != ratio: # NaN
1787            ratio = None
1788        if not ok:
1789            ratio = None
1790            value, ok = index.data(Qt.DisplayRole).toDouble()
1791            if ok and getattr(self.widget, "showBars", False) and table is not None:
1792                col = index.column()
1793                if col < len(table.normalizers):
1794                    max, span = table.normalizers[col]
1795                    ratio = (max - value) / span
1796
1797        color = self.color
1798        if self.color_schema is not None and table is not None \
1799            and table.domain.classVar \
1800            and isinstance(table.domain.classVar, orange.EnumVariable):
1801            class_ = index.data(TableClassValueRole).toPyObject()
1802            if not class_.isSpecial():
1803                color = self.color_schema[int(class_)]
1804        else:
1805            color = self.color
1806
1807        if ratio is not None:
1808#            painter.save()
1809#            pen = QPen(QBrush(color), 3, Qt.SolidLine, Qt.FlatCap, Qt.BevelJoin)
1810#            painter.setPen(pen)
1811#            painter.drawRect(option.rect.adjusted(0, 3, -option.rect.width() * ratio, -3))
1812#            painter.restore()
1813
1814#            painter.fillRect(option.rect.adjusted(0, 3, -option.rect.width() * ratio, -3), color)
1815
1816            painter.save()
1817            painter.setPen(QPen(QBrush(color), 5, Qt.SolidLine, Qt.RoundCap))
1818            rect = option.rect.adjusted(3, 0, -3, -5)
1819            x, y = rect.x(), rect.y() + rect.height()
1820            painter.drawLine(x, y, x + rect.width() * ratio, y)
1821            painter.restore()
1822            # raise the lower edge 3 pixels up
1823            text_rect = option.rect.adjusted(0, 0, 0, -3)
1824        else:
1825            text_rect = option.rect
1826        text = index.data(Qt.DisplayRole).toString()
1827
1828        self.drawDisplay(painter, option, text_rect, text)
1829        painter.restore()
1830       
1831class BarItemDelegate(QStyledItemDelegate):
1832    def __init__(self, parent, brush=QBrush(QColor(255, 170, 127)), scale=(0.0, 1.0)):
1833        QStyledItemDelegate.__init__(self, parent) 
1834        self.brush = brush
1835        self.scale = scale
1836       
1837    def paint(self, painter, option, index):
1838        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
1839        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
1840        rect = option.rect
1841        val, ok = index.data(Qt.DisplayRole).toDouble()
1842        if ok:
1843            min, max = self.scale
1844            val = (val - min) / (max - min)
1845            painter.save()
1846            if option.state & QStyle.State_Selected:
1847                painter.setOpacity(0.75)
1848            painter.setBrush(self.brush)
1849            painter.drawRect(rect.adjusted(1, 1, - rect.width() * (1.0 - val) -2, -2))
1850            painter.restore()
1851       
1852class IndicatorItemDelegate(QStyledItemDelegate):
1853    IndicatorRole = OrangeUserRole.next()
1854    def __init__(self, parent, role=IndicatorRole, indicatorSize=2):
1855        QStyledItemDelegate.__init__(self, parent)
1856        self.role = role
1857        self.indicatorSize = indicatorSize
1858       
1859    def paint(self, painter, option, index):
1860        QStyledItemDelegate.paint(self, painter, option, index)
1861        rect = option.rect
1862        indicator, valid = index.data(self.role).toString(), True
1863        indicator = False if indicator == "false" else indicator
1864        if valid and indicator:
1865            painter.save()
1866            painter.setRenderHints(QPainter.Antialiasing)
1867            painter.setBrush(QBrush(Qt.black))
1868            painter.drawEllipse(rect.center(), self.indicatorSize, self.indicatorSize) #rect.adjusted(rect.width() / 2 - 5, rect.height() - 5, -rect.width() /2 + 5, -rect.height()/2 + 5))
1869            painter.restore()
1870
1871class LinkStyledItemDelegate(QStyledItemDelegate):
1872    LinkRole = OrangeUserRole.next()
1873    def __init__(self, parent):
1874        QStyledItemDelegate.__init__(self, parent)
1875        self.mousePressState = QModelIndex(), QPoint()
1876        self.connect(parent, SIGNAL("entered(QModelIndex)"), self.onEntered)
1877           
1878    def sizeHint(self, option, index):
1879        size = QStyledItemDelegate.sizeHint(self, option, index)
1880        return QSize(size.width(), max(size.height(), 20))
1881   
1882    def linkRect(self, option, index):
1883        style = self.parent().style()
1884        text = self.displayText(index.data(Qt.DisplayRole), QLocale.system())
1885        self.initStyleOption(option, index)
1886        textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option)
1887        if not textRect.isValid():
1888            textRect = option.rect
1889        margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1
1890        textRect = textRect.adjusted(margin, 0, -margin, 0)
1891        font = index.data(Qt.FontRole)
1892        if font.isValid():
1893            font = QFont(font)
1894        else:
1895            font = option.font
1896        metrics = QFontMetrics(font)
1897        elideText = metrics.elidedText(text, option.textElideMode, textRect.width())
1898        try:
1899            str(elideText)  ## on Windows with PyQt 4.4 sometimes this fails
1900        except Exception, ex:
1901            elideText = text
1902        return metrics.boundingRect(textRect, option.displayAlignment, elideText)
1903     
1904    def editorEvent(self, event, model, option, index):
1905        if event.type()==QEvent.MouseButtonPress and self.linkRect(option, index).contains(event.pos()):
1906            self.mousePressState = QPersistentModelIndex(index), QPoint(event.pos())
1907           
1908        elif event.type()== QEvent.MouseButtonRelease:
1909            link = index.data(LinkRole)
1910            pressedIndex, pressPos = self.mousePressState
1911            if pressedIndex == index and (pressPos - event.pos()).manhattanLength() < 5 and link.isValid():
1912                 import webbrowser
1913                 webbrowser.open(link.toString())
1914            self.mousePressState = QModelIndex(), event.pos()
1915           
1916        elif event.type()==QEvent.MouseMove:
1917            link = index.data(LinkRole)
1918            if link.isValid() and self.linkRect(option, index).contains(event.pos()):
1919                self.parent().viewport().setCursor(Qt.PointingHandCursor)
1920            else:
1921                self.parent().viewport().setCursor(Qt.ArrowCursor)
1922           
1923        return QStyledItemDelegate.editorEvent(self, event, model, option, index)
1924   
1925    def onEntered(self, index):
1926        link = index.data(LinkRole)
1927        if not link.isValid():
1928            self.parent().viewport().setCursor(Qt.ArrowCursor)
1929   
1930    def paint(self, painter, option, index):
1931        if index.data(LinkRole).isValid():
1932            style = qApp.style()
1933            style.drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
1934            style.drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
1935            text = self.displayText(index.data(Qt.DisplayRole), QLocale.system())
1936            textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option)
1937            if not textRect.isValid():
1938                textRect = option.rect
1939            margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1
1940            textRect = textRect.adjusted(margin, 0, -margin, 0)
1941            elideText = QFontMetrics(option.font).elidedText(text, option.textElideMode, textRect.width())
1942            painter.save()
1943            font = index.data(Qt.FontRole)
1944            if font.isValid():
1945                painter.setFont(QFont(font))
1946            else:
1947                painter.setFont(option.font)
1948            painter.setPen(QPen(Qt.blue))
1949            painter.drawText(textRect, option.displayAlignment, elideText)
1950            painter.restore()
1951        else:
1952            QStyledItemDelegate.paint(self, painter, option, index)
1953   
1954LinkRole = LinkStyledItemDelegate.LinkRole
1955
1956def _toPyObject(variant):
1957    val = variant.toPyObject()
1958    if isinstance(val, type(NotImplemented)):
1959        # PyQt 4.4 converts python int, floats ... to C types and
1960        # cannot convert them back again and returns an exception instance.
1961        qtype = variant.type()
1962        if qtype == QVariant.Double:
1963            val, ok = variant.toDouble()
1964        elif qtype == QVariant.Int:
1965            val, ok = variant.toInt()
1966        elif qtype == QVariant.LongLong:
1967            val, ok = variant.toLongLong()
1968        elif qtype == QVariant.String:
1969            val = variant.toString()
1970    return val
1971
1972class ColoredBarItemDelegate(QStyledItemDelegate):
1973    """ Item delegate that can also draws a distribution bar
1974    """
1975    def __init__(self, parent=None, decimals=3, color=Qt.red):
1976        QStyledItemDelegate.__init__(self, parent)
1977        self.decimals = decimals
1978        self.float_fmt = "%%.%if" % decimals
1979        self.color = QColor(color)
1980       
1981    def displayText(self, value, locale):
1982        obj = _toPyObject(value)
1983        if isinstance(obj, float):
1984            return self.float_fmt % obj
1985        elif isinstance(obj, basestring):
1986            return obj
1987        elif obj is None:
1988            return "NA"
1989        else:
1990            return obj.__str__()
1991       
1992    def sizeHint(self, option, index):
1993        font = self.get_font(option, index)
1994        metrics = QFontMetrics(font)
1995        height = metrics.lineSpacing() + 8 # 4 pixel margin
1996        width = metrics.width(self.displayText(index.data(Qt.DisplayRole), QLocale())) + 8
1997        return QSize(width, height)
1998   
1999    def paint(self, painter, option, index):
2000        self.initStyleOption(option, index)
2001        text = self.displayText(index.data(Qt.DisplayRole), QLocale())
2002       
2003        ratio, have_ratio = self.get_bar_ratio(option, index)
2004       
2005        rect = option.rect
2006        if have_ratio:
2007            # The text is raised 3 pixels above the bar.
2008            text_rect = rect.adjusted(4, 1, -4, -4) # TODO: Style dependent margins?
2009        else:
2010            text_rect = rect.adjusted(4, 4, -4, -4)
2011           
2012        painter.save()
2013        font = self.get_font(option, index)
2014        painter.setFont(font)
2015       
2016        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
2017        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
2018       
2019        # TODO: Check ForegroundRole.
2020        if option.state & QStyle.State_Selected:
2021            color = option.palette.highlightedText().color()
2022        else:
2023            color = option.palette.text().color()
2024        painter.setPen(QPen(color))
2025       
2026        align = self.get_text_align(option, index)
2027           
2028        metrics = QFontMetrics(font)
2029        elide_text = metrics.elidedText(text, option.textElideMode, text_rect.width())
2030        painter.drawText(text_rect, align, elide_text)
2031       
2032        painter.setRenderHint(QPainter.Antialiasing, True)
2033        if have_ratio:
2034            brush = self.get_bar_brush(option, index)
2035           
2036            painter.setBrush(brush)
2037            painter.setPen(QPen(brush, 1))
2038            bar_rect = QRect(text_rect)
2039            bar_rect.setTop(bar_rect.bottom() - 1)
2040            bar_rect.setBottom(bar_rect.bottom() + 1)
2041            w = text_rect.width()
2042            bar_rect.setWidth(max(0, min(w * ratio, w)))
2043            painter.drawRoundedRect(bar_rect, 2, 2)
2044        painter.restore()
2045   
2046    def get_font(self, option, index):
2047        font = index.data(Qt.FontRole)
2048        if font.isValid():
2049            font = font.toPyObject()
2050        else:
2051            font = option.font
2052        return font
2053   
2054    def get_text_align(self, option, index):
2055        align = index.data(Qt.TextAlignmentRole)
2056        if align.isValid():
2057            align = align.toInt()
2058        else:
2059            align = Qt.AlignLeft | Qt.AlignVCenter
2060        return align
2061   
2062    def get_bar_ratio(self, option, index):
2063        bar_ratio = index.data(BarRatioRole)
2064        ratio, have_ratio = bar_ratio.toDouble()
2065        return ratio, have_ratio
2066   
2067    def get_bar_brush(self, option, index):
2068        bar_brush = index.data(BarBrushRole)
2069        if bar_brush.isValid():
2070            bar_brush = bar_brush.toPyObject()
2071            if not isinstance(bar_brush, (QColor, QBrush)):
2072                bar_brush = None
2073        else:
2074            bar_brush = None
2075        if bar_brush is None:
2076            bar_brush = self.color
2077        return QBrush(bar_brush)
2078           
2079##############################################################################
2080# progress bar management
2081
2082class ProgressBar:
2083    def __init__(self, widget, iterations):
2084        self.iter = iterations
2085        self.widget = widget
2086        self.count = 0
2087        self.widget.progressBarInit()
2088
2089    def advance(self, count=1):
2090        self.count += count
2091        self.widget.progressBarSet(int(self.count*100/self.iter))
2092
2093    def finish(self):
2094        self.widget.progressBarFinished()
2095       
2096from Orange.utils import progress_bar_milestones as progressBarMilestones
2097
2098##############################################################################
2099
2100def tabWidget(widget):
2101    w = QTabWidget(widget)
2102    if widget.layout() is not None:
2103        widget.layout().addWidget(w)
2104    return w
2105
2106def createTabPage(tabWidget, name, widgetToAdd = None, canScroll = False):
2107    if widgetToAdd == None:
2108        widgetToAdd = widgetBox(tabWidget, addToLayout = 0, margin = 4)
2109    if canScroll:
2110        scrollArea = QScrollArea() 
2111        tabWidget.addTab(scrollArea, name)
2112        scrollArea.setWidget(widgetToAdd)
2113        scrollArea.setWidgetResizable(1) 
2114        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 
2115        scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
2116    else:
2117        tabWidget.addTab(widgetToAdd, name)
2118    return widgetToAdd
2119
2120def table(widget, rows = 0, columns = 0, selectionMode = -1, addToLayout = 1):
2121    w = QTableWidget(rows, columns, widget)
2122    if widget and addToLayout and widget.layout() is not None:
2123        widget.layout().addWidget(w)
2124    if selectionMode != -1:
2125        w.setSelectionMode(selectionMode)   
2126    w.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
2127    w.horizontalHeader().setMovable(True)
2128    return w
2129
2130class VisibleHeaderSectionContextEventFilter(QObject):
2131    def __init__(self, parent, itemView=None):
2132        QObject.__init__(self, parent)
2133        self.itemView = itemView
2134    def eventFilter(self, view, event):
2135        if type(event) == QContextMenuEvent:
2136            model = view.model()
2137            headers = [(view.isSectionHidden(i), model.headerData(i, view.orientation(), Qt.DisplayRole)) for i in range(view.count())]
2138            menu = QMenu("Visible headers", view)
2139           
2140            for i, (checked, name) in enumerate(headers):
2141                action = QAction(name.toString(), menu)
2142                action.setCheckable(True)
2143                action.setChecked(not checked)
2144                menu.addAction(action)
2145               
2146                def toogleHidden(bool, section=i):
2147                    view.setSectionHidden(section, not bool)
2148                    if bool:
2149                        if self.itemView:
2150                            self.itemView.resizeColumnToContents(section)
2151                        else:
2152                            view.resizeSection(section, max(view.sectionSizeHint(section), 10))
2153                       
2154                self.connect(action, SIGNAL("toggled(bool)"), toogleHidden)
2155#                self.connect(action, SIGNAL("toggled(bool)"), lambda bool, section=i: view.setSectionHidden(section, not bool))
2156            menu.exec_(event.globalPos())
2157            return True
2158       
2159        return False
2160   
2161def checkButtonOffsetHint(button, style=None):
2162    option = QStyleOptionButton()
2163    option.initFrom(button)
2164    if style is None:
2165        style = button.style()
2166    if isinstance(button, QCheckBox):
2167        pm_spacing = QStyle.PM_CheckBoxLabelSpacing
2168        pm_indicator_width = QStyle.PM_IndicatorWidth
2169    else:
2170        pm_spacing = QStyle.PM_RadioButtonLabelSpacing
2171        pm_indicator_width = QStyle.PM_ExclusiveIndicatorWidth
2172    space = style.pixelMetric(pm_spacing, option, button)
2173    width = style.pixelMetric(pm_indicator_width, option, button)
2174    style_correction = {"macintosh (aqua)": -2, "macintosh(aqua)": -2, "plastique": 1, "cde": 1, "motif": 1} #TODO: add other styles (Maybe load corrections from .cfg file?)
2175    return space + width + style_correction.get(str(qApp.style().objectName()).lower(), 0)
2176   
2177   
2178def toolButtonSizeHint(button=None, style=None):
2179    if button is None and style is None:
2180        style = qApp.style()
2181    elif style is None:
2182        style = button.style()
2183   
2184    button_size = style.pixelMetric(QStyle.PM_SmallIconSize) + \
2185                  style.pixelMetric(QStyle.PM_ButtonMargin)
2186    return button_size
2187   
2188class FloatSlider(QSlider):
2189    def __init__(self, orientation, min_value, max_value, step, parent=None):
2190        QSlider.__init__(self, orientation, parent)
2191        self.setScale(min_value, max_value, step)
2192        QObject.connect(self, SIGNAL("valueChanged(int)"), self.sendValue)
2193       
2194    def update(self):
2195        self.setSingleStep(1)
2196        if self.min_value != self.max_value:
2197            self.setEnabled(True)
2198            self.setMinimum(int(self.min_value/self.step))
2199            self.setMaximum(int(self.max_value/self.step))
2200        else:
2201            self.setEnabled(False)
2202   
2203    def sendValue(self, slider_value):
2204        value = min(max(slider_value * self.step, self.min_value), self.max_value)
2205        self.emit(SIGNAL("valueChangedFloat(double)"), value)
2206       
2207    def setValue(self, value):
2208        QSlider.setValue(self, int(value/self.step))
2209       
2210    def setScale(self, minValue, maxValue, step=0):
2211        if minValue >= maxValue:
2212            ## It would be more logical to disable the slider in this case (self.setEnabled(False))
2213            ## However, we do nothing to keep consistency with Qwt
2214            return
2215        if step <= 0 or step > (maxValue-minValue):
2216            if type(maxValue) == int and type(minValue) == int:
2217                step = 1
2218            else:
2219                step = float(minValue-maxValue)/100.0
2220        self.min_value = float(minValue)
2221        self.max_value = float(maxValue)
2222        self.step = step
2223        self.update()
2224       
2225    def setRange(self, minValue, maxValue, step=1.0):
2226        # For compatibility with qwtSlider
2227        self.setScale(minValue, maxValue, step)
Note: See TracBrowser for help on using the repository browser.