source: orange/Orange/OrangeWidgets/OWGUI.py @ 11572:27c26e9d7be5

Revision 11572:27c26e9d7be5, 82.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 11 months ago (diff)

Cleanup of TableBarItem class.

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
1758    def __init__(self, parent, table=None, color=QColor(255, 170, 127),
1759                 color_schema=None):
1760        """
1761        :param parent: OWWidget instance
1762        :type parent: :class:`PyQt4.QtCore.QObject`
1763        :param table: Table (unused, here for backwards compatibility).
1764        :type table: :class:`Orange.data.Table`
1765        :param color: Color of the distribution bar.
1766        :type color: :class:`PyQt4.QtCore.QColor`
1767        :param color_schema: If not None it must be an instance of
1768            :class:`OWColorPalette.ColorPaletteGenerator` (note: this
1769            parameter, if set, overrides the ``color``).
1770        :type color_schema: :class:`OWColorPalette.ColorPaletteGenerator`
1771
1772        """
1773        QItemDelegate.__init__(self, parent)
1774        self.color = color
1775        self.color_schema = color_schema
1776        self.table = table
1777
1778    def paint(self, painter, option, index):
1779        painter.save()
1780        self.drawBackground(painter, option, index)
1781
1782        ratio, ok = index.data(TableBarItem.BarRole).toDouble()
1783        if not ok or ratio > 1.0 or ratio < 0.0 or ratio != ratio:
1784            # not a float, out of 0..1 range or a NaN.
1785            ratio = None
1786
1787        color = self.color
1788        if self.color_schema is not None:
1789            class_ = index.data(TableClassValueRole).toPyObject()
1790            if isinstance(class_, orange.Value) and \
1791                    isinstance(class_.variable, orange.EnumVariable) and \
1792                    not class_.isSpecial():
1793                color = self.color_schema[int(class_)]
1794
1795        if ratio is not None:
1796            painter.save()
1797            painter.setPen(QPen(QBrush(color), 5, Qt.SolidLine, Qt.RoundCap))
1798            rect = option.rect.adjusted(3, 0, -3, -5)
1799            x, y = rect.x(), rect.y() + rect.height()
1800            painter.drawLine(x, y, x + rect.width() * ratio, y)
1801            painter.restore()
1802            # raise the lower edge 3 pixels up
1803            text_rect = option.rect.adjusted(0, 0, 0, -3)
1804        else:
1805            text_rect = option.rect
1806        text = index.data(Qt.DisplayRole).toString()
1807
1808        self.drawDisplay(painter, option, text_rect, text)
1809        painter.restore()
1810
1811
1812class BarItemDelegate(QStyledItemDelegate):
1813    def __init__(self, parent, brush=QBrush(QColor(255, 170, 127)), scale=(0.0, 1.0)):
1814        QStyledItemDelegate.__init__(self, parent) 
1815        self.brush = brush
1816        self.scale = scale
1817       
1818    def paint(self, painter, option, index):
1819        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
1820        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
1821        rect = option.rect
1822        val, ok = index.data(Qt.DisplayRole).toDouble()
1823        if ok:
1824            min, max = self.scale
1825            val = (val - min) / (max - min)
1826            painter.save()
1827            if option.state & QStyle.State_Selected:
1828                painter.setOpacity(0.75)
1829            painter.setBrush(self.brush)
1830            painter.drawRect(rect.adjusted(1, 1, - rect.width() * (1.0 - val) -2, -2))
1831            painter.restore()
1832       
1833class IndicatorItemDelegate(QStyledItemDelegate):
1834    IndicatorRole = OrangeUserRole.next()
1835    def __init__(self, parent, role=IndicatorRole, indicatorSize=2):
1836        QStyledItemDelegate.__init__(self, parent)
1837        self.role = role
1838        self.indicatorSize = indicatorSize
1839       
1840    def paint(self, painter, option, index):
1841        QStyledItemDelegate.paint(self, painter, option, index)
1842        rect = option.rect
1843        indicator, valid = index.data(self.role).toString(), True
1844        indicator = False if indicator == "false" else indicator
1845        if valid and indicator:
1846            painter.save()
1847            painter.setRenderHints(QPainter.Antialiasing)
1848            painter.setBrush(QBrush(Qt.black))
1849            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))
1850            painter.restore()
1851
1852class LinkStyledItemDelegate(QStyledItemDelegate):
1853    LinkRole = OrangeUserRole.next()
1854    def __init__(self, parent):
1855        QStyledItemDelegate.__init__(self, parent)
1856        self.mousePressState = QModelIndex(), QPoint()
1857        self.connect(parent, SIGNAL("entered(QModelIndex)"), self.onEntered)
1858           
1859    def sizeHint(self, option, index):
1860        size = QStyledItemDelegate.sizeHint(self, option, index)
1861        return QSize(size.width(), max(size.height(), 20))
1862   
1863    def linkRect(self, option, index):
1864        style = self.parent().style()
1865        text = self.displayText(index.data(Qt.DisplayRole), QLocale.system())
1866        self.initStyleOption(option, index)
1867        textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option)
1868        if not textRect.isValid():
1869            textRect = option.rect
1870        margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1
1871        textRect = textRect.adjusted(margin, 0, -margin, 0)
1872        font = index.data(Qt.FontRole)
1873        if font.isValid():
1874            font = QFont(font)
1875        else:
1876            font = option.font
1877        metrics = QFontMetrics(font)
1878        elideText = metrics.elidedText(text, option.textElideMode, textRect.width())
1879        try:
1880            str(elideText)  ## on Windows with PyQt 4.4 sometimes this fails
1881        except Exception, ex:
1882            elideText = text
1883        return metrics.boundingRect(textRect, option.displayAlignment, elideText)
1884     
1885    def editorEvent(self, event, model, option, index):
1886        if event.type()==QEvent.MouseButtonPress and self.linkRect(option, index).contains(event.pos()):
1887            self.mousePressState = QPersistentModelIndex(index), QPoint(event.pos())
1888           
1889        elif event.type()== QEvent.MouseButtonRelease:
1890            link = index.data(LinkRole)
1891            pressedIndex, pressPos = self.mousePressState
1892            if pressedIndex == index and (pressPos - event.pos()).manhattanLength() < 5 and link.isValid():
1893                 import webbrowser
1894                 webbrowser.open(link.toString())
1895            self.mousePressState = QModelIndex(), event.pos()
1896           
1897        elif event.type()==QEvent.MouseMove:
1898            link = index.data(LinkRole)
1899            if link.isValid() and self.linkRect(option, index).contains(event.pos()):
1900                self.parent().viewport().setCursor(Qt.PointingHandCursor)
1901            else:
1902                self.parent().viewport().setCursor(Qt.ArrowCursor)
1903           
1904        return QStyledItemDelegate.editorEvent(self, event, model, option, index)
1905   
1906    def onEntered(self, index):
1907        link = index.data(LinkRole)
1908        if not link.isValid():
1909            self.parent().viewport().setCursor(Qt.ArrowCursor)
1910   
1911    def paint(self, painter, option, index):
1912        if index.data(LinkRole).isValid():
1913            style = qApp.style()
1914            style.drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
1915            style.drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
1916            text = self.displayText(index.data(Qt.DisplayRole), QLocale.system())
1917            textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option)
1918            if not textRect.isValid():
1919                textRect = option.rect
1920            margin = style.pixelMetric(QStyle.PM_FocusFrameHMargin, option) + 1
1921            textRect = textRect.adjusted(margin, 0, -margin, 0)
1922            elideText = QFontMetrics(option.font).elidedText(text, option.textElideMode, textRect.width())
1923            painter.save()
1924            font = index.data(Qt.FontRole)
1925            if font.isValid():
1926                painter.setFont(QFont(font))
1927            else:
1928                painter.setFont(option.font)
1929            painter.setPen(QPen(Qt.blue))
1930            painter.drawText(textRect, option.displayAlignment, elideText)
1931            painter.restore()
1932        else:
1933            QStyledItemDelegate.paint(self, painter, option, index)
1934   
1935LinkRole = LinkStyledItemDelegate.LinkRole
1936
1937def _toPyObject(variant):
1938    val = variant.toPyObject()
1939    if isinstance(val, type(NotImplemented)):
1940        # PyQt 4.4 converts python int, floats ... to C types and
1941        # cannot convert them back again and returns an exception instance.
1942        qtype = variant.type()
1943        if qtype == QVariant.Double:
1944            val, ok = variant.toDouble()
1945        elif qtype == QVariant.Int:
1946            val, ok = variant.toInt()
1947        elif qtype == QVariant.LongLong:
1948            val, ok = variant.toLongLong()
1949        elif qtype == QVariant.String:
1950            val = variant.toString()
1951    return val
1952
1953class ColoredBarItemDelegate(QStyledItemDelegate):
1954    """ Item delegate that can also draws a distribution bar
1955    """
1956    def __init__(self, parent=None, decimals=3, color=Qt.red):
1957        QStyledItemDelegate.__init__(self, parent)
1958        self.decimals = decimals
1959        self.float_fmt = "%%.%if" % decimals
1960        self.color = QColor(color)
1961       
1962    def displayText(self, value, locale):
1963        obj = _toPyObject(value)
1964        if isinstance(obj, float):
1965            return self.float_fmt % obj
1966        elif isinstance(obj, basestring):
1967            return obj
1968        elif obj is None:
1969            return "NA"
1970        else:
1971            return obj.__str__()
1972       
1973    def sizeHint(self, option, index):
1974        font = self.get_font(option, index)
1975        metrics = QFontMetrics(font)
1976        height = metrics.lineSpacing() + 8 # 4 pixel margin
1977        width = metrics.width(self.displayText(index.data(Qt.DisplayRole), QLocale())) + 8
1978        return QSize(width, height)
1979   
1980    def paint(self, painter, option, index):
1981        self.initStyleOption(option, index)
1982        text = self.displayText(index.data(Qt.DisplayRole), QLocale())
1983       
1984        ratio, have_ratio = self.get_bar_ratio(option, index)
1985       
1986        rect = option.rect
1987        if have_ratio:
1988            # The text is raised 3 pixels above the bar.
1989            text_rect = rect.adjusted(4, 1, -4, -4) # TODO: Style dependent margins?
1990        else:
1991            text_rect = rect.adjusted(4, 4, -4, -4)
1992           
1993        painter.save()
1994        font = self.get_font(option, index)
1995        painter.setFont(font)
1996       
1997        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewRow, option, painter)
1998        qApp.style().drawPrimitive(QStyle.PE_PanelItemViewItem, option, painter)
1999       
2000        # TODO: Check ForegroundRole.
2001        if option.state & QStyle.State_Selected:
2002            color = option.palette.highlightedText().color()
2003        else:
2004            color = option.palette.text().color()
2005        painter.setPen(QPen(color))
2006       
2007        align = self.get_text_align(option, index)
2008           
2009        metrics = QFontMetrics(font)
2010        elide_text = metrics.elidedText(text, option.textElideMode, text_rect.width())
2011        painter.drawText(text_rect, align, elide_text)
2012       
2013        painter.setRenderHint(QPainter.Antialiasing, True)
2014        if have_ratio:
2015            brush = self.get_bar_brush(option, index)
2016           
2017            painter.setBrush(brush)
2018            painter.setPen(QPen(brush, 1))
2019            bar_rect = QRect(text_rect)
2020            bar_rect.setTop(bar_rect.bottom() - 1)
2021            bar_rect.setBottom(bar_rect.bottom() + 1)
2022            w = text_rect.width()
2023            bar_rect.setWidth(max(0, min(w * ratio, w)))
2024            painter.drawRoundedRect(bar_rect, 2, 2)
2025        painter.restore()
2026   
2027    def get_font(self, option, index):
2028        font = index.data(Qt.FontRole)
2029        if font.isValid():
2030            font = font.toPyObject()
2031        else:
2032            font = option.font
2033        return font
2034   
2035    def get_text_align(self, option, index):
2036        align = index.data(Qt.TextAlignmentRole)
2037        if align.isValid():
2038            align = align.toInt()
2039        else:
2040            align = Qt.AlignLeft | Qt.AlignVCenter
2041        return align
2042   
2043    def get_bar_ratio(self, option, index):
2044        bar_ratio = index.data(BarRatioRole)
2045        ratio, have_ratio = bar_ratio.toDouble()
2046        return ratio, have_ratio
2047   
2048    def get_bar_brush(self, option, index):
2049        bar_brush = index.data(BarBrushRole)
2050        if bar_brush.isValid():
2051            bar_brush = bar_brush.toPyObject()
2052            if not isinstance(bar_brush, (QColor, QBrush)):
2053                bar_brush = None
2054        else:
2055            bar_brush = None
2056        if bar_brush is None:
2057            bar_brush = self.color
2058        return QBrush(bar_brush)
2059           
2060##############################################################################
2061# progress bar management
2062
2063class ProgressBar:
2064    def __init__(self, widget, iterations):
2065        self.iter = iterations
2066        self.widget = widget
2067        self.count = 0
2068        self.widget.progressBarInit()
2069
2070    def advance(self, count=1):
2071        self.count += count
2072        self.widget.progressBarSet(int(self.count*100/self.iter))
2073
2074    def finish(self):
2075        self.widget.progressBarFinished()
2076       
2077from Orange.utils import progress_bar_milestones as progressBarMilestones
2078
2079##############################################################################
2080
2081def tabWidget(widget):
2082    w = QTabWidget(widget)
2083    if widget.layout() is not None:
2084        widget.layout().addWidget(w)
2085    return w
2086
2087def createTabPage(tabWidget, name, widgetToAdd = None, canScroll = False):
2088    if widgetToAdd == None:
2089        widgetToAdd = widgetBox(tabWidget, addToLayout = 0, margin = 4)
2090    if canScroll:
2091        scrollArea = QScrollArea() 
2092        tabWidget.addTab(scrollArea, name)
2093        scrollArea.setWidget(widgetToAdd)
2094        scrollArea.setWidgetResizable(1) 
2095        scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 
2096        scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
2097    else:
2098        tabWidget.addTab(widgetToAdd, name)
2099    return widgetToAdd
2100
2101def table(widget, rows = 0, columns = 0, selectionMode = -1, addToLayout = 1):
2102    w = QTableWidget(rows, columns, widget)
2103    if widget and addToLayout and widget.layout() is not None:
2104        widget.layout().addWidget(w)
2105    if selectionMode != -1:
2106        w.setSelectionMode(selectionMode)   
2107    w.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
2108    w.horizontalHeader().setMovable(True)
2109    return w
2110
2111class VisibleHeaderSectionContextEventFilter(QObject):
2112    def __init__(self, parent, itemView=None):
2113        QObject.__init__(self, parent)
2114        self.itemView = itemView
2115    def eventFilter(self, view, event):
2116        if type(event) == QContextMenuEvent:
2117            model = view.model()
2118            headers = [(view.isSectionHidden(i), model.headerData(i, view.orientation(), Qt.DisplayRole)) for i in range(view.count())]
2119            menu = QMenu("Visible headers", view)
2120           
2121            for i, (checked, name) in enumerate(headers):
2122                action = QAction(name.toString(), menu)
2123                action.setCheckable(True)
2124                action.setChecked(not checked)
2125                menu.addAction(action)
2126               
2127                def toogleHidden(bool, section=i):
2128                    view.setSectionHidden(section, not bool)
2129                    if bool:
2130                        if self.itemView:
2131                            self.itemView.resizeColumnToContents(section)
2132                        else:
2133                            view.resizeSection(section, max(view.sectionSizeHint(section), 10))
2134                       
2135                self.connect(action, SIGNAL("toggled(bool)"), toogleHidden)
2136#                self.connect(action, SIGNAL("toggled(bool)"), lambda bool, section=i: view.setSectionHidden(section, not bool))
2137            menu.exec_(event.globalPos())
2138            return True
2139       
2140        return False
2141   
2142def checkButtonOffsetHint(button, style=None):
2143    option = QStyleOptionButton()
2144    option.initFrom(button)
2145    if style is None:
2146        style = button.style()
2147    if isinstance(button, QCheckBox):
2148        pm_spacing = QStyle.PM_CheckBoxLabelSpacing
2149        pm_indicator_width = QStyle.PM_IndicatorWidth
2150    else:
2151        pm_spacing = QStyle.PM_RadioButtonLabelSpacing
2152        pm_indicator_width = QStyle.PM_ExclusiveIndicatorWidth
2153    space = style.pixelMetric(pm_spacing, option, button)
2154    width = style.pixelMetric(pm_indicator_width, option, button)
2155    style_correction = {"macintosh (aqua)": -2, "macintosh(aqua)": -2, "plastique": 1, "cde": 1, "motif": 1} #TODO: add other styles (Maybe load corrections from .cfg file?)
2156    return space + width + style_correction.get(str(qApp.style().objectName()).lower(), 0)
2157   
2158   
2159def toolButtonSizeHint(button=None, style=None):
2160    if button is None and style is None:
2161        style = qApp.style()
2162    elif style is None:
2163        style = button.style()
2164   
2165    button_size = style.pixelMetric(QStyle.PM_SmallIconSize) + \
2166                  style.pixelMetric(QStyle.PM_ButtonMargin)
2167    return button_size
2168   
2169class FloatSlider(QSlider):
2170    def __init__(self, orientation, min_value, max_value, step, parent=None):
2171        QSlider.__init__(self, orientation, parent)
2172        self.setScale(min_value, max_value, step)
2173        QObject.connect(self, SIGNAL("valueChanged(int)"), self.sendValue)
2174       
2175    def update(self):
2176        self.setSingleStep(1)
2177        if self.min_value != self.max_value:
2178            self.setEnabled(True)
2179            self.setMinimum(int(self.min_value/self.step))
2180            self.setMaximum(int(self.max_value/self.step))
2181        else:
2182            self.setEnabled(False)
2183   
2184    def sendValue(self, slider_value):
2185        value = min(max(slider_value * self.step, self.min_value), self.max_value)
2186        self.emit(SIGNAL("valueChangedFloat(double)"), value)
2187       
2188    def setValue(self, value):
2189        QSlider.setValue(self, int(value/self.step))
2190       
2191    def setScale(self, minValue, maxValue, step=0):
2192        if minValue >= maxValue:
2193            ## It would be more logical to disable the slider in this case (self.setEnabled(False))
2194            ## However, we do nothing to keep consistency with Qwt
2195            return
2196        if step <= 0 or step > (maxValue-minValue):
2197            if type(maxValue) == int and type(minValue) == int:
2198                step = 1
2199            else:
2200                step = float(minValue-maxValue)/100.0
2201        self.min_value = float(minValue)
2202        self.max_value = float(maxValue)
2203        self.step = step
2204        self.update()
2205       
2206    def setRange(self, minValue, maxValue, step=1.0):
2207        # For compatibility with qwtSlider
2208        self.setScale(minValue, maxValue, step)
Note: See TracBrowser for help on using the repository browser.