source: orange/Orange/OrangeWidgets/OWGUI.py @ 10728:4e80c9933ef4

Revision 10728:4e80c9933ef4, 82.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 2 years ago (diff)

Added decimals keyword argument to doubleSpin.

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