source: orange/Orange/OrangeWidgets/OWGUI.py @ 11782:d6d8961f5872

Revision 11782:d6d8961f5872, 82.4 KB checked in by Ales Erjavec <ales.erjavec@…>, 4 months ago (diff)

Fixed attribute icon painting.

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