source: orange/orange/OrangeWidgets/OWGUI.py @ 9325:3bb514c1e6e8

Revision 9325:3bb514c1e6e8, 82.8 KB checked in by ales_erjavec <ales.erjavec@…>, 2 years ago (diff)

Fixed unequal sizes for add/remove label buttons.
Added toolButtonSizeHint function to OWGUI.

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