source: orange/Orange/OrangeWidgets/OWGUI.py @ 10608:8eb49e810732

Revision 10608:8eb49e810732, 83.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Move the text slightly up when drawing a distribution bar (fixes #989).

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