source: orange/Orange/OrangeWidgets/OWGUI.py @ 11762:c8297aec9f31

Revision 11762:c8297aec9f31, 82.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 5 months ago (diff)

Return the QSpinBox from the 'spin' function when no container widget is created.

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